From b839a19a87dd4692cbd909e2b68fee455209ea9b Mon Sep 17 00:00:00 2001 From: Mateusz Czeladka Date: Wed, 2 Oct 2024 23:07:24 +0200 Subject: [PATCH] feat: support hash param on CIP-30. --- .../client/cip/cip30/CIP30DataSigner.java | 65 +++++++++++++++---- .../client/cip/cip30/CIP30DataSignerTest.java | 49 +++++++++++++- .../cip/cip8/builder/COSESign1Builder.java | 12 +++- 3 files changed, 111 insertions(+), 15 deletions(-) diff --git a/cip/cip30/src/main/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSigner.java b/cip/cip30/src/main/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSigner.java index a386c7cd..4b0fd213 100644 --- a/cip/cip30/src/main/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSigner.java +++ b/cip/cip30/src/main/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSigner.java @@ -1,6 +1,7 @@ package com.bloxbean.cardano.client.cip.cip30; import co.nstant.in.cbor.model.ByteString; +import co.nstant.in.cbor.model.SimpleValue; import co.nstant.in.cbor.model.UnsignedInteger; import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.Address; @@ -20,14 +21,14 @@ public enum CIP30DataSigner { INSTANCE(); CIP30DataSigner() { - } /** * Sign and create DataSignature in CIP30's signData() format + * * @param addressBytes Address bytes - * @param payload payload bytes to sign - * @param signer signing account + * @param payload payload bytes to sign + * @param signer signing account * @return DataSignature * @throws DataSignError */ @@ -36,7 +37,39 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl byte[] pvtKey = signer.privateKeyBytes(); byte[] pubKey = signer.publicKeyBytes(); - return signData(addressBytes, payload, pvtKey, pubKey); + return signData(addressBytes, payload, pvtKey, pubKey, false); + } + + /** + * Sign and create DataSignature in CIP30's signData() format + * + * @param addressBytes Address bytes + * @param payload payload bytes to sign + * @param signer signing account + * @param hashPayload hash the payload before signing + * @return DataSignature + * @throws DataSignError + */ + public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull Account signer, boolean hashPayload) + throws DataSignError { + byte[] pvtKey = signer.privateKeyBytes(); + byte[] pubKey = signer.publicKeyBytes(); + + return signData(addressBytes, payload, pvtKey, pubKey, hashPayload); + } + + /** + * Sign and create DataSignature in CIP30's signData() format + * + * @param addressBytes Address bytes + * @param payload payload bytes to sign + * @param pvtKey private key bytes + * @param pubKey public key bytes to add + * @return DataSignature + * @throws DataSignError + */ + public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull byte[] pvtKey, @NonNull byte[] pubKey) throws DataSignError { + return signData(addressBytes, payload, pvtKey, pubKey, false); } /** @@ -45,10 +78,11 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl * @param payload payload bytes to sign * @param pvtKey private key bytes * @param pubKey public key bytes to add + * @param hashPayload hash the payload before signing * @return DataSignature * @throws DataSignError */ - public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull byte[] pvtKey, @NonNull byte[] pubKey) + public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payload, @NonNull byte[] pvtKey, @NonNull byte[] pubKey, boolean hashPayload) throws DataSignError { try { HeaderMap protectedHeaderMap = new HeaderMap() @@ -56,16 +90,20 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl .keyId(addressBytes) .addOtherHeader(ADDRESS_KEY, new ByteString(addressBytes)); + HeaderMap unprotectedHeaderMap = new HeaderMap(); + if (hashPayload) { + unprotectedHeaderMap.addOtherHeader("hashed", SimpleValue.TRUE); + } + Headers headers = new Headers() ._protected(new ProtectedHeaderMap(protectedHeaderMap)) - .unprotected(new HeaderMap()); + .unprotected(unprotectedHeaderMap); - COSESign1Builder coseSign1Builder = new COSESign1Builder(headers, payload, false); + COSESign1Builder coseSign1Builder = new COSESign1Builder(headers, payload, false, hashPayload); SigStructure sigStructure = coseSign1Builder.makeDataToSign(); byte[] signature; - if (pvtKey.length >= 64) { //64 bytes expanded pvt key signature = Configuration.INSTANCE.getSigningProvider().signExtended(sigStructure.serializeAsBytes(), pvtKey); } else { //32 bytes pvt key @@ -74,7 +112,6 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl COSESign1 coseSign1 = coseSign1Builder.build(signature); - //COSEKey COSEKey coseKey = new COSEKey() .keyType(OKP) //OKP .keyId(addressBytes) @@ -82,8 +119,10 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl .addOtherHeader(CRV_KEY, new UnsignedInteger(CRV_Ed25519)) //crv Ed25519 .addOtherHeader(X_KEY, new ByteString(pubKey)); //x pub key used to sign sig_structure - return new DataSignature(HexUtil.encodeHexString(coseSign1.serializeAsBytes()), - HexUtil.encodeHexString(coseKey.serializeAsBytes())); + String sig = HexUtil.encodeHexString(coseSign1.serializeAsBytes()); + String key = HexUtil.encodeHexString(coseKey.serializeAsBytes()); + + return new DataSignature(sig, key); } catch (Exception e) { throw new DataSignError("Error signing data", e); } @@ -91,6 +130,7 @@ public DataSignature signData(@NonNull byte[] addressBytes, @NonNull byte[] payl /** * Verify CIP30 signData signature + * * @param dataSignature * @return true if verification is successful, otherwise false */ @@ -106,8 +146,9 @@ public boolean verify(@NonNull DataSignature dataSignature) { .verify(signature, sigStructure.serializeAsBytes(), pubKey); //Verify address - byte[] addressBytes = coseSign1.headers()._protected().getAsHeaderMap().otherHeaderAsBytes(ADDRESS_KEY); + byte[] addressBytes = coseSign1.headers()._protected().getAsHeaderMap().otherHeaderAsBytes(ADDRESS_KEY); Address address = new Address(addressBytes); + boolean addressVerified = AddressProvider.verifyAddress(address, pubKey); return sigVerified && addressVerified; diff --git a/cip/cip30/src/test/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSignerTest.java b/cip/cip30/src/test/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSignerTest.java index 4a2ebee8..633d2879 100644 --- a/cip/cip30/src/test/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSignerTest.java +++ b/cip/cip30/src/test/java/com/bloxbean/cardano/client/cip/cip30/CIP30DataSignerTest.java @@ -12,7 +12,9 @@ import static org.assertj.core.api.Assertions.assertThat; class CIP30DataSignerTest { + 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"; + Account account = new Account(Networks.testnet(), mnemonic); @Test @@ -68,5 +70,50 @@ void verifyNamiSignature_invalidKey() { assertThat(verified).isFalse(); } -} + @Test + void verifyHashedLedgerHardwareWallet() { + DataSignature dataSignature = new DataSignature() + .signature("84582aa201276761646472657373581de103d205532089ad2f7816892e2ef42849b7b52788e41b3fd43a6e01cfa166686173686564f5581c1c1afc33a1ed48205eadcbbda2fc8e61442af2e04673616f21b7d0385840954858f672e9ca51975655452d79a8f106011e9535a2ebfb909f7bbcce5d10d246ae62df2da3a7790edd8f93723cbdfdffc5341d08135b1a40e7a998e8b2ed06") + .key("a4010103272006215820c13745be35c2dfc3fa9523140030dda5b5346634e405662b1aae5c61389c55b3"); + boolean verified = CIP30DataSigner.INSTANCE.verify(dataSignature); + + assertThat(verified).isTrue(); + } + + @Test + void verifySignDataHashedPayload() { + DataSignature dataSignature = new DataSignature() + .signature("845846a2012767616464726573735839003175d03902583e82037438cc86732f6e539f803f9a8b2d4ee164b9d0c77e617030631811f60a1f8a8be26d65a57ff71825b336cc6b76361da166686173686564f44b48656c6c6f20576f726c64584036c2151e1230364b0bf9e40cb65dbdca4c5decf4187e3c5511945d410ea59a1e733b5e68178c234979053ed75b0226ba826fb951c5a79fabf10bddcabda8dc05") + .key("a4010103272006215820a5f73966e73d0bb9eadc75c5857eafd054a0202d716ac6dde00303ee9c0019e3"); + + boolean verified = CIP30DataSigner.INSTANCE.verify(dataSignature); + assertThat(verified).isTrue(); + } + + @Test + void signDataHashedPayload() throws DataSignError { + byte[] payload = "Hello World".getBytes(); + + Address address = new Address(account.baseAddress()); + DataSignature dataSignature = CIP30DataSigner.INSTANCE.signData(address.getBytes(), payload, account, true); + + assertThat(dataSignature).isNotNull(); + assertThat(dataSignature.signature()).isEqualTo("845882a3012704583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e6761646472657373583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0ea166686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d5840d6348538f8c69f5ac30615700b78597dc29795d5fef2aa6165f17ac208b3163b2d2d55405beb6cd8fc66e3beaac1d08b91fae7b9679cc0ae212c65cfe277d608"); + assertThat(dataSignature.key()).isEqualTo("a5010102583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e03272006215820097c8507b71063f99e38147f09eacf76f25576a2ddfac2f40da8feee8dab2d5d"); + assertThat(HexUtil.encodeHexString(dataSignature.address())).isEqualTo("00327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e"); + } + + @Test + public void verifySignedHashedPayload() { + String sig = "845882a3012704583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e6761646472657373583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0ea166686173686564f5581c19790463ef4ad09bdb724e3a6550c640593d4870f6e192ac8147f35d5840d6348538f8c69f5ac30615700b78597dc29795d5fef2aa6165f17ac208b3163b2d2d55405beb6cd8fc66e3beaac1d08b91fae7b9679cc0ae212c65cfe277d608"; + String key = "a5010102583900327d065c4c135860b9ac6a758c9ef032100a724865998a6b1b8219f3d11c3061dfc0c16e14f5b6779fef214eab7aaa3dffdc5e30c1272f0e03272006215820097c8507b71063f99e38147f09eacf76f25576a2ddfac2f40da8feee8dab2d5d"; + + DataSignature dataSig = new DataSignature().signature(sig).key(key); + + boolean isVerified = CIP30DataSigner.INSTANCE.verify(dataSig); + + assertThat(isVerified).isTrue(); + } + +} diff --git a/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1Builder.java b/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1Builder.java index 78910880..79ff2f34 100644 --- a/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1Builder.java +++ b/cip/cip8/src/main/java/com/bloxbean/cardano/client/cip/cip8/builder/COSESign1Builder.java @@ -24,6 +24,13 @@ public COSESign1Builder(Headers headers, byte[] payload, boolean isPayloadExtern this.isPayloadExternal = isPayloadExternal; } + public COSESign1Builder(Headers headers, byte[] payload, boolean isPayloadExternal, boolean isHashed) { + this.headers = headers; + this.payload = payload; + this.isPayloadExternal = isPayloadExternal; + this.hashed = isHashed; + } + public SigStructure makeDataToSign() { Headers headersCopy = headers.copy(); @@ -31,7 +38,7 @@ public SigStructure makeDataToSign() { .sigContext(SigContext.Signature1) .bodyProtected(headersCopy._protected()) .externalAad(externalAad != null ? externalAad.clone() : new byte[0]) - .payload(payload.clone()); + .payload(hashed ? Blake2bUtil.blake2bHash224(payload) : payload.clone()); } public COSESign1 build(byte[] signedSigStructure) { @@ -41,8 +48,9 @@ public COSESign1 build(byte[] signedSigStructure) { byte[] finalPayload; if (hashed) { //blake2b224 hash finalPayload = Blake2bUtil.blake2bHash224(payload); - } else + } else { finalPayload = payload.clone(); + } return new COSESign1() .headers(allHeader)