Skip to content

Commit f75a093

Browse files
committed
Support extended bip32 keys
This change will enable users to load and use extended bip32 keys to sign transactions.
1 parent ee037a3 commit f75a093

File tree

9 files changed

+200
-32
lines changed

9 files changed

+200
-32
lines changed

integration-test/keys/extended.skey

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "PaymentExtendedSigningKeyShelley_ed25519_bip32",
3+
"description": "Payment Signing Key",
4+
"cborHex": "5880e8428867ab9cc9304379a3ce0c238a592bd6d2349d2ebaf8a6ed2c6d2974a15ad59c74b6d8fa3edd032c6261a73998b7deafe983b6eeaff8b6fb3fab06bdf8019b693a62bce7a3cad1b9c02d22125767201c65db27484bb67d3cee7df7288d62c099ac0ce4a215355b149fd3114a2a7ef0438f01f8872c4487a61b469e26aae4"
5+
}

integration-test/run_tests.sh

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ docker-compose down --volume
1717
docker-compose up -d
1818

1919
export PAYMENT_KEY="$ROOT"/configs/local-alonzo/shelley/utxo-keys/utxo1.skey
20+
export EXTENDED_PAYMENT_KEY="$ROOT"/keys/extended.skey
2021
poetry run pytest -s "$ROOT"/test
2122

2223
# Cleanup

integration-test/test/test_mint_nft.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import tempfile
66
import time
77

8-
import websocket
98
from retry import retry
109

1110
from pycardano import *
@@ -28,12 +27,17 @@ def test_mint(self):
2827
chain_context = OgmiosChainContext(self.OGMIOS_WS, Network.TESTNET)
2928

3029
payment_key_path = os.environ.get("PAYMENT_KEY")
31-
if not payment_key_path:
30+
extended_key_path = os.environ.get("EXTENDED_PAYMENT_KEY")
31+
if not payment_key_path or not extended_key_path:
3232
raise Exception(
33-
"Cannot find payment key. Please specify environment variable PAYMENT_KEY"
33+
"Cannot find payment key. Please specify environment variable PAYMENT_KEY and extended_key_path"
3434
)
3535
payment_skey = PaymentSigningKey.load(payment_key_path)
3636
payment_vkey = PaymentVerificationKey.from_signing_key(payment_skey)
37+
extended_payment_skey = PaymentExtendedSigningKey.load(extended_key_path)
38+
extended_payment_vkey = PaymentExtendedVerificationKey.from_signing_key(
39+
extended_payment_skey
40+
)
3741
address = Address(payment_vkey.hash(), network=self.NETWORK)
3842

3943
# Load payment keys or create them if they don't exist
@@ -67,13 +71,16 @@ def load_or_create_key_pair(base_dir, base_name):
6771

6872
"""Create policy"""
6973
# A policy that requires a signature from the policy key we generated above
70-
pub_key_policy = ScriptPubkey(policy_vkey.hash())
74+
pub_key_policy_1 = ScriptPubkey(policy_vkey.hash())
75+
76+
# A policy that requires a signature from the extended payment key
77+
pub_key_policy_2 = ScriptPubkey(extended_payment_vkey.hash())
7178

7279
# A time policy that disallows token minting after 10000 seconds from last block
7380
must_before_slot = InvalidHereAfter(chain_context.last_block_slot + 10000)
7481

7582
# Combine two policies using ScriptAll policy
76-
policy = ScriptAll([pub_key_policy, must_before_slot])
83+
policy = ScriptAll([pub_key_policy_1, pub_key_policy_2, must_before_slot])
7784

7885
# Calculate policy ID, which is the hash of the policy
7986
policy_id = policy.hash()
@@ -149,13 +156,17 @@ def load_or_create_key_pair(base_dir, base_name):
149156
# Sign the transaction body hash using the payment signing key
150157
payment_signature = payment_skey.sign(tx_body.hash())
151158

159+
# Sign the transaction body hash using the extended payment signing key
160+
extended_payment_signature = extended_payment_skey.sign(tx_body.hash())
161+
152162
# Sign the transaction body hash using the policy signing key because we are minting new tokens
153163
policy_signature = policy_skey.sign(tx_body.hash())
154164

155165
# Add verification keys and their signatures to the witness set
156166
vk_witnesses = [
157167
VerificationKeyWitness(payment_vkey, payment_signature),
158168
VerificationKeyWitness(policy_vkey, policy_signature),
169+
VerificationKeyWitness(extended_payment_vkey, extended_payment_signature),
159170
]
160171

161172
# Create final signed transaction

