Skip to content

Commit da52c74

Browse files
Merge pull request #6 from rubixchain/arnab/fix-verify-func
Signature verification is made available as a standalone function
2 parents 0c3028c + 96a1951 commit da52c74

File tree

7 files changed

+153
-48
lines changed

7 files changed

+153
-48
lines changed

examples/crypto.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1-
from rubix.crypto.secp256k1 import Secp256k1Keypair
1+
from rubix.client import RubixClient
2+
from rubix.signer import Signer
3+
from rubix.crypto.secp256k1 import Secp256k1Keypair, secp256k1_verify
24

3-
def sign_arbitrary_data(data: bytes):
5+
def sign_and_verify_arbitrary_data(data: bytes):
46
# Generate a new Secp256k1 keypair
5-
keypair = Secp256k1Keypair.from_private_key(bytes.fromhex("<private key in hex>"))
7+
client = RubixClient("<Rubix Node URL>")
68

7-
print("Public Key (hex):", keypair.public_key)
9+
signer = Signer(
10+
rubixClient=client,
11+
mnemonic="<Enter 24-word long BIP-39 mnemonic>"
12+
)
13+
14+
print("Public Key (hex): ", signer.get_keypair().public_key)
15+
keypair = signer.get_keypair()
816

917
signature_bytes = keypair.sign(data)
18+
19+
is_valid = secp256k1_verify(bytes.fromhex(keypair.public_key), data, signature_bytes)
20+
print("Is the signature valid?: ", is_valid)

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "rubix-py"
7-
version = "0.2.0"
7+
version = "0.3.0"
88
description = "Rubix Client SDK for Python"
99
requires-python = ">=3.10"
1010
license = { text = "MIT" }
@@ -22,7 +22,8 @@ dependencies = [
2222
"bip32utils>=0.3.post4",
2323
"mnemonic>=0.21",
2424
"ECPy>=1.2.5",
25-
"py-cid>=0.3.1"
25+
"py-cid>=0.3.1",
26+
"coincurve>=21.0.0"
2627
]
2728

2829
[project.urls]

rubix/crypto/secp256k1.py

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,64 @@
11
import bip32utils
2-
import hashlib
3-
import base64
42

5-
from ecpy.curves import Curve
6-
from ecpy.keys import ECPrivateKey
3+
from ecpy.curves import Curve, Point
4+
from ecpy.keys import ECPrivateKey, ECPublicKey
75
from ecpy.ecdsa import ECDSA
8-
from ecdsa import SigningKey, SECP256k1, util
6+
from ecdsa import SigningKey, SECP256k1
7+
from coincurve import PublicKey as CoincurvePublicKey
8+
9+
def secp256k1_sign(private_key: bytes, message: bytes) -> bytes:
10+
"""Signs a message using secp256k1 private key.
11+
12+
Args:
13+
private_key (bytes): Secp256k1 private key.
14+
message (bytes): The message to sign.
15+
16+
Returns:
17+
bytes: The generated signature in bytes.
18+
"""
19+
cv = Curve.get_curve('secp256k1')
20+
pv_key = ECPrivateKey(int.from_bytes(private_key, 'big'), cv)
21+
signer = ECDSA()
22+
23+
sig = signer.sign(message, pv_key)
24+
if sig is None:
25+
raise ValueError("Failed to sign the message.")
26+
27+
return bytes(sig)
28+
29+
def secp256k1_verify(public_key: bytes, message: bytes, signature: bytes) -> bool:
30+
"""Verifies secp256k1 signature.
31+
32+
Args:
33+
public_key (bytes): Compressed public key.
34+
message (bytes): The original message that was signed.
35+
signature (bytes): The signature to verify.
36+
37+
Returns:
38+
bool: True if signature is valid, False otherwise.
39+
"""
40+
cv = Curve.get_curve('secp256k1')
41+
42+
# check if the public key is in compressed format
43+
uncompressed_public_key = None
44+
45+
if len(public_key) == 33:
46+
coincurve_pub_key = CoincurvePublicKey(public_key)
47+
uncompressed_public_key = coincurve_pub_key.format(compressed=False)
48+
elif len(public_key) == 65:
49+
uncompressed_public_key = public_key
50+
else:
51+
raise ValueError(f"Invalid public key of length {len(public_key)}.")
52+
53+
# Form public key object
54+
x = int.from_bytes(uncompressed_public_key[1:33], 'big')
55+
y = int.from_bytes(uncompressed_public_key[33:], 'big')
56+
ec_point = Point(x, y, cv)
57+
pub_key_obj = ECPublicKey(ec_point)
58+
59+
# Verify signature
60+
verifier = ECDSA()
61+
return verifier.verify(message, signature, pub_key_obj)
962

