UNPKG

@0xsequence/tee-verifier

Version:

A library for verifying TEE enclave attestation

173 lines (172 loc) 6.91 kB
import { decode as cborDecode, encode as cborEncode } from 'cbor2'; import { Certificate, CertificateChainValidationEngine } from 'pkijs'; export function createAttestationVerifyingFetch(options = {}) { const mergedOptions = { ...options, // From https://docs.aws.amazon.com/enclaves/latest/user/verify-root.html#validation-process rootCertFingerprint: '641a0321a3e244efe456463195d606317ed7cdcc3c1756e09893f3c68f79bb5b', verifyRootOfTrust: true, verifySignature: true, verifyNonce: true, verifyContentHash: true, }; return async (input, init = {}) => { let expectedNonce = ''; if (mergedOptions.verifyNonce) { const nonceArray = new Uint8Array(12); crypto.getRandomValues(nonceArray); expectedNonce = btoa(String.fromCharCode.apply(null, [...nonceArray])); init.headers = { ...init.headers, 'x-attestation-nonce': expectedNonce, }; } const fetch = mergedOptions.fetch ?? window.fetch; const res = await fetch(input, init); const attestationDoc = res.headers.get('x-attestation-document'); if (!attestationDoc) { console.warn('No attestation document found in response'); return res; } const startTime = performance.now(); const att = SignedAttestation.fromDocument(attestationDoc); if (mergedOptions.verifyNonce && att.nonce !== expectedNonce) { throw new Error('Invalid nonce'); } if (mergedOptions.expectedPCRs) { for (const [pcr, expectedHash] of mergedOptions.expectedPCRs.entries()) { const actualHash = att.pcrs.get(pcr); if (!actualHash) { throw new Error(`Missing PCR${pcr}`); } if (Array.isArray(expectedHash)) { if (!expectedHash.includes(actualHash)) { throw new Error(`Invalid PCR${pcr}: ${actualHash}`); } } else if (actualHash !== expectedHash) { throw new Error(`Invalid PCR${pcr}: ${actualHash}`); } } } if (mergedOptions.verifyRootOfTrust) { const valid = await att.verifyRootOfTrust(mergedOptions.checkDate); if (!valid) { throw new Error('Invalid root of trust'); } const actualFingerprint = await att.rootCertFingerprint(); if (mergedOptions.rootCertFingerprint && actualFingerprint !== mergedOptions.rootCertFingerprint) { throw new Error(`Invalid root certificate fingerprint: ${actualFingerprint}`); } } if (mergedOptions.verifySignature) { const valid = await att.verifySignature(); if (!valid) { throw new Error('Invalid signature'); } } if (mergedOptions.verifyContentHash && att.userData.startsWith('Sequence/1:')) { const attHash = att.userData.split(':')[1]; const method = init.method ?? 'GET'; let url; if (input instanceof URL) { url = input; } else { const inputUrl = typeof input === 'string' ? input : input.url; url = new URL(inputUrl, window.location.origin); } const path = url.pathname; const reqBody = init.body ?? ''; const resBody = await res.clone().text(); const buffer = new TextEncoder().encode(`${method} ${path}\n${reqBody}\n${resBody}`); const expectedHash = await crypto.subtle.digest('SHA-256', buffer); if (attHash !== btoa(String.fromCharCode.apply(null, [...new Uint8Array(expectedHash)]))) { throw new Error('Invalid user data hash'); } } if (mergedOptions.logTiming) { const endTime = performance.now(); const timeTaken = endTime - startTime; console.log(`Attestation verification took ${timeTaken.toFixed(2)}ms`); } return res; }; } export class Attestation { raw; cert; intermediates; rootCert; pcrs; constructor(raw) { this.raw = raw; if (!raw.cabundle[0]) { throw new Error('Invalid attestation CA bundle'); } this.cert = Certificate.fromBER(raw.certificate); this.rootCert = Certificate.fromBER(raw.cabundle[0]); this.intermediates = raw.cabundle.slice(1).map((cert) => Certificate.fromBER(cert)); this.pcrs = new Map(Array.from(raw.pcrs.entries()).map(([k, v]) => [ Number(k), Array.from(new Uint8Array(v)) .map((b) => b.toString(16).padStart(2, '0')) .join(''), ])); } get nonce() { return new TextDecoder().decode(this.raw.nonce); } get userData() { return new TextDecoder().decode(this.raw.user_data); } async verifyRootOfTrust(checkDate) { const chainEngine = new CertificateChainValidationEngine({ certs: [...this.intermediates, this.cert], trustedCerts: [this.rootCert], checkDate, }); const chain = await chainEngine.verify(); return chain.result; } async rootCertFingerprint() { const hashBuffer = await window.crypto.subtle.digest('SHA-256', this.raw.cabundle[0]); return Array.from(new Uint8Array(hashBuffer)) .map((b) => b.toString(16).padStart(2, '0')) .join(''); } } export class SignedAttestation extends Attestation { sign1; constructor(raw, sign1) { if (sign1.length !== 4) { throw new Error('Invalid COSE Sign1'); } super(raw); this.sign1 = sign1; } static fromDocument(document) { const attestationBytes = base64ToBytes(document); const coseSign1 = cborDecode(attestationBytes); if (coseSign1.length !== 4) { throw new Error('Invalid COSE Sign1'); } const payload = cborDecode(coseSign1[2]); return new SignedAttestation(payload, coseSign1); } async verifySignature() { const sigStructure = ['Signature1', this.sign1[0], new Uint8Array(), this.sign1[2]]; const sigStructureBytes = cborEncode(sigStructure); const sig = this.sign1[3]; const pubKey = await this.cert.getPublicKey(); const verified = await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-384', }, pubKey, sig, sigStructureBytes); return verified; } } function base64ToBytes(base64) { const binString = atob(base64); return Uint8Array.from(binString, (m) => m.codePointAt(0)); }