Skip to content

Commit

Permalink
feat(certificates): implement certificate manager
Browse files Browse the repository at this point in the history
After the implementation from cloudmos

refs #76
  • Loading branch information
ygrishajev committed May 13, 2024
1 parent 0211266 commit bbdf668
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 10 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"cosmwasm": "^1.1.1",
"js-yaml": "^4.1.0",
"json-stable-stringify": "^1.0.2",
"jsrsasign": "^11.1.0",
"keytar": "^7.7.0",
"node-fetch": "2",
"pkijs": "^3.0.0",
Expand Down
61 changes: 61 additions & 0 deletions src/certificates/certificate-manager/CertificateManager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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", () => {
expect(certificateManager.generatePEM(address)).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$/)
});
});
});

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");
});
});
});
106 changes: 106 additions & 0 deletions src/certificates/certificate-manager/CertificateManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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 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): CertificatePem {
const { notBeforeStr, notAfterStr } = this.createValidityRange();
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() {
const notBefore = new Date();
const notAfter = new Date();
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));
}
}
5 changes: 5 additions & 0 deletions src/certificates/certificate-manager/index.ts
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 };
Empty file.
45 changes: 35 additions & 10 deletions src/certificates/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import { create as create509, pems } from "./generate509";
import { SigningStargateClient } from "@cosmjs/stargate";
import { DeliverTxResponse } from "@cosmjs/stargate/build/stargateclient";
import { toBase64 } from "pvutils";
import { CertificateFilter, QueryCertificatesRequest, QueryCertificatesResponse } from "@akashnetwork/akash-api/akash/cert/v1beta3";

import type { pems } from "./generate509";
import { Message as stargateMessages } from "../stargate";
import { createStarGateMessage } from "../pbclient/pbclient";

import { QueryCertificatesRequest, QueryCertificatesResponse, CertificateFilter } from "@akashnetwork/akash-api/akash/cert/v1beta3";
import type { CertificatePem } from "./certificate-manager/CertificateManager";
import { certificateManager } from "./certificate-manager";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const JsonRPC = require("simple-jsonrpc-js");

import { toBase64 } from "pvutils";

const jrpc = JsonRPC.connect_xhr("https://bridge.testnet.akash.network/akashnetwork");

export type { pems };

export async function broadcastCertificate({ csr, publicKey }: pems, owner: string, client: SigningStargateClient) {
const encodedCsr = base64ToUInt(toBase64(csr));
const encdodedPublicKey = base64ToUInt(toBase64(publicKey));
export async function broadcastCertificate(
pem: Pick<CertificatePem, "certKey" | "publicKey">,
owner: string,
client: SigningStargateClient
): Promise<DeliverTxResponse>;
export async function broadcastCertificate(pem: pems, owner: string, client: SigningStargateClient): Promise<DeliverTxResponse>;
export async function broadcastCertificate(
pem: Pick<CertificatePem, "certKey" | "publicKey"> | pems,
owner: string,
client: SigningStargateClient
): Promise<DeliverTxResponse> {
if ("csr" in pem) {
console.warn("The `csr` field is deprecated. Use `certKey` instead.");
}
const certKey = "certKey" in pem ? pem.certKey : pem.csr;
const encodedCsr = base64ToUInt(toBase64(certKey));
const encdodedPublicKey = base64ToUInt(toBase64(pem.publicKey));
const message = createStarGateMessage(stargateMessages.MsgCreateCertificate, {
owner: owner,
cert: encodedCsr,
Expand All @@ -27,8 +43,17 @@ export async function broadcastCertificate({ csr, publicKey }: pems, owner: stri
}

export async function createCertificate(bech32Address: string) {
const certificate = create509(bech32Address);
return certificate;
const pem = certificateManager.generatePEM(bech32Address);

return {
get csr() {
console.warn("The `csr` field is deprecated. Use `certKey` instead.");
return pem.certKey;
},
certKey: pem.certKey,
publicKey: pem.publicKey,
privateKey: pem.privateKey
};
}

export async function revokeCertificate(owner: string, serial: string, client: SigningStargateClient) {
Expand Down

0 comments on commit bbdf668

Please sign in to comment.