Skip to content
This repository was archived by the owner on Aug 4, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified coupling_runtime.exp
Binary file not shown.
Binary file modified coupling_runtime.lib
Binary file not shown.
223 changes: 223 additions & 0 deletions src/attestation.native.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { existsSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { dlopen, FFIType, ptr } from 'bun:ffi';
import { handleAttestationSubmit, handleCouplingProof } from './attestation';

/**
* Cross-implementation channel interop: the REAL native collector DLL (BCrypt ECDH + manual HKDF +
* AES-GCM) seals a payload to a server key we generate; the closed service (Web Crypto) must decrypt
* it. A successful decrypt + matching nonce proves the two independent crypto stacks agree on the
* wire format. Skips automatically when the DLL has not been built on this host.
*/

const DLL_PATH = fileURLToPath(
new URL(
'../../CreatorAssistant/Verify/Native/coupling-runtime-com/out/win-x64/Release/CouplingRuntimeCom.dll',
import.meta.url
)
);

const dllExists = existsSync(DLL_PATH);
const subtle = globalThis.crypto.subtle;

function b64(bytes: ArrayBuffer | Uint8Array): string {
const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
return Buffer.from(u8).toString('base64');
}
function cstr(s: string): Uint8Array {
return new Uint8Array(Buffer.from(`${s}\0`, 'utf8'));
}
function readCstr(buf: Uint8Array): string {
const end = buf.indexOf(0);
return Buffer.from(buf.subarray(0, end < 0 ? buf.length : end)).toString('utf8');
}

let originalFetch: typeof fetch;
let lastRelay: { path: string; body: any } | null = null;

beforeEach(() => {
lastRelay = null;
originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: string, init?: RequestInit) => {
lastRelay = { path: new URL(url).pathname, body: JSON.parse(String(init?.body ?? '{}')) };
return new Response(JSON.stringify({ success: true, blocked: false }), { status: 200 });
}) as unknown as typeof fetch;
});

afterEach(() => {
globalThis.fetch = originalFetch;
});

