fix(federation): use real PEM certs in enrollment + ca service tests (#507)
This commit was merged in pull request #507.
This commit is contained in:
138
apps/gateway/src/federation/__tests__/helpers/test-cert.ts
Normal file
138
apps/gateway/src/federation/__tests__/helpers/test-cert.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Test helpers for generating real X.509 PEM certificates in unit tests.
|
||||
*
|
||||
* PR #501 (FED-M2-11) introduced strict `new X509Certificate(certPem)` parsing
|
||||
* in both EnrollmentService.extractCertNotAfter and CaService.issueCert — dummy
|
||||
* cert strings now throw `error:0680007B:asn1 encoding routines::header too long`.
|
||||
*
|
||||
* These helpers produce minimal but cryptographically valid self-signed EC P-256
|
||||
* certificates via @peculiar/x509 + Node.js webcrypto, suitable for test mocks.
|
||||
*
|
||||
* Two variants:
|
||||
* - makeSelfSignedCert() Plain cert — satisfies node:crypto X509Certificate parse.
|
||||
* - makeMosaicIssuedCert(opts) Cert with custom Mosaic OID extensions — satisfies the
|
||||
* CRIT-1 OID presence + value checks in CaService.issueCert.
|
||||
*/
|
||||
|
||||
import { webcrypto } from 'node:crypto';
|
||||
import {
|
||||
X509CertificateGenerator,
|
||||
Extension,
|
||||
KeyUsagesExtension,
|
||||
KeyUsageFlags,
|
||||
BasicConstraintsExtension,
|
||||
cryptoProvider,
|
||||
} from '@peculiar/x509';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Encode a string as an ASN.1 UTF8String TLV:
|
||||
* 0x0C (tag) + 1-byte length (for strings ≤ 127 bytes) + UTF-8 bytes.
|
||||
*
|
||||
* CaService.issueCert reads the extension value as:
|
||||
* decoder.decode(grantIdExt.value.slice(2))
|
||||
* i.e. it skips the tag + length byte and decodes the remainder as UTF-8.
|
||||
* So we must produce exactly this encoding as the OCTET STRING content.
|
||||
*/
|
||||
function encodeUtf8String(value: string): Uint8Array {
|
||||
const utf8 = new TextEncoder().encode(value);
|
||||
if (utf8.length > 127) {
|
||||
throw new Error('encodeUtf8String: value too long for single-byte length encoding');
|
||||
}
|
||||
const buf = new Uint8Array(2 + utf8.length);
|
||||
buf[0] = 0x0c; // ASN.1 UTF8String tag
|
||||
buf[1] = utf8.length;
|
||||
buf.set(utf8, 2);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mosaic OID constants (must match production CaService)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const OID_MOSAIC_GRANT_ID = '1.3.6.1.4.1.99999.1';
|
||||
const OID_MOSAIC_SUBJECT_USER_ID = '1.3.6.1.4.1.99999.2';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generate a minimal self-signed EC P-256 certificate valid for 1 day.
|
||||
* CN=harness-test, no custom extensions.
|
||||
*
|
||||
* Suitable for:
|
||||
* - EnrollmentService.extractCertNotAfter (just needs parseable PEM)
|
||||
* - Any mock that returns certPem / certChainPem without OID checks
|
||||
*/
|
||||
export async function makeSelfSignedCert(): Promise<string> {
|
||||
// Ensure @peculiar/x509 uses Node.js webcrypto (available as globalThis.crypto in Node 19+,
|
||||
// but we set it explicitly here to be safe on all Node 18+ versions).
|
||||
cryptoProvider.set(webcrypto as unknown as Parameters<typeof cryptoProvider.set>[0]);
|
||||
|
||||
const alg = { name: 'ECDSA', namedCurve: 'P-256', hash: 'SHA-256' } as const;
|
||||
const keys = await webcrypto.subtle.generateKey(alg, false, ['sign', 'verify']);
|
||||
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now.getTime() + 86_400_000);
|
||||
|
||||
const cert = await X509CertificateGenerator.createSelfSigned({
|
||||
serialNumber: '01',
|
||||
name: 'CN=harness-test',
|
||||
notBefore: now,
|
||||
notAfter: tomorrow,
|
||||
signingAlgorithm: alg,
|
||||
keys,
|
||||
extensions: [
|
||||
new BasicConstraintsExtension(false),
|
||||
new KeyUsagesExtension(KeyUsageFlags.digitalSignature),
|
||||
],
|
||||
});
|
||||
|
||||
return cert.toString('pem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a self-signed EC P-256 certificate that contains the two custom
|
||||
* Mosaic OID extensions required by CaService.issueCert's CRIT-1 check:
|
||||
* OID 1.3.6.1.4.1.99999.1 → mosaic_grant_id (value = grantId)
|
||||
* OID 1.3.6.1.4.1.99999.2 → mosaic_subject_user_id (value = subjectUserId)
|
||||
*
|
||||
* The extension value encoding matches the production parser's `.slice(2)` assumption:
|
||||
* each extension value is an OCTET STRING wrapping an ASN.1 UTF8String TLV.
|
||||
*/
|
||||
export async function makeMosaicIssuedCert(opts: {
|
||||
grantId: string;
|
||||
subjectUserId: string;
|
||||
}): Promise<string> {
|
||||
// Ensure @peculiar/x509 uses Node.js webcrypto.
|
||||
cryptoProvider.set(webcrypto as unknown as Parameters<typeof cryptoProvider.set>[0]);
|
||||
|
||||
const alg = { name: 'ECDSA', namedCurve: 'P-256', hash: 'SHA-256' } as const;
|
||||
const keys = await webcrypto.subtle.generateKey(alg, false, ['sign', 'verify']);
|
||||
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now.getTime() + 86_400_000);
|
||||
|
||||
const cert = await X509CertificateGenerator.createSelfSigned({
|
||||
serialNumber: '01',
|
||||
name: 'CN=mosaic-issued-test',
|
||||
notBefore: now,
|
||||
notAfter: tomorrow,
|
||||
signingAlgorithm: alg,
|
||||
keys,
|
||||
extensions: [
|
||||
new BasicConstraintsExtension(false),
|
||||
new KeyUsagesExtension(KeyUsageFlags.digitalSignature),
|
||||
// mosaic_grant_id — OID 1.3.6.1.4.1.99999.1
|
||||
new Extension(OID_MOSAIC_GRANT_ID, false, encodeUtf8String(opts.grantId)),
|
||||
// mosaic_subject_user_id — OID 1.3.6.1.4.1.99999.2
|
||||
new Extension(OID_MOSAIC_SUBJECT_USER_ID, false, encodeUtf8String(opts.subjectUserId)),
|
||||
],
|
||||
});
|
||||
|
||||
return cert.toString('pem');
|
||||
}
|
||||
Reference in New Issue
Block a user