diff --git a/core/tools/binctl b/core/tools/binctl index c86c8bf56..9685fa3dd 100755 --- a/core/tools/binctl +++ b/core/tools/binctl @@ -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") diff --git a/core/tools/keyctl b/core/tools/keyctl index dfcd90736..d1c9aaf0c 100755 --- a/core/tools/keyctl +++ b/core/tools/keyctl @@ -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()) diff --git a/core/tools/keyctl-coordinator b/core/tools/keyctl-coordinator index 75499a96c..3ed7ac24a 100755 --- a/core/tools/keyctl-coordinator +++ b/core/tools/keyctl-coordinator @@ -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()) diff --git a/python/src/trezorlib/cosi.py b/python/src/trezorlib/cosi.py index 057693c9a..51e59acb3 100644 --- a/python/src/trezorlib/cosi.py +++ b/python/src/trezorlib/cosi.py @@ -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: diff --git a/python/src/trezorlib/firmware.py b/python/src/trezorlib/firmware.py index 74ac0b23c..461e9d286 100644 --- a/python/src/trezorlib/firmware.py +++ b/python/src/trezorlib/firmware.py @@ -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.") diff --git a/python/tests/test_cosi.py b/python/tests/test_cosi.py index 88281f576..e58b6b1bf 100644 --- a/python/tests/test_cosi.py +++ b/python/tests/test_cosi.py @@ -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): - # wrong sigmask - cosi.verify_m_of_n(global_sig, message, 1, 4, 5, pubkeys) + 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(ValueError): - # can't use "0 of N" scheme - cosi.verify_m_of_n(global_sig, message, 0, 4, sigmask, pubkeys) + with pytest.raises(_ed25519.SignatureMismatch) as e: + # wrong sigmask + cosi.verify(global_sig, message, 3, pubkeys, 7) + assert "signature does not pass verification" in e.value.args[0] diff --git a/tests/device_tests/test_cosi.py b/tests/device_tests/test_cosi.py index aadb33e9d..1f562bb58 100644 --- a/tests/device_tests/test_cosi.py +++ b/tests/device_tests/test_cosi.py @@ -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)