Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions crates/auths-keri/src/cesr_encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,16 @@ pub(crate) fn encode_verkey(raw: &[u8], code: &str) -> Result<String, KeriDecode
///
/// Args:
/// * `qb64`: The CESR-qualified verkey string (e.g. `"DAAB…"`, `"1AAJ…"`).
///
/// PRECONDITION: `qb64` must already be a validated, exact-length CESR primitive —
/// every caller (`KeriPublicKey::parse`, `curve_of_aid`) runs [`take_matter_qb64`]
/// first. cesride's decoder slices the input by a code-derived length, so handing it
/// an unvalidated (truncated/non-ASCII) string would slice out of bounds and panic;
/// the guard makes that unreachable. Do not call this with unvalidated input.
pub(crate) fn decode_verkey(qb64: &str) -> Result<(Vec<u8>, 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
Expand Down
37 changes: 37 additions & 0 deletions crates/auths-keri/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![
Expand Down
3 changes: 2 additions & 1 deletion crates/auths-keri/src/witness/cesr_receipt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ pub fn encode_sig(curve: CurveType, sig: &[u8]) -> Result<String, KeriDecodeErro

/// The signing curve a witness AID names, from its in-band CESR verkey code.
fn curve_of_aid(aid: &str) -> Result<CurveType, KeriDecodeError> {
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 {
Expand Down
13 changes: 1 addition & 12 deletions crates/auths-keri/tests/cases/keripy_interop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
21 changes: 1 addition & 20 deletions crates/auths-keri/tests/cases/keripy_kel_oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,7 @@ fn cesr_stream(parts: &[(&[u8], &[u8])]) -> Vec<u8> {
/// 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;
}

Expand Down Expand Up @@ -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<u64> {
Expand Down
36 changes: 36 additions & 0 deletions crates/auths-keri/tests/cases/keripy_oracle.rs
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions crates/auths-keri/tests/cases/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading