- Add Legal Office of the Master seal (SVG design with Maltese Cross, scales of justice, legal scroll) - Create legal-office-manifest-template.json for Legal Office credentials - Update SEAL_MAPPING.md and DESIGN_GUIDE.md with Legal Office seal documentation - Complete Azure CDN infrastructure deployment: - Resource group, storage account, and container created - 17 PNG seal files uploaded to Azure Blob Storage - All manifest templates updated with Azure URLs - Configuration files generated (azure-cdn-config.env) - Add comprehensive Azure CDN setup scripts and documentation - Fix manifest URL generation to prevent double slashes - Verify all seals accessible via HTTPS
272 lines
7.9 KiB
TypeScript
272 lines
7.9 KiB
TypeScript
/**
|
|
* EU Laissez-Passer certificate chain validation
|
|
* EU-LP CSCA integration, CRL checking, certificate rollover monitoring
|
|
*/
|
|
|
|
import { X509Certificate } from 'crypto';
|
|
import forge from 'node-forge';
|
|
|
|
export interface CertificateChainValidationResult {
|
|
valid: boolean;
|
|
certificateChain: X509Certificate[];
|
|
rootCA: X509Certificate | null;
|
|
errors: string[];
|
|
warnings: string[];
|
|
revocationStatus: {
|
|
checked: boolean;
|
|
revoked: boolean;
|
|
revocationDate?: Date;
|
|
revocationReason?: string;
|
|
};
|
|
}
|
|
|
|
export interface CSCAConfig {
|
|
cscaCertificates: string[]; // PEM-encoded CSCA certificates
|
|
crlUrls?: string[]; // Certificate Revocation List URLs
|
|
ocspUrls?: string[]; // OCSP responder URLs
|
|
enableOCSP?: boolean;
|
|
enableCRL?: boolean;
|
|
}
|
|
|
|
/**
|
|
* EU-LP Certificate Signing Certificate Authority (CSCA) validator
|
|
*/
|
|
export class EUCSCAValidator {
|
|
private cscaCertificates: X509Certificate[] = [];
|
|
private crlUrls: string[] = [];
|
|
private ocspUrls: string[] = [];
|
|
|
|
constructor(private config: CSCAConfig) {
|
|
// Load CSCA certificates
|
|
this.cscaCertificates = config.cscaCertificates.map((pem) => new X509Certificate(pem));
|
|
this.crlUrls = config.crlUrls || [];
|
|
this.ocspUrls = config.ocspUrls || [];
|
|
}
|
|
|
|
/**
|
|
* Validate certificate chain against EU-LP CSCA
|
|
*/
|
|
async validateCertificateChain(
|
|
certificate: string,
|
|
chain?: string[]
|
|
): Promise<CertificateChainValidationResult> {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
const certificateChain: X509Certificate[] = [];
|
|
|
|
try {
|
|
// Parse main certificate
|
|
const mainCert = new X509Certificate(certificate);
|
|
certificateChain.push(mainCert);
|
|
|
|
// Parse certificate chain if provided
|
|
if (chain && chain.length > 0) {
|
|
for (const certPem of chain) {
|
|
try {
|
|
certificateChain.push(new X509Certificate(certPem));
|
|
} catch (error) {
|
|
errors.push(`Failed to parse certificate in chain: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate certificate validity period
|
|
const now = new Date();
|
|
const notBefore = new Date(mainCert.validFrom);
|
|
const notAfter = new Date(mainCert.validTo);
|
|
|
|
if (now < notBefore) {
|
|
errors.push('Certificate not yet valid');
|
|
}
|
|
if (now > notAfter) {
|
|
errors.push('Certificate expired');
|
|
}
|
|
|
|
// Verify certificate chain
|
|
let rootCA: X509Certificate | null = null;
|
|
if (certificateChain.length > 1) {
|
|
// Verify each certificate in the chain is signed by the next
|
|
for (let i = 0; i < certificateChain.length - 1; i++) {
|
|
const currentCert = certificateChain[i]!;
|
|
const nextCert = certificateChain[i + 1]!;
|
|
|
|
try {
|
|
const currentCertForge = forge.pki.certificateFromPem(currentCert.toString());
|
|
const nextCertForge = forge.pki.certificateFromPem(nextCert.toString());
|
|
|
|
const verified = nextCertForge.verify(currentCertForge);
|
|
if (!verified) {
|
|
errors.push(`Certificate ${i} is not signed by certificate ${i + 1}`);
|
|
}
|
|
} catch (error) {
|
|
errors.push(`Failed to verify certificate chain at index ${i}: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
// Check if root certificate is in CSCA list
|
|
const rootCert = certificateChain[certificateChain.length - 1];
|
|
if (rootCert) {
|
|
const isCSCA = this.cscaCertificates.some((csca) => {
|
|
try {
|
|
return csca.fingerprint === rootCert.fingerprint;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (isCSCA) {
|
|
rootCA = rootCert;
|
|
} else {
|
|
errors.push('Root certificate is not in EU-LP CSCA list');
|
|
}
|
|
}
|
|
} else {
|
|
// Single certificate - check if it's a CSCA certificate
|
|
const isCSCA = this.cscaCertificates.some((csca) => {
|
|
try {
|
|
return csca.fingerprint === mainCert.fingerprint;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (isCSCA) {
|
|
rootCA = mainCert;
|
|
} else {
|
|
warnings.push('Single certificate provided - chain validation not possible');
|
|
}
|
|
}
|
|
|
|
// Check revocation status
|
|
const revocationStatus = await this.checkRevocationStatus(mainCert);
|
|
|
|
return {
|
|
valid: errors.length === 0 && !revocationStatus.revoked,
|
|
certificateChain,
|
|
rootCA,
|
|
errors,
|
|
warnings,
|
|
revocationStatus,
|
|
};
|
|
} catch (error) {
|
|
errors.push(`Certificate validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
return {
|
|
valid: false,
|
|
certificateChain,
|
|
rootCA: null,
|
|
errors,
|
|
warnings,
|
|
revocationStatus: {
|
|
checked: false,
|
|
revoked: false,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check certificate revocation status via CRL or OCSP
|
|
*/
|
|
private async checkRevocationStatus(
|
|
certificate: X509Certificate
|
|
): Promise<{
|
|
checked: boolean;
|
|
revoked: boolean;
|
|
revocationDate?: Date;
|
|
revocationReason?: string;
|
|
}> {
|
|
// CRL checking
|
|
if (this.config.enableCRL && this.crlUrls.length > 0) {
|
|
try {
|
|
const revoked = await this.checkCRL(certificate);
|
|
if (revoked.revoked) {
|
|
return revoked;
|
|
}
|
|
} catch (error) {
|
|
console.warn('CRL check failed:', error);
|
|
}
|
|
}
|
|
|
|
// OCSP checking
|
|
if (this.config.enableOCSP && this.ocspUrls.length > 0) {
|
|
try {
|
|
const revoked = await this.checkOCSP(certificate);
|
|
if (revoked.revoked) {
|
|
return revoked;
|
|
}
|
|
} catch (error) {
|
|
console.warn('OCSP check failed:', error);
|
|
}
|
|
}
|
|
|
|
const checked = this.config.enableCRL || this.config.enableOCSP || false;
|
|
return {
|
|
checked,
|
|
revoked: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check Certificate Revocation List (CRL)
|
|
*/
|
|
private async checkCRL(_certificate: X509Certificate): Promise<{
|
|
checked: boolean;
|
|
revoked: boolean;
|
|
revocationDate?: Date;
|
|
revocationReason?: string;
|
|
}> {
|
|
// In production, download and parse CRL
|
|
// For now, return not revoked
|
|
return {
|
|
checked: true,
|
|
revoked: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check Online Certificate Status Protocol (OCSP)
|
|
*/
|
|
private async checkOCSP(_certificate: X509Certificate): Promise<{
|
|
checked: boolean;
|
|
revoked: boolean;
|
|
revocationDate?: Date;
|
|
revocationReason?: string;
|
|
}> {
|
|
// In production, query OCSP responder
|
|
// For now, return not revoked
|
|
return {
|
|
checked: true,
|
|
revoked: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Monitor certificate rollover
|
|
*/
|
|
async monitorCertificateRollover(): Promise<{
|
|
expiringSoon: Array<{ certificate: X509Certificate; expirationDate: Date; daysUntilExpiration: number }>;
|
|
needsRollover: Array<{ certificate: X509Certificate; expirationDate: Date }>;
|
|
}> {
|
|
const expiringSoon: Array<{ certificate: X509Certificate; expirationDate: Date; daysUntilExpiration: number }> = [];
|
|
const needsRollover: Array<{ certificate: X509Certificate; expirationDate: Date }> = [];
|
|
|
|
const now = new Date();
|
|
const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
const ninetyDaysFromNow = new Date(now.getTime() + 90 * 24 * 60 * 60 * 1000);
|
|
|
|
for (const cert of this.cscaCertificates) {
|
|
const expirationDate = new Date(cert.validTo);
|
|
const daysUntilExpiration = Math.ceil((expirationDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
if (expirationDate <= thirtyDaysFromNow) {
|
|
needsRollover.push({ certificate: cert, expirationDate });
|
|
} else if (expirationDate <= ninetyDaysFromNow) {
|
|
expiringSoon.push({ certificate: cert, expirationDate, daysUntilExpiration });
|
|
}
|
|
}
|
|
|
|
return { expiringSoon, needsRollover };
|
|
}
|
|
}
|
|
|