Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,12 @@ function rawSign(extendedSecretKey: Uint8Array, data: Uint8Array): Uint8Array {

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

// pubKey = scalar * G
const publicKey = ed25519.Point.BASE.multiply(reducedScalar)
Expand Down
45 changes: 45 additions & 0 deletions packages/transact/src/signer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,51 @@ describe('signer', () => {
)
})

test('wrapped HD extended private key rejects scalar with high bit set during pubkey derivation', async () => {
const extendedKey = new Uint8Array(96)
// Fill with arbitrary data
for (let i = 0; i < 96; i++) {
extendedKey[i] = i
}
// Set bit 255 in scalar (byte 31 of the scalar, which is byte 31 overall)
extendedKey[31] = 0x80

const wrappedHdExtendedPrivateKey = {
unwrapHdExtendedPrivateKey: async () => extendedKey,
wrapHdExtendedPrivateKey: async () => {},
}

await expect(nobleEd25519SigningKeyFromWrappedSecret(wrappedHdExtendedPrivateKey)).rejects.toThrow(
'Invalid HD-expanded Ed25519 secret scalar: most-significant bit (bit 255) of the 32-byte scalar must be 0 for rawSign/rawPubkey inputs.',
)
})

test('wrapped HD extended private key rejects scalar with high bit set during signing', async () => {
// First create a valid key to get a working signer
const { accountGenerator } = await peikertXHdWalletGenerator()
const generated = await accountGenerator(0, 0)

let callCount = 0
const extendedKey = new Uint8Array(generated.extendedPrivateKey)
// Create an invalid version with bit 255 set in scalar
const invalidExtendedKey = new Uint8Array(extendedKey)
invalidExtendedKey[31] = 0x80

const wrappedHdExtendedPrivateKey = {
unwrapHdExtendedPrivateKey: async () => {
callCount++
return callCount === 1 ? extendedKey : invalidExtendedKey
},
wrapHdExtendedPrivateKey: async () => {},
}

const signingKey = await nobleEd25519SigningKeyFromWrappedSecret(wrappedHdExtendedPrivateKey)

await expect(signingKey.rawEd25519Signer(new Uint8Array([1, 2, 3]))).rejects.toThrow(
'Invalid HD-expanded Ed25519 secret scalar: most-significant bit (bit 255) of the 32-byte scalar must be 0 for rawSign/rawPubkey inputs.',
)
})

test('wrapped seed zeroes seed after successful signing', async () => {
const seed = ed.utils.randomSecretKey()
const wrappedSeed = {
Expand Down
Loading