Skip to content

Commit 0835932

Browse files
committed
#79 Update signing service to include recovery byte in verify
1 parent 1d9f7c3 commit 0835932

8 files changed

Lines changed: 153 additions & 39 deletions

File tree

src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.unicitylabs.sdk.api.bft;
22

33
import org.unicitylabs.sdk.api.NetworkId;
4+
import org.unicitylabs.sdk.crypto.secp256k1.Signature;
45
import org.unicitylabs.sdk.serializer.cbor.CborDeserializer;
56
import org.unicitylabs.sdk.serializer.cbor.CborDeserializer.CborTag;
67
import org.unicitylabs.sdk.serializer.cbor.CborSerializationException;
@@ -162,7 +163,7 @@ public static UnicitySeal fromCbor(byte[] bytes) {
162163
CborDeserializer.decodeMap(data.get(7)).stream()
163164
.map(entry -> new SignatureEntry(
164165
CborDeserializer.decodeTextString(entry.getKey()),
165-
CborDeserializer.decodeByteString(entry.getValue())
166+
Signature.fromCbor(entry.getValue())
166167
))
167168
.collect(Collectors.toSet())
168169
);
@@ -191,7 +192,7 @@ public byte[] toCbor() {
191192
signatures.stream()
192193
.map(entry -> new CborMap.Entry(
193194
CborSerializer.encodeTextString(entry.getKey()),
194-
CborSerializer.encodeByteString(entry.getSignature())
195+
entry.getSignature().toCbor()
195196
)
196197
)
197198
.collect(Collectors.toSet())
@@ -258,9 +259,9 @@ public String toString() {
258259

259260
public static final class SignatureEntry {
260261
private final String key;
261-
private final byte[] signature;
262+
private final Signature signature;
262263

263-
SignatureEntry(String key, byte[] signature) {
264+
SignatureEntry(String key, Signature signature) {
264265
this.key = key;
265266
this.signature = signature;
266267
}
@@ -269,8 +270,8 @@ public String getKey() {
269270
return this.key;
270271
}
271272

272-
public byte[] getSignature() {
273-
return Arrays.copyOf(this.signature, this.signature.length);
273+
public Signature getSignature() {
274+
return this.signature;
274275
}
275276

276277
@Override
@@ -287,7 +288,7 @@ public int hashCode() {
287288

288289
@Override
289290
public String toString() {
290-
return String.format("SignatureEntry{key=%s, signature=%s}", this.key, HexConverter.encode(this.signature));
291+
return String.format("SignatureEntry{key=%s, signature=%s}", this.key, this.signature);
291292
}
292293
}
293294
}

src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import org.unicitylabs.sdk.crypto.hash.DataHash;
77
import org.unicitylabs.sdk.crypto.hash.DataHasher;
88
import org.unicitylabs.sdk.crypto.hash.HashAlgorithm;
9+
import org.unicitylabs.sdk.crypto.secp256k1.Signature;
910
import org.unicitylabs.sdk.crypto.secp256k1.SigningService;
1011
import org.unicitylabs.sdk.util.verification.VerificationResult;
1112
import org.unicitylabs.sdk.util.verification.VerificationStatus;
1213

1314
import java.util.ArrayList;
14-
import java.util.Arrays;
1515
import java.util.List;
1616

1717
/**
@@ -38,13 +38,13 @@ public static VerificationResult<VerificationStatus> verify(RootTrustBase trustB
3838
int successful = 0;
3939
for (UnicitySeal.SignatureEntry entry : unicitySeal.getSignatures()) {
4040
String nodeId = entry.getKey();
41-
byte[] signature = entry.getSignature();
41+
Signature signature = entry.getSignature();
4242

4343
VerificationResult<?> result = UnicitySealQuorumSignaturesVerificationRule.verifySignature(
4444
trustBase,
4545
nodeId,
4646
signature,
47-
hash.getData()
47+
hash
4848
);
4949
results.add(result);
5050

@@ -73,8 +73,8 @@ public static VerificationResult<VerificationStatus> verify(RootTrustBase trustB
7373
private static VerificationResult<?> verifySignature(
7474
RootTrustBase trustBase,
7575
String nodeId,
76-
byte[] signature,
77-
byte[] hash
76+
Signature signature,
77+
DataHash hash
7878
) {
7979
NodeInfo node = trustBase.getRootNode(nodeId);
8080
if (node == null) {
@@ -87,7 +87,7 @@ private static VerificationResult<?> verifySignature(
8787

8888
if (!SigningService.verifyWithPublicKey(
8989
hash,
90-
Arrays.copyOf(signature, signature.length - 1),
90+
signature,
9191
node.getSigningKey()
9292
)) {
9393
return new VerificationResult<>(

src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -146,30 +146,30 @@ public Signature sign(DataHash hash) {
146146
* @return true if successful
147147
*/
148148
public boolean verify(DataHash hash, Signature signature) {
149-
return verifyWithPublicKey(hash, signature.getBytes(), this.publicKey);
149+
return SigningService.verifyWithPublicKey(hash, signature, this.publicKey);
150150
}
151151

152152
/**
153-
* Verify signature with public key.
153+
* Verify secp256k1 signature against the given public key.
154154
*
155155
* @param hash data hash
156-
* @param signature signature bytes
156+
* @param signature compact signature bytes
157157
* @param publicKey public key
158158
* @return true if successful
159159
*/
160-
public static boolean verifyWithPublicKey(DataHash hash, byte[] signature, byte[] publicKey) {
161-
return SigningService.verifyWithPublicKey(hash.getData(), signature, publicKey);
160+
public static boolean verify(DataHash hash, byte[] signature, byte[] publicKey) {
161+
return SigningService.verify(hash.getData(), signature, publicKey);
162162
}
163163

164164
/**
165-
* Verify signature with public key and data hash bytes.
165+
* Verify secp256k1 signature against the given public key and data hash bytes.
166166
*
167167
* @param hash hash bytes
168-
* @param signature signature bytes
168+
* @param signature compact signature bytes
169169
* @param publicKey public key
170170
* @return true if successful
171171
*/
172-
public static boolean verifyWithPublicKey(byte[] hash, byte[] signature, byte[] publicKey) {
172+
public static boolean verify(byte[] hash, byte[] signature, byte[] publicKey) {
173173
ECPoint pubPoint = EC_SPEC.getCurve().decodePoint(publicKey);
174174
ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, EC_DOMAIN_PARAMETERS);
175175

@@ -183,6 +183,25 @@ public static boolean verifyWithPublicKey(byte[] hash, byte[] signature, byte[]
183183
return verifier.verifySignature(hash, r, s);
184184
}
185185

186+
/**
187+
* Verify a recoverable signature against an expected public key. Unlike {@link #verify(DataHash,
188+
* byte[], byte[])}, this binds the signature's recovery byte: the public key is recovered from
189+
* the signature and must equal {@code publicKey}, so a tampered recovery byte fails verification.
190+
*
191+
* @param hash data hash
192+
* @param signature recoverable signature
193+
* @param publicKey expected compressed public key
194+
* @return true if the signature verifies and the recovered public key matches {@code publicKey}
195+
*/
196+
public static boolean verifyWithPublicKey(DataHash hash, Signature signature, byte[] publicKey) {
197+
byte[] recoveredPublicKey = SigningService.recoverPublicKey(hash, signature);
198+
if (recoveredPublicKey == null || !Arrays.equals(publicKey, recoveredPublicKey)) {
199+
return false;
200+
}
201+
202+
return SigningService.verify(hash, signature.getBytes(), publicKey);
203+
}
204+
186205

187206
private byte[] toFixedLength(BigInteger value, int length) {
188207
byte[] bytes = value.toByteArray();
@@ -261,22 +280,43 @@ private static ECPoint decompressKey(BigInteger x, boolean ybit, ECCurve curve)
261280
}
262281

263282
/**
264-
* Verify signature with recovered public key - extract public key from signature.
283+
* Recover the public key from the signature's recovery byte and verify the signature against
284+
* {@code hash}. The recovered key defines the signer's identity; no expected key is supplied.
265285
*
266286
* @param hash data hash
267-
* @param signature signature
287+
* @param signature recoverable signature
268288
* @return true if successful
269289
*/
270-
public static boolean verifySignatureWithRecoveredPublicKey(DataHash hash, Signature signature) {
271-
// Extract r and s from signature
272-
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 0, 32));
273-
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 32, 64));
274-
275-
ECPoint recovered = recoverFromSignature(signature.getRecovery(), r, s, hash.getData());
276-
if (recovered == null || !recovered.isValid()) {
290+
public static boolean verifyWithRecoveredPublicKey(DataHash hash, Signature signature) {
291+
byte[] recoveredPublicKey = SigningService.recoverPublicKey(hash, signature);
292+
if (recoveredPublicKey == null) {
277293
return false;
278294
}
279295

280-
return verifyWithPublicKey(hash, signature.getBytes(), recovered.getEncoded(true));
296+
return SigningService.verify(hash, signature.getBytes(), recoveredPublicKey);
297+
}
298+
299+
/**
300+
* Recover the compressed public key that produced {@code signature} over {@code hash}, using the
301+
* signature's recovery byte.
302+
*
303+
* @param hash data hash
304+
* @param signature recoverable signature
305+
* @return recovered compressed public key, or {@code null} if the signature is not recoverable
306+
*/
307+
private static byte[] recoverPublicKey(DataHash hash, Signature signature) {
308+
try {
309+
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 0, 32));
310+
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 32, 64));
311+
312+
ECPoint recovered = recoverFromSignature(signature.getRecovery(), r, s, hash.getData());
313+
if (recovered == null || !recovered.isValid()) {
314+
return null;
315+
}
316+
317+
return recovered.getEncoded(true);
318+
} catch (Exception e) {
319+
return null;
320+
}
281321
}
282322
}

