pub mod client;
pub use client::{Client, HubError};
pub use nucleus_substrate_core::{Projection, Receipt, ReceiptError, Session};
pub use nucleus_identity_projection::{
IdentityBody, IdentityVerifyError, JwtSvidClaims, identity_projection,
verify_identity_projection,
};
pub use nucleus_flow_projection::{
FlowBody, FlowVerifyError, flow_projection, verify_flow_projection_shape,
};
pub use nucleus_mechanism_vcg::{
EconomicBody, EconomicVerifyError, vickrey_projection,
vcg_knapsack_projection, verify_economic_projection_shape,
VickreyBody, VickreyOutcome, VcgKnapsackBody, VcgKnapsackOutcome,
};
pub use nucleus_substrate_core::mechanism::vcg::{
AgentBid, ExternalityProfile, MatchResult, OpaqueSignedClaim,
PostedAuction, ResourceDim, VcgMatchResult,
};
#[derive(Debug, Clone)]
pub struct VerifyReport {
pub projection_kinds: Vec<String>,
pub identity_subject: Option<String>,
pub flow_clean: bool,
pub has_adversarial_bid: bool,
}
pub fn verify_receipt_fully(
receipt: &Receipt,
jwks: &serde_json::Value,
) -> Result<VerifyReport, SubstrateVerifyError> {
let vk_bytes = extract_ed25519_vk(jwks, &receipt.session.issuer_kid).ok_or_else(|| {
SubstrateVerifyError::JwksMissingKid(receipt.session.issuer_kid.clone())
})?;
receipt
.verify(&vk_bytes)
.map_err(SubstrateVerifyError::Receipt)?;
let mut projection_kinds = Vec::new();
let mut identity_subject = None;
let mut flow_clean = false;
let mut has_adversarial_bid = false;
for p in &receipt.projections {
projection_kinds.push(p.kind().to_string());
match p {
Projection::Identity(body) => {
let typed: IdentityBody = serde_json::from_value(body.clone())
.map_err(|e| SubstrateVerifyError::ProjectionParse {
kind: "identity",
error: e.to_string(),
})?;
let claims = verify_identity_projection(&typed, jwks)
.map_err(SubstrateVerifyError::Identity)?;
identity_subject = Some(claims.claims.sub);
}
Projection::Flow(body) => {
let typed: FlowBody = serde_json::from_value(body.clone()).map_err(|e| {
SubstrateVerifyError::ProjectionParse {
kind: "flow",
error: e.to_string(),
}
})?;
verify_flow_projection_shape(&typed)
.map_err(SubstrateVerifyError::Flow)?;
flow_clean = true;
if typed.has_adversarial_bid {
has_adversarial_bid = true;
}
}
Projection::Economic(body) => {
verify_economic_projection_shape(body)
.map_err(SubstrateVerifyError::Economic)?;
}
Projection::Capability(_) => {
}
_ => {
}
}
}
Ok(VerifyReport {
projection_kinds,
identity_subject,
flow_clean,
has_adversarial_bid,
})
}
#[derive(Debug, thiserror::Error)]
pub enum SubstrateVerifyError {
#[error("JWKS missing key with kid {0}")]
JwksMissingKid(String),
#[error("receipt signature/hash check failed: {0}")]
Receipt(ReceiptError),
#[error("identity projection failed: {0}")]
Identity(IdentityVerifyError),
#[error("flow projection failed: {0}")]
Flow(FlowVerifyError),
#[error("economic projection failed: {0}")]
Economic(EconomicVerifyError),
#[error("could not parse {kind} projection body: {error}")]
ProjectionParse {
kind: &'static str,
error: String,
},
}
fn extract_ed25519_vk(jwks: &serde_json::Value, kid: &str) -> Option<[u8; 32]> {
use base64::Engine;
let keys = jwks.get("keys")?.as_array()?;
for k in keys {
if k.get("kid")?.as_str()? == kid
&& k.get("kty")?.as_str()? == "OKP"
&& k.get("crv")?.as_str()? == "Ed25519"
{
let x = k.get("x")?.as_str()?;
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(x)
.ok()?;
return bytes.try_into().ok();
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
fn build_jwks(sk: &SigningKey, kid: &str) -> serde_json::Value {
use base64::Engine;
let vk_bytes = sk.verifying_key().to_bytes();
serde_json::json!({
"keys": [{
"kty": "OKP",
"crv": "Ed25519",
"kid": kid,
"alg": "EdDSA",
"x": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&vk_bytes),
}]
})
}
#[test]
fn verify_receipt_fully_walks_flow_and_economic() {
let sk = SigningKey::from_bytes(&[5u8; 32]);
let session = Session {
session_id: "spiffe://test/agent".into(),
issuer_kid: "kid-1".into(),
issued_at_micros: 1_717_000_000_000_000,
parent_chain: vec![],
};
let flow = flow_projection(
3,
"internal",
"trusted",
"informational",
"user_derived",
false,
false,
true,
);
let auction = PostedAuction {
auction_id: "a1".into(),
required_capabilities: Default::default(),
reward_micro_usd: 1_000_000,
pigouvian_rates: vec![],
scale: 1_000_000,
};
let bid = AgentBid {
agent_spiffe_id: "spiffe://test/agent".into(),
auction_id: "a1".into(),
effective_value_micro_usd: 500_000,
externality_profile: None,
};
let mr = MatchResult {
auction_id: "a1".into(),
winner_spiffe_id: Some("spiffe://test/agent".into()),
clearing_price_micro_usd: 250_000,
};
let economic = vickrey_projection(auction, vec![bid], mr);
let receipt = Receipt::sign(session, vec![flow, economic], &sk);
let jwks = build_jwks(&sk, "kid-1");
let report = verify_receipt_fully(&receipt, &jwks).expect("happy path");
assert_eq!(report.projection_kinds, vec!["flow", "economic"]);
assert!(report.flow_clean);
assert!(!report.has_adversarial_bid);
}
#[test]
fn tampered_receipt_fails_top_level_verify() {
let sk = SigningKey::from_bytes(&[5u8; 32]);
let session = Session {
session_id: "spiffe://test/agent".into(),
issuer_kid: "kid-1".into(),
issued_at_micros: 1_717_000_000_000_000,
parent_chain: vec![],
};
let mut receipt = Receipt::sign(session, vec![], &sk);
receipt.session.session_id = "spiffe://attacker".into();
let jwks = build_jwks(&sk, "kid-1");
let err = verify_receipt_fully(&receipt, &jwks).unwrap_err();
assert!(matches!(err, SubstrateVerifyError::Receipt(_)));
}
#[test]
fn missing_kid_in_jwks_short_circuits() {
let sk = SigningKey::from_bytes(&[5u8; 32]);
let session = Session {
session_id: "spiffe://test/agent".into(),
issuer_kid: "kid-1".into(),
issued_at_micros: 1_717_000_000_000_000,
parent_chain: vec![],
};
let receipt = Receipt::sign(session, vec![], &sk);
let empty_jwks = serde_json::json!({"keys": []});
let err = verify_receipt_fully(&receipt, &empty_jwks).unwrap_err();
assert!(matches!(err, SubstrateVerifyError::JwksMissingKid(_)));
}
#[test]
fn adversarial_flow_propagates_to_report() {
let sk = SigningKey::from_bytes(&[5u8; 32]);
let session = Session {
session_id: "spiffe://test/agent".into(),
issuer_kid: "kid-1".into(),
issued_at_micros: 1_717_000_000_000_000,
parent_chain: vec![],
};
let flow = flow_projection(
2,
"internal",
"adversarial",
"informational",
"user_derived",
true,
false,
true,
);
let receipt = Receipt::sign(session, vec![flow], &sk);
let jwks = build_jwks(&sk, "kid-1");
let report = verify_receipt_fully(&receipt, &jwks).expect("happy path");
assert!(report.has_adversarial_bid);
}
}