From f6d2dc456e83a04e954095723138876106f333f1 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sun, 24 Dec 2023 21:49:08 -0800 Subject: [PATCH] server: fix bug with constantly reissuing certs --- server/.vscode/launch.json | 2 +- server/package-lock.json | 4 +-- server/src/cert.ts | 55 ++++++++++++++++++++---------- server/src/scrypted-server-main.ts | 11 +++--- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/server/.vscode/launch.json b/server/.vscode/launch.json index 4ea9286b72..99e468dd23 100644 --- a/server/.vscode/launch.json +++ b/server/.vscode/launch.json @@ -29,7 +29,7 @@ "env": { // force usage of system python because brew python is 3.11 // which has no wheels for coreml tools or tflite-runtime - "SCRYPTED_PYTHON_PATH": "/usr/bin/python3", + // "SCRYPTED_PYTHON_PATH": "/usr/bin/python3", // "SCRYPTED_SHARED_WORKER": "true", // "SCRYPTED_DISABLE_AUTHENTICATION": "true", // "DEBUG": "*", diff --git a/server/package-lock.json b/server/package-lock.json index d0628379df..d9a63f4282 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/server", - "version": "0.72.0", + "version": "0.73.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@scrypted/server", - "version": "0.72.0", + "version": "0.73.0", "license": "ISC", "dependencies": { "@mapbox/node-pre-gyp": "^1.0.11", diff --git a/server/src/cert.ts b/server/src/cert.ts index 0bcf58be84..4ddbfca7d5 100644 --- a/server/src/cert.ts +++ b/server/src/cert.ts @@ -6,37 +6,56 @@ const { pki } = forge; export const CURRENT_SELF_SIGNED_CERTIFICATE_VERSION = 'v2'; +const SIXTY_DAYS_MS = 60 * 24 * 60 * 60 * 1000; -export function createSelfSignedCertificate(serviceKey?: string) { - let privateKey: ReturnType; - const cert = pki.createCertificate(); +export interface SelfSignedCertificate { + serviceKey: string; + certificate: string; + version: string, +}; - if (serviceKey) { - privateKey = pki.privateKeyFromPem(serviceKey); - cert.publicKey = pki.rsa.setPublicKey(privateKey.n, privateKey.e); +export function createSelfSignedCertificate(existing?: SelfSignedCertificate): SelfSignedCertificate { + let serviceKey: ReturnType; + // check if existing key is usable + if (existing?.certificate && existing?.serviceKey && existing?.version === CURRENT_SELF_SIGNED_CERTIFICATE_VERSION) { + try { + const certificate = pki.certificateFromPem(existing.certificate); + if (certificate.validity.notAfter.getTime() > Date.now() + SIXTY_DAYS_MS) + return existing; + serviceKey = pki.privateKeyFromPem(existing.serviceKey); + } + catch (e) { + } + } + + const certificate = pki.createCertificate(); + + if (existing?.serviceKey) { + certificate.publicKey = pki.rsa.setPublicKey(serviceKey.n, serviceKey.e); } else { // generate a keypair and create an X.509v3 certificate const keys = pki.rsa.generateKeyPair(2048); - privateKey = keys.privateKey; - cert.publicKey = keys.publicKey; + serviceKey = keys.privateKey; + certificate.publicKey = keys.publicKey; } + // NOTE: serialNumber is the hex encoded value of an ASN.1 INTEGER. // Conforming CAs should ensure serialNumber is: // - no more than 20 octets // - non-negative (prefix a '00' if your value starts with a '1' bit) - cert.serialNumber = '01' + crypto.randomBytes(19).toString("hex"); // 1 octet = 8 bits = 1 byte = 2 hex chars - cert.validity.notBefore = new Date(); - cert.validity.notAfter = new Date(); - cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); // adding 1 year of validity from now + certificate.serialNumber = '01' + crypto.randomBytes(19).toString("hex"); // 1 octet = 8 bits = 1 byte = 2 hex chars + certificate.validity.notBefore = new Date(); + certificate.validity.notAfter = new Date(); + certificate.validity.notAfter.setFullYear(certificate.validity.notBefore.getFullYear() + 5); // adding 5 years of validity from now const attrs = [{ name: 'commonName', value: 'localhost' }]; - cert.setSubject(attrs); - cert.setIssuer(attrs); - cert.setExtensions([{ + certificate.setSubject(attrs); + certificate.setIssuer(attrs); + certificate.setExtensions([{ name: 'basicConstraints', cA: true }, { @@ -73,10 +92,10 @@ export function createSelfSignedCertificate(serviceKey?: string) { }]); // self-sign certificate - cert.sign(privateKey); + certificate.sign(serviceKey); return { - serviceKey: pki.privateKeyToPem(privateKey), - certificate: pki.certificateToPem(cert), + serviceKey: pki.privateKeyToPem(serviceKey), + certificate: pki.certificateToPem(certificate), version: CURRENT_SELF_SIGNED_CERTIFICATE_VERSION, }; } diff --git a/server/src/scrypted-server-main.ts b/server/src/scrypted-server-main.ts index f77e91c854..e757487b5f 100644 --- a/server/src/scrypted-server-main.ts +++ b/server/src/scrypted-server-main.ts @@ -128,15 +128,14 @@ async function start(mainFilename: string, options?: { if (certSetting?.value?.version !== CURRENT_SELF_SIGNED_CERTIFICATE_VERSION) { keyPair = createSelfSignedCertificate(); - - certSetting = new Settings(); - certSetting._id = 'certificate'; - certSetting.value = keyPair; - certSetting = await db.upsert(certSetting); } else { - keyPair = createSelfSignedCertificate(keyPair.serviceKey); + keyPair = createSelfSignedCertificate(keyPair); } + certSetting = new Settings(); + certSetting._id = 'certificate'; + certSetting.value = keyPair; + certSetting = await db.upsert(certSetting); const basicAuth = httpAuth.basic({ realm: 'Scrypted',