From d91b88f56ad0def18a8ac46637e5559eeefb55ab Mon Sep 17 00:00:00 2001 From: quantova-inc Date: Sat, 7 Nov 2026 13:53:44 +0800 Subject: [PATCH] Add optional post quantum attestation to the core SDK This adds an opt in FIPS 204 ML-DSA attestation of JPYC operations. It signs a canonical payload such as a transfer or a mint request so a holder can confirm the operation is authentic and has not been altered using a signature scheme that stays safe once large quantum computers exist. Integrity is always covered by the payload digest and authenticity is covered when the verifier pins the public key. The module is self contained behind an optional pq extra and does not change the existing SDK. ruff, ruff format and strict mypy are clean and the tests cover build and verify, a tampered payload, a wrong signer, a forged signature, a malformed field and canonical encoding. Signed-off-by: quantova-inc --- packages/core/jpyc_core_sdk/attestation.py | 110 +++++++++++++++++++++ packages/core/pyproject.toml | 7 ++ tests/core/test_attestation.py | 64 ++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 packages/core/jpyc_core_sdk/attestation.py create mode 100644 tests/core/test_attestation.py diff --git a/packages/core/jpyc_core_sdk/attestation.py b/packages/core/jpyc_core_sdk/attestation.py new file mode 100644 index 0000000..8fb32e1 --- /dev/null +++ b/packages/core/jpyc_core_sdk/attestation.py @@ -0,0 +1,110 @@ +"""Post quantum attestation of JPYC operations. + +Signs a canonical payload such as a transfer or a mint request with FIPS 204 +ML-DSA so a holder can confirm the operation is authentic and has not been +altered, using a signature scheme that stays safe once large quantum computers +exist. + +The attestation is self contained. It carries the public key and the signature, +so a verifier needs only the attestation and the payload. Integrity is always +covered by the payload digest and authenticity is covered when the verifier +pins the expected public key. + +The scheme comes from the optional pq extra. Install it with +``pip install jpyc-core-sdk[pq]``. +""" + +import base64 +import hashlib +import json +from typing import Any, TypedDict + +SCHEME = "ML-DSA-65" +ISSUER = "Quantova" + + +class Attestation(TypedDict): + """A self contained post quantum attestation over a payload. + + Attributes: + scheme (str): The FIPS 204 signature scheme used. + issuer (str): The party that built the attestation. + payload_sha256 (str): Hex digest of the signed payload. + public_key (str): Base64 encoded public key. + signature (str): Base64 encoded signature. + """ + + scheme: str + issuer: str + payload_sha256: str + public_key: str + signature: str + + +def _backend() -> Any: + """Return the ML-DSA backend or raise if the pq extra is not installed.""" + try: + from dilithium_py.ml_dsa import ML_DSA_65 # type: ignore[import-untyped] + except ImportError as error: + raise RuntimeError( + "Post quantum attestation needs the pq extra. " + "Install it with pip install jpyc-core-sdk[pq]." + ) from error + return ML_DSA_65 + + +def canonicalize(payload: dict[str, Any]) -> bytes: + """Return the canonical byte encoding an attestation is computed over. + + Keys are sorted and whitespace is fixed so the same payload always yields + the same bytes on any machine. + """ + return json.dumps( + payload, ensure_ascii=False, sort_keys=True, separators=(",", ":") + ).encode("utf-8") + + +def generate_keypair() -> tuple[bytes, bytes]: + """Return a fresh (public_key, secret_key) pair for the signer.""" + public_key, secret_key = _backend().keygen() + return public_key, secret_key + + +def build_attestation( + payload: bytes, public_key: bytes, secret_key: bytes +) -> Attestation: + """Return a self contained attestation over payload.""" + signature = _backend().sign(secret_key, payload, deterministic=True) + return Attestation( + scheme=SCHEME, + issuer=ISSUER, + payload_sha256=hashlib.sha256(payload).hexdigest(), + public_key=base64.b64encode(public_key).decode("ascii"), + signature=base64.b64encode(signature).decode("ascii"), + ) + + +def verify_attestation( + payload: bytes, + attestation: Attestation, + expected_public_key: bytes | None = None, +) -> bool: + """Return True when attestation matches payload. + + The payload digest is always checked. When expected_public_key is given the + signer is pinned too, so a valid signature made under a different key is + rejected. + """ + if attestation["payload_sha256"] != hashlib.sha256(payload).hexdigest(): + return False + try: + public_key = base64.b64decode(attestation["public_key"], validate=True) + signature = base64.b64decode(attestation["signature"], validate=True) + except ValueError: + return False + if expected_public_key is not None and public_key != expected_public_key: + return False + try: + return bool(_backend().verify(public_key, payload, signature)) + except (ValueError, TypeError): + return False diff --git a/packages/core/pyproject.toml b/packages/core/pyproject.toml index 5ae7a50..7e62db1 100644 --- a/packages/core/pyproject.toml +++ b/packages/core/pyproject.toml @@ -41,6 +41,13 @@ dependencies = [ "web3>=7.11.0", ] +[project.optional-dependencies] +# Optional post quantum attestation (jpyc_core_sdk/attestation.py). +# Install with `pip install jpyc-core-sdk[pq]`. +pq = [ + "dilithium-py>=1.0.1", +] + [project.urls] repository = "https://github.com/jcam1/python-sdk" documentation = "https://github.com/jcam1/python-sdk/tree/main/docs/core" diff --git a/tests/core/test_attestation.py b/tests/core/test_attestation.py new file mode 100644 index 0000000..aa84852 --- /dev/null +++ b/tests/core/test_attestation.py @@ -0,0 +1,64 @@ +import base64 + +import pytest + +pytest.importorskip("dilithium_py") + +from packages.core.jpyc_core_sdk import attestation # noqa: E402 + +TRANSFER = { + "chain_id": 1, + "operation": "transfer", + "from": "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF", + "to": "0x1111111111111111111111111111111111111111", + "value": "1000000000000000000", + "nonce": 7, +} + + +def test_build_and_verify_roundtrip() -> None: + payload = attestation.canonicalize(TRANSFER) + public_key, secret_key = attestation.generate_keypair() + att = attestation.build_attestation(payload, public_key, secret_key) + assert att["scheme"] == "ML-DSA-65" + assert att["issuer"] == "Quantova" + assert attestation.verify_attestation(payload, att) is True + assert attestation.verify_attestation(payload, att, public_key) is True + + +def test_tampered_payload_is_rejected() -> None: + payload = attestation.canonicalize(TRANSFER) + public_key, secret_key = attestation.generate_keypair() + att = attestation.build_attestation(payload, public_key, secret_key) + altered = attestation.canonicalize({**TRANSFER, "value": "9" * 18}) + assert attestation.verify_attestation(altered, att) is False + + +def test_wrong_signer_is_rejected_when_pinned() -> None: + payload = attestation.canonicalize(TRANSFER) + public_key, secret_key = attestation.generate_keypair() + other_public_key, _ = attestation.generate_keypair() + att = attestation.build_attestation(payload, public_key, secret_key) + assert attestation.verify_attestation(payload, att, other_public_key) is False + + +def test_forged_signature_is_rejected() -> None: + payload = attestation.canonicalize(TRANSFER) + public_key, secret_key = attestation.generate_keypair() + att = attestation.build_attestation(payload, public_key, secret_key) + att["signature"] = base64.b64encode(b"\x00" * 64).decode("ascii") + assert attestation.verify_attestation(payload, att) is False + + +def test_malformed_field_is_rejected() -> None: + payload = attestation.canonicalize(TRANSFER) + public_key, secret_key = attestation.generate_keypair() + att = attestation.build_attestation(payload, public_key, secret_key) + att["public_key"] = "!!!not base64!!!" + assert attestation.verify_attestation(payload, att) is False + + +def test_canonicalize_is_order_independent() -> None: + assert attestation.canonicalize({"a": 1, "b": 2}) == attestation.canonicalize( + {"b": 2, "a": 1} + )