Skip to content

Commit b61d1ba

Browse files
Fix the three red binding/pages CI workflows (all pre-existing)
python-binding, wasm-binding, and pages had been red since the bindings landed (2026-05-20) — none related to the 0.5 crypto work. Root causes: Code drift vs the current pqf-reader API (the real CI compile errors): - Both bindings referenced a removed `Header.alg` field. The reader validates then discards the alg identifiers, so a parsed header is guaranteed conformant. Expose them as canonical pub consts (ALG_AEAD/COMBINER/KDF/KEM/SIG) from pqf-reader and have both bindings report those — single source of truth, no re-drift. - wasm: chunk_size is u64 (cast to u32 for the JS view). - python: SignerEntry fields are ed25519_pub/mldsa87_pub, not classical_pub/pqc_pub (kept the spec-named dict keys). Build-config fixes: - wasm: aes-gcm pulls getrandom 0.2, which needs the "js" feature on wasm32-unknown-unknown. - python: pyo3 0.22 gates the &PyDict/&PyBytes GIL-ref API this binding uses behind the "gil-refs" feature. - python: pyproject license pointed at ../../LICENSE (outside the project); newer maturin rejects it — use the SPDX `license = "MIT"` string. - python-binding.yml: `maturin develop` needs an active virtualenv; create and activate one (cross-platform). Also update the smoke-test combiner assertion to pqf1-bind-extract-v1. Verified locally: reader conformance still 47/47; wasm builds for wasm32-unknown-unknown; python `maturin develop` + smoke parses and decrypts TV-001 (4291 bytes) under the new bind-extract crypto. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fb163e3 commit b61d1ba

10 files changed

Lines changed: 67 additions & 33 deletions

File tree

.github/workflows/python-binding.yml

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,16 @@ jobs:
4646
impl/rust/pqf-reader/target
4747
key: ${{ runner.os }}-py${{ matrix.python }}-cargo-${{ hashFiles('bindings/python/Cargo.toml', 'impl/rust/pqf-reader/Cargo.toml') }}
4848

49-
- name: Install maturin
50-
run: pip install --upgrade pip maturin
51-
52-
- name: Build + install the binding into the venv
53-
working-directory: bindings/python
54-
run: maturin develop --release
55-
56-
- name: Smoke test (import + parse a vector)
49+
- name: Build binding + smoke test (in a venv)
5750
working-directory: bindings/python
5851
shell: bash
5952
run: |
53+
# maturin develop requires an active virtualenv; create and activate
54+
# one (cross-platform: bin/ on unix, Scripts/ on windows runners).
55+
python -m venv .venv
56+
if [ -f .venv/bin/activate ]; then source .venv/bin/activate; else source .venv/Scripts/activate; fi
57+
python -m pip install --upgrade pip maturin
58+
maturin develop --release
6059
python - <<'PY'
6160
import pqf, json, pathlib
6261
repo = pathlib.Path("../..").resolve()
@@ -65,7 +64,7 @@ jobs:
6564
blob = path.read_bytes()
6665
header = pqf.parse_header(blob)
6766
assert header["alg"]["kem"] == "x25519+ml-kem-1024", header
68-
assert header["alg"]["combiner"] == "pqf1-concat-extract-v1"
67+
assert header["alg"]["combiner"] == "pqf1-bind-extract-v1"
6968
assert len(header["recipients"]) >= 1
7069
print("OK: pqf.parse_header round-trips on TV-001")
7170

bindings/python/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target
2+
Cargo.lock
3+
.venv/

bindings/python/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@ crate-type = ["cdylib"]
1313
path = "src/lib.rs"
1414

1515
[dependencies]
16-
pyo3 = { version = "0.22", features = ["extension-module", "abi3-py38"] }
16+
# gil-refs re-enables the deprecated &PyDict / &PyBytes GIL-ref API this
17+
# binding is written against (PyDict::new, PyBytes::new, &'py PyDict returns).
18+
pyo3 = { version = "0.22", features = ["extension-module", "abi3-py38", "gil-refs"] }
1719
pqf-reader = { path = "../../impl/rust/pqf-reader" }

