Skip to content

Instantly share code, notes, and snippets.

@bencmbrook
Last active July 18, 2024 21:40
Show Gist options
  • Save bencmbrook/505188d6ab99032e1ecd8727720e08e8 to your computer and use it in GitHub Desktop.
Save bencmbrook/505188d6ab99032e1ecd8727720e08e8 to your computer and use it in GitHub Desktop.
Derive EC key from secret in Node
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