fix: parse pem certificates correctly

This commit is contained in:
Onur Güzel
2025-06-10 16:10:19 +02:00
parent 1089a8247d
commit 556a437251

View File

@@ -1,80 +1,82 @@
export const extractExpirationDate = (certData: string): Date | null => { export const extractExpirationDate = (certData: string): Date | null => {
try { try {
const match = certData.match( // Decode PEM base64 to DER binary
/-----BEGIN CERTIFICATE-----\s*([^-]+)\s*-----END CERTIFICATE-----/, const b64 = certData.replace(/-----[^-]+-----/g, '').replace(/\s+/g, '');
); const binStr = atob(b64);
if (!match?.[1]) return null; const der = new Uint8Array(binStr.length);
for (let i = 0; i < binStr.length; i++) {
const base64Cert = match[1].replace(/\s/g, ""); der[i] = binStr.charCodeAt(i);
const binaryStr = window.atob(base64Cert);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i);
} }
// ASN.1 tag for UTCTime is 0x17, GeneralizedTime is 0x18 let offset = 0;
// We need to find the second occurrence of either tag as it's the "not after" (expiration) date
let dateFound = false;
for (let i = 0; i < bytes.length - 2; i++) {
// Look for sequence containing validity period (0x30)
if (bytes[i] === 0x30) {
// Check next bytes for UTCTime or GeneralizedTime
let j = i + 1;
while (j < bytes.length - 2) {
if (bytes[j] === 0x17 || bytes[j] === 0x18) {
const dateType = bytes[j];
const dateLength = bytes[j + 1];
if (typeof dateLength === "undefined") break;
if (!dateFound) { // Helper: read ASN.1 length field
// Skip "not before" date function readLength(pos: number): { length: number; offset: number } {
dateFound = true; let len = der[pos++];
j += dateLength + 2; if (len & 0x80) {
continue; const bytes = len & 0x7f;
len = 0;
for (let i = 0; i < bytes; i++) {
len = (len << 8) + der[pos++];
}
}
return { length: len, offset: pos };
} }
// Found "not after" date // Skip the outer certificate sequence
let dateStr = ""; if (der[offset++] !== 0x30) throw new Error("Expected sequence");
for (let k = 0; k < dateLength; k++) { ({ offset } = readLength(offset));
const charCode = bytes[j + 2 + k];
if (typeof charCode === "undefined") continue; // Skip tbsCertificate sequence
dateStr += String.fromCharCode(charCode); if (der[offset++] !== 0x30) throw new Error("Expected tbsCertificate");
({ offset } = readLength(offset));
// Check for optional version field (context-specific tag [0])
if (der[offset] === 0xA0) {
offset++;
const versionLen = readLength(offset);
offset = versionLen.offset + versionLen.length;
} }
if (dateType === 0x17) { // Skip serialNumber, signature, issuer
// UTCTime (YYMMDDhhmmssZ) for (let i = 0; i < 3; i++) {
const year = Number.parseInt(dateStr.slice(0, 2)); if (der[offset] !== 0x30 && der[offset] !== 0x02) throw new Error("Unexpected structure");
const fullYear = year >= 50 ? 1900 + year : 2000 + year; offset++;
return new Date( const fieldLen = readLength(offset);
Date.UTC( offset = fieldLen.offset + fieldLen.length;
fullYear,
Number.parseInt(dateStr.slice(2, 4)) - 1,
Number.parseInt(dateStr.slice(4, 6)),
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
),
);
} }
// GeneralizedTime (YYYYMMDDhhmmssZ) // Validity sequence (notBefore and notAfter)
return new Date( if (der[offset++] !== 0x30) throw new Error("Expected validity sequence");
Date.UTC( const validityLen = readLength(offset);
Number.parseInt(dateStr.slice(0, 4)), offset = validityLen.offset;
Number.parseInt(dateStr.slice(4, 6)) - 1,
Number.parseInt(dateStr.slice(6, 8)), // notBefore
Number.parseInt(dateStr.slice(8, 10)), offset++;
Number.parseInt(dateStr.slice(10, 12)), const notBeforeLen = readLength(offset);
Number.parseInt(dateStr.slice(12, 14)), offset = notBeforeLen.offset + notBeforeLen.length;
),
); // notAfter
} offset++;
j++; const notAfterLen = readLength(offset);
const notAfterStr = new TextDecoder().decode(der.slice(notAfterLen.offset, notAfterLen.offset + notAfterLen.length));
// Parse GeneralizedTime (15 chars) or UTCTime (13 chars)
function parseTime(str: string): Date {
if (str.length === 13) {
// UTCTime YYMMDDhhmmssZ
const year = parseInt(str.slice(0, 2), 10);
const fullYear = year < 50 ? 2000 + year : 1900 + year;
return new Date(`${fullYear}-${str.slice(2, 4)}-${str.slice(4, 6)}T${str.slice(6, 8)}:${str.slice(8, 10)}:${str.slice(10, 12)}Z`);
} else if (str.length === 15) {
// GeneralizedTime YYYYMMDDhhmmssZ
return new Date(`${str.slice(0, 4)}-${str.slice(4, 6)}-${str.slice(6, 8)}T${str.slice(8, 10)}:${str.slice(10, 12)}:${str.slice(12, 14)}Z`);
} else {
throw new Error("Invalid ASN.1 time format");
} }
} }
}
return null; return parseTime(notAfterStr);
} catch (error) { } catch (error) {
console.error("Error parsing certificate:", error); console.error("Error parsing certificate:", error);
return null; return null;