@0xsequence/tee-verifier
Version:
A library for verifying TEE enclave attestation
173 lines (172 loc) • 6.91 kB
JavaScript
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));
}