Skip to content

Commit 472678a

Browse files
committed
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.
1 parent d185518 commit 472678a

File tree

1 file changed

+11
-2
lines changed

1 file changed

+11
-2
lines changed

packages/crypto/src/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,17 @@ const throwWrapUnwrapErrors = (operationError: unknown, wrapError: unknown, oper
4040
)
4141
}
4242

43+
// If the xHD lib is used, the top bit should never be set but we check just in
44+
// case to ensure consistent behavior for any scalar
45+
function assertTopBitNotSet(scalar: bigint): void {
46+
if ((scalar & (1n << 255n)) !== 0n) {
47+
throw new Error('The top bit of the scalar must not be set for ed25519 operations.')
48+
}
49+
}
50+
4351
function rawSign(extendedSecretKey: Uint8Array, data: Uint8Array): Uint8Array {
4452
const scalar = bytesToNumberLE(extendedSecretKey.slice(0, 32))
53+
assertTopBitNotSet(scalar)
4554

4655
const kR = extendedSecretKey.slice(32, 64)
4756

@@ -67,8 +76,8 @@ function rawSign(extendedSecretKey: Uint8Array, data: Uint8Array): Uint8Array {
6776

6877
function rawPubkey(extendedSecretKey: Uint8Array): Uint8Array {
6978
const scalar = bytesToNumberLE(extendedSecretKey.slice(0, 32))
70-
const clearedTopBitScalar = scalar & ((1n << 255n) - 1n)
71-
const reducedScalar = mod(clearedTopBitScalar, ed25519.Point.Fn.ORDER)
79+
assertTopBitNotSet(scalar)
80+
const reducedScalar = mod(scalar, ed25519.Point.Fn.ORDER)
7281

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

0 commit comments

Comments
 (0)