Skip to content

Commit b9373d4

Browse files
Merge pull request #1636 from input-output-hk/fix/LW-13126-fix-large-message-signing-on-ledger
fix: fix long message signing on ledger
2 parents 1e1b1ba + 2d33995 commit b9373d4

File tree

2 files changed

+67
-7
lines changed

2 files changed

+67
-7
lines changed

packages/hardware-ledger/src/LedgerKeyAgent.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
3232
import { HID } from 'node-hid';
3333
import { HexBlob, areNumbersEqualInConstantTime, areStringsEqualInConstantTime } from '@cardano-sdk/util';
3434
import { LedgerDevice, LedgerTransportType } from './types';
35+
import { blake2b } from '@cardano-sdk/crypto';
3536
import { getFirstLedgerDevice } from '@ledgerhq/hw-transport-webusb/lib/webusb';
3637
import { str_to_path } from '@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address';
3738
import { toLedgerTx } from './transformers';
@@ -66,9 +67,38 @@ const LedgerConnection = (_LedgerConnection as any).default
6667
: _LedgerConnection;
6768
type LedgerConnection = _LedgerConnection;
6869

70+
const CIP08_SIGN_HASH_THRESHOLD = 100;
71+
6972
const isUsbDevice = (device: any): device is USBDevice =>
7073
typeof USBDevice !== 'undefined' && device instanceof USBDevice;
7174

75+
/* Sets the hashed entry in the COSESign1 CBOR structure */
76+
const setCOSESignHashed = (COSESignCbor: string, isHashed: boolean) => {
77+
const reader = new Serialization.CborReader(HexBlob(COSESignCbor));
78+
reader.readStartArray();
79+
80+
const headers = reader.readEncodedValue();
81+
// Skip hashed entry
82+
reader.readEncodedValue();
83+
const payload = reader.readEncodedValue();
84+
const signature = reader.readEncodedValue();
85+
86+
reader.readEndArray();
87+
88+
const writer = new Serialization.CborWriter();
89+
90+
writer.writeStartArray(4);
91+
writer.writeEncodedValue(headers);
92+
writer.writeStartMap(1);
93+
writer.writeTextString('hashed');
94+
writer.writeBoolean(isHashed);
95+
96+
writer.writeEncodedValue(payload);
97+
writer.writeEncodedValue(signature);
98+
99+
return writer.encodeAsHex();
100+
};
101+
72102
const isDeviceAlreadyOpenError = (error: unknown) => {
73103
if (typeof error !== 'object') return false;
74104
const innerError = (error as any).innerError;
@@ -780,6 +810,7 @@ export class LedgerKeyAgent extends KeyAgentBase {
780810
}
781811

782812
async signCip8Data(request: cip8.Cip8SignDataContext): Promise<Cip30DataSignature> {
813+
const hashPayload = request.payload.length >= CIP08_SIGN_HASH_THRESHOLD;
783814
try {
784815
const dRepPublicKey = await this.derivePublicKey(util.DREP_KEY_DERIVATION_PATH);
785816
const dRepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex();
@@ -797,9 +828,8 @@ export class LedgerKeyAgent extends KeyAgentBase {
797828
? {
798829
address: addressParams,
799830
addressFieldType: MessageAddressFieldType.ADDRESS,
800-
hashPayload: false,
831+
hashPayload,
801832
messageHex: request.payload,
802-
803833
network: {
804834
networkId: this.chainId.networkId,
805835
protocolMagic: this.chainId.networkMagic
@@ -809,7 +839,7 @@ export class LedgerKeyAgent extends KeyAgentBase {
809839
}
810840
: {
811841
addressFieldType: MessageAddressFieldType.KEY_HASH,
812-
hashPayload: false,
842+
hashPayload,
813843
messageHex: request.payload,
814844
preferHexDisplay: false,
815845
signingPath
@@ -830,18 +860,20 @@ export class LedgerKeyAgent extends KeyAgentBase {
830860
protectedHeaders.set_algorithm_id(Label.from_algorithm_id(AlgorithmId.EdDSA));
831861
protectedHeaders.set_header(cip8.CoseLabel.address, CBORValue.new_bytes(addressBytes));
832862

863+
const sigPayload = coreUtils.hexToBytes(hashPayload ? blake2b.hash(request.payload, 28) : request.payload);
833864
const builder = COSESign1Builder.new(
834865
Headers.new(ProtectedHeaderMap.new(protectedHeaders), HeaderMap.new()),
835-
coreUtils.hexToBytes(request.payload),
866+
sigPayload,
836867
false
837868
);
838869

839870
const coseSign1 = builder.build(Buffer.from(result.signatureHex, 'hex'));
840871
const coseKey = cip8.createCoseKey(addressBytes, Crypto.Ed25519PublicKeyHex(result.signingPublicKeyHex));
841872

873+
const coseSigHex = coreUtils.bytesToHex(coseSign1.to_bytes());
842874
return {
843875
key: coreUtils.bytesToHex(coseKey.to_bytes()),
844-
signature: coreUtils.bytesToHex(coseSign1.to_bytes())
876+
signature: hashPayload ? setCOSESignHashed(coseSigHex, true) : coseSigHex
845877
};
846878
} catch (error: any) {
847879
if (error.code === 28_169) {

packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ const cleanupEstablishedConnections = async () => {
4747
LedgerKeyAgent.deviceConnections = [];
4848
};
4949

50-
const signAndDecode = async (signWith: Cardano.PaymentAddress | Cardano.RewardAccount, wallet: BaseWallet) => {
50+
const signAndDecode = async (
51+
signWith: Cardano.PaymentAddress | Cardano.RewardAccount,
52+
wallet: BaseWallet,
53+
message = HexBlob('abc123')
54+
) => {
5155
const dataSignature = await wallet.signData({
52-
payload: HexBlob('abc123'),
56+
payload: message,
5357
signWith
5458
});
5559

@@ -799,6 +803,30 @@ describe('LedgerKeyAgent', () => {
799803
});
800804

801805
describe('CIP-008 Messages', () => {
806+
it('can sign a long message', async () => {
807+
const message = HexBlob(
808+
Buffer.from(
809+
'STAR 00000000000 to addr_test1qpj0zrza6l4caj9q4yfnvugcfum5rls7tkzvezzatp488tev0tc8arve599qvp6r28hsf6tm0cfdm3f70u58rjuycgtsrypwh5 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
810+
'ascii'
811+
).toString('hex')
812+
);
813+
const signWith = (await firstValueFrom(wallet.addresses$))[0].address;
814+
const { coseSign1, publicKeyHex, signedData } = await signAndDecode(signWith as any, wallet, message);
815+
const signedDataBytes = HexBlob.fromBytes(signedData.to_bytes());
816+
const signatureBytes = HexBlob.fromBytes(coseSign1.signature()) as unknown as Crypto.Ed25519SignatureHex;
817+
const cryptoProvider = await Crypto.SodiumBip32Ed25519.create();
818+
819+
testAddressHeader(signedData, signWith);
820+
821+
expect(
822+
cryptoProvider.verify(
823+
signatureBytes,
824+
signedDataBytes,
825+
publicKeyHex as unknown as Crypto.Ed25519PublicKeyHex
826+
)
827+
).toBe(true);
828+
});
829+
802830
it('can sign with reward account', async () => {
803831
const signWith = (await firstValueFrom(wallet.addresses$))[0].rewardAccount;
804832
const { coseSign1, publicKeyHex, signedData } = await signAndDecode(signWith, wallet);

0 commit comments

Comments
 (0)