diff --git a/.github/workflows/pr-comment-validate.yml b/.github/workflows/pr-comment-validate.yml index 1b4846dd2..6c7640106 100644 --- a/.github/workflows/pr-comment-validate.yml +++ b/.github/workflows/pr-comment-validate.yml @@ -1,6 +1,8 @@ name: PR Validation Checker -on: issue_comment +on: + issue_comment: + types: [created] jobs: pr_commented: @@ -8,7 +10,7 @@ jobs: if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, 'validate') }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest] # macos-latest not tested due to crashing. version: ["3.7", "3.8", "3.9", "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: diff --git a/ipv8/keyvault/private/cryptography25519.py b/ipv8/keyvault/private/cryptography25519.py new file mode 100644 index 000000000..dbf630987 --- /dev/null +++ b/ipv8/keyvault/private/cryptography25519.py @@ -0,0 +1,60 @@ +from binascii import hexlify +from typing import cast + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat + +from ...keyvault.keys import PrivateKey +from ..public.cryptography25519 import KEY_LENGTH, Cryptography25519PK + + +class Cryptography25519SK(PrivateKey, Cryptography25519PK): + """ + A fallback implementation for LibNaCL secret keys. + """ + + def __init__(self, binarykey: bytes = b"") -> None: + """ + Create a new LibNaCL secret key. Optionally load it from a string representation. + Otherwise, generate it from the 25519 curve. + + :param binarykey: load the sk from this string (see key_to_bin()) + """ + # Load the key, if specified + if binarykey: + crypt, seed = (binarykey[:KEY_LENGTH], binarykey[KEY_LENGTH: KEY_LENGTH * 2]) + key = X25519PrivateKey.from_private_bytes(crypt) + self.vk = Ed25519PrivateKey.from_private_bytes(seed) + else: + key = X25519PrivateKey.generate() + self.vk = Ed25519PrivateKey.generate() + hex_vk = hexlify(self.vk.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)) + + super().__init__(pk=key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw), hex_vk=hex_vk) + self.key = key # type: ignore[assignment] + + def pub(self) -> Cryptography25519PK: + """ + Get the public key for this secret key. + """ + return Cryptography25519PK(pk=cast(X25519PrivateKey, self.key).public_key().public_bytes(Encoding.Raw, + PublicFormat.Raw), + hex_vk=hexlify(self.veri.public_bytes(Encoding.Raw, PublicFormat.Raw))) + + def signature(self, msg: bytes) -> bytes: + """ + Create a signature for a message. + + :param msg: the message to sign + :return: the signature for the message + """ + return self.vk.sign(msg) + + def key_to_bin(self) -> bytes: + """ + Get the string representation of this key. + """ + return (b"LibNaCLSK:" + + cast(X25519PrivateKey, self.key).private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption()) + + self.vk.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())) diff --git a/ipv8/keyvault/private/libnaclkey.py b/ipv8/keyvault/private/libnaclkey.py index c9de0d1e7..2b893b9d3 100644 --- a/ipv8/keyvault/private/libnaclkey.py +++ b/ipv8/keyvault/private/libnaclkey.py @@ -1,52 +1,68 @@ -import libnacl -import libnacl.dual -import libnacl.sign - from ...keyvault.keys import PrivateKey from ...keyvault.public.libnaclkey import LibNaCLPK +try: + import libnacl + import libnacl.dual + import libnacl.sign -class LibNaCLSK(PrivateKey, LibNaCLPK): - """ - A LibNaCL implementation of a secret key. - """ - - def __init__(self, binarykey: bytes = b"") -> None: - """ - Create a new LibNaCL secret key. Optionally load it from a string representation. - Otherwise generate it from the 25519 curve. - :param binarykey: load the sk from this string (see key_to_bin()) - """ - # Load the key, if specified - if binarykey: - crypt, seed = (binarykey[:libnacl.crypto_box_SECRETKEYBYTES], - binarykey[libnacl.crypto_box_SECRETKEYBYTES: libnacl.crypto_box_SECRETKEYBYTES - + libnacl.crypto_sign_SEEDBYTES]) - key = libnacl.dual.DualSecret(crypt, seed) - else: - key = libnacl.dual.DualSecret() - - super().__init__(pk=key.pk, hex_vk=key.hex_vk()) - self.key = key - - def pub(self) -> LibNaCLPK: + class LibNaCLSK(PrivateKey, LibNaCLPK): """ - Get the public key for this secret key. + A LibNaCL implementation of a secret key. """ - return LibNaCLPK(pk=self.key.pk, hex_vk=self.veri.hex_vk()) - def signature(self, msg: bytes) -> bytes: - """ - Create a signature for a message. + def __init__(self, binarykey: bytes = b"") -> None: + """ + Create a new LibNaCL secret key. Optionally load it from a string representation. + Otherwise, generate it from the 25519 curve. - :param msg: the message to sign - :return: the signature for the message - """ - return self.key.signature(msg) + :param binarykey: load the sk from this string (see key_to_bin()) + """ + # Load the key, if specified + if binarykey: + crypt, seed = (binarykey[:libnacl.crypto_box_SECRETKEYBYTES], + binarykey[libnacl.crypto_box_SECRETKEYBYTES: libnacl.crypto_box_SECRETKEYBYTES + + libnacl.crypto_sign_SEEDBYTES]) + key = libnacl.dual.DualSecret(crypt, seed) + else: + key = libnacl.dual.DualSecret() + + super().__init__(pk=key.pk, hex_vk=key.hex_vk()) + self.key = key - def key_to_bin(self) -> bytes: + def pub(self) -> LibNaCLPK: + """ + Get the public key for this secret key. + """ + return LibNaCLPK(pk=self.key.pk, hex_vk=self.veri.hex_vk()) + + def signature(self, msg: bytes) -> bytes: + """ + Create a signature for a message. + + :param msg: the message to sign + :return: the signature for the message + """ + return self.key.signature(msg) + + def key_to_bin(self) -> bytes: + """ + Get the string representation of this key. + """ + return b"LibNaCLSK:" + self.key.sk + self.key.seed + +except (ImportError, OSError): + from .cryptography25519 import Cryptography25519SK + + + class LibNaCLSK(Cryptography25519SK): # type: ignore[no-redef] """ - Get the string representation of this key. + Fallback class, actually Cryptography25519SK. """ - return b"LibNaCLSK:" + self.key.sk + self.key.seed + + def pub(self) -> LibNaCLPK: # type: ignore[override] + """ + Get the public key for this secret key. + """ + return LibNaCLPK(super().key_to_bin()[10:]) diff --git a/ipv8/keyvault/public/cryptography25519.py b/ipv8/keyvault/public/cryptography25519.py new file mode 100644 index 000000000..59b9e7f4a --- /dev/null +++ b/ipv8/keyvault/public/cryptography25519.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from binascii import unhexlify +from typing import cast + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +from ...keyvault.keys import PublicKey + +KEY_LENGTH = 32 + + +class Cryptography25519PK(PublicKey): + """ + A fallback implementation for LibNaCL public keys. + """ + + def __init__(self, binarykey: bytes = b"", pk: bytes | None = None, hex_vk: bytes | None = None) -> None: + """ + Create a new LibNaCL public key. Optionally load it from a string representation or + using a public key and verification key. + + :param binarykey: load the pk from this string (see key_to_bin()) + :param pk: the libnacl public key to use in byte format + :param hex_vk: a verification key in hex format + """ + # Load the key, if specified + if binarykey: + pk, vk = (binarykey[:KEY_LENGTH], binarykey[KEY_LENGTH: KEY_LENGTH * 2]) + else: + vk = unhexlify(cast(bytes, hex_vk)) + # Construct the public key and verifier objects + self.key = Ed25519PublicKey.from_public_bytes(cast(bytes, pk)) + self.veri = Ed25519PublicKey.from_public_bytes(vk) + + def verify(self, signature: bytes, msg: bytes) -> bool: + """ + Verify whether a given signature is correct for a message. + + :param signature: the given signature + :param msg: the given message + """ + try: + self.veri.verify(signature, msg) + return True + except InvalidSignature: + return False + + def key_to_bin(self) -> bytes: + """ + Get the string representation of this key. + """ + return (b"LibNaCLPK:" + + self.key.public_bytes(Encoding.Raw, PublicFormat.Raw) + + self.veri.public_bytes(Encoding.Raw, PublicFormat.Raw)) + + def get_signature_length(self) -> int: + """ + Returns the length, in bytes, of each signature made using EC. + """ + return KEY_LENGTH diff --git a/ipv8/keyvault/public/libnaclkey.py b/ipv8/keyvault/public/libnaclkey.py index f567e80cc..5b23b0cdd 100644 --- a/ipv8/keyvault/public/libnaclkey.py +++ b/ipv8/keyvault/public/libnaclkey.py @@ -1,54 +1,64 @@ from __future__ import annotations -import libnacl -import libnacl.encode -import libnacl.public -import libnacl.sign - from ...keyvault.keys import PublicKey +try: + import libnacl + import libnacl.encode + import libnacl.public + import libnacl.sign -class LibNaCLPK(PublicKey): - """ - A LibNaCL implementation of a public key. - """ - - def __init__(self, binarykey: bytes = b"", pk: bytes | None = None, hex_vk: bytes | None = None) -> None: - """ - Create a new LibNaCL public key. Optionally load it from a string representation or - using a public key and verification key. - :param binarykey: load the pk from this string (see key_to_bin()) - :param pk: the libnacl public key to use in byte format - :param hex_vk: a verification key in hex format + class LibNaCLPK(PublicKey): """ - # Load the key, if specified - if binarykey: - pk, vk = (binarykey[:libnacl.crypto_box_SECRETKEYBYTES], - binarykey[libnacl.crypto_box_SECRETKEYBYTES: libnacl.crypto_box_SECRETKEYBYTES - + libnacl.crypto_sign_SEEDBYTES]) - hex_vk = libnacl.encode.hex_encode(vk) - # Construct the public key and verifier objects - self.key = libnacl.public.PublicKey(pk) - self.veri = libnacl.sign.Verifier(hex_vk) - - def verify(self, signature: bytes, msg: bytes) -> bool: + A LibNaCL implementation of a public key. """ - Verify whether a given signature is correct for a message. - :param signature: the given signature - :param msg: the given message - """ - return self.veri.verify(signature + msg) + def __init__(self, binarykey: bytes = b"", pk: bytes | None = None, hex_vk: bytes | None = None) -> None: + """ + Create a new LibNaCL public key. Optionally load it from a string representation or + using a public key and verification key. + + :param binarykey: load the pk from this string (see key_to_bin()) + :param pk: the libnacl public key to use in byte format + :param hex_vk: a verification key in hex format + """ + # Load the key, if specified + if binarykey: + pk, vk = (binarykey[:libnacl.crypto_box_SECRETKEYBYTES], + binarykey[libnacl.crypto_box_SECRETKEYBYTES: libnacl.crypto_box_SECRETKEYBYTES + + libnacl.crypto_sign_SEEDBYTES]) + hex_vk = libnacl.encode.hex_encode(vk) + # Construct the public key and verifier objects + self.key = libnacl.public.PublicKey(pk) + self.veri = libnacl.sign.Verifier(hex_vk) + + def verify(self, signature: bytes, msg: bytes) -> bool: + """ + Verify whether a given signature is correct for a message. + + :param signature: the given signature + :param msg: the given message + """ + return self.veri.verify(signature + msg) + + def key_to_bin(self) -> bytes: + """ + Get the string representation of this key. + """ + return b"LibNaCLPK:" + self.key.pk + self.veri.vk + + def get_signature_length(self) -> int: + """ + Returns the length, in bytes, of each signature made using EC. + """ + return libnacl.crypto_sign_BYTES + +except (ImportError, OSError): + from .cryptography25519 import Cryptography25519PK - def key_to_bin(self) -> bytes: - """ - Get the string representation of this key. - """ - return b"LibNaCLPK:" + self.key.pk + self.veri.vk - def get_signature_length(self) -> int: + class LibNaCLPK(Cryptography25519PK): # type: ignore[no-redef] """ - Returns the length, in bytes, of each signature made using EC. + Fallback class, actually Cryptography25519PK. """ - return libnacl.crypto_sign_BYTES diff --git a/ipv8/test/keyvault/test_25519fallback.py b/ipv8/test/keyvault/test_25519fallback.py new file mode 100644 index 000000000..cc499bb66 --- /dev/null +++ b/ipv8/test/keyvault/test_25519fallback.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from ...keyvault.private.cryptography25519 import Cryptography25519SK +from ...keyvault.private.libnaclkey import LibNaCLSK +from ..base import TestBase + + +class TestFallback(TestBase): + """ + Check if the fallback NaCL implementation does the same as the real thing (albeit slower). + """ + + nacl_key: LibNaCLSK + nacl_copy: LibNaCLSK + crypto_key: Cryptography25519SK + crypto_copy: Cryptography25519SK + + @classmethod + def setUpClass(cls: TestFallback) -> None: + """ + Create a real NaCL key and a cryptography-based copy. + """ + super().setUpClass() + + cls.nacl_key = LibNaCLSK() # Generates a new NaCL key + cls.crypto_copy = Cryptography25519SK(cls.nacl_key.key_to_bin()[10:]) + + cls.crypto_key = Cryptography25519SK() # Generates a new cryptography-based key + cls.nacl_copy = LibNaCLSK(cls.crypto_key.key_to_bin()[10:]) + + def test_sk_to_bin(self) -> None: + """ + Check if the secret key binary formats are interchangeable. + """ + self.assertEqual(self.nacl_key.key_to_bin(), self.crypto_copy.key_to_bin()) + self.assertEqual(self.crypto_key.key_to_bin(), self.nacl_copy.key_to_bin()) + + def test_pk_to_bin(self) -> None: + """ + Check if the public key binary formats are interchangeable. + """ + self.assertEqual(self.nacl_key.pub().key_to_bin(), self.crypto_copy.pub().key_to_bin()) + self.assertEqual(self.crypto_key.pub().key_to_bin(), self.nacl_copy.pub().key_to_bin()) + + def test_sk_to_hash(self) -> None: + """ + Check if the hashes of the secret keys are interchangeable. + """ + self.assertEqual(self.nacl_key.key_to_hash(), self.crypto_copy.key_to_hash()) + self.assertEqual(self.crypto_key.key_to_hash(), self.nacl_copy.key_to_hash()) + + def test_pk_to_hash(self) -> None: + """ + Check if the hashes of the secret keys are interchangeable. + """ + self.assertEqual(self.nacl_key.pub().key_to_hash(), self.crypto_copy.pub().key_to_hash()) + self.assertEqual(self.crypto_key.pub().key_to_hash(), self.nacl_copy.pub().key_to_hash()) + + def test_sign_and_verify_nacl_to_crypto(self) -> None: + """ + Check if NaCL signatures can be verified by the cryptography fallback. + """ + signed = self.nacl_key.signature(b"hello") + self.assertTrue(self.crypto_copy.pub().verify(signed, b"hello")) + + def test_sign_and_verify_crypto_to_nacl(self) -> None: + """ + Check if the cryptography fallback signatures can be verified by NaCL. + """ + signed = self.crypto_key.signature(b"hello") + self.assertTrue(self.nacl_copy.pub().verify(signed, b"hello"))