Skip to content

Commit bcbfb70

Browse files
committed
add support for deriving SKs from mnemonics (experimental)
1 parent 66e01f9 commit bcbfb70

File tree

3 files changed

+116
-5
lines changed

3 files changed

+116
-5
lines changed

src/account/SingleKeyAccount.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,7 @@ export class SingleKeyAccount implements Account, SingleKeySigner {
151151
privateKey = Secp256k1PrivateKey.generate();
152152
break;
153153
case SigningSchemeInput.SlhDsaSha2128s:
154-
// For SLH-DSA, we need to generate a key pair since public key cannot be derived from private key
155-
const keyPair = SlhDsaSha2128sKeyPair.generate();
156-
privateKey = keyPair.privateKey;
154+
privateKey = SlhDsaSha2128sPrivateKey.generate();
157155
break;
158156
default:
159157
throw new Error(`Unsupported signature scheme ${scheme}`);
@@ -185,7 +183,8 @@ export class SingleKeyAccount implements Account, SingleKeySigner {
185183
privateKey = Secp256k1PrivateKey.fromDerivationPath(path, mnemonic);
186184
break;
187185
case SigningSchemeInput.SlhDsaSha2128s:
188-
throw new Error("SLH-DSA-SHA2-128s does not support derivation from mnemonic path");
186+
privateKey = SlhDsaSha2128sPrivateKey.fromDerivationPath(path, mnemonic);
187+
break;
189188
default:
190189
throw new Error(`Unsupported signature scheme ${scheme}`);
191190
}

src/core/crypto/slhDsaSha2128s.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PublicKey } from "./publicKey";
1010
import { Signature } from "./signature";
1111
import { convertSigningMessage } from "./utils";
1212
import { AptosConfig } from "../../api";
13+
import { CKDPriv, deriveKey, HARDENED_OFFSET, isValidHardenedPath, mnemonicToSeed, splitPath } from "./hdKey";
1314

1415
/**
1516
* Represents a SLH-DSA-SHA2-128s public key.
@@ -193,6 +194,17 @@ export class SlhDsaSha2128sPrivateKey extends Serializable implements PrivateKey
193194
*/
194195
static readonly LENGTH: number = 48;
195196

