Skip to content
Merged
49 changes: 16 additions & 33 deletions src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@
import org.unicitylabs.sdk.crypto.hash.DataHash;
import org.unicitylabs.sdk.crypto.hash.DataHasher;
import org.unicitylabs.sdk.crypto.hash.HashAlgorithm;
import org.unicitylabs.sdk.smt.radix.FinalizedBranch;
import org.unicitylabs.sdk.smt.radix.FinalizedLeafBranch;
import org.unicitylabs.sdk.smt.radix.FinalizedNodeBranch;
import org.unicitylabs.sdk.util.BitString;
import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils;
import org.unicitylabs.sdk.smt.radix.SparseMerkleTreeRootNode;
import org.unicitylabs.sdk.util.HexConverter;
import org.unicitylabs.sdk.util.LongConverter;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand All @@ -29,35 +26,23 @@ private InclusionCertificate(byte[] bitmap, List<DataHash> siblings) {
this.siblings = siblings;
}

public static InclusionCertificate create(FinalizedNodeBranch root, byte[] key) {
FinalizedBranch node = root;
public static InclusionCertificate create(SparseMerkleTreeRootNode root, byte[] key) {
Objects.requireNonNull(root, "root cannot be null");
Objects.requireNonNull(key, "key cannot be null");
if (key.length != 32) {
throw new IllegalArgumentException("Key must be 32 bytes long.");
}

ArrayList<DataHash> siblings = new ArrayList<>();
byte[] bitmap = new byte[InclusionCertificate.BITMAP_SIZE];
BigInteger keyPath = BitString.fromBytesReversedLSB(key).toBigInteger();

while (node != null) {
if (node instanceof FinalizedLeafBranch) {
FinalizedLeafBranch leaf = (FinalizedLeafBranch) node;
if (!Arrays.equals(leaf.getKey(), key)) {
throw new RuntimeException(String.format("Leaf not found for key: %s", HexConverter.encode(key)));
}

return new InclusionCertificate(bitmap, siblings);
}

FinalizedNodeBranch nodeBranch = (FinalizedNodeBranch) node;
boolean isRight = keyPath.testBit(nodeBranch.getDepth());
FinalizedBranch sibling = isRight ? nodeBranch.getLeft() : nodeBranch.getRight();
if (sibling != null) {
bitmap[nodeBranch.getDepth() / 8] |= (byte) (1 << nodeBranch.getDepth() % 8);
siblings.add(sibling.getHash());
}

node = isRight ? nodeBranch.getRight() : nodeBranch.getLeft();
for (SparseMerkleTreeRootNode.Sibling sibling : root.getPath(key)) {
int depth = sibling.getDepth();
bitmap[depth >> 3] |= (byte) (0x80 >> (depth & 7));
siblings.add(sibling.getHash());
}

throw new RuntimeException("Could not construct inclusion certificate: Invalid path");
return new InclusionCertificate(bitmap, siblings);
}

public static InclusionCertificate decode(byte[] bytes) {
Expand Down Expand Up @@ -113,20 +98,17 @@ public boolean verify(StateId leafKey, DataHash leafValue, DataHash expectedRoot
.update(value)
.digest();

BigInteger keyPath = BitString.fromBytesReversedLSB(key).toBigInteger();
BigInteger bitmapPath = BitString.fromBytesReversedLSB(this.bitmap).toBigInteger();

int position = this.siblings.size();
for (int depth = InclusionCertificate.MAX_DEPTH; depth >= 0; depth--) {
if (!bitmapPath.testBit(depth)) continue;
if (SparseMerkleTreePathUtils.getBitAtDepth(this.bitmap, depth) == 0) continue;

position -= 1;
if (position < 0) return false;

DataHash sibling = this.siblings.get(position);

byte[] left, right;
if (keyPath.testBit(depth)) {
if (SparseMerkleTreePathUtils.getBitAtDepth(key, depth) == 1) {
left = sibling.getData();
right = hash.getData();
} else {
Expand All @@ -137,6 +119,7 @@ public boolean verify(StateId leafKey, DataHash leafValue, DataHash expectedRoot
hash = new DataHasher(HashAlgorithm.SHA256)
.update(new byte[]{0x01})
.update(LongConverter.encode(depth))
.update(SparseMerkleTreePathUtils.regionFromKey(key, depth))
.update(left)
.update(right)
.digest();
Expand Down
15 changes: 8 additions & 7 deletions src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.unicitylabs.sdk.api.bft;

import org.unicitylabs.sdk.api.NetworkId;
import org.unicitylabs.sdk.crypto.secp256k1.Signature;
import org.unicitylabs.sdk.serializer.cbor.CborDeserializer;
import org.unicitylabs.sdk.serializer.cbor.CborDeserializer.CborTag;
import org.unicitylabs.sdk.serializer.cbor.CborSerializationException;
Expand Down Expand Up @@ -162,7 +163,7 @@ public static UnicitySeal fromCbor(byte[] bytes) {
CborDeserializer.decodeMap(data.get(7)).stream()
.map(entry -> new SignatureEntry(
CborDeserializer.decodeTextString(entry.getKey()),
CborDeserializer.decodeByteString(entry.getValue())
Signature.fromCbor(entry.getValue())
))
.collect(Collectors.toSet())
);
Expand Down Expand Up @@ -191,7 +192,7 @@ public byte[] toCbor() {
signatures.stream()
.map(entry -> new CborMap.Entry(
CborSerializer.encodeTextString(entry.getKey()),
CborSerializer.encodeByteString(entry.getSignature())
entry.getSignature().toCbor()
)
)
.collect(Collectors.toSet())
Expand Down Expand Up @@ -258,9 +259,9 @@ public String toString() {

public static final class SignatureEntry {
private final String key;
private final byte[] signature;
private final Signature signature;

SignatureEntry(String key, byte[] signature) {
SignatureEntry(String key, Signature signature) {
this.key = key;
this.signature = signature;
}
Expand All @@ -269,8 +270,8 @@ public String getKey() {
return this.key;
}

public byte[] getSignature() {
return Arrays.copyOf(this.signature, this.signature.length);
public Signature getSignature() {
return this.signature;
}

@Override
Expand All @@ -287,7 +288,7 @@ public int hashCode() {

@Override
public String toString() {
return String.format("SignatureEntry{key=%s, signature=%s}", this.key, HexConverter.encode(this.signature));
return String.format("SignatureEntry{key=%s, signature=%s}", this.key, this.signature);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import org.unicitylabs.sdk.crypto.hash.DataHash;
import org.unicitylabs.sdk.crypto.hash.DataHasher;
import org.unicitylabs.sdk.crypto.hash.HashAlgorithm;
import org.unicitylabs.sdk.crypto.secp256k1.Signature;
import org.unicitylabs.sdk.crypto.secp256k1.SigningService;
import org.unicitylabs.sdk.util.verification.VerificationResult;
import org.unicitylabs.sdk.util.verification.VerificationStatus;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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

VerificationResult<?> result = UnicitySealQuorumSignaturesVerificationRule.verifySignature(
trustBase,
nodeId,
signature,
hash.getData()
hash
);
results.add(result);

Expand Down Expand Up @@ -73,8 +73,8 @@ public static VerificationResult<VerificationStatus> verify(RootTrustBase trustB
private static VerificationResult<?> verifySignature(
RootTrustBase trustBase,
String nodeId,
byte[] signature,
byte[] hash
Signature signature,
DataHash hash
) {
NodeInfo node = trustBase.getRootNode(nodeId);
if (node == null) {
Expand All @@ -87,7 +87,7 @@ private static VerificationResult<?> verifySignature(

if (!SigningService.verifyWithPublicKey(
hash,
Arrays.copyOf(signature, signature.length - 1),
signature,
node.getSigningKey()
)) {
return new VerificationResult<>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,30 +146,30 @@ public Signature sign(DataHash hash) {
* @return true if successful
*/
public boolean verify(DataHash hash, Signature signature) {
return verifyWithPublicKey(hash, signature.getBytes(), this.publicKey);
return SigningService.verifyWithPublicKey(hash, signature, this.publicKey);
}

/**
* Verify signature with public key.
* Verify secp256k1 signature against the given public key.
*
* @param hash data hash
* @param signature signature bytes
* @param signature compact signature bytes
* @param publicKey public key
* @return true if successful
*/
public static boolean verifyWithPublicKey(DataHash hash, byte[] signature, byte[] publicKey) {
return SigningService.verifyWithPublicKey(hash.getData(), signature, publicKey);
public static boolean verify(DataHash hash, byte[] signature, byte[] publicKey) {
return SigningService.verify(hash.getData(), signature, publicKey);
}

/**
* Verify signature with public key and data hash bytes.
* Verify secp256k1 signature against the given public key and data hash bytes.
*
* @param hash hash bytes
* @param signature signature bytes
* @param signature compact signature bytes
* @param publicKey public key
* @return true if successful
*/
public static boolean verifyWithPublicKey(byte[] hash, byte[] signature, byte[] publicKey) {
public static boolean verify(byte[] hash, byte[] signature, byte[] publicKey) {
ECPoint pubPoint = EC_SPEC.getCurve().decodePoint(publicKey);
ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, EC_DOMAIN_PARAMETERS);

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

/**
* Verify a recoverable signature against an expected public key. Unlike {@link #verify(DataHash,
* byte[], byte[])}, this binds the signature's recovery byte: the public key is recovered from
* the signature and must equal {@code publicKey}, so a tampered recovery byte fails verification.
*
* @param hash data hash
* @param signature recoverable signature
* @param publicKey expected compressed public key
* @return true if the signature verifies and the recovered public key matches {@code publicKey}
*/
public static boolean verifyWithPublicKey(DataHash hash, Signature signature, byte[] publicKey) {
byte[] recoveredPublicKey = SigningService.recoverPublicKey(hash, signature);
if (recoveredPublicKey == null || !Arrays.equals(publicKey, recoveredPublicKey)) {
return false;
}

return SigningService.verify(hash, signature.getBytes(), publicKey);
}


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

/**
* Verify signature with recovered public key - extract public key from signature.
* Recover the public key from the signature's recovery byte and verify the signature against
* {@code hash}. The recovered key defines the signer's identity; no expected key is supplied.
*
* @param hash data hash
* @param signature signature
* @param signature recoverable signature
* @return true if successful
*/
public static boolean verifySignatureWithRecoveredPublicKey(DataHash hash, Signature signature) {
// Extract r and s from signature
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 32, 64));

ECPoint recovered = recoverFromSignature(signature.getRecovery(), r, s, hash.getData());
if (recovered == null || !recovered.isValid()) {
public static boolean verifyWithRecoveredPublicKey(DataHash hash, Signature signature) {
byte[] recoveredPublicKey = SigningService.recoverPublicKey(hash, signature);
if (recoveredPublicKey == null) {
return false;
}

return verifyWithPublicKey(hash, signature.getBytes(), recovered.getEncoded(true));
return SigningService.verify(hash, signature.getBytes(), recoveredPublicKey);
}

/**
* Recover the compressed public key that produced {@code signature} over {@code hash}, using the
* signature's recovery byte.
*
* @param hash data hash
* @param signature recoverable signature
* @return recovered compressed public key, or {@code null} if the signature is not recoverable
*/
private static byte[] recoverPublicKey(DataHash hash, Signature signature) {
try {
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 32, 64));

ECPoint recovered = recoverFromSignature(signature.getRecovery(), r, s, hash.getData());
if (recovered == null || !recovered.isValid()) {
return null;
}

return recovered.getEncoded(true);
} catch (Exception e) {
return null;
}
}
}
Loading
Loading