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 2 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()
}
73 changes: 72 additions & 1 deletion hermit/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
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 @@ -43,6 +45,19 @@ def validate_coordinator_signature_if_necessary(original_psbt: PSBT) -> None:
validate_rsa_signature(unsigned_psbt_base64_bytes, sig_bytes)


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 = PrivateKey.parse(private_key_file.read().strip())

signature = private_key.sign_message(message)
return signature.der()


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

Expand Down Expand Up @@ -92,8 +107,43 @@ def validate_rsa_signature(message: bytes, signature: bytes) -> None:
raise InvalidCoordinatorSignature("Coordinator signature is invalid.")


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`).

Will raise :class:`~hermit.errors.InvalidCoordinatorSignature` if
the public key is missing or invalid or if the signature is
invalid.

"""
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 = S256Point.parse(bytes.fromhex(public_key_text))
except Exception:
raise InvalidCoordinatorSignature(
"Coordinator signature is present but coordinator public key is invalid."
)

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.
return extract_signature_params(original_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 @@ -117,6 +167,9 @@ def extract_rsa_signature_params(original_psbt: PSBT) -> Tuple[bytes, bytes]:

def add_rsa_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()
Expand All @@ -128,3 +181,21 @@ def add_rsa_signature(original_psbt: PSBT, private_key_path: str) -> PSBT:

original_psbt.extra_map[COORDINATOR_SIGNATURE_KEY] = sig_bytes
return original_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_secp256k1_signature(
bytes(psbt_base64, "utf-8"),
private_key_path,
)

original_psbt.extra_map[COORDINATOR_SIGNATURE_KEY] = sig_bytes
return original_psbt
1 change: 1 addition & 0 deletions tests/fixtures/coordinator.privkey
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
L2mQE1xS3wSj2miAUhT9MGNhCsPNRzd3xvmDozsk6uuWSVGNt2oC
1 change: 1 addition & 0 deletions tests/fixtures/coordinator.pubkey
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
02acbc80af6db9f7308db032d782d51855745bd64a239496b60b239dc96d6dba12
65 changes: 58 additions & 7 deletions tests/test_coordinator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from unittest.mock import Mock, patch
from pytest import raises
from buidl.psbt import PSBT
from buidl import PrivateKey, PSBT

from hermit import InvalidCoordinatorSignature
from hermit.coordinator import (
Expand All @@ -9,6 +9,9 @@
create_rsa_signature,
add_rsa_signature,
extract_rsa_signature_params,
create_secp256k1_signature,
add_secp256k1_signature,
validate_secp256k1_signature,
COORDINATOR_SIGNATURE_KEY,
)

Expand Down Expand Up @@ -208,14 +211,12 @@ def setup(self):
self.public_key = open("tests/fixtures/coordinator.pub", "r").read()
self.private_key_path = "tests/fixtures/coordinator.pem"

self.original_psbt_base64 = open("tests/fixtures/signature_requests/2-of-2.p2sh.testnet.psbt", "r").read()
self.original_psbt_base64 = open(
"tests/fixtures/signature_requests/2-of-2.p2sh.testnet.psbt", "r"
).read()

self.psbt = PSBT.parse_base64(self.original_psbt_base64)
self.psbt_base64 = self.psbt.serialize_base64()
self.signature = create_rsa_signature(
bytes(self.psbt_base64, "utf8"),
self.private_key_path
)

self.config = Mock()
self.coordinator_config = dict(public_key=self.public_key)
Expand All @@ -230,9 +231,12 @@ def test_psbt_serialization_stable(self, mock_config):
def test_psbt_signature(self, mock_config):
mock_config.return_value = self.config

signature = create_rsa_signature(
bytes(self.psbt_base64, "utf-8"), self.private_key_path
)
add_rsa_signature(self.psbt, self.private_key_path)

assert self.psbt.extra_map[COORDINATOR_SIGNATURE_KEY] == self.signature
assert self.psbt.extra_map[COORDINATOR_SIGNATURE_KEY] == signature

def test_validate_psbt_signature(self, mock_config):
mock_config.return_value = self.config
Expand All @@ -241,3 +245,50 @@ def test_validate_psbt_signature(self, mock_config):

unsigned_psbt_base64_bytes, sig_bytes = extract_rsa_signature_params(self.psbt)
validate_rsa_signature(unsigned_psbt_base64_bytes, sig_bytes)


@patch("hermit.coordinator.get_config")
class TestPSBTSignatureSecP256K1Basics(object):
def setup(self):
# Pubkey stored in hex in the fixtures folder
self.public_key = open("tests/fixtures/coordinator.pubkey", "r").read().strip()

# Privkey stored in wif format in the fixtures folder
self.private_key_path = "tests/fixtures/coordinator.privkey"
# self.private_key = PrivateKey.parse(open(self.private_key_path, "r").read().strip())

self.original_psbt_base64 = open(
"tests/fixtures/signature_requests/2-of-2.p2sh.testnet.psbt", "r"
).read()

self.psbt = PSBT.parse_base64(self.original_psbt_base64)
self.psbt_base64 = self.psbt.serialize_base64()

self.config = Mock()
self.coordinator_config = dict(public_key=self.public_key)
self.config.coordinator = self.coordinator_config

def test_psbt_serialization_stable(self, mock_config):
mock_config.return_value = self.config

p2 = PSBT.parse_base64(self.psbt_base64)
assert p2.serialize_base64() == self.psbt.serialize_base64()

def test_psbt_signature(self, mock_config):
mock_config.return_value = self.config

signature = create_secp256k1_signature(
bytes(self.psbt_base64, "utf-8"), self.private_key_path
)

add_secp256k1_signature(self.psbt, self.private_key_path)

assert self.psbt.extra_map[COORDINATOR_SIGNATURE_KEY] == signature

def test_validate_psbt_signature(self, mock_config):
mock_config.return_value = self.config

add_secp256k1_signature(self.psbt, self.private_key_path)

unsigned_psbt_base64_bytes, sig_bytes = extract_rsa_signature_params(self.psbt)
validate_secp256k1_signature(unsigned_psbt_base64_bytes, sig_bytes)