mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 14:48:06 -05:00
202 lines
7.9 KiB
TypeScript
202 lines
7.9 KiB
TypeScript
import { ethers } from 'ethers';
|
|
import forge from 'node-forge';
|
|
|
|
import { PCR0_MANAGER_ADDRESS, RPC_URL } from '../constants/constants.js';
|
|
|
|
const GCP_ROOT_CERT = `
|
|
-----BEGIN CERTIFICATE-----
|
|
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
|
|
uyT5CnJulxSC5CT1
|
|
-----END CERTIFICATE-----
|
|
`;
|
|
|
|
const PCR0ManagerABI = ['function isPCR0Set(bytes calldata pcr0) external view returns (bool)'];
|
|
|
|
function base64UrlDecodeToBytes(input: string): string {
|
|
const base64 = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
|
|
return forge.util.decode64(padded);
|
|
}
|
|
|
|
function base64UrlDecodeToString(input: string): string {
|
|
const base64 = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
|
|
return forge.util.decodeUtf8(forge.util.decode64(padded));
|
|
}
|
|
|
|
type PKICertificates = {
|
|
leaf: forge.pki.Certificate;
|
|
intermediate: forge.pki.Certificate;
|
|
root: forge.pki.Certificate;
|
|
};
|
|
|
|
function extractCertificates(x5c: string[]): PKICertificates {
|
|
const decode = (b64: string) =>
|
|
forge.pki.certificateFromAsn1(forge.asn1.fromDer(forge.util.decode64(b64)));
|
|
|
|
return {
|
|
leaf: decode(x5c[0]),
|
|
intermediate: decode(x5c[1]),
|
|
root: decode(x5c[2]),
|
|
};
|
|
}
|
|
|
|
function compareCertificates(cert1: forge.pki.Certificate, cert2: forge.pki.Certificate): boolean {
|
|
const hash1 = forge.md.sha256
|
|
.create()
|
|
.update(forge.asn1.toDer(forge.pki.certificateToAsn1(cert1)).getBytes())
|
|
.digest()
|
|
.toHex();
|
|
const hash2 = forge.md.sha256
|
|
.create()
|
|
.update(forge.asn1.toDer(forge.pki.certificateToAsn1(cert2)).getBytes())
|
|
.digest()
|
|
.toHex();
|
|
return hash1 === hash2;
|
|
}
|
|
|
|
function verifyCertificateChain({ leaf, intermediate, root }: PKICertificates) {
|
|
const caStore = forge.pki.createCaStore([root]);
|
|
|
|
forge.pki.verifyCertificateChain(caStore, [leaf, intermediate, root], (vfd, depth) => {
|
|
if (vfd !== true) {
|
|
throw new Error(`Certificate verification failed at depth ${depth}`);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
const now = new Date();
|
|
if (now < root.validity.notBefore || now > root.validity.notAfter) {
|
|
throw new Error('Certificate is not within validity period');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Queries the PCR0Manager contract to verify that the PCR0 value extracted from the attestation
|
|
* is mapped to true.
|
|
* @param attestation An array of numbers representing the COSE_Sign1 encoded attestation document.
|
|
* @return A promise that resolves to true if the PCR0 value is set in the contract, or false otherwise.
|
|
*/
|
|
export async function checkPCR0Mapping(imageHashHex: string): Promise<boolean> {
|
|
// The getImageHash function returns a hex string (without the "0x" prefix)
|
|
// For a SHA384 hash, we expect 64 hex characters (32 bytes)
|
|
if (imageHashHex.length !== 64) {
|
|
throw new Error(
|
|
`Invalid PCR0 hash length: expected 64 hex characters, got ${imageHashHex.length}`
|
|
);
|
|
}
|
|
|
|
// Convert the PCR0 hash from hex to a byte array, ensuring proper "0x" prefix
|
|
const pcr0Bytes = ethers.getBytes(`0x${imageHashHex.padStart(96, '0')}`);
|
|
if (pcr0Bytes.length !== 48) {
|
|
throw new Error(`Invalid PCR0 bytes length: expected 48, got ${pcr0Bytes.length}`);
|
|
}
|
|
|
|
const celoProvider = new ethers.JsonRpcProvider(RPC_URL);
|
|
|
|
// Create a contract instance for the PCR0Manager
|
|
const pcr0Manager = new ethers.Contract(PCR0_MANAGER_ADDRESS, PCR0ManagerABI, celoProvider);
|
|
|
|
try {
|
|
// Query the contract: isPCR0Set returns true if the given PCR0 value is set
|
|
return await pcr0Manager.isPCR0Set(pcr0Bytes);
|
|
} catch (error) {
|
|
console.error('Error checking PCR0 mapping:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function validatePKIToken(
|
|
attestationToken: string,
|
|
dev: boolean = true
|
|
): {
|
|
userPubkey: Buffer;
|
|
serverPubkey: Buffer;
|
|
imageHash: string;
|
|
verified: boolean;
|
|
} {
|
|
// Decode JWT header
|
|
const [encodedHeader, encodedPayload, encodedSignature] = attestationToken.split('.');
|
|
const header = JSON.parse(forge.util.decodeUtf8(forge.util.decode64(encodedHeader)));
|
|
if (header.alg !== 'RS256') throw new Error(`Invalid alg: ${header.alg}`);
|
|
|
|
const x5c = header.x5c;
|
|
if (!x5c || x5c.length !== 3) throw new Error('x5c header must contain exactly 3 certificates');
|
|
const certificates = extractCertificates(x5c);
|
|
const storedRootCert = forge.pki.certificateFromPem(GCP_ROOT_CERT);
|
|
// Compare root certificate fingerprint
|
|
if (!compareCertificates(storedRootCert, certificates.root)) {
|
|
throw new Error('Root certificate does not match expected root');
|
|
}
|
|
verifyCertificateChain(certificates);
|
|
// Verify JWT signature
|
|
const pemPublicKey = forge.pki.publicKeyToPem(certificates.leaf.publicKey);
|
|
try {
|
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
|
|
// Decode signature (base64url → binary)
|
|
const signatureBytes = base64UrlDecodeToBytes(encodedSignature); // string of binary bytes
|
|
|
|
// Verify RS256 signature
|
|
const md = forge.md.sha256.create();
|
|
md.update(signingInput, 'utf8');
|
|
const rsaPublicKey = certificates.leaf.publicKey as forge.pki.rsa.PublicKey; // cast to RSA type
|
|
const verified = rsaPublicKey.verify(md.digest().bytes(), signatureBytes);
|
|
if (!verified) throw new Error('Signature verification failed');
|
|
|
|
const payloadStr = base64UrlDecodeToString(encodedPayload);
|
|
const payload = JSON.parse(payloadStr);
|
|
if (!dev) {
|
|
if (payload.dbgstat !== 'disabled-since-boot') {
|
|
throw new Error('Debug mode is enabled');
|
|
}
|
|
}
|
|
return {
|
|
verified: true,
|
|
userPubkey: Buffer.from(payload.eat_nonce[0], 'base64'),
|
|
serverPubkey: Buffer.from(payload.eat_nonce[1], 'base64'),
|
|
//slice the sha256: prefix
|
|
imageHash: payload.submods.container.image_digest.slice(7),
|
|
};
|
|
} catch (err) {
|
|
console.error('TEE JWT signature verification failed:', err);
|
|
return {
|
|
verified: false,
|
|
userPubkey: Buffer.from([]),
|
|
serverPubkey: Buffer.from([]),
|
|
imageHash: '',
|
|
};
|
|
}
|
|
}
|