mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-27 16:48:09 +00:00
python/cosi: improve API
cosi.verify was renamed to verify_combined, because it is pretty much ed25519.verify, and the new name implies what it does in terms of the CoSi scheme: verify a signature with already-combined public keys. cosi.verify_m_of_n signature was simplified by not requiring the `n` parameter, which is not important for verification. The updated function was renamed to cosi.verify, because this is the standard CoSi verification operation: given signature, digest, required number of signatures, sigmask, and a list of public keys, verify that enough signatures are indicated and that they sign the digest.
This commit is contained in:
parent
941087179f
commit
15bd35824b
@ -311,15 +311,19 @@ def binopen(filename):
|
||||
firmware = FirmwareImage(data, vheader.hdrlen)
|
||||
# check signatures against signing keys in the vendor header
|
||||
if firmware.sigmask > 0:
|
||||
pk = [vheader.vpub[i] for i in range(8) if firmware.sigmask & (1 << i)]
|
||||
global_pk = cosi.combine_keys(pk)
|
||||
hdr = (
|
||||
subdata[: IMAGE_HEADER_SIZE - IMAGE_SIG_SIZE]
|
||||
+ IMAGE_SIG_SIZE * b"\x00"
|
||||
)
|
||||
digest = pyblake2.blake2s(hdr).digest()
|
||||
try:
|
||||
cosi.verify(firmware.sig, digest, global_pk)
|
||||
cosi.verify(
|
||||
firmware.sig,
|
||||
digest,
|
||||
vheader.vsig_m,
|
||||
vheader.vpub,
|
||||
firmware.sigmask,
|
||||
)
|
||||
print("Firmware signature OK")
|
||||
except ValueError:
|
||||
print("Firmware signature INCORRECT")
|
||||
|
@ -55,7 +55,7 @@ def sign(index, filename, seckeys):
|
||||
sigs.append(sig)
|
||||
# compute global signature
|
||||
sig = cosi.combine_sig(global_R, sigs)
|
||||
cosi.verify(sig, digest, global_pk)
|
||||
cosi.verify_combined(sig, digest, global_pk)
|
||||
print(binascii.hexlify(sig).decode())
|
||||
|
||||
|
||||
|
@ -66,7 +66,7 @@ def sign(index, filename, participants):
|
||||
print("collected signature #%d from %s" % (i, p._pyroUri.host))
|
||||
# compute global signature
|
||||
sig = cosi.combine_sig(global_R, sigs)
|
||||
cosi.verify(sig, digest, global_pk)
|
||||
cosi.verify_combined(sig, digest, global_pk)
|
||||
print("global signature:")
|
||||
print(binascii.hexlify(sig).decode())
|
||||
|
||||
|
@ -67,31 +67,45 @@ def get_nonce(
|
||||
return r, Ed25519PublicPoint(_ed25519.encodepoint(R))
|
||||
|
||||
|
||||
def verify(
|
||||
def verify_combined(
|
||||
signature: Ed25519Signature, digest: bytes, pub_key: Ed25519PublicPoint
|
||||
) -> None:
|
||||
"""Verify Ed25519 signature. Raise exception if the signature is invalid."""
|
||||
"""Verify Ed25519 signature. Raise exception if the signature is invalid.
|
||||
|
||||
A CoSi combined signature is equivalent to a plain Ed25519 signature with a public
|
||||
key that is a combination of the cosigners' public keys. This function takes the
|
||||
combined public key and performs simple Ed25519 verification.
|
||||
"""
|
||||
# XXX this *might* change to bool function
|
||||
_ed25519.checkvalid(signature, digest, pub_key)
|
||||
|
||||
|
||||
def verify_m_of_n(
|
||||
def verify(
|
||||
signature: Ed25519Signature,
|
||||
digest: bytes,
|
||||
m: int,
|
||||
n: int,
|
||||
mask: int,
|
||||
sigs_required: int,
|
||||
keys: List[Ed25519PublicPoint],
|
||||
mask: int,
|
||||
) -> None:
|
||||
if m < 1:
|
||||
raise ValueError("At least 1 signer must be specified")
|
||||
selected_keys = [keys[i] for i in range(n) if mask & (1 << i)]
|
||||
if len(selected_keys) < m:
|
||||
raise ValueError(
|
||||
"Not enough signers ({} required, {} found)".format(m, len(selected_keys))
|
||||
)
|
||||
"""Verify a CoSi multi-signature. Raise exception if the signature is invalid.
|
||||
|
||||
This function verifies a M-of-N signature scheme. The arguments are:
|
||||
- the minimum number M of signatures required
|
||||
- public keys of all N possible cosigners
|
||||
- a bitmask specifying which of the N cosigners have produced the signature.
|
||||
|
||||
The verification checks that the mask specifies at least M cosigners, then combines
|
||||
the selected public keys and verifies the signature against the combined key.
|
||||
"""
|
||||
if sigs_required < 1:
|
||||
raise ValueError("At least one signer must be specified.")
|
||||
if mask.bit_length() > len(keys):
|
||||
raise ValueError("Sigmask specifies more public keys than provided.")
|
||||
selected_keys = [key for i, key in enumerate(keys) if mask & (1 << i)]
|
||||
if len(selected_keys) < sigs_required:
|
||||
raise _ed25519.SignatureMismatch("Insufficient number of signatures.")
|
||||
global_pk = combine_keys(selected_keys)
|
||||
return verify(signature, digest, global_pk)
|
||||
return verify_combined(signature, digest, global_pk)
|
||||
|
||||
|
||||
def pubkey_from_privkey(privkey: Ed25519PrivateKey) -> Ed25519PublicPoint:
|
||||
|
@ -49,8 +49,8 @@ V2_BOOTLOADER_KEYS = [
|
||||
"b8307a71f552c60a4cbb317ff48b82cdbf6b6bb5f04c920fec7badf017883751",
|
||||
)
|
||||
]
|
||||
V2_BOOTLOADER_M = 2
|
||||
V2_BOOTLOADER_N = 3
|
||||
|
||||
V2_SIGS_REQUIRED = 2
|
||||
|
||||
ONEV2_CHUNK_SIZE = 1024 * 64
|
||||
V2_CHUNK_SIZE = 1024 * 128
|
||||
@ -408,13 +408,12 @@ def validate_v2(fw: c.Container, skip_vendor_header: bool = False) -> None:
|
||||
try:
|
||||
# if you want to validate a custom vendor header, you can modify
|
||||
# the global variables to match your keys and m-of-n scheme
|
||||
cosi.verify_m_of_n(
|
||||
cosi.verify(
|
||||
fw.vendor_header.signature,
|
||||
vendor_fingerprint,
|
||||
V2_BOOTLOADER_M,
|
||||
V2_BOOTLOADER_N,
|
||||
fw.vendor_header.sigmask,
|
||||
V2_SIGS_REQUIRED,
|
||||
V2_BOOTLOADER_KEYS,
|
||||
fw.vendor_header.sigmask,
|
||||
)
|
||||
except Exception:
|
||||
raise InvalidSignatureError("Invalid vendor header signature.")
|
||||
@ -425,13 +424,12 @@ def validate_v2(fw: c.Container, skip_vendor_header: bool = False) -> None:
|
||||
# raise ValueError("Vendor header expired.")
|
||||
|
||||
try:
|
||||
cosi.verify_m_of_n(
|
||||
cosi.verify(
|
||||
fw.image.header.signature,
|
||||
fingerprint,
|
||||
fw.vendor_header.vendor_sigs_required,
|
||||
fw.vendor_header.vendor_sigs_n,
|
||||
fw.image.header.sigmask,
|
||||
fw.vendor_header.pubkeys,
|
||||
fw.image.header.sigmask,
|
||||
)
|
||||
except Exception:
|
||||
raise InvalidSignatureError("Invalid firmware signature.")
|
||||
|
@ -104,13 +104,13 @@ def test_single_eddsa_vector(privkey, pubkey, message, signature):
|
||||
my_pubkey = cosi.pubkey_from_privkey(privkey)
|
||||
assert my_pubkey == pubkey
|
||||
try:
|
||||
cosi.verify(signature, message, pubkey)
|
||||
cosi.verify_combined(signature, message, pubkey)
|
||||
except ValueError:
|
||||
pytest.fail("Signature does not verify.")
|
||||
|
||||
fake_signature = signature[:37] + b"\xf0" + signature[38:]
|
||||
with pytest.raises(_ed25519.SignatureMismatch):
|
||||
cosi.verify(fake_signature, message, pubkey)
|
||||
cosi.verify_combined(fake_signature, message, pubkey)
|
||||
|
||||
|
||||
def test_combine_keys():
|
||||
@ -148,7 +148,7 @@ def test_cosi_combination(keyset):
|
||||
global_sig = cosi.combine_sig(global_commit, signatures)
|
||||
|
||||
try:
|
||||
cosi.verify(global_sig, message, global_pk)
|
||||
cosi.verify_combined(global_sig, message, global_pk)
|
||||
except Exception:
|
||||
pytest.fail("Failed to validate global signature")
|
||||
|
||||
@ -175,25 +175,27 @@ def test_m_of_n():
|
||||
|
||||
try:
|
||||
# this is what we are actually doing
|
||||
cosi.verify_m_of_n(global_sig, message, 3, 4, sigmask, pubkeys)
|
||||
cosi.verify(global_sig, message, 3, pubkeys, sigmask)
|
||||
# we can require less signers too
|
||||
cosi.verify_m_of_n(global_sig, message, 1, 4, sigmask, pubkeys)
|
||||
cosi.verify(global_sig, message, 1, pubkeys, sigmask)
|
||||
except Exception:
|
||||
pytest.fail("Failed to validate by sigmask")
|
||||
|
||||
# and now for various ways that should fail
|
||||
with pytest.raises(ValueError) as e:
|
||||
cosi.verify_m_of_n(global_sig, message, 4, 4, sigmask, pubkeys)
|
||||
assert "Not enough signers" in e.value.args[0]
|
||||
cosi.verify(global_sig, message, 3, pubkeys[:2], sigmask)
|
||||
assert "more public keys than provided" in e.value.args[0]
|
||||
|
||||
with pytest.raises(_ed25519.SignatureMismatch):
|
||||
# when N < number of possible signers, the topmost signers will be ignored
|
||||
cosi.verify_m_of_n(global_sig, message, 2, 3, sigmask, pubkeys)
|
||||
with pytest.raises(ValueError) as e:
|
||||
cosi.verify(global_sig, message, 0, pubkeys, 0)
|
||||
assert "At least one signer" in e.value.args[0]
|
||||
|
||||
with pytest.raises(_ed25519.SignatureMismatch):
|
||||
with pytest.raises(_ed25519.SignatureMismatch) as e:
|
||||
# at least 5 signatures required
|
||||
cosi.verify(global_sig, message, 5, pubkeys, sigmask)
|
||||
assert "Insufficient number of signatures" in e.value.args[0]
|
||||
|
||||
with pytest.raises(_ed25519.SignatureMismatch) as e:
|
||||
# wrong sigmask
|
||||
cosi.verify_m_of_n(global_sig, message, 1, 4, 5, pubkeys)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
# can't use "0 of N" scheme
|
||||
cosi.verify_m_of_n(global_sig, message, 0, 4, sigmask, pubkeys)
|
||||
cosi.verify(global_sig, message, 3, pubkeys, 7)
|
||||
assert "signature does not pass verification" in e.value.args[0]
|
||||
|
@ -73,7 +73,7 @@ class TestCosi:
|
||||
global_R, [sig0.signature, sig1.signature, sig2.signature]
|
||||
)
|
||||
|
||||
cosi.verify(sig, digest, global_pk)
|
||||
cosi.verify_combined(sig, digest, global_pk)
|
||||
|
||||
def test_cosi_compat(self, client):
|
||||
digest = sha256(b"this is not a pipe").digest()
|
||||
@ -94,4 +94,4 @@ class TestCosi:
|
||||
)
|
||||
sig = cosi.combine_sig(global_R, [remote_sig.signature, local_sig])
|
||||
|
||||
cosi.verify(sig, digest, global_pk)
|
||||
cosi.verify_combined(sig, digest, global_pk)
|
||||
|
Loading…
Reference in New Issue
Block a user