Last active
July 18, 2024 21:40
-
-
Save bencmbrook/505188d6ab99032e1ecd8727720e08e8 to your computer and use it in GitHub Desktop.
Derive EC key from secret in Node
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
import { KeyObject, createECDH, createPrivateKey } from "node:crypto"; | |
/** | |
* The size of the private key for P-384 in bytes. | |
* 384 bits / 8 = 48 bytes | |
*/ | |
const P384_BYTE_LENGTH = 48; | |
export interface DeriveEcPrivateKeyArgs { | |
/** A high entropy, uniformly random secret. Must have at least 384 bits of entropy and must be uniformly random. */ | |
secret: Buffer; | |
} | |
/** | |
* Derives an EC private key from a uniformly random secret | |
* This deterministically generates the JWT_ECDSA_KEY from a high-entropy secret root key | |
* | |
* @param args - The arguments to derive the EC private key | |
* @returns The derived EC private key | |
*/ | |
export function deriveEcPrivateKey({ | |
secret, | |
}: DeriveEcPrivateKeyArgs): KeyObject { | |
if (secret.length !== P384_BYTE_LENGTH) { | |
throw new RangeError( | |
`Secret must have 48 bytes (384 bits) of entropy for curve P-384.`, | |
); | |
} | |
// Generate the public key from the secret. This uses the ECDH library, but it's the EC key we want. The EC key can be used in ECDSA. | |
const ecdh = createECDH("secp384r1"); | |
// Set the secret as the private key in ECDH. This allows us to use native Node crypto to calculate the public key | |
ecdh.setPrivateKey(secret); | |
// Get the public key. Uncompressed buffer | |
const publicKey = ecdh.getPublicKey(); | |
/** | |
* Get public key point coordinates | |
* | |
* @see https://www.secg.org/sec1-v2.pdf#page17 - Section 2.3.4 Octect String to Elliptic Curve Point conversion | |
*/ | |
const [prefix, x, y] = [ | |
publicKey.subarray(0, 1), | |
publicKey.subarray(1, 1 + P384_BYTE_LENGTH), | |
publicKey.subarray(1 + P384_BYTE_LENGTH), | |
]; | |
if (prefix[0] !== 0x04) { | |
throw new Error("Public key must start with 0x04"); | |
} | |
if (x.length !== 48 || y.length !== 48) { | |
throw new Error("Public key fields must be 96 bytes long"); | |
} | |
/** | |
* Construct the private key as a JWK to import it as a Node key object | |
* @see https://tools.ietf.org/html/rfc7517 | |
*/ | |
const jwk = { | |
kty: "EC", | |
crv: "P-384", | |
x: x.toString("base64"), | |
y: y.toString("base64"), | |
d: secret.toString("base64"), | |
}; | |
const privateKey = createPrivateKey({ | |
key: jwk, | |
format: "jwk", | |
}); | |
return privateKey; | |
} | |
/** | |
* EXAMPLE | |
*/ | |
// user inputted random secret | |
const secret = Buffer.from( | |
"8d190ca968b725c7c7ac8774f17887f55384ae624df6144f1056ab7d76482d9c105a6313716fc8433dadbb95105cbd78", | |
"hex", | |
); | |
const privateKey = deriveEcPrivateKey({ secret }); | |
const pem = privateKey.export({ | |
type: "sec1", | |
format: "pem", | |
}); | |
console.log("A PEM from the provided secret:\n", pem.toString()); | |
/** | |
* TEST | |
* | |
* Test against an OpenSSL generated key | |
*/ | |
console.log("Running tests..."); | |
// A result from openssl ecparam -name secp384r1 -genkey -noout -out "$PRIVATE_FILE" | |
const fixture = `-----BEGIN EC PRIVATE KEY----- | |
MIGkAgEBBDBPHRqPRXsCH69G95yoPhzC0X0mRjFiGwz0g7MspWHOjBOnidTtuV46 | |
kk8mqOhO2D+gBwYFK4EEACKhZANiAAT49L2E8XNDyCBpdzpaScRviv+sUgtSrGeX | |
trkQ9RUjPrdu0j38NjkJqRSVQDSEiJj7VwFrFbwko/Bx/MyDi2OCtsD+dZG42V38 | |
oBz6SQ26sLlbUSJUDAhX7ZxWlLaqKAw= | |
-----END EC PRIVATE KEY----- | |
`; | |
const keyFixture = createPrivateKey(fixture); | |
const jwkFixture = keyFixture.export({ | |
format: "jwk", | |
}); | |
// A fixture for the secret value that someone might pass to deriveEcPrivateKey() | |
const secretFixture = Buffer.from(jwkFixture.d!, "base64"); | |
// Confirm that we can create the same PEM given the same secret | |
const privateKeyTest = deriveEcPrivateKey({ secret: secretFixture }); | |
const pemTest = privateKeyTest.export({ | |
type: "sec1", | |
format: "pem", | |
}); | |
if (pemTest === fixture) { | |
console.log("Tests PASSED"); | |
} else { | |
throw new Error("Tests FAILED"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment