Zion Boggan
repos/Oversight/tests/test_hw_p256.py
zionboggan.com ↗
122 lines · python
History for this file →
1
"""Unit tests for the OSGT-HW-P256-v1 (hardware-backed P-256) suite in
2
oversight_core.crypto. The tests exercise the pure-Python wrap/unwrap path
3
that mirrors oversight-rust's seal_hw_p256 + unwrap_dek_with_provider_p256;
4
cross-language conformance against the Rust reference is layered in a
5
separate harness.
6
"""
7
from __future__ import annotations
8
 
9
import os
10
import pytest
11
 
12
from cryptography.hazmat.primitives import serialization
13
from cryptography.hazmat.primitives.asymmetric import ec
14
 
15
from oversight_core import crypto, container
16
 
17
 
18
def _gen_p256_pair() -> tuple[ec.EllipticCurvePrivateKey, bytes]:
19
    """Generate a P-256 keypair, returning (priv, pub_sec1_uncompressed)."""
20
    sk = ec.generate_private_key(ec.SECP256R1())
21
    pub_sec1 = sk.public_key().public_bytes(
22
        encoding=serialization.Encoding.X962,
23
        format=serialization.PublicFormat.UncompressedPoint,
24
    )
25
    return sk, pub_sec1
26
 
27
 
28
def test_constants_match_rust_reference():
29
    assert crypto.SUITE_HW_P256_V1 == "OSGT-HW-P256-v1"
30
    assert crypto.P256_PUBLIC_KEY_LEN == 65
31
    assert container.SUITE_HW_P256_V1_ID == 3
32
    assert container.SUITE_ID_TO_NAME[3] == "OSGT-HW-P256-v1"
33
 
34
 
35
def test_wrap_unwrap_round_trip_with_private_key_object():
36
    sk, pub = _gen_p256_pair()
37
    dek = os.urandom(32)
38
    wrapped = crypto.wrap_dek_for_recipient_p256(dek, pub)
39
    recovered = crypto.unwrap_dek_p256(wrapped, sk)
40
    assert recovered == dek
41
 
42
 
43
def test_wrap_unwrap_round_trip_with_pkcs8_bytes():
44
    sk, pub = _gen_p256_pair()
45
    pkcs8 = sk.private_bytes(
46
        encoding=serialization.Encoding.DER,
47
        format=serialization.PrivateFormat.PKCS8,
48
        encryption_algorithm=serialization.NoEncryption(),
49
    )
50
    dek = os.urandom(32)
51
    wrapped = crypto.wrap_dek_for_recipient_p256(dek, pub)
52
    recovered = crypto.unwrap_dek_p256(wrapped, pkcs8)
53
    assert recovered == dek
54
 
55
 
56
def test_wrap_unwrap_round_trip_with_raw_int_scalar():
57
    sk, pub = _gen_p256_pair()
58
    scalar = sk.private_numbers().private_value
59
    dek = os.urandom(32)
60
    wrapped = crypto.wrap_dek_for_recipient_p256(dek, pub)
61
    recovered = crypto.unwrap_dek_p256(wrapped, scalar)
62
    assert recovered == dek
63
 
64
 
65
def test_envelope_shape_matches_spec():
66
    sk, pub = _gen_p256_pair()
67
    wrapped = crypto.wrap_dek_for_recipient_p256(os.urandom(32), pub)
68
    assert set(wrapped.keys()) == {"suite", "ephemeral_pub", "nonce", "wrapped_dek"}
69
    assert wrapped["suite"] == "OSGT-HW-P256-v1"
70
    eph_pub = bytes.fromhex(wrapped["ephemeral_pub"])
71
    assert len(eph_pub) == 65, "P-256 ephemeral pub MUST be 65 bytes (SEC1 uncompressed)"
72
    assert eph_pub[0] == 0x04, "SEC1 uncompressed encoding starts with 0x04"
73
    assert len(bytes.fromhex(wrapped["nonce"])) == 24, "XChaCha20 nonce MUST be 24 bytes"
74
 
75
 
76
def test_wrong_recipient_rejected():
77
    alice_sk, alice_pub = _gen_p256_pair()
78
    bob_sk, _ = _gen_p256_pair()
79
    dek = os.urandom(32)
80
    wrapped = crypto.wrap_dek_for_recipient_p256(dek, alice_pub)
81
    with pytest.raises(Exception):
82
        crypto.unwrap_dek_p256(wrapped, bob_sk)
83
 
84
 
85
def test_wrap_rejects_wrong_pub_length():
86
    with pytest.raises(ValueError, match="65 bytes"):
87
        crypto.wrap_dek_for_recipient_p256(os.urandom(32), b"\x04" + b"\x00" * 31)
88
 
89
 
90
def test_unwrap_rejects_wrong_ephemeral_length():
91
    sk, pub = _gen_p256_pair()
92
    wrapped = crypto.wrap_dek_for_recipient_p256(os.urandom(32), pub)
93
    wrapped["ephemeral_pub"] = wrapped["ephemeral_pub"][:64]
94
    with pytest.raises(ValueError, match="65 bytes"):
95
        crypto.unwrap_dek_p256(wrapped, sk)
96
 
97
 
98
def test_unwrap_rejects_missing_fields():
99
    sk, _ = _gen_p256_pair()
100
    incomplete = {"suite": "OSGT-HW-P256-v1", "nonce": "00" * 24, "wrapped_dek": "deadbeef"}
101
    with pytest.raises(ValueError, match="ephemeral_pub"):
102
        crypto.unwrap_dek_p256(incomplete, sk)
103
 
104
 
105
def test_aad_binding_classic_envelope_does_not_unwrap():
106
    """
107
    Sanity check that a classic-suite wrapped_dek (X25519, 32-byte
108
    ephemeral, info=oversight-v1-dek-wrap, AAD=oversight-dek) does not
109
    accidentally decrypt through the P-256 path even if you bend the
110
    field shapes. The two suites use different HKDF info strings and
111
    different AEAD AAD values; either of those diverging is enough to
112
    make AEAD authentication fail.
113
    """
114
    sk, pub = _gen_p256_pair()
115
    wrapped = crypto.wrap_dek_for_recipient_p256(os.urandom(32), pub)
116
    bogus_aead_nonce, bogus_wrapped = crypto.aead_encrypt(
117
        b"\x00" * 32, b"would-be DEK", aad=b"oversight-dek"
118
    )
119
    wrapped["nonce"] = bogus_aead_nonce.hex()
120
    wrapped["wrapped_dek"] = bogus_wrapped.hex()
121
    with pytest.raises(Exception):
122
        crypto.unwrap_dek_p256(wrapped, sk)