-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(certificates): implement certificate manager
After the implementation from cloudmos refs #76
- Loading branch information
1 parent
4657dc5
commit 4e495d6
Showing
6 changed files
with
265 additions
and
10 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
101 changes: 101 additions & 0 deletions
101
src/certificates/certificate-manager/CertificateManager.spec.ts
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 |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { faker } from "@faker-js/faker"; | ||
|
||
import { CertificateManager } from "./CertificateManager"; | ||
|
||
describe("CertificateManager", () => { | ||
let certificateManager: CertificateManager; | ||
let address: string; | ||
|
||
beforeEach(() => { | ||
certificateManager = new CertificateManager(); | ||
address = `akash1${faker.string.alpha({ length: 38 })}`; | ||
}); | ||
|
||
describe("prototype.generateCertificate", () => { | ||
it("should generate certificate PEMs with the default validity range", () => { | ||
const now = new Date(); | ||
now.setMilliseconds(0); | ||
const inOneYear = getTime1yearFrom(now); | ||
const pem = certificateManager.generatePEM(address); | ||
const meta = certificateManager.parsePem(pem.certKey); | ||
|
||
expect(pem).toMatchObject({ | ||
certKey: expect.stringMatching(/^-----BEGIN CERTIFICATE-----[\s\S]*-----END CERTIFICATE-----\r\n$/), | ||
publicKey: expect.stringMatching(/^-----BEGIN EC PUBLIC KEY-----[\s\S]*-----END EC PUBLIC KEY-----\r\n$/), | ||
privateKey: expect.stringMatching(/^-----BEGIN PRIVATE KEY-----[\s\S]*-----END PRIVATE KEY-----\r\n$/) | ||
}); | ||
expect(new Date(meta.issuedOn).getTime()).toBeGreaterThanOrEqual(now.getTime()); | ||
expect(new Date(meta.issuedOn).getTime()).toBeLessThan(new Date().getTime()); | ||
expect(new Date(meta.expiresOn).getTime()).toBeGreaterThanOrEqual(inOneYear); | ||
expect(new Date(meta.issuedOn).getTime()).toBeLessThan(getTime1yearFrom(new Date())); | ||
}); | ||
}); | ||
|
||
it("should generate certificate PEMs with the provided validity range", () => { | ||
const now = new Date(); | ||
const MONTH = 1000 * 60 * 60 * 24 * 30; | ||
const inOneMonth = new Date(now.getTime() + MONTH); | ||
const inTwoMonths = new Date(now.getTime() + MONTH * 2); | ||
const pem = certificateManager.generatePEM(address, { | ||
validFrom: inOneMonth, | ||
validTo: inTwoMonths | ||
}); | ||
const meta = certificateManager.parsePem(pem.certKey); | ||
|
||
expect(pem).toMatchObject({ | ||
certKey: expect.stringMatching(/^-----BEGIN CERTIFICATE-----[\s\S]*-----END CERTIFICATE-----\r\n$/), | ||
publicKey: expect.stringMatching(/^-----BEGIN EC PUBLIC KEY-----[\s\S]*-----END EC PUBLIC KEY-----\r\n$/), | ||
privateKey: expect.stringMatching(/^-----BEGIN PRIVATE KEY-----[\s\S]*-----END PRIVATE KEY-----\r\n$/) | ||
}); | ||
|
||
inOneMonth.setMilliseconds(0); | ||
inTwoMonths.setMilliseconds(0); | ||
expect(new Date(meta.issuedOn).getTime()).toBe(inOneMonth.getTime()); | ||
expect(new Date(meta.expiresOn).getTime()).toBe(inTwoMonths.getTime()); | ||
}); | ||
|
||
describe("prototype.parsePem", () => { | ||
it("should extract certificate data", () => { | ||
const cert = certificateManager.parsePem(certificateManager.generatePEM(address).certKey); | ||
|
||
expect(cert).toMatchObject({ | ||
hSerial: expect.any(String), | ||
sIssuer: expect.any(String), | ||
sSubject: expect.any(String), | ||
sNotBefore: expect.any(String), | ||
sNotAfter: expect.any(String), | ||
issuedOn: expect.any(Date), | ||
expiresOn: expect.any(Date) | ||
}); | ||
}); | ||
}); | ||
|
||
describe("prototype.strToDate", () => { | ||
it("should convert string to date", () => { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-expect-error | ||
const date = certificateManager.strToDate("240507122350Z"); | ||
|
||
expect(date).toBeInstanceOf(Date); | ||
expect(date.toISOString()).toBe("2024-05-07T12:23:50.000Z"); | ||
}); | ||
}); | ||
|
||
describe("prototype.dateToStr", () => { | ||
it("should convert date to string", () => { | ||
const date = new Date("2024-05-07T12:23:50.000Z"); | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-expect-error | ||
const str = certificateManager.dateToStr(date); | ||
|
||
expect(str).toBe("240507122350Z"); | ||
}); | ||
}); | ||
}); | ||
|
||
function getTime1yearFrom(date = new Date()): number { | ||
const clone = new Date(date); | ||
const inOneYear = new Date(date); | ||
inOneYear.setFullYear(clone.getFullYear() + 1); | ||
return inOneYear.getTime(); | ||
} |
114 changes: 114 additions & 0 deletions
114
src/certificates/certificate-manager/CertificateManager.ts
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 |
---|---|---|
@@ -0,0 +1,114 @@ | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-expect-error | ||
import rs from "jsrsasign"; | ||
|
||
export interface CertificatePem { | ||
certKey: string; | ||
publicKey: string; | ||
privateKey: string; | ||
} | ||
|
||
export interface CertificateInfo { | ||
hSerial: string; | ||
sIssuer: string; | ||
sSubject: string; | ||
sNotBefore: string; | ||
sNotAfter: string; | ||
issuedOn: Date; | ||
expiresOn: Date; | ||
} | ||
|
||
export interface ValidityRangeOptions { | ||
validFrom?: Date; | ||
validTo?: Date; | ||
} | ||
|
||
export class CertificateManager { | ||
parsePem(certPEM: string): CertificateInfo { | ||
const certificate = new rs.X509(); | ||
certificate.readCertPEM(certPEM); | ||
const hSerial: string = certificate.getSerialNumberHex(); | ||
const sIssuer: string = certificate.getIssuerString(); | ||
const sSubject: string = certificate.getSubjectString(); | ||
const sNotBefore: string = certificate.getNotBefore(); | ||
const sNotAfter: string = certificate.getNotAfter(); | ||
|
||
return { | ||
hSerial, | ||
sIssuer, | ||
sSubject, | ||
sNotBefore, | ||
sNotAfter, | ||
issuedOn: this.strToDate(sNotBefore), | ||
expiresOn: this.strToDate(sNotAfter) | ||
}; | ||
} | ||
|
||
generatePEM(address: string, options?: ValidityRangeOptions): CertificatePem { | ||
const { notBeforeStr, notAfterStr } = this.createValidityRange(options); | ||
const { prvKeyObj, pubKeyObj } = rs.KEYUTIL.generateKeypair("EC", "secp256r1"); | ||
const cert = new rs.KJUR.asn1.x509.Certificate({ | ||
version: 3, | ||
serial: { int: Math.floor(new Date().getTime() * 1000) }, | ||
issuer: { str: "/CN=" + address }, | ||
notbefore: notBeforeStr, | ||
notafter: notAfterStr, | ||
subject: { str: "/CN=" + address }, | ||
sbjpubkey: pubKeyObj, | ||
ext: [ | ||
{ extname: "keyUsage", critical: true, names: ["keyEncipherment", "dataEncipherment"] }, | ||
{ | ||
extname: "extKeyUsage", | ||
array: [{ name: "clientAuth" }] | ||
}, | ||
{ extname: "basicConstraints", cA: true, critical: true } | ||
], | ||
sigalg: "SHA256withECDSA", | ||
cakey: prvKeyObj | ||
}); | ||
const publicKey: string = rs.KEYUTIL.getPEM(pubKeyObj, "PKCS8PUB").replaceAll("PUBLIC KEY", "EC PUBLIC KEY"); | ||
const certKey: string = cert.getPEM(); | ||
|
||
return { | ||
certKey, | ||
publicKey, | ||
privateKey: rs.KEYUTIL.getPEM(prvKeyObj, "PKCS8PRV") | ||
}; | ||
} | ||
|
||
private createValidityRange(options?: ValidityRangeOptions) { | ||
const notBefore = options?.validFrom || new Date(); | ||
const notAfter = options?.validTo || new Date(); | ||
|
||
if (!options?.validTo) { | ||
notAfter.setFullYear(notBefore.getFullYear() + 1); | ||
} | ||
|
||
const notBeforeStr = this.dateToStr(notBefore); | ||
const notAfterStr = this.dateToStr(notAfter); | ||
|
||
return { notBeforeStr, notAfterStr }; | ||
} | ||
|
||
private dateToStr(date: Date): string { | ||
const year = date.getUTCFullYear().toString().substring(2).padStart(2, "0"); | ||
const month = (date.getUTCMonth() + 1).toString().padStart(2, "0"); | ||
const day = date.getUTCDate().toString().padStart(2, "0"); | ||
const hours = date.getUTCHours().toString().padStart(2, "0"); | ||
const minutes = date.getUTCMinutes().toString().padStart(2, "0"); | ||
const secs = date.getUTCSeconds().toString().padStart(2, "0"); | ||
|
||
return `${year}${month}${day}${hours}${minutes}${secs}Z`; | ||
} | ||
|
||
private strToDate(str: string): Date { | ||
const year = parseInt(`20${str.substring(0, 2)}`); | ||
const month = parseInt(str.substring(2, 4)) - 1; | ||
const day = parseInt(str.substring(4, 6)); | ||
const hours = parseInt(str.substring(6, 8)); | ||
const minutes = parseInt(str.substring(8, 10)); | ||
const secs = parseInt(str.substring(10, 12)); | ||
|
||
return new Date(Date.UTC(year, month, day, hours, minutes, secs)); | ||
} | ||
} |
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 |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { CertificateManager } from "./CertificateManager"; | ||
|
||
const certificateManager = new CertificateManager(); | ||
|
||
export { CertificateManager, certificateManager }; |
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