import { bytesToArrayBuffer } from "../crypto/encoding"; import { derEcdsaToRaw } from "./verify"; export interface DerElement { tag: number; start: number; contentStart: number; end: number; } export function readDerElement(bytes: Uint8Array, offset: number): DerElement { if (offset < 0 || offset + 2 > bytes.length) throw new Error("DER value is truncated."); const start = offset; const tag = bytes[offset++]!; let length = bytes[offset++]!; if (length & 0x80) { const count = length & 0x7f; if (count === 0 || count > 4 || offset + count > bytes.length) throw new Error("DER length is invalid."); length = 0; for (let index = 0; index < count; index += 1) length = length * 256 + bytes[offset++]!; } if (length < 0 || offset + length > bytes.length) throw new Error("DER content is truncated."); return { tag, start, contentStart: offset, end: offset + length }; } export function derChildren( bytes: Uint8Array, parent: DerElement, ): DerElement[] { const children: DerElement[] = []; let offset = parent.contentStart; while (offset < parent.end) { const child = readDerElement(bytes, offset); if (child.end > parent.end) throw new Error("DER child exceeds its parent."); children.push(child); offset = child.end; } return children; } function certificateTbs(bytes: Uint8Array): DerElement { const certificate = readDerElement(bytes, 0); if (certificate.tag !== 0x30 || certificate.end !== bytes.length) throw new Error("Attestation certificate is not a complete DER sequence."); const tbs = derChildren(bytes, certificate)[0]; if (!tbs || tbs.tag !== 0x30) throw new Error("Certificate TBSCertificate is missing."); return tbs; } export function certificateSpki(bytes: Uint8Array): Uint8Array { const fields = derChildren(bytes, certificateTbs(bytes)); let index = fields[0]?.tag === 0xa0 ? 1 : 0; index += 5; // serial, signature, issuer, validity, subject const spki = fields[index]; if (!spki || spki.tag !== 0x30) throw new Error("Certificate SubjectPublicKeyInfo is missing."); return bytes.slice(spki.start, spki.end); } export function certificatePublicKeyBytes(bytes: Uint8Array): Uint8Array { const spki = readDerElement(certificateSpki(bytes), 0); const children = derChildren(certificateSpki(bytes), spki); const bitString = children[1]; if ( !bitString || bitString.tag !== 0x03 || bitString.contentStart >= bitString.end || certificateSpki(bytes)[bitString.contentStart] !== 0 ) throw new Error("Certificate public-key bit string is invalid."); return certificateSpki(bytes).slice( bitString.contentStart + 1, bitString.end, ); } function containsBytes(certificate: Uint8Array, value: Uint8Array): boolean { if (!value.length || value.length > certificate.length) return false; outer: for ( let offset = 0; offset <= certificate.length - value.length; offset += 1 ) { for (let index = 0; index < value.length; index += 1) if (certificate[offset + index] !== value[index]) continue outer; return true; } return false; } function oidBytes(oid: string): Uint8Array { const arcs = oid.split(".").map(Number); if ( arcs.length < 2 || arcs.some((arc) => !Number.isSafeInteger(arc) || arc < 0) || arcs[0]! > 2 || (arcs[0]! < 2 && arcs[1]! > 39) ) throw new Error("Object identifier is invalid."); const output = [arcs[0]! * 40 + arcs[1]!]; for (const arc of arcs.slice(2)) { const encoded = [arc & 0x7f]; let remaining = Math.floor(arc / 128); while (remaining) { encoded.unshift((remaining & 0x7f) | 0x80); remaining = Math.floor(remaining / 128); } output.push(...encoded); } return Uint8Array.from(output); } export function certificateExtension( certificate: Uint8Array, oid: string, ): Uint8Array | undefined { const fields = derChildren(certificate, certificateTbs(certificate)); const wrapper = fields.find((field) => field.tag === 0xa3); if (!wrapper) return undefined; const sequence = derChildren(certificate, wrapper)[0]; if (!sequence || sequence.tag !== 0x30) throw new Error("Certificate extensions are malformed."); const expectedOid = oidBytes(oid); for (const extension of derChildren(certificate, sequence)) { if (extension.tag !== 0x30) throw new Error("Certificate extension is malformed."); const parts = derChildren(certificate, extension); const name = parts[0]; const value = parts.at(-1); if (!name || name.tag !== 0x06 || !value || value.tag !== 0x04) continue; const actualOid = certificate.slice(name.contentStart, name.end); if (bytesEqual(actualOid, expectedOid)) return certificate.slice(value.contentStart, value.end); } return undefined; } function bytesEqual(left: Uint8Array, right: Uint8Array): boolean { if (left.length !== right.length) return false; return left.every((byte, index) => byte === right[index]); } export function extensionContainsBytes( certificate: Uint8Array, oid: string, value: Uint8Array, ): boolean { const extension = certificateExtension(certificate, oid); return extension ? containsBytes(extension, value) : false; } export interface AttestationAlgorithm { importAlgorithm: | AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams; verifyAlgorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams; normalizeSignature(signature: Uint8Array): Uint8Array; hash: "SHA-256" | "SHA-384" | "SHA-512"; } export function attestationAlgorithm( coseAlgorithm: number, ): AttestationAlgorithm { if (coseAlgorithm === -7) return { importAlgorithm: { name: "ECDSA", namedCurve: "P-256" }, verifyAlgorithm: { name: "ECDSA", hash: "SHA-256" }, normalizeSignature: (value) => derEcdsaToRaw(value, 32), hash: "SHA-256", }; if (coseAlgorithm === -35) return { importAlgorithm: { name: "ECDSA", namedCurve: "P-384" }, verifyAlgorithm: { name: "ECDSA", hash: "SHA-384" }, normalizeSignature: (value) => derEcdsaToRaw(value, 48), hash: "SHA-384", }; if (coseAlgorithm === -36) return { importAlgorithm: { name: "ECDSA", namedCurve: "P-521" }, verifyAlgorithm: { name: "ECDSA", hash: "SHA-512" }, normalizeSignature: (value) => derEcdsaToRaw(value, 66), hash: "SHA-512", }; if (coseAlgorithm === -257) return { importAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, verifyAlgorithm: "RSASSA-PKCS1-v1_5", normalizeSignature: (value) => value, hash: "SHA-256", }; if (coseAlgorithm === -258) return { importAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, verifyAlgorithm: "RSASSA-PKCS1-v1_5", normalizeSignature: (value) => value, hash: "SHA-384", }; if (coseAlgorithm === -259) return { importAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, verifyAlgorithm: "RSASSA-PKCS1-v1_5", normalizeSignature: (value) => value, hash: "SHA-512", }; if (coseAlgorithm === -37) return { importAlgorithm: { name: "RSA-PSS", hash: "SHA-256" }, verifyAlgorithm: { name: "RSA-PSS", saltLength: 32 }, normalizeSignature: (value) => value, hash: "SHA-256", }; throw new Error(`Unsupported attestation COSE algorithm ${coseAlgorithm}.`); } export async function verifyCertificateSignature( certificate: Uint8Array, coseAlgorithm: number, signature: Uint8Array, signed: Uint8Array, ): Promise { const algorithm = attestationAlgorithm(coseAlgorithm); const key = await crypto.subtle.importKey( "spki", bytesToArrayBuffer(certificateSpki(certificate)), algorithm.importAlgorithm, false, ["verify"], ); return crypto.subtle.verify( algorithm.verifyAlgorithm, key, bytesToArrayBuffer(algorithm.normalizeSignature(signature)), bytesToArrayBuffer(signed), ); }