2* @description MeshCentral WebAuthn module
6// This code is based on a portion of the webauthn module at: https://www.npmjs.com/package/webauthn
10const crypto = require('crypto');
11const cbor = require('cbor');
12//const iso_3166_1 = require('iso-3166-1')
13//const Certificate = null; //require('@fidm/x509')
15module.exports.CreateWebAuthnModule = function () {
18 obj.generateRegistrationChallenge = function (rpName, user) {
22 challenge: crypto.randomBytes(64).toString('base64'),
23 pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
29 obj.verifyAuthenticatorAttestationResponse = function (webauthnResponse) {
30 const attestationBuffer = Buffer.from(webauthnResponse.attestationObject, 'base64');
31 const ctapMakeCredResp = cbor.decodeAllSync(attestationBuffer)[0];
32 const authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);
33 //console.log('***CTAP_RESPONSE', ctapMakeCredResp)
34 //console.log('***AUTHR_DATA_STRUCT', authrDataStruct)
36 const response = { 'verified': false };
38 if ((ctapMakeCredResp.fmt === 'none') || (ctapMakeCredResp.fmt === 'fido-u2f') || (ctapMakeCredResp.fmt === 'packed')) {
39 if (!(authrDataStruct.flags & 0x01)) { throw new Error('User was NOT presented during authentication!'); } // U2F_USER_PRESENTED
41 const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey);
42 response.verified = true;
44 if (response.verified) {
45 response.authrInfo = {
47 publicKey: ASN1toPEM(publicKey),
48 counter: authrDataStruct.counter,
49 keyId: authrDataStruct.credID.toString('base64')
54 else if (ctapMakeCredResp.fmt === 'fido-u2f') {
55 if (!(authrDataStruct.flags & 0x01)) // U2F_USER_PRESENTED
56 throw new Error('User was NOT presented during authentication!');
58 const clientDataHash = hash(webauthnResponse.clientDataJSON)
59 const reservedByte = Buffer.from([0x00]);
60 const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
61 const signatureBase = Buffer.concat([reservedByte, authrDataStruct.rpIdHash, clientDataHash, authrDataStruct.credID, publicKey]);
63 const PEMCertificate = ASN1toPEM(ctapMakeCredResp.attStmt.x5c[0]);
64 const signature = ctapMakeCredResp.attStmt.sig;
66 response.verified = verifySignature(signature, signatureBase, PEMCertificate)
68 if (response.verified) {
69 response.authrInfo = {
71 publicKey: ASN1toPEM(publicKey),
72 counter: authrDataStruct.counter,
73 keyId: authrDataStruct.credID.toString('base64')
76 } else if (ctapMakeCredResp.fmt === 'packed' && ctapMakeCredResp.attStmt.hasOwnProperty('x5c')) {
77 if (!(authrDataStruct.flags & 0x01)) // U2F_USER_PRESENTED
78 throw new Error('User was NOT presented durring authentication!');
80 const clientDataHash = hash(webauthnResponse.clientDataJSON)
81 const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
82 const signatureBase = Buffer.concat([ctapMakeCredResp.authData, clientDataHash]);
84 const PEMCertificate = ASN1toPEM(ctapMakeCredResp.attStmt.x5c[0]);
85 const signature = ctapMakeCredResp.attStmt.sig;
87 const pem = Certificate.fromPEM(PEMCertificate);
89 // Getting requirements from https://www.w3.org/TR/webauthn/#packed-attestation
90 const aaguid_ext = pem.getExtension('1.3.6.1.4.1.45724.1.1.4')
92 response.verified = // Verify that sig is a valid signature over the concatenation of authenticatorData
93 // and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
94 verifySignature(signature, signatureBase, PEMCertificate) &&
95 // version must be 3 (which is indicated by an ASN.1 INTEGER with value 2)
97 // ISO 3166 valid country
98 typeof iso_3166_1.whereAlpha2(pem.subject.countryName) !== 'undefined' &&
99 // Legal name of the Authenticator vendor (UTF8String)
100 pem.subject.organizationName &&
101 // Literal string “Authenticator Attestation” (UTF8String)
102 pem.subject.organizationalUnitName === 'Authenticator Attestation' &&
103 // A UTF8String of the vendor’s choosing
104 pem.subject.commonName &&
105 // The Basic Constraints extension MUST have the CA component set to false
106 !pem.extensions.isCA &&
107 // If attestnCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid)
108 // verify that the value of this extension matches the aaguid in authenticatorData.
109 // The extension MUST NOT be marked as critical.
110 (aaguid_ext != null ?
111 (authrDataStruct.hasOwnProperty('aaguid') ?
112 !aaguid_ext.critical && aaguid_ext.value.slice(2).equals(authrDataStruct.aaguid) : false)
115 if (response.verified) {
116 response.authrInfo = {
118 publicKey: publicKey,
119 counter: authrDataStruct.counter,
120 keyId: authrDataStruct.credID.toString('base64')
125 } else if (ctapMakeCredResp.fmt === 'packed') {
126 if (!(authrDataStruct.flags & 0x01)) // U2F_USER_PRESENTED
127 throw new Error('User was NOT presented durring authentication!');
129 const clientDataHash = hash(webauthnResponse.clientDataJSON)
130 const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
131 const signatureBase = Buffer.concat([ctapMakeCredResp.authData, clientDataHash]);
132 const PEMCertificate = ASN1toPEM(publicKey);
134 const { attStmt: { sig: signature, alg } } = ctapMakeCredResp
136 response.verified = // Verify that sig is a valid signature over the concatenation of authenticatorData
137 // and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
138 verifySignature(signature, signatureBase, PEMCertificate) && alg === -7
140 if (response.verified) {
141 response.authrInfo = {
143 publicKey: ASN1toPEM(publicKey),
144 counter: authrDataStruct.counter,
145 keyId: authrDataStruct.credID.toString('base64')
149 } else if (ctapMakeCredResp.fmt === 'android-safetynet') {
150 console.log("Android safetynet request\n")
151 console.log(ctapMakeCredResp)
153 const authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);
154 console.log('AUTH_DATA', authrDataStruct)
155 //console.log('CLIENT_DATA_JSON ', webauthnResponse.clientDataJSON)
157 const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
159 let [header, payload, signature] = ctapMakeCredResp.attStmt.response.toString('utf8').split('.')
160 const signatureBase = Buffer.from([header, payload].join('.'))
162 header = JSON.parse(header)
163 payload = JSON.parse(payload)
165 console.log('JWS HEADER', header)
166 console.log('JWS PAYLOAD', payload)
167 console.log('JWS SIGNATURE', signature)
169 const PEMCertificate = ASN1toPEM(Buffer.from(header.x5c[0], 'base64'))
171 const pem = Certificate.fromPEM(PEMCertificate)
173 console.log('PEM', pem)
175 response.verified = // Verify that sig is a valid signature over the concatenation of authenticatorData
176 // and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
177 verifySignature(signature, signatureBase, PEMCertificate) &&
178 // version must be 3 (which is indicated by an ASN.1 INTEGER with value 2)
180 pem.subject.commonName === 'attest.android.com'
182 if (response.verified) {
183 response.authrInfo = {
185 publicKey: ASN1toPEM(publicKey),
186 counter: authrDataStruct.counter,
187 keyId: authrDataStruct.credID.toString('base64')
191 console.log('RESPONSE', response)
194 throw new Error(`Unsupported attestation format: ${ctapMakeCredResp.fmt}`);
200 obj.verifyAuthenticatorAssertionResponse = function (webauthnResponse, authr) {
201 const response = { 'verified': false }
202 if (['fido-u2f'].includes(authr.fmt)) {
203 const authrDataStruct = parseGetAssertAuthData(webauthnResponse.authenticatorData);
204 if (!(authrDataStruct.flags & 0x01)) { throw new Error('User was not presented durring authentication!'); } // U2F_USER_PRESENTED
205 response.counter = authrDataStruct.counter;
206 response.verified = verifySignature(webauthnResponse.signature, Buffer.concat([authrDataStruct.rpIdHash, authrDataStruct.flagsBuf, authrDataStruct.counterBuf, hash(webauthnResponse.clientDataJSON)]), authr.publicKey);
211 function hash(data) { return crypto.createHash('sha256').update(data).digest() }
212 function verifySignature(signature, data, publicKey) { return crypto.createVerify('SHA256').update(data).verify(publicKey, signature); }
214 function parseGetAssertAuthData(buffer) {
215 const rpIdHash = buffer.slice(0, 32);
216 buffer = buffer.slice(32);
217 const flagsBuf = buffer.slice(0, 1);
218 buffer = buffer.slice(1);
219 const flags = flagsBuf[0];
220 const counterBuf = buffer.slice(0, 4);
221 buffer = buffer.slice(4);
222 const counter = counterBuf.readUInt32BE(0);
223 return { rpIdHash, flagsBuf, flags, counter, counterBuf };
226 function parseMakeCredAuthData(buffer) {
227 const rpIdHash = buffer.slice(0, 32);
228 buffer = buffer.slice(32);
229 const flagsBuf = buffer.slice(0, 1);
230 buffer = buffer.slice(1);
231 const flags = flagsBuf[0];
232 const counterBuf = buffer.slice(0, 4);
233 buffer = buffer.slice(4);
234 const counter = counterBuf.readUInt32BE(0);
235 const aaguid = buffer.slice(0, 16);
236 buffer = buffer.slice(16);
237 const credIDLenBuf = buffer.slice(0, 2);
238 buffer = buffer.slice(2);
239 const credIDLen = credIDLenBuf.readUInt16BE(0);
240 const credID = buffer.slice(0, credIDLen);
241 buffer = buffer.slice(credIDLen);
242 const COSEPublicKey = buffer;
243 return { rpIdHash, flagsBuf, flags, counter, counterBuf, aaguid, credID, COSEPublicKey };
246 function COSEECDHAtoPKCS(COSEPublicKey) {
247 const coseStruct = cbor.decodeAllSync(COSEPublicKey)[0];
248 return Buffer.concat([Buffer.from([0x04]), coseStruct.get(-2), coseStruct.get(-3)]);
251 function ASN1toPEM(pkBuffer) {
252 if (!Buffer.isBuffer(pkBuffer)) { throw new Error("ASN1toPEM: pkBuffer must be Buffer."); }
254 if (pkBuffer.length == 65 && pkBuffer[0] == 0x04) { pkBuffer = Buffer.concat([Buffer.from("3059301306072a8648ce3d020106082a8648ce3d030107034200", "hex"), pkBuffer]); type = 'PUBLIC KEY'; } else { type = 'CERTIFICATE'; }
255 const b64cert = pkBuffer.toString('base64');
257 for (let i = 0; i < Math.ceil(b64cert.length / 64); i++) { const start = 64 * i; PEMKey += b64cert.substr(start, 64) + '\n'; }
258 PEMKey = `-----BEGIN ${type}-----\n` + PEMKey + `-----END ${type}-----\n`;