UNPKG

@turnkey/crypto

Version:

Encryption, decryption, and key related utility functions

245 lines (239 loc) 10.8 kB
'use strict'; var encoding = require('@turnkey/encoding'); var p256 = require('@noble/curves/p256'); var sha2 = require('@noble/hashes/sha2'); var CBOR = require('cbor-js'); var x509 = require('@peculiar/x509'); var constants = require('./constants.js'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var CBOR__namespace = /*#__PURE__*/_interopNamespaceDefault(CBOR); var x509__namespace = /*#__PURE__*/_interopNamespaceDefault(x509); const getCryptoInstance = async () => { let cryptoInstance; // Use globalThis.crypto.subtle if available if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle) { cryptoInstance = globalThis.crypto; x509__namespace.cryptoProvider.set(cryptoInstance); return cryptoInstance; } else { throw new Error("Web Crypto API is not available in this environment. You may need to polyfill it."); } }; /** * Utility: SHA-256 digest → hex (uppercase) */ async function sha256Hex(data) { const cryptoInstance = await getCryptoInstance(); const digest = await cryptoInstance.subtle.digest("SHA-256", data); return encoding.uint8ArrayToHexString(new Uint8Array(digest)).toUpperCase(); } /** * Utility: Import SPKI public key for ECDSA verify */ async function importEcdsaPublicKey(spki) { const cryptoInstance = await getCryptoInstance(); return cryptoInstance.subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: "P-384" }, // AWS Nitro uses ES384 false, ["verify"]); } /** * verify goes through the following verification steps for an app proof & boot proof pair: * - Verify app proof signature * - Verify the boot proof * - Attestation doc was signed by AWS * - Attestation doc's `user_data` is the hash of the qos manifest * - Verify the connection between the app proof & boot proof i.e. that the ephemeral keys match * * For more information, check out https://whitepaper.turnkey.com/foundations */ async function verify(appProof, bootProof) { // 1. Verify App Proof verifyAppProofSignature(appProof); // 2. Verify Boot Proof // Parse attestation const coseSign1Der = Uint8Array.from(atob(bootProof.awsAttestationDocB64) .split("") .map((c) => c.charCodeAt(0))); const coseSign1 = CBOR__namespace.decode(coseSign1Der.buffer); const [, , payload] = coseSign1; const attestationDoc = CBOR__namespace.decode(new Uint8Array(payload).buffer); // Verify cose sign1 signature await verifyCoseSign1Sig(coseSign1, attestationDoc.certificate); // Verify certificate chain const appProofTimestampMs = parseInt(JSON.parse(appProof.proofPayload).timestampMs); await verifyCertificateChain(attestationDoc.cabundle, constants.AWS_ROOT_CERT_PEM, attestationDoc.certificate, appProofTimestampMs); // Verify manifest digest const decodedBootProofManifest = Uint8Array.from(atob(bootProof.qosManifestB64) .split("") .map((c) => c.charCodeAt(0))); const manifestDigest = sha2.sha256(decodedBootProofManifest); if (!bytesEq(manifestDigest, attestationDoc.user_data)) { throw new Error(`attestationDoc's user_data doesn't match the hash of the manifest. attestationDoc.user_data: ${attestationDoc.user_data} , manifest digest: ${manifestDigest}`); } // 3. Verify that all the ephemeral public keys match: app proof, boot proof structure, actual attestation doc const publicKeyBytes = new Uint8Array(attestationDoc.public_key); const attestationPubKey = encoding.uint8ArrayToHexString(publicKeyBytes); if (appProof.publicKey !== attestationPubKey || attestationPubKey !== bootProof.ephemeralPublicKeyHex) { throw new Error(`Ephemeral pub keys from app proof: ${appProof.publicKey}, boot proof structure ${bootProof.ephemeralPublicKeyHex}, and attestation doc ${attestationPubKey} should all match`); } } /** * Verify app proof signature with @noble/curves */ function verifyAppProofSignature(appProof) { if (appProof.scheme !== "SIGNATURE_SCHEME_EPHEMERAL_KEY_P256") { throw new Error("Unsupported signature scheme"); } // Decode public key let publicKeyBytes; try { publicKeyBytes = encoding.uint8ArrayFromHexString(appProof.publicKey); } catch { throw new Error("Failed to decode public key"); } if (publicKeyBytes.length !== 130) { throw new Error(`Expected 130 bytes (encryption + signing pub keys), got ${publicKeyBytes.length} bytes`); } // Extract signing key (last 65 bytes, uncompressed P-256 point) const signingKeyBytes = publicKeyBytes.slice(65); if (signingKeyBytes.length !== 65 || signingKeyBytes[0] !== 0x04) { throw new Error("Invalid signing key format: expected 65-byte uncompressed P-256 point (0x04||X||Y)"); } // Validate it's a valid P-256 public key by attempting to create a point try { p256.p256.ProjectivePoint.fromHex(signingKeyBytes); } catch (error) { throw new Error(`Invalid P-256 public key: ${error}`); } // Decode signature (64 bytes = 32 bytes r + 32 bytes s) let signatureBytes; try { signatureBytes = encoding.uint8ArrayFromHexString(appProof.signature); } catch { throw new Error("Failed to decode signature"); } if (signatureBytes.length !== 64) { throw new Error(`Expected 64 bytes signature (r||s), got ${signatureBytes.length} bytes`); } // Hash the proof payload const payloadBytes = new TextEncoder().encode(appProof.proofPayload); const payloadDigest = sha2.sha256(payloadBytes); // Verify ECDSA signature const isValid = p256.p256.verify(signatureBytes, payloadDigest, signingKeyBytes); if (!isValid) { throw new Error("Signature verification failed"); } } async function verifyCertificateChain(cabundle, rootCertPem, leafCert, timestampMs) { try { // Check root and assert fingerprint const rootX509 = new x509__namespace.X509Certificate(rootCertPem); const rootDer = new Uint8Array(rootX509.rawData); const rootSha = await sha256Hex(rootDer); if (rootSha !== constants.AWS_ROOT_CERT_SHA256) { throw new Error(`Pinned AWS root fingerprint mismatch: expected=${constants.AWS_ROOT_CERT_SHA256} actual=${rootSha}`); } // Bundle starts with root certificate. We're replacing the root with our hardcoded known certificate, so remove first element const bundleWithoutRoot = cabundle.slice(1); const intermediatesX509 = bundleWithoutRoot.map((c) => { if (!c) throw new Error("Invalid certificate data in cabundle"); return new x509__namespace.X509Certificate(c); }); const leaf = new x509__namespace.X509Certificate(leafCert); // Build path leaf → intermediates → root, with our hardcoded known root certificate const builder = new x509__namespace.X509ChainBuilder({ certificates: [rootX509, ...intermediatesX509], }); const chain = await builder.build(leaf); if (chain.length !== intermediatesX509.length + 2) { throw new Error(`Incorrect number of certs in X509 Chain. Expected ${intermediatesX509.length + 2}, got ${chain.length}`); } const appProofDate = new Date(timestampMs); for (let i = 0; i < chain.length; i++) { const cert = chain[i]; if (!cert) throw new Error("Invalid certificate in chain"); if (i === chain.length - 1) { // is root // Self-signature verification for root certificate const ok = await cert.verify({ publicKey: cert.publicKey, date: appProofDate, }); if (!ok) throw new Error("Pinned root failed self-signature verification"); } else { // Verify signature against issuer const issuer = chain[i + 1]; if (!issuer) throw new Error("Issuer can't be null"); // Attestation docs technically expire after 3 hours, so an app proof generated 3+ hours after an enclave // boots up will fail verification due to certificate expiration. This is okay because enclaves are immutable; // even if the cert is technically invalid, the code contained within it cannot change. To prevent the cert // expiration failure, we set `signatureOnly: true`. const ok = await cert.verify({ publicKey: issuer.publicKey, signatureOnly: true, date: appProofDate, }); if (!ok) { throw new Error(`Signature check failed: ${cert.subject} not signed by ${issuer?.subject}`); } } } } catch (error) { throw new Error(`Certificate chain verification failed: ${error instanceof Error ? error.message : String(error)}`); } } async function verifyCoseSign1Sig(coseSign1, leaf) { const [protectedHeaders, , payload, signature] = coseSign1; const tbs = new Uint8Array(CBOR__namespace.encode([ "Signature1", new Uint8Array(protectedHeaders), new Uint8Array(0), new Uint8Array(payload), ])); const leafCert = new x509__namespace.X509Certificate(leaf); const pubKey = await importEcdsaPublicKey(leafCert.publicKey.rawData); const cryptoInstance = await getCryptoInstance(); const ok = await cryptoInstance.subtle.verify({ name: "ECDSA", hash: { name: "SHA-384" } }, pubKey, new Uint8Array(signature), tbs); if (!ok) throw new Error("COSE_Sign1 ES384 verification failed"); } function bytesEq(a, b) { const A = new Uint8Array(a), B = new Uint8Array(b); if (A.length !== B.length) return false; for (let i = 0; i < A.length; i++) if (A[i] !== B[i]) return false; return true; } exports.getCryptoInstance = getCryptoInstance; exports.verify = verify; exports.verifyAppProofSignature = verifyAppProofSignature; exports.verifyCertificateChain = verifyCertificateChain; exports.verifyCoseSign1Sig = verifyCoseSign1Sig; //# sourceMappingURL=proof.js.map