Skip to content

Commit 7b1f060

Browse files
feat: support for CIP-30 payload hash with an extra overloaded methods (#464)
* feat: support hash param on CIP-30. * Refactor COSESign builders for external payload and hashing Refactor `COSESignBuilder` and `COSESign1Builder` to handle external payloads and add support for payload hashing. Update `CIP30DataSigner` logic to remove redundant hashed payload header. Adjust tests to align with these changes and add new test files for Rust code comparisons. --------- Co-authored-by: Mateusz Czeladka <[email protected]>
1 parent 467a2e6 commit 7b1f060

File tree

10 files changed

+242
-24
lines changed

10 files changed

+242
-24
lines changed

cip/cip30/src/main/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSigner.java

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.bloxbean.cardano.client.cip.cip30;
22

33
import co.nstant.in.cbor.model.ByteString;
4+
import co.nstant.in.cbor.model.SimpleValue;
45
import co.nstant.in.cbor.model.UnsignedInteger;
56
import com.bloxbean.cardano.client.account.Account;
67
import com.bloxbean.cardano.client.address.Address;
@@ -17,17 +18,18 @@
1718
* CIP30 signData() implementation to create and verify signature
1819
*/
1920
public enum CIP30DataSigner {
21+
2022
INSTANCE();
2123

2224
CIP30DataSigner() {
23-
2425
}
2526

2627
/**
2728
* Sign and create DataSignature in CIP30's signData() format
29+
*
2830
* @param addressBytes Address bytes
29-
* @param payload payload bytes to sign
30-
* @param signer signing account
31+
* @param payload payload bytes to sign
32+
* @param signer signing account
3133
* @return DataSignature
3234
* @throws DataSignError
3335
*/
@@ -36,7 +38,39 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl
3638
byte[] pvtKey = signer.privateKeyBytes();
3739
byte[] pubKey = signer.publicKeyBytes();
3840

39-
return signData(addressBytes, payload, pvtKey, pubKey);
41+
return signData(addressBytes, payload, pvtKey, pubKey, false);
42+
}
43+
44+
/**
45+
* Sign and create DataSignature in CIP30's signData() format
46+
*
47+
* @param addressBytes Address bytes
48+
* @param payload payload bytes to sign
49+
* @param signer signing account
50+
* @param hashPayload indicates if the payload is expected to be hashed
51+
* @return DataSignature
52+
* @throws DataSignError
53+
*/
54+
public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull Account signer, boolean hashPayload)
55+
throws DataSignError {
56+
byte[] pvtKey = signer.privateKeyBytes();
57+
byte[] pubKey = signer.publicKeyBytes();
58+
59+
return signData(addressBytes, payload, pvtKey, pubKey, hashPayload);
60+
}
61+
62+
/**
63+
* Sign and create DataSignature in CIP30's signData() format
64+
*
65+
* @param addressBytes Address bytes
66+
* @param payload payload bytes to sign
67+
* @param pvtKey private key bytes
68+
* @param pubKey public key bytes to add
69+
* @return DataSignature
70+
* @throws DataSignError
71+
*/
72+
public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull byte[] pvtKey, @NonNull byte[] pubKey) throws DataSignError {
73+
return signData(addressBytes, payload, pvtKey, pubKey, false);
4074
}
4175

4276
/**
@@ -45,10 +79,11 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl
4579
* @param payload payload bytes to sign
4680
* @param pvtKey private key bytes
4781
* @param pubKey public key bytes to add
82+
* @param hashPayload indicates if the payload is expected to be hashed
4883
* @return DataSignature
4984
* @throws DataSignError
5085
*/
51-
public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull byte[] pvtKey, @NonNull byte[] pubKey)
86+
public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull byte[] pvtKey, @NonNull byte[] pubKey, boolean hashPayload)
5287
throws DataSignError {
5388
try {
5489
HeaderMap protectedHeaderMap = new HeaderMap()
@@ -60,12 +95,11 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl
6095
._protected(new ProtectedHeaderMap(protectedHeaderMap))
6196
.unprotected(new HeaderMap());
6297

63-
COSESign1Builder coseSign1Builder = new COSESign1Builder(headers, payload, false);
98+
COSESign1Builder coseSign1Builder = new COSESign1Builder(headers, payload, false).hashed(hashPayload);
6499

65100
SigStructure sigStructure = coseSign1Builder.makeDataToSign();
66101

67102
byte[] signature;
68-
69103
if (pvtKey.length >= 64) { //64 bytes expanded pvt key
70104
signature = Configuration.INSTANCE.getSigningProvider().signExtended(sigStructure.serializeAsBytes(), pvtKey);
71105
} else { //32 bytes pvt key
@@ -74,23 +108,25 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl
74108

75109
COSESign1 coseSign1 = coseSign1Builder.build(signature);
76110

77-
//COSEKey
78111
COSEKey coseKey = new COSEKey()
79112
.keyType(OKP) //OKP
80113
.keyId(addressBytes)
81114
.algorithmId(ALG_EdDSA) //EdDSA
82115
.addOtherHeader(CRV_KEY, new UnsignedInteger(CRV_Ed25519)) //crv Ed25519
83116
.addOtherHeader(X_KEY, new ByteString(pubKey)); //x pub key used to sign sig_structure
84117

85-
return new DataSignature(HexUtil.encodeHexString(coseSign1.serializeAsBytes()),
86-
HexUtil.encodeHexString(coseKey.serializeAsBytes()));
118+
String sig = HexUtil.encodeHexString(coseSign1.serializeAsBytes());
119+
String key = HexUtil.encodeHexString(coseKey.serializeAsBytes());
120+
121+
return new DataSignature(sig, key);
87122
} catch (Exception e) {
88123
throw new DataSignError("Error signing data", e);
89124
}
90125
}
91126

92127
/**
93128
* Verify CIP30 signData signature
129+
*
94130
* @param dataSignature
95131
* @return true if verification is successful, otherwise false
96132
*/
@@ -106,8 +142,9 @@ public boolean verify(@NonNull DataSignature dataSignature) {
106142
.verify(signature, sigStructure.serializeAsBytes(), pubKey);
107143

108144
//Verify address
109-
byte[] addressBytes = coseSign1.headers()._protected().getAsHeaderMap().otherHeaderAsBytes(ADDRESS_KEY);
145+
byte[] addressBytes = coseSign1.headers()._protected().getAsHeaderMap().otherHeaderAsBytes(ADDRESS_KEY);
110146
Address address = new Address(addressBytes);
147+
111148
boolean addressVerified = AddressProvider.verifyAddress(address, pubKey);
112149

113150
return sigVerified && addressVerified;

cip/cip30/src/test/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSignerTest.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
import com.bloxbean.cardano.client.address.Address;
77
import com.bloxbean.cardano.client.cip.cip8.COSEKey;
88
import com.bloxbean.cardano.client.common.model.Networks;
9+
import com.bloxbean.cardano.client.crypto.Blake2bUtil;
910
import com.bloxbean.cardano.client.util.HexUtil;
1011
import org.junit.jupiter.api.Test;
1112

1213
import static org.assertj.core.api.Assertions.assertThat;
1314

1415
class CIP30DataSignerTest {
16+
1517
String mnemonic = "nice orient enjoy teach jump office alert inquiry apart unaware seat tumble unveil device have bullet morning eyebrow time image embody divide version uniform";
18+
1619
Account account = new Account(Networks.testnet(), mnemonic);
1720

1821
@Test
@@ -68,5 +71,50 @@ void verifyNamiSignature_invalidKey() {
6871
assertThat(verified).isFalse();
6972
}
7073

71-
}
74+
@Test
75+
void verifyHashedLedgerHardwareWallet() {
76+
DataSignature dataSignature = new DataSignature()
77+
.signature("84582aa201276761646472657373581de103d205532089ad2f7816892e2ef42849b7b52788e41b3fd43a6e01cfa166686173686564f5581c1c1afc33a1ed48205eadcbbda2fc8e61442af2e04673616f21b7d0385840954858f672e9ca51975655452d79a8f106011e9535a2ebfb909f7bbcce5d10d246ae62df2da3a7790edd8f93723cbdfdffc5341d08135b1a40e7a998e8b2ed06")
78+
.key("a4010103272006215820c13745be35c2dfc3fa9523140030dda5b5346634e405662b1aae5c61389c55b3");
7279

80+
boolean verified = CIP30DataSigner.INSTANCE.verify(dataSignature);
81+
82+
assertThat(verified).isTrue();
83+
}
84+
85+
@Test
86+
void verifySignDataHashedPayload() {
87+
DataSignature dataSignature = new DataSignature()
88+
.signature("845846a2012767616464726573735839003175d03902583e82037438cc86732f6e539f803f9a8b2d4ee164b9d0c77e617030631811f60a1f8a8be26d65a57ff71825b336cc6b76361da166686173686564f44b48656c6c6f20576f726c64584036c2151e1230364b0bf9e40cb65dbdca4c5decf4187e3c5511945d410ea59a1e733b5e68178c234979053ed75b0226ba826fb951c5a79fabf10bddcabda8dc05")
89+
.key("a4010103272006215820a5f73966e73d0bb9eadc75c5857eafd054a0202d716ac6dde00303ee9c0019e3");
90+
91+
boolean verified = CIP30DataSigner.INSTANCE.verify(dataSignature);
92+
assertThat(verified).isTrue();
93+
}
94+
95+
@Test
96+
void signDataHashedPayload() throws DataSignError {
97+
byte[] payload = "Hello World".getBytes();
98+
99+
Address address = new Address(account.baseAddress());
100+
DataSignature dataSignature = CIP30DataSigner.INSTANCE.signData(address.getBytes(), payload, account, true);
101+
102+
assertThat(dataSignature).isNotNull();
103+
assertThat(dataSignature.signature()).isEqualTo("845882a3012704583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e6761646472657373583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0ea166686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d5840d6348538f8c69f5ac30615700b78597dc29795d5fef2aa6165f17ac208b3163b2d2d55405beb6cd8fc66e3beaac1d08b91fae7b9679cc0ae212c65cfe277d608");
104+
assertThat(dataSignature.key()).isEqualTo("a5010102583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e03272006215820097c8507b71063f99e38147f09eacf76f25576a2ddfac2f40da8feee8dab2d5d");
105+
assertThat(HexUtil.encodeHexString(dataSignature.address())).isEqualTo("00327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e");
106+
}
107+
108+
@Test
109+
public void verifySignedHashedPayload() {
110+
String sig = "845882a3012704583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e6761646472657373583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0ea166686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d5840d6348538f8c69f5ac30615700b78597dc29795d5fef2aa6165f17ac208b3163b2d2d55405beb6cd8fc66e3beaac1d08b91fae7b9679cc0ae212c65cfe277d608";
111+
String key = "a5010102583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e03272006215820097c8507b71063f99e38147f09eacf76f25576a2ddfac2f40da8feee8dab2d5d";
112+
113+
DataSignature dataSig = new DataSignature().signature(sig).key(key);
114+
115+
boolean isVerified = CIP30DataSigner.INSTANCE.verify(dataSig);
116+
117+
assertThat(isVerified).isTrue();
118+
}
119+
120+
}

cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/COSESign1.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,4 @@ public SigStructure signedData(byte[] externalAad, byte[] externalPayload) {
113113
.externalAad(externalAad);
114114
}
115115
}
116+

cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/ProtectedHeaderMap.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public static ProtectedHeaderMap deserialize(DataItem dataItem) {
3333
} else {
3434
throw new CborRuntimeException(
3535
String.format("Deserialization error: Invalid type for ProtectedHeaderMap, type: %s, " +
36-
"expected type: ByteString" + dataItem.getMajorType()));
36+
"expected type: ByteString", dataItem.getMajorType()));
3737
}
3838
}
3939

cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1Builder.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,20 @@ public COSESign1Builder(Headers headers, byte[] payload, boolean isPayloadExtern
2626

2727
public SigStructure makeDataToSign() {
2828
Headers headersCopy = headers.copy();
29+
headersCopy.unprotected().addOtherHeader("hashed", hashed ? SimpleValue.TRUE : SimpleValue.FALSE);
30+
31+
byte[] finalPayload;
32+
if (isPayloadExternal) {
33+
finalPayload = payload.clone();
34+
} else {
35+
finalPayload = hashed ? Blake2bUtil.blake2bHash224(payload): payload.clone();
36+
}
2937

3038
return new SigStructure()
3139
.sigContext(SigContext.Signature1)
3240
.bodyProtected(headersCopy._protected())
3341
.externalAad(externalAad != null ? externalAad.clone() : new byte[0])
34-
.payload(payload.clone());
42+
.payload(finalPayload);
3543
}
3644

3745
public COSESign1 build(byte[] signedSigStructure) {

cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESignBuilder.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,17 @@ public COSESignBuilder(Headers headers, byte[] payload, boolean isPayloadExterna
2626
public SigStructure makeDataToSign() {
2727
Headers headersCopy = headers.copy();
2828

29+
byte[] finalPayload;
30+
if (isPayloadExternal) {
31+
finalPayload = payload.clone();
32+
} else
33+
finalPayload = hashed? Blake2bUtil.blake2bHash224(payload): payload.clone();
34+
2935
return new SigStructure()
3036
.sigContext(SigContext.Signature)
3137
.bodyProtected(headersCopy._protected())
3238
.externalAad(externalAad != null ? externalAad.clone() : new byte[0])
33-
.payload(payload.clone());
39+
.payload(finalPayload);
3440
}
3541

3642
public COSESign build(List<COSESignature> coseSignatures) {

cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1BuilderTest.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ void buildCOSESign1() throws CborException {
3535
.unprotected(unpHeadermap);
3636

3737
byte[] payload = "Hello World".getBytes();
38+
System.out.println("Payload: " + HexUtil.encodeHexString(payload));
3839
COSESign1Builder coseSign1Builder = new COSESign1Builder(headers, payload, false)
3940
.hashed(true);
4041

@@ -45,12 +46,14 @@ void buildCOSESign1() throws CborException {
4546

4647
COSESign1 coseSign1 = coseSign1Builder.build(signedSigStructure);
4748
String serHex = HexUtil.encodeHexString(coseSign1.serializeAsBytes());
48-
System.out.println(serHex);
4949

50-
//This hex is the result from message-signing rust impl.
51-
String expected = "8447a2010e033903e7a2386371536f6d65206865616465722076616c756566686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d58400a448415208ba496d5cd58407a05269b8f0fd14a3c690b761b03c58e2ac70dd36a6bb9d0e03c5baa9d68da99af4be2a8245892325535ec3656435505ba182703";
50+
//This hex is the result from message-signing rust impl. (Check cose_sign1_builder.rs)
51+
String expected = "8447a2010e033903e7a2386371536f6d65206865616465722076616c756566686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d58400a810f4fef824d98bb3d08a93f32b2bffb236ecc87100142911605509b953701b0680ce347a13d54e6f626c1f368e69e422d75870db21f8c8ad9f1e40f51ca04";
5252
COSESign1 coseSign12 = COSESign1.deserialize(CborDecoder.decode(HexUtil.decodeHexString(serHex)).get(0));
5353

54+
System.out.println("Serialized Hex: " + serHex.length());
55+
System.out.println("Expected Hex: " + expected.length());
56+
5457
assertThat(serHex).isEqualTo(expected);
5558
assertThat(coseSign12).isEqualTo(coseSign1);
5659
}
@@ -130,7 +133,6 @@ void buildCOSESign1_withPayLoadExTrue_additionalHeaders() throws CborException {
130133

131134
COSESign1 coseSign1 = coseSign1Builder.build(signedSigStructure);
132135
String serHex = HexUtil.encodeHexString(coseSign1.serializeAsBytes());
133-
System.out.println(serHex);
134136

135137
//This hex is the result from message-signing rust impl.
136138
String expected = "8458baa7010e02816d637269746963616c6974792d31033903e704430102030543040506064304050607828340a10281055840030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303038340a20103028108584005050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505aa01181802816d637269746963616c6974792d32033907cf04430102030543040506064304050607828340a10281145840030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303038340a20103028108584005050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505386371736f6d65206865616465722076616c7565646b6579314307080966686173686564f5f65840ba0e7cb56486b33c1fc3fb2730968cf46b9215bcf57dfdec15102cad72391b4ac2f5c6a23fe3e2545b6d0d2381fc5fbb090467e02f74d57eee8380b8cf9d1605";

cip/cip8/src/test/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESignBuilderTest.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.bloxbean.cardano.client.cip.cip8.*;
1010
import com.bloxbean.cardano.client.common.model.Networks;
1111
import com.bloxbean.cardano.client.config.Configuration;
12+
import com.bloxbean.cardano.client.crypto.Blake2bUtil;
1213
import com.bloxbean.cardano.client.crypto.api.SigningProvider;
1314
import com.bloxbean.cardano.client.util.HexUtil;
1415
import org.junit.jupiter.api.Test;
@@ -45,12 +46,14 @@ void buildCOSESign() throws CborException {
4546
String serHex = HexUtil.encodeHexString(coseSign.serializeAsBytes());
4647
System.out.println(serHex);
4748

48-
//This hex is the result from message-signing rust impl.
49-
String expected = "8447a2010e033903e7a2386371536f6d65206865616465722076616c756566686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d828340a238c77819616e6f74686572206164646974696f6e616c20686561646572646b6579316a6b6579312076616c756558408a991fa149aa4ac06cfea4a36f798b06e86cd7231e5dada423893a302de2e7278b7589de9bada77a8e597c5a1916d28787a052f5f19c510c980faae1d4e909078340a239018f781a616e6f74686572206164646974696f6e616c2068656164657232646b6579326a6b6579322076616c7565584093279db72ff01b677f4cdef33fefc0ac48932b5f15e4eb2427553e83127cc3bac4e7b459ff3c39b0d874c22c5250130aba15e3981eccfc0a2f58f53dcb06a90e";
50-
COSESign coseSign2 = COSESign.deserialize(CborDecoder.decode(HexUtil.decodeHexString(serHex)).get(0));
49+
//This hex is the result from message-signing rust impl. (Check cose_sign_builder.rs)
50+
String expected = "8447a2010e033903e7a1386371536f6d65206865616465722076616c7565581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d828340a238c77819616e6f74686572206164646974696f6e616c20686561646572646b6579316a6b6579312076616c7565584098b74a575e435c5506ec80bc4b47aceba462a4edaf785c345c022acb80957ddbdb36177f3a95cee97efdb474bbdcb66db0fe93e9b011523a8a36d8b443dbb5008340a239018f781a616e6f74686572206164646974696f6e616c2068656164657232646b6579326a6b6579322076616c756558406161857b10b1bfa62bdf6f3ae9d751cc361446af41ec79fa2fca8fe67d27f3d8622ad99786539aa3dedd4d7456d5e13d5474f3d72babd37f6dbe09bfc8c12701";
51+
COSESign expectedCoseSign2 = COSESign.deserialize(CborDecoder.decode(HexUtil.decodeHexString(expected)).get(0));
5152

52-
assertThat(serHex).isEqualTo(expected);
53-
assertThat(coseSign2).isEqualTo(coseSign);
53+
//rust message-signing lib doesn't add hashed key to the unprotected headers. So, we are just checking the signatures here
54+
//But rust COSESign1Builder adds hashed key to the unprotected headers. Not sure why?
55+
assertThat(coseSign.signatures().get(0).signature()).endsWith(expectedCoseSign2.signatures().get(0).signature());
56+
assertThat(coseSign.signatures().get(1).signature()).endsWith(expectedCoseSign2.signatures().get(1).signature());
5457
}
5558

5659
@Test

0 commit comments

Comments
 (0)