bindings/python/pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ name = "pqf"
77
version = "0.1.0"
88
description = "Python bindings for the PQF (PostQuantum.FileFormat) Rust reader"
99
requires-python = ">=3.8"
10-
license = { file = "../../LICENSE" }
10+
license = "MIT"
1111
authors = [
1212
{ name = "Paul Clark", email = "systemslibrarian@gmail.com" }
1313
]
1414
keywords = ["post-quantum", "cryptography", "file-format", "hybrid", "ml-kem", "ml-dsa"]
1515
classifiers = [
1616
"Development Status :: 3 - Alpha",
1717
"Intended Audience :: Developers",
18-
"License :: OSI Approved :: MIT License",
1918
"Programming Language :: Python :: 3",
2019
"Programming Language :: Rust",
2120
"Topic :: Security :: Cryptography",

bindings/python/src/lib.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
//! identity = pqf.Identity.from_manifest("id-a", pub_b64, x25519_b64, mlkem_b64)
1717
//! plaintext = pqf.decrypt(open("a.pqf","rb").read(), identity)
1818
19-
use pqf_reader::{decrypt as rust_decrypt, parse as rust_parse, Identity as RustIdentity};
19+
use pqf_reader::{
20+
decrypt as rust_decrypt, parse as rust_parse, Identity as RustIdentity, ALG_AEAD, ALG_COMBINER,
21+
ALG_KDF, ALG_KEM, ALG_SIG,
22+
};
2023
use pyo3::exceptions::{PyRuntimeError, PyValueError};
2124
use pyo3::prelude::*;
2225
use pyo3::types::{PyBytes, PyDict, PyList};
@@ -51,12 +54,15 @@ fn parse_header<'py>(py: Python<'py>, file_bytes: &[u8]) -> PyResult<&'py PyDict
5154
let h = &parsed.header;
5255
let out = PyDict::new(py);
5356

57+
// A parsed header is guaranteed to carry exactly the v1 algorithm
58+
// identifiers (the reader refuses anything else), so report them from the
59+
// reader's canonical constants rather than re-reading the header.
5460
let alg = PyDict::new(py);
55-
alg.set_item("aead", &h.alg.aead)?;
56-
alg.set_item("combiner", &h.alg.combiner)?;
57-
alg.set_item("kdf", &h.alg.kdf)?;
58-
alg.set_item("kem", &h.alg.kem)?;
59-
alg.set_item("sig", &h.alg.sig)?;
61+
alg.set_item("aead", ALG_AEAD)?;
62+
alg.set_item("combiner", ALG_COMBINER)?;
63+
alg.set_item("kdf", ALG_KDF)?;
64+
alg.set_item("kem", ALG_KEM)?;
65+
alg.set_item("sig", ALG_SIG)?;
6066
out.set_item("alg", alg)?;
6167

6268
out.set_item("chunk_size", h.chunk_size)?;
@@ -71,8 +77,8 @@ fn parse_header<'py>(py: Python<'py>, file_bytes: &[u8]) -> PyResult<&'py PyDict
7177

