Zion Boggan
repos/Oversight/tools/gen_hw_p256_sample.py
zionboggan.com ↗
209 lines · python
History for this file →
1
"""Generate an OSGT-HW-P256-v1 .sealed sample + matching identity JSON.
2
 
3
Mirrors `tools/gen_hybrid_sample.py`. Self-contained: depends only on
4
`cryptography` (no `oqs` needed because no PQ). Writes a sample that the
5
viewer's `decryptSealedHwP256` (and `oversight-rust`'s
6
`open_sealed_with_provider`) can both consume.
7
 
8
Usage:
9
    python3 gen_hw_p256_sample.py --out-dir ./out
10
 
11
Outputs:
12
    out/tutorial-hw-p256.sealed          - viewer test fixture
13
    out/tutorial-hw-p256-identity.json   - recipient P-256 priv/pub
14
"""
15
from __future__ import annotations
16
 
17
import argparse
18
import hashlib
19
import json
20
import os
21
import struct
22
import sys
23
from pathlib import Path
24
 
25
from cryptography.hazmat.primitives import hashes, serialization
26
from cryptography.hazmat.primitives.asymmetric import ec
27
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
28
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
29
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
30
 
31
MAGIC = b"OSGT\x01\x00"
32
FORMAT_VERSION = 1
33
SUITE_HW_P256_V1_ID = 3
34
SUITE_HW_P256_V1 = "OSGT-HW-P256-v1"
35
P256_PUBLIC_KEY_LEN = 65
36
 
37
 
38
def _hchacha20(key: bytes, nonce16: bytes) -> bytes:
39
    assert len(key) == 32 and len(nonce16) == 16
40
    state = bytearray(64)
41
    state[0:4]  = b"expa"; state[4:8]  = b"nd 3"; state[8:12]  = b"2-by"; state[12:16] = b"te k"
42
    state[16:48] = key
43
    state[48:64] = nonce16
44
    s = list(struct.unpack("<16I", bytes(state)))
45
 
46
    def rotl(v, n): return ((v << n) & 0xFFFFFFFF) | (v >> (32 - n))
47
    def qr(a, b, c, d):
48
        s[a] = (s[a] + s[b]) & 0xFFFFFFFF; s[d] = rotl(s[d] ^ s[a], 16)
49
        s[c] = (s[c] + s[d]) & 0xFFFFFFFF; s[b] = rotl(s[b] ^ s[c], 12)
50
        s[a] = (s[a] + s[b]) & 0xFFFFFFFF; s[d] = rotl(s[d] ^ s[a], 8)
51
        s[c] = (s[c] + s[d]) & 0xFFFFFFFF; s[b] = rotl(s[b] ^ s[c], 7)
52
 
53
    for _ in range(10):
54
        qr(0, 4,  8, 12); qr(1, 5,  9, 13); qr(2, 6, 10, 14); qr(3, 7, 11, 15)
55
        qr(0, 5, 10, 15); qr(1, 6, 11, 12); qr(2, 7,  8, 13); qr(3, 4,  9, 14)
56
    return struct.pack("<8I", s[0], s[1], s[2], s[3], s[12], s[13], s[14], s[15])
57
 
58
 
59
def xchacha20poly1305_encrypt(key: bytes, nonce24: bytes, plaintext: bytes, aad: bytes) -> bytes:
60
    if len(key) != 32 or len(nonce24) != 24:
61
        raise ValueError("xchacha20poly1305 requires 32-byte key and 24-byte nonce")
62
    subkey = _hchacha20(key, nonce24[:16])
63
    nonce12 = b"\x00\x00\x00\x00" + nonce24[16:24]
64
    return ChaCha20Poly1305(subkey).encrypt(nonce12, plaintext, aad)
65
 
66
 
67
def canonical_bytes(obj: dict) -> bytes:
68
    return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
69
 
70
 
71
def strip_none(obj):
72
    if isinstance(obj, dict):
73
        return {k: strip_none(v) for k, v in obj.items() if v is not None}
74
    if isinstance(obj, list):
75
        return [strip_none(v) for v in obj if v is not None]
76
    return obj
77
 
78
 
79
def hw_p256_wrap_dek(dek: bytes, recipient_p256_pub_sec1: bytes) -> dict:
80
    if len(recipient_p256_pub_sec1) != P256_PUBLIC_KEY_LEN:
81
        raise ValueError(f"recipient pubkey must be {P256_PUBLIC_KEY_LEN} bytes")
82
 
83
    peer = ec.EllipticCurvePublicKey.from_encoded_point(
84
        ec.SECP256R1(), recipient_p256_pub_sec1
85
    )
86
    eph = ec.generate_private_key(ec.SECP256R1())
87
    shared = eph.exchange(ec.ECDH(), peer)
88
 
89
    kek = HKDF(
90
        algorithm=hashes.SHA256(), length=32, salt=None,
91
        info=b"oversight-hw-p256-v1-dek-wrap",
92
    ).derive(shared)
93
 
94
    nonce = os.urandom(24)
95
    wrapped = xchacha20poly1305_encrypt(kek, nonce, dek, aad=b"oversight-hw-p256-dek")
96
 
97
    eph_pub_bytes = eph.public_key().public_bytes(
98
        encoding=serialization.Encoding.X962,
99
        format=serialization.PublicFormat.UncompressedPoint,
100
    )
