From 556a4372519591c05f6e1798166ad6ea343b3556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20G=C3=BCzel?= <104548+onurguzel@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:10:19 +0200 Subject: [PATCH 1/2] fix: parse pem certificates correctly --- .../dashboard/settings/certificates/utils.ts | 144 +++++++++--------- 1 file changed, 73 insertions(+), 71 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/certificates/utils.ts b/apps/dokploy/components/dashboard/settings/certificates/utils.ts index 80f332d8d..5e4cce6d0 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/utils.ts +++ b/apps/dokploy/components/dashboard/settings/certificates/utils.ts @@ -1,80 +1,82 @@ export const extractExpirationDate = (certData: string): Date | null => { try { - const match = certData.match( - /-----BEGIN CERTIFICATE-----\s*([^-]+)\s*-----END CERTIFICATE-----/, - ); - if (!match?.[1]) return null; - - const base64Cert = match[1].replace(/\s/g, ""); - const binaryStr = window.atob(base64Cert); - const bytes = new Uint8Array(binaryStr.length); - - for (let i = 0; i < binaryStr.length; i++) { - bytes[i] = binaryStr.charCodeAt(i); + // Decode PEM base64 to DER binary + const b64 = certData.replace(/-----[^-]+-----/g, '').replace(/\s+/g, ''); + const binStr = atob(b64); + const der = new Uint8Array(binStr.length); + for (let i = 0; i < binStr.length; i++) { + der[i] = binStr.charCodeAt(i); } - - // ASN.1 tag for UTCTime is 0x17, GeneralizedTime is 0x18 - // 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) { - // Skip "not before" date - dateFound = true; - j += dateLength + 2; - continue; - } - - // Found "not after" date - 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++; + + let offset = 0; + + // Helper: read ASN.1 length field + function readLength(pos: number): { length: number; offset: number } { + let len = der[pos++]; + if (len & 0x80) { + const bytes = len & 0x7f; + len = 0; + for (let i = 0; i < bytes; i++) { + len = (len << 8) + der[pos++]; } } + 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) { console.error("Error parsing certificate:", error); return null; From b30f8944c4d53252f87480da613a5fffe498917b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 23:53:09 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- .../dashboard/settings/certificates/utils.ts | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/certificates/utils.ts b/apps/dokploy/components/dashboard/settings/certificates/utils.ts index 5e4cce6d0..716087db2 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/utils.ts +++ b/apps/dokploy/components/dashboard/settings/certificates/utils.ts @@ -1,15 +1,15 @@ export const extractExpirationDate = (certData: string): Date | null => { try { // Decode PEM base64 to DER binary - const b64 = certData.replace(/-----[^-]+-----/g, '').replace(/\s+/g, ''); + const b64 = certData.replace(/-----[^-]+-----/g, "").replace(/\s+/g, ""); const binStr = atob(b64); const der = new Uint8Array(binStr.length); for (let i = 0; i < binStr.length; i++) { der[i] = binStr.charCodeAt(i); } - + let offset = 0; - + // Helper: read ASN.1 length field function readLength(pos: number): { length: number; offset: number } { let len = der[pos++]; @@ -22,60 +22,67 @@ export const extractExpirationDate = (certData: string): Date | null => { } return { length: len, offset: pos }; } - + // 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) { + 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"); + 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)); - + 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`); + 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`); + 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) { console.error("Error parsing certificate:", error);