Zion Boggan zionboggan.com ↗

tools: gen_hw_p256_sample.py for OSGT-HW-P256-v1 fixtures

Self-contained Python generator that produces a sealed file + recipient
identity for the hardware P-256 suite. Mirrors gen_hybrid_sample.py
without needing oqs (no PQ primitives), so any host with cryptography
installed can produce viewer test fixtures.

Used to generate the public viewer's tutorial-hw-p256.sealed and
tutorial-hw-p256-identity.json under viewer/samples/, deployed to
gh-pages with the corresponding decryptSealedHwP256 viewer.js path.
55ef0fe   Zion Boggan committed on May 8, 2026 (1 month ago)
tools/gen_hw_p256_sample.py +219 -0
@@ -0,0 +1,219 @@
+"""Generate an OSGT-HW-P256-v1 .sealed sample + matching identity JSON.
+
+Mirrors `tools/gen_hybrid_sample.py`. Self-contained: depends only on
+`cryptography` (no `oqs` needed because no PQ). Writes a sample that the
+viewer's `decryptSealedHwP256` (and `oversight-rust`'s
+`open_sealed_with_provider`) can both consume.
+
+Usage:
+ python3 gen_hw_p256_sample.py --out-dir ./out
+
+Outputs:
+ out/tutorial-hw-p256.sealed - viewer test fixture
+ out/tutorial-hw-p256-identity.json - recipient P-256 priv/pub
+"""
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+import struct
+import sys
+from pathlib import Path
+
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
+
+# Container constants matching oversight_core/container.py and
+# oversight-rust/oversight-container.
+MAGIC = b"OSGT\x01\x00"
+FORMAT_VERSION = 1
+SUITE_HW_P256_V1_ID = 3
+SUITE_HW_P256_V1 = "OSGT-HW-P256-v1"
+P256_PUBLIC_KEY_LEN = 65
+
+
+# ---------- XChaCha20-Poly1305 helper (HChaCha20 + ChaCha20-Poly1305) ----------
+def _hchacha20(key: bytes, nonce16: bytes) -> bytes:
+ assert len(key) == 32 and len(nonce16) == 16
+ state = bytearray(64)
+ state[0:4] = b"expa"; state[4:8] = b"nd 3"; state[8:12] = b"2-by"; state[12:16] = b"te k"
+ state[16:48] = key
+ state[48:64] = nonce16
+ s = list(struct.unpack("<16I", bytes(state)))
+
+ def rotl(v, n): return ((v << n) & 0xFFFFFFFF) | (v >> (32 - n))
+ def qr(a, b, c, d):
+ s[a] = (s[a] + s[b]) & 0xFFFFFFFF; s[d] = rotl(s[d] ^ s[a], 16)
+ s[c] = (s[c] + s[d]) & 0xFFFFFFFF; s[b] = rotl(s[b] ^ s[c], 12)
+ s[a] = (s[a] + s[b]) & 0xFFFFFFFF; s[d] = rotl(s[d] ^ s[a], 8)
+ s[c] = (s[c] + s[d]) & 0xFFFFFFFF; s[b] = rotl(s[b] ^ s[c], 7)
+
+ for _ in range(10):
+ qr(0, 4, 8, 12); qr(1, 5, 9, 13); qr(2, 6, 10, 14); qr(3, 7, 11, 15)
+ qr(0, 5, 10, 15); qr(1, 6, 11, 12); qr(2, 7, 8, 13); qr(3, 4, 9, 14)
+ return struct.pack("<8I", s[0], s[1], s[2], s[3], s[12], s[13], s[14], s[15])
+
+
+def xchacha20poly1305_encrypt(key: bytes, nonce24: bytes, plaintext: bytes, aad: bytes) -> bytes:
+ if len(key) != 32 or len(nonce24) != 24:
+ raise ValueError("xchacha20poly1305 requires 32-byte key and 24-byte nonce")
+ subkey = _hchacha20(key, nonce24[:16])
+ nonce12 = b"\x00\x00\x00\x00" + nonce24[16:24]
+ return ChaCha20Poly1305(subkey).encrypt(nonce12, plaintext, aad)
+
+
+def canonical_bytes(obj: dict) -> bytes:
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
+
+
+def strip_none(obj):
+ if isinstance(obj, dict):
+ return {k: strip_none(v) for k, v in obj.items() if v is not None}
+ if isinstance(obj, list):
+ return [strip_none(v) for v in obj if v is not None]
+ return obj
+
+
+def hw_p256_wrap_dek(dek: bytes, recipient_p256_pub_sec1: bytes) -> dict:
+ if len(recipient_p256_pub_sec1) != P256_PUBLIC_KEY_LEN:
+ raise ValueError(f"recipient pubkey must be {P256_PUBLIC_KEY_LEN} bytes")
+
+ peer = ec.EllipticCurvePublicKey.from_encoded_point(
+ ec.SECP256R1(), recipient_p256_pub_sec1
+ )
+ eph = ec.generate_private_key(ec.SECP256R1())
+ shared = eph.exchange(ec.ECDH(), peer)
+
+ kek = HKDF(
+ algorithm=hashes.SHA256(), length=32, salt=None,
+ info=b"oversight-hw-p256-v1-dek-wrap",
+ ).derive(shared)
+
+ nonce = os.urandom(24)
+ wrapped = xchacha20poly1305_encrypt(kek, nonce, dek, aad=b"oversight-hw-p256-dek")
+
+ eph_pub_bytes = eph.public_key().public_bytes(
+ encoding=serialization.Encoding.X962,
+ format=serialization.PublicFormat.UncompressedPoint,
+ )
+ assert len(eph_pub_bytes) == P256_PUBLIC_KEY_LEN
+
+ return {
+ "suite": SUITE_HW_P256_V1,
+ "ephemeral_pub": eph_pub_bytes.hex(),
+ "nonce": nonce.hex(),
+ "wrapped_dek": wrapped.hex(),
+ }
+
+
+def main() -> int:
+ p = argparse.ArgumentParser()
+ p.add_argument("--out-dir", required=True, type=Path)
+ p.add_argument("--message", default="hello hardware-keys oversight\n")
+ args = p.parse_args()
+ args.out_dir.mkdir(parents=True, exist_ok=True)
+
+ # Recipient identity (P-256)
+ rx_priv = ec.generate_private_key(ec.SECP256R1())
+ rx_pub_sec1 = rx_priv.public_key().public_bytes(
+ encoding=serialization.Encoding.X962,
+ format=serialization.PublicFormat.UncompressedPoint,
+ )
+ rx_priv_pkcs8 = rx_priv.private_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+ # Also export the raw 32-byte scalar for viewers that want it.
+ rx_priv_scalar = rx_priv.private_numbers().private_value.to_bytes(32, "big")
+
+ # Issuer Ed25519
+ issuer = Ed25519PrivateKey.generate()
+ issuer_pub_bytes = issuer.public_key().public_bytes(
+ encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw,
+ )
+
+ # Plaintext + DEK
+ plaintext = args.message.encode("utf-8")
+ content_hash = hashlib.sha256(plaintext).hexdigest()
+ dek = os.urandom(32)
+
+ # Outer AEAD (DEK encrypts plaintext, AAD = content_hash hex string ASCII)
+ aead_nonce = os.urandom(24)
+ ciphertext = xchacha20poly1305_encrypt(
+ dek, aead_nonce, plaintext, aad=content_hash.encode("ascii")
+ )
+
+ # Wrap DEK for recipient (P-256 ECDH)
+ wrapped_dek = hw_p256_wrap_dek(dek, rx_pub_sec1)
+
+ # Manifest
+ manifest = {
+ "suite": SUITE_HW_P256_V1,
+ "format": "oversight/v1",
+ "issuer_id": "tutorial-hw-p256@oversightprotocol.dev",
+ "issuer_ed25519_pub": issuer_pub_bytes.hex(),
+ "issuer_ml_dsa_pub": "",
+ "recipient": {
+ "id": "tutorial@oversightprotocol.dev",
+ "x25519_pub": "",
+ "p256_pub": rx_pub_sec1.hex(),
+ },
+ "content_type": "text/plain",
+ "content_hash": content_hash,
+ "canonical_content_hash": content_hash,
+ "l3_policy": {"enabled": False, "mode": "off"},
+ "filename": "hello-hw-p256.txt",
+ "signature_ed25519": "",
+ "signature_ml_dsa": "",
+ }
+
+ manifest_for_sign = strip_none(manifest)
+ manifest_for_sign["signature_ed25519"] = ""
+ manifest_for_sign["signature_ml_dsa"] = ""
+ sig_bytes = issuer.sign(canonical_bytes(manifest_for_sign))
+ manifest["signature_ed25519"] = sig_bytes.hex()
+
+ manifest_serialized = canonical_bytes(strip_none(manifest))
+ wrapped_dek_serialized = canonical_bytes(wrapped_dek)
+
+ container = bytearray()
+ container.extend(MAGIC)
+ container.extend(bytes([FORMAT_VERSION, SUITE_HW_P256_V1_ID]))
+ container.extend(struct.pack(">I", len(manifest_serialized)))
+ container.extend(manifest_serialized)
+ container.extend(struct.pack(">I", len(wrapped_dek_serialized)))
+ container.extend(wrapped_dek_serialized)
+ container.extend(aead_nonce)
+ container.extend(struct.pack(">I", len(ciphertext)))
+ container.extend(ciphertext)
+
+ sealed_path = args.out_dir / "tutorial-hw-p256.sealed"
+ identity_path = args.out_dir / "tutorial-hw-p256-identity.json"
+
+ sealed_path.write_bytes(bytes(container))
+ identity = {
+ "recipient_id": "tutorial@oversightprotocol.dev",
+ "p256_priv_scalar": rx_priv_scalar.hex(),
+ "p256_priv_pkcs8": rx_priv_pkcs8.hex(),
+ "p256_pub": rx_pub_sec1.hex(),
+ "ed25519_priv": "public-tutorial-key-does-not-sign",
+ "ed25519_pub": "public-tutorial-key-does-not-sign",
+ "_note": "PUBLIC TUTORIAL KEY for OSGT-HW-P256-v1. Demo-only.",
+ }
+ identity_path.write_text(json.dumps(identity, indent=2))
+
+ print(f"[+] wrote {sealed_path} ({sealed_path.stat().st_size} bytes)")
+ print(f"[+] wrote {identity_path}")
+ print(f" plaintext SHA-256: {content_hash}")
+ print(f" suite: {SUITE_HW_P256_V1}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())