Files
smartacme/test/test.acme-crypto.ts

162 lines
5.6 KiB
TypeScript

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();