@dwn-protocol/id-sdk
Version:
SDK for accessing the features and capabilities
322 lines (283 loc) • 12.8 kB
text/typescript
import type { BytesKeyPair } from '../types/crypto-key.js';
import { sha256 } from '@noble/hashes/sha256';
import { secp256k1 } from '@noble/curves/secp256k1';
import { numberToBytesBE } from '@noble/curves/abstract/utils';
export type HashFunction = (data: Uint8Array) => Uint8Array;
/**
* The `Secp256k1` class provides an interface for generating secp256k1 key pairs,
* computing public keys from private keys, generating shaerd secrets, and
* signing and verifying messages.
*
* The class uses the '@noble/secp256k1' package for the cryptographic operations,
* and the '@noble/hashes/sha256' package for generating the hash digests needed
* for the signing and verification operations.
*
* The methods of this class are all asynchronous and return Promises. They all use
* the Uint8Array type for keys, signatures, and data, providing a consistent
* interface for working with binary data.
*
* Example usage:
*
* ```ts
* const keyPair = await Secp256k1.generateKeyPair();
* const message = new TextEncoder().encode('Hello, world!');
* const signature = await Secp256k1.sign({
* algorithm: { hash: 'SHA-256' },
* key: keyPair.privateKey,
* data: message
* });
* const isValid = await Secp256k1.verify({
* algorithm: { hash: 'SHA-256' },
* key: keyPair.publicKey,
* signature,
* data: message
* });
* console.log(isValid); // true
* ```
*/
export class Secp256k1 {
/**
* A private static field containing a map of hash algorithm names to their
* corresponding hash functions. The map is used in the 'sign' and 'verify'
* methods to get the specified hash function.
*/
private static hashAlgorithms: Record<string, HashFunction> = {
'SHA-256': sha256
};
/**
* Converts a public key between its compressed and uncompressed forms.
*
* Given a public key, this method can either compress or decompress it
* depending on the provided `compressedPublicKey` option. The conversion
* process involves decoding the Weierstrass points from the key bytes
* and then returning the key in the desired format.
*
* This is useful in scenarios where space is a consideration or when
* interfacing with systems that expect a specific public key format.
*
* @param options - The options for the public key conversion.
* @param options.publicKey - The original public key, represented as a Uint8Array.
* @param options.compressedPublicKey - A boolean indicating whether the output
* should be in compressed form. If true, the
* method returns the compressed form of the
* provided public key. If false, it returns
* the uncompressed form.
*
* @returns A Promise that resolves to the converted public key as a Uint8Array.
*/
public static async convertPublicKey(options: {
publicKey: Uint8Array,
compressedPublicKey: boolean
}): Promise<Uint8Array> {
let { publicKey, compressedPublicKey } = options;
// Decode Weierstrass points from key bytes.
const point = secp256k1.ProjectivePoint.fromHex(publicKey);
// Return either the compressed or uncompressed form of hte public key.
return point.toRawBytes(compressedPublicKey);
}
/**
* Generates a secp256k1 key pair.
*
* @param options - Optional parameters for the key generation.
* @param options.compressedPublicKey - If true, generates a compressed public key. Defaults to true.
* @returns A Promise that resolves to an object containing the private and public keys as Uint8Array.
*/
public static async generateKeyPair(options?: {
compressedPublicKey?: boolean
}): Promise<BytesKeyPair> {
let { compressedPublicKey } = options ?? { };
compressedPublicKey ??= true; // Default to compressed public key, matching the default of @noble/secp256k1.
// Generate the private key and compute its public key.
const privateKey = secp256k1.utils.randomPrivateKey();
const publicKey = secp256k1.getPublicKey(privateKey, compressedPublicKey);
const keyPair = {
privateKey : privateKey,
publicKey : publicKey
};
return keyPair;
}
/**
* Returns the elliptic curve points (x and y coordinates) for a given secp256k1 key.
*
* In the case of a private key, the public key is first computed from the private key,
* then the x and y coordinates of the public key point on the elliptic curve are returned.
*
* In the case of a public key, the x and y coordinates of the key point on the elliptic
* curve are returned directly.
*
* The returned coordinates can be used to perform various operations on the elliptic curve,
* such as addition and multiplication of points, which can be used in various cryptographic
* schemes and protocols.
*
* @param options - The options for the operation.
* @param options.key - The key for which to get the elliptic curve points.
* Can be either a private key or a public key.
* The key should be passed as a Uint8Array.
* @returns A Promise that resolves to an object with properties 'x' and 'y',
* each being a Uint8Array representing the x and y coordinates of the key point on the elliptic curve.
*/
public static async getCurvePoints(options: {
key: Uint8Array
}): Promise<{ x: Uint8Array, y: Uint8Array }> {
let { key } = options;
// If key is a private key, first compute the public key.
if (key.byteLength === 32) {
key = await Secp256k1.getPublicKey({ privateKey: key });
}
// Decode Weierstrass points from key bytes.
const point = secp256k1.ProjectivePoint.fromHex(key);
// Get x- and y-coordinate values and convert to Uint8Array.
const x = numberToBytesBE(point.x, 32);
const y = numberToBytesBE(point.y, 32);
return { x, y };
}
/**
* Computes the public key from a given private key.
* If compressedPublicKey=true then the output is a 33-byte public key.
* If compressedPublicKey=false then the output is a 65-byte public key.
*
* @param options - The options for the public key computation.
* @param options.privateKey - The 32-byte private key from which to compute the public key.
* @param options.compressedPublicKey - If true, returns a compressed public key. Defaults to true.
* @returns A Promise that resolves to the computed public key as a Uint8Array.
*/
public static async getPublicKey(options: {
privateKey: Uint8Array,
compressedPublicKey?: boolean
}): Promise<Uint8Array> {
let { privateKey, compressedPublicKey } = options;
compressedPublicKey ??= true; // Default to compressed public key, matching the default of @noble/secp256k1.
// Compute public key.
const publicKey = secp256k1.getPublicKey(privateKey, compressedPublicKey);
return publicKey;
}
/**
* Generates a RFC6090 ECDH shared secret given the private key of one party
* and the public key another party.
*
* Note: When performing Elliptic Curve Diffie-Hellman (ECDH) key agreement,
* the resulting shared secret is a point on the elliptic curve, which
* consists of an x-coordinate and a y-coordinate. With a 256-bit curve like
* secp256k1, each of these coordinates is 32 bytes (256 bits) long. However,
* in the ECDH process, it's standard practice to use only the x-coordinate
* of the shared secret point as the resulting shared key. This is because
* the y-coordinate does not add to the entropy of the key, and both parties
* can independently compute the x-coordinate, so using just the x-coordinate
* simplifies matters.
*/
public static async sharedSecret(options: {
compressedSecret?: boolean,
privateKey: Uint8Array,
publicKey: Uint8Array
}): Promise<Uint8Array> {
let { privateKey, publicKey } = options;
// Compute the shared secret between the public and private keys.
const sharedSecret = secp256k1.getSharedSecret(privateKey, publicKey);
// Remove the leading byte that indicates the sign of the y-coordinate
// of the point on the elliptic curve. See note above.
return sharedSecret.slice(1);
}
/**
* Generates a RFC6979 ECDSA signature of given data with a given private key and hash algorithm.
*
* @param options - The options for the signing operation.
* @param options.data - The data to sign.
* @param options.hash - The hash algorithm to use to generate a digest of the data.
* @param options.key - The private key to use for signing.
* @returns A Promise that resolves to the signature as a Uint8Array.
*/
public static async sign(options: {
data: Uint8Array,
hash: string,
key: Uint8Array
}): Promise<Uint8Array> {
const { data, hash, key } = options;
// Generate a digest of the data using the specified hash function.
const hashFunction = this.hashAlgorithms[hash];
const digest = hashFunction(data);
// Signature operation returns a Signature instance with { r, s, recovery } properties.
const signatureObject = secp256k1.sign(digest, key);
// Convert Signature object to Uint8Array.
const signature = signatureObject.toCompactRawBytes();
return signature;
}
/**
* Validates a given private key to ensure that it's a valid 32-byte number
* that is less than the secp256k1 curve's order.
*
* This method checks the byte length of the key and its numerical validity
* according to the secp256k1 curve's parameters. It doesn't verify whether
* the key corresponds to a known or authorized entity or whether it has
* been compromised.
*
* @param options - The options for the key validation.
* @param options.key - The private key to validate, represented as a Uint8Array.
* @returns A Promise that resolves to a boolean indicating whether the private
* key is a valid 32-byte number less than the secp256k1 curve's order.
*/
public static async validatePrivateKey(options: {
key: Uint8Array
}): Promise<boolean> {
const { key } = options;
return secp256k1.utils.isValidPrivateKey(key);
}
/**
* Validates a given public key to ensure that it corresponds to a
* valid point on the secp256k1 elliptic curve.
*
* This method decodes the Weierstrass points from the key bytes and
* asserts their validity on the curve. If the points are not valid,
* the method returns false. If the points are valid, the method
* returns true.
*
* Note: This method does not check whether the key corresponds to a
* known or authorized entity, or whether it has been compromised.
* It only checks the mathematical validity of the key.
*
* @param options - The options for the key validation.
* @param options.key - The key to validate, represented as a Uint8Array.
* @returns A Promise that resolves to a boolean indicating whether the key
* corresponds to a valid point on the secp256k1 elliptic curve.
*/
public static async validatePublicKey(options: {
key: Uint8Array
}): Promise<boolean> {
const { key } = options;
try {
// Decode Weierstrass points from key bytes.
const point = secp256k1.ProjectivePoint.fromHex(key);
// Check if points are on the Short Weierstrass curve.
point.assertValidity();
} catch(error: any) {
return false;
}
return true;
}
/**
* Verifies a RFC6979 ECDSA signature of given data with a given public key and hash algorithm.
*
* @param options - The options for the verification operation.
* @param options.data - The data that was signed.
* @param options.hash - The hash algorithm to use to generate a digest of the data.
* @param options.key - The public key to use for verification.
* @param options.signature - The signature to verify.
* @returns A Promise that resolves to a boolean indicating whether the signature is valid.
*/
public static async verify(options: {
data: Uint8Array,
hash: string,
key: Uint8Array,
signature: Uint8Array
}): Promise<boolean> {
const { data, hash, key, signature } = options;
// Generate a digest of the data using the specified hash function.
const hashFunction = this.hashAlgorithms[hash];
const digest = hashFunction(data);
// Verify operation with malleability check disabled. Guaranteed support
// for low-s signatures across languages.
// Notable Cloud KMS providers do not natively support it however,
// low-s signatures are a requirement for Bitcoin.
const isValid = secp256k1.verify(signature, digest, key, { lowS: false });
return isValid;
}
}