mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #2025 from onurguzel/fix-cert-expiration-date
fix: parse pem certificates correctly
This commit is contained in:
@@ -1,80 +1,89 @@
|
|||||||
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++) {
|
||||||
// Found "not after" date
|
len = (len << 8) + der[pos++];
|
||||||
let dateStr = "";
|
|
||||||
for (let k = 0; k < dateLength; k++) {
|
|
||||||
const charCode = bytes[j + 2 + k];
|
|
||||||
if (typeof charCode === "undefined") continue;
|
|
||||||
dateStr += String.fromCharCode(charCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateType === 0x17) {
|
|
||||||
// UTCTime (YYMMDDhhmmssZ)
|
|
||||||
const year = Number.parseInt(dateStr.slice(0, 2));
|
|
||||||
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
|
|
||||||
return new Date(
|
|
||||||
Date.UTC(
|
|
||||||
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)
|
|
||||||
return new Date(
|
|
||||||
Date.UTC(
|
|
||||||
Number.parseInt(dateStr.slice(0, 4)),
|
|
||||||
Number.parseInt(dateStr.slice(4, 6)) - 1,
|
|
||||||
Number.parseInt(dateStr.slice(6, 8)),
|
|
||||||
Number.parseInt(dateStr.slice(8, 10)),
|
|
||||||
Number.parseInt(dateStr.slice(10, 12)),
|
|
||||||
Number.parseInt(dateStr.slice(12, 14)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
j++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { length: len, offset: pos };
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
// Skip the outer certificate sequence
|
||||||
|
if (der[offset++] !== 0x30) throw new Error("Expected sequence");
|
||||||
|
({ offset } = readLength(offset));
|
||||||
|
|
||||||
|
// Skip tbsCertificate sequence
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip serialNumber, signature, issuer
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (der[offset] !== 0x30 && der[offset] !== 0x02)
|
||||||
|
throw new Error("Unexpected structure");
|
||||||
|
offset++;
|
||||||
|
const fieldLen = readLength(offset);
|
||||||
|
offset = fieldLen.offset + fieldLen.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validity sequence (notBefore and notAfter)
|
||||||
|
if (der[offset++] !== 0x30) throw new Error("Expected validity sequence");
|
||||||
|
const validityLen = readLength(offset);
|
||||||
|
offset = validityLen.offset;
|
||||||
|
|
||||||
|
// notBefore
|
||||||
|
offset++;
|
||||||
|
const notBeforeLen = readLength(offset);
|
||||||
|
offset = notBeforeLen.offset + notBeforeLen.length;
|
||||||
|
|
||||||
|
// notAfter
|
||||||
|
offset++;
|
||||||
|
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 parseTime(notAfterStr);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error parsing certificate:", error);
|
console.error("Error parsing certificate:", error);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user