From cdd9ed43186b258a474adb252770ba0e11ed96c3 Mon Sep 17 00:00:00 2001 From: Plamen Hristov Date: Sun, 21 May 2023 15:29:44 +0400 Subject: [PATCH] ECDSA implementation (#3) --- configs/tsconfig.base.json | 2 +- src/constants.ts | 236 +++++++++++++++++++++--------- src/ecdsa.ts | 285 +++++++++++++++++++++++++++++++++++++ src/eddsa.ts | 43 +++--- src/index.ts | 18 +-- src/types.ts | 10 +- test/ecdsa.spec.ts | 137 ++++++++++++++++++ test/eddsa.spec.ts | 12 +- 8 files changed, 628 insertions(+), 115 deletions(-) create mode 100644 src/ecdsa.ts create mode 100644 test/ecdsa.spec.ts diff --git a/configs/tsconfig.base.json b/configs/tsconfig.base.json index 1d98cb6..a8c1692 100644 --- a/configs/tsconfig.base.json +++ b/configs/tsconfig.base.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "strict": true, + "strict": false, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, diff --git a/src/constants.ts b/src/constants.ts index 6c0da1a..9bc03e1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,73 +1,177 @@ -export enum EC_CURVE { - brainpoolP160r1 = "brainpoolP160r1", - brainpoolP192r1 = "brainpoolP192r1", - brainpoolP224r1 = "brainpoolP224r1", - brainpoolP256r1 = "brainpoolP256r1", - brainpoolP320r1 = "brainpoolP320r1", - brainpoolP384r1 = "brainpoolP384r1", - brainpoolP512r1 = "brainpoolP512r1", - prime192v1 = "prime192v1", - prime192v2 = "prime192v2", - prime192v3 = "prime192v3", - prime239v1 = "prime239v1", - prime239v2 = "prime239v2", - prime239v3 = "prime239v3", - prime256v1 = "prime256v1", - secp112r1 = "secp112r1", - secp112r2 = "secp112r2", - secp128r1 = "secp128r1", - secp128r2 = "secp128r2", - secp160k1 = "secp160k1", - secp160r1 = "secp160r1", - secp160r2 = "secp160r2", - secp192k1 = "secp192k1", - secp224k1 = "secp224k1", - secp224r1 = "secp224r1", - secp256k1 = "secp256k1", - secp384r1 = "secp384r1", - secp521r1 = "secp521r1", -} - -export const EC_CURVE_TO_DER_MARKER: Record = { - [EC_CURVE.brainpoolP160r1]: "06092B2403030208010101", - [EC_CURVE.brainpoolP192r1]: "06092B2403030208010103", - [EC_CURVE.brainpoolP224r1]: "06092B2403030208010105", - [EC_CURVE.brainpoolP256r1]: "06092B2403030208010107", - [EC_CURVE.brainpoolP320r1]: "06092B2403030208010109", - [EC_CURVE.brainpoolP384r1]: "06092B240303020801010B", - [EC_CURVE.brainpoolP512r1]: "06092B240303020801010D", - [EC_CURVE.prime192v1]: "06082A8648CE3D030101", - [EC_CURVE.prime192v2]: "06082A8648CE3D030102", - [EC_CURVE.prime192v3]: "06082A8648CE3D030103", - [EC_CURVE.prime239v1]: "06082A8648CE3D030104", - [EC_CURVE.prime239v2]: "06082A8648CE3D030105", - [EC_CURVE.prime239v3]: "06082A8648CE3D030106", - [EC_CURVE.prime256v1]: "06082A8648CE3D030107", - [EC_CURVE.secp112r1]: "06052B81040006", - [EC_CURVE.secp112r2]: "06052B81040007", - [EC_CURVE.secp128r1]: "06052B8104001C", - [EC_CURVE.secp128r2]: "06052B8104001D", - [EC_CURVE.secp160k1]: "06052B81040009", - [EC_CURVE.secp160r1]: "06052B8104001E", - [EC_CURVE.secp160r2]: "06052B81040021", - [EC_CURVE.secp192k1]: "06052B8104001F", - [EC_CURVE.secp224k1]: "06052B81040020", - [EC_CURVE.secp224r1]: "06052B81040021", - [EC_CURVE.secp256k1]: "06052B8104000A", - [EC_CURVE.secp384r1]: "06052B81040022", - [EC_CURVE.secp521r1]: "06052B81040023", +export const BYTE_LENGTH_IN_HEX = 2 +export enum Key { + publicKey = "publicKey", + privateKey = "privateKey", } - export enum ED_CURVE { - ed25519 = "ed25519", - ed488 = "ed488", + ed25519 = "ed25519", + ed488 = "ed488", } -export const Curve = {... ED_CURVE, ...EC_CURVE} +export enum EC_CURVE { + SM2 = "SM2", + brainpoolP160r1 = "brainpoolP160r1", + brainpoolP160t1 = "brainpoolP160t1", + brainpoolP192r1 = "brainpoolP192r1", + brainpoolP192t1 = "brainpoolP192t1", + brainpoolP224r1 = "brainpoolP224r1", + brainpoolP224t1 = "brainpoolP224t1", + brainpoolP256r1 = "brainpoolP256r1", + brainpoolP256t1 = "brainpoolP256t1", + brainpoolP320r1 = "brainpoolP320r1", + brainpoolP320t1 = "brainpoolP320t1", + brainpoolP384r1 = "brainpoolP384r1", + brainpoolP384t1 = "brainpoolP384t1", + brainpoolP512r1 = "brainpoolP512r1", + brainpoolP512t1 = "brainpoolP512t1", + c2pnb163v1 = "c2pnb163v1", + c2pnb163v2 = "c2pnb163v2", + c2pnb163v3 = "c2pnb163v3", + c2pnb176v1 = "c2pnb176v1", + c2pnb208w1 = "c2pnb208w1", + c2pnb272w1 = "c2pnb272w1", + c2pnb304w1 = "c2pnb304w1", + c2pnb368w1 = "c2pnb368w1", + c2tnb191v1 = "c2tnb191v1", + c2tnb191v2 = "c2tnb191v2", + c2tnb191v3 = "c2tnb191v3", + c2tnb239v1 = "c2tnb239v1", + c2tnb239v2 = "c2tnb239v2", + c2tnb239v3 = "c2tnb239v3", + c2tnb359v1 = "c2tnb359v1", + c2tnb431r1 = "c2tnb431r1", + prime192v1 = "prime192v1", + prime192v2 = "prime192v2", + prime192v3 = "prime192v3", + prime239v1 = "prime239v1", + prime239v2 = "prime239v2", + prime239v3 = "prime239v3", + prime256v1 = "prime256v1", + secp112r1 = "secp112r1", + secp112r2 = "secp112r2", + secp128r1 = "secp128r1", + secp128r2 = "secp128r2", + secp160k1 = "secp160k1", + secp160r1 = "secp160r1", + secp160r2 = "secp160r2", + secp192k1 = "secp192k1", + secp224k1 = "secp224k1", + secp224r1 = "secp224r1", + secp256k1 = "secp256k1", + secp384r1 = "secp384r1", + secp521r1 = "secp521r1", + sect113r1 = "sect113r1", + sect113r2 = "sect113r2", + sect131r1 = "sect131r1", + sect131r2 = "sect131r2", + sect163k1 = "sect163k1", + sect163r1 = "sect163r1", + sect163r2 = "sect163r2", + sect193r1 = "sect193r1", + sect193r2 = "sect193r2", + sect233k1 = "sect233k1", + sect233r1 = "sect233r1", + sect239k1 = "sect239k1", + sect283k1 = "sect283k1", + sect283r1 = "sect283r1", + sect409k1 = "sect409k1", + sect409r1 = "sect409r1", + sect571k1 = "sect571k1", + sect571r1 = "sect571r1", + wap_wsg_idm_ecid_wtls1 = "wap-wsg-idm-ecid-wtls1", + wap_wsg_idm_ecid_wtls10 = "wap-wsg-idm-ecid-wtls10", + wap_wsg_idm_ecid_wtls11 = "wap-wsg-idm-ecid-wtls11", + wap_wsg_idm_ecid_wtls12 = "wap-wsg-idm-ecid-wtls12", + wap_wsg_idm_ecid_wtls3 = "wap-wsg-idm-ecid-wtls3", + wap_wsg_idm_ecid_wtls4 = "wap-wsg-idm-ecid-wtls4", + wap_wsg_idm_ecid_wtls5 = "wap-wsg-idm-ecid-wtls5", + wap_wsg_idm_ecid_wtls6 = "wap-wsg-idm-ecid-wtls6", + wap_wsg_idm_ecid_wtls7 = "wap-wsg-idm-ecid-wtls7", + wap_wsg_idm_ecid_wtls8 = "wap-wsg-idm-ecid-wtls8", + wap_wsg_idm_ecid_wtls9 = "wap-wsg-idm-ecid-wtls9", +} -export enum Key { - publicKey = "publicKey", - privateKey = "privateKey", +export const EC_CURVE_TO_OID: Record = { + [EC_CURVE.SM2]: "06082a811ccf5501822d", + [EC_CURVE.brainpoolP160r1]: "06092b2403030208010101", + [EC_CURVE.brainpoolP160t1]: "06092b2403030208010102", + [EC_CURVE.brainpoolP192r1]: "06092b2403030208010103", + [EC_CURVE.brainpoolP192t1]: "06092b2403030208010104", + [EC_CURVE.brainpoolP224r1]: "06092b2403030208010105", + [EC_CURVE.brainpoolP224t1]: "06092b2403030208010106", + [EC_CURVE.brainpoolP256r1]: "06092b2403030208010107", + [EC_CURVE.brainpoolP256t1]: "06092b2403030208010108", + [EC_CURVE.brainpoolP320r1]: "06092b2403030208010109", + [EC_CURVE.brainpoolP320t1]: "06092b240303020801010a", + [EC_CURVE.brainpoolP384r1]: "06092b240303020801010b", + [EC_CURVE.brainpoolP384t1]: "06092b240303020801010c", + [EC_CURVE.brainpoolP512r1]: "06092b240303020801010d", + [EC_CURVE.brainpoolP512t1]: "06092b240303020801010e", + [EC_CURVE.c2pnb163v1]: "06082a8648ce3d030001", + [EC_CURVE.c2pnb163v2]: "06082a8648ce3d030002", + [EC_CURVE.c2pnb163v3]: "06082a8648ce3d030003", + [EC_CURVE.c2pnb176v1]: "06082a8648ce3d030004", + [EC_CURVE.c2pnb208w1]: "06082a8648ce3d03000a", + [EC_CURVE.c2pnb272w1]: "06082a8648ce3d030010", + [EC_CURVE.c2pnb304w1]: "06082a8648ce3d030011", + [EC_CURVE.c2pnb368w1]: "06082a8648ce3d030013", + [EC_CURVE.c2tnb191v1]: "06082a8648ce3d030005", + [EC_CURVE.c2tnb191v2]: "06082a8648ce3d030006", + [EC_CURVE.c2tnb191v3]: "06082a8648ce3d030007", + [EC_CURVE.c2tnb239v1]: "06082a8648ce3d03000b", + [EC_CURVE.c2tnb239v2]: "06082a8648ce3d03000c", + [EC_CURVE.c2tnb239v3]: "06082a8648ce3d03000d", + [EC_CURVE.c2tnb359v1]: "06082a8648ce3d030012", + [EC_CURVE.c2tnb431r1]: "06082a8648ce3d030014", + [EC_CURVE.prime192v1]: "06082a8648ce3d030101", + [EC_CURVE.prime192v2]: "06082a8648ce3d030102", + [EC_CURVE.prime192v3]: "06082a8648ce3d030103", + [EC_CURVE.prime239v1]: "06082a8648ce3d030104", + [EC_CURVE.prime239v2]: "06082a8648ce3d030105", + [EC_CURVE.prime239v3]: "06082a8648ce3d030106", + [EC_CURVE.prime256v1]: "06082a8648ce3d030107", + [EC_CURVE.secp112r1]: "06052b81040006", + [EC_CURVE.secp112r2]: "06052b81040007", + [EC_CURVE.secp128r1]: "06052b8104001c", + [EC_CURVE.secp128r2]: "06052b8104001d", + [EC_CURVE.secp160k1]: "06052b81040009", + [EC_CURVE.secp160r1]: "06052b81040008", + [EC_CURVE.secp160r2]: "06052b8104001e", + [EC_CURVE.secp192k1]: "06052b8104001f", + [EC_CURVE.secp224k1]: "06052b81040020", + [EC_CURVE.secp224r1]: "06052b81040021", + [EC_CURVE.secp256k1]: "06052b8104000a", + [EC_CURVE.secp384r1]: "06052b81040022", + [EC_CURVE.secp521r1]: "06052b81040023", + [EC_CURVE.sect113r1]: "06052b81040004", + [EC_CURVE.sect113r2]: "06052b81040005", + [EC_CURVE.sect131r1]: "06052b81040016", + [EC_CURVE.sect131r2]: "06052b81040017", + [EC_CURVE.sect163k1]: "06052b81040001", + [EC_CURVE.sect163r1]: "06052b81040002", + [EC_CURVE.sect163r2]: "06052b8104000f", + [EC_CURVE.sect193r1]: "06052b81040018", + [EC_CURVE.sect193r2]: "06052b81040019", + [EC_CURVE.sect233k1]: "06052b8104001a", + [EC_CURVE.sect233r1]: "06052b8104001b", + [EC_CURVE.sect239k1]: "06052b81040003", + [EC_CURVE.sect283k1]: "06052b81040010", + [EC_CURVE.sect283r1]: "06052b81040011", + [EC_CURVE.sect409k1]: "06052b81040024", + [EC_CURVE.sect409r1]: "06052b81040025", + [EC_CURVE.sect571k1]: "06052b81040026", + [EC_CURVE.sect571r1]: "06052b81040027", + [EC_CURVE.wap_wsg_idm_ecid_wtls1]: "0605672b010401", + [EC_CURVE.wap_wsg_idm_ecid_wtls10]: "0605672b01040a", + [EC_CURVE.wap_wsg_idm_ecid_wtls11]: "0605672b01040b", + [EC_CURVE.wap_wsg_idm_ecid_wtls12]: "0605672b01040c", + [EC_CURVE.wap_wsg_idm_ecid_wtls3]: "0605672b010403", + [EC_CURVE.wap_wsg_idm_ecid_wtls4]: "0605672b010404", + [EC_CURVE.wap_wsg_idm_ecid_wtls5]: "0605672b010405", + [EC_CURVE.wap_wsg_idm_ecid_wtls6]: "0605672b010406", + [EC_CURVE.wap_wsg_idm_ecid_wtls7]: "0605672b010407", + [EC_CURVE.wap_wsg_idm_ecid_wtls8]: "0605672b010408", + [EC_CURVE.wap_wsg_idm_ecid_wtls9]: "0605672b010409" } export const ED_CURVE_TO_DER_MARKER: Record> = { @@ -80,3 +184,5 @@ export const ED_CURVE_TO_DER_MARKER: Record> = { [Key.publicKey]: "3042300506032b6571033b00", }, } + +export const Curve = {...ED_CURVE, ...EC_CURVE} diff --git a/src/ecdsa.ts b/src/ecdsa.ts new file mode 100644 index 0000000..b5a6882 --- /dev/null +++ b/src/ecdsa.ts @@ -0,0 +1,285 @@ +import * as crypto from "crypto" + +import {BYTE_LENGTH_IN_HEX, EC_CURVE, EC_CURVE_TO_OID, Key} from "./constants" +import {ISigner, SignatureEncoding, SignatureResponse, SignatureType} from "./types" + +export class ECDSA implements ISigner { + private readonly EC_PUBLIC_KEY_OID = "06072a8648ce3d0201" + private readonly ECDSA_OID_PREFIX = "02010030" + private readonly ECDSA_OID_SUFFIX = "020101" + private readonly PUBLIC_KEY_START_INDICATOR = "00" + + private readonly ecdh: crypto.ECDH + private readonly oid: string + private _privateKey: crypto.KeyObject + private _publicKey: crypto.KeyObject + + constructor(private readonly curve: EC_CURVE) { + if (!curve) { + throw new Error("Curve is required.") + } + if (!Object.values(EC_CURVE).includes(curve)) { + throw new Error(`Unsupported curve: ${curve}.`) + } + + this.ecdh = crypto.createECDH(curve) + this.oid = EC_CURVE_TO_OID[curve] + } + + static withCurve(curve: EC_CURVE): ECDSA { + return new ECDSA(curve) + } + + public get privateKey(): string { + const pkcs8Hex = this._privateKey.export({ + format: "der", + type: "pkcs8", + }).toString("hex") + const privateKeyLengthSizeIndex = pkcs8Hex.indexOf(this.ECDSA_OID_SUFFIX) + this.ECDSA_OID_SUFFIX.length + 2 + const privateKeyLengthSizeIndexEnd = privateKeyLengthSizeIndex + 2 + const privateKeySize = pkcs8Hex.substring(privateKeyLengthSizeIndex, privateKeyLengthSizeIndexEnd) + const privateKeyEnd = privateKeyLengthSizeIndexEnd + this._decodeOidLength(privateKeySize) * BYTE_LENGTH_IN_HEX + return pkcs8Hex.substring(privateKeyLengthSizeIndexEnd, privateKeyEnd) + } + + public get publicKey(): string { + const pkcs8Hex = this.export("der", Key.publicKey).toString("hex") + const pkLengthIndexStart = pkcs8Hex.indexOf(this.oid) + this.oid.length + BYTE_LENGTH_IN_HEX + + let pkLengthIndexEnd = pkLengthIndexStart + while (pkcs8Hex.substring(pkLengthIndexEnd, pkLengthIndexEnd + BYTE_LENGTH_IN_HEX) != this.PUBLIC_KEY_START_INDICATOR) { + pkLengthIndexEnd += BYTE_LENGTH_IN_HEX + } + + const publicKeySize = pkcs8Hex.substring(pkLengthIndexStart, pkLengthIndexEnd) + const publicKeyStart= pkLengthIndexEnd + this.PUBLIC_KEY_START_INDICATOR.length + const publicKeyEnd = publicKeyStart + (this._decodeOidLength(publicKeySize) * BYTE_LENGTH_IN_HEX) + return pkcs8Hex.substring(publicKeyStart, publicKeyEnd) + } + + private export(format: crypto.KeyFormat, key: Key = Key.privateKey): Buffer { + if (key == Key.privateKey) { + return this._privateKey.export({ + format: format as any, + type: "pkcs8", + }) as Buffer + } + return this._publicKey.export({ + format: format as any, + type: "spki", + }) as Buffer + } + + private import(keyData: string | Buffer, format: crypto.KeyFormat, key: Key) { + this.checkPrivateKeyNotAlreadyImported() + + if (key == Key.privateKey) { + this._privateKey = crypto.createPrivateKey({ + key: keyData, + format, + type: "pkcs8", + }) + this._publicKey = crypto.createPublicKey(this._privateKey) + } else { + this._publicKey = crypto.createPublicKey({ + key: keyData, + format, + type: "spki", + }) + } + + } + + public fromDER(der: string, key: Key = Key.privateKey): ECDSA { + this.import(Buffer.from(der, "hex"), "der", key) + return this + } + + public fromPEM(pem: string, key: Key = Key.privateKey): ECDSA { + this.import(pem, "pem", key) + return this + } + + public toDER(key: Key = Key.privateKey): Buffer { + this.validateKeyExists(key) + if (key == Key.publicKey) + return this._publicKey.export({ + format: "der", + type: "spki", + }) + + return this._privateKey.export({ + format: "der", + type: "pkcs8", + }) + } + + public toPEM(key: Key = Key.privateKey): string { + this.validateKeyExists(key) + + return this._encodePEM(this.toDER(key).toString("base64"), key) + } + + sign(msg: string, enc: T): SignatureResponse[T] { + this.validateKeyExists(Key.privateKey) + const signature = crypto.sign( + null, + Buffer.isBuffer(msg) ? msg : Buffer.from(msg, "hex"), + { + key: this._privateKey, + dsaEncoding: "ieee-p1363" + } + ) + + if (enc === "hex") return signature.toString("hex") as SignatureResponse[T] + if (enc === "buffer") return signature as SignatureResponse[T] + if (enc === "object") + return { + r: signature.subarray(0, signature.length / 2).toString("hex"), + s: signature.subarray(signature.length / 2, signature.length).toString("hex") + } as SignatureResponse[T] + + throw new Error(`Unsupported encoding: ${enc}`) + } + + verify(msg: string, signature: SignatureType): boolean { + this.validateKeyExists(Key.publicKey) + const castedSignature = this._castSignature(signature) + + return crypto.verify( + null, + Buffer.from(msg, "hex"), + { + key: this._publicKey, + dsaEncoding: "ieee-p1363" + }, + castedSignature + ) + } + + private _castSignature(signature: SignatureType): Buffer { + if (Buffer.isBuffer(signature)) + return signature + + if (typeof signature === "object") + signature = signature.r + signature.s + + if (!this._publicKey) throw new Error("No public key set") + return Buffer.from(signature, "hex") + } + + private validateKeyExists(key: Key) { + if (key == Key.privateKey && !this._privateKey) + throw new Error("No private key set") + if (key == Key.publicKey && !this._publicKey) + throw new Error("No public key set") + } + + private _encodePEM(keyDer: string, key: Key): string { + if (key == Key.privateKey) + return `-----BEGIN PRIVATE KEY-----\n${keyDer}\n-----END PRIVATE KEY-----` + + return `-----BEGIN PUBLIC KEY-----\n${keyDer}\n-----END PUBLIC KEY-----` + } + + private _encodeDER(hex: string, key: Key): Buffer { + return key == Key.privateKey ? + this._derEncodePrivateKey(hex) : + this._derEncodePublicKey(hex) + } + + private _derEncodePublicKey(publicKeyHex: string): Buffer { + const paddedPublicKeyHex = `${this.PUBLIC_KEY_START_INDICATOR}${publicKeyHex}` + const encodedPublicKey = `03${this._encodeOidLength(paddedPublicKeyHex)}${paddedPublicKeyHex}` + const keyMetadata = `${this.EC_PUBLIC_KEY_OID}${this.oid}` + const algorithmIdentifier = `30${this._encodeOidLength(keyMetadata)}${keyMetadata}` + const fullString = `${algorithmIdentifier}${encodedPublicKey}` + return Buffer.from("30" + this._encodeOidLength(fullString) + fullString, "hex") + } + + private checkPrivateKeyNotAlreadyImported(): void { + if (this._privateKey) throw new Error("Private key already imported") + } + + keyFromPublic(publicKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): ECDSA { + if (this._privateKey) throw new Error("Cannot import public key when private key is set") + + const serializedKey = Buffer.isBuffer(publicKey) ? publicKey : Buffer.from(publicKey, enc) + this._publicKey = crypto.createPublicKey({ + key: this._encodeDER(serializedKey.toString("hex"), Key.publicKey), + format: "der", + type: "spki", + }) + return this + } + + genKeyPair(): ECDSA { + const keypair = crypto.generateKeyPairSync("ec", { + namedCurve: this.curve + }) + this._privateKey = keypair.privateKey + this._publicKey = keypair.publicKey + return this + } + + keyFromPrivate(privateKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): ECDSA { + const serializedKey = Buffer.isBuffer(privateKey) ? privateKey : Buffer.from(privateKey, enc) + + this.ecdh.setPrivateKey(serializedKey) + + const publicKey = this.ecdh.getPublicKey() + this._publicKey = crypto.createPublicKey({ + key: this._encodeDER(publicKey.toString("hex"), Key.publicKey), + format: "der", + type: "spki", + }) + + const derPrivateKey = this._derEncodePrivateKey(serializedKey.toString("hex")) + this._privateKey = crypto.createPrivateKey({ + key: derPrivateKey, + format: "der", + type: "pkcs8", + }) + + return this + } + + private _decodeOidLength(hexString: string): number { + const firstByte = parseInt(hexString.slice(0, BYTE_LENGTH_IN_HEX), 16) + + if (firstByte < 128) + return firstByte + + const byteCount = firstByte - 128 + const lengthBytes = hexString.slice(BYTE_LENGTH_IN_HEX, BYTE_LENGTH_IN_HEX * (byteCount + 1)) + return parseInt(lengthBytes, 16) + } + + private _encodeOidLength(hexString: string): string { + const BYTE_LENGTH_IN_HEX = 2 + const length = hexString.length / BYTE_LENGTH_IN_HEX + + if (length < 128) + return length.toString(16).padStart(BYTE_LENGTH_IN_HEX, "0") + + let lengthBytes = length.toString(16) + const byteCount = Math.ceil(lengthBytes.length / BYTE_LENGTH_IN_HEX) + const initialByte = (128 + byteCount).toString(16) + lengthBytes = lengthBytes.padStart(byteCount * BYTE_LENGTH_IN_HEX, "0") + return initialByte + lengthBytes + } + + private _derEncodePrivateKey(privateKeyHex: string): Buffer { + const publicKeyHex = `${this.PUBLIC_KEY_START_INDICATOR}${this.publicKey}` + + const encodedPublicKey = `03${this._encodeOidLength(publicKeyHex)}${publicKeyHex}` + const encodedPrivateKey = `04${this._encodeOidLength(privateKeyHex)}${privateKeyHex}` + const privateKeyAndPublicKey = `${this.ECDSA_OID_SUFFIX}${encodedPrivateKey}A1${this._encodeOidLength(encodedPublicKey)}${encodedPublicKey}` + let privateKeyAndPublicKeyEncoding = `30${this._encodeOidLength(privateKeyAndPublicKey)}${privateKeyAndPublicKey}` + privateKeyAndPublicKeyEncoding = `04${this._encodeOidLength(privateKeyAndPublicKeyEncoding)}${privateKeyAndPublicKeyEncoding}` + const ecMetadata = `${this.EC_PUBLIC_KEY_OID}${this.oid}` + const fullEncoding = `${this.ECDSA_OID_PREFIX}${this._encodeOidLength(ecMetadata)}${ecMetadata}${privateKeyAndPublicKeyEncoding}` + const derPk = `30${this._encodeOidLength(fullEncoding)}${fullEncoding}` + return Buffer.from(derPk, "hex") + } +} diff --git a/src/eddsa.ts b/src/eddsa.ts index c5b73b1..5a5ebf7 100644 --- a/src/eddsa.ts +++ b/src/eddsa.ts @@ -1,13 +1,13 @@ import * as crypto from "crypto" -import {ED_CURVE, ED_CURVE_TO_DER_MARKER, Key} from "./constants" -import {ISigner, SignatureEncoding, SignatureResponse} from "./types" +import { ED_CURVE, ED_CURVE_TO_DER_MARKER, Key } from "./constants" +import {ISigner, SignatureEncoding, SignatureResponse, SignatureType} from "./types" export class EdDSA implements ISigner { private curve: ED_CURVE private _privateKey: crypto.KeyObject private _publicKey: crypto.KeyObject - private readonly privateKeyPrefix - private readonly publicKeyPrefix + private readonly privateKeyPrefix: string + private readonly publicKeyPrefix: string constructor(curve: ED_CURVE) { if(!curve) { @@ -69,8 +69,8 @@ export class EdDSA implements ISigner { } - public fromDER(der: string, key: Key = Key.privateKey): EdDSA { - this.import(Buffer.from(der, "base64"),"der", key) + public fromDER(der: string|Buffer, key: Key = Key.privateKey): EdDSA { + this.import(Buffer.isBuffer(der) ? der : Buffer.from(der, "hex"),"der", key) return this } @@ -79,10 +79,10 @@ export class EdDSA implements ISigner { return this } - public toDER(key: Key = Key.privateKey): string { + public toDER(key: Key = Key.privateKey): Buffer { this.validateKeyExists(key) const keyToEncode = key == Key.privateKey ? this.privateKey : this.publicKey - return this._encodeDER(keyToEncode, key).toString("base64") + return this._encodeDER(keyToEncode, key) } public toPEM(key: Key = Key.privateKey): string { @@ -91,25 +91,22 @@ export class EdDSA implements ISigner { return this._encodePEM(this.toDER(key), key) } - sign( - msg: string | Buffer, - enc: SignatureEncoding = "object" - ): SignatureResponse { + sign(msg: string | Buffer, enc: T): SignatureResponse[T] { this.validateKeyExists(Key.privateKey) const signature = crypto.sign( null, Buffer.isBuffer(msg) ? msg : Buffer.from(msg, "hex"), - this._privateKey + { key:this._privateKey , dsaEncoding: "ieee-p1363"} ) - if (enc === "hex") return signature.toString("hex") - if (enc === "buffer") return signature + if (enc === "hex") return signature.toString("hex") as SignatureResponse[T] + if (enc === "buffer") return signature as SignatureResponse[T] const [r, s] = signature.toString("hex").match(/.{1,64}/g) as string[] - return {r, s} + return {r, s} as SignatureResponse[T] } - verify(msg: string, signature: SignatureResponse): boolean { + verify(msg: string, signature: SignatureType): boolean { this.validateKeyExists(Key.publicKey) if (Buffer.isBuffer(signature)) { @@ -127,7 +124,7 @@ export class EdDSA implements ISigner { ) } - keyFromPrivate(privateKey: string | Buffer, enc: crypto.BinaryToTextEncoding): EdDSA { + keyFromPrivate(privateKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): EdDSA { const serializedKey = Buffer.isBuffer(privateKey) ? privateKey : Buffer.from(privateKey, enc) this._privateKey = crypto.createPrivateKey({ key: this._encodeDER(serializedKey.toString("hex"), Key.privateKey), @@ -164,14 +161,14 @@ export class EdDSA implements ISigner { throw new Error("No public key set") } - private _encodePEM(keyDer: string, key): string { + private _encodePEM(keyDer: Buffer, key: Key): string { if (key == Key.privateKey) - return `-----BEGIN PRIVATE KEY-----\n${keyDer}\n-----END PRIVATE KEY-----` + return `-----BEGIN PRIVATE KEY-----\n${keyDer.toString("base64")}\n-----END PRIVATE KEY-----` - return `-----BEGIN PUBLIC KEY-----\n${keyDer}\n-----END PUBLIC KEY-----` + return `-----BEGIN PUBLIC KEY-----\n${keyDer.toString("base64")}\n-----END PUBLIC KEY-----` } - private _encodeDER(hex: string, key): Buffer { + private _encodeDER(hex: string, key: Key): Buffer { const prefix = key == Key.privateKey ? this.privateKeyPrefix : this.publicKeyPrefix return Buffer.concat([ Buffer.from(prefix, "hex"), @@ -182,5 +179,3 @@ export class EdDSA implements ISigner { if (this._privateKey) throw new Error("Private key already imported") } } - -export default EdDSA diff --git a/src/index.ts b/src/index.ts index e4849d3..bbeb2ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,4 @@ -export function helloWorld(): string { - const message = "Hello World from my example modern npm package!" - return message -} - -export function goodBye(): string { - const message = "Goodbye from my example modern npm package!" - return message -} - -export default { - helloWorld, - goodBye, -} \ No newline at end of file +export * from "./ecdsa" +export * from "./eddsa" +export * from "./constants" +export * from "./types" diff --git a/src/types.ts b/src/types.ts index 5fcff1b..2b2b7de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,9 @@ -export type SignatureEncoding = "hex" | "object" | "buffer"; -export type SignatureResponse = string | Buffer | { r: string; s: string }; +export type SignatureEncoding = "hex" | "buffer" | "object"; +export type SignatureObject = { r: string; s: string }; +export type SignatureType = string | Buffer | SignatureObject; +export type SignatureResponse = { "hex": string; "buffer": Buffer; "object": SignatureObject; }; export interface ISigner { - sign: (msg: string, enc: SignatureEncoding) => SignatureResponse; - verify: (msg: string, signature: SignatureResponse) => boolean; + sign: (msg: string, enc: T) => SignatureResponse[T]; + verify: (msg: string, signature: SignatureType) => boolean; } diff --git a/test/ecdsa.spec.ts b/test/ecdsa.spec.ts new file mode 100644 index 0000000..251b02b --- /dev/null +++ b/test/ecdsa.spec.ts @@ -0,0 +1,137 @@ +import {ECDSA, Key, EC_CURVE} from "../src" +import * as crypto from "crypto" + +describe("ECDSA", () => { + + test("all EC curves without ignored ones are supported", () => { + // missing OID in nodejs + const ignoredCurves = ["Oakley-EC2N-3", "Oakley-EC2N-4"] + expect(crypto.getCurves().filter(curve => !ignoredCurves.includes(curve))).toEqual(Object.values(EC_CURVE)) + }) + + Object.values(EC_CURVE).forEach((curve: EC_CURVE) => { + let ecdsa + + beforeEach(() => { + ecdsa = new ECDSA(curve) + }) + + test(`${curve} throws error for unsupported curve`, () => { + expect(() => new ECDSA("unsupportedCurve" as EC_CURVE)).toThrow("Unsupported curve: unsupportedCurve.") + }) + + test(`${curve} generate key pair`, () => { + ecdsa.genKeyPair() + + expect(ecdsa.privateKey).toBeDefined() + expect(ecdsa.publicKey).toBeDefined() + }) + + test(`${curve} exports private key to DER format`, () => { + ecdsa.genKeyPair() + const privateKeyDER = ecdsa.toDER(Key.privateKey) + + expect(privateKeyDER).toBeDefined() + expect(typeof privateKeyDER).toEqual("object") + }) + + test(`${curve} exports private key to PEM format`, () => { + ecdsa.genKeyPair() + const privateKeyPEM = ecdsa.toPEM(Key.privateKey) + + expect(privateKeyPEM).toBeDefined() + expect(typeof privateKeyPEM).toEqual("string") + }) + + test(`${curve} sign message`, () => { + ecdsa.genKeyPair() + const message = "test message" + const signature = ecdsa.sign(message) + + expect(signature).toBeDefined() + }) + + test(`${curve} verifies signature`, () => { + ecdsa.genKeyPair() + const message = "test message" + const signature = ecdsa.sign(message) + + expect(ecdsa.verify(message, signature)).toBeTruthy() + }) + + test(`${curve} throws error when private key is not set for signing`, () => { + const message = "test message" + + expect(() => ecdsa.sign(message)).toThrow("No private key set") + }) + + test(`${curve} throws error when public key is not set for verification`, () => { + const message = "test message" + const signature = "test signature" + + expect(() => ecdsa.verify(message, signature)).toThrow("No public key set") + }) + + + test(`${curve} converts correctly from PEM to DER and back to PEM for private key`, () => { + ecdsa.genKeyPair() + const originalPrivateKeyPEM = ecdsa.toPEM(Key.privateKey) + + const privateKeyDER = ecdsa.toDER(Key.privateKey) + const ecdsa2 = ECDSA.withCurve(curve) + ecdsa2.fromDER(privateKeyDER, Key.privateKey) + const convertedPrivateKeyPEM = ecdsa2.toPEM(Key.privateKey) + + expect(convertedPrivateKeyPEM).toEqual(originalPrivateKeyPEM) + }) + + test(`${curve} converts correctly from DER to PEM and back to DER for private key`, () => { + ecdsa.genKeyPair() + const originalPrivateKeyDER = ecdsa.toDER(Key.privateKey) + + const privateKeyPEM = ecdsa.toPEM(Key.privateKey) + const ecdsa2 = ECDSA.withCurve(curve) + ecdsa2.fromPEM(privateKeyPEM, Key.privateKey) + const convertedPrivateKeyDER = ecdsa2.toDER(Key.privateKey) + + expect(convertedPrivateKeyDER).toEqual(originalPrivateKeyDER) + }) + + test(`${curve} converts correctly from PEM to DER and back to PEM for public key`, () => { + ecdsa.genKeyPair() + const originalPublicKeyPEM = ecdsa.toPEM(Key.publicKey) + + const publicKeyDER = ecdsa.toDER(Key.publicKey) + const ecdsa2 = ECDSA.withCurve(curve) + ecdsa2.fromDER(publicKeyDER, Key.publicKey) + const convertedPublicKeyPEM = ecdsa2.toPEM(Key.publicKey) + + expect(convertedPublicKeyPEM).toEqual(originalPublicKeyPEM) + }) + + test(`${curve} converts correctly from DER to PEM and back to DER for public key`, () => { + ecdsa.genKeyPair() + const originalPublicKeyDER = ecdsa.toDER(Key.publicKey) + + const publicKeyPEM = ecdsa.toPEM(Key.publicKey) + const ecdsa2 = ECDSA.withCurve(curve) + ecdsa2.fromPEM(publicKeyPEM, Key.publicKey) + const convertedPublicKeyDER = ecdsa2.toDER(Key.publicKey) + + expect(convertedPublicKeyDER).toEqual(originalPublicKeyDER) + }) + + test(`${curve} correctly imports hex encoded private key`, () => { + ecdsa.genKeyPair() + + const importedECDSA = ECDSA.withCurve(curve).keyFromPrivate(ecdsa.privateKey) + expect(importedECDSA.privateKey).toEqual(ecdsa.privateKey) + }) + + test(`${curve} correctly imports hex encoded public key`, () => { + ecdsa.genKeyPair() + const importedECDSA = ECDSA.withCurve(curve).keyFromPublic(ecdsa.publicKey) + expect(importedECDSA.publicKey).toEqual(ecdsa.publicKey) + }) + }) +}) diff --git a/test/eddsa.spec.ts b/test/eddsa.spec.ts index a7cf9ac..bc6678e 100644 --- a/test/eddsa.spec.ts +++ b/test/eddsa.spec.ts @@ -1,5 +1,4 @@ -import Eddsa, {EdDSA} from "../src/eddsa" -import {Curve, ED_CURVE, ED_CURVE_TO_DER_MARKER, Key} from "../src/constants" +import {Curve, ED_CURVE, ED_CURVE_TO_DER_MARKER, EdDSA, Key} from "../src" describe("EdDSA", () => { let eddsa @@ -21,7 +20,6 @@ describe("EdDSA", () => { }) test("sign and verify messages with Ed25519 - Test 1", () => { - const privKey = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" const pubKey = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" const message = "" @@ -65,7 +63,7 @@ describe("EdDSA", () => { expect(pemPrivateKey).toContain("BEGIN PRIVATE KEY") expect(pemPrivateKey).toContain("END PRIVATE KEY") - const importedKey = Eddsa.withCurve(Curve.ed25519).fromPEM(pemPrivateKey, Key.privateKey).privateKey + const importedKey = EdDSA.withCurve(Curve.ed25519).fromPEM(pemPrivateKey, Key.privateKey).privateKey expect(eddsa.privateKey).toEqual(importedKey) }) @@ -75,7 +73,7 @@ describe("EdDSA", () => { expect(pemPrivateKey).toContain("BEGIN PUBLIC KEY") expect(pemPrivateKey).toContain("END PUBLIC KEY") - const importedKey = Eddsa.withCurve(Curve.ed25519).fromPEM(pemPrivateKey, Key.publicKey).publicKey + const importedKey = EdDSA.withCurve(Curve.ed25519).fromPEM(pemPrivateKey, Key.publicKey).publicKey expect(eddsa.publicKey).toEqual(importedKey) }) @@ -86,7 +84,7 @@ describe("EdDSA", () => { expect(derPrivateKey).not.toContain("BEGIN PRIVATE KEY") expect(derPrivateKey).not.toContain("END PRIVATE KEY") - expect(eddsa.privateKey).toEqual(Eddsa.withCurve(Curve.ed25519).fromDER(derPrivateKey,Key.privateKey).privateKey) + expect(eddsa.privateKey).toEqual(EdDSA.withCurve(Curve.ed25519).fromDER(derPrivateKey, Key.privateKey).privateKey) }) test("converts a public key from and to DER", () => { @@ -95,7 +93,7 @@ describe("EdDSA", () => { expect(derPrivateKey).not.toContain("BEGIN PRIVATE KEY") expect(derPrivateKey).not.toContain("END PRIVATE KEY") - expect(eddsa.publicKey).toEqual(Eddsa.withCurve(Curve.ed25519).fromDER(derPrivateKey,Key.publicKey).publicKey) + expect(eddsa.publicKey).toEqual(EdDSA.withCurve(Curve.ed25519).fromDER(derPrivateKey,Key.publicKey).publicKey) }) test("correctly sets prefix based on curve", () => {