Skip to content

Commit 96e2445

Browse files
authored
#449 - Replaced existing key derivation function using a hardened scheme (#459)
* #449 - replaced existing key derivation function using a hardened scheme * removed unused code * rerun tests
1 parent 6af10e7 commit 96e2445

File tree

5 files changed

+34
-102
lines changed

5 files changed

+34
-102
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Decentralized Web Node (DWN) SDK <!-- omit in toc -->
44

55
Code Coverage
6-
![Statements](https://img.shields.io/badge/statements-97.55%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-94.53%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-93.82%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-97.55%25-brightgreen.svg?style=flat)
6+
![Statements](https://img.shields.io/badge/statements-97.54%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-94.51%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-93.8%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-97.54%25-brightgreen.svg?style=flat)
77

88

99
- [Introduction](#introduction)

package-lock.json

Lines changed: 11 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@
8686
"multiformats": "11.0.2",
8787
"randombytes": "2.1.0",
8888
"readable-stream": "4.4.0",
89-
"secp256k1": "5.0.0",
9089
"ulid": "2.3.0",
9190
"uuid": "8.3.2",
9291
"varint": "6.0.0"

src/utils/secp256k1.ts

Lines changed: 16 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { PrivateJwk, PublicJwk } from '../types/jose-types.js';
22

33
import * as secp256k1 from '@noble/secp256k1';
4-
import secp256k1Derivation from 'secp256k1';
54

65
import { Encoder } from '../utils/encoder.js';
76
import { sha256 } from 'multiformats/hashes/sha2';
@@ -157,67 +156,42 @@ export class Secp256k1 {
157156
}
158157

159158
/**
160-
* Derives a hierarchical deterministic public key.
161-
* @param key Either a private or an uncompressed public key used to derive the descendant public key.
159+
* Derives a hardened hierarchical deterministic public key.
162160
* @returns uncompressed public key
163161
*/
164-
public static async derivePublicKey(key: Uint8Array, relativePath: string[]): Promise<Uint8Array> {
162+
public static async derivePublicKey(privateKey: Uint8Array, relativePath: string[]): Promise<Uint8Array> {
165163
Secp256k1.validateKeyDerivationPath(relativePath);
166164

167-
let currentPublicKey: Uint8Array;
168-
if (key.length === 32) {
169-
// private key is always 32 bytes
170-
currentPublicKey = secp256k1.getPublicKey(key, false); // `false` = uncompressed
171-
} else {
172-
currentPublicKey = key;
173-
}
174-
175-
for (const segment of relativePath) {
176-
const hash = await sha256.encode(Encoder.stringToBytes(segment));
177-
currentPublicKey = Secp256k1.deriveChildPublicKey(currentPublicKey, hash);
178-
}
179-
180-
return currentPublicKey;
165+
// derive the private key first then compute the derived public key from the derive private key
166+
const derivedPrivateKey = await Secp256k1.derivePrivateKey(privateKey, relativePath);
167+
const derivedPublicKey = await Secp256k1.getPublicKey(derivedPrivateKey);
168+
return derivedPublicKey;
181169
}
182170

183171
/**
184-
* Derives a hierarchical deterministic private key.
172+
* Derives a hardened hierarchical deterministic private key.
185173
*/
186174
public static async derivePrivateKey(privateKey: Uint8Array, relativePath: string[]): Promise<Uint8Array> {
187175
Secp256k1.validateKeyDerivationPath(relativePath);
188176

189177
let currentPrivateKey = privateKey;
190178
for (const segment of relativePath) {
191-
const hash = await sha256.encode(Encoder.stringToBytes(segment));
192-
currentPrivateKey = Secp256k1.deriveChildPrivateKey(currentPrivateKey, hash);
179+
const derivationSegment = Encoder.stringToBytes(segment);
180+
currentPrivateKey = await Secp256k1.deriveChildPrivateKey(currentPrivateKey, derivationSegment);
193181
}
194182

195183
return currentPrivateKey;
196184
}
197185

198186
/**
199-
* Derives a child public key using the given tweak input.
200-
*/
201-
public static deriveChildPublicKey(uncompressedPublicKey: Uint8Array, tweakInput: Uint8Array): Uint8Array {
202-
// underlying library requires Buffer as input
203-
const compressedPublicKey = false;
204-
const publicKeyBuffer = Buffer.from(uncompressedPublicKey);
205-
const tweakBuffer = Buffer.from(tweakInput);
206-
const derivedPublicKey = secp256k1Derivation.publicKeyTweakAdd(publicKeyBuffer, tweakBuffer, compressedPublicKey);
207-
return derivedPublicKey;
208-
}
209-
210-
/**
211-
* Derives a child private key using the given tweak input.
187+
* Derives a child private key using the given derivation path segment.
212188
*/
213-
public static deriveChildPrivateKey(privateKey: Uint8Array, tweakInput: Uint8Array): Uint8Array {
214-
// NOTE: passing in private key to v5.0.0 of `secp256k1.privateKeyTweakAdd()` has the side effect of modifying the input private key bytes.
215-
// `secp256k1.publicKeyTweakAdd()` does not have this side effect.
216-
// before there is a fix for it (we can also investigate and submit a PR), cloning the private key to workaround is a MUST
217-
// also underlying library requires Buffer as input
218-
const privateKeyBuffer = Buffer.from(privateKey);
219-
const tweakBuffer = Buffer.from(tweakInput);
220-
const derivedPrivateKey = secp256k1Derivation.privateKeyTweakAdd(privateKeyBuffer, tweakBuffer);
189+
public static async deriveChildPrivateKey(privateKey: Uint8Array, derivationPathSegment: Uint8Array): Promise<Uint8Array> {
190+
// hash the private key & derivation segment
191+
const privateKeyHash = await sha256.encode(privateKey);
192+
const derivationPathSegmentHash = await sha256.encode(derivationPathSegment);
193+
const combinedBytes = secp256k1.etc.concatBytes(privateKeyHash, derivationPathSegmentHash);
194+
const derivedPrivateKey = secp256k1.etc.hashToPrivateKey(combinedBytes);
221195
return derivedPrivateKey;
222196
}
223197

tests/utils/secp256k1.spec.ts

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -42,55 +42,27 @@ describe('Secp256k1', () => {
4242
});
4343
});
4444

45-
describe('deriveChildPublic/PrivateKey()', () => {
46-
it('should derive the same key pair counterparts when given the same tweak input', async () => {
47-
const { publicKey, privateKey } = await Secp256k1.generateKeyPairRaw();
48-
49-
const tweakInput = TestDataGenerator.randomBytes(32);
50-
const derivedPrivateKey = Secp256k1.deriveChildPrivateKey(privateKey, tweakInput);
51-
const derivedPublicKey = Secp256k1.deriveChildPublicKey(publicKey, tweakInput);
52-
53-
const publicKeyFromDerivedPrivateKey = await Secp256k1.getPublicKey(derivedPrivateKey);
54-
expect(ArrayUtility.byteArraysEqual(derivedPublicKey, publicKeyFromDerivedPrivateKey)).to.be.true;
55-
});
56-
});
57-
5845
describe('derivePublic/PrivateKey()', () => {
5946
it('should be able to derive same key using different ancestor along the chain path', async () => {
60-
const { publicKey, privateKey } = await Secp256k1.generateKeyPairRaw();
47+
const { privateKey } = await Secp256k1.generateKeyPairRaw();
6148

6249
const fullPathToG = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
6350
const fullPathToD = ['a', 'b', 'c', 'd'];
6451
const relativePathFromDToG = ['e', 'f', 'g'];
6552

66-
// testing public key derivation from different ancestor in the same chain
67-
const publicKeyG = await Secp256k1.derivePublicKey(publicKey, fullPathToG);
68-
const publicKeyD = await Secp256k1.derivePublicKey(publicKey, fullPathToD);
69-
const publicKeyGFromD = await Secp256k1.derivePublicKey(publicKeyD, relativePathFromDToG);
70-
expect(ArrayUtility.byteArraysEqual(publicKeyG, publicKeyGFromD)).to.be.true;
71-
7253
// testing private key derivation from different ancestor in the same chain
7354
const privateKeyG = await Secp256k1.derivePrivateKey(privateKey, fullPathToG);
7455
const privateKeyD = await Secp256k1.derivePrivateKey(privateKey, fullPathToD);
7556
const privateKeyGFromD = await Secp256k1.derivePrivateKey(privateKeyD, relativePathFromDToG);
7657
expect(ArrayUtility.byteArraysEqual(privateKeyG, privateKeyGFromD)).to.be.true;
7758

78-
// testing that the derived private key matches up with the derived public key
79-
const publicKeyGFromPrivateKeyG = await Secp256k1.getPublicKey(privateKeyG);
80-
expect(ArrayUtility.byteArraysEqual(publicKeyGFromPrivateKeyG, publicKeyG)).to.be.true;
81-
});
82-
83-
it('should derive the same public key using either the private or public counterpart of the same key pair', async () => {
84-
const { publicKey, privateKey } = await Secp256k1.generateKeyPairRaw();
85-
86-
const path = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'];
87-
88-
const derivedKeyFromPublicKey = await Secp256k1.derivePublicKey(publicKey, path);
89-
const derivedKeyFromPrivateKey = await Secp256k1.derivePublicKey(privateKey, path);
90-
expect(ArrayUtility.byteArraysEqual(derivedKeyFromPublicKey, derivedKeyFromPrivateKey)).to.be.true;
59+
// testing public key derivation from different ancestor private key in the same chain
60+
const publicKeyG = await Secp256k1.derivePublicKey(privateKey, fullPathToG);
61+
const publicKeyGFromD = await Secp256k1.derivePublicKey(privateKeyD, relativePathFromDToG);
62+
expect(ArrayUtility.byteArraysEqual(publicKeyG, publicKeyGFromD)).to.be.true;
9163
});
9264

93-
it('should derive the same public key using either the private or public counterpart of the same key pair', async () => {
65+
it('should throw if derivation path is invalid', async () => {
9466
const { publicKey, privateKey } = await Secp256k1.generateKeyPairRaw();
9567

9668
const invalidPath = ['should not have segment with empty string', ''];

0 commit comments

Comments
 (0)