cultnet-rs: example generating Rust-owned Idunn runtime fixtures
Real enrolled identities, a validated Expected, an Activation issued by IdunnRuntimeActivationLaunch, bundle files through the CultCache store, and runtime-presence vectors signed by the Rust signers and run through authenticate/correlate before they are written. cultnet-ts tests will commit the output. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
de57d8dc9c
commit
0e7d2204cd
1 changed files with 276 additions and 0 deletions
276
packages/cultnet-rs/examples/idunn_runtime_fixture.rs
Normal file
276
packages/cultnet-rs/examples/idunn_runtime_fixture.rs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
//! Generate the Idunn runtime fixtures that cultnet-ts is tested against.
|
||||
//!
|
||||
//! The Rust owner builds every input: real enrolled identities, an Expected that
|
||||
//! passes `validate`, an Activation issued by `IdunnRuntimeActivationLaunch`, the
|
||||
//! bundle files written through `SingleFileMessagePackBackingStore`, and
|
||||
//! runtime-presence vectors signed by the Rust signers. Each vector is also run
|
||||
//! through `authenticate_runtime_presence_claim` and
|
||||
//! `correlate_runtime_presence_claim` before it is written, so the bytes
|
||||
//! committed to the TypeScript tests are ones this verifier accepted (or, for
|
||||
//! the low-capacity vector, ones it correlates to the typed capacity
|
||||
//! disagreement). Ed25519 is deterministic, so the TypeScript signer must
|
||||
//! reproduce each vector byte for byte from the same fields.
|
||||
//!
|
||||
//! Linux only: the provider identity is bound to `/etc/machine-id`, and the
|
||||
//! generated `machine-id` file records the value the identities were bound to.
|
||||
//!
|
||||
//! Usage: idunn_runtime_fixture <out-dir>
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Result, bail, ensure};
|
||||
use cultcache_rs::{
|
||||
CacheBackingStore, CultCacheEnvelope, DatabaseEntry, SingleFileMessagePackBackingStore,
|
||||
};
|
||||
use cultnet_rs::{
|
||||
GameCultProviderHealthIdentity, GameCultRuntimeCapability,
|
||||
GameCultRuntimePresenceHealthPurpose, GameCultRuntimePresenceHealthRecord,
|
||||
IDUNN_EXPECTED_INCARNATION_SCHEMA, IDUNN_RUNTIME_ACTIVATION_SCHEMA,
|
||||
IdunnExpectedCapability, IdunnExpectedDependency, IdunnExpectedIncarnationRecord,
|
||||
IdunnExpectedRoute, IdunnRuntimeActivationLaunch, IdunnRuntimeActivationRecord,
|
||||
IdunnRuntimeActivationSigner, IdunnServiceIdentity, RuntimePresenceAuthenticationContext,
|
||||
authenticate_runtime_presence_claim, correlate_runtime_presence_claim,
|
||||
enroll_service_identity_at, verify_runtime_authority,
|
||||
};
|
||||
|
||||
/// The TypeScript tests pin the same values; drift shows up as a byte mismatch.
|
||||
const OBSERVED_AT: u64 = 1_790_000_000_000;
|
||||
const ISSUED_AT: u64 = OBSERVED_AT - 5_000;
|
||||
const SEQUENCE: u64 = 1;
|
||||
const DETAIL: &str = "route-observation:route-challenge-1";
|
||||
const STORED_AT: &str = "2026-01-01T00:00:00.000Z";
|
||||
|
||||
struct Set {
|
||||
dir: &'static str,
|
||||
target: &'static str,
|
||||
transport: &'static str,
|
||||
stable: &'static str,
|
||||
candidate: &'static str,
|
||||
minimum_capacity: u32,
|
||||
odin_dependency: bool,
|
||||
}
|
||||
|
||||
const SETS: [Set; 3] = [
|
||||
Set {
|
||||
dir: "web",
|
||||
target: "streampixels-web",
|
||||
transport: "http",
|
||||
stable: "http://127.0.0.1:8830",
|
||||
candidate: "http://127.0.0.1:18830",
|
||||
minimum_capacity: 1,
|
||||
odin_dependency: false,
|
||||
},
|
||||
Set {
|
||||
dir: "service",
|
||||
target: "streampixels-service",
|
||||
transport: "tcp",
|
||||
stable: "tcp://127.0.0.1:8831",
|
||||
candidate: "tcp://127.0.0.1:18831",
|
||||
minimum_capacity: 2,
|
||||
odin_dependency: true,
|
||||
},
|
||||
Set {
|
||||
dir: "rudp-route",
|
||||
target: "streampixels-rudp",
|
||||
transport: "rudp",
|
||||
stable: "rudp://127.0.0.1:8832",
|
||||
candidate: "rudp://127.0.0.1:18832",
|
||||
minimum_capacity: 1,
|
||||
odin_dependency: false,
|
||||
},
|
||||
];
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let Some(out) = std::env::args().nth(1) else {
|
||||
bail!("usage: idunn_runtime_fixture <out-dir>");
|
||||
};
|
||||
let machine_id = fs::read_to_string("/etc/machine-id")?;
|
||||
fs::create_dir_all(&out)?;
|
||||
fs::write(Path::new(&out).join("machine-id"), machine_id.trim())?;
|
||||
let idunn = enroll_service_identity_at::<IdunnServiceIdentity>(
|
||||
&Path::new(&out).join("idunn-identity.private"),
|
||||
)?;
|
||||
let idunn_anchor = idunn.trust_anchor()?;
|
||||
fs::remove_file(Path::new(&out).join("idunn-identity.private"))?;
|
||||
let _ = fs::remove_file(Path::new(&out).join("idunn-identity.private.lock"));
|
||||
for set in &SETS {
|
||||
build(Path::new(&out).join(set.dir).as_path(), set, &idunn, &idunn_anchor)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn digest(byte: char) -> String {
|
||||
format!("sha256-{}", byte.to_string().repeat(64))
|
||||
}
|
||||
|
||||
fn build(
|
||||
dir: &Path,
|
||||
set: &Set,
|
||||
idunn: &cultnet_rs::ServiceIdentitySigner<IdunnServiceIdentity>,
|
||||
idunn_anchor: &cultnet_rs::ServiceIdentityTrustAnchor,
|
||||
) -> Result<()> {
|
||||
fs::create_dir_all(dir)?;
|
||||
let provider_path = dir.join("provider-identity.credential");
|
||||
let provider = enroll_service_identity_at::<GameCultProviderHealthIdentity>(&provider_path)?;
|
||||
let provider_anchor = provider.trust_anchor()?;
|
||||
let _ = fs::remove_file(dir.join("provider-identity.credential.lock"));
|
||||
|
||||
let capability = format!("{}.api", set.target);
|
||||
let capability_schema = format!("{}.api.v1", set.target);
|
||||
let expected = IdunnExpectedIncarnationRecord {
|
||||
schema_version: IDUNN_EXPECTED_INCARNATION_SCHEMA.into(),
|
||||
target: set.target.into(),
|
||||
plan_id: digest('1'),
|
||||
incarnation_id: format!("{}/incarnation-1", set.target),
|
||||
sealed_release_id: digest('2'),
|
||||
source_repository: "github.com/GameCult/StreamPixels".into(),
|
||||
source_revision: "a".repeat(40),
|
||||
recipe_sha256: digest('3'),
|
||||
runtime_id: format!("{}-yggdrasil", set.target),
|
||||
expected_signer_identity_id: provider_anchor.identity_id.clone(),
|
||||
health_contract: format!("{}.runtime-health", set.target),
|
||||
artifact_sha256: digest('4'),
|
||||
state_schema_generation: None,
|
||||
state_contract_sha256: None,
|
||||
write_lease_required: false,
|
||||
route: Some(IdunnExpectedRoute {
|
||||
route_id: format!("{}-route", set.target),
|
||||
transport: set.transport.into(),
|
||||
stable_endpoint: set.stable.into(),
|
||||
candidate_endpoint: set.candidate.into(),
|
||||
}),
|
||||
capabilities: vec![IdunnExpectedCapability {
|
||||
capability: capability.clone(),
|
||||
schema: capability_schema.clone(),
|
||||
compatibility: "v1".into(),
|
||||
minimum_capacity: set.minimum_capacity,
|
||||
}],
|
||||
dependencies: if set.odin_dependency {
|
||||
vec![IdunnExpectedDependency {
|
||||
kind: "shared-infrastructure".into(),
|
||||
capability: "odin.verse-rendezvous".into(),
|
||||
schema: "odin.verse-topology.v1".into(),
|
||||
compatibility: "v1".into(),
|
||||
minimum_capacity: 1,
|
||||
startup: "before-promotion".into(),
|
||||
provider_id: Some("odin".into()),
|
||||
provider_authority: Some("managed-incarnation".into()),
|
||||
provider_expected_projection_sha256: Some(digest('5')),
|
||||
provider_endpoint: Some("rudp://127.0.0.1:17871".into()),
|
||||
}]
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
};
|
||||
expected.validate()?;
|
||||
|
||||
let launch = IdunnRuntimeActivationLaunch::issue(&expected, digest('6'), ISSUED_AT, idunn)?;
|
||||
let mut credential = Vec::new();
|
||||
let activation = launch.write_credential(&mut credential)?;
|
||||
ensure!(credential.len() == 32, "activation credential is a 32-byte seed");
|
||||
fs::write(dir.join("activation-key.seed"), &credential)?;
|
||||
let activation_signer = IdunnRuntimeActivationSigner::from_credential_reader(&credential[..])?;
|
||||
|
||||
write_record(
|
||||
&dir.join("expected.cc"),
|
||||
&expected.target,
|
||||
IdunnExpectedIncarnationRecord::TYPE,
|
||||
IDUNN_EXPECTED_INCARNATION_SCHEMA,
|
||||
expected.canonical_bytes()?,
|
||||
)?;
|
||||
write_record(
|
||||
&dir.join("activation.cc"),
|
||||
&expected.target,
|
||||
IdunnRuntimeActivationRecord::TYPE,
|
||||
IDUNN_RUNTIME_ACTIVATION_SCHEMA,
|
||||
activation.canonical_bytes()?,
|
||||
)?;
|
||||
|
||||
let authority =
|
||||
verify_runtime_authority(&expected, &activation, idunn_anchor, &provider_anchor.public_key)?;
|
||||
let vectors = [
|
||||
("presence-active.bin", "active", set.minimum_capacity, None),
|
||||
("presence-warming.bin", "warming", set.minimum_capacity, None),
|
||||
(
|
||||
"presence-active-below-minimum.bin",
|
||||
"active",
|
||||
set.minimum_capacity.saturating_sub(1),
|
||||
Some("expected-capability-000-capacity"),
|
||||
),
|
||||
];
|
||||
for (name, state, capacity, disagreement) in vectors {
|
||||
if capacity == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut presence = GameCultRuntimePresenceHealthRecord {
|
||||
schema_version: cultnet_rs::GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA.into(),
|
||||
target: expected.target.clone(),
|
||||
expected_projection_sha256: activation.expected_projection_sha256.clone(),
|
||||
plan_id: expected.plan_id.clone(),
|
||||
incarnation_id: expected.incarnation_id.clone(),
|
||||
sealed_release_id: expected.sealed_release_id.clone(),
|
||||
activation_witness_sha256: activation.canonical_sha256()?,
|
||||
state_schema_generation: None,
|
||||
state_contract_sha256: None,
|
||||
runtime_id: expected.runtime_id.clone(),
|
||||
runtime_instance_id: activation.runtime_instance_id.clone(),
|
||||
bound_endpoint: Some(set.candidate.into()),
|
||||
capabilities: vec![GameCultRuntimeCapability {
|
||||
capability: capability.clone(),
|
||||
schema: capability_schema.clone(),
|
||||
compatibility: "v1".into(),
|
||||
capacity,
|
||||
}],
|
||||
health_contract: expected.health_contract.clone(),
|
||||
state: state.into(),
|
||||
detail: DETAIL.into(),
|
||||
write_lease_sha256: None,
|
||||
signer_identity_id: provider_anchor.identity_id.clone(),
|
||||
publisher_sequence: SEQUENCE,
|
||||
observed_at_unix_millis: OBSERVED_AT,
|
||||
signature_algorithm: "ed25519".into(),
|
||||
signature: Vec::new(),
|
||||
activation_signer_identity_id: activation_signer.identity_id(),
|
||||
activation_signature: Vec::new(),
|
||||
};
|
||||
let proof = presence.canonical_proof_payload()?;
|
||||
presence.signature = provider
|
||||
.sign::<GameCultRuntimePresenceHealthPurpose>(&proof)
|
||||
.signature;
|
||||
presence.activation_signature = activation_signer.sign_presence_proof(&presence)?;
|
||||
let bytes = rmp_serde::to_vec(&presence)?;
|
||||
|
||||
let claim = authenticate_runtime_presence_claim(
|
||||
&bytes,
|
||||
&authority,
|
||||
RuntimePresenceAuthenticationContext {
|
||||
trusted_received_at_unix_millis: OBSERVED_AT + 1_000,
|
||||
maximum_age_millis: 30_000,
|
||||
maximum_future_skew_millis: 0,
|
||||
},
|
||||
)?;
|
||||
let correlation = correlate_runtime_presence_claim(claim, &authority)?;
|
||||
let codes: Vec<&str> = correlation
|
||||
.disagreements()
|
||||
.iter()
|
||||
.map(|entry| entry.code.as_str())
|
||||
.collect();
|
||||
match disagreement {
|
||||
None => ensure!(codes.is_empty(), "{name}: unexpected disagreements {codes:?}"),
|
||||
Some(code) => ensure!(codes == [code], "{name}: expected only {code}, got {codes:?}"),
|
||||
}
|
||||
fs::write(dir.join(name), bytes)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_record(path: &Path, key: &str, kind: &str, schema: &str, payload: Vec<u8>) -> Result<()> {
|
||||
SingleFileMessagePackBackingStore::new(path).push(&CultCacheEnvelope {
|
||||
key: key.into(),
|
||||
r#type: kind.into(),
|
||||
payload,
|
||||
stored_at: STORED_AT.into(),
|
||||
schema_id: Some(schema.into()),
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue