mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-22 07:28:10 +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)
|
firmware = FirmwareImage(data, vheader.hdrlen)
|
||||||
# check signatures against signing keys in the vendor header
|
# check signatures against signing keys in the vendor header
|
||||||
if firmware.sigmask > 0:
|
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 = (
|
hdr = (
|
||||||
subdata[: IMAGE_HEADER_SIZE - IMAGE_SIG_SIZE]
|
subdata[: IMAGE_HEADER_SIZE - IMAGE_SIG_SIZE]
|
||||||
+ IMAGE_SIG_SIZE * b"\x00"
|
+ IMAGE_SIG_SIZE * b"\x00"
|
||||||
)
|
)
|
||||||
digest = pyblake2.blake2s(hdr).digest()
|
digest = pyblake2.blake2s(hdr).digest()
|
||||||
try:
|
try:
|
||||||
cosi.verify(firmware.sig, digest, global_pk)
|
cosi.verify(
|
||||||
|
firmware.sig,
|
||||||
|
digest,
|
||||||
|
vheader.vsig_m,
|
||||||
|
vheader.vpub,
|
||||||
|
firmware.sigmask,
|
||||||
|
)
|
||||||
print("Firmware signature OK")
|
print("Firmware signature OK")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("Firmware signature INCORRECT")
|
print("Firmware signature INCORRECT")
|
||||||
|
@ -55,7 +55,7 @@ def sign(index, filename, seckeys):
|
|||||||
sigs.append(sig)
|
sigs.append(sig)
|
||||||
# compute global signature
|
# compute global signature
|
||||||
sig = cosi.combine_sig(global_R, sigs)
|
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())
|
print(binascii.hexlify(sig).decode())
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ def sign(index, filename, participants):
|
|||||||
print("collected signature #%d from %s" % (i, p._pyroUri.host))
|
print("collected signature #%d from %s" % (i, p._pyroUri.host))
|
||||||
# compute global signature
|
# compute global signature
|
||||||
sig = cosi.combine_sig(global_R, sigs)
|
sig = cosi.combine_sig(global_R, sigs)
|
||||||
cosi.verify(sig, digest, global_pk)
|
cosi.verify_combined(sig, digest, global_pk)
|
||||||
print("global signature:")
|
print("global signature:")
|
||||||
print(binascii.hexlify(sig).decode())
|
print(binascii.hexlify(sig).decode())
|
||||||
|
|
||||||
|
@ -67,31 +67,45 @@ def get_nonce(
|
|||||||
return r, Ed25519PublicPoint(_ed25519.encodepoint(R))
|
return r, Ed25519PublicPoint(_ed25519.encodepoint(R))
|
||||||
|
|
||||||
|
|
||||||
def verify(
|
def verify_combined(
|
||||||
signature: Ed25519Signature, digest: bytes, pub_key: Ed25519PublicPoint
|
signature: Ed25519Signature, digest: bytes, pub_key: Ed25519PublicPoint
|
||||||
) -> None:
|
) -> 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
|
# XXX this *might* change to bool function
|
||||||
_ed25519.checkvalid(signature, digest, pub_key)
|
_ed25519.checkvalid(signature, digest, pub_key)
|
||||||
|
|
||||||
|
|
||||||
def verify_m_of_n(
|
def verify(
|
||||||
signature: Ed25519Signature,
|
signature: Ed25519Signature,
|
||||||
digest: bytes,
|
digest: bytes,
|
||||||
m: int,
|
sigs_required: int,
|
||||||
n: int,
|
|
||||||
mask: int,
|
|
||||||
keys: List[Ed25519PublicPoint],
|
keys: List[Ed25519PublicPoint],
|
||||||
|
mask: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
if m < 1:
|
"""Verify a CoSi multi-signature. Raise exception if the signature is invalid.
|
||||||
raise ValueError("At least 1 signer must be specified")
|
|
||||||
selected_keys = [keys[i] for i in range(n) if mask & (1 << i)]
|
This function verifies a M-of-N signature scheme. The arguments are:
|
||||||
if len(selected_keys) < m:
|
- the minimum number M of signatures required
|
||||||
raise ValueError(
|
- public keys of all N possible cosigners
|
||||||
"Not enough signers ({} required, {} found)".format(m, len(selected_keys))
|
- 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)
|
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:
|
def pubkey_from_privkey(privkey: Ed25519PrivateKey) -> Ed25519PublicPoint:
|
||||||
|
@ -49,8 +49,8 @@ V2_BOOTLOADER_KEYS = [
|
|||||||
"b8307a71f552c60a4cbb317ff48b82cdbf6b6bb5f04c920fec7badf017883751",
|
"b8307a71f552c60a4cbb317ff48b82cdbf6b6bb5f04c920fec7badf017883751",
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
V2_BOOTLOADER_M = 2
|
|
||||||
V2_BOOTLOADER_N = 3
|
V2_SIGS_REQUIRED = 2
|
||||||
|
|
||||||
ONEV2_CHUNK_SIZE = 1024 * 64
|
ONEV2_CHUNK_SIZE = 1024 * 64
|
||||||
V2_CHUNK_SIZE = 1024 * 128
|
V2_CHUNK_SIZE = 1024 * 128
|
||||||
@ -408,13 +408,12 @@ def validate_v2(fw: c.Container, skip_vendor_header: bool = False) -> None:
|
|||||||
try:
|
try:
|
||||||
# if you want to validate a custom vendor header, you can modify
|
# if you want to validate a custom vendor header, you can modify
|
||||||
# the global variables to match your keys and m-of-n scheme
|
# the global variables to match your keys and m-of-n scheme
|
||||||
cosi.verify_m_of_n(
|
cosi.verify(
|
||||||
fw.vendor_header.signature,
|
fw.vendor_header.signature,
|
||||||
vendor_fingerprint,
|
vendor_fingerprint,
|
||||||
V2_BOOTLOADER_M,
|
V2_SIGS_REQUIRED,
|
||||||
V2_BOOTLOADER_N,
|
|
||||||
fw.vendor_header.sigmask,
|
|
||||||
V2_BOOTLOADER_KEYS,
|
V2_BOOTLOADER_KEYS,
|
||||||
|
fw.vendor_header.sigmask,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise InvalidSignatureError("Invalid vendor header signature.")
|
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.")
|
# raise ValueError("Vendor header expired.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cosi.verify_m_of_n(
|
cosi.verify(
|
||||||
fw.image.header.signature,
|
fw.image.header.signature,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
fw.vendor_header.vendor_sigs_required,
|
fw.vendor_header.vendor_sigs_required,
|
||||||
fw.vendor_header.vendor_sigs_n,
|
|
||||||
fw.image.header.sigmask,
|
|
||||||
fw.vendor_header.pubkeys,
|
fw.vendor_header.pubkeys,
|
||||||
|
fw.image.header.sigmask,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise InvalidSignatureError("Invalid firmware signature.")
|
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)
|
my_pubkey = cosi.pubkey_from_privkey(privkey)
|
||||||
assert my_pubkey == pubkey
|
assert my_pubkey == pubkey
|
||||||
try:
|
try:
|
||||||
cosi.verify(signature, message, pubkey)
|
cosi.verify_combined(signature, message, pubkey)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pytest.fail("Signature does not verify.")
|
pytest.fail("Signature does not verify.")
|
||||||
|
|
||||||
fake_signature = signature[:37] + b"\xf0" + signature[38:]
|
fake_signature = signature[:37] + b"\xf0" + signature[38:]
|
||||||
with pytest.raises(_ed25519.SignatureMismatch):
|
with pytest.raises(_ed25519.SignatureMismatch):
|
||||||
cosi.verify(fake_signature, message, pubkey)
|
cosi.verify_combined(fake_signature, message, pubkey)
|
||||||
|
|
||||||
|
|
||||||
def test_combine_keys():
|
def test_combine_keys():
|
||||||
@ -148,7 +148,7 @@ def test_cosi_combination(keyset):
|
|||||||
global_sig = cosi.combine_sig(global_commit, signatures)
|
global_sig = cosi.combine_sig(global_commit, signatures)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cosi.verify(global_sig, message, global_pk)
|
cosi.verify_combined(global_sig, message, global_pk)
|
||||||
except Exception:
|
except Exception:
|
||||||
pytest.fail("Failed to validate global signature")
|
pytest.fail("Failed to validate global signature")
|
||||||
|
|
||||||
@ -175,25 +175,27 @@ def test_m_of_n():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# this is what we are actually doing
|
# 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
|
# 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:
|
except Exception:
|
||||||
pytest.fail("Failed to validate by sigmask")
|
pytest.fail("Failed to validate by sigmask")
|
||||||
|
|
||||||
# and now for various ways that should fail
|
# and now for various ways that should fail
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError) as e:
|
||||||
cosi.verify_m_of_n(global_sig, message, 4, 4, sigmask, pubkeys)
|
cosi.verify(global_sig, message, 3, pubkeys[:2], sigmask)
|
||||||
assert "Not enough signers" in e.value.args[0]
|
assert "more public keys than provided" in e.value.args[0]
|
||||||
|
|
||||||
with pytest.raises(_ed25519.SignatureMismatch):
|
with pytest.raises(ValueError) as e:
|
||||||
# when N < number of possible signers, the topmost signers will be ignored
|
cosi.verify(global_sig, message, 0, pubkeys, 0)
|
||||||
cosi.verify_m_of_n(global_sig, message, 2, 3, sigmask, pubkeys)
|
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
|
# wrong sigmask
|
||||||
cosi.verify_m_of_n(global_sig, message, 1, 4, 5, pubkeys)
|
cosi.verify(global_sig, message, 3, pubkeys, 7)
|
||||||
|
assert "signature does not pass verification" 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)
|
|
||||||
|
@ -73,7 +73,7 @@ class TestCosi:
|
|||||||
global_R, [sig0.signature, sig1.signature, sig2.signature]
|
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):
|
def test_cosi_compat(self, client):
|
||||||
digest = sha256(b"this is not a pipe").digest()
|
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])
|
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