7278
if let Some(signer) = &h.signer {
7379
let s = PyDict::new(py);
74-
s.set_item("classical_pub", PyBytes::new(py, &signer.classical_pub))?;
75-
s.set_item("pqc_pub", PyBytes::new(py, &signer.pqc_pub))?;
80+
s.set_item("classical_pub", PyBytes::new(py, &signer.ed25519_pub))?;
81+
s.set_item("pqc_pub", PyBytes::new(py, &signer.mldsa87_pub))?;
7682
out.set_item("signer", s)?;
7783
} else {
7884
out.set_item("signer", py.None())?;

bindings/wasm/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target
2+
Cargo.lock
3+
pkg/

bindings/wasm/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ serde = { version = "1", features = ["derive"] }
1717
serde-wasm-bindgen = "0.6"
1818
pqf-reader = { path = "../../impl/rust/pqf-reader" }
1919
console_error_panic_hook = { version = "0.1", optional = true }
20+
# aes-gcm (via the reader) pulls getrandom 0.2; the wasm32-unknown-unknown
21+
# target needs its "js" backend explicitly enabled to build.
22+
getrandom = { version = "0.2", features = ["js"] }
2023

2124
[features]
2225
default = ["console_error_panic_hook"]

bindings/wasm/src/lib.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
//! payload. See demo/index.html for a minimal "paste a file, see the
88
//! header" page that loads them directly.
99
10-
use pqf_reader::{decrypt as rust_decrypt, parse as rust_parse, Identity as RustIdentity};
10+
use pqf_reader::{
11+
decrypt as rust_decrypt, parse as rust_parse, Identity as RustIdentity, ALG_AEAD, ALG_COMBINER,
12+
ALG_KDF, ALG_KEM, ALG_SIG,
13+
};
1114
use serde::Serialize;
1215
use wasm_bindgen::prelude::*;
1316

@@ -46,14 +49,17 @@ pub fn parse_header(file_bytes: &[u8]) -> Result<JsValue, JsError> {
4649
let parsed = rust_parse(file_bytes).map_err(map_err)?;
4750
let h = &parsed.header;
4851
let js = HeaderJs {
52+
// A parsed header is guaranteed to carry exactly the v1 algorithm
53+
// identifiers (the reader refuses anything else), so report them from
54+
// the reader's canonical constants rather than re-reading the header.
4955
alg: AlgJs {
50-
aead: h.alg.aead.clone(),
51-
combiner: h.alg.combiner.clone(),
52-
kdf: h.alg.kdf.clone(),
53-
kem: h.alg.kem.clone(),
54-
sig: h.alg.sig.clone(),
56+
aead: ALG_AEAD.to_string(),
57+
combiner: ALG_COMBINER.to_string(),
58+
kdf: ALG_KDF.to_string(),
59+
kem: ALG_KEM.to_string(),
60+
sig: ALG_SIG.to_string(),
5561
},
56-
chunk_size: h.chunk_size,
62+
chunk_size: h.chunk_size as u32,
5763
created: h.created.clone(),
5864
file_id_hex: hex_lower(&h.file_id),
5965
recipient_count: h.recipients.len(),

impl/rust/pqf-reader/src/header.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,23 @@ const KNOWN_TOP_FIELDS: &[&str] = &[
2121
"signer",
2222
];
2323

24+
// The exact algorithm identifiers a conformant PQF v1 header carries. A
25+
// successfully parsed `Header` is guaranteed to match these (the parser refuses
26+
// any other values), so consumers that want to display the algorithm suite can
27+
// rely on these constants instead of re-deriving — and stay in sync if the
28+
// suite ever changes. Exposed via the crate root.
29+
pub const ALG_AEAD: &str = "aes-256-gcm-chunked";
30+
pub const ALG_COMBINER: &str = "pqf1-bind-extract-v1";
31+
pub const ALG_KDF: &str = "hkdf-sha256";
32+
pub const ALG_KEM: &str = "x25519+ml-kem-1024";
33+
pub const ALG_SIG: &str = "ed25519+ml-dsa-87";
34+
2435
const ALG_REQUIRED: &[(&str, &str)] = &[
25-
("aead", "aes-256-gcm-chunked"),
26-
("combiner", "pqf1-bind-extract-v1"),
27-
("kdf", "hkdf-sha256"),
28-
("kem", "x25519+ml-kem-1024"),
29-
("sig", "ed25519+ml-dsa-87"),
36+
("aead", ALG_AEAD),
37+
("combiner", ALG_COMBINER),
38+
("kdf", ALG_KDF),
39+
("kem", ALG_KEM),
40+
("sig", ALG_SIG),
3041
];
3142

3243
const RECIPIENT_FIELDS: &[&str] = &["classical_epk", "pqc_ct", "wrapped_dek", "wrapped_dek_nonce"];

impl/rust/pqf-reader/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub mod identity;
2424
pub mod reader;
2525

2626
pub use error::{PqfError, RefusalReason, Result};
27-
pub use header::{Header, RecipientEntry, SignerEntry};
27+
pub use header::{
28+
Header, RecipientEntry, SignerEntry, ALG_AEAD, ALG_COMBINER, ALG_KDF, ALG_KEM, ALG_SIG,
29+
};
2830
pub use identity::Identity;
2931
pub use reader::{decrypt, parse, ParsedFile};

0 commit comments

Comments
 (0)