diff --git a/CHANGELOG.md b/CHANGELOG.md index ea22ce90d..500abe0d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T # 5.2.0 (2025-12-10) - Add `http2` as an optional parameter in `ClientConfig` +- Add SLH-DSA-SHA2-128s as one of the support signature schemes # 5.1.6 (2025-12-06) diff --git a/jest.config.js b/jest.config.js index 3e08abf44..863624f7f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,17 @@ module.exports = { "^(\\.{1,2}/.*)\\.js$": "$1", }, testEnvironment: "node", + transformIgnorePatterns: [ + "node_modules/(?!.*@noble/(post-quantum|hashes))", + ], + transform: { + "^.+\\.tsx?$": "ts-jest", + "^.+\\.jsx?$": ["ts-jest", { + tsconfig: { + allowJs: true, + }, + }], + }, coveragePathIgnorePatterns: [ "./src/internal/queries/", "./src/types/generated", diff --git a/package.json b/package.json index a58627606..bc17387c7 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@aptos-labs/aptos-client": "^2.1.0", "@noble/curves": "^1.9.0", "@noble/hashes": "^1.5.0", + "@noble/post-quantum": "^0.5.2", "@scure/bip32": "^1.4.0", "@scure/bip39": "^1.3.0", "eventemitter3": "^5.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13cc2c9f9..be2de4bbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@noble/hashes': specifier: ^1.5.0 version: 1.8.0 + '@noble/post-quantum': + specifier: ^0.5.2 + version: 0.5.2 '@scure/bip32': specifier: ^1.4.0 version: 1.7.0 @@ -2830,11 +2833,31 @@ packages: '@noble/hashes': 1.8.0 dev: false + /@noble/curves@2.0.1: + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + dependencies: + '@noble/hashes': 2.0.1 + dev: false + /@noble/hashes@1.8.0: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} dev: false + /@noble/hashes@2.0.1: + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + dev: false + + /@noble/post-quantum@0.5.2: + resolution: {integrity: sha512-etMDBkCuB95Xj/gfsWYBD2x+84IjL4uMLd/FhGoUUG/g+eh0K2eP7pJz1EmvpN8Df3vKdoWVAc7RxIBCHQfFHQ==} + engines: {node: '>= 20.19.0'} + dependencies: + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} diff --git a/src/account/SingleKeyAccount.ts b/src/account/SingleKeyAccount.ts index 9714c25e8..85999e998 100644 --- a/src/account/SingleKeyAccount.ts +++ b/src/account/SingleKeyAccount.ts @@ -8,6 +8,7 @@ import { KeylessSignature, PrivateKeyInput, Secp256k1PrivateKey, + SlhDsaSha2128sPrivateKey, Signature, } from "../core/crypto"; import type { Account } from "./Account"; @@ -148,6 +149,9 @@ export class SingleKeyAccount implements Account, SingleKeySigner { case SigningSchemeInput.Secp256k1Ecdsa: privateKey = Secp256k1PrivateKey.generate(); break; + case SigningSchemeInput.SlhDsaSha2128s: + privateKey = SlhDsaSha2128sPrivateKey.generate(); + break; default: throw new Error(`Unsupported signature scheme ${scheme}`); } @@ -177,6 +181,9 @@ export class SingleKeyAccount implements Account, SingleKeySigner { case SigningSchemeInput.Secp256k1Ecdsa: privateKey = Secp256k1PrivateKey.fromDerivationPath(path, mnemonic); break; + case SigningSchemeInput.SlhDsaSha2128s: + privateKey = SlhDsaSha2128sPrivateKey.fromDerivationPath(path, mnemonic); + break; default: throw new Error(`Unsupported signature scheme ${scheme}`); } diff --git a/src/core/crypto/index.ts b/src/core/crypto/index.ts index 352ac8a31..5e1d90a56 100644 --- a/src/core/crypto/index.ts +++ b/src/core/crypto/index.ts @@ -14,6 +14,7 @@ export * from "./privateKey"; export * from "./publicKey"; export * from "./secp256k1"; export * from "./secp256r1"; +export * from "./slhDsaSha2128s"; export * from "./signature"; export * from "./singleKey"; export * from "./types"; diff --git a/src/core/crypto/privateKey.ts b/src/core/crypto/privateKey.ts index 28071d3a0..161102d78 100644 --- a/src/core/crypto/privateKey.ts +++ b/src/core/crypto/privateKey.ts @@ -46,6 +46,7 @@ export class PrivateKey { [PrivateKeyVariants.Ed25519]: "ed25519-priv-", [PrivateKeyVariants.Secp256k1]: "secp256k1-priv-", [PrivateKeyVariants.Secp256r1]: "secp256r1-priv-", + [PrivateKeyVariants.SlhDsaSha2128s]: "slh-dsa-sha2-128s-priv-", }; /** diff --git a/src/core/crypto/singleKey.ts b/src/core/crypto/singleKey.ts index 85cae0415..2dff5a415 100644 --- a/src/core/crypto/singleKey.ts +++ b/src/core/crypto/singleKey.ts @@ -9,13 +9,14 @@ import { AuthenticationKey } from "../authenticationKey"; import { Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature } from "./ed25519"; import { AccountPublicKey, PublicKey } from "./publicKey"; import { Secp256k1PrivateKey, Secp256k1PublicKey, Secp256k1Signature } from "./secp256k1"; +import { SlhDsaSha2128sPrivateKey, SlhDsaSha2128sPublicKey, SlhDsaSha2128sSignature } from "./slhDsaSha2128s"; import { KeylessPublicKey, KeylessSignature } from "./keyless"; import { Signature } from "./signature"; import { FederatedKeylessPublicKey } from "./federatedKeyless"; import { AptosConfig } from "../../api"; import { Secp256r1PublicKey, WebAuthnSignature } from "./secp256r1"; -export type PrivateKeyInput = Ed25519PrivateKey | Secp256k1PrivateKey; +export type PrivateKeyInput = Ed25519PrivateKey | Secp256k1PrivateKey | SlhDsaSha2128sPrivateKey; /** * Represents any public key supported by Aptos. @@ -62,6 +63,8 @@ export class AnyPublicKey extends AccountPublicKey { this.variant = AnyPublicKeyVariant.Secp256k1; } else if (publicKey instanceof Secp256r1PublicKey) { this.variant = AnyPublicKeyVariant.Secp256r1; + } else if (publicKey instanceof SlhDsaSha2128sPublicKey) { + this.variant = AnyPublicKeyVariant.SlhDsaSha2128s; } else if (publicKey instanceof KeylessPublicKey) { this.variant = AnyPublicKeyVariant.Keyless; } else if (publicKey instanceof FederatedKeylessPublicKey) { @@ -194,6 +197,9 @@ export class AnyPublicKey extends AccountPublicKey { case AnyPublicKeyVariant.Secp256r1: publicKey = Secp256r1PublicKey.deserialize(deserializer); break; + case AnyPublicKeyVariant.SlhDsaSha2128s: + publicKey = SlhDsaSha2128sPublicKey.deserialize(deserializer); + break; case AnyPublicKeyVariant.Keyless: publicKey = KeylessPublicKey.deserialize(deserializer); break; @@ -284,6 +290,8 @@ export class AnySignature extends Signature { this.variant = AnySignatureVariant.Ed25519; } else if (signature instanceof Secp256k1Signature) { this.variant = AnySignatureVariant.Secp256k1; + } else if (signature instanceof SlhDsaSha2128sSignature) { + this.variant = AnySignatureVariant.SlhDsaSha2128s; } else if (signature instanceof WebAuthnSignature) { this.variant = AnySignatureVariant.WebAuthn; } else if (signature instanceof KeylessSignature) { @@ -326,6 +334,9 @@ export class AnySignature extends Signature { case AnySignatureVariant.Secp256k1: signature = Secp256k1Signature.deserialize(deserializer); break; + case AnySignatureVariant.SlhDsaSha2128s: + signature = SlhDsaSha2128sSignature.deserialize(deserializer); + break; case AnySignatureVariant.WebAuthn: signature = WebAuthnSignature.deserialize(deserializer); break; diff --git a/src/core/crypto/slhDsaSha2128s.ts b/src/core/crypto/slhDsaSha2128s.ts new file mode 100644 index 000000000..2064f4aaf --- /dev/null +++ b/src/core/crypto/slhDsaSha2128s.ts @@ -0,0 +1,503 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { slh_dsa_sha2_128s } from "@noble/post-quantum/slh-dsa.js"; +import { Serializable, Deserializer, Serializer } from "../../bcs"; +import { Hex } from "../hex"; +import { HexInput, PrivateKeyVariants } from "../../types"; +import { PrivateKey } from "./privateKey"; +import { PublicKey } from "./publicKey"; +import { Signature } from "./signature"; +import { convertSigningMessage } from "./utils"; +import { AptosConfig } from "../../api"; +import { CKDPriv, deriveKey, HARDENED_OFFSET, isValidHardenedPath, mnemonicToSeed, splitPath } from "./hdKey"; + +/** + * Represents a SLH-DSA-SHA2-128s public key. + * + * @extends PublicKey + * @property LENGTH - The length of the SLH-DSA-SHA2-128s public key in bytes (32 bytes). + * @group Implementation + * @category Serialization + */ +export class SlhDsaSha2128sPublicKey extends PublicKey { + /** + * Length of SLH-DSA-SHA2-128s public key + * @group Implementation + * @category Serialization + */ + static readonly LENGTH: number = 32; + + /** + * Hex value of the public key + * @private + * @group Implementation + * @category Serialization + */ + private readonly key: Hex; + + /** + * Identifier to distinguish from other public key types + * @group Implementation + * @category Serialization + */ + public readonly keyType: string = "slh-dsa-sha2-128s"; + + /** + * Create a new PublicKey instance from a HexInput, which can be a string or Uint8Array. + * This constructor validates the length of the provided public key data. + * + * @param hexInput - A HexInput (string or Uint8Array) representing the public key data. + * @throws Error if the length of the public key data is not equal to SlhDsaSha2128sPublicKey.LENGTH. + * @group Implementation + * @category Serialization + */ + constructor(hexInput: HexInput) { + super(); + + const hex = Hex.fromHexInput(hexInput); + const { length } = hex.toUint8Array(); + if (length !== SlhDsaSha2128sPublicKey.LENGTH) { + throw new Error(`PublicKey length should be ${SlhDsaSha2128sPublicKey.LENGTH}, received ${length}`); + } + this.key = hex; + } + + // region PublicKey + + /** + * Verifies a SLH-DSA-SHA2-128s signature against the public key. + * + * This function checks the validity of a signature for a given message. + * + * @param args - The arguments for verifying the signature. + * @param args.message - The message that was signed. + * @param args.signature - The signature to verify against the public key. + * @group Implementation + * @category Serialization + */ + verifySignature(args: { message: HexInput; signature: SlhDsaSha2128sSignature }): boolean { + const { message, signature } = args; + const messageToVerify = convertSigningMessage(message); + const messageBytes = Hex.fromHexInput(messageToVerify).toUint8Array(); + const signatureBytes = signature.toUint8Array(); + const publicKeyBytes = this.key.toUint8Array(); + return slh_dsa_sha2_128s.verify(signatureBytes, messageBytes, publicKeyBytes); + } + + /** + * Note: SLH-DSA-SHA2-128s signatures can be verified synchronously. + * + * Verifies the provided signature against the given message. + * This function helps ensure the integrity and authenticity of the message by confirming that the signature is valid. + * + * @param args - The arguments for signature verification. + * @param args.aptosConfig - The configuration object for connecting to the Aptos network + * @param args.message - The message that was signed. + * @param args.signature - The signature to verify, which must be an instance of SlhDsaSha2128sSignature. + * @returns A boolean indicating whether the signature is valid for the given message. + * @group Implementation + * @category Serialization + */ + async verifySignatureAsync(args: { + aptosConfig: AptosConfig; + message: HexInput; + signature: SlhDsaSha2128sSignature; + }): Promise { + return this.verifySignature(args); + } + + /** + * Get the data as a Uint8Array representation. + * + * @returns Uint8Array representation of the data. + * @group Implementation + * @category Serialization + */ + toUint8Array(): Uint8Array { + return this.key.toUint8Array(); + } + + // endregion + + // region Serializable + + /** + * Serializes the data into a byte array using the provided serializer. + * This function is essential for converting data into a format suitable for transmission or storage. + * + * @param serializer - The serializer instance used to convert the data. + * @group Implementation + * @category Serialization + */ + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.key.toUint8Array()); + } + + /** + * Deserializes a SlhDsaSha2128sPublicKey from the provided deserializer. + * This function allows you to reconstruct a SlhDsaSha2128sPublicKey object from its serialized byte representation. + * + * @param deserializer - The deserializer instance used to read the serialized data. + * @group Implementation + * @category Serialization + */ + static deserialize(deserializer: Deserializer): SlhDsaSha2128sPublicKey { + const bytes = deserializer.deserializeBytes(); + return new SlhDsaSha2128sPublicKey(bytes); + } + + // endregion + + /** + * Determine if the provided public key is an instance of SlhDsaSha2128sPublicKey. + * + * @deprecated use `instanceof SlhDsaSha2128sPublicKey` instead + * @param publicKey - The public key to check. + * @group Implementation + * @category Serialization + */ + static isPublicKey(publicKey: PublicKey): publicKey is SlhDsaSha2128sPublicKey { + return publicKey instanceof SlhDsaSha2128sPublicKey; + } + + /** + * Determines if the provided public key is a valid instance of a SLH-DSA-SHA2-128s public key. + * This function checks for the presence of a "key" property and validates the length of the key data. + * + * @param publicKey - The public key to validate. + * @returns A boolean indicating whether the public key is a valid SLH-DSA-SHA2-128s public key. + * @group Implementation + * @category Serialization + */ + static isInstance(publicKey: PublicKey): publicKey is SlhDsaSha2128sPublicKey { + return ( + "key" in publicKey && + (publicKey.key as any)?.data?.length === SlhDsaSha2128sPublicKey.LENGTH && + "keyType" in publicKey && + (publicKey as any).keyType === "slh-dsa-sha2-128s" + ); + } +} + +/** + * Represents a SLH-DSA-SHA2-128s private key, providing functionality to create, sign messages, + * derive public keys, and serialize/deserialize the key. + * @group Implementation + * @category Serialization + */ +export class SlhDsaSha2128sPrivateKey extends Serializable implements PrivateKey { + /** + * Length of SLH-DSA-SHA2-128s private key (48 bytes: SK seed + PRF seed + PK seed) + * @group Implementation + * @category Serialization + */ + static readonly LENGTH: number = 48; + + /** + * The SLH-DSA-SHA2-128s key seed to use for BIP-32 compatibility + * See more {@link https://github.com/satoshilabs/slips/blob/master/slip-0010.md} + * + * TODO: This is not standardized... AFAIK. + * + * @group Implementation + * @category Serialization + */ + static readonly SLIP_0010_SEED = "SLH-DSA-SHA2-128s seed"; + + /** + * The 48-byte three seeds (SK seed + PRF seed + PK seed) used for serialization + * @private + * @group Implementation + * @category Serialization + */ + private readonly threeSeeds: Hex; + + /** + * The full secret key from noble-post-quantum, computed from the three seeds. + * @private + * @group Implementation + * @category Serialization + */ + private readonly secretKey: Uint8Array; + + // region Constructors + + /** + * Create a new PrivateKey instance from a Uint8Array or String. + * + * [Read about AIP-80](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md) + * + * @param hexInput A HexInput (string or Uint8Array) + * @param strict If true, private key must AIP-80 compliant. + * @group Implementation + * @category Serialization + */ + constructor(hexInput: HexInput, strict?: boolean) { + super(); + + const privateKeyHex = PrivateKey.parseHexInput(hexInput, PrivateKeyVariants.SlhDsaSha2128s, strict); + if (privateKeyHex.toUint8Array().length !== SlhDsaSha2128sPrivateKey.LENGTH) { + throw new Error(`PrivateKey length should be ${SlhDsaSha2128sPrivateKey.LENGTH}`); + } + + this.threeSeeds = privateKeyHex; + // Compute the secret key immediately from the three seeds + const threeSeedsBytes = this.threeSeeds.toUint8Array(); + const keys = slh_dsa_sha2_128s.keygen(threeSeedsBytes); + this.secretKey = keys.secretKey; + } + + /** + * Generate a new random private key. + * The private key contains the 48-byte three seeds (SK seed + PRF seed + PK seed). + * The public key can be derived from these seeds using the publicKey() method. + * + * @returns SlhDsaSha2128sPrivateKey - A newly generated SLH-DSA-SHA2-128s private key. + * @group Implementation + * @category Serialization + */ + static generate(): SlhDsaSha2128sPrivateKey { + // Generate a random 48-byte three seeds (3 * 16 bytes: SK seed + PRF seed + PK seed) + const threeSeeds = new Uint8Array(48); + crypto.getRandomValues(threeSeeds); + const privateKey = new SlhDsaSha2128sPrivateKey(threeSeeds, false); + return privateKey; + } + + // endregion + + // region PrivateKey + + /** + * Sign the given message with the private key. + * This function generates a cryptographic signature for the provided message. + * + * @param message - A message in HexInput format to be signed. + * @returns Signature - The generated signature for the provided message. + * @group Implementation + * @category Serialization + */ + sign(message: HexInput): SlhDsaSha2128sSignature { + const messageToSign = convertSigningMessage(message); + const messageBytes = Hex.fromHexInput(messageToSign).toUint8Array(); + // Use the pre-computed secret key for fast signing + const signatureBytes = slh_dsa_sha2_128s.sign(messageBytes, this.secretKey); + return new SlhDsaSha2128sSignature(signatureBytes); + } + + /** + * Derives a private key from a mnemonic seed phrase using a specified BIP44 path. + * To derive multiple keys from the same phrase, change the path + * + * IMPORTANT: SLH-DSA-SHA2-128s supports hardened derivation only, as it lacks a key homomorphism, making non-hardened derivation impossible. + * + * @param path - The BIP44 path used for key derivation. + * @param mnemonics - The mnemonic seed phrase from which the key will be derived. + * @throws Error if the provided path is not a valid hardened path. + * @group Implementation + * @category Serialization + */ + static fromDerivationPath(path: string, mnemonics: string): SlhDsaSha2128sPrivateKey { + if (!isValidHardenedPath(path)) { + throw new Error(`Invalid derivation path ${path}`); + } + return SlhDsaSha2128sPrivateKey.fromDerivationPathInner(path, mnemonicToSeed(mnemonics)); + } + + /** + * Derives a child private key from a given BIP44 path and seed. + * + * We derive our 48-byte SLH-DSA key (three 16-byte seeds) from: + * - the 32-byte, BIP-32-derived, secret key + * - the first 16 bytes of the BIP-32-derived chain code + * + * @param path - The BIP44 path used for key derivation. + * @param seed - The seed phrase created by the mnemonics, represented as a Uint8Array. + * @param offset - The offset used for key derivation, defaults to HARDENED_OFFSET. + * @returns An instance of SlhDsaSha2128sPrivateKey derived from the specified path and seed. + * @group Implementation + * @category Serialization + */ + private static fromDerivationPathInner( + path: string, + seed: Uint8Array, + offset = HARDENED_OFFSET, + ): SlhDsaSha2128sPrivateKey { + const { key, chainCode } = deriveKey(SlhDsaSha2128sPrivateKey.SLIP_0010_SEED, seed); + + const segments = splitPath(path).map((el) => parseInt(el, 10)); + + // Derive the child key based on the path + const { key: privateKey, chainCode: finalChainCode } = segments.reduce( + (parentKeys, segment) => CKDPriv(parentKeys, segment + offset), + { + key, + chainCode, + }, + ); + + const threeSeeds = new Uint8Array(SlhDsaSha2128sPrivateKey.LENGTH); + threeSeeds.set(privateKey, 0); // First 32 bytes from the derived secret key + + // TODO: We would need to reason about the security of this. + // e.g., is it okay to treat the chain code as public? + threeSeeds.set(finalChainCode.slice(0, 16), 32); // Last 16 bytes from the derived chain code + + return new SlhDsaSha2128sPrivateKey(threeSeeds, false); + } + + /** + * Derive the SlhDsaSha2128sPublicKey from this private key. + * The public key is extracted from the pre-computed secret key. + * + * @returns SlhDsaSha2128sPublicKey The derived public key. + * @group Implementation + * @category Serialization + */ + publicKey(): SlhDsaSha2128sPublicKey { + // Use getPublicKey to extract the public key from the secret key + const publicKeyBytes = slh_dsa_sha2_128s.getPublicKey(this.secretKey); + return new SlhDsaSha2128sPublicKey(publicKeyBytes); + } + + /** + * Get the private key in bytes (Uint8Array). + * Returns the 48-byte three seeds (SK seed + PRF seed + PK seed) for serialization. + * + * @returns The 48-byte three seeds + * @group Implementation + * @category Serialization + */ + toUint8Array(): Uint8Array { + return this.threeSeeds.toUint8Array(); + } + + /** + * Get the private key as a string representation. + * + * @returns string representation of the private key + * @group Implementation + * @category Serialization + */ + toString(): string { + return this.toAIP80String(); + } + + /** + * Get the private key as a hex string with the 0x prefix. + * + * @returns string representation of the private key. + */ + toHexString(): string { + return this.threeSeeds.toString(); + } + + /** + * Get the private key as a AIP-80 compliant hex string. + * + * [Read about AIP-80](https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md) + * + * @returns AIP-80 compliant string representation of the private key. + */ + toAIP80String(): string { + return PrivateKey.formatPrivateKey(this.threeSeeds.toString(), PrivateKeyVariants.SlhDsaSha2128s); + } + + // endregion + + // region Serializable + + serialize(serializer: Serializer): void { + // Serialize only the 48-byte three seeds (not the full secret key) + serializer.serializeBytes(this.threeSeeds.toUint8Array()); + } + + static deserialize(deserializer: Deserializer): SlhDsaSha2128sPrivateKey { + // Deserialize the 48-byte three seeds + const bytes = deserializer.deserializeBytes(); + // The constructor will compute the secret key immediately + return new SlhDsaSha2128sPrivateKey(bytes, false); + } + + // endregion + + /** + * Determines if the provided private key is an instance of SlhDsaSha2128sPrivateKey. + * + * @param privateKey - The private key to be checked. + * + * @deprecated use `instanceof SlhDsaSha2128sPrivateKey` instead + * @group Implementation + * @category Serialization + */ + static isPrivateKey(privateKey: PrivateKey): privateKey is SlhDsaSha2128sPrivateKey { + return privateKey instanceof SlhDsaSha2128sPrivateKey; + } +} + +/** + * Represents a signature of a message signed using a SLH-DSA-SHA2-128s private key. + * + * @group Implementation + * @category Serialization + */ +export class SlhDsaSha2128sSignature extends Signature { + /** + * SLH-DSA-SHA2-128s signatures are 7,856 bytes. + * @group Implementation + * @category Serialization + */ + static readonly LENGTH = 7856; + + /** + * The signature bytes + * @private + * @group Implementation + * @category Serialization + */ + private readonly data: Hex; + + // region Constructors + + /** + * Create a new Signature instance from a Uint8Array or String. + * + * @param hexInput A HexInput (string or Uint8Array) + * @group Implementation + * @category Serialization + */ + constructor(hexInput: HexInput) { + super(); + const data = Hex.fromHexInput(hexInput); + if (data.toUint8Array().length !== SlhDsaSha2128sSignature.LENGTH) { + throw new Error( + `Signature length should be ${SlhDsaSha2128sSignature.LENGTH}, received ${data.toUint8Array().length}`, + ); + } + this.data = data; + } + + // endregion + + // region Signature + + toUint8Array(): Uint8Array { + return this.data.toUint8Array(); + } + + // endregion + + // region Serializable + + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.data.toUint8Array()); + } + + static deserialize(deserializer: Deserializer): SlhDsaSha2128sSignature { + const hex = deserializer.deserializeBytes(); + return new SlhDsaSha2128sSignature(hex); + } + + // endregion +} diff --git a/src/internal/account.ts b/src/internal/account.ts index fd4c8bac8..d01e43f42 100644 --- a/src/internal/account.ts +++ b/src/internal/account.ts @@ -82,7 +82,14 @@ import { GetAuthKeysForPublicKey, GetAccountAddressesForAuthKey, } from "../types/generated/queries"; -import { Secp256k1PrivateKey, AuthenticationKey, Ed25519PrivateKey, createObjectAddress, Hex } from "../core"; +import { + Secp256k1PrivateKey, + AuthenticationKey, + Ed25519PrivateKey, + SlhDsaSha2128sPrivateKey, + createObjectAddress, + Hex, +} from "../core"; import { CurrentFungibleAssetBalancesBoolExp } from "../types/generated/types"; import { getTableItem } from "./table"; import { APTOS_COIN } from "../utils"; @@ -1286,7 +1293,7 @@ async function deriveOwnedAccountsFromKeylessSigner(args: { async function deriveOwnedAccountsFromPrivateKey(args: { aptosConfig: AptosConfig; - privateKey: Ed25519PrivateKey | Secp256k1PrivateKey; + privateKey: Ed25519PrivateKey | Secp256k1PrivateKey | SlhDsaSha2128sPrivateKey; options?: { includeUnverified?: boolean; noMultiKey?: boolean }; }): Promise { const { aptosConfig, privateKey, options } = args; diff --git a/src/types/types.ts b/src/types/types.ts index 7942540c9..49f797533 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -156,11 +156,14 @@ export enum AccountAuthenticatorVariant { /** * Variants of private keys that can comply with the AIP-80 standard. * {@link https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md} + * + * Note: This must match the AIP-80 strings defined in the Rust `aptos-crypto` crate. */ export enum PrivateKeyVariants { Ed25519 = "ed25519", Secp256k1 = "secp256k1", Secp256r1 = "secp256r1", + SlhDsaSha2128s = "slh-dsa-sha2-128s", } /** @@ -172,6 +175,7 @@ export enum AnyPublicKeyVariant { Secp256r1 = 2, Keyless = 3, FederatedKeyless = 4, + SlhDsaSha2128s = 5, } export function anyPublicKeyVariantToString(variant: AnyPublicKeyVariant): string { @@ -186,6 +190,8 @@ export function anyPublicKeyVariantToString(variant: AnyPublicKeyVariant): strin return "keyless"; case AnyPublicKeyVariant.FederatedKeyless: return "federated_keyless"; + case AnyPublicKeyVariant.SlhDsaSha2128s: + return "slh_dsa_sha2_128s"; default: throw new Error("Unknown public key variant"); } @@ -199,6 +205,7 @@ export enum AnySignatureVariant { Secp256k1 = 1, WebAuthn = 2, Keyless = 3, + SlhDsaSha2128s = 4, } /** @@ -1716,6 +1723,10 @@ export enum SigningSchemeInput { * For Secp256k1Ecdsa */ Secp256k1Ecdsa = 2, + /** + * For SlhDsaSha2128s + */ + SlhDsaSha2128s = 3, } /** @@ -1770,4 +1781,12 @@ export type GenerateAccountWithSingleSignerSecp256k1Key = { legacy?: false; }; -export type GenerateAccount = GenerateAccountWithEd25519 | GenerateAccountWithSingleSignerSecp256k1Key; +export type GenerateAccountWithSingleSignerSlhDsaSha2128sKey = { + scheme: SigningSchemeInput.SlhDsaSha2128s; + legacy?: false; +}; + +export type GenerateAccount = + | GenerateAccountWithEd25519 + | GenerateAccountWithSingleSignerSecp256k1Key + | GenerateAccountWithSingleSignerSlhDsaSha2128sKey; diff --git a/tests/e2e/transaction/transactionSubmission.test.ts b/tests/e2e/transaction/transactionSubmission.test.ts index 34a9c3179..b9ad76b57 100644 --- a/tests/e2e/transaction/transactionSubmission.test.ts +++ b/tests/e2e/transaction/transactionSubmission.test.ts @@ -46,6 +46,7 @@ describe("transaction submission", () => { const legacyED25519SenderAccount = Account.generate(); const receiverAccounts = [Account.generate(), Account.generate()]; const singleSignerSecp256k1Account = Account.generate({ scheme: SigningSchemeInput.Secp256k1Ecdsa }); + const singleSignerSlhDsaSha2128sAccount = Account.generate({ scheme: SigningSchemeInput.SlhDsaSha2128s }); const secondarySignerAccount = Account.generate(); const feePayerAccount = Account.generate(); beforeAll(async () => { @@ -53,6 +54,7 @@ describe("transaction submission", () => { contractPublisherAccount, singleSignerED25519SenderAccount, singleSignerSecp256k1Account, + singleSignerSlhDsaSha2128sAccount, legacyED25519SenderAccount, ...receiverAccounts, secondarySignerAccount, @@ -430,6 +432,193 @@ describe("transaction submission", () => { }); }); }); + describe("Single Sender SLH-DSA-SHA2-128s", () => { + describe("single signer", () => { + test("with script payload", async () => { + const transaction = await aptos.transaction.build.simple({ + sender: singleSignerSlhDsaSha2128sAccount.accountAddress, + data: { + bytecode: singleSignerScriptBytecode, + functionArguments: [new U64(1), receiverAccounts[0].accountAddress], + }, + }); + const response = await aptos.signAndSubmitTransaction({ + signer: singleSignerSlhDsaSha2128sAccount, + transaction, + }); + await aptos.waitForTransaction({ + transactionHash: response.hash, + }); + expect(response.signature?.type).toBe("single_sender"); + }); + test("with entry function payload", async () => { + const transaction = await aptos.transaction.build.simple({ + sender: singleSignerSlhDsaSha2128sAccount.accountAddress, + data: { + function: `${contractPublisherAccount.accountAddress}::transfer::transfer`, + functionArguments: [1, receiverAccounts[0].accountAddress], + }, + }); + const response = await aptos.signAndSubmitTransaction({ + signer: singleSignerSlhDsaSha2128sAccount, + transaction, + }); + await aptos.waitForTransaction({ + transactionHash: response.hash, + }); + expect(response.signature?.type).toBe("single_sender"); + }); + }); + describe("multi agent", () => { + test("with script payload", async () => { + const transaction = await aptos.transaction.build.multiAgent({ + sender: singleSignerSlhDsaSha2128sAccount.accountAddress, + secondarySignerAddresses: [secondarySignerAccount.accountAddress], + data: { + bytecode: multiSignerScriptBytecode, + functionArguments: [ + new U64(BigInt(100)), + new U64(BigInt(200)), + receiverAccounts[0].accountAddress, + receiverAccounts[1].accountAddress, + new U64(BigInt(50)), + ], + }, + }); + + const senderAuthenticator = aptos.transaction.sign({ signer: singleSignerSlhDsaSha2128sAccount, transaction }); + const secondarySignerAuthenticator = aptos.transaction.sign({ signer: secondarySignerAccount, transaction }); + + const response = await aptos.transaction.submit.multiAgent({ + transaction, + senderAuthenticator, + additionalSignersAuthenticators: [secondarySignerAuthenticator], + }); + + await aptos.waitForTransaction({ + transactionHash: response.hash, + }); + expect(response.signature?.type).toBe("multi_agent_signature"); + }); + + test( + "with entry function payload", + async () => { + const transaction = await aptos.transaction.build.multiAgent({ + sender: singleSignerSlhDsaSha2128sAccount.accountAddress, + secondarySignerAddresses: [secondarySignerAccount.accountAddress], + data: { + function: `${contractPublisherAccount.accountAddress}::transfer::two_by_two`, + functionArguments: [100, 200, receiverAccounts[0].accountAddress, receiverAccounts[1].accountAddress, 50], + }, + }); + + const senderAuthenticator = aptos.transaction.sign({ + signer: singleSignerSlhDsaSha2128sAccount, + transaction, + }); + const secondarySignerAuthenticator = aptos.transaction.sign({ signer: secondarySignerAccount, transaction }); + + const response = await aptos.transaction.submit.multiAgent({ + transaction, + senderAuthenticator, + additionalSignersAuthenticators: [secondarySignerAuthenticator], + }); + + await aptos.waitForTransaction({ + transactionHash: response.hash, + }); + expect(response.signature?.type).toBe("multi_agent_signature"); + }, + longTestTimeout, + ); + }); + describe("fee payer", () => { + test("with script payload", async () => { + const transaction = await aptos.transaction.build.simple({ + sender: singleSignerSlhDsaSha2128sAccount.accountAddress, + data: { + bytecode: singleSignerScriptBytecode, + functionArguments: [new U64(1), receiverAccounts[0].accountAddress], + }, + withFeePayer: true, + }); + + const senderAuthenticator = aptos.transaction.sign({ signer: singleSignerSlhDsaSha2128sAccount, transaction }); + const feePayerSignerAuthenticator = aptos.transaction.signAsFeePayer({ + signer: feePayerAccount, + transaction, + }); + + const response = await aptos.transaction.submit.simple({ + transaction, + senderAuthenticator, + feePayerAuthenticator: feePayerSignerAuthenticator, + }); + + await aptos.waitForTransaction({ + transactionHash: response.hash, + }); + expect(response.signature?.type).toBe("fee_payer_signature"); + }); + test("with entry function payload", async () => { + const transaction = await aptos.transaction.build.simple({ + sender: singleSignerSlhDsaSha2128sAccount.accountAddress, + data: { + function: `${contractPublisherAccount.accountAddress}::transfer::transfer`, + functionArguments: [1, receiverAccounts[0].accountAddress], + }, + withFeePayer: true, + }); + const senderAuthenticator = aptos.transaction.sign({ signer: singleSignerSlhDsaSha2128sAccount, transaction }); + const feePayerSignerAuthenticator = aptos.transaction.signAsFeePayer({ + signer: feePayerAccount, + transaction, + }); + + const response = await aptos.transaction.submit.simple({ + transaction, + senderAuthenticator, + feePayerAuthenticator: feePayerSignerAuthenticator, + }); + + await aptos.waitForTransaction({ + transactionHash: response.hash, + }); + expect(response.signature?.type).toBe("fee_payer_signature"); + }); + test("with multi agent transaction", async () => { + const transaction = await aptos.transaction.build.multiAgent({ + sender: singleSignerSlhDsaSha2128sAccount.accountAddress, + secondarySignerAddresses: [secondarySignerAccount.accountAddress], + data: { + function: `${contractPublisherAccount.accountAddress}::transfer::two_by_two`, + functionArguments: [100, 200, receiverAccounts[0].accountAddress, receiverAccounts[1].accountAddress, 50], + }, + withFeePayer: true, + }); + + const senderAuthenticator = aptos.transaction.sign({ signer: singleSignerSlhDsaSha2128sAccount, transaction }); + const secondarySignerAuthenticator = aptos.transaction.sign({ signer: secondarySignerAccount, transaction }); + const feePayerSignerAuthenticator = aptos.transaction.signAsFeePayer({ + signer: feePayerAccount, + transaction, + }); + + const response = await aptos.transaction.submit.multiAgent({ + transaction, + senderAuthenticator, + additionalSignersAuthenticators: [secondarySignerAuthenticator], + feePayerAuthenticator: feePayerSignerAuthenticator, + }); + + await aptos.waitForTransaction({ + transactionHash: response.hash, + }); + expect(response.signature?.type).toBe("fee_payer_signature"); + }); + }); + }); describe("Legacy ED25519", () => { describe("single signer", () => { test("with script payload", async () => { diff --git a/tests/unit/hdKey.test.ts b/tests/unit/hdKey.test.ts index becc535fd..4debb532f 100644 --- a/tests/unit/hdKey.test.ts +++ b/tests/unit/hdKey.test.ts @@ -1,5 +1,12 @@ import { secp256k1WalletTestObject, wallet } from "./helper"; -import { Ed25519PrivateKey, Hex, isValidBIP44Path, isValidHardenedPath, Secp256k1PrivateKey } from "../../src"; +import { + Ed25519PrivateKey, + Hex, + isValidBIP44Path, + isValidHardenedPath, + Secp256k1PrivateKey, + SlhDsaSha2128sPrivateKey, +} from "../../src"; describe("Hierarchical Deterministic Key (hdkey)", () => { describe("hardened path", () => { @@ -163,4 +170,49 @@ describe("Hierarchical Deterministic Key (hdkey)", () => { }); }); }); + + // testing HD derivation for slh-dsa-sha2-128s + describe("slh-dsa-sha2-128s", () => { + const slhDsaSha2128s = [ + { + seed: Hex.fromHexInput("000102030405060708090a0b0c0d0e0f"), + vectors: [ + { + chain: "m", + private: "53c14b2681fcca8600d3ac7ce3459d6ceece7abc0a3b4c0376fc845c1b6503d66c5107d9484171c02f9eb0409849fceb", + }, + { + chain: "m/0'", + private: "473049c70d5414379363b94f52de6b194c9f1651cb6a11b80cfee19f6ca67975201cdbec137ff57e2e9cd434fca0680d", + }, + { + chain: "m/0'/1'", + private: "b609bf21df9baae65045e2bb8c200e97fbf86201f58e4187197a60bfc827158e6ba452ffe50f57849c022b24d01ac123", + }, + { + chain: "m/0'/1'/2'", + private: "60b55f07c62916f7f27a96be371a9e87adedb9acabd84d25c41e8aa5c8c326e2e64024132f8833181bc2ab008541ef2b", + }, + { + chain: "m/0'/1'/2'/2'", + private: "fb7242320fc3bc620587cfdfa54c2abed7d034f6c616ade3a9ad1780a099b268ed7a5bea0297711e997ff199b24bb82b", + }, + { + chain: "m/0'/1'/2'/2'/1000000000'", + private: "c72d4e3fb352a785aa375be20e0767ae20edbce1e18de90e0ea6e9d1677d17e84a410bb833bf091655dfabde17cc2011", + }, + ], + }, + ]; + + slhDsaSha2128s.forEach(({ seed, vectors }) => { + vectors.forEach(({ chain, private: privateKey }) => { + it(`should generate correct key pair for ${chain}`, () => { + // eslint-disable-next-line @typescript-eslint/dot-notation + const key = SlhDsaSha2128sPrivateKey["fromDerivationPathInner"](chain, seed.toUint8Array()); + expect(key.toHexString()).toBe(`0x${privateKey}`); + }); + }); + }); + }); }); diff --git a/tests/unit/slhDsaSha2128s.test.ts b/tests/unit/slhDsaSha2128s.test.ts new file mode 100644 index 000000000..93e625d2e --- /dev/null +++ b/tests/unit/slhDsaSha2128s.test.ts @@ -0,0 +1,229 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { + Deserializer, + Hex, + PrivateKey, + PrivateKeyVariants, + Serializer, + SlhDsaSha2128sPrivateKey, + SlhDsaSha2128sPublicKey, + SlhDsaSha2128sSignature, +} from "../../src"; + +/* eslint-disable max-len */ +describe("SlhDsaSha2128sPublicKey", () => { + it("should create the instance correctly without error", () => { + // Generate a private key to get a valid public key + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const publicKeyBytes = privateKey.publicKey().toUint8Array(); + + // Create from Uint8Array + const publicKey = new SlhDsaSha2128sPublicKey(publicKeyBytes); + expect(publicKey).toBeInstanceOf(SlhDsaSha2128sPublicKey); + expect(publicKey.toUint8Array()).toEqual(publicKeyBytes); + + // Create from hex string + const hexStr = Hex.fromHexInput(publicKeyBytes).toString(); + const publicKey2 = new SlhDsaSha2128sPublicKey(hexStr); + expect(publicKey2).toBeInstanceOf(SlhDsaSha2128sPublicKey); + expect(publicKey2.toUint8Array()).toEqual(publicKeyBytes); + }); + + it("should throw an error with invalid hex input length", () => { + const invalidHexInput = new Uint8Array(31); // Invalid length (should be 32) + expect(() => new SlhDsaSha2128sPublicKey(invalidHexInput)).toThrowError( + `PublicKey length should be ${SlhDsaSha2128sPublicKey.LENGTH}`, + ); + }); + + it("should verify the signature correctly", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const pubKey = privateKey.publicKey(); + const message = new TextEncoder().encode("hello aptos"); + const signature = privateKey.sign(message); + + // Verify with correct signed message + expect(pubKey.verifySignature({ message, signature })).toBe(true); + + // Verify with incorrect signed message + const wrongMessage = new TextEncoder().encode("wrong message"); + expect(pubKey.verifySignature({ message: wrongMessage, signature })).toBe(false); + }); + + it("should serialize correctly", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const publicKey = privateKey.publicKey(); + const serializer = new Serializer(); + publicKey.serialize(serializer); + + const serialized = serializer.toUint8Array(); + expect(serialized.length).toBeGreaterThan(SlhDsaSha2128sPublicKey.LENGTH); // Includes length prefix + }); + + it("should deserialize correctly", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const originalPublicKey = privateKey.publicKey(); + const serializer = new Serializer(); + originalPublicKey.serialize(serializer); + + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserializedPublicKey = SlhDsaSha2128sPublicKey.deserialize(deserializer); + + expect(deserializedPublicKey.toUint8Array()).toEqual(originalPublicKey.toUint8Array()); + }); + + it("should serialize and deserialize correctly", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const publicKey = privateKey.publicKey(); + const serializer = new Serializer(); + publicKey.serialize(serializer); + + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserializedPublicKey = SlhDsaSha2128sPublicKey.deserialize(deserializer); + + expect(deserializedPublicKey.toUint8Array()).toEqual(publicKey.toUint8Array()); + }); +}); + +describe("SlhDsaSha2128sPrivateKey", () => { + it("should create the instance correctly without error with AIP-80 compliant private key", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + expect(privateKey).toBeInstanceOf(SlhDsaSha2128sPrivateKey); + expect(privateKey.toString()).toContain("slh-dsa-sha2-128s-priv-"); + }); + + it("should create the instance correctly without error with non-AIP-80 compliant private key", () => { + const originalPrivateKey = SlhDsaSha2128sPrivateKey.generate(); + const privateKeyHex = originalPrivateKey.toHexString(); + const privateKey = new SlhDsaSha2128sPrivateKey(privateKeyHex, false); + expect(privateKey).toBeInstanceOf(SlhDsaSha2128sPrivateKey); + expect(privateKey.toHexString()).toEqual(privateKeyHex); + }); + + it("should create the instance correctly without error with Uint8Array private key", () => { + const originalPrivateKey = SlhDsaSha2128sPrivateKey.generate(); + const privateKeyBytes = originalPrivateKey.toUint8Array(); + const privateKey = new SlhDsaSha2128sPrivateKey(privateKeyBytes, false); + expect(privateKey).toBeInstanceOf(SlhDsaSha2128sPrivateKey); + expect(privateKey.toUint8Array()).toEqual(privateKeyBytes); + }); + + it("should print in AIP-80 format", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + expect(privateKey.toString()).toContain("slh-dsa-sha2-128s-priv-"); + }); + + it("should throw an error with invalid hex input length", () => { + const invalidHexInput = new Uint8Array(47); // Invalid length (should be 48) + expect(() => new SlhDsaSha2128sPrivateKey(invalidHexInput, false)).toThrowError( + `PrivateKey length should be ${SlhDsaSha2128sPrivateKey.LENGTH}`, + ); + }); + + it("should sign the message correctly", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const message = new TextEncoder().encode("hello aptos"); + const signature = privateKey.sign(message); + + expect(signature).toBeInstanceOf(SlhDsaSha2128sSignature); + expect(signature.toUint8Array().length).toBe(SlhDsaSha2128sSignature.LENGTH); + + // Verify the signature + const publicKey = privateKey.publicKey(); + const isValid = publicKey.verifySignature({ message, signature }); + expect(isValid).toBe(true); + }); + + it("should serialize correctly", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const serializer = new Serializer(); + privateKey.serialize(serializer); + + const serialized = serializer.toUint8Array(); + expect(serialized.length).toBeGreaterThan(SlhDsaSha2128sPrivateKey.LENGTH); // Includes length prefix + }); + + it("should deserialize correctly", () => { + const originalPrivateKey = SlhDsaSha2128sPrivateKey.generate(); + const serializer = new Serializer(); + originalPrivateKey.serialize(serializer); + + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserializedPrivateKey = SlhDsaSha2128sPrivateKey.deserialize(deserializer); + + expect(deserializedPrivateKey.toUint8Array()).toEqual(originalPrivateKey.toUint8Array()); + }); + + it("should serialize and deserialize correctly", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const serializer = new Serializer(); + privateKey.serialize(serializer); + + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserializedPrivateKey = SlhDsaSha2128sPrivateKey.deserialize(deserializer); + + expect(deserializedPrivateKey.toUint8Array()).toEqual(privateKey.toUint8Array()); + }); + + it("should derive same public key from randomly-generated private key or from-bytes private key", () => { + const originalPrivateKey = SlhDsaSha2128sPrivateKey.generate(); + const privateKeyBytes = originalPrivateKey.toUint8Array(); + // Create a new private key instance + const privateKey = new SlhDsaSha2128sPrivateKey(privateKeyBytes, false); + // Public key should be derivable + const derivedPublicKey = privateKey.publicKey(); + expect(derivedPublicKey).toBeInstanceOf(SlhDsaSha2128sPublicKey); + expect(derivedPublicKey.toUint8Array()).toEqual(originalPrivateKey.publicKey().toUint8Array()); + }); + + it("should get public key from private key", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const publicKey = privateKey.publicKey(); + expect(publicKey).toBeInstanceOf(SlhDsaSha2128sPublicKey); + expect(publicKey.toUint8Array().length).toBe(SlhDsaSha2128sPublicKey.LENGTH); + }); +}); + +describe("SlhDsaSha2128sSignature", () => { + it("should create an instance correctly without error", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const message = new TextEncoder().encode("hello aptos"); + const signature = privateKey.sign(message); + + expect(signature).toBeInstanceOf(SlhDsaSha2128sSignature); + expect(signature.toUint8Array().length).toBe(SlhDsaSha2128sSignature.LENGTH); + }); + + it("should throw an error with invalid value length", () => { + const invalidSignatureValue = new Uint8Array(SlhDsaSha2128sSignature.LENGTH - 1); // Invalid length + expect(() => new SlhDsaSha2128sSignature(invalidSignatureValue)).toThrowError( + `Signature length should be ${SlhDsaSha2128sSignature.LENGTH}`, + ); + }); + + it("should serialize correctly", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const message = new TextEncoder().encode("hello aptos"); + const signature = privateKey.sign(message); + const serializer = new Serializer(); + signature.serialize(serializer); + + const serialized = serializer.toUint8Array(); + expect(serialized.length).toBeGreaterThan(SlhDsaSha2128sSignature.LENGTH); // Includes length prefix + }); + + it("should deserialize correctly", () => { + const privateKey = SlhDsaSha2128sPrivateKey.generate(); + const message = new TextEncoder().encode("hello aptos"); + const originalSignature = privateKey.sign(message); + const serializer = new Serializer(); + originalSignature.serialize(serializer); + + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserializedSignature = SlhDsaSha2128sSignature.deserialize(deserializer); + + expect(deserializedSignature.toUint8Array()).toEqual(originalSignature.toUint8Array()); + }); +});