@turnkey/crypto
Version:
Encryption, decryption, and key related utility functions
245 lines (239 loc) • 10.8 kB
JavaScript
;
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