diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a54b328..7fe0189e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to ### Changed - all: The TypeScript compilation target is now ES2018. +- @cosmjs/crypto: Add `Secp256k1.uncompressPubkey`. - @cosmjs/faucet: Set default value of `FAUCET_GAS_LIMIT` to 100_000 to better support Cosmos SDK 0.45 chains. - @cosmjs/stargate: The `AminoTypes` now always requires an argument of type diff --git a/packages/crypto/src/secp256k1.spec.ts b/packages/crypto/src/secp256k1.spec.ts index 0e90d4a2..fcbcca2c 100644 --- a/packages/crypto/src/secp256k1.spec.ts +++ b/packages/crypto/src/secp256k1.spec.ts @@ -570,6 +570,30 @@ describe("Secp256k1", () => { }); }); + describe("uncompressPubkey", () => { + it("throws for a pubkey with invalid length", () => { + const pubkey = fromHex("aa".repeat(32)); + expect(() => Secp256k1.uncompressPubkey(pubkey)).toThrowError(/invalid pubkey length/i); + }); + + it("returns an uncompressPubkey pubkey unchanged", () => { + // Test data generated at https://iancoleman.io/bitcoin-key-compression/ + const pubkey = fromHex( + "044f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029013b587a681e836cc187a8164b98a5848a2b89b3173315fdd0740d5032e259cd5", + ); + expect(Secp256k1.uncompressPubkey(pubkey)).toEqual(pubkey); + }); + + it("uncompresses a compressed pubkey", () => { + // Test data generated at https://iancoleman.io/bitcoin-key-compression/ + const uncompressed = fromHex( + "044f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029013b587a681e836cc187a8164b98a5848a2b89b3173315fdd0740d5032e259cd5", + ); + const compressed = fromHex("034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c70290"); + expect(Secp256k1.uncompressPubkey(compressed)).toEqual(uncompressed); + }); + }); + describe("trimRecoveryByte", () => { it("throws for a signature with invalid length", () => { const signature = fromHex("aa".repeat(66)); diff --git a/packages/crypto/src/secp256k1.ts b/packages/crypto/src/secp256k1.ts index d9357f98..b16ca917 100644 --- a/packages/crypto/src/secp256k1.ts +++ b/packages/crypto/src/secp256k1.ts @@ -8,11 +8,26 @@ const secp256k1 = new elliptic.ec("secp256k1"); const secp256k1N = new BN("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", "hex"); export interface Secp256k1Keypair { + /** A 32 byte private key */ readonly pubkey: Uint8Array; + /** + * A raw secp256k1 public key. + * + * The type itself does not give you any guarantee if this is + * compressed or uncompressed. If you are unsure where the data + * is coming from, use `Secp256k1.compressPubkey` or + * `Secp256k1.uncompressPubkey` (both idempotent) before processing it. + */ readonly privkey: Uint8Array; } export class Secp256k1 { + /** + * Takes a 32 byte private key and returns a privkey/pubkey pair. + * + * The resulting pubkey is uncompressed. For the use in Cosmos it should + * be compressed first using `Secp256k1.compressPubkey`. + */ public static async makeKeypair(privkey: Uint8Array): Promise { if (privkey.length !== 32) { // is this check missing in secp256k1.validatePrivateKey? @@ -43,10 +58,12 @@ export class Secp256k1 { return out; } - // Creates a signature that is - // - deterministic (RFC 6979) - // - lowS signature - // - DER encoded + /** + * Creates a signature that is + * - deterministic (RFC 6979) + * - lowS signature + * - DER encoded + */ public static async createSignature( messageHash: Uint8Array, privkey: Uint8Array, @@ -110,6 +127,11 @@ export class Secp256k1 { return fromHex(keypair.getPublic(false, "hex")); } + /** + * Takes a compressed or uncompressed pubkey and return a compressed one. + * + * This function is idempotent. + */ public static compressPubkey(pubkey: Uint8Array): Uint8Array { switch (pubkey.length) { case 33: @@ -121,6 +143,22 @@ export class Secp256k1 { } } + /** + * Takes a compressed or uncompressed pubkey and returns an uncompressed one. + * + * This function is idempotent. + */ + public static uncompressPubkey(pubkey: Uint8Array): Uint8Array { + switch (pubkey.length) { + case 33: + return Uint8Array.from(secp256k1.keyFromPublic(pubkey).getPublic(false, "array")); + case 65: + return pubkey; + default: + throw new Error("Invalid pubkey length"); + } + } + public static trimRecoveryByte(signature: Uint8Array): Uint8Array { switch (signature.length) { case 64: