Files
SmartThings-Local/tests/test_certificate_profiles.py
T

1006 lines
31 KiB
Python

"""Synthetic security and wire-contract tests for certificate profiles."""
from __future__ import annotations
import logging
from dataclasses import asdict
from datetime import UTC, datetime, timedelta
from types import SimpleNamespace
from uuid import UUID
import pytest
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
from OpenSSL import SSL, crypto
import smartthings_local.protocol.auth as auth_module
from smartthings_local.protocol.auth import (
CertificateAuth,
SamsungServerProfile,
SamsungServerRole,
ServerCertificateAuth,
)
_IDENTITY = UUID(bytes=b"\xab" * 16)
_OTHER_IDENTITY = UUID(bytes=b"\xcd" * 16)
def _build_certificate(
*,
subject: x509.Name,
issuer: x509.Name,
public_key,
issuer_key,
serial_number: int,
is_ca: bool | None,
) -> x509.Certificate:
now = datetime.now(UTC)
builder = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(public_key)
.serial_number(serial_number)
.not_valid_before(now - timedelta(minutes=1))
.not_valid_after(now + timedelta(hours=1))
)
if is_ca is not None:
builder = builder.add_extension(
x509.BasicConstraints(ca=is_ca, path_length=None),
critical=True,
).add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=is_ca,
crl_sign=is_ca,
encipher_only=None,
decipher_only=None,
),
critical=True,
)
if is_ca is False:
builder = builder.add_extension(
x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]),
critical=False,
)
return builder.sign(issuer_key, hashes.SHA256())
def _encode_der_length(length: int) -> bytes:
if length < 0x80:
return bytes([length])
encoded = length.to_bytes((length.bit_length() + 7) // 8, "big")
return bytes([0x80 | len(encoded)]) + encoded
def _encode_der_element(tag: int, contents: bytes) -> bytes:
return bytes([tag]) + _encode_der_length(len(contents)) + contents
def _read_der_element(
encoded: bytes,
offset: int,
) -> tuple[int, int, int, int]:
tag = encoded[offset]
first_length_octet = encoded[offset + 1]
if first_length_octet < 0x80:
length_octets = 0
length = first_length_octet
else:
length_octets = first_length_octet & 0x7F
assert 0 < length_octets <= 4
length = int.from_bytes(
encoded[offset + 2 : offset + 2 + length_octets],
"big",
)
contents_start = offset + 2 + length_octets
contents_end = contents_start + length
assert contents_end <= len(encoded)
return tag, contents_start, contents_end, contents_end
def _add_tbs_signature_algorithm_trailing_null(
certificate: x509.Certificate,
issuer_key,
) -> bytes:
"""Make a signed synthetic leaf with Samsung-style algorithm extra data."""
encoded = certificate.public_bytes(serialization.Encoding.DER)
outer_tag, outer_start, _outer_end, certificate_end = _read_der_element(
encoded,
0,
)
assert outer_tag == 0x30
assert certificate_end == len(encoded)
tbs_tag, tbs_start, tbs_end, tbs_next = _read_der_element(
encoded,
outer_start,
)
assert tbs_tag == 0x30
signature_algorithm_offset = tbs_start
for _ in range(2):
_, _, _, signature_algorithm_offset = _read_der_element(
encoded,
signature_algorithm_offset,
)
(
signature_algorithm_tag,
signature_algorithm_start,
signature_algorithm_end,
_,
) = _read_der_element(encoded, signature_algorithm_offset)
assert signature_algorithm_tag == 0x30
malformed_signature_algorithm = _encode_der_element(
0x30,
encoded[signature_algorithm_start:signature_algorithm_end]
+ b"\x05\x00",
)
malformed_tbs = _encode_der_element(
0x30,
encoded[tbs_start:signature_algorithm_offset]
+ malformed_signature_algorithm
+ encoded[signature_algorithm_end:tbs_end],
)
(
outer_signature_tag,
outer_signature_start,
outer_signature_end,
_,
) = _read_der_element(
encoded,
tbs_next,
)
assert outer_signature_tag == 0x30
malformed_outer_signature_algorithm = _encode_der_element(
0x30,
encoded[outer_signature_start:outer_signature_end] + b"\x05\x00",
)
signature = issuer_key.sign(malformed_tbs, ec.ECDSA(hashes.SHA256()))
return _encode_der_element(
0x30,
malformed_tbs
+ malformed_outer_signature_algorithm
+ _encode_der_element(0x03, b"\x00" + signature),
)
def _make_generated_chain(
identity: UUID,
*,
organizational_unit: str = "OCF HA Device",
intermediate_has_constraints: bool = True,
leaf_signature_algorithm_trailing_null: bool = False,
):
"""Create a throwaway three-level chain unrelated to real devices."""
root_key = ec.generate_private_key(ec.SECP256R1())
root_name = x509.Name(
[x509.NameAttribute(NameOID.COMMON_NAME, "Synthetic profile root")]
)
root = _build_certificate(
subject=root_name,
issuer=root_name,
public_key=root_key.public_key(),
issuer_key=root_key,
serial_number=101,
is_ca=True,
)
intermediate_key = ec.generate_private_key(ec.SECP256R1())
intermediate_name = x509.Name(
[
x509.NameAttribute(
NameOID.COMMON_NAME,
"Synthetic profile intermediate",
)
]
)
intermediate = _build_certificate(
subject=intermediate_name,
issuer=root_name,
public_key=intermediate_key.public_key(),
issuer_key=root_key,
serial_number=102,
is_ca=True if intermediate_has_constraints else None,
)
leaf_key = ec.generate_private_key(ec.SECP256R1())
leaf_name = x509.Name(
[
x509.NameAttribute(NameOID.COUNTRY_NAME, "KR"),
x509.NameAttribute(
NameOID.ORGANIZATION_NAME,
"Samsung Electronics",
),
x509.NameAttribute(
NameOID.ORGANIZATIONAL_UNIT_NAME,
organizational_unit,
),
x509.NameAttribute(
NameOID.COMMON_NAME,
f"OCF Device: Test ({identity})",
),
]
)
leaf = _build_certificate(
subject=leaf_name,
issuer=intermediate_name,
public_key=leaf_key.public_key(),
issuer_key=intermediate_key,
serial_number=103,
is_ca=False,
)
root_pem = root.public_bytes(serialization.Encoding.PEM).decode()
intermediate_pem = intermediate.public_bytes(serialization.Encoding.PEM).decode()
if leaf_signature_algorithm_trailing_null:
leaf_der = _add_tbs_signature_algorithm_trailing_null(
leaf,
intermediate_key,
)
openssl_leaf = crypto.load_certificate(crypto.FILETYPE_ASN1, leaf_der)
leaf_pem = crypto.dump_certificate(
crypto.FILETYPE_PEM,
openssl_leaf,
).decode()
else:
leaf_pem = leaf.public_bytes(serialization.Encoding.PEM).decode()
openssl_leaf = crypto.load_certificate(crypto.FILETYPE_PEM, leaf_pem)
key_pem = leaf_key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
).decode()
return SimpleNamespace(
root_pem=root_pem,
certificate_pem=leaf_pem + intermediate_pem,
private_key_pem=key_pem,
leaf=openssl_leaf,
intermediate=crypto.load_certificate(
crypto.FILETYPE_PEM,
intermediate_pem,
),
)
@pytest.fixture(scope="module")
def generated_chain():
return _make_generated_chain(_IDENTITY)
def _configured_context(chain, profile=None, *, server_only=False) -> SSL.Context:
context = SSL.Context(SSL.DTLS_METHOD)
if server_only:
ServerCertificateAuth(server_profile=profile).configure_context(context)
else:
CertificateAuth.from_memory(
chain.certificate_pem,
chain.private_key_pem,
server_profile=profile,
).configure_context(context)
return context
def _configured_certificate_server_context(chain) -> SSL.Context:
context = SSL.Context(SSL.DTLS_METHOD)
certificates = auth_module._PEM_CERT_RE.findall(
chain.certificate_pem.encode()
)
context.use_certificate(
crypto.load_certificate(crypto.FILETYPE_PEM, certificates[0])
)
for certificate in certificates[1:]:
context.add_extra_chain_cert(
crypto.load_certificate(crypto.FILETYPE_PEM, certificate)
)
context.use_privatekey(
crypto.load_privatekey(
crypto.FILETYPE_PEM,
chain.private_key_pem.encode(),
)
)
context.check_privatekey()
# Request a client certificate but permit an empty certificate message,
# matching a server-authenticated manufacturer-certificate OTM carrier.
context.set_verify(SSL.VERIFY_PEER, lambda *_args: True)
return context
def _first_client_hello(context: SSL.Context) -> bytes:
connection = SSL.Connection(context, None)
connection.set_connect_state()
with pytest.raises(SSL.WantReadError):
connection.do_handshake()
chunks = []
while True:
try:
chunks.append(connection.bio_read(65535))
except SSL.WantReadError:
break
assert len(chunks) == 1
return chunks[0]
def _parse_client_hello(datagram: bytes):
"""Return cipher suites and extensions from one DTLS ClientHello."""
assert datagram[0] == 22
record_length = int.from_bytes(datagram[11:13], "big")
handshake = datagram[13 : 13 + record_length]
assert handshake[0] == 1
body = handshake[12:]
offset = 2 + 32
session_id_length = body[offset]
offset += 1 + session_id_length
cookie_length = body[offset]
offset += 1 + cookie_length
cipher_length = int.from_bytes(body[offset : offset + 2], "big")
offset += 2
ciphers = [
int.from_bytes(body[index : index + 2], "big")
for index in range(offset, offset + cipher_length, 2)
]
offset += cipher_length
compression_length = body[offset]
offset += 1 + compression_length
extensions_length = int.from_bytes(body[offset : offset + 2], "big")
offset += 2
extensions_end = offset + extensions_length
extensions = {}
while offset < extensions_end:
extension_type = int.from_bytes(body[offset : offset + 2], "big")
extension_length = int.from_bytes(
body[offset + 2 : offset + 4],
"big",
)
offset += 4
assert extension_type not in extensions
extensions[extension_type] = body[offset : offset + extension_length]
offset += extension_length
assert offset == extensions_end == len(body)
return ciphers, extensions
def _vector_values(extension: bytes) -> list[int]:
vector_length = int.from_bytes(extension[:2], "big")
assert vector_length == len(extension) - 2
return [
int.from_bytes(extension[index : index + 2], "big")
for index in range(2, len(extension), 2)
]
def _drive_memory_bio_handshake(
client_context: SSL.Context,
server_context: SSL.Context,
) -> tuple[SSL.Connection, SSL.Connection]:
client = SSL.Connection(client_context, None)
client.set_connect_state()
client.set_ciphertext_mtu(1200)
server = SSL.Connection(server_context, None)
server.set_accept_state()
server.set_ciphertext_mtu(1200)
client_done = False
server_done = False
for _ in range(20):
if not client_done:
try:
client.do_handshake()
client_done = True
except (SSL.WantReadError, SSL.WantWriteError):
pass
while True:
try:
server.bio_write(client.bio_read(65535))
except SSL.WantReadError:
break
if not server_done:
try:
server.do_handshake()
server_done = True
except (SSL.WantReadError, SSL.WantWriteError):
pass
while True:
try:
client.bio_write(server.bio_read(65535))
except SSL.WantReadError:
break
if client_done and server_done:
return client, server
raise AssertionError("synthetic DTLS handshake did not complete")
def _verify_chain(context: SSL.Context, chain) -> None:
crypto.X509StoreContext(
context.get_cert_store(),
chain.leaf,
[chain.intermediate],
).verify_certificate()
def test_profile_accepts_only_canonical_nonzero_identity():
assert repr(SamsungServerProfile.bound_device(_IDENTITY)) == (
"SamsungServerProfile()"
)
assert repr(SamsungServerProfile.bound_device(str(_IDENTITY))) == (
"SamsungServerProfile()"
)
for invalid in (
UUID(int=0),
str(UUID(int=0)),
str(_IDENTITY).upper(),
"{" + str(_IDENTITY) + "}",
"not-an-identity",
):
with pytest.raises(ValueError, match="canonical non-zero UUID"):
SamsungServerProfile.bound_device(invalid)
for invalid in (True, 1, b"not-an-identity"):
with pytest.raises(TypeError, match="UUID or string"):
SamsungServerProfile.bound_device(invalid)
def test_profile_roles_are_explicit_and_fail_closed():
home_chain = _make_generated_chain(_IDENTITY)
video_chain = _make_generated_chain(
_IDENTITY,
organizational_unit="OCF VD Device",
)
mismatched_video_chain = _make_generated_chain(
_OTHER_IDENTITY,
organizational_unit="OCF VD Device",
)
home_profile = SamsungServerProfile.bound_device(_IDENTITY)
video_profile = SamsungServerProfile.bound_device(
_IDENTITY,
role=SamsungServerRole.VD_DEVICE,
)
assert home_profile._verify_peer(None, home_chain.leaf, 0, 0, True) is True
assert home_profile._verify_peer(None, video_chain.leaf, 0, 0, True) is False
assert video_profile._verify_peer(None, video_chain.leaf, 0, 0, True) is True
assert video_profile._verify_peer(None, home_chain.leaf, 0, 0, True) is False
assert (
video_profile._verify_peer(
None,
mismatched_video_chain.leaf,
0,
0,
True,
)
is False
)
for invalid in ("OCF VD Device", "video_device", None, object()):
with pytest.raises(TypeError, match="SamsungServerRole"):
SamsungServerProfile.bound_device(_IDENTITY, role=invalid)
def test_profile_additional_ca_input_is_bounded_and_parsed(generated_chain):
profile = SamsungServerProfile.bound_device(
_IDENTITY,
additional_ca_pem=generated_chain.root_pem,
)
assert repr(profile) == "SamsungServerProfile()"
with pytest.raises(TypeError, match="must be a string"):
SamsungServerProfile.bound_device(
_IDENTITY,
additional_ca_pem=generated_chain.root_pem.encode(),
)
for invalid in (
"",
generated_chain.root_pem + "unexpected trailing material",
generated_chain.root_pem * 5,
generated_chain.root_pem * 2,
generated_chain.root_pem + (" " * (32 * 1024)),
generated_chain.certificate_pem,
"-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----",
"non-ascii-\N{SNOWMAN}",
):
with pytest.raises(ValueError):
SamsungServerProfile.bound_device(
_IDENTITY,
additional_ca_pem=invalid,
)
def test_profile_is_immutable_and_has_no_public_identity_or_ca_surface(
generated_chain,
):
profile = SamsungServerProfile.bound_device(
_IDENTITY,
additional_ca_pem=generated_chain.root_pem,
)
rendered = repr(profile)
assert str(_IDENTITY) not in rendered
assert "BEGIN CERTIFICATE" not in rendered
assert not hasattr(profile, "expected_certificate_identity")
assert not hasattr(profile, "additional_ca_pem")
with pytest.raises(TypeError):
vars(profile)
with pytest.raises(TypeError):
asdict(profile)
with pytest.raises(AttributeError, match="immutable"):
profile.expected_certificate_identity = _OTHER_IDENTITY
with pytest.raises(AttributeError, match="immutable"):
del profile._expected_certificate_identity
def test_server_certificate_auth_is_explicit_immutable_and_redacted(
generated_chain,
):
profile = SamsungServerProfile.bound_device(
_IDENTITY,
role=SamsungServerRole.VD_DEVICE,
additional_ca_pem=generated_chain.root_pem,
)
provider = ServerCertificateAuth(server_profile=profile)
assert repr(provider) == "ServerCertificateAuth()"
assert str(_IDENTITY) not in repr(provider)
assert generated_chain.root_pem not in repr(provider)
with pytest.raises(TypeError):
vars(provider)
with pytest.raises(TypeError):
asdict(provider)
with pytest.raises(AttributeError, match="immutable"):
provider.server_profile = profile
with pytest.raises(AttributeError, match="immutable"):
del provider._server_profile
for invalid in (None, object()):
with pytest.raises(TypeError, match="SamsungServerProfile"):
ServerCertificateAuth(server_profile=invalid)
def test_certificate_auth_requires_the_exact_profile_type(generated_chain):
with pytest.raises(TypeError, match="SamsungServerProfile"):
CertificateAuth.from_memory(
generated_chain.certificate_pem,
generated_chain.private_key_pem,
server_profile=object(),
)
def test_profile_emits_the_exact_client_hello_and_reuses_cold(
generated_chain,
):
profile = SamsungServerProfile.bound_device(
_IDENTITY,
additional_ca_pem=generated_chain.root_pem,
)
first = _parse_client_hello(
_first_client_hello(_configured_context(generated_chain, profile))
)
second = _parse_client_hello(
_first_client_hello(_configured_context(generated_chain, profile))
)
assert first == second
ciphers, extensions = first
# Older OpenSSL appends the non-negotiable renegotiation SCSV (0x00ff).
# No other negotiable cipher may enter the profile.
assert ciphers[0] == 0xC02B
assert set(ciphers) <= {0xC02B, 0x00FF}
assert _vector_values(extensions[10]) == [23]
assert _vector_values(extensions[13]) == [
0x0401,
0x0403,
0x0201,
0x0203,
]
assert 35 not in extensions
def test_server_only_profile_emits_the_same_client_hello_without_credentials(
generated_chain,
):
profile = SamsungServerProfile.bound_device(
_IDENTITY,
role=SamsungServerRole.VD_DEVICE,
additional_ca_pem=generated_chain.root_pem,
)
certificate_hello = _parse_client_hello(
_first_client_hello(_configured_context(generated_chain, profile))
)
server_only_hello = _parse_client_hello(
_first_client_hello(
_configured_context(
generated_chain,
profile,
server_only=True,
)
)
)
assert server_only_hello == certificate_hello
def test_server_only_profile_completes_without_a_client_certificate():
video_chain = _make_generated_chain(
_IDENTITY,
organizational_unit="OCF VD Device",
)
profile = SamsungServerProfile.bound_device(
_IDENTITY,
role=SamsungServerRole.VD_DEVICE,
additional_ca_pem=video_chain.root_pem,
)
client_context = _configured_context(
video_chain,
profile,
server_only=True,
)
client, server = _drive_memory_bio_handshake(
client_context,
_configured_certificate_server_context(video_chain),
)
assert client.get_peer_certificate() is not None
assert server.get_peer_certificate() is None
def test_profile_verifies_openssl_accepted_non_der_vd_leaf():
video_chain = _make_generated_chain(
_IDENTITY,
organizational_unit="OCF VD Device",
leaf_signature_algorithm_trailing_null=True,
)
try:
video_chain.leaf.to_cryptography()
except ValueError as error:
# cryptography 50 rejects this shape; the dependency floor accepts it.
assert "TbsCertificate" in str(error)
assert "signature_alg" in str(error)
video_profile = SamsungServerProfile.bound_device(
_IDENTITY,
role=SamsungServerRole.VD_DEVICE,
additional_ca_pem=video_chain.root_pem,
)
client_context = _configured_context(
video_chain,
video_profile,
server_only=True,
)
_verify_chain(client_context, video_chain)
client, server = _drive_memory_bio_handshake(
client_context,
_configured_certificate_server_context(video_chain),
)
assert client.get_peer_certificate() is not None
assert server.get_peer_certificate() is None
assert (
SamsungServerProfile.bound_device(
_OTHER_IDENTITY,
role=SamsungServerRole.VD_DEVICE,
)._verify_peer(None, video_chain.leaf, 0, 0, True)
is False
)
assert (
SamsungServerProfile.bound_device(_IDENTITY)._verify_peer(
None,
video_chain.leaf,
0,
0,
True,
)
is False
)
def test_python_floor_curve_fallback_has_the_same_wire_contract(
monkeypatch,
generated_chain,
):
signature_setter = auth_module._util.lib.SSL_CTX_set1_sigalgs_list
monkeypatch.setattr(
auth_module,
"_util",
SimpleNamespace(
lib=SimpleNamespace(
SSL_CTX_set1_sigalgs_list=signature_setter,
)
),
)
profile = SamsungServerProfile.bound_device(
_IDENTITY,
additional_ca_pem=generated_chain.root_pem,
)
ciphers, extensions = _parse_client_hello(
_first_client_hello(_configured_context(generated_chain, profile))
)
assert ciphers[0] == 0xC02B
assert set(ciphers) <= {0xC02B, 0x00FF}
assert _vector_values(extensions[10]) == [23]
assert _vector_values(extensions[13]) == [
0x0401,
0x0403,
0x0201,
0x0203,
]
assert 35 not in extensions
def test_profile_fails_closed_when_exact_openssl_support_is_unavailable(
monkeypatch,
):
class FallbackContext:
_context = object()
def set_tmp_ecdh(self, _curve):
return None
monkeypatch.setattr(
auth_module,
"_util",
SimpleNamespace(lib=SimpleNamespace()),
)
profile = SamsungServerProfile.bound_device(_IDENTITY)
with pytest.raises(RuntimeError, match="rejected"):
profile._configure_context(FallbackContext())
def test_default_and_profile_verification_are_selected_per_provider(
monkeypatch,
):
valid_chain = _make_generated_chain(_IDENTITY)
mismatch_chain = _make_generated_chain(_OTHER_IDENTITY)
class RecordingContext:
def __init__(self):
self._context = object()
self.calls = []
self.verify_callback = None
def load_verify_locations(self, path):
self.calls.append(("load_verify_locations", path))
def set_verify(self, mode, callback):
self.calls.append(("set_verify", mode))
self.verify_callback = callback
def set_cipher_list(self, ciphers):
self.calls.append(("set_cipher_list", ciphers))
def use_certificate_chain_file(self, path):
self.calls.append(("use_certificate_chain_file", path))
def use_privatekey_file(self, path):
self.calls.append(("use_privatekey_file", path))
def check_privatekey(self):
self.calls.append(("check_privatekey",))
def set_options(self, options):
self.calls.append(("set_options", options))
profile_calls = []
monkeypatch.setattr(
auth_module,
"_util",
SimpleNamespace(
lib=SimpleNamespace(
SSL_CTX_set1_curves_list=(
lambda handle, value: (
profile_calls.append(("curves", handle, value)) or 1
)
),
SSL_CTX_set1_sigalgs_list=(
lambda handle, value: (
profile_calls.append(("signature_algorithms", handle, value))
or 1
)
),
)
),
)
default_context = RecordingContext()
CertificateAuth.from_files("/synthetic/cert", "/synthetic/key").configure_context(
default_context
)
assert default_context.verify_callback(None, None, 0, 0, True) is True
assert default_context.verify_callback(None, None, 0, 0, False) is False
assert not profile_calls
assert all(call[0] != "set_options" for call in default_context.calls)
profiled_context = RecordingContext()
profile = SamsungServerProfile.bound_device(_IDENTITY)
CertificateAuth.from_files(
"/synthetic/cert",
"/synthetic/key",
server_profile=profile,
).configure_context(profiled_context)
assert [call[0] for call in profile_calls] == [
"curves",
"signature_algorithms",
]
assert ("set_options", SSL.OP_NO_TICKET) in profiled_context.calls
callback = profiled_context.verify_callback
assert callback(None, valid_chain.leaf, 0, 0, True) is True
assert callback(None, mismatch_chain.leaf, 0, 0, True) is False
assert callback(None, valid_chain.leaf, 0, 0, False) is False
profile_calls.clear()
video_chain = _make_generated_chain(
_IDENTITY,
organizational_unit="OCF VD Device",
)
server_only_context = RecordingContext()
video_profile = SamsungServerProfile.bound_device(
_IDENTITY,
role=SamsungServerRole.VD_DEVICE,
)
ServerCertificateAuth(server_profile=video_profile).configure_context(
server_only_context
)
assert [call[0] for call in profile_calls] == [
"curves",
"signature_algorithms",
]
assert all(
call[0]
not in {
"use_certificate_chain_file",
"use_privatekey_file",
"check_privatekey",
}
for call in server_only_context.calls
)
assert (
server_only_context.verify_callback(
None,
video_chain.leaf,
0,
0,
True,
)
is True
)
assert (
server_only_context.verify_callback(
None,
valid_chain.leaf,
0,
0,
True,
)
is False
)
def test_profile_identity_verification_rejects_malformed_subjects(caplog):
profile = SamsungServerProfile.bound_device(_IDENTITY)
wrong_role = _make_generated_chain(
_IDENTITY,
organizational_unit="Unexpected Device",
)
assert profile._verify_peer(None, wrong_role.leaf, 0, 0, True) is False
assert profile._verify_peer(None, None, 0, 0, True) is False
assert profile._verify_peer(None, wrong_role.leaf, 0, -1, True) is False
assert profile._verify_peer(None, object(), 0, 1, True) is True
duplicate_common_name = SimpleNamespace(
get_subject=lambda: SimpleNamespace(
get_components=lambda: [
(b"CN", f"OCF Device: First ({_IDENTITY})".encode()),
(b"CN", f"OCF Device: Other ({_IDENTITY})".encode()),
(b"OU", b"OCF HA Device"),
(b"O", b"Samsung Electronics"),
(b"C", b"KR"),
]
)
)
assert (
profile._verify_peer(
None,
duplicate_common_name,
0,
0,
True,
)
is False
)
invalid_encoding = SimpleNamespace(
get_subject=lambda: SimpleNamespace(
get_components=lambda: [(b"CN", b"\xff")]
)
)
caplog.clear()
with caplog.at_level(logging.WARNING, logger=auth_module.__name__):
assert profile._verify_peer(None, object(), 0, 0, True) is False
assert (
profile._verify_peer(
None,
invalid_encoding,
0,
0,
True,
)
is False
)
assert caplog.messages == [
"Unable to parse Samsung server certificate subject",
"Unable to parse Samsung server certificate subject",
]
caplog.clear()
assert profile._verify_peer(None, wrong_role.leaf, 0, 0, True) is False
assert not caplog.records
def test_additional_ca_is_scoped_and_invalid_intermediate_is_rejected(
generated_chain,
):
default_context = _configured_context(generated_chain)
with pytest.raises(crypto.X509StoreContextError):
_verify_chain(default_context, generated_chain)
profile = SamsungServerProfile.bound_device(
_IDENTITY,
additional_ca_pem=generated_chain.root_pem,
)
profiled_context = _configured_context(generated_chain, profile)
_verify_chain(profiled_context, generated_chain)
missing_constraints = _make_generated_chain(
_IDENTITY,
intermediate_has_constraints=False,
)
missing_constraints_profile = SamsungServerProfile.bound_device(
_IDENTITY,
additional_ca_pem=missing_constraints.root_pem,
)
missing_constraints_context = _configured_context(
missing_constraints,
missing_constraints_profile,
)
with pytest.raises(crypto.X509StoreContextError):
_verify_chain(missing_constraints_context, missing_constraints)
def test_profile_errors_and_provider_repr_do_not_echo_inputs(generated_chain):
marker = "private" + "-profile-marker"
with pytest.raises(ValueError) as captured:
SamsungServerProfile.bound_device(marker)
assert marker not in str(captured.value)
profile = SamsungServerProfile.bound_device(
_IDENTITY,
additional_ca_pem=generated_chain.root_pem,
)
provider = CertificateAuth.from_memory(
generated_chain.certificate_pem,
generated_chain.private_key_pem,
server_profile=profile,
)
rendered = repr(provider) + repr(profile)
assert str(_IDENTITY) not in rendered
assert generated_chain.root_pem not in rendered
assert generated_chain.private_key_pem not in rendered