diff --git a/trezorlib/cosi.py b/trezorlib/cosi.py index 1bbdbdcf2..8bb4f4ea8 100644 --- a/trezorlib/cosi.py +++ b/trezorlib/cosi.py @@ -83,6 +83,8 @@ def verify_m_of_n( mask: int, keys: List[Ed25519PublicPoint], ) -> 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( diff --git a/trezorlib/tests/unit_tests/test_cosi.py b/trezorlib/tests/unit_tests/test_cosi.py index 8672c9d6c..06ba32004 100644 --- a/trezorlib/tests/unit_tests/test_cosi.py +++ b/trezorlib/tests/unit_tests/test_cosi.py @@ -149,5 +149,51 @@ def test_cosi_combination(keyset): try: cosi.verify(global_sig, message, global_pk) - except ValueError: + except Exception: pytest.fail("Failed to validate global signature") + + +def test_m_of_n(): + privkeys, pubkeys, _, _ = zip(*RFC8032_VECTORS) + message = hashlib.sha512(b"My hovercraft is full of eels!").digest() + + signer_ids = 0, 2, 3 + signers = [privkeys[i] for i in signer_ids] + signer_pubkeys = [pubkeys[i] for i in signer_ids] + sigmask = sum(1 << i for i in signer_ids) + + # generate multisignature + nonce_pairs = [cosi.get_nonce(pk, message) for pk in signers] + nonces, commits = zip(*nonce_pairs) + global_pk = cosi.combine_keys(signer_pubkeys) + global_commit = cosi.combine_keys(commits) + signatures = [ + cosi.sign_with_privkey(message, privkey, global_pk, nonce, global_commit) + for privkey, nonce in zip(signers, nonces) + ] + global_sig = cosi.combine_sig(global_commit, signatures) + + try: + # this is what we are actually doing + cosi.verify_m_of_n(global_sig, message, 3, 4, sigmask, pubkeys) + # we can require less signers too + cosi.verify_m_of_n(global_sig, message, 1, 4, sigmask, pubkeys) + 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] + + 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(_ed25519.SignatureMismatch): + # 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)