1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! End-to-end fixture test for the CellOS authority-derivation pipeline.
//!
//! Exercises the full operator-facing path against committed example files:
//!
//! 1. JSON spec → `serde_json` → `ExecutionCellDocument` (schema validation)
//! 2. Keys file → `serde_json` → `HashMap<String,String>` (mirrors
//! `cellos_supervisor::composition::load_authority_keys`'s on-disk format)
//! 3. `verify_authority_derivation(&spec, &token, &keys)` (ED25519 verify)
//! 4. Tamper the signature byte-for-byte and re-verify (negative path)
//!
//! The fixture files live at:
//! - `contracts/examples/execution-cell-ci-runner-signed.valid.json`
//! - `contracts/examples/authority-keys.example.json`
//!
//! The signature embedded in the spec is generated deterministically from
//! seed `[0x42; 32]` by the `gen_signed_ci_runner_fixture` test in
//! `cellos-core::spec_validation::tests`. If the leaf capability, role root,
//! or parentRunId in the fixture changes, regenerate with:
//!
//! ```text
//! cargo test -p cellos-core --lib gen_signed_ci_runner_fixture \
//! -- --ignored --nocapture
//! ```
//!
//! Note on the keys-file loader: `load_authority_keys` is `pub(crate)` in
//! `cellos-supervisor::composition`, so this integration test cannot call it
//! directly. It instead deserializes the file with the same JSON shape the
//! loader expects (`HashMap<String, String>` of role-id → base64 verifying
//! key) — proving the operator-facing fixture format is correct.
use std::collections::HashMap;
use std::path::PathBuf;
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
use cellos_core::spec_validation::{validate_execution_cell_document, verify_authority_derivation};
use cellos_core::{AuthorityDerivationToken, ExecutionCellDocument};
/// Resolve `contracts/examples/<name>` from the workspace root, regardless of
/// whether tests run from the crate dir or workspace dir.
fn fixture_path(name: &str) -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest_dir
.parent()
.and_then(|p| p.parent())
.expect("cellos-supervisor lives under <workspace>/crates/");
workspace_root.join("contracts").join("examples").join(name)
}
fn load_signed_fixture() -> (
ExecutionCellDocument,
AuthorityDerivationToken,
HashMap<String, String>,
) {
let spec_path = fixture_path("execution-cell-ci-runner-signed.valid.json");
let keys_path = fixture_path("authority-keys.example.json");
let spec_raw = std::fs::read_to_string(&spec_path)
.unwrap_or_else(|e| panic!("read fixture {}: {e}", spec_path.display()));
let doc: ExecutionCellDocument = serde_json::from_str(&spec_raw)
.unwrap_or_else(|e| panic!("parse fixture {}: {e}", spec_path.display()));
// Schema validation must pass — the fixture is asserted "valid" by name.
validate_execution_cell_document(&doc).expect("fixture must pass schema validation");
let token = doc
.spec
.authority
.authority_derivation
.clone()
.expect("fixture spec must carry an authorityDerivation token");
let keys_raw = std::fs::read_to_string(&keys_path)
.unwrap_or_else(|e| panic!("read keys file {}: {e}", keys_path.display()));
// Same JSON shape that load_authority_keys consumes (HashMap<role,b64>).
let keys: HashMap<String, String> = serde_json::from_str(&keys_raw)
.unwrap_or_else(|e| panic!("parse keys file {}: {e}", keys_path.display()));
(doc, token, keys)
}
#[test]
fn signed_ci_runner_fixture_verifies_end_to_end() {
let (doc, token, keys) = load_signed_fixture();
// Sanity-check the fixture wiring before crypto: the role in the token
// must be present in the keys file, otherwise the failure below would
// be ambiguous (unknown-role vs. bad-signature).
let role = token.role_root.to_string();
assert!(
keys.contains_key(&role),
"keys file must publish the verifying key for {role}"
);
verify_authority_derivation(&doc.spec, &token, &keys)
.expect("signed fixture must verify against the published verifying key");
}
#[test]
fn signed_ci_runner_fixture_rejects_tampered_signature() {
let (doc, mut token, keys) = load_signed_fixture();
// Flip the last byte of the raw signature (post-base64-decode) and
// re-encode. ED25519 `verify_strict` must reject this.
let mut sig_bytes = STANDARD
.decode(token.grantor_signature.bytes.as_bytes())
.expect("fixture signature is base64");
let last = sig_bytes.len() - 1;
sig_bytes[last] ^= 0x01;
token.grantor_signature.bytes = STANDARD.encode(&sig_bytes);
let err = verify_authority_derivation(&doc.spec, &token, &keys)
.expect_err("tampered signature must not verify");
// The negative path must surface as InvalidSpec("authority derivation
// signature invalid") — NOT a structural rejection (subset check) and
// NOT an unknown-role rejection. Asserting on the exact variant + the
// exact message catches both wrong-error-type regressions and silent
// re-routing of crypto failures into structural ones.
match err {
cellos_core::error::CellosError::InvalidSpec(msg) => {
assert!(
msg.contains("authority derivation signature invalid"),
"expected signature-invalid message, got: {msg}"
);
}
other => panic!("expected CellosError::InvalidSpec, got {other:?}"),
}
}