feat(09-04): GREEN — implement splitCerts + SplitResponse interface
- Export SplitEntry + SplitResponse interfaces - splitCerts: PEM chain path via parsePemChain; P7B path via messageFromPem (PEM) or messageFromAsn1 (DER) - Each cert entry: index, filename cert-N.pem, content base64 PEM, subject.cn, validity.notAfter - BadRequestException on malformed input / unsupported format (T-09-01) - POST /split already wired in controller with 5MB file limit (T-09-03, T-09-04) - All 19 API tests green; type-check clean
This commit is contained in:
@@ -18,6 +18,24 @@ export interface CertDetails {
|
|||||||
pemPreview: string;
|
pemPreview: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SplitResponse — the structured result returned by splitCerts
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface SplitEntry {
|
||||||
|
index: number;
|
||||||
|
filename: string;
|
||||||
|
/** PEM content base64-encoded (one BEGIN CERTIFICATE block per entry) */
|
||||||
|
content: string;
|
||||||
|
subject: { cn: string };
|
||||||
|
validity: { notAfter: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitResponse {
|
||||||
|
count: number;
|
||||||
|
certs: SplitEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Reverse OID map (OID string -> human-readable algorithm name)
|
// Reverse OID map (OID string -> human-readable algorithm name)
|
||||||
// Built once at module load — node-forge's pki.oids is name->OID
|
// Built once at module load — node-forge's pki.oids is name->OID
|
||||||
@@ -263,11 +281,88 @@ export class CertManagerService {
|
|||||||
// Remaining operation stubs (implemented in later plan slices)
|
// Remaining operation stubs (implemented in later plan slices)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async splitCerts(_input: {
|
/**
|
||||||
|
* Split a fullchain PEM or P7B/PKCS7 bundle into individual certificates.
|
||||||
|
*
|
||||||
|
* Security contract (T-09-01):
|
||||||
|
* - All forge calls wrapped in try/catch → BadRequestException on malformed input
|
||||||
|
*
|
||||||
|
* Security contract (T-09-03):
|
||||||
|
* - File size limit 5 MB enforced by FileInterceptor in the controller
|
||||||
|
*/
|
||||||
|
async splitCerts(input: {
|
||||||
file?: any;
|
file?: any;
|
||||||
password?: string;
|
password?: string;
|
||||||
}): Promise<never> {
|
}): Promise<SplitResponse> {
|
||||||
throw new NotImplementedException('splitCerts is not yet implemented');
|
const { file } = input;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
throw new BadRequestException('No file provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
let certs: forge.pki.Certificate[];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const format = this.detectFormat(file.originalname as string, file.buffer as Buffer);
|
||||||
|
|
||||||
|
if (format === 'pem') {
|
||||||
|
// ── PEM chain (fullchain.pem, .crt — both map to 'pem' in detectFormat) ─
|
||||||
|
const pemStr = (file.buffer as Buffer).toString('utf-8');
|
||||||
|
certs = this.parsePemChain(pemStr);
|
||||||
|
if (certs.length === 0) {
|
||||||
|
throw new Error('No certificate blocks found in PEM file');
|
||||||
|
}
|
||||||
|
} else if (format === 'p7b') {
|
||||||
|
// ── P7B/PKCS7 bundle — PEM-wrapped or binary DER (Pitfall 4) ─────────
|
||||||
|
const isPemP7b = (file.buffer as Buffer)
|
||||||
|
.slice(0, 27)
|
||||||
|
.toString('ascii')
|
||||||
|
.includes('-----BEGIN');
|
||||||
|
let p7: any;
|
||||||
|
if (isPemP7b) {
|
||||||
|
// PEM-wrapped PKCS7 (e.g. -----BEGIN PKCS7-----)
|
||||||
|
p7 = forge.pkcs7.messageFromPem((file.buffer as Buffer).toString('utf-8'));
|
||||||
|
} else {
|
||||||
|
// Binary DER PKCS7
|
||||||
|
const p7Asn1 = forge.asn1.fromDer(this.toForgeBuffer(file.buffer as Buffer));
|
||||||
|
p7 = forge.pkcs7.messageFromAsn1(p7Asn1);
|
||||||
|
}
|
||||||
|
certs = (p7.certificates as forge.pki.Certificate[]) ?? [];
|
||||||
|
if (certs.length === 0) {
|
||||||
|
throw new Error('No certificates found in P7B/PKCS7 bundle');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// DER / PFX — not a valid chain/bundle format for splitting
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Only PEM chains (.pem, .crt) and P7B bundles (.p7b) can be split',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof BadRequestException) throw err;
|
||||||
|
this.logger.warn('splitCerts: failed to parse bundle');
|
||||||
|
throw new BadRequestException('Failed to split certificates: invalid format or corrupted file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build SplitResponse ────────────────────────────────────────────────
|
||||||
|
const certEntries: SplitEntry[] = certs.map((cert, index) => {
|
||||||
|
const pemStr = forge.pki.certificateToPem(cert);
|
||||||
|
const content = Buffer.from(pemStr, 'utf-8').toString('base64');
|
||||||
|
const cn: string = cert.subject.getField('CN')?.value ?? '';
|
||||||
|
const notAfter: string = cert.validity.notAfter.toISOString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
filename: `cert-${index + 1}.pem`,
|
||||||
|
content,
|
||||||
|
subject: { cn },
|
||||||
|
validity: { notAfter },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
count: certEntries.length,
|
||||||
|
certs: certEntries,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async mergeCerts(_input: {
|
async mergeCerts(_input: {
|
||||||
|
|||||||
Reference in New Issue
Block a user