UNPKG

fido2-lib

Version:

A library for performing FIDO 2.0 / WebAuthn functionality

109 lines (78 loc) 3.41 kB
import { ab2str, appendBuffer, coerceToBase64, tools } from "../utils.js"; import { Certificate } from "../certUtils.js"; function androidSafetyNetParseFn(attStmt) { const ret = new Map(); // console.log("android-safetynet", attStmt); ret.set("ver", attStmt.ver); const response = ab2str(attStmt.response); ret.set("response", response); // console.log("returning", ret); return ret; } // Validation: // https://www.w3.org/TR/webauthn/#android-safetynet-attestation (verification procedure) async function androidSafetyNetValidateFn() { const response = this.authnrData.get("response"); // parse JWS const protectedHeader = await tools.decodeProtectedHeader(response); const publicKey = await tools.getEmbeddedJwk(protectedHeader); const parsedJws = await tools.jwtVerify( response, await tools.importJWK(publicKey), ); // Append now verified header to jws parsedJws.header = protectedHeader; this.authnrData.set("payload", parsedJws.payload); // Required: verify that ctsProfileMatch attribute in the parsedJws.payload is true if (!parsedJws.payload.ctsProfileMatch){ throw new Error("android-safetynet attestation: ctsProfileMatch: the device is not compatible"); } // Required: verify nonce // response.nonce === base64( sha256( authenticatorData concatenated with clientDataHash )) const rawClientData = this.clientData.get("rawClientDataJson"); const rawAuthnrData = this.authnrData.get("rawAuthnrData"); // create clientData SHA-256 hash const clientDataHash = await tools.hashDigest(rawClientData); // concatenate buffers const rawAuthnrDataBuf = new Uint8Array(rawAuthnrData); const clientDataHashBuf = new Uint8Array(clientDataHash); const concatenated = appendBuffer(rawAuthnrDataBuf, clientDataHashBuf); // create hash of the concatenation const hash = await tools.hashDigest(concatenated); const nonce = tools.base64.fromArrayBuffer(hash); // check result if(nonce!==parsedJws.payload.nonce){ throw new Error("android-safetynet attestation: nonce check hash failed"); } // check for any safetynet errors if(parsedJws.payload.error){ throw new Error("android-safetynet: " + parsedJws.payload.error + "advice: " + parsedJws.payload.advice); } this.audit.journal.add("payload"); this.audit.journal.add("ver"); this.audit.journal.add("response"); // get certs this.authnrData.set("attCert", parsedJws.header.x5c.shift()); this.authnrData.set("x5c", parsedJws.header.x5c); this.audit.journal.add("attCert"); this.audit.journal.add("x5c"); // TODO: verify attCert is issued to the hostname "attest.android.com" const attCert = new Certificate(coerceToBase64(parsedJws.header.x5c.shift(), "parsedAttCert")); this.audit.info.set("organization-name", attCert.getSubject().get("organization-name")); // attCert.getExtensions() // TODO: verify cert chain // var rootCerts; // if (Array.isArray(rootCert)) rootCerts = rootCert; // else rootCerts = [rootCert]; // var ret = await CertManager.verifyCertChain(parsedJws.header.x5c, rootCerts, crls); // If successful, return attestation type Basic and attestation trust path attCert. this.audit.info.set("attestation-type", "basic"); this.audit.journal.add("fmt"); return true; } const androidSafetyNetAttestation = { name: "android-safetynet", parseFn: androidSafetyNetParseFn, validateFn: androidSafetyNetValidateFn, }; export { androidSafetyNetAttestation };