PySecretHandshake/tests/test_crypto.py

208 lines
8.5 KiB
Python
Raw Normal View History

# SPDX-License-Identifier: MIT
#
2017-06-04 19:50:49 +00:00
# Copyright (c) 2017 PySecretHandshake contributors (see AUTHORS for more details)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
2023-10-29 08:55:39 +00:00
"""Tests for the crypto components"""
2017-06-04 19:50:49 +00:00
import hashlib
2023-11-06 04:10:56 +00:00
from typing import Literal
2017-06-04 19:50:49 +00:00
2023-11-06 04:10:56 +00:00
from nacl.exceptions import CryptoError
2017-06-04 19:50:49 +00:00
from nacl.public import PrivateKey
from nacl.signing import SigningKey
2023-10-29 09:47:21 +00:00
import pytest
2023-11-06 04:10:56 +00:00
from pytest_mock import MockerFixture
2017-06-04 19:50:49 +00:00
2023-11-06 04:10:56 +00:00
from secret_handshake.crypto import SHSClientCrypto, SHSError, SHSServerCrypto
2017-06-04 19:50:49 +00:00
APP_KEY = hashlib.sha256(b"app_key").digest()
SERVER_KEY_SEED = b"\xcaw\x01\xc2cQ\xfd\x94\x9f\x14\x84\x0c0<l\xd8\xe4\xf5>\x12\\\x96\xcd\x9b\x0c\x02z&\x96!\xe0\xa2"
CLIENT_KEY_SEED = b"\xbf\x02<\xd3e\x9d\xac-\xd1\x9e-{\xe5q\x90\x03\x11\xba\x8cSQ\xa0\xc3p~\x89\xe6\xeeb\xaa\x1c\x17"
2017-06-04 19:50:49 +00:00
SERVER_EPH_KEY_SEED = b"ed\x1c\x01\x03s\x04\xdc\x8e`\xd6Z\xd0u;\xcbX\x91\xd8ZO\xf8\xf0\xd6'\xd5\xb1Yy\x13yH"
CLIENT_EPH_KEY_SEED = b"u8\xd0\xe3\x85d_Pz\x0c\xf5\xfd\x15\xce2p#\xb0\xf0\x9f\xe6!\xe1\xcb\xf6\x93\t\xebr{1\x8b"
2017-06-04 19:50:49 +00:00
2023-10-29 08:55:39 +00:00
@pytest.fixture
def server() -> SHSServerCrypto:
2023-10-29 08:55:39 +00:00
"""A testing SHS server"""
2017-06-04 19:50:49 +00:00
server_key = SigningKey(SERVER_KEY_SEED)
server_eph_key = PrivateKey(SERVER_EPH_KEY_SEED)
2023-10-29 08:55:39 +00:00
2017-06-04 19:50:49 +00:00
return SHSServerCrypto(server_key, server_eph_key, application_key=APP_KEY)
2023-10-29 08:55:39 +00:00
@pytest.fixture
2023-11-06 04:10:56 +00:00
def client(request: pytest.FixtureRequest) -> SHSClientCrypto:
2023-10-29 08:55:39 +00:00
"""A testing SHS client"""
2023-11-06 04:10:56 +00:00
app_key = None
for marker in request.node.iter_markers(name="client_app_key"):
app_key = marker.args[0]
if app_key is None:
app_key = APP_KEY
2017-06-04 19:50:49 +00:00
client_key = SigningKey(CLIENT_KEY_SEED)
server_key = SigningKey(SERVER_KEY_SEED)
client_eph_key = PrivateKey(CLIENT_EPH_KEY_SEED)
2023-10-29 08:55:39 +00:00
2023-11-06 04:10:56 +00:00
return SHSClientCrypto(client_key, bytes(server_key.verify_key), client_eph_key, application_key=app_key)
2017-06-04 19:50:49 +00:00
CLIENT_CHALLENGE = (
b"d\xe8\xccD\xec\xb9E\xbb\xaa\xa7\x7f\xe38\x15\x16\xef\xca\xd22u\x1d\xfe<\xe7j"
b"\xd7\xf0uc\xf0r\xf3\x7f\t\x18\xec\x8c\xf7\xff\x8e\xa9\xc83\x13\x18R\x16\x1d"
b"\xe5\xc6K\xae\x94\xdbVt\x84\xdc\x1c@+D\x1c%"
)
CLIENT_AUTH = (
2023-10-29 08:55:39 +00:00
b"\xf2\xaf?z\x15\x10\xd0\xf0\xdf\xe3\x91\xfe\x14\x1c}z\xab\xeey\xf5\xef\xfc\xa1EdV\xf2T\x95s[!$"
b"z\xeb\x8f\x1b\x96JP\x17^\x92\xc8\x9e\xb4*5`\xf2\x8fI.\x93\xb9\x14:\xca@\x06\xff\xd1\xf1J\xc8t"
b"\xc4\xd8\xc3$[\xc5\x94je\x83\x00%\x99\x10\x16\xb1\xa2\xb2\xb7\xbf\xc9\x88\x14\xb9\xbb^\tzq"
b"\xa4\xef\xc5\xf5\x1f7#\xed\x92X\xb2\xe3\xe5\x8b[t3"
)
SERVER_CHALLENGE = (
2023-10-29 08:55:39 +00:00
b"S\\\x06\x8d\xe5\xeb&*\xb8\x0bp\xb3Z\x8e\\\x85\x14\xaa\x1c\x8di\x9d\x7f\xa9\xeawl\xb9}\x85\xc3"
b"ik\x0c ($E\xb4\x8ax\xc4)t<\xd7\x8b\xd6\x07\xb7\xecw\x84\r\xe1-Iz`\xeb\x04\x89\xd6{"
)
SERVER_ACCEPT = (
2023-10-29 08:55:39 +00:00
b'\xb4\xd0\xea\xfb\xfb\xf6s\xcc\x10\xc4\x99\x95"\x13 y\xa6\xea.G\xeed\x8d=t9\x88|\x94\xd1\xbcK'
b"\xd47\xd8\xbcG1h\xac\xd0\xeb*\x1f\x8d\xae\x0b\x91G\xa1\xe6\x96b\xf2\xda90u\xeb_\xab\xdb\xcb%d"
b"7}\xb5\xce(k\x15\xe3L\x9d)\xd5\xa1|:"
)
INTER_SHARED_SECRET = (
b"vf\xd82\xaeU\xda]\x08\x9eZ\xd6\x06\xcc\xd3\x99\xfd\xce\xc5\x16e8n\x9a\x04\x04\x84\xc5\x1a" b"\x8f\xf2M"
)
BOX_SECRET = b"\x03\xfe\xe3\x8c u\xbcl^\x17eD\x96\xa3\xa6\x880f\x11\x7f\x85\xf2:\xa3[`\x06[#l\xbcr"
SHARED_SECRET = b"UV\xad*\x8e\xce\x88\xf2\x87l\x13iZ\x12\xd7\xa6\xd1\x9c-\x9d\x07\xf5\xa96\x03w\x11\xe5\x96$m\x1d"
CLIENT_ENCRYPT_KEY = (
b"\xec\x1f,\x82\x9f\xedA\xc0\xda\x87[\xf9u\xbf\xac\x9cI\xa5T\xd1\x91\xff\xa8.\xd0 \xfbU\xc7\x14" b")\xc7"
)
CLIENT_DECRYPT_KEY = b"\xf9e\xa0As\xb2=\xb7P~\xf3\xf9(\xfd\x7f\xfe\xb7TZhn\xd7\x8c=\xea.o\x9e\x8c9)\x10"
CLIENT_ENCRYPT_NONCE = b"S\\\x06\x8d\xe5\xeb&*\xb8\x0bp\xb3Z\x8e\\\x85\x14\xaa\x1c\x8di\x9d\x7f\xa9"
CLIENT_DECRYPT_NONCE = b"d\xe8\xccD\xec\xb9E\xbb\xaa\xa7\x7f\xe38\x15\x16\xef\xca\xd22u\x1d\xfe<\xe7"
2017-06-04 19:50:49 +00:00
def test_handshake(client: SHSClientCrypto, server: SHSServerCrypto) -> None: # pylint: disable=redefined-outer-name
2023-10-29 08:55:39 +00:00
"""Test the handshake procedure"""
2017-06-04 19:50:49 +00:00
client_challenge = client.generate_challenge()
assert client_challenge == CLIENT_CHALLENGE
assert server.verify_challenge(client_challenge)
server_challenge = server.generate_challenge()
assert server_challenge == SERVER_CHALLENGE
assert client.verify_server_challenge(server_challenge)
assert client.shared_secret == INTER_SHARED_SECRET
client_auth = client.generate_client_auth()
assert client_auth == CLIENT_AUTH
assert server.verify_client_auth(client_auth)
assert server.shared_secret == client.shared_secret
server_accept = server.generate_accept()
assert server_accept == SERVER_ACCEPT
assert client.verify_server_accept(server_accept)
assert client.box_secret == BOX_SECRET
assert client.box_secret == server.box_secret
client_keys = client.get_box_keys()
server_keys = server.get_box_keys()
assert client_keys["shared_secret"] == SHARED_SECRET
assert client_keys["encrypt_key"] == CLIENT_ENCRYPT_KEY
assert client_keys["decrypt_key"] == CLIENT_DECRYPT_KEY
assert client_keys["encrypt_nonce"] == CLIENT_ENCRYPT_NONCE
assert client_keys["decrypt_nonce"] == CLIENT_DECRYPT_NONCE
2017-06-04 19:50:49 +00:00
assert client_keys["shared_secret"] == server_keys["shared_secret"]
assert client_keys["encrypt_key"] == server_keys["decrypt_key"]
assert client_keys["encrypt_nonce"] == server_keys["decrypt_nonce"]
2023-11-06 04:10:56 +00:00
@pytest.mark.client_app_key(b"a" * 32)
def test_verify_challenge_different_app_keys(
client: SHSClientCrypto, server: SHSServerCrypto # pylint: disable=redefined-outer-name
) -> None:
"""Test challenge verification when the application keys of the client and server dont match"""
challenge = client.generate_challenge()
assert not server.verify_challenge(challenge)
def test_verify_challenge(
client: SHSClientCrypto, server: SHSServerCrypto # pylint: disable=redefined-outer-name
) -> None:
"""Test challenge verification when the application keys of the client and server match"""
challenge = client.generate_challenge()
assert server.verify_challenge(challenge)
@pytest.mark.parametrize("type_", ("server", "client"))
@pytest.mark.parametrize("provide_key", (True, False))
def test_clean(
type_: Literal["client", "server"], provide_key: bool, request: pytest.FixtureRequest, mocker: MockerFixture
) -> None:
"""Test the clean() method"""
if type_ == "server":
actor = request.getfixturevalue("server")
elif type_ == "client": # pragma: no branch
actor = request.getfixturevalue("client")
mocked_private_key = mocker.patch("secret_handshake.crypto.PrivateKey")
mocked_private_key.generate = mocker.MagicMock(return_value=PrivateKey(b"g" * 32))
new_key = PrivateKey(b"p" * 32) if provide_key else None
actor.clean(new_ephemeral_key=new_key)
assert actor.shared_secret is None
assert actor.shared_hash is None
assert actor.remote_ephemeral_key is None
assert isinstance(actor.local_ephemeral_key, PrivateKey)
if provide_key:
assert actor.local_ephemeral_key == new_key
else:
assert actor.local_ephemeral_key.encode() == b"g" * 32
def test_failing_server_accept(
client: SHSClientCrypto, server: SHSServerCrypto, mocker: MockerFixture # pylint: disable=redefined-outer-name
) -> None:
"""Test if verify_server_accept raises the correct exception type"""
server.verify_challenge(client.generate_challenge())
client.verify_server_challenge(server.generate_challenge())
server.verify_client_auth(client.generate_client_auth())
server_accept = server.generate_accept()
mocker.patch("secret_handshake.crypto.crypto_box_open_afternm", side_effect=CryptoError())
with pytest.raises(SHSError):
client.verify_server_accept(server_accept)