src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import org.unicitylabs.sdk.crypto.hash.DataHash;
44
import org.unicitylabs.sdk.crypto.hash.DataHasher;
55
import org.unicitylabs.sdk.crypto.hash.HashAlgorithm;
6-
import org.unicitylabs.sdk.crypto.secp256k1.Signature;
76
import org.unicitylabs.sdk.crypto.secp256k1.SigningService;
87
import org.unicitylabs.sdk.predicate.EncodedPredicate;
98
import org.unicitylabs.sdk.predicate.builtin.BuiltInPredicateType;
109
import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate;
10+
import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript;
1111
import org.unicitylabs.sdk.serializer.cbor.CborSerializer;
1212
import org.unicitylabs.sdk.util.verification.VerificationResult;
1313
import org.unicitylabs.sdk.util.verification.VerificationStatus;
@@ -32,8 +32,11 @@ public BuiltInPredicateType getType() {
3232
@Override
3333
public VerificationResult<VerificationStatus> verify(EncodedPredicate encodedPredicate,
3434
DataHash sourceStateHash,
35-
DataHash transactionHash, byte[] unlockScript) {
35+
DataHash transactionHash,
36+
byte[] unlockScriptBytes) {
3637
SignaturePredicate predicate = SignaturePredicate.fromPredicate(encodedPredicate);
38+
SignaturePredicateUnlockScript unlockScript =
39+
SignaturePredicateUnlockScript.decode(unlockScriptBytes);
3740

3841
boolean result = SigningService.verifyWithPublicKey(
3942
new DataHasher(HashAlgorithm.SHA256)
@@ -44,7 +47,7 @@ public VerificationResult<VerificationStatus> verify(EncodedPredicate encodedPre
4447
)
4548
)
4649
.digest(),
47-
Signature.decode(unlockScript).getBytes(),
50+
unlockScript.getSignature(),
4851
predicate.getPublicKey()
4952
);
5053

src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public static UnicityCertificate generateCertificate(
9595
"NODE",
9696
signingService.sign(
9797
new DataHasher(HashAlgorithm.SHA256).update(seal.toCbor()).digest()
98-
).encode()
98+
)
9999
)
100100
)
101101
)

