webauthn.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. /**
  2. * @description MeshCentral WebAuthn module
  3. * @version v0.0.1
  4. */
  5. // This code is based on a portion of the webauthn module at: https://www.npmjs.com/package/webauthn
  6. "use strict"
  7. const crypto = require('crypto');
  8. const cbor = require('cbor');
  9. //const iso_3166_1 = require('iso-3166-1')
  10. //const Certificate = null; //require('@fidm/x509')
  11. module.exports.CreateWebAuthnModule = function () {
  12. var obj = {};
  13. obj.generateRegistrationChallenge = function (rpName, user) {
  14. return {
  15. rp: { name: rpName },
  16. user: user,
  17. challenge: crypto.randomBytes(64).toString('base64'),
  18. pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
  19. timeout: 60000,
  20. attestation: 'none'
  21. };
  22. }
  23. obj.verifyAuthenticatorAttestationResponse = function (webauthnResponse) {
  24. const attestationBuffer = Buffer.from(webauthnResponse.attestationObject, 'base64');
  25. const ctapMakeCredResp = cbor.decodeAllSync(attestationBuffer)[0];
  26. const authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);
  27. //console.log('***CTAP_RESPONSE', ctapMakeCredResp)
  28. //console.log('***AUTHR_DATA_STRUCT', authrDataStruct)
  29. const response = { 'verified': false };
  30. if ((ctapMakeCredResp.fmt === 'none') || (ctapMakeCredResp.fmt === 'fido-u2f') || (ctapMakeCredResp.fmt === 'packed')) {
  31. if (!(authrDataStruct.flags & 0x01)) { throw new Error('User was NOT presented during authentication!'); } // U2F_USER_PRESENTED
  32. const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey);
  33. response.verified = true;
  34. if (response.verified) {
  35. response.authrInfo = {
  36. fmt: 'none',
  37. publicKey: ASN1toPEM(publicKey),
  38. counter: authrDataStruct.counter,
  39. keyId: authrDataStruct.credID.toString('base64')
  40. };
  41. }
  42. }
  43. /*
  44. else if (ctapMakeCredResp.fmt === 'fido-u2f') {
  45. if (!(authrDataStruct.flags & 0x01)) // U2F_USER_PRESENTED
  46. throw new Error('User was NOT presented during authentication!');
  47. const clientDataHash = hash(webauthnResponse.clientDataJSON)
  48. const reservedByte = Buffer.from([0x00]);
  49. const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
  50. const signatureBase = Buffer.concat([reservedByte, authrDataStruct.rpIdHash, clientDataHash, authrDataStruct.credID, publicKey]);
  51. const PEMCertificate = ASN1toPEM(ctapMakeCredResp.attStmt.x5c[0]);
  52. const signature = ctapMakeCredResp.attStmt.sig;
  53. response.verified = verifySignature(signature, signatureBase, PEMCertificate)
  54. if (response.verified) {
  55. response.authrInfo = {
  56. fmt: 'fido-u2f',
  57. publicKey: ASN1toPEM(publicKey),
  58. counter: authrDataStruct.counter,
  59. keyId: authrDataStruct.credID.toString('base64')
  60. }
  61. }
  62. } else if (ctapMakeCredResp.fmt === 'packed' && ctapMakeCredResp.attStmt.hasOwnProperty('x5c')) {
  63. if (!(authrDataStruct.flags & 0x01)) // U2F_USER_PRESENTED
  64. throw new Error('User was NOT presented durring authentication!');
  65. const clientDataHash = hash(webauthnResponse.clientDataJSON)
  66. const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
  67. const signatureBase = Buffer.concat([ctapMakeCredResp.authData, clientDataHash]);
  68. const PEMCertificate = ASN1toPEM(ctapMakeCredResp.attStmt.x5c[0]);
  69. const signature = ctapMakeCredResp.attStmt.sig;
  70. const pem = Certificate.fromPEM(PEMCertificate);
  71. // Getting requirements from https://www.w3.org/TR/webauthn/#packed-attestation
  72. const aaguid_ext = pem.getExtension('1.3.6.1.4.1.45724.1.1.4')
  73. response.verified = // Verify that sig is a valid signature over the concatenation of authenticatorData
  74. // and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
  75. verifySignature(signature, signatureBase, PEMCertificate) &&
  76. // version must be 3 (which is indicated by an ASN.1 INTEGER with value 2)
  77. pem.version == 3 &&
  78. // ISO 3166 valid country
  79. typeof iso_3166_1.whereAlpha2(pem.subject.countryName) !== 'undefined' &&
  80. // Legal name of the Authenticator vendor (UTF8String)
  81. pem.subject.organizationName &&
  82. // Literal string “Authenticator Attestation” (UTF8String)
  83. pem.subject.organizationalUnitName === 'Authenticator Attestation' &&
  84. // A UTF8String of the vendor’s choosing
  85. pem.subject.commonName &&
  86. // The Basic Constraints extension MUST have the CA component set to false
  87. !pem.extensions.isCA &&
  88. // If attestnCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid)
  89. // verify that the value of this extension matches the aaguid in authenticatorData.
  90. // The extension MUST NOT be marked as critical.
  91. (aaguid_ext != null ?
  92. (authrDataStruct.hasOwnProperty('aaguid') ?
  93. !aaguid_ext.critical && aaguid_ext.value.slice(2).equals(authrDataStruct.aaguid) : false)
  94. : true);
  95. if (response.verified) {
  96. response.authrInfo = {
  97. fmt: 'fido-u2f',
  98. publicKey: publicKey,
  99. counter: authrDataStruct.counter,
  100. keyId: authrDataStruct.credID.toString('base64')
  101. }
  102. }
  103. // Self signed
  104. } else if (ctapMakeCredResp.fmt === 'packed') {
  105. if (!(authrDataStruct.flags & 0x01)) // U2F_USER_PRESENTED
  106. throw new Error('User was NOT presented durring authentication!');
  107. const clientDataHash = hash(webauthnResponse.clientDataJSON)
  108. const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
  109. const signatureBase = Buffer.concat([ctapMakeCredResp.authData, clientDataHash]);
  110. const PEMCertificate = ASN1toPEM(publicKey);
  111. const { attStmt: { sig: signature, alg } } = ctapMakeCredResp
  112. response.verified = // Verify that sig is a valid signature over the concatenation of authenticatorData
  113. // and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
  114. verifySignature(signature, signatureBase, PEMCertificate) && alg === -7
  115. if (response.verified) {
  116. response.authrInfo = {
  117. fmt: 'fido-u2f',
  118. publicKey: ASN1toPEM(publicKey),
  119. counter: authrDataStruct.counter,
  120. keyId: authrDataStruct.credID.toString('base64')
  121. }
  122. }
  123. } else if (ctapMakeCredResp.fmt === 'android-safetynet') {
  124. console.log("Android safetynet request\n")
  125. console.log(ctapMakeCredResp)
  126. const authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);
  127. console.log('AUTH_DATA', authrDataStruct)
  128. //console.log('CLIENT_DATA_JSON ', webauthnResponse.clientDataJSON)
  129. const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
  130. let [header, payload, signature] = ctapMakeCredResp.attStmt.response.toString('utf8').split('.')
  131. const signatureBase = Buffer.from([header, payload].join('.'))
  132. header = JSON.parse(header)
  133. payload = JSON.parse(payload)
  134. console.log('JWS HEADER', header)
  135. console.log('JWS PAYLOAD', payload)
  136. console.log('JWS SIGNATURE', signature)
  137. const PEMCertificate = ASN1toPEM(Buffer.from(header.x5c[0], 'base64'))
  138. const pem = Certificate.fromPEM(PEMCertificate)
  139. console.log('PEM', pem)
  140. response.verified = // Verify that sig is a valid signature over the concatenation of authenticatorData
  141. // and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
  142. verifySignature(signature, signatureBase, PEMCertificate) &&
  143. // version must be 3 (which is indicated by an ASN.1 INTEGER with value 2)
  144. pem.version == 3 &&
  145. pem.subject.commonName === 'attest.android.com'
  146. if (response.verified) {
  147. response.authrInfo = {
  148. fmt: 'fido-u2f',
  149. publicKey: ASN1toPEM(publicKey),
  150. counter: authrDataStruct.counter,
  151. keyId: authrDataStruct.credID.toString('base64')
  152. }
  153. }
  154. console.log('RESPONSE', response)
  155. } */
  156. else {
  157. throw new Error(`Unsupported attestation format: ${ctapMakeCredResp.fmt}`);
  158. }
  159. return response;
  160. }
  161. obj.verifyAuthenticatorAssertionResponse = function (webauthnResponse, authr) {
  162. const response = { 'verified': false }
  163. if (['fido-u2f'].includes(authr.fmt)) {
  164. const authrDataStruct = parseGetAssertAuthData(webauthnResponse.authenticatorData);
  165. if (!(authrDataStruct.flags & 0x01)) { throw new Error('User was not presented durring authentication!'); } // U2F_USER_PRESENTED
  166. response.counter = authrDataStruct.counter;
  167. response.verified = verifySignature(webauthnResponse.signature, Buffer.concat([authrDataStruct.rpIdHash, authrDataStruct.flagsBuf, authrDataStruct.counterBuf, hash(webauthnResponse.clientDataJSON)]), authr.publicKey);
  168. }
  169. return response;
  170. }
  171. function hash(data) { return crypto.createHash('sha256').update(data).digest() }
  172. function verifySignature(signature, data, publicKey) { return crypto.createVerify('SHA256').update(data).verify(publicKey, signature); }
  173. function parseGetAssertAuthData(buffer) {
  174. const rpIdHash = buffer.slice(0, 32);
  175. buffer = buffer.slice(32);
  176. const flagsBuf = buffer.slice(0, 1);
  177. buffer = buffer.slice(1);
  178. const flags = flagsBuf[0];
  179. const counterBuf = buffer.slice(0, 4);
  180. buffer = buffer.slice(4);
  181. const counter = counterBuf.readUInt32BE(0);
  182. return { rpIdHash, flagsBuf, flags, counter, counterBuf };
  183. }
  184. function parseMakeCredAuthData(buffer) {
  185. const rpIdHash = buffer.slice(0, 32);
  186. buffer = buffer.slice(32);
  187. const flagsBuf = buffer.slice(0, 1);
  188. buffer = buffer.slice(1);
  189. const flags = flagsBuf[0];
  190. const counterBuf = buffer.slice(0, 4);
  191. buffer = buffer.slice(4);
  192. const counter = counterBuf.readUInt32BE(0);
  193. const aaguid = buffer.slice(0, 16);
  194. buffer = buffer.slice(16);
  195. const credIDLenBuf = buffer.slice(0, 2);
  196. buffer = buffer.slice(2);
  197. const credIDLen = credIDLenBuf.readUInt16BE(0);
  198. const credID = buffer.slice(0, credIDLen);
  199. buffer = buffer.slice(credIDLen);
  200. const COSEPublicKey = buffer;
  201. return { rpIdHash, flagsBuf, flags, counter, counterBuf, aaguid, credID, COSEPublicKey };
  202. }
  203. function COSEECDHAtoPKCS(COSEPublicKey) {
  204. const coseStruct = cbor.decodeAllSync(COSEPublicKey)[0];
  205. return Buffer.concat([Buffer.from([0x04]), coseStruct.get(-2), coseStruct.get(-3)]);
  206. }
  207. function ASN1toPEM(pkBuffer) {
  208. if (!Buffer.isBuffer(pkBuffer)) { throw new Error("ASN1toPEM: pkBuffer must be Buffer."); }
  209. let type;
  210. if (pkBuffer.length == 65 && pkBuffer[0] == 0x04) { pkBuffer = Buffer.concat([Buffer.from("3059301306072a8648ce3d020106082a8648ce3d030107034200", "hex"), pkBuffer]); type = 'PUBLIC KEY'; } else { type = 'CERTIFICATE'; }
  211. const b64cert = pkBuffer.toString('base64');
  212. let PEMKey = '';
  213. for (let i = 0; i < Math.ceil(b64cert.length / 64); i++) { const start = 64 * i; PEMKey += b64cert.substr(start, 64) + '\n'; }
  214. PEMKey = `-----BEGIN ${type}-----\n` + PEMKey + `-----END ${type}-----\n`;
  215. return PEMKey;
  216. }
  217. return obj;
  218. }