Skip to content

Commit a049642

Browse files
authored
chore: assert top bit isn't set (#552)
* chore: assert top bit isn't set In the xHD lib, the signing function uses [crypto_scalarmult_ed25519_base_noclamp](https://github.com/algorandfoundation/xHD-Wallet-API-ts/blob/96e7a4be6bca67a4f77252206811f7676e59e5ec/src/x.hd.wallet.api.crypto.ts#L144-L144) to get the public key which [clears the top bit](https://github.com/algorandfoundation/xHD-Wallet-API-ts/blob/9849fb3e90cecfb6348e188ff445b55806bfde00/src/sumo.facade.ts#L106-L106). Then for the signing, the [raw scalar](https://github.com/algorandfoundation/xHD-Wallet-API-ts/blob/96e7a4be6bca67a4f77252206811f7676e59e5ec/src/x.hd.wallet.api.crypto.ts#L156-L156) is used without clearing the top bit. Since this is not an exported function and the keys used are always from the known derivation function (which ensure the top bit is clear), then this is not an issue. In AlgoKit, however, we have no guarantees about where the scalar comes from. As such, it's possible for someone to pass a scalar that does not have the top bit cleared. The two options are to either clear it automatically or error, but since a scalar without the top bit cleared is invalid ed255519 scalar it seems preferable to just throw an error. * test: add scalar top-bit tests
1 parent b0cb993 commit a049642

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

packages/crypto/src/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,12 @@ function rawSign(extendedSecretKey: Uint8Array, data: Uint8Array): Uint8Array {
6767

6868
function rawPubkey(extendedSecretKey: Uint8Array): Uint8Array {
6969
const scalar = bytesToNumberLE(extendedSecretKey.subarray(0, 32))
70-
const clearedTopBitScalar = scalar & ((1n << 255n) - 1n)
71-
const reducedScalar = mod(clearedTopBitScalar, ed25519.Point.Fn.ORDER)
70+
if ((scalar & (1n << 255n)) !== 0n) {
71+
throw new Error(
72+
'Invalid HD-expanded Ed25519 secret scalar: most-significant bit (bit 255) of the 32-byte scalar must be 0 for rawSign/rawPubkey inputs.',
73+
)
74+
}
75+
const reducedScalar = mod(scalar, ed25519.Point.Fn.ORDER)
7276

7377
// pubKey = scalar * G
7478
const publicKey = ed25519.Point.BASE.multiply(reducedScalar)

packages/transact/src/signer.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,51 @@ describe('signer', () => {
217217
)
218218
})
219219

220+
test('wrapped HD extended private key rejects scalar with high bit set during pubkey derivation', async () => {
221+
const extendedKey = new Uint8Array(96)
222+
// Fill with arbitrary data
223+
for (let i = 0; i < 96; i++) {
224+
extendedKey[i] = i
225+
}
226+
// Set bit 255 in scalar (byte 31 of the scalar, which is byte 31 overall)
227+
extendedKey[31] = 0x80
228+
229+
const wrappedHdExtendedPrivateKey = {
230+
unwrapHdExtendedPrivateKey: async () => extendedKey,
231+
wrapHdExtendedPrivateKey: async () => {},
232+
}
233+
234+
await expect(nobleEd25519SigningKeyFromWrappedSecret(wrappedHdExtendedPrivateKey)).rejects.toThrow(
235+
'Invalid HD-expanded Ed25519 secret scalar: most-significant bit (bit 255) of the 32-byte scalar must be 0 for rawSign/rawPubkey inputs.',
236+
)
237+
})
238+
239+
test('wrapped HD extended private key rejects scalar with high bit set during signing', async () => {
240+
// First create a valid key to get a working signer
241+
const { accountGenerator } = await peikertXHdWalletGenerator()
242+
const generated = await accountGenerator(0, 0)
243+
244+
let callCount = 0
245+
const extendedKey = new Uint8Array(generated.extendedPrivateKey)
246+
// Create an invalid version with bit 255 set in scalar
247+
const invalidExtendedKey = new Uint8Array(extendedKey)
248+
invalidExtendedKey[31] = 0x80
249+
250+
const wrappedHdExtendedPrivateKey = {
251+
unwrapHdExtendedPrivateKey: async () => {
252+
callCount++
253+
return callCount === 1 ? extendedKey : invalidExtendedKey
254+
},
255+
wrapHdExtendedPrivateKey: async () => {},
256+
}
257+
258+
const signingKey = await nobleEd25519SigningKeyFromWrappedSecret(wrappedHdExtendedPrivateKey)
259+
260+
await expect(signingKey.rawEd25519Signer(new Uint8Array([1, 2, 3]))).rejects.toThrow(
261+
'Invalid HD-expanded Ed25519 secret scalar: most-significant bit (bit 255) of the 32-byte scalar must be 0 for rawSign/rawPubkey inputs.',
262+
)
263+
})
264+
220265
test('wrapped seed zeroes seed after successful signing', async () => {
221266
const seed = ed.utils.randomSecretKey()
222267
const wrappedSeed = {

0 commit comments

Comments
 (0)