import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as crypto from 'node:crypto'; import { AcmeCrypto } from '../ts/acme/acme.classes.crypto.js'; // --- createRsaPrivateKey --- tap.test('createRsaPrivateKey returns valid PEM', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); expect(pem).toStartWith('-----BEGIN PRIVATE KEY-----'); expect(pem).toInclude('-----END PRIVATE KEY-----'); }); tap.test('createRsaPrivateKey creates RSA key type', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const keyObj = crypto.createPrivateKey(pem); expect(keyObj.asymmetricKeyType).toEqual('rsa'); }); tap.test('createRsaPrivateKey respects modulusLength', async () => { const pem = AcmeCrypto.createRsaPrivateKey(4096); const keyObj = crypto.createPrivateKey(pem); expect(keyObj.asymmetricKeyDetails!.modulusLength).toEqual(4096); }); // --- getJwk --- tap.test('getJwk returns sorted keys {e, kty, n}', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const jwk = AcmeCrypto.getJwk(pem); const keys = Object.keys(jwk); expect(keys).toEqual(['e', 'kty', 'n']); expect(jwk.kty).toEqual('RSA'); expect(typeof jwk.e).toEqual('string'); expect(typeof jwk.n).toEqual('string'); }); tap.test('getJwk is deterministic for same key', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const jwk1 = AcmeCrypto.getJwk(pem); const jwk2 = AcmeCrypto.getJwk(pem); expect(JSON.stringify(jwk1)).toEqual(JSON.stringify(jwk2)); }); // --- getJwkThumbprint --- tap.test('getJwkThumbprint matches manual SHA-256 computation', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const jwk = AcmeCrypto.getJwk(pem); const thumbprint = AcmeCrypto.getJwkThumbprint(jwk); // Manual computation const canonical = JSON.stringify({ e: jwk.e, kty: jwk.kty, n: jwk.n }); const expected = crypto.createHash('sha256').update(canonical).digest().toString('base64url'); expect(thumbprint).toEqual(expected); }); tap.test('getJwkThumbprint is base64url format (no +, /, =)', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const jwk = AcmeCrypto.getJwk(pem); const thumbprint = AcmeCrypto.getJwkThumbprint(jwk); expect(thumbprint).not.toInclude('+'); expect(thumbprint).not.toInclude('/'); expect(thumbprint).not.toInclude('='); }); // --- createJws --- tap.test('createJws returns correct structure', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const jwk = AcmeCrypto.getJwk(pem); const jws = AcmeCrypto.createJws(pem, 'https://acme.example/new-acct', { foo: 'bar' }, { nonce: 'test-nonce', jwk, }); expect(typeof jws.protected).toEqual('string'); expect(typeof jws.payload).toEqual('string'); expect(typeof jws.signature).toEqual('string'); }); tap.test('createJws protected header contains alg, nonce, url, jwk', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const jwk = AcmeCrypto.getJwk(pem); const jws = AcmeCrypto.createJws(pem, 'https://acme.example/new-acct', { foo: 'bar' }, { nonce: 'test-nonce', jwk, }); const header = JSON.parse(Buffer.from(jws.protected, 'base64url').toString()); expect(header.alg).toEqual('RS256'); expect(header.nonce).toEqual('test-nonce'); expect(header.url).toEqual('https://acme.example/new-acct'); expect(header.jwk).toBeTruthy(); expect(header.kid).toBeFalsy(); }); tap.test('createJws uses kid when provided instead of jwk', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const jws = AcmeCrypto.createJws(pem, 'https://acme.example/order', { test: 1 }, { nonce: 'nonce-2', kid: 'https://acme.example/acct/1', }); const header = JSON.parse(Buffer.from(jws.protected, 'base64url').toString()); expect(header.kid).toEqual('https://acme.example/acct/1'); expect(header.jwk).toBeFalsy(); }); tap.test('createJws POST-as-GET produces empty payload', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const jws = AcmeCrypto.createJws(pem, 'https://acme.example/order/1', null, { nonce: 'nonce-3', kid: 'https://acme.example/acct/1', }); expect(jws.payload).toEqual(''); }); tap.test('createJws signature is verifiable', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const jwk = AcmeCrypto.getJwk(pem); const jws = AcmeCrypto.createJws(pem, 'https://acme.example/test', { val: 1 }, { nonce: 'nonce-v', jwk, }); const sigInput = `${jws.protected}.${jws.payload}`; const pubKey = crypto.createPublicKey(pem); const verified = crypto.verify( 'sha256', Buffer.from(sigInput), pubKey, Buffer.from(jws.signature, 'base64url'), ); expect(verified).toBeTrue(); }); // --- createCsr --- tap.test('createCsr returns [keyPem, csrPem] with valid PEM formats', async () => { const [keyPem, csrPem] = await AcmeCrypto.createCsr({ commonName: 'example.com' }); expect(keyPem).toStartWith('-----BEGIN PRIVATE KEY-----'); expect(csrPem).toInclude('CERTIFICATE REQUEST'); }); tap.test('createCsr uses existing key when provided', async () => { const existingKey = AcmeCrypto.createRsaPrivateKey(); const [keyPem, csrPem] = await AcmeCrypto.createCsr({ commonName: 'example.com' }, existingKey); expect(keyPem).toEqual(existingKey); expect(csrPem).toInclude('CERTIFICATE REQUEST'); }); // --- pemToBuffer --- tap.test('pemToBuffer strips headers and returns Buffer', async () => { const pem = AcmeCrypto.createRsaPrivateKey(); const buf = AcmeCrypto.pemToBuffer(pem); expect(buf).toBeInstanceOf(Buffer); expect(buf.length).toBeGreaterThan(0); // Verify it doesn't contain PEM header text const str = buf.toString('utf-8'); expect(str).not.toInclude('-----BEGIN'); }); export default tap.start();