Skip to content
This repository was archived by the owner on Feb 16, 2025. It is now read-only.
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
4 changes: 2 additions & 2 deletions hermit/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,6 @@ def _interpolate_commands(self) -> None:

def _interpolate_paths(self) -> None:
self.config["paths"] = {
key : os.path.expandvars(os.path.expanduser(value))
for key, value in self.config['paths'].items()
key: os.path.expandvars(os.path.expanduser(value))
for key, value in self.config["paths"].items()
}
53 changes: 25 additions & 28 deletions hermit/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from typing import Tuple

from buidl import PSBT
from Crypto.Hash import SHA256
from Crypto.Signature import PKCS1_v1_5
from Crypto.PublicKey import RSA

from .config import get_config
from .errors import InvalidCoordinatorSignature

from buidl import PrivateKey, S256Point, Signature

#: The key that holds an optional coordinator signature for a PSBT.
COORDINATOR_SIGNATURE_KEY: bytes = "coordinator_sig".encode("utf8")

Expand Down Expand Up @@ -39,30 +38,25 @@ def validate_coordinator_signature_if_necessary(original_psbt: PSBT) -> None:
else:
return

unsigned_psbt_base64_bytes, sig_bytes = extract_rsa_signature_params(original_psbt)
validate_rsa_signature(unsigned_psbt_base64_bytes, sig_bytes)

unsigned_psbt_base64_bytes, sig_bytes = extract_signature_params(original_psbt)
validate_secp256k1_signature(unsigned_psbt_base64_bytes, sig_bytes)

def create_rsa_signature(message: bytes, private_key_path: str) -> bytes:
"""Create an RSA signature.

This function is not called within usual Hermit operation. It is
useful for scripts and tests.
def create_secp256k1_signature(message: bytes, private_key_path: str) -> bytes:
"""Create a secp256k1 signature.

This function is not called within usual Hermit operation. It is useful
for scripts and tests.
"""

with open(private_key_path, mode="r") as private_key_file:
private_key = RSA.importKey(private_key_file.read())
private_key = PrivateKey.parse(private_key_file.read().strip())

digest = SHA256.new()
digest.update(message)
signer = PKCS1_v1_5.new(private_key)
signature = signer.sign(digest)
return signature
signature = private_key.sign_message(message)
return signature.der()


def validate_rsa_signature(message: bytes, signature: bytes) -> None:
"""Validate an RSA signature.
def validate_secp256k1_signature(message: bytes, signature: bytes) -> None:
"""Validate a secp256k1 signature.

Uses the public key from Hermit's configuration for verification
(see :attr:`~hermit.config.DefaultCoordinator`).
Expand All @@ -73,27 +67,27 @@ def validate_rsa_signature(message: bytes, signature: bytes) -> None:

"""
public_key_text = get_config().coordinator.get("public_key")

if public_key_text is None:
raise InvalidCoordinatorSignature(
"Coordinator signature is present but no public key is configured."
)

try:
public_key = RSA.importKey(public_key_text)
public_key = S256Point.parse(bytes.fromhex(public_key_text))
except Exception:
raise InvalidCoordinatorSignature(
"Coordinator signature is present but coordinator public key is invalid."
)

digest = SHA256.new()
digest.update(message)
verifier = PKCS1_v1_5.new(public_key)
if not verifier.verify(digest, signature): # type: ignore
sig = Signature.parse(signature)

if not public_key.verify_message(message, sig):
raise InvalidCoordinatorSignature("Coordinator signature is invalid.")


def extract_rsa_signature_params(original_psbt: PSBT) -> Tuple[bytes, bytes]:
"""Extract RSA signature parameters from a PSBT.
def extract_signature_params(original_psbt: PSBT) -> Tuple[bytes, bytes]:
"""Extract signature parameters from a PSBT.

The value of the :attr:`COORDINATOR_SIGNATURE_KEY` key within the
PSBT's `extra_map` is extracted as the signature bytes.
Expand All @@ -115,13 +109,16 @@ def extract_rsa_signature_params(original_psbt: PSBT) -> Tuple[bytes, bytes]:
return unsigned_psbt_base64_bytes, sig_bytes


def add_rsa_signature(original_psbt: PSBT, private_key_path: str) -> PSBT:
def add_secp256k1_signature(original_psbt: PSBT, private_key_path: str) -> PSBT:
"""Add a signature to a PSBT.

This is useful for scripts and tests, but not actually ever called in
the course of regular Hermit operation
"""

psbt_base64 = original_psbt.serialize_base64()

sig_bytes = create_rsa_signature(
sig_bytes = create_secp256k1_signature(
bytes(psbt_base64, "utf-8"),
private_key_path,
)
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ pluggy==1.0.0
prompt-toolkit==2.0.7
py==1.10.0
pycodestyle==2.7.0
pycryptodome==3.11.0
pyflakes==2.3.1
Pygments==2.10.0
pyparsing==2.4.7
Expand Down
26 changes: 26 additions & 0 deletions scripts/normalize_psbt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from sys import stdin, argv
from os.path import basename

from buidl import PSBT


HELP = f"""usage: cat ... | python {basename(__file__)}

This program reads a PSBT (in base64) over STDIN, reads it into
buidl and prints the resulting PSBT in connonical order.
"""

if __name__ == "__main__":

if ("--help" in argv) or ("-h" in argv) or len(argv) != 1:
print(HELP)
exit(1)

raw_unsigned_psbt_base64 = stdin.read().strip()
if len(raw_unsigned_psbt_base64) == 0:
print("Input PSBT is required.")
exit(2)

psbt = PSBT.parse_base64(raw_unsigned_psbt_base64)

print(psbt.serialize_base64())
21 changes: 12 additions & 9 deletions scripts/sign_psbt_as_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
from buidl import PSBT

from hermit.coordinator import (
COORDINATOR_SIGNATURE_KEY,
create_rsa_signature,
add_secp256k1_signature,
)

HELP = f"""usage: cat ... | python {basename(__file__)} PRIVATE_KEY_PATH

This program reads an unsigned PSBT (in base64) over STDIN, signs that
PSBT as a coordinator, and prints the resulting PSBT..

The RSA private key at PRIVATE_KEY_PATH is used for signing."""
The secp256k1 private key at PRIVATE_KEY_PATH is used for signing."""

if __name__ == "__main__":

Expand All @@ -23,15 +22,19 @@

private_key_path = argv[1]

unsigned_psbt_base64 = stdin.read().strip()
if len(unsigned_psbt_base64) == 0:
raw_unsigned_psbt_base64 = stdin.read().strip()
if len(raw_unsigned_psbt_base64) == 0:
print("Input PSBT is required.")
exit(2)
psbt = PSBT.parse_base64(unsigned_psbt_base64)

message = unsigned_psbt_base64.encode("utf8")
signature = create_rsa_signature(message, private_key_path)
psbt = PSBT.parse_base64(raw_unsigned_psbt_base64)
unsigned_psbt_base64 = psbt.serialize_base64()

if unsigned_psbt_base64 != raw_unsigned_psbt_base64:
print("Input PSBT must be generated by buidl.")
exit(2)

psbt.extra_map[COORDINATOR_SIGNATURE_KEY] = signature
message = unsigned_psbt_base64.encode("utf8")
add_secp256k1_signature(psbt, private_key_path)

print(psbt.serialize_base64())
15 changes: 0 additions & 15 deletions tests/fixtures/coordinator.pem

This file was deleted.

1 change: 1 addition & 0 deletions tests/fixtures/coordinator.privkey
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
L2mQE1xS3wSj2miAUhT9MGNhCsPNRzd3xvmDozsk6uuWSVGNt2oC
6 changes: 0 additions & 6 deletions tests/fixtures/coordinator.pub

This file was deleted.

1 change: 1 addition & 0 deletions tests/fixtures/coordinator.pubkey
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
02acbc80af6db9f7308db032d782d51855745bd64a239496b60b239dc96d6dba12
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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
Loading