-
-
Notifications
You must be signed in to change notification settings - Fork 444
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: use node-forge to generate private key and certificate
- Loading branch information
Showing
11 changed files
with
180 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,68 +1,73 @@ | ||
import { spawn } from 'node:child_process'; | ||
import { writeFile, readFile, unlink } from 'node:fs/promises'; | ||
import { tmpdir } from 'node:os'; | ||
// eslint-disable-next-line unicorn/import-style | ||
import { join } from 'node:path'; | ||
import crypto from 'node:crypto'; | ||
|
||
import { generatePrivateKey } from '@logto/cli/lib/commands/database/utils.js'; | ||
import { SupportedSigningKeyAlgorithm } from '@logto/schemas'; | ||
import { addDays } from 'date-fns'; | ||
import forge from 'node-forge'; | ||
|
||
const generateCertificate = async (privateKeyPem: string) => { | ||
const temporaryDirectory = tmpdir(); | ||
const privateKeyPath = join(temporaryDirectory, 'private.key'); | ||
const certPath = join(temporaryDirectory, 'cert.pem'); | ||
|
||
await writeFile(privateKeyPath, privateKeyPem); | ||
|
||
const opensslResult = await new Promise<{ status: number | undefined; stderr: Uint8Array }>( | ||
(resolve, reject) => { | ||
const process = spawn('openssl', [ | ||
'req', | ||
'-new', | ||
'-x509', | ||
'-key', | ||
privateKeyPath, | ||
'-out', | ||
certPath, | ||
'-days', | ||
'365', | ||
'-subj', | ||
'/CN=SAML Service Provider/O=Logto', | ||
]); | ||
|
||
const stderr = new Uint8Array(); | ||
process.stderr.on('data', (data: Uint8Array) => { | ||
stderr.set(new Uint8Array(data), stderr.length); | ||
}); | ||
export const generateKeyPairAndCertificate = async ( | ||
lifeSpanInDays = 365, | ||
type: SupportedSigningKeyAlgorithm = SupportedSigningKeyAlgorithm.RSA | ||
) => { | ||
if (type === SupportedSigningKeyAlgorithm.RSA) { | ||
const keypair = forge.pki.rsa.generateKeyPair({ bits: 4096 }); | ||
return createCertificate(keypair, lifeSpanInDays); | ||
} | ||
|
||
process.on('close', (status) => { | ||
// `conditional()` is not used here to avoid mistakenly treating status = 0 as undefined. | ||
resolve({ stderr, status: typeof status === 'number' ? status : undefined }); | ||
}); | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
if (type === SupportedSigningKeyAlgorithm.EC) { | ||
const ecdsa = forge.pki.ed25519; | ||
const keypair = ecdsa.generateKeyPair(); | ||
return createCertificate(keypair, lifeSpanInDays); | ||
} | ||
|
||
process.on('error', reject); | ||
} | ||
); | ||
throw new Error(`Unsupported key type ${String(type)}`); | ||
}; | ||
|
||
if (opensslResult.status !== 0) { | ||
throw new Error(`Failed to generate certificate: ${opensslResult.stderr.toString()}`); | ||
} | ||
const createCertificate = (keypair: forge.pki.KeyPair, lifeSpanInDays: number) => { | ||
const cert = forge.pki.createCertificate(); | ||
const notBefore = new Date(); | ||
const notAfter = addDays(notBefore, lifeSpanInDays); | ||
|
||
const certificate = await readFile(certPath, 'utf8'); | ||
// Can not initialize the certificate with the keypair directly, so we need to set the public key manually. | ||
/* eslint-disable @silverhand/fp/no-mutation */ | ||
cert.publicKey = keypair.publicKey; | ||
// Use cryptographically secure pseudorandom number generator (CSPRNG) to generate a random serial number (usually more than 8 bytes). | ||
// `serialNumber` should be IDENTICAL across different certificates, better not to be incremental. | ||
cert.serialNumber = crypto.randomBytes(16).toString('hex'); | ||
cert.validity.notBefore = notBefore; | ||
cert.validity.notAfter = notAfter; | ||
/* eslint-enable @silverhand/fp/no-mutation */ | ||
|
||
await Promise.all([unlink(privateKeyPath), unlink(certPath)]); | ||
// TODO: read from tenant config or let user customize before downloading | ||
const subjectAttributes: forge.pki.CertificateField[] = [ | ||
{ | ||
name: 'commonName', | ||
value: 'example.com', | ||
}, | ||
]; | ||
|
||
return certificate; | ||
}; | ||
const issuerAttributes: forge.pki.CertificateField[] = [ | ||
{ | ||
name: 'commonName', | ||
value: 'logto.io', | ||
}, | ||
{ | ||
name: 'organizationName', | ||
value: 'Logto', | ||
}, | ||
{ | ||
name: 'countryName', | ||
value: 'US', | ||
}, | ||
]; | ||
|
||
export const generateSamlCredentials = async ( | ||
algorithm: SupportedSigningKeyAlgorithm = SupportedSigningKeyAlgorithm.EC | ||
) => { | ||
const privateKey = await generatePrivateKey(algorithm); | ||
const certificate = await generateCertificate(privateKey); | ||
cert.setSubject(subjectAttributes); | ||
cert.setIssuer(issuerAttributes); | ||
cert.sign(keypair.privateKey); | ||
|
||
return { | ||
privateKey, | ||
certificate, | ||
privateKey: forge.pki.privateKeyToPem(keypair.privateKey), | ||
certificate: forge.pki.certificateToPem(cert), | ||
notAfter, | ||
}; | ||
}; | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.