From 3a9788c42834321aa7af7b079c8f0ec2d8596655 Mon Sep 17 00:00:00 2001 From: bordumb Date: Mon, 22 Jun 2026 22:10:21 +0100 Subject: [PATCH 1/3] test(auths-keri): unify the keripy subprocess gate and run both in CI The two keripy differential tests each re-implemented the KERIPY_INTEROP gate and the python importability probe, and the SAID-equality one ran in no CI job and skipped silently. Extract one shared should_run_keripy gate (honoring KERIPY_REQUIRED) and run both subprocess differentials in the conformance job, so neither can quietly revert to a self-oracle. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- .github/workflows/ci.yml | 2 +- .../auths-keri/tests/cases/keripy_interop.rs | 13 +------ .../tests/cases/keripy_kel_oracle.rs | 21 +---------- .../auths-keri/tests/cases/keripy_oracle.rs | 36 +++++++++++++++++++ crates/auths-keri/tests/cases/mod.rs | 1 + 5 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 crates/auths-keri/tests/cases/keripy_oracle.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f84096b36..349bff648 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -397,7 +397,7 @@ jobs: env: KERIPY_INTEROP: "1" KERIPY_REQUIRED: "1" - run: uv run cargo test --manifest-path ../../Cargo.toml -p auths-keri --test integration keripy_subprocess_agrees_on_accept_and_reject -- --nocapture + run: uv run cargo test --manifest-path ../../Cargo.toml -p auths-keri --test integration subprocess -- --nocapture - name: Upload conformance results if: always() uses: actions/upload-artifact@v4 diff --git a/crates/auths-keri/tests/cases/keripy_interop.rs b/crates/auths-keri/tests/cases/keripy_interop.rs index ea8f0a9e5..b8b4dd189 100644 --- a/crates/auths-keri/tests/cases/keripy_interop.rs +++ b/crates/auths-keri/tests/cases/keripy_interop.rs @@ -143,20 +143,9 @@ fn icp_version_string_byte_count_matches() { /// otherwise with a clear log line. #[test] fn subprocess_mode_when_keripy_available() { - if std::env::var("KERIPY_INTEROP").ok().as_deref() != Some("1") { - eprintln!("[SKIP] KERIPY_INTEROP != 1; not invoking python subprocess"); + if !super::keripy_oracle::should_run_keripy("import keri.core.serdering") { return; } - let probe = std::process::Command::new("python3") - .args(["-c", "import keri.core.serdering"]) - .status(); - match probe { - Ok(s) if s.success() => {} - _ => { - eprintln!("[SKIP] python3 + keri.core.serdering not available"); - return; - } - } let icp = make_icp(); let wire = serde_json::to_vec(&Event::Icp(icp.clone())).unwrap(); diff --git a/crates/auths-keri/tests/cases/keripy_kel_oracle.rs b/crates/auths-keri/tests/cases/keripy_kel_oracle.rs index 9661e0ce9..e24be28f4 100644 --- a/crates/auths-keri/tests/cases/keripy_kel_oracle.rs +++ b/crates/auths-keri/tests/cases/keripy_kel_oracle.rs @@ -141,18 +141,7 @@ fn cesr_stream(parts: &[(&[u8], &[u8])]) -> Vec { /// keripy is unavailable. #[test] fn keripy_subprocess_agrees_on_accept_and_reject() { - // `KERIPY_REQUIRED=1` (set by the CI conformance job) turns the otherwise-silent - // skips into hard failures: a venv/PATH break that left keripy unreachable would - // otherwise revert the reject cases to a self-oracle while the job stayed green. - let required = std::env::var("KERIPY_REQUIRED").ok().as_deref() == Some("1"); - if std::env::var("KERIPY_INTEROP").ok().as_deref() != Some("1") { - assert!(!required, "KERIPY_REQUIRED=1 but KERIPY_INTEROP is not set"); - eprintln!("[SKIP] KERIPY_INTEROP != 1; not invoking keripy"); - return; - } - if !keripy_importable() { - assert!(!required, "KERIPY_REQUIRED=1 but keripy is not importable"); - eprintln!("[SKIP] keripy not importable"); + if !super::keripy_oracle::should_run_keripy("import keri.core.eventing, keri.core.parsing") { return; } @@ -212,14 +201,6 @@ fn keripy_subprocess_agrees_on_accept_and_reject() { } } -fn keripy_importable() -> bool { - std::process::Command::new("python3") - .args(["-c", "import keri.core.eventing, keri.core.parsing"]) - .status() - .map(|s| s.success()) - .unwrap_or(false) -} - /// Drive a CESR stream through keripy's `Parser`/`Kevery` and return the highest sequence /// number the (single) AID reached, or `None` if no event was accepted. fn keripy_highest_sn(stream: &[u8]) -> Option { diff --git a/crates/auths-keri/tests/cases/keripy_oracle.rs b/crates/auths-keri/tests/cases/keripy_oracle.rs new file mode 100644 index 000000000..2fe0f149a --- /dev/null +++ b/crates/auths-keri/tests/cases/keripy_oracle.rs @@ -0,0 +1,36 @@ +//! Shared gate for the keripy-subprocess differentials. + +/// Decide whether a keripy subprocess cross-check should run. +/// +/// Returns `true` to run, `false` to skip. Panics if `KERIPY_REQUIRED=1` but keripy +/// is not reachable (the CI conformance job sets it), so a venv/PATH break fails the +/// build instead of silently reverting the cross-check to a self-oracle. +/// +/// Args: +/// * `import_probe`: a python import statement that succeeds iff the keripy modules +/// this differential needs are importable (e.g. `"import keri.core.parsing"`). +/// +/// Usage: +/// ```ignore +/// if !super::keripy_oracle::should_run_keripy("import keri.core.parsing") { +/// return; +/// } +/// ``` +pub fn should_run_keripy(import_probe: &str) -> bool { + let required = std::env::var("KERIPY_REQUIRED").ok().as_deref() == Some("1"); + if std::env::var("KERIPY_INTEROP").ok().as_deref() != Some("1") { + assert!(!required, "KERIPY_REQUIRED=1 but KERIPY_INTEROP is not set"); + eprintln!("[SKIP] KERIPY_INTEROP != 1; not invoking keripy"); + return false; + } + let importable = std::process::Command::new("python3") + .args(["-c", import_probe]) + .status() + .map(|s| s.success()) + .unwrap_or(false); + if !importable { + assert!(!required, "KERIPY_REQUIRED=1 but keripy is not importable"); + eprintln!("[SKIP] keripy not importable"); + } + importable +} diff --git a/crates/auths-keri/tests/cases/mod.rs b/crates/auths-keri/tests/cases/mod.rs index 9d55f0b56..eea4b977e 100644 --- a/crates/auths-keri/tests/cases/mod.rs +++ b/crates/auths-keri/tests/cases/mod.rs @@ -7,6 +7,7 @@ mod event; mod interop_vectors; mod keripy_interop; mod keripy_kel_oracle; +mod keripy_oracle; mod multi_key_threshold; #[cfg(feature = "cesr")] mod roundtrip; From e82f2bff23ddaafbd8a5e85bc06ad438c30741e5 Mon Sep 17 00:00:00 2001 From: bordumb Date: Mon, 22 Jun 2026 22:10:37 +0100 Subject: [PATCH 2/3] test(auths-keri): pin the saider and indexer length tables to cesride H3 pinned matter_full_len; do the same for the other two mirrored cesride size tables. Encode a Blake3 digest and single-/dual-index Ed25519 sigers and assert saider_qb64_len and indexer_sizage match cesride's actual qb64 widths, so a cesride bump that shifts a size fails loudly. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- crates/auths-keri/src/events.rs | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/auths-keri/src/events.rs b/crates/auths-keri/src/events.rs index 74d3f3bc0..9e460e3c9 100644 --- a/crates/auths-keri/src/events.rs +++ b/crates/auths-keri/src/events.rs @@ -1709,6 +1709,43 @@ mod tests { assert!(parse_source_seal_couples(seal.as_bytes()).is_err()); } + #[test] + fn cesr_length_tables_match_cesride_encoding() { + use cesride::{Diger, Matter, matter}; + // saider_qb64_len: pin the Blake3-256 SAID code auths emits to cesride's width. + let digest = Diger::new( + None, + Some(matter::Codex::Blake3_256), + Some(&[0u8; 32]), + None, + None, + None, + ) + .and_then(|d| d.qb64()) + .unwrap(); + assert_eq!(saider_qb64_len(digest.as_bytes()[0]), Some(digest.len())); + + // indexer_sizage: pin the single-index (`A`) and dual-index (`2A`) Ed25519 siger + // widths auths emits, by measuring a real encoded siger (after the 4-char counter). + for prior_index in [None, Some(1u32)] { + let att = serialize_attachment(&[IndexedSignature { + index: 0, + prior_index, + sig: vec![0u8; 64], + }]) + .unwrap(); + let att = std::str::from_utf8(&att).unwrap(); + let siger = &att[4..]; + let code = &siger[..indexer_hard_len(siger.as_bytes()[0])]; + assert_eq!( + indexer_sizage(code).map(|(fs, _)| fs), + Some(siger.len()), + "indexer_sizage({code:?}) drifted from cesride's {} chars", + siger.len() + ); + } + } + #[test] fn attachment_roundtrip_three_sigs() { let sigs = vec![ From 09c4a4ff38b891322929cba01e739456a2b0e88c Mon Sep 17 00:00:00 2001 From: bordumb Date: Mon, 22 Jun 2026 22:10:37 +0100 Subject: [PATCH 3/3] refactor(auths-keri): route curve_of_aid through the shared CESR guard curve_of_aid handed an AID straight to decode_verkey, the one caller that did not pre-validate, which is why decode_verkey carried a catch_unwind backstop. Validate via take_matter_qb64 first; now both callers pass an exact, validated primitive, so drop the redundant catch_unwind and document the pre-validation precondition. Malformed-input tests still fail closed. Auths-Id: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Device: did:keri:EB5cPHY0t-ejNC_rUzPS1dclTvd6kG-R9mQzjozCuGgd Auths-Anchor-Seq: 1 --- crates/auths-keri/src/cesr_encode.rs | 18 +++++++++--------- crates/auths-keri/src/witness/cesr_receipt.rs | 3 ++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/auths-keri/src/cesr_encode.rs b/crates/auths-keri/src/cesr_encode.rs index 11f15092a..7b4e7f66d 100644 --- a/crates/auths-keri/src/cesr_encode.rs +++ b/crates/auths-keri/src/cesr_encode.rs @@ -53,16 +53,16 @@ pub(crate) fn encode_verkey(raw: &[u8], code: &str) -> Result Result<(Vec, String), KeriDecodeError> { - // cesride derives a primitive's length from its derivation code and indexes - // into the qb64 by that length, so a truncated or malformed code slices out - // of bounds and panics. Contain the panic at this untrusted-input boundary so - // a bad verkey fails closed with a typed error instead of crashing the caller. - std::panic::catch_unwind(|| { - Verfer::new(None, None, None, Some(qb64), None).map(|v| (v.raw(), v.code())) - }) - .map_err(|_| KeriDecodeError::DecodeError("malformed CESR verkey primitive".into()))? - .map_err(|e| KeriDecodeError::DecodeError(e.to_string())) + Verfer::new(None, None, None, Some(qb64), None) + .map(|v| (v.raw(), v.code())) + .map_err(|e| KeriDecodeError::DecodeError(e.to_string())) } /// CESR Matter hard-code character length, keyed by the code's first character diff --git a/crates/auths-keri/src/witness/cesr_receipt.rs b/crates/auths-keri/src/witness/cesr_receipt.rs index e4d52b18e..9f31612f7 100644 --- a/crates/auths-keri/src/witness/cesr_receipt.rs +++ b/crates/auths-keri/src/witness/cesr_receipt.rs @@ -84,7 +84,8 @@ pub fn encode_sig(curve: CurveType, sig: &[u8]) -> Result Result { - let (_raw, code) = crate::cesr_encode::decode_verkey(aid)?; + let primitive = crate::cesr_encode::take_matter_qb64(aid)?; + let (_raw, code) = crate::cesr_encode::decode_verkey(primitive)?; if code == matter::Codex::Ed25519 || code == matter::Codex::Ed25519N { Ok(CurveType::Ed25519) } else if code == matter::Codex::ECDSA_256r1 || code == matter::Codex::ECDSA_256r1N {