src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ void testSignatureRecoveryId() {
3535

3636
// Verify signature with known public key
3737
byte[] publicKey = signingService.getPublicKey();
38-
assertTrue(SigningService.verifyWithPublicKey(hash, signature.getBytes(), publicKey));
38+
assertTrue(SigningService.verify(hash, signature.getBytes(), publicKey));
3939
}
4040

4141
@Test
@@ -55,7 +55,7 @@ void testPublicKeyRecovery() {
5555
Signature signature = signingService.sign(hash);
5656

5757
// Verify signature using recovered public key
58-
assertTrue(SigningService.verifySignatureWithRecoveredPublicKey(hash, signature),
58+
assertTrue(SigningService.verifyWithRecoveredPublicKey(hash, signature),
5959
"Signature verification with recovered public key should succeed");
6060
}
6161

@@ -82,7 +82,7 @@ void testSignatureFormatCompliance() {
8282
DataHash transactionHash = DataHash.fromImprint(HexConverter.decode(transactionHashHex));
8383

8484
// Verify using recovered public key
85-
assertTrue(SigningService.verifySignatureWithRecoveredPublicKey(transactionHash, signature),
85+
assertTrue(SigningService.verifyWithRecoveredPublicKey(transactionHash, signature),
8686
"Should verify with recovered public key");
8787
}
8888
}

src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public void testVerifyWithPublicKey() {
5454
Signature signature = service.sign(hash);
5555

5656
// Verify with public key
57-
boolean isValid = SigningService.verifyWithPublicKey(hash, signature.getBytes(), publicKey);
57+
boolean isValid = SigningService.verify(hash, signature.getBytes(), publicKey);
5858

5959
assertTrue(isValid);
6060
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.unicitylabs.sdk.predicate.builtin.verification;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.Test;
5+
import org.unicitylabs.sdk.crypto.hash.DataHash;
6+
import org.unicitylabs.sdk.crypto.hash.DataHasher;
7+
import org.unicitylabs.sdk.crypto.hash.HashAlgorithm;
8+
import org.unicitylabs.sdk.crypto.secp256k1.Signature;
9+
import org.unicitylabs.sdk.crypto.secp256k1.SigningService;
10+
import org.unicitylabs.sdk.predicate.EncodedPredicate;
11+
import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate;
12+
import org.unicitylabs.sdk.serializer.cbor.CborSerializer;
13+
import org.unicitylabs.sdk.util.verification.VerificationStatus;
14+
15+
public class SignaturePredicateVerifierTest {
16+
17+
private final SignaturePredicateVerifier verifier = new SignaturePredicateVerifier();
18+
private final SigningService signingService = SigningService.generate();
19+
private final EncodedPredicate encodedPredicate = EncodedPredicate.fromPredicate(
20+
SignaturePredicate.fromSigningService(this.signingService));
21+
22+
private static DataHash hash(byte[] data) {
23+
return new DataHasher(HashAlgorithm.SHA256).update(data).digest();
24+
}
25+
26+
private final DataHash sourceStateHash = hash(new byte[]{1});
27+
private final DataHash transactionHash = hash(new byte[]{2});
28+
29+
private Signature signUnlock() {
30+
DataHash digest = hash(
31+
CborSerializer.encodeArray(
32+
CborSerializer.encodeByteString(this.sourceStateHash.getData()),
33+
CborSerializer.encodeByteString(this.transactionHash.getData())
34+
)
35+
);
36+
return this.signingService.sign(digest);
37+
}
38+
39+
@Test
40+
public void shouldAcceptValidUnlockScript() {
41+
Signature signature = signUnlock();
42+
43+
Assertions.assertEquals(
44+
VerificationStatus.OK,
45+
this.verifier.verify(this.encodedPredicate, this.sourceStateHash, this.transactionHash,
46+
signature.encode()).getStatus());
47+
}
48+
49+
@Test
50+
public void shouldRejectTamperedRecoveryByte() {
51+
byte[] tampered = signUnlock().encode();
52+
tampered[64] ^= 1;
53+
54+
Assertions.assertEquals(
55+
VerificationStatus.FAIL,
56+
this.verifier.verify(this.encodedPredicate, this.sourceStateHash, this.transactionHash,
57+
tampered).getStatus());
58+
}
59+
60+
@Test
61+
public void shouldFailWhenRecoveryByteMakesSignatureUnrecoverable() {
62+
byte[] tampered = signUnlock().encode();
63+
tampered[64] = 2;
64+
65+
Assertions.assertEquals(
66+
VerificationStatus.FAIL,
67+
this.verifier.verify(this.encodedPredicate, this.sourceStateHash, this.transactionHash,
68+
tampered).getStatus());
69+
}
70+
}

0 commit comments

Comments
 (0)