Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 15 additions & 34 deletions src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +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.SparseMerkleTreePathUtils;
import org.unicitylabs.sdk.smt.radix.FinalizedNodeBranch;
import org.unicitylabs.sdk.util.BitString;
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 @@ -30,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.");
}

Comment thread
martti007 marked this conversation as resolved.
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 @@ -114,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 @@ -138,7 +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.pathToRegion(keyPath, 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