Fixed 267U.pre2
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
import * as crypto from "node:crypto";
|
||||
import * as jsrsasign from "jsrsasign";
|
||||
import config from "../../config/index.js";
|
||||
const ECC_PRELUDE = Buffer.from([
|
||||
0x04
|
||||
]);
|
||||
const NULL_BYTE = Buffer.from([
|
||||
0
|
||||
]);
|
||||
const PEM_PRELUDE = Buffer.from("3059301306072a8648ce3d020106082a8648ce3d030107034200", "hex");
|
||||
// Android Safetynet attestations are signed with this cert:
|
||||
const GSR2 = `-----BEGIN CERTIFICATE-----
|
||||
MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
|
||||
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
|
||||
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
|
||||
MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
|
||||
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
|
||||
v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
|
||||
eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
|
||||
tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
|
||||
C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
|
||||
zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
|
||||
mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
|
||||
V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
|
||||
bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
|
||||
3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
|
||||
J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
|
||||
291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
|
||||
ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
|
||||
AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
|
||||
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
|
||||
-----END CERTIFICATE-----\n`;
|
||||
function base64URLDecode(source) {
|
||||
return Buffer.from(source.replace(/\-/g, "+").replace(/_/g, "/"), "base64");
|
||||
}
|
||||
function getCertSubject(certificate) {
|
||||
const subjectCert = new jsrsasign.X509();
|
||||
subjectCert.readCertPEM(certificate);
|
||||
const subjectString = subjectCert.getSubjectString();
|
||||
const subjectFields = subjectString.slice(1).split("/");
|
||||
const fields = {};
|
||||
for (const field of subjectFields){
|
||||
const eqIndex = field.indexOf("=");
|
||||
fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
function verifyCertificateChain(certificates) {
|
||||
let valid = true;
|
||||
for(let i = 0; i < certificates.length; i++){
|
||||
const Cert = certificates[i];
|
||||
const certificate = new jsrsasign.X509();
|
||||
certificate.readCertPEM(Cert);
|
||||
const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1];
|
||||
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex, 0, [
|
||||
0
|
||||
]);
|
||||
const algorithm = certificate.getSignatureAlgorithmField();
|
||||
const signatureHex = certificate.getSignatureValueHex();
|
||||
// Verify against CA
|
||||
const Signature = new jsrsasign.KJUR.crypto.Signature({
|
||||
alg: algorithm
|
||||
});
|
||||
Signature.init(CACert);
|
||||
Signature.updateHex(certStruct);
|
||||
valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
function PEMString(pemBuffer, type = "CERTIFICATE") {
|
||||
if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) {
|
||||
pemBuffer = Buffer.concat([
|
||||
PEM_PRELUDE,
|
||||
pemBuffer
|
||||
], 91);
|
||||
type = "PUBLIC KEY";
|
||||
}
|
||||
const cert = pemBuffer.toString("base64");
|
||||
const keyParts = [];
|
||||
const max = Math.ceil(cert.length / 64);
|
||||
let start = 0;
|
||||
for(let i = 0; i < max; i++){
|
||||
keyParts.push(cert.substring(start, start + 64));
|
||||
start += 64;
|
||||
}
|
||||
return `-----BEGIN ${type}-----\n${keyParts.join("\n")}\n-----END ${type}-----\n`;
|
||||
}
|
||||
export function hash(data) {
|
||||
return crypto.createHash("sha256").update(data).digest();
|
||||
}
|
||||
export function verifyLogin({ publicKey, authenticatorData, clientDataJSON, clientData, signature, challenge }) {
|
||||
if (clientData.type !== "webauthn.get") {
|
||||
throw new Error("type is not webauthn.get");
|
||||
}
|
||||
if (hash(clientData.challenge).toString("hex") !== challenge) {
|
||||
throw new Error("challenge mismatch");
|
||||
}
|
||||
if (clientData.origin !== `${config.scheme}://${config.host}`) {
|
||||
throw new Error("origin mismatch");
|
||||
}
|
||||
const verificationData = Buffer.concat([
|
||||
authenticatorData,
|
||||
hash(clientDataJSON)
|
||||
], 32 + authenticatorData.length);
|
||||
return crypto.createVerify("SHA256").update(verificationData).verify(PEMString(publicKey), signature);
|
||||
}
|
||||
export const procedures = {
|
||||
none: {
|
||||
verify ({ publicKey }) {
|
||||
const negTwo = publicKey.get(-2);
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error("invalid or no -2 key given");
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error("invalid or no -3 key given");
|
||||
}
|
||||
const publicKeyU2F = Buffer.concat([
|
||||
ECC_PRELUDE,
|
||||
negTwo,
|
||||
negThree
|
||||
], 1 + 32 + 32);
|
||||
return {
|
||||
publicKey: publicKeyU2F,
|
||||
valid: true
|
||||
};
|
||||
}
|
||||
},
|
||||
"android-key": {
|
||||
verify ({ attStmt, authenticatorData, clientDataHash, publicKey, rpIdHash, credentialId }) {
|
||||
if (attStmt.alg !== -7) {
|
||||
throw new Error("alg mismatch");
|
||||
}
|
||||
const verificationData = Buffer.concat([
|
||||
authenticatorData,
|
||||
clientDataHash
|
||||
]);
|
||||
const attCert = attStmt.x5c[0];
|
||||
const negTwo = publicKey.get(-2);
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error("invalid or no -2 key given");
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error("invalid or no -3 key given");
|
||||
}
|
||||
const publicKeyData = Buffer.concat([
|
||||
ECC_PRELUDE,
|
||||
negTwo,
|
||||
negThree
|
||||
], 1 + 32 + 32);
|
||||
if (!attCert.equals(publicKeyData)) {
|
||||
throw new Error("public key mismatch");
|
||||
}
|
||||
const isValid = crypto.createVerify("SHA256").update(verificationData).verify(PEMString(attCert), attStmt.sig);
|
||||
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
|
||||
return {
|
||||
valid: isValid,
|
||||
publicKey: publicKeyData
|
||||
};
|
||||
}
|
||||
},
|
||||
// what a stupid attestation
|
||||
"android-safetynet": {
|
||||
verify ({ attStmt, authenticatorData, clientDataHash, publicKey, rpIdHash, credentialId }) {
|
||||
const verificationData = hash(Buffer.concat([
|
||||
authenticatorData,
|
||||
clientDataHash
|
||||
]));
|
||||
const jwsParts = attStmt.response.toString("utf-8").split(".");
|
||||
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString("utf-8"));
|
||||
const response = JSON.parse(base64URLDecode(jwsParts[1]).toString("utf-8"));
|
||||
const signature = jwsParts[2];
|
||||
if (!verificationData.equals(Buffer.from(response.nonce, "base64"))) {
|
||||
throw new Error("invalid nonce");
|
||||
}
|
||||
const certificateChain = header.x5c.map((key)=>PEMString(key)).concat([
|
||||
GSR2
|
||||
]);
|
||||
if (getCertSubject(certificateChain[0]).CN !== "attest.android.com") {
|
||||
throw new Error("invalid common name");
|
||||
}
|
||||
if (!verifyCertificateChain(certificateChain)) {
|
||||
throw new Error("Invalid certificate chain!");
|
||||
}
|
||||
const signatureBase = Buffer.from(`${jwsParts[0]}.${jwsParts[1]}`, "utf-8");
|
||||
const valid = crypto.createVerify("sha256").update(signatureBase).verify(certificateChain[0], base64URLDecode(signature));
|
||||
const negTwo = publicKey.get(-2);
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error("invalid or no -2 key given");
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error("invalid or no -3 key given");
|
||||
}
|
||||
const publicKeyData = Buffer.concat([
|
||||
ECC_PRELUDE,
|
||||
negTwo,
|
||||
negThree
|
||||
], 1 + 32 + 32);
|
||||
return {
|
||||
valid,
|
||||
publicKey: publicKeyData
|
||||
};
|
||||
}
|
||||
},
|
||||
packed: {
|
||||
verify ({ attStmt, authenticatorData, clientDataHash, publicKey, rpIdHash, credentialId }) {
|
||||
const verificationData = Buffer.concat([
|
||||
authenticatorData,
|
||||
clientDataHash
|
||||
]);
|
||||
if (attStmt.x5c) {
|
||||
const attCert = attStmt.x5c[0];
|
||||
const validSignature = crypto.createVerify("SHA256").update(verificationData).verify(PEMString(attCert), attStmt.sig);
|
||||
const negTwo = publicKey.get(-2);
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error("invalid or no -2 key given");
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error("invalid or no -3 key given");
|
||||
}
|
||||
const publicKeyData = Buffer.concat([
|
||||
ECC_PRELUDE,
|
||||
negTwo,
|
||||
negThree
|
||||
], 1 + 32 + 32);
|
||||
return {
|
||||
valid: validSignature,
|
||||
publicKey: publicKeyData
|
||||
};
|
||||
} else if (attStmt.ecdaaKeyId) {
|
||||
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation
|
||||
throw new Error("ECDAA-Verify is not supported");
|
||||
} else {
|
||||
if (attStmt.alg !== -7) throw new Error("alg mismatch");
|
||||
throw new Error("self attestation is not supported");
|
||||
}
|
||||
}
|
||||
},
|
||||
"fido-u2f": {
|
||||
verify ({ attStmt, authenticatorData, clientDataHash, publicKey, rpIdHash, credentialId }) {
|
||||
const x5c = attStmt.x5c;
|
||||
if (x5c.length !== 1) {
|
||||
throw new Error("x5c length does not match expectation");
|
||||
}
|
||||
const attCert = x5c[0];
|
||||
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
|
||||
const negTwo = publicKey.get(-2);
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error("invalid or no -2 key given");
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error("invalid or no -3 key given");
|
||||
}
|
||||
const publicKeyU2F = Buffer.concat([
|
||||
ECC_PRELUDE,
|
||||
negTwo,
|
||||
negThree
|
||||
], 1 + 32 + 32);
|
||||
const verificationData = Buffer.concat([
|
||||
NULL_BYTE,
|
||||
rpIdHash,
|
||||
clientDataHash,
|
||||
credentialId,
|
||||
publicKeyU2F
|
||||
]);
|
||||
const validSignature = crypto.createVerify("SHA256").update(verificationData).verify(PEMString(attCert), attStmt.sig);
|
||||
return {
|
||||
valid: validSignature,
|
||||
publicKey: publicKeyU2F
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user