101
    assert len(eph_pub_bytes) == P256_PUBLIC_KEY_LEN
102
 
103
    return {
104
        "suite": SUITE_HW_P256_V1,
105
        "ephemeral_pub": eph_pub_bytes.hex(),
106
        "nonce": nonce.hex(),
107
        "wrapped_dek": wrapped.hex(),
108
    }
109
 
110
 
111
def main() -> int:
112
    p = argparse.ArgumentParser()
113
    p.add_argument("--out-dir", required=True, type=Path)
114
    p.add_argument("--message", default="hello hardware-keys oversight\n")
115
    args = p.parse_args()
116
    args.out_dir.mkdir(parents=True, exist_ok=True)
117
 
118
    rx_priv = ec.generate_private_key(ec.SECP256R1())
119
    rx_pub_sec1 = rx_priv.public_key().public_bytes(
120
        encoding=serialization.Encoding.X962,
121
        format=serialization.PublicFormat.UncompressedPoint,
122
    )
123
    rx_priv_pkcs8 = rx_priv.private_bytes(
124
        encoding=serialization.Encoding.DER,
125
        format=serialization.PrivateFormat.PKCS8,
126
        encryption_algorithm=serialization.NoEncryption(),
127
    )
128
    rx_priv_scalar = rx_priv.private_numbers().private_value.to_bytes(32, "big")
129
 
130
    issuer = Ed25519PrivateKey.generate()
131
    issuer_pub_bytes = issuer.public_key().public_bytes(
132
        encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw,
133
    )
134
 
135
    plaintext = args.message.encode("utf-8")
136
    content_hash = hashlib.sha256(plaintext).hexdigest()
137
    dek = os.urandom(32)
138
 
139
    aead_nonce = os.urandom(24)
140
    ciphertext = xchacha20poly1305_encrypt(
141
        dek, aead_nonce, plaintext, aad=content_hash.encode("ascii")
142
    )
143
 
144
    wrapped_dek = hw_p256_wrap_dek(dek, rx_pub_sec1)
145
 
146
    manifest = {
147
        "suite": SUITE_HW_P256_V1,
148
        "format": "oversight/v1",
149
        "issuer_id": "tutorial-hw-p256@oversightprotocol.dev",
150
        "issuer_ed25519_pub": issuer_pub_bytes.hex(),
151
        "issuer_ml_dsa_pub": "",
152
        "recipient": {
153
            "id": "tutorial@oversightprotocol.dev",
154
            "x25519_pub": "",
155
            "p256_pub": rx_pub_sec1.hex(),
156
        },
157
        "content_type": "text/plain",
158
        "content_hash": content_hash,
159
        "canonical_content_hash": content_hash,
160
        "l3_policy": {"enabled": False, "mode": "off"},
161
        "filename": "hello-hw-p256.txt",
162
        "signature_ed25519": "",
163
        "signature_ml_dsa": "",
164
    }
165
 
166
    manifest_for_sign = strip_none(manifest)
167
    manifest_for_sign["signature_ed25519"] = ""
168
    manifest_for_sign["signature_ml_dsa"] = ""
169
    sig_bytes = issuer.sign(canonical_bytes(manifest_for_sign))
170
    manifest["signature_ed25519"] = sig_bytes.hex()
171
 
172
    manifest_serialized = canonical_bytes(strip_none(manifest))
173
    wrapped_dek_serialized = canonical_bytes(wrapped_dek)
174
 
175
    container = bytearray()
176
    container.extend(MAGIC)
177
    container.extend(bytes([FORMAT_VERSION, SUITE_HW_P256_V1_ID]))
178
    container.extend(struct.pack(">I", len(manifest_serialized)))
179
    container.extend(manifest_serialized)
180
    container.extend(struct.pack(">I", len(wrapped_dek_serialized)))
181
    container.extend(wrapped_dek_serialized)
182
    container.extend(aead_nonce)
183
    container.extend(struct.pack(">I", len(ciphertext)))
184
    container.extend(ciphertext)
185
 
186
    sealed_path = args.out_dir / "tutorial-hw-p256.sealed"
187
    identity_path = args.out_dir / "tutorial-hw-p256-identity.json"
188
 
189
    sealed_path.write_bytes(bytes(container))
190
    identity = {
191
        "recipient_id": "tutorial@oversightprotocol.dev",
192
        "p256_priv_scalar": rx_priv_scalar.hex(),
193
        "p256_priv_pkcs8": rx_priv_pkcs8.hex(),
194
        "p256_pub": rx_pub_sec1.hex(),
195
        "ed25519_priv": "public-tutorial-key-does-not-sign",
196
        "ed25519_pub": "public-tutorial-key-does-not-sign",
197
        "_note": "PUBLIC TUTORIAL KEY for OSGT-HW-P256-v1. Demo-only.",
198
    }
199
    identity_path.write_text(json.dumps(identity, indent=2))
200
 
201
    print(f"[+] wrote {sealed_path} ({sealed_path.stat().st_size} bytes)")
202
    print(f"[+] wrote {identity_path}")
203
    print(f"    plaintext SHA-256: {content_hash}")
204
    print(f"    suite: {SUITE_HW_P256_V1}")
205
    return 0
206
 
207
 
208
if __name__ == "__main__":
209
    sys.exit(main())