diff --git a/CHANGELOG.md b/CHANGELOG.md index a66dd7a82..82979c637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T ## Added +- Add standalone `verifySignature` and `verifySignatureAsync` utility functions that verify a signature against any supported public key type (Ed25519, Secp256k1, MultiEd25519, MultiKey, Keyless) without requiring callers to know the key type in advance - Add MultiKey (K-of-N mixed key types) transfer example (`examples/typescript/multikey_transfer.ts`) - Add MultiEd25519 (K-of-N Ed25519) transfer example (`examples/typescript/multi_ed25519_transfer.ts`) diff --git a/src/core/crypto/index.ts b/src/core/crypto/index.ts index 352ac8a31..b83b3c8c4 100644 --- a/src/core/crypto/index.ts +++ b/src/core/crypto/index.ts @@ -18,3 +18,4 @@ export * from "./signature"; export * from "./singleKey"; export * from "./types"; export * from "./deserializationUtils"; +export * from "./verifySignature"; diff --git a/src/core/crypto/verifySignature.ts b/src/core/crypto/verifySignature.ts new file mode 100644 index 000000000..6f493ade7 --- /dev/null +++ b/src/core/crypto/verifySignature.ts @@ -0,0 +1,162 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import type { AptosConfig } from "../../api"; +import { HexInput } from "../../types"; +import { PublicKey } from "./publicKey"; +import { Signature } from "./signature"; +import { AnyPublicKey, AnySignature } from "./singleKey"; +import { KeylessPublicKey } from "./keyless"; +import { FederatedKeylessPublicKey } from "./federatedKeyless"; + +/** + * Arguments for the standalone {@link verifySignature} function. + * + * @param message - The message that was signed, as a hex string or Uint8Array. + * @param signature - The signature to verify. + * @param publicKey - The public key to verify against. + * @group Implementation + * @category Serialization + */ +export interface VerifyMessageSignatureArgs { + message: HexInput; + signature: Signature; + publicKey: PublicKey; +} + +/** + * Arguments for the standalone {@link verifySignatureAsync} function. + * + * Required for signature types that depend on network state (e.g. Keyless). + * + * @param aptosConfig - The Aptos configuration for network calls. + * @param message - The message that was signed, as a hex string or Uint8Array. + * @param signature - The signature to verify. + * @param publicKey - The public key to verify against. + * @param options - Optional verification options. + * @group Implementation + * @category Serialization + */ +export interface VerifyMessageSignatureAsyncArgs extends VerifyMessageSignatureArgs { + aptosConfig: AptosConfig; + options?: { throwErrorWithReason?: boolean }; +} + +function wrapInAnySignature(signature: Signature): AnySignature { + return signature instanceof AnySignature ? signature : new AnySignature(signature); +} + +/** + * Verifies a digital signature for a given message, automatically handling + * all supported public key and signature types. + * + * This function removes the need to know the specific key type (Ed25519, Secp256k1, + * MultiEd25519, MultiKey, etc.) before performing verification. Simply pass any + * public key, its corresponding signature, and the original message. + * + * **Note:** Keyless and FederatedKeyless signatures require network state for verification. + * Use {@link verifySignatureAsync} for those types, or when the key type is unknown and + * may be Keyless. + * + * @param args - The verification arguments. + * @param args.message - The original message that was signed. + * @param args.signature - The signature to verify. + * @param args.publicKey - The signer's public key. + * @returns `true` if the signature is valid, `false` otherwise. + * @throws Error if the public key is a Keyless variant (use `verifySignatureAsync` instead). + * + * @example + * ```typescript + * import { Ed25519PublicKey, Ed25519Signature, verifySignature } from "@aptos-labs/ts-sdk"; + * + * const publicKey = new Ed25519PublicKey("0x..."); + * const signature = new Ed25519Signature("0x..."); + * const isValid = verifySignature({ + * message: "hello world", + * signature, + * publicKey, + * }); + * ``` + * + * @example + * ```typescript + * import { AnyPublicKey, AnySignature, verifySignature } from "@aptos-labs/ts-sdk"; + * import { Deserializer } from "@aptos-labs/ts-sdk"; + * + * // Deserialize BCS-encoded public key and signature from wallet + * const publicKey = AnyPublicKey.deserialize(new Deserializer(publicKeyBytes)); + * const signature = AnySignature.deserialize(new Deserializer(signatureBytes)); + * const isValid = verifySignature({ + * message: "hello world", + * signature, + * publicKey, + * }); + * ``` + * @group Implementation + * @category Serialization + */ +export function verifySignature(args: VerifyMessageSignatureArgs): boolean { + const { message, signature, publicKey } = args; + + if (publicKey instanceof AnyPublicKey) { + if (publicKey.publicKey instanceof KeylessPublicKey || publicKey.publicKey instanceof FederatedKeylessPublicKey) { + throw new Error( + "Keyless signatures require async verification with an AptosConfig. Use verifySignatureAsync instead.", + ); + } + return publicKey.verifySignature({ message, signature: wrapInAnySignature(signature) }); + } + + // For Ed25519PublicKey, Secp256k1PublicKey, MultiEd25519PublicKey, MultiKey, etc. + // the concrete verifySignature narrows the signature type but accepts the base at runtime. + return publicKey.verifySignature({ message, signature } as Parameters[0]); +} + +/** + * Verifies a digital signature for a given message, supporting all key types + * including those that require asynchronous network lookups (e.g. Keyless). + * + * This is the recommended verification function when the key type is unknown, + * as it handles every supported type including Keyless and FederatedKeyless. + * + * @param args - The verification arguments. + * @param args.aptosConfig - The Aptos network configuration (needed for Keyless verification). + * @param args.message - The original message that was signed. + * @param args.signature - The signature to verify. + * @param args.publicKey - The signer's public key. + * @param args.options - Optional settings for verification. + * @param args.options.throwErrorWithReason - If true, throws an error with the reason for failure + * instead of returning false. + * @returns `true` if the signature is valid, `false` otherwise. + * + * @example + * ```typescript + * import { Aptos, AptosConfig, Network, verifySignatureAsync } from "@aptos-labs/ts-sdk"; + * + * const config = new AptosConfig({ network: Network.TESTNET }); + * + * // Works with any public key type — Ed25519, Secp256k1, Keyless, MultiKey, etc. + * const isValid = await verifySignatureAsync({ + * aptosConfig: config, + * message: "hello world", + * signature: anySignatureObject, + * publicKey: anyPublicKeyObject, + * }); + * ``` + * @group Implementation + * @category Serialization + */ +export async function verifySignatureAsync(args: VerifyMessageSignatureAsyncArgs): Promise { + const { message, signature, publicKey, aptosConfig, options } = args; + + if (publicKey instanceof AnyPublicKey) { + return publicKey.verifySignatureAsync({ + aptosConfig, + message, + signature: wrapInAnySignature(signature), + options, + }); + } + + return publicKey.verifySignatureAsync({ aptosConfig, message, signature }); +} diff --git a/tests/unit/verifySignature.test.ts b/tests/unit/verifySignature.test.ts new file mode 100644 index 000000000..e7725fe0d --- /dev/null +++ b/tests/unit/verifySignature.test.ts @@ -0,0 +1,143 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { + Account, + AnyPublicKey, + Ed25519PrivateKey, + Ed25519PublicKey, + Ed25519Signature, + MultiEd25519Account, + MultiEd25519PublicKey, + MultiKey, + MultiKeyAccount, + Secp256k1PrivateKey, + Secp256k1PublicKey, + SigningSchemeInput, +} from "../../src"; +import { verifySignature } from "../../src/core/crypto/verifySignature"; +import { ed25519, secp256k1TestObject, singleSignerED25519 } from "./helper"; + +describe("verifySignature", () => { + const message = "hello world"; + + describe("Ed25519 (legacy)", () => { + it("verifies a valid signature", () => { + const publicKey = new Ed25519PublicKey(ed25519.publicKey); + const signature = new Ed25519Signature(ed25519.signatureHex); + expect(verifySignature({ message: ed25519.messageEncoded, signature, publicKey })).toBe(true); + }); + + it("rejects an invalid signature", () => { + const publicKey = new Ed25519PublicKey(ed25519.publicKey); + const wrongSig = new Ed25519Signature( + "0xc5de9e40ac00b371cd83b1c197fa5b665b7449b33cd3cdd305bb78222e06a671a49625ab9aea8a039d4bb70e275768084d62b094bc1b31964f2357b7c1af7e0a", + ); + expect(verifySignature({ message: ed25519.messageEncoded, signature: wrongSig, publicKey })).toBe(false); + }); + + it("rejects a message that was not signed", () => { + const publicKey = new Ed25519PublicKey(ed25519.publicKey); + const signature = new Ed25519Signature(ed25519.signatureHex); + expect(verifySignature({ message: "wrong message", signature, publicKey })).toBe(false); + }); + }); + + describe("Ed25519 (SingleKey / AnyPublicKey)", () => { + it("verifies a valid signature with AnySignature", () => { + const privateKey = new Ed25519PrivateKey(singleSignerED25519.privateKey); + const account = Account.fromPrivateKey({ privateKey, legacy: false }); + const signature = account.sign(message); + expect(verifySignature({ message, signature, publicKey: account.publicKey })).toBe(true); + }); + + it("verifies a valid signature with raw Ed25519Signature auto-wrapped", () => { + const privateKey = new Ed25519PrivateKey(singleSignerED25519.privateKey); + const rawSignature = privateKey.sign(message); + const publicKey = new AnyPublicKey(privateKey.publicKey()); + expect(verifySignature({ message, signature: rawSignature, publicKey })).toBe(true); + }); + + it("rejects an invalid message", () => { + const privateKey = new Ed25519PrivateKey(singleSignerED25519.privateKey); + const account = Account.fromPrivateKey({ privateKey, legacy: false }); + const signature = account.sign(message); + expect(verifySignature({ message: "wrong message", signature, publicKey: account.publicKey })).toBe(false); + }); + }); + + describe("Secp256k1 (SingleKey / AnyPublicKey)", () => { + it("verifies a valid signature", () => { + const privateKey = new Secp256k1PrivateKey(secp256k1TestObject.privateKey); + const account = Account.fromPrivateKey({ privateKey }); + const signature = account.sign(message); + expect(verifySignature({ message, signature, publicKey: account.publicKey })).toBe(true); + }); + + it("verifies a valid signature with raw Secp256k1Signature auto-wrapped", () => { + const privateKey = new Secp256k1PrivateKey(secp256k1TestObject.privateKey); + const rawSignature = privateKey.sign(message); + const publicKey = new AnyPublicKey(new Secp256k1PublicKey(secp256k1TestObject.publicKey)); + expect(verifySignature({ message, signature: rawSignature, publicKey })).toBe(true); + }); + + it("rejects a wrong message", () => { + const privateKey = new Secp256k1PrivateKey(secp256k1TestObject.privateKey); + const account = Account.fromPrivateKey({ privateKey }); + const signature = account.sign(message); + expect(verifySignature({ message: "other message", signature, publicKey: account.publicKey })).toBe(false); + }); + }); + + describe("MultiEd25519", () => { + const privateKey1 = Ed25519PrivateKey.generate(); + const privateKey2 = Ed25519PrivateKey.generate(); + const privateKey3 = Ed25519PrivateKey.generate(); + const multiPublicKey = new MultiEd25519PublicKey({ + publicKeys: [privateKey1.publicKey(), privateKey2.publicKey(), privateKey3.publicKey()], + threshold: 2, + }); + + it("verifies a valid multi-ed25519 signature", () => { + const account = new MultiEd25519Account({ + publicKey: multiPublicKey, + signers: [privateKey1, privateKey3], + }); + const signature = account.sign(message); + expect(verifySignature({ message, signature, publicKey: multiPublicKey })).toBe(true); + }); + }); + + describe("MultiKey", () => { + const ed25519Account = Account.generate({ scheme: SigningSchemeInput.Ed25519, legacy: false }); + const secp256k1Account = Account.generate({ scheme: SigningSchemeInput.Secp256k1Ecdsa }); + const multiKey = new MultiKey({ + publicKeys: [ed25519Account.publicKey, secp256k1Account.publicKey], + signaturesRequired: 2, + }); + + it("verifies a valid multi-key signature", () => { + const account = new MultiKeyAccount({ + multiKey, + signers: [ed25519Account, secp256k1Account], + }); + const signature = account.sign(message); + expect(verifySignature({ message, signature, publicKey: multiKey })).toBe(true); + }); + }); + + describe("cross-key-type verification", () => { + it("returns false when signature does not match the public key", () => { + const ed25519Key = Ed25519PrivateKey.generate(); + const otherKey = Ed25519PrivateKey.generate(); + const signature = ed25519Key.sign(message); + expect( + verifySignature({ + message, + signature, + publicKey: otherKey.publicKey(), + }), + ).toBe(false); + }); + }); +});