1063
class Secp256k1Keypair:
1164
def __init__(self, private_key: str, public_key: str):
@@ -68,33 +121,4 @@ def sign(self, message: bytes) -> bytes:
68121
Returns:
69122
bytes: The generated signature in bytes.
70123
"""
71-
cv = Curve.get_curve('secp256k1')
72-
pv_key = ECPrivateKey(int(self.__private_key, 16), cv)
73-
signer = ECDSA()
74-
75-
sig = signer.sign(message, pv_key)
76-
if sig is None:
77-
raise ValueError("Failed to sign the message.")
78-
79-
return sig
80-
81-
def verify(self, message: bytes, signature: bytes) -> bool:
82-
"""Verifies secp256k1 signature.
83-
84-
Args:
85-
message (bytes): The original message that was signed.
86-
signature (bytes): The signature to verify.
87-
88-
Returns:
89-
bool: True if signature is valid, False otherwise.
90-
"""
91-
cv = Curve.get_curve('secp256k1')
92-
priv_key = ECPrivateKey(int(self.__private_key, 16), cv)
93-
94-
pub_key = priv_key.get_public_key()
95-
signer = ECDSA()
96-
97-
try:
98-
return signer.verify(message, signature, pub_key)
99-
except Exception:
100-
return False
124+
return secp256k1_sign(bytes.fromhex(self.__private_key), message)

rubix/did.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,44 @@ def create_did(keypair: Secp256k1Keypair, rubixNodeBaseUrl: str) -> None:
136136

137137
return user_did
138138

139+
def online_signature_verify(rubixNodeBaseUrl: str, did: str, message: bytes, signature: bytes) -> bool:
140+
"""
141+
Verifies a signature using Rubix node's online verification service.
142+
143+
Args:
144+
rubixNodeBaseUrl (str): Base URL of the Rubix node.
145+
did (str): The DID of the signer.
146+
message (bytes): The original message that was signed.
147+
signature (bytes): The signature to verify.
148+
149+
Returns:
150+
bool: True if signature is valid, False otherwise.
151+
"""
152+
153+
verify_signature_url = urljoin(rubixNodeBaseUrl, "/api/verify-signature")
154+
155+
verify_signature_body = {
156+
"signer_did": did,
157+
"signed_msg": message.decode('utf-8'),
158+
"signature": signature.hex()
159+
}
160+
161+
try:
162+
response = requests.get(
163+
verify_signature_url,
164+
params=verify_signature_body,
165+
timeout=300
166+
)
167+
168+
response.raise_for_status()
169+
170+
response_body = response.json()
171+
return response_body.get("status", False)
172+
except requests.exceptions.Timeout:
173+
raise signatureResponseError("Request to Rubix node timed out")
174+
except requests.exceptions.ConnectionError:
175+
raise signatureResponseError(f"Failed to connect to Rubix node at {rubixNodeBaseUrl}")
176+
except requests.exceptions.HTTPError as e:
177+
raise signatureResponseError(f"HTTP error from Rubix node: {e}")
178+
except requests.exceptions.RequestException as e:
179+
raise signatureResponseError(f"Request failed: {e}")

rubix/signer.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import base64
22
import os
3-
from urllib.parse import urljoin
43

54
from .client import RubixClient
65
from .crypto.bip39 import generate_bip39_mnemonic, get_seed_from_mnemonic
76
from .crypto.secp256k1 import Secp256k1Keypair
87
from .did import create_did
9-
from .models.result import Response
108

119
class Signer:
1210
"""

tests/crypto/test_online_verify.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import pytest
2+
3+
from rubix.did import online_signature_verify
4+
from rubix.signer import Signer
5+
from rubix.client import RubixClient
6+
7+
@pytest.mark.skip(reason="requires online Rubix node")
8+
def test_online_verify_valid_signature():
9+
"""Test verifying a valid signature using an online verification service."""
10+
node_url = "http://localhost:20000"
11+
client = RubixClient(node_url)
12+
13+
signer = Signer(
14+
rubixClient=client,
15+
mnemonic="buffalo tumble defy laundry call almost little pig lift party property pool frame erosion mind library sample floor ring enemy word enemy foster ill"
16+
)
17+
18+
signer_did = signer.did
19+
20+
message = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
21+
22+
signature = signer.get_keypair().sign(message)
23+
24+
is_valid = online_signature_verify(
25+
rubixNodeBaseUrl=node_url,
26+
did=signer_did,
27+
message=message,
28+
signature=signature
29+
)
30+
assert is_valid is True

tests/crypto/test_secp256k1.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from rubix.crypto.secp256k1 import Secp256k1Keypair
2-
1+
from rubix.crypto.secp256k1 import Secp256k1Keypair, secp256k1_verify
32

43
def test_secp256k1_keypair_from_private_key():
54
"""Test the generation of a Secp256k1 keypair from a private key."""
@@ -37,8 +36,9 @@ def test_secp256k1_verify_valid_message():
3736
keypair = Secp256k1Keypair.from_private_key(private_key_bytes)
3837
signature = keypair.sign(message)
3938
assert isinstance(signature, bytes)
40-
41-
is_valid = keypair.verify(message, signature)
39+
40+
# Verify the signature
41+
is_valid = secp256k1_verify(bytes.fromhex(keypair.public_key), message, signature)
4242
assert is_valid is True
4343

4444
def test_secp256k1_verify_invalid_message():
@@ -55,6 +55,6 @@ def test_secp256k1_verify_invalid_message():
5555
signature = keypair.sign(message_1)
5656
assert isinstance(signature, bytes)
5757

58-
is_valid = keypair.verify(message_2, signature)
58+
is_valid = secp256k1_verify(bytes.fromhex(keypair.public_key), message_2, signature)
5959
assert is_valid is False
6060

0 commit comments

Comments
 (0)