describe.skipIf(!dllExists)('native-sealed channel decrypts on the closed service', () => {
it('round-trips a real native seal through handleAttestationSubmit', async () => {
// Server attestation keypair: public goes to the native collector, private to the closed decrypt.
const serverEcdh = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
const serverPubSpkiB64 = b64(new Uint8Array(await subtle.exportKey('spki', serverEcdh.publicKey)));
const serverPrivPkcs8B64 = b64(new Uint8Array(await subtle.exportKey('pkcs8', serverEcdh.privateKey)));

process.env.ATTESTATION_CHANNEL_PRIVATE_KEY_PKCS8 = serverPrivPkcs8B64;
process.env.ATTESTATION_FINGERPRINT_SALT = b64(crypto.getRandomValues(new Uint8Array(32)));
process.env.YUCP_TPM_ROOTS_JSON = JSON.stringify([]); // TPM verify will fail; channel is the test
process.env.CONVEX_API_SECRET = 'test-secret';
process.env.CONVEX_SITE_URL = 'http://control-plane.test';

const lib = dlopen(DLL_PATH, {
xg_0201: {
args: [
FFIType.ptr,
FFIType.ptr,
FFIType.ptr,
FFIType.ptr,
FFIType.ptr,
FFIType.i32,
FFIType.ptr,
],
returns: FFIType.i32,
},
});

const nonce = 'a'.repeat(64);
const outBuf = new Uint8Array(64 * 1024);
const needed = new Int32Array(1);

const rc = lib.symbols.xg_0201(
ptr(cstr(serverPubSpkiB64)),
ptr(cstr(nonce)),
ptr(cstr('corr-native')),
ptr(cstr('{}')),
ptr(outBuf),
outBuf.length,
ptr(needed)
);
lib.close();

expect(rc).toBe(0);
const sealedJson = readCstr(outBuf);
const sealed = JSON.parse(sealedJson) as {
ephemeralPublicSpkiB64: string;
ivB64: string;
ciphertextB64: string;
};
expect(sealed.ephemeralPublicSpkiB64.length).toBeGreaterThan(0);
expect(sealed.ciphertextB64.length).toBeGreaterThan(0);

// The closed service (Web Crypto) decrypts the BCrypt-sealed channel. Success proves interop.
const result = await handleAttestationSubmit(sealed);

expect(result.ok).toBe(true);
expect(result.status).toBe(200);
// The decrypted payload carried our nonce, confirming the channel round-tripped correctly.
expect(lastRelay?.path).toBe('/v1/attestation/internal/record');
expect(lastRelay?.body.nonce).toBe(nonce);
});

it('enforces native self-integrity: pinned hash accepts, unknown hash is rejected', async () => {
const serverEcdh = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
const serverPubSpkiB64 = b64(new Uint8Array(await subtle.exportKey('spki', serverEcdh.publicKey)));
const serverPrivPkcs8B64 = b64(new Uint8Array(await subtle.exportKey('pkcs8', serverEcdh.privateKey)));
process.env.ATTESTATION_CHANNEL_PRIVATE_KEY_PKCS8 = serverPrivPkcs8B64;
process.env.ATTESTATION_FINGERPRINT_SALT = b64(crypto.getRandomValues(new Uint8Array(32)));
process.env.YUCP_TPM_ROOTS_JSON = JSON.stringify([]);
process.env.CONVEX_API_SECRET = 'test-secret';
process.env.CONVEX_SITE_URL = 'http://control-plane.test';

// The runtime reports SHA-256 of its own on-disk module; the closed service pins it.
const realHash = createHash('sha256').update(readFileSync(DLL_PATH)).digest('hex');

const lib = dlopen(DLL_PATH, {
xg_0201: {
args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.i32, FFIType.ptr],
returns: FFIType.i32,
},
});
const nonce = 's'.repeat(64);
const outBuf = new Uint8Array(64 * 1024);
const needed = new Int32Array(1);
const rc = lib.symbols.xg_0201(
ptr(cstr(serverPubSpkiB64)), ptr(cstr(nonce)), ptr(cstr('corr-self')), ptr(cstr('{}')),
ptr(outBuf), outBuf.length, ptr(needed)
);
lib.close();
expect(rc).toBe(0);
const sealed = JSON.parse(readCstr(outBuf)) as {
ephemeralPublicSpkiB64: string; ivB64: string; ciphertextB64: string;
};

// Pinned to the real runtime hash -> accepted (proves the native emitted the correct self-hash).
process.env.YUCP_RUNTIME_SELF_HASHES_JSON = JSON.stringify([realHash]);
const accepted = await handleAttestationSubmit(sealed);
expect(accepted.ok).toBe(true);

// Pinned to a different hash -> the runtime looks modified -> rejected, with a generic error.
process.env.YUCP_RUNTIME_SELF_HASHES_JSON = JSON.stringify(['00'.repeat(32)]);
const rejected = await handleAttestationSubmit(sealed);
expect(rejected.ok).toBe(false);
expect(rejected.status).toBe(422);
expect(JSON.stringify(rejected.body)).not.toContain(realHash);

delete process.env.YUCP_RUNTIME_SELF_HASHES_JSON;
});

it('rejects a native coupling proof when no pinned TPM root can authenticate it', async () => {
const serverEcdh = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
const serverPubSpkiB64 = b64(new Uint8Array(await subtle.exportKey('spki', serverEcdh.publicKey)));
const serverPrivPkcs8B64 = b64(new Uint8Array(await subtle.exportKey('pkcs8', serverEcdh.privateKey)));
process.env.ATTESTATION_CHANNEL_PRIVATE_KEY_PKCS8 = serverPrivPkcs8B64;
process.env.ATTESTATION_FINGERPRINT_SALT = b64(crypto.getRandomValues(new Uint8Array(32)));
process.env.YUCP_TPM_ROOTS_JSON = JSON.stringify([]);
delete process.env.YUCP_RUNTIME_SELF_HASHES_JSON;

const dir = mkdtempSync(join(tmpdir(), 'yucp-coupling-'));
const assetA = join(dir, 'a.bytes');
const assetB = join(dir, 'b.bytes');
writeFileSync(assetA, Buffer.from('watermarked-asset-A'));
writeFileSync(assetB, Buffer.from('watermarked-asset-B'));

const lib = dlopen(DLL_PATH, {
xg_0202: {
args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.i32, FFIType.ptr],
returns: FFIType.i32,
},
});

const seal = (assetPathsNl: string) => {
const outBuf = new Uint8Array(64 * 1024);
const needed = new Int32Array(1);
const rc = lib.symbols.xg_0202(
ptr(cstr(serverPubSpkiB64)), ptr(cstr('a'.repeat(64))), ptr(cstr('corr-coupling')),
ptr(cstr(assetPathsNl)), ptr(outBuf), outBuf.length, ptr(needed)
);
return { rc, json: readCstr(outBuf) };
};

// The channel and asset hashing path are real, but without a pinned TPM root the proof is not
// authenticated strongly enough to persist forensic asset evidence.
const ok = seal(`${assetA}\n${assetB}`);
expect(ok.rc).toBe(0);
const sealed = JSON.parse(ok.json) as { ephemeralPublicSpkiB64: string; ivB64: string; ciphertextB64: string };
const rejected = await handleCouplingProof(sealed);
expect(rejected.ok).toBe(false);
expect(rejected.status).toBe(422);
expect(rejected.body).toEqual({ error: 'Coupling proof rejected' });
expect(lastRelay).toBeNull();

// A tampered channel -> decryption fails closed.
const tampered = { ...sealed, ciphertextB64: b64(crypto.getRandomValues(new Uint8Array(48))) };
const tamperResult = await handleCouplingProof(tampered);
expect(tamperResult.ok).toBe(false);

// A proof carrying no assets -> rejected.
const empty = seal('');
lib.close();
expect(empty.rc).toBe(0);
const emptySealed = JSON.parse(empty.json) as { ephemeralPublicSpkiB64: string; ivB64: string; ciphertextB64: string };
const emptyResult = await handleCouplingProof(emptySealed);
expect(emptyResult.ok).toBe(false);

rmSync(dir, { recursive: true, force: true });
});
});
Loading