Skip to content

Commit

Permalink
feat: added support for unprotected header section, e.g. hashed: true…
Browse files Browse the repository at this point in the history
… that is needed for hardware wallets. (#27)
  • Loading branch information
matiwinnetou authored Oct 1, 2024
1 parent 4658e65 commit 98ba9f6
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 28 deletions.
10 changes: 10 additions & 0 deletions src/main/java/org/cardanofoundation/cip30/CIP30Verifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ public Cip30VerificationResult verify() {
var dataItems = ((Array) coseCbor).getDataItems();
var protectedHeader = (ByteString) dataItems.get(0); // 1

var unprotectedHeaderMap = (Map) dataItems.get(1); // 2

if (unprotectedHeaderMap.getMajorType() != MAP) {
logger.error("Invalid CIP-30 signature. unprotected header structure is not a map.");
return Cip30VerificationResult.createInvalid(CIP8_FORMAT_ERROR);
}

var isHashed = unprotectedHeaderMap.get(new UnicodeString("hashed")) == SimpleValue.TRUE;

var messageByteString = (ByteString) dataItems.get(2); // 3

var ed25519SignatureByteString = (ByteString) dataItems.get(3); // 4
Expand Down Expand Up @@ -179,6 +188,7 @@ public Cip30VerificationResult verify() {
}

var b = Cip30VerificationResult.Builder.newBuilder();
b.isHashed(isHashed);

if (isSignatureVerified && isAddressVerified) {
b.valid();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public class Cip30VerificationResult {
*/
private byte[] cosePayload;

/**
* whether body of the message is hashed rather than full body
*/
private boolean isHashed;

public static class Builder {


Expand All @@ -65,6 +70,8 @@ public static class Builder {

private byte[] cosePayload;

private boolean isHashed;

/**
* Creates an object {@code Builder} in charge of building the class
* {@code Cip30VerificationResult}.
Expand Down Expand Up @@ -114,6 +121,11 @@ public Builder cosePayload(byte[] cosePayload) {
return Builder.this;
}

public Builder isHashed(boolean isHashed) {
this.isHashed = isHashed;
return Builder.this;
}

/**
* Creates an instance of the class {@code Cip30VerificationResult} using the information
* stored.
Expand All @@ -135,6 +147,7 @@ private Cip30VerificationResult(Builder builder) {
this.ed25519Signature = builder.ed25519Signature;
this.message = builder.message;
this.cosePayload = builder.cosePayload;
this.isHashed = builder.isHashed;
}

/**
Expand Down Expand Up @@ -209,6 +222,14 @@ public Optional<String> getAddress(AddressFormat format) {
return cosePayload;
}

/**
* return whether body is hashed or not (e.g. hardware wallet scenario)
* @return
*/
public boolean isHashed() {
return isHashed;
}

/**
* Returns the Ed25519 public key in a specific encoding format and charset.
* <p>
Expand Down Expand Up @@ -380,6 +401,7 @@ public String toString() {
", ed25519Signature=" + ed25519Signature +
", message=" + message +
", cosePayload=" + cosePayload +
", isHashed=" + isHashed +
'}';
}

Expand Down
70 changes: 42 additions & 28 deletions src/test/java/org/cardanofoundation/cip30/CIP30VerifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,35 @@ void validSignatureWithAddressAndPublicKey1() {
var sig = "84584aa3012704581de11d813fd4ab9c1e5f7a35da16f75c2e664edfb2a127fc17a4a7ebbfee6761646472657373581de11d813fd4ab9c1e5f7a35da16f75c2e664edfb2a127fc17a4a7ebbfeea166686173686564f45901740a202020207b0a202020202020202022757269223a202268747470733a2f2f65766f74696e672e63617264616e6f2e6f72672f766f6c7461697265222c0a202020202020202022616374696f6e223a20224c4f47494e222c0a202020202020202022616374696f6e54657874223a20224c6f67696e222c0a202020202020202022736c6f74223a2022313034323536313535222c0a20202020202020202264617461223a207b0a2020202020202020202020202261646472657373223a20227374616b6531757977637a3037353477777075686d36786864706461367539656e796168616a35796e6c63396179356c346d6c6d736a74777a7134222c0a202020202020202020202020226576656e74223a202243465f53554d4d49545f323032335f5445535432222c0a202020202020202020202020226e6574776f726b223a20224d41494e222c0a20202020202020202020202022726f6c65223a2022564f544552220a20202020202020207d0a2020207d0a5840ed875c0a7067c6a50a73195d9e86e6d8d4908de0bb209126a5193ff974844332b307c9a4f9a38978b973b400268cf1bc40bb8326f2ad954ffa262f7c663ff706";
var key = "a5010102581de01d813fd4ab9c1e5f7a35da16f75c2e664edfb2a127fc17a4a7ebbfee03272006215820c4821499cef96eda9c00cdd0bfbcd2abf7d09436ad424ac7288653a8b4252014";

var p = new CIP30Verifier(sig, key);
var cip30Verifier = new CIP30Verifier(sig, key);

var result = p.verify();
var result = cip30Verifier.verify();

assertTrue(result.isValid());

assertTrue(result.getAddress().isPresent(), "Optional address is included in the signature...");

assertEquals("stake1uywcz0754wwpuhm6xhdpda6u9enyahaj5ynlc9ay5l4mlmsjtwzq4", result.getAddress(AddressFormat.TEXT).orElseThrow());
assertTrue(result.getMessage(MessageFormat.TEXT).contains("LOGIN"));
assertFalse(result.isHashed());
}

@Test
void validSignatureWithAddressAndPublicKey2() {
var sig = "84582aa201276761646472657373581de1b83abf370a14870fdfd6ccb35f8b3e62a68e465ed1e096c5a6f5b9d6a166686173686564f4565468697320697320612074657374206d657373616765584042e2bfc4e1929769a0501b884f66794ae3485860f42c01b70fac37f75e40af074c6b2a61b04c6cf8a493c0dced1455b4f1129dbf653ad9801c52ce49ff6d5a0e";
var key = "a40101032720062158202f1867873147cf53c442435723c17e83beeb8e2153851cd73ccfb1b5e68994a4";

var p = new CIP30Verifier(sig, key);
var cip30Verifier = new CIP30Verifier(sig, key);

var result = p.verify();
var result = cip30Verifier.verify();

assertTrue(result.isValid());

assertTrue(result.getAddress().isPresent(), "Optional address is included in the signature...");

assertArrayEquals(decodeHexString("e1b83abf370a14870fdfd6ccb35f8b3e62a68e465ed1e096c5a6f5b9d6"), result.getAddress().orElseThrow());
assertEquals(sig, p.getCOSESign1());
assertEquals(key, p.getCoseKey().orElseThrow());
assertEquals(sig, cip30Verifier.getCOSESign1());
assertEquals(key, cip30Verifier.getCoseKey().orElseThrow());

assertArrayEquals(decodeHexString("2f1867873147cf53c442435723c17e83beeb8e2153851cd73ccfb1b5e68994a4"), result.getEd25519PublicKey());
assertArrayEquals(decodeHexString("42e2bfc4e1929769a0501b884f66794ae3485860f42c01b70fac37f75e40af074c6b2a61b04c6cf8a493c0dced1455b4f1129dbf653ad9801c52ce49ff6d5a0e"), result.getEd25519Signature());
Expand All @@ -58,15 +59,16 @@ void validSignatureWithAddressAndPublicKey2() {

assertEquals("2f1867873147cf53c442435723c17e83beeb8e2153851cd73ccfb1b5e68994a4", result.getEd25519PublicKey(HEX));
assertEquals("846a5369676e617475726531582aa201276761646472657373581de1b83abf370a14870fdfd6ccb35f8b3e62a68e465ed1e096c5a6f5b9d640565468697320697320612074657374206d657373616765", result.getCosePayload(HEX));
assertFalse(result.isHashed());
}

@Test
void validSignatureWithAddressWithEmptyAddressAndPublicKey() {
var sig = "844ca20127676164647265737340a166686173686564f4565468697320697320612074657374206d6573736167655840a6cec002ecec0c7140a029feb9152edb444bbd8a58c6a0a4eceac6a0e30943e53f9ebe029d766a08b4198aaae71d656319fff25780eab816ab0937e6704bb001";
var key = "a401010327200621582052b92d51dc638d085f8663103d5509f0da29bbee418d75f1f2dc7025d69c9643";

var p = new CIP30Verifier(sig, key);
var result = p.verify();
var cip30Verifier = new CIP30Verifier(sig, key);
var result = cip30Verifier.verify();

assertTrue(result.isValid());
assertTrue(result.getAddress().isEmpty(), "address is NOT baked in (serialised in CIP-30).");
Expand All @@ -76,43 +78,46 @@ void validSignatureWithAddressWithEmptyAddressAndPublicKey() {

assertEquals("846a5369676e6174757265314ca2012767616464726573734040565468697320697320612074657374206d657373616765", result.getCosePayload(HEX));
assertEquals("a6cec002ecec0c7140a029feb9152edb444bbd8a58c6a0a4eceac6a0e30943e53f9ebe029d766a08b4198aaae71d656319fff25780eab816ab0937e6704bb001", result.getEd25519Signature(HEX));
assertFalse(result.isHashed());
}

@Test
void validSignatureWitPublicKeyWithEmptyMessage() {
var sig = "84582aa201276761646472657373581de01d813fd4ab9c1e5f7a35da16f75c2e664edfb2a127fc17a4a7ebbfeea166686173686564f44058406ad1822a992684ed10c2802f2c689516254511e92559f19d5288df96f05d002c560d02e0130f73fe2c762170b185d9f9193c3e1efec5f599cb99dfee662d4f0e";
var key = "a4010103272006215820c4821499cef96eda9c00cdd0bfbcd2abf7d09436ad424ac7288653a8b4252014";

var p = new CIP30Verifier(sig, key);
var result = p.verify();
var cip30Verifier = new CIP30Verifier(sig, key);
var result = cip30Verifier.verify();

assertTrue(result.isValid());

assertEquals("", result.getMessage(MessageFormat.TEXT), "message not available.");
assertEquals("stake_test1uqwcz0754wwpuhm6xhdpda6u9enyahaj5ynlc9ay5l4mlms4pyqyg", result.getAddress(AddressFormat.TEXT).orElseThrow());
assertFalse(result.isHashed());
}

@Test
void validSignatureWithoutPublicKeyInKid4_1() {
var sig = "84582aa201276761646472657373581de19090058641fa866e47d656f62be510cb10a90d48b0aafc868f25291ea166686173686564f458ae7b2270726f706f73616c223a2231366436623066393930663563353266393765323338363235623464356362633138333866326439353334313138313664323466643362613234363364666462222c227265717565737465644174223a223734363935373136222c22766f746572223a227374616b6531757867667170767867386167766d6a383665743076326c397a72393370326764667a6332346c797833756a6a6a3873663678763376227d5840ae514d8d246790d728855f69a0ae32b0c5e59f44e00183b20bf110a42d83fa7c209a290b60a65571648220fc36c4efcb9d472e319bf0afdae42fb078085e4206";

var p = new CIP30Verifier(sig);
var result = p.verify();
var cip30Verifier = new CIP30Verifier(sig);
var result = cip30Verifier.verify();

assertFalse(result.isValid());
assertTrue(result.getAddress().isPresent(), "address is available.");

assertEquals("stake1uxgfqpvxg8agvmj86et0v2l9zr93p2gdfzc24lyx3ujjj8sf6xv3v", result.getAddress(AddressFormat.TEXT).orElseThrow());

assertEquals("{\"proposal\":\"16d6b0f990f5c52f97e238625b4d5cbc1838f2d953411816d24fd3ba2463dfdb\",\"requestedAt\":\"74695716\",\"voter\":\"stake1uxgfqpvxg8agvmj86et0v2l9zr93p2gdfzc24lyx3ujjj8sf6xv3v\"}", result.getMessage(MessageFormat.TEXT));
assertFalse(result.isHashed());
}

@Test
void validSignatureWithoutPublicKeyInKid4_2() {
var sig = "84582aa201276761646472657373581de19090058641fa866e47d656f62be510cb10a90d48b0aafc868f25291ea166686173686564f458ae7b2270726f706f73616c223a2231366436623066393930663563353266393765323338363235623464356362633138333866326439353334313138313664323466643362613234363364666462222c227265717565737465644174223a223734363935373136222c22766f746572223a227374616b6531757867667170767867386167766d6a383665743076326c397a72393370326764667a6332346c797833756a6a6a3873663678763376227d5840ae514d8d246790d728855f69a0ae32b0c5e59f44e00183b20bf110a42d83fa7c209a290b60a65571648220fc36c4efcb9d472e319bf0afdae42fb078085e4206";

var p = new CIP30Verifier(sig, (String) null);
var result = p.verify();
var cip30Verifier = new CIP30Verifier(sig, (String) null);
var result = cip30Verifier.verify();

assertFalse(result.isValid());
assertEquals(UNKNOWN, result.getValidationError().orElseThrow());
Expand All @@ -121,15 +126,16 @@ void validSignatureWithoutPublicKeyInKid4_2() {
assertEquals("stake1uxgfqpvxg8agvmj86et0v2l9zr93p2gdfzc24lyx3ujjj8sf6xv3v", result.getAddress(AddressFormat.TEXT).orElseThrow());

assertEquals("{\"proposal\":\"16d6b0f990f5c52f97e238625b4d5cbc1838f2d953411816d24fd3ba2463dfdb\",\"requestedAt\":\"74695716\",\"voter\":\"stake1uxgfqpvxg8agvmj86et0v2l9zr93p2gdfzc24lyx3ujjj8sf6xv3v\"}", result.getMessage(MessageFormat.TEXT));
assertFalse(result.isHashed());
}

@Test
void checkIfExceptionsAreThrown() {
var sig = "844ca20127676164647265737340a166686173686564f4565468697320697320612074657374206d6573736167655840a6cec002ecec0c7140a029feb9152edb444bbd8a58c6a0a4eceac6a0e30943e53f9ebe029d766a08b4198aaae71d656319fff25780eab816ab0937e6704bb001";
var key = "a401010327200621582052b92d51dc638d085f8663103d5509f0da29bbee418d75f1f2dc7025d69c9643";

var p = new CIP30Verifier(sig, key);
var result = p.verify();
var cip30Verifier = new CIP30Verifier(sig, key);
var result = cip30Verifier.verify();

assertThrows(IllegalArgumentException.class, () -> {
result.getEd25519PublicKey(null, UTF_8);
Expand All @@ -148,10 +154,11 @@ void invalidSignatureCheck() {
var sig = "84582aa201276761646472657373581de1b8344f370a14870fdfd6ccb35f8b3e62a68e465ed1e096c5a6f5b9d6a166686173686564f4565468697320697320612074657374206d657373616765584042e2bfc4e1929769a0501b884f66794ae3485860f42c01b70fac37f75e40af074c6b2a61b04c6cf8a493c0dced1455b4f1129dbf653ad9801c52ce49ff6d5a0e";
var key = "a401010327200621582052b92d51dc638d085f8663103d5509f0da29bbee418d75f1f2dc7025d69c9643";

var p = new CIP30Verifier(sig, key);
var result = p.verify();
var cip30Verifier = new CIP30Verifier(sig, key);
var result = cip30Verifier.verify();

assertFalse(result.isValid(), "signature is invalid and should fail to validate");
assertFalse(result.isHashed());
}

@Test
Expand All @@ -160,36 +167,43 @@ void publicKeysMismatch() {
//var key = "c4821499cef96eda9c00cdd0bfbcd2abf7d09436ad424ac7288653a8b4252014"; // correct key
var key = "a4010103272006215820a5f73966e73d0bb9eadc75c5857eafd054a0202d716ac6dde00303ee9c0019e3"; // incorrect key

var p = new CIP30Verifier(sig, key);
var cip30Verifier = new CIP30Verifier(sig, key);

var result = p.verify();
var result = cip30Verifier.verify();

assertFalse(result.isValid(), "ED 25519 public key within signature doesn't match with passed in key");
assertFalse(result.isHashed());
}

@Test
void publicKeysMismatch2() {
var sig = "84584aa3012704581de01d813fd4ab9c1e5f7a35da16f75c2e664edfb2a127fc17a4a7ebbfee6761646472657373581de01d813fd4ab9c1e5f7a35da16f75c2e664edfb2a127fc17a4a7ebbfeea166686173686564f458e67b22616374696f6e223a2246554c4c5f4d455441444154415f5343414e222c22616374696f6e54657874223a2246554c4c5f4d455441444154415f5343414e222c22757269223a22687474703a2f2f6c6f63616c686f73743a383038302f6170692f61646d696e2f66756c6c2d6d657461646174612d7363616e222c2264617461223a7b2261646472657373223a227374616b655f7465737431757177637a3037353477777075686d36786864706461367539656e796168616a35796e6c63396179356c346d6c6d73347079717967222c226e6574776f726b223a2250524550524f44227d7d5840b69fbe15912f0dabd46f6a3a5eebae58acefc7c80e82da70803c52860293d1fa53c9cb0a04758cf36fbec726835cac5e519b60ebd2c2cd04d66ad94c46b07604";
var key = "a4010103272006215820c4821499cef96eda9c00cdd0bfbcd2abf7d09436ad424ac7288653a8b4252014";

var p = new CIP30Verifier(sig, key);
var cip30Verifier = new CIP30Verifier(sig, key);

var result = p.verify();
var result = cip30Verifier.verify();

assertFalse(result.isValid(), "ED 25519 public key within signature doesn't match with passed in key");
assertFalse(result.isHashed());
}


@Test
// typhon 3.0.14 wallet case -> we should be ignoring now KID4 in protected headers
void signatureWithOptionalKid4SetIncorrectly() {
var sig = "84584da30127045820c4821499cef96eda9c00cdd0bfbcd2abf7d09436ad424ac7288653a8b42520146761646472657373581de01d813fd4ab9c1e5f7a35da16f75c2e664edfb2a127fc17a4a7ebbfeea166686173686564f44568656c6c6f5840c0d5f54ab847cb78e8aeff10691bb1dcc5eec9a52fbf9011a0cfe89a51c53c2e22f408708da3a2fb35bf8f518f63e79ae8388f12b198cb1bdbd0d40b72081b0d";
var key = "a4010103272006215820c4821499cef96eda9c00cdd0bfbcd2abf7d09436ad424ac7288653a8b4252014";
// hardware wallet, e.g. ledger with Eternl
void signatureWithHashed() {
var sig = "84582aa201276761646472657373581de103d205532089ad2f7816892e2ef42849b7b52788e41b3fd43a6e01cfa166686173686564f5581c1c1afc33a1ed48205eadcbbda2fc8e61442af2e04673616f21b7d0385840954858f672e9ca51975655452d79a8f106011e9535a2ebfb909f7bbcce5d10d246ae62df2da3a7790edd8f93723cbdfdffc5341d08135b1a40e7a998e8b2ed06";
var key = "a4010103272006215820c13745be35c2dfc3fa9523140030dda5b5346634e405662b1aae5c61389c55b3";

var p = new CIP30Verifier(sig, key);
var cip30Verifier = new CIP30Verifier(sig, key);

var result = p.verify();
var result = cip30Verifier.verify();

assertTrue(result.isValid());

assertEquals("stake1uypayp2nyzy66tmcz6yjuth59pym0df83rjpk0758fhqrncq8vcdz", result.getAddress(AddressFormat.TEXT).orElseThrow());
assertEquals("1c1afc33a1ed48205eadcbbda2fc8e61442af2e04673616f21b7d038", result.getMessage(HEX));
assertTrue(result.isHashed());
}

}

0 comments on commit 98ba9f6

Please sign in to comment.