poetry.lock

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pycardano/crypto/bip32.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
BIP32 implemented on curve ED25519
3+
Paper: https://github.com/LedgerHQ/orakolo/blob/master/papers/Ed25519_BIP%20Final.pdf
4+
This is a modified version of https://github.com/johnoliverdriscoll/py-edhd/blob/master/src/edhd/__init__.py
5+
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import hashlib
11+
12+
from nacl import bindings
13+
14+
15+
class BIP32ED25519PrivateKey:
16+
def __init__(self, private_key: bytes, chain_code: bytes):
17+
self.private_key = private_key
18+
self.left = self.private_key[:32]
19+
self.right = self.private_key[32:]
20+
self.chain_code = chain_code
21+
self.public_key = bindings.crypto_scalarmult_ed25519_base_noclamp(self.left)
22+
23+
def sign(self, message: bytes) -> bytes:
24+
r = bindings.crypto_core_ed25519_scalar_reduce(
25+
hashlib.sha512(self.right + message).digest(),
26+
)
27+
R = bindings.crypto_scalarmult_ed25519_base_noclamp(r)
28+
hram = bindings.crypto_core_ed25519_scalar_reduce(
29+
hashlib.sha512(R + self.public_key + message).digest(),
30+
)
31+
S = bindings.crypto_core_ed25519_scalar_add(
32+
bindings.crypto_core_ed25519_scalar_mul(hram, self.left),
33+
r,
34+
)
35+
return R + S
36+
37+
38+
class BIP32ED25519PublicKey:
39+
def __init__(self, public_key: bytes, chain_code: bytes):
40+
self.public_key = public_key
41+
self.chain_code = chain_code
42+
43+
@classmethod
44+
def from_private_key(
45+
cls, private_key: BIP32ED25519PrivateKey
46+
) -> BIP32ED25519PublicKey:
47+
return cls(private_key.public_key, private_key.chain_code)
48+
49+
def verify(self, signature, message):
50+
return bindings.crypto_sign_open(signature + message, self.public_key)

pycardano/key.py

+70-17
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,29 @@
55
import json
66
import os
77

8-
from nacl.bindings import crypto_sign_PUBLICKEYBYTES, crypto_sign_SEEDBYTES
98
from nacl.encoding import RawEncoder
109
from nacl.hash import blake2b
1110
from nacl.public import PrivateKey
1211
from nacl.signing import SigningKey as NACLSigningKey
1312

13+
from pycardano.crypto.bip32 import BIP32ED25519PrivateKey
1414
from pycardano.exception import InvalidKeyTypeException
1515
from pycardano.hash import VERIFICATION_KEY_HASH_SIZE, VerificationKeyHash
1616
from pycardano.serialization import CBORSerializable
1717

