feat: key bridge prototype — derive sidechain + document keys from Lethean spend key
One Lethean seed phrase derives keys for: - Main chain (ed25519, native) - HNS sidechain (secp256k1, derived via HKDF) - Document signing (ed25519, for hash timestamping) - Audit trail (ed25519, for revision verification) Domain-separated HKDF ensures keys can't be reversed or collide. Foundation for Web3 identity bridge and corporate document control. Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
cbc49e93fe
commit
c6def43d69
1 changed files with 153 additions and 0 deletions
153
docker/tools/key-bridge-prototype.py
Normal file
153
docker/tools/key-bridge-prototype.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lethean Key Bridge Prototype
|
||||
Derives an HNS sidechain secp256k1 keypair from a Lethean ed25519 spend key.
|
||||
|
||||
One seed phrase → both chains.
|
||||
|
||||
Lethean seed phrase
|
||||
↓
|
||||
ed25519 spend key (32 bytes)
|
||||
├── Main chain wallet (native CryptoNote)
|
||||
└── HKDF("lethean-hns-bridge", spend_key)
|
||||
↓
|
||||
secp256k1 private key (32 bytes)
|
||||
└── Sidechain wallet (derived)
|
||||
|
||||
Usage:
|
||||
python3 key-bridge-prototype.py <hex_spend_key>
|
||||
python3 key-bridge-prototype.py --from-wallet http://127.0.0.1:46944/json_rpc
|
||||
|
||||
Dependencies:
|
||||
pip install cryptography secp256k1 (or use hashlib for prototype)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import sys
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
|
||||
def derive_sidechain_key(spend_key_hex: str) -> dict:
|
||||
"""
|
||||
Derive a secp256k1 private key from a Lethean ed25519 spend key.
|
||||
|
||||
Uses HKDF-like construction:
|
||||
prk = HMAC-SHA256(key="lethean-hns-bridge", msg=spend_key)
|
||||
secp256k1_key = HMAC-SHA256(key=prk, msg="hns-sidechain-v1" || 0x01)
|
||||
|
||||
The domain separation ensures:
|
||||
- Different derivation paths produce different keys
|
||||
- The ed25519 key cannot be recovered from the secp256k1 key
|
||||
- Future derivation paths (v2, v3) won't collide
|
||||
"""
|
||||
spend_key = bytes.fromhex(spend_key_hex)
|
||||
assert len(spend_key) == 32, f"Spend key must be 32 bytes, got {len(spend_key)}"
|
||||
|
||||
# Step 1: Extract — domain-separated PRK
|
||||
salt = b"lethean-hns-bridge"
|
||||
prk = hmac.new(salt, spend_key, hashlib.sha256).digest()
|
||||
|
||||
# Step 2: Expand — derive the secp256k1 key
|
||||
info = b"hns-sidechain-v1\x01"
|
||||
secp256k1_privkey = hmac.new(prk, info, hashlib.sha256).digest()
|
||||
|
||||
# secp256k1 requires the key to be in range [1, n-1]
|
||||
# n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
key_int = int.from_bytes(secp256k1_privkey, 'big')
|
||||
if key_int == 0 or key_int >= n:
|
||||
# Extremely unlikely but handle it — rehash
|
||||
secp256k1_privkey = hmac.new(prk, b"hns-sidechain-v1\x02", hashlib.sha256).digest()
|
||||
key_int = int.from_bytes(secp256k1_privkey, 'big') % (n - 1) + 1
|
||||
secp256k1_privkey = key_int.to_bytes(32, 'big')
|
||||
|
||||
# Derive additional keys for future use
|
||||
document_signing_key = hmac.new(prk, b"document-signing-v1\x01", hashlib.sha256).digest()
|
||||
audit_key = hmac.new(prk, b"audit-trail-v1\x01", hashlib.sha256).digest()
|
||||
|
||||
return {
|
||||
"source": {
|
||||
"type": "ed25519",
|
||||
"chain": "lethean-mainchain",
|
||||
"spend_key": spend_key_hex,
|
||||
},
|
||||
"derived": {
|
||||
"hns_sidechain": {
|
||||
"type": "secp256k1",
|
||||
"chain": "lethean-hns-sidechain",
|
||||
"private_key": secp256k1_privkey.hex(),
|
||||
"derivation": "HKDF(salt=lethean-hns-bridge, ikm=spend_key, info=hns-sidechain-v1)",
|
||||
},
|
||||
"document_signing": {
|
||||
"type": "ed25519",
|
||||
"purpose": "document hash timestamping",
|
||||
"private_key": document_signing_key.hex(),
|
||||
"derivation": "HKDF(salt=lethean-hns-bridge, ikm=spend_key, info=document-signing-v1)",
|
||||
},
|
||||
"audit": {
|
||||
"type": "ed25519",
|
||||
"purpose": "audit trail verification",
|
||||
"private_key": audit_key.hex(),
|
||||
"derivation": "HKDF(salt=lethean-hns-bridge, ikm=spend_key, info=audit-trail-v1)",
|
||||
},
|
||||
},
|
||||
"derivation_version": "1.0",
|
||||
"note": "All keys derived deterministically from the Lethean spend key. One seed, all chains and purposes.",
|
||||
}
|
||||
|
||||
|
||||
def get_spend_key_from_wallet(rpc_url: str) -> str:
|
||||
"""Fetch the spend key from a running wallet RPC."""
|
||||
payload = json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"id": "0",
|
||||
"method": "get_wallet_info",
|
||||
"params": {}
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(
|
||||
rpc_url,
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
data = json.loads(resp.read())
|
||||
# The spend key isn't directly in get_wallet_info
|
||||
# We'd need a custom RPC or the seed to derive it
|
||||
# For prototype, use the address as a stand-in
|
||||
print("Note: Full implementation needs 'get_spend_key' RPC or seed phrase input")
|
||||
print(f"Wallet address: {data.get('result', {}).get('wi', {}).get('address', '?')}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error connecting to wallet: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
print("\nExample with test key:")
|
||||
test_key = "a" * 64 # 32 bytes of 0xAA
|
||||
result = derive_sidechain_key(test_key)
|
||||
print(json.dumps(result, indent=2))
|
||||
return
|
||||
|
||||
if sys.argv[1] == "--from-wallet":
|
||||
rpc_url = sys.argv[2] if len(sys.argv) > 2 else "http://127.0.0.1:46944/json_rpc"
|
||||
spend_key = get_spend_key_from_wallet(rpc_url)
|
||||
if not spend_key:
|
||||
print("\nUsing test key for demonstration:")
|
||||
spend_key = "7b9f1e2a3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7"
|
||||
else:
|
||||
spend_key = sys.argv[1]
|
||||
|
||||
result = derive_sidechain_key(spend_key)
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Reference in a new issue