197+
/**
198+
* The SLH-DSA-SHA2-128s key seed to use for BIP-32 compatibility
199+
* See more {@link https://github.com/satoshilabs/slips/blob/master/slip-0010.md}
200+
*
201+
* TODO: This is not standardized... AFAIK.
202+
*
203+
* @group Implementation
204+
* @category Serialization
205+
*/
206+
static readonly SLIP_0010_SEED = "SLH-DSA-SHA2-128s seed";
207+
196208
/**
197209
* The 48-byte three seeds (SK seed + PRF seed + PK seed) used for serialization
198210
* @private
@@ -274,6 +286,61 @@ export class SlhDsaSha2128sPrivateKey extends Serializable implements PrivateKey
274286
return new SlhDsaSha2128sSignature(signatureBytes);
275287
}
276288

289+
290+
/**
291+
* Derives a private key from a mnemonic seed phrase using a specified BIP44 path.
292+
* To derive multiple keys from the same phrase, change the path
293+
*
294+
* IMPORTANT: SLH-DSA-SHA2-128s supports hardened derivation only, as it lacks a key homomorphism, making non-hardened derivation impossible.
295+
*
296+
* @param path - The BIP44 path used for key derivation.
297+
* @param mnemonics - The mnemonic seed phrase from which the key will be derived.
298+
* @throws Error if the provided path is not a valid hardened path.
299+
* @group Implementation
300+
* @category Serialization
301+
*/
302+
static fromDerivationPath(path: string, mnemonics: string): SlhDsaSha2128sPrivateKey {
303+
if (!isValidHardenedPath(path)) {
304+
throw new Error(`Invalid derivation path ${path}`);
305+
}
306+
return SlhDsaSha2128sPrivateKey.fromDerivationPathInner(path, mnemonicToSeed(mnemonics));
307+
}
308+
309+
/**
310+
* Derives a child private key from a given BIP44 path and seed.
311+
*
312+
* We derive our 48-byte SLH-DSA key (three 16-byte seeds) from:
313+
* - the 32-byte, BIP-32-derived, secret key
314+
* - the first 16 bytes of the BIP-32-derived chain code
315+
*
316+
* @param path - The BIP44 path used for key derivation.
317+
* @param seed - The seed phrase created by the mnemonics, represented as a Uint8Array.
318+
* @param offset - The offset used for key derivation, defaults to HARDENED_OFFSET.
319+
* @returns An instance of SlhDsaSha2128sPrivateKey derived from the specified path and seed.
320+
* @group Implementation
321+
* @category Serialization
322+
*/
323+
private static fromDerivationPathInner(path: string, seed: Uint8Array, offset = HARDENED_OFFSET): SlhDsaSha2128sPrivateKey {
324+
const { key, chainCode } = deriveKey(SlhDsaSha2128sPrivateKey.SLIP_0010_SEED, seed);
325+
326+
const segments = splitPath(path).map((el) => parseInt(el, 10));
327+
328+
// Derive the child key based on the path
329+
const { key: privateKey, chainCode: finalChainCode } = segments.reduce((parentKeys, segment) => CKDPriv(parentKeys, segment + offset), {
330+
key,
331+
chainCode,
332+
});
333+
334+
const threeSeeds = new Uint8Array(48);
335+
threeSeeds.set(privateKey, 0); // First 32 bytes from the derived secret key
336+
337+
// TODO: We would need to reason about the security of this.
338+
// e.g., is it okay to treat the chain code as public?
339+
threeSeeds.set(finalChainCode.slice(0, 16), 32); // Last 16 bytes from the derived chain code
340+
341+
return new SlhDsaSha2128sPrivateKey(threeSeeds, false);
342+
}
343+
277344
/**
278345
* Derive the SlhDsaSha2128sPublicKey from this private key.
279346
* The public key is extracted from the pre-computed secret key.

tests/unit/hdKey.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { secp256k1WalletTestObject, wallet } from "./helper";
2-
import { Ed25519PrivateKey, Hex, isValidBIP44Path, isValidHardenedPath, Secp256k1PrivateKey } from "../../src";
2+
import { Ed25519PrivateKey, Hex, isValidBIP44Path, isValidHardenedPath, Secp256k1PrivateKey, SlhDsaSha2128sPrivateKey } from "../../src";
33

44
describe("Hierarchical Deterministic Key (hdkey)", () => {
55
describe("hardened path", () => {
@@ -163,4 +163,49 @@ describe("Hierarchical Deterministic Key (hdkey)", () => {
163163
});
164164
});
165165
});
166+
167+
// testing HD derivation for slh-dsa-sha2-128s
168+
describe("slh-dsa-sha2-128s", () => {
169+
const slhDsaSha2128s = [
170+
{
171+
seed: Hex.fromHexInput("000102030405060708090a0b0c0d0e0f"),
172+
vectors: [
173+
{
174+
chain: "m",
175+
private: "53c14b2681fcca8600d3ac7ce3459d6ceece7abc0a3b4c0376fc845c1b6503d66c5107d9484171c02f9eb0409849fceb",
176+
},
177+
{
178+
chain: "m/0'",
179+
private: "473049c70d5414379363b94f52de6b194c9f1651cb6a11b80cfee19f6ca67975201cdbec137ff57e2e9cd434fca0680d",
180+
},
181+
{
182+
chain: "m/0'/1'",
183+
private: "b609bf21df9baae65045e2bb8c200e97fbf86201f58e4187197a60bfc827158e6ba452ffe50f57849c022b24d01ac123",
184+
},
185+
{
186+
chain: "m/0'/1'/2'",
187+
private: "60b55f07c62916f7f27a96be371a9e87adedb9acabd84d25c41e8aa5c8c326e2e64024132f8833181bc2ab008541ef2b",
188+
},
189+
{
190+
chain: "m/0'/1'/2'/2'",
191+
private: "fb7242320fc3bc620587cfdfa54c2abed7d034f6c616ade3a9ad1780a099b268ed7a5bea0297711e997ff199b24bb82b",
192+
},
193+
{
194+
chain: "m/0'/1'/2'/2'/1000000000'",
195+
private: "c72d4e3fb352a785aa375be20e0767ae20edbce1e18de90e0ea6e9d1677d17e84a410bb833bf091655dfabde17cc2011",
196+
},
197+
],
198+
},
199+
];
200+
201+
slhDsaSha2128s.forEach(({ seed, vectors }) => {
202+
vectors.forEach(({ chain, private: privateKey }) => {
203+
it(`should generate correct key pair for ${chain}`, () => {
204+
// eslint-disable-next-line @typescript-eslint/dot-notation
205+
const key = SlhDsaSha2128sPrivateKey["fromDerivationPathInner"](chain, seed.toUint8Array());
206+
expect(key.toHexString()).toBe(`0x${privateKey}`);
207+
});
208+
});
209+
});
210+
});
166211
});

0 commit comments

Comments
 (0)