1818
__all__ = [
1919
"Key",
20+
"ExtendedSigningKey",
21+
"ExtendedVerificationKey",
2022
"VerificationKey",
2123
"SigningKey",
24+
"PaymentExtendedSigningKey",
25+
"PaymentExtendedVerificationKey",
2226
"PaymentSigningKey",
2327
"PaymentVerificationKey",
2428
"PaymentKeyPair",
29+
"StakeExtendedSigningKey",
30+
"StakeExtendedVerificationKey",
2531
"StakeSigningKey",
2632
"StakeVerificationKey",
2733
"StakeKeyPair",
@@ -132,16 +138,21 @@ def __repr__(self) -> str:
132138
return self.to_json()
133139

134140

135-
class VerificationKey(Key):
141+
class SigningKey(Key):
142+
def sign(self, data: bytes) -> bytes:
143+
signed_message = NACLSigningKey(self.payload).sign(data)
144+
return signed_message.signature
145+
146+
@classmethod
147+
def generate(cls) -> SigningKey:
148+
signing_key = PrivateKey.generate()
149+
return cls(bytes(signing_key))
136150

137-
SIZE = crypto_sign_PUBLICKEYBYTES
138151

152+
class VerificationKey(Key):
139153
def hash(self) -> VerificationKeyHash:
140154
"""Compute a blake2b hash from the key
141155
142-
Args:
143-
hash_size: Size of the hash output in bytes.
144-
145156
Returns:
146157
VerificationKeyHash: Hash output in bytes.
147158
"""
@@ -152,33 +163,65 @@ def hash(self) -> VerificationKeyHash:
152163
@classmethod
153164
def from_signing_key(cls, key: SigningKey) -> VerificationKey:
154165
verification_key = NACLSigningKey(bytes(key)).verify_key
155-
return cls(bytes(verification_key))
166+
return cls(
167+
bytes(verification_key),
168+
key.key_type.replace("Signing", "Verification"),
169+
key.description.replace("Signing", "Verification"),
170+
)
156171

157172

158-
class SigningKey(Key):
173+
class ExtendedSigningKey(Key):
174+
def sign(self, data: bytes) -> bytes:
175+
private_key = BIP32ED25519PrivateKey(self.payload[:64], self.payload[96:])
176+
return private_key.sign(data)
159177

160-
SIZE = crypto_sign_SEEDBYTES
161178

162-
def sign(self, data: bytes) -> bytes:
163-
signed_message = NACLSigningKey(self.payload).sign(data)
164-
return signed_message.signature
179+
class ExtendedVerificationKey(Key):
180+
def hash(self) -> VerificationKeyHash:
181+
"""Compute a blake2b hash from the key, excluding chain code
182+
183+
Returns:
184+
VerificationKeyHash: Hash output in bytes.
185+
"""
186+
return self.to_non_extended().hash()
165187

166188
@classmethod
167-
def generate(cls) -> SigningKey:
168-
signing_key = PrivateKey.generate()
169-
return cls(bytes(signing_key))
189+
def from_signing_key(cls, key: ExtendedSigningKey) -> ExtendedVerificationKey:
190+
return cls(
191+
key.payload[64:],
192+
key.key_type.replace("Signing", "Verification"),
193+
key.description.replace("Signing", "Verification"),
194+
)
195+
196+
def to_non_extended(self) -> VerificationKey:
197+
"""Get the 32-byte verification with chain code trimmed off
198+
199+
Returns:
200+
VerificationKey: 32-byte verification with chain code trimmed off
201+
"""
202+
return VerificationKey(self.payload[:32])
170203

171204

172205
class PaymentSigningKey(SigningKey):
173206
KEY_TYPE = "PaymentSigningKeyShelley_ed25519"
174-
DESCRIPTION = "Payment Verification Key"
207+
DESCRIPTION = "Payment Signing Key"
175208

176209

177210
class PaymentVerificationKey(VerificationKey):
178211
KEY_TYPE = "PaymentVerificationKeyShelley_ed25519"
179212
DESCRIPTION = "Payment Verification Key"
180213

181214

215+
class PaymentExtendedSigningKey(ExtendedSigningKey):
216+
KEY_TYPE = "PaymentExtendedSigningKeyShelley_ed25519_bip32"
217+
DESCRIPTION = "Payment Signing Key"
218+
219+
220+
class PaymentExtendedVerificationKey(ExtendedVerificationKey):
221+
KEY_TYPE = "PaymentExtendedVerificationKeyShelley_ed25519_bip32"
222+
DESCRIPTION = "Payment Verification Key"
223+
224+
182225
class PaymentKeyPair:
183226
def __init__(
184227
self, signing_key: PaymentSigningKey, verification_key: PaymentVerificationKey
@@ -205,14 +248,24 @@ def __eq__(self, other):
205248

206249
class StakeSigningKey(SigningKey):
207250
KEY_TYPE = "StakeSigningKeyShelley_ed25519"
208-
DESCRIPTION = "Stake Verification Key"
251+
DESCRIPTION = "Stake Signing Key"
209252

210253

211254
class StakeVerificationKey(VerificationKey):
212255
KEY_TYPE = "StakeVerificationKeyShelley_ed25519"
213256
DESCRIPTION = "Stake Verification Key"
214257

215258

259+
class StakeExtendedSigningKey(ExtendedSigningKey):
260+
KEY_TYPE = "StakeExtendedSigningKeyShelley_ed25519_bip32"
261+
DESCRIPTION = "Stake Signing Key"
262+
263+
264+
class StakeExtendedVerificationKey(ExtendedVerificationKey):
265+
KEY_TYPE = "StakeExtendedVerificationKeyShelley_ed25519_bip32"
266+
DESCRIPTION = "Stake Verification Key"
267+
268+
216269
class StakeKeyPair:
217270
def __init__(
218271
self, signing_key: StakeSigningKey, verification_key: StakeVerificationKey

pycardano/witness.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Transaction witness."""
22

33
from dataclasses import dataclass, field
4-
from typing import Any, List
4+
from typing import Any, List, Union
55

6-
from pycardano.key import VerificationKey
6+
from pycardano.key import ExtendedVerificationKey, VerificationKey
77
from pycardano.nativescript import NativeScript
88
from pycardano.plutus import PlutusData, Redeemer
99
from pycardano.serialization import (
@@ -17,9 +17,15 @@
1717

1818
@dataclass(repr=False)
1919
class VerificationKeyWitness(ArrayCBORSerializable):
20-
vkey: VerificationKey
20+
vkey: Union[VerificationKey, ExtendedVerificationKey]
2121
signature: bytes
2222

23+
def __post_init__(self):
24+
# When vkey is in extended format, we need to convert it to non-extended, so it can match the
25+
# key hash of the input address we are trying to spend.
26+
if isinstance(self.vkey, ExtendedVerificationKey):
27+
self.vkey = self.vkey.to_non_extended()
28+
2329

2430
@dataclass(repr=False)
2531
class TransactionWitnessSet(MapCBORSerializable):

0 commit comments

Comments
 (0)