/** * Shared OID extraction helpers for Mosaic federation certificates. * * Custom OID registry (PRD §6, docs/federation/SETUP.md): * 1.3.6.1.4.1.99999.1 — mosaic_grant_id * 1.3.6.1.4.1.99999.2 — mosaic_subject_user_id * * The encoding convention: each extension value is an OCTET STRING wrapping * an ASN.1 UTF8String TLV: * 0x0C (tag) + 1-byte length + UTF-8 bytes * * CaService encodes values this way via encodeUtf8String(), and this module * decodes them with the corresponding `.slice(2)` to skip tag + length byte. * * This module is intentionally pure — no NestJS, no DB, no network I/O. */ import { X509Certificate } from '@peculiar/x509'; // --------------------------------------------------------------------------- // OID constants // --------------------------------------------------------------------------- export const OID_MOSAIC_GRANT_ID = '1.3.6.1.4.1.99999.1'; export const OID_MOSAIC_SUBJECT_USER_ID = '1.3.6.1.4.1.99999.2'; // --------------------------------------------------------------------------- // Extraction result types // --------------------------------------------------------------------------- export interface MosaicOids { grantId: string; subjectUserId: string; } export type OidExtractionResult = | { ok: true; value: MosaicOids } | { ok: false; error: 'MISSING_GRANT_ID' | 'MISSING_SUBJECT_USER_ID' | 'PARSE_ERROR'; detail?: string; }; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const decoder = new TextDecoder(); /** * Decode an extension value encoded as ASN.1 UTF8String TLV * (tag 0x0C + 1-byte length + UTF-8 bytes). * Validates tag, length byte, and buffer bounds before decoding. * Throws a descriptive Error on malformed input; caller wraps in try/catch. */ function decodeUtf8StringTlv(value: ArrayBuffer): string { const bytes = new Uint8Array(value); // Need at least tag + length bytes if (bytes.length < 2) { throw new Error(`UTF8String TLV too short: expected at least 2 bytes, got ${bytes.length}`); } // Tag byte must be 0x0C (ASN.1 UTF8String) if (bytes[0] !== 0x0c) { throw new Error( `UTF8String TLV tag mismatch: expected 0x0C, got 0x${bytes[0]!.toString(16).toUpperCase()}`, ); } // Only single-byte length form is supported (values 0–127); long form not needed // for OID strings of this length. const declaredLength = bytes[1]!; if (declaredLength > 127) { throw new Error( `UTF8String TLV uses long-form length (0x${declaredLength.toString(16).toUpperCase()}), which is not supported`, ); } // Declared length must match actual remaining bytes if (declaredLength !== bytes.length - 2) { throw new Error( `UTF8String TLV length mismatch: declared ${declaredLength}, actual ${bytes.length - 2}`, ); } // Skip: tag (1 byte) + length (1 byte) return decoder.decode(bytes.slice(2)); } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /** * Extract Mosaic custom OIDs (grantId, subjectUserId) from an X.509 certificate * already parsed via @peculiar/x509. * * Returns `{ ok: true, value: MosaicOids }` on success, or * `{ ok: false, error: , detail? }` on any failure — never throws. */ export function extractMosaicOids(cert: X509Certificate): OidExtractionResult { try { const grantIdExt = cert.getExtension(OID_MOSAIC_GRANT_ID); if (!grantIdExt) { return { ok: false, error: 'MISSING_GRANT_ID' }; } const subjectUserIdExt = cert.getExtension(OID_MOSAIC_SUBJECT_USER_ID); if (!subjectUserIdExt) { return { ok: false, error: 'MISSING_SUBJECT_USER_ID' }; } const grantId = decodeUtf8StringTlv(grantIdExt.value); const subjectUserId = decodeUtf8StringTlv(subjectUserIdExt.value); return { ok: true, value: { grantId, subjectUserId }, }; } catch (err) { return { ok: false, error: 'PARSE_ERROR', detail: err instanceof Error ? err.message : String(err), }; } } /** * Parse a PEM-encoded certificate and extract Mosaic OIDs. * Returns an OidExtractionResult — never throws. */ export function extractMosaicOidsFromPem(certPem: string): OidExtractionResult { let cert: X509Certificate; try { cert = new X509Certificate(certPem); } catch (err) { return { ok: false, error: 'PARSE_ERROR', detail: err instanceof Error ? err.message : String(err), }; } return extractMosaicOids(cert); }