use ed25519_dalek::Signature;
use serde::{Deserialize, Serialize};
use super::org::{current_timestamp, OrgError, OrgId, OrgMembershipCert};
use super::org_grant::{CapabilityAuthorityId, OrgCapabilityGrant, OrgDispatcherGrant};
use crate::adapter::net::identity::{EntityId, EntityKeypair, MAX_TOKEN_CLOCK_SKEW_SECS};
pub const ORG_CALL_BINDING_CONTEXT: &str = "net-org-call-v1";
pub const ORG_ADMISSION_HEADER: &str = "net-org-admission";
pub const MAX_ORG_PROOF_TTL_SECS: u64 = 30;
pub const MAX_ORG_CALL_PROOF_BYTES: usize = 1024;
fn credential_digest(bytes: &[u8]) -> [u8; 32] {
blake3::hash(bytes).into()
}
pub struct CallBinding {
pub acting_org: OrgId,
pub caller: EntityId,
pub provider_org: OrgId,
pub callee: EntityId,
pub call_id: u64,
pub capability: CapabilityAuthorityId,
pub proof_expires_at_unix_ns: u64,
pub membership_digest: [u8; 32],
pub dispatcher_grant_digest: [u8; 32],
pub capability_grant_digest: [u8; 32],
pub request_digest: [u8; 32],
}
impl CallBinding {
fn transcript_hash(&self) -> [u8; 32] {
let mut buf = Vec::with_capacity(32 * 6 + 8 + 8 + 32);
buf.extend_from_slice(self.acting_org.as_bytes());
buf.extend_from_slice(self.caller.as_bytes());
buf.extend_from_slice(self.provider_org.as_bytes());
buf.extend_from_slice(self.callee.as_bytes());
buf.extend_from_slice(&self.call_id.to_le_bytes());
buf.extend_from_slice(self.capability.as_bytes());
buf.extend_from_slice(&self.proof_expires_at_unix_ns.to_le_bytes());
buf.extend_from_slice(&self.membership_digest);
buf.extend_from_slice(&self.dispatcher_grant_digest);
buf.extend_from_slice(&self.capability_grant_digest);
buf.extend_from_slice(&self.request_digest);
blake3::derive_key(ORG_CALL_BINDING_CONTEXT, &buf)
}
pub fn sign(&self, caller_keypair: &EntityKeypair) -> [u8; 64] {
caller_keypair.sign(&self.transcript_hash()).to_bytes()
}
pub fn verify(&self, signature: &[u8; 64]) -> Result<(), OrgError> {
let sig = Signature::from_bytes(signature);
self.caller
.verify(&self.transcript_hash(), &sig)
.map_err(|_| OrgError::InvalidSignature)
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrgCallProof {
pub caller_membership: OrgMembershipCert,
pub dispatcher_grant: OrgDispatcherGrant,
pub capability_grant: Option<OrgCapabilityGrant>,
pub proof_expires_at_unix_ns: u64,
#[serde(with = "sig_bytes")]
pub call_binding_sig: [u8; 64],
}
impl OrgCallProof {
#[allow(clippy::too_many_arguments)]
pub fn sign_for_call(
caller_keypair: &EntityKeypair,
caller_membership: OrgMembershipCert,
dispatcher_grant: OrgDispatcherGrant,
capability_grant: Option<OrgCapabilityGrant>,
acting_org: OrgId,
provider_org: OrgId,
callee: EntityId,
call_id: u64,
capability: CapabilityAuthorityId,
proof_expires_at_unix_ns: u64,
request_digest: [u8; 32],
) -> Self {
let binding = CallBinding {
acting_org,
caller: caller_keypair.entity_id().clone(),
provider_org,
callee,
call_id,
capability,
proof_expires_at_unix_ns,
membership_digest: credential_digest(&caller_membership.to_bytes()),
dispatcher_grant_digest: credential_digest(&dispatcher_grant.to_bytes()),
capability_grant_digest: capability_grant
.as_ref()
.map(|g| credential_digest(&g.to_bytes()))
.unwrap_or([0u8; 32]),
request_digest,
};
let call_binding_sig = binding.sign(caller_keypair);
Self {
caller_membership,
dispatcher_grant,
capability_grant,
proof_expires_at_unix_ns,
call_binding_sig,
}
}
pub fn binding_for_verify(
&self,
provider_org: OrgId,
callee: EntityId,
call_id: u64,
capability: CapabilityAuthorityId,
request_digest: [u8; 32],
) -> CallBinding {
CallBinding {
acting_org: self.dispatcher_grant.org_id,
caller: self.caller_membership.member.clone(),
provider_org,
callee,
call_id,
capability,
proof_expires_at_unix_ns: self.proof_expires_at_unix_ns,
membership_digest: credential_digest(&self.caller_membership.to_bytes()),
dispatcher_grant_digest: credential_digest(&self.dispatcher_grant.to_bytes()),
capability_grant_digest: self
.capability_grant
.as_ref()
.map(|g| credential_digest(&g.to_bytes()))
.unwrap_or([0u8; 32]),
request_digest,
}
}
pub fn check_expiry(&self, skew_secs: u64) -> Result<(), OrgError> {
self.check_expiry_at(current_timestamp().saturating_mul(1_000_000_000), skew_secs)
}
pub fn check_expiry_at(&self, now_ns: u64, skew_secs: u64) -> Result<(), OrgError> {
if skew_secs > MAX_TOKEN_CLOCK_SKEW_SECS {
return Err(OrgError::ClockSkewTooLarge);
}
let skew_ns = skew_secs.saturating_mul(1_000_000_000);
if now_ns >= self.proof_expires_at_unix_ns.saturating_add(skew_ns) {
return Err(OrgError::Expired);
}
let ceiling_ns = now_ns
.saturating_add(MAX_ORG_PROOF_TTL_SECS.saturating_mul(1_000_000_000))
.saturating_add(skew_ns);
if self.proof_expires_at_unix_ns > ceiling_ns {
return Err(OrgError::TtlTooLong);
}
Ok(())
}
pub fn encode(&self) -> Result<Vec<u8>, OrgError> {
let bytes = postcard::to_allocvec(self).map_err(|_| OrgError::InvalidFormat)?;
if bytes.len() > MAX_ORG_CALL_PROOF_BYTES {
return Err(OrgError::InvalidFormat);
}
Ok(bytes)
}
pub fn decode(bytes: &[u8]) -> Result<Self, OrgError> {
if bytes.len() > MAX_ORG_CALL_PROOF_BYTES {
return Err(OrgError::InvalidFormat);
}
postcard::from_bytes(bytes).map_err(|_| OrgError::InvalidFormat)
}
}
impl std::fmt::Debug for OrgCallProof {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OrgCallProof")
.field("caller", &self.caller_membership.member)
.field("acting_org", &self.dispatcher_grant.org_id)
.field("has_capability_grant", &self.capability_grant.is_some())
.field("proof_expires_at_unix_ns", &self.proof_expires_at_unix_ns)
.finish()
}
}
mod sig_bytes {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(sig: &[u8; 64], s: S) -> Result<S::Ok, S::Error> {
s.serialize_bytes(sig)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 64], D::Error> {
let v = <Vec<u8>>::deserialize(d)?;
v.as_slice()
.try_into()
.map_err(|_| serde::de::Error::custom("signature must be 64 bytes"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::net::behavior::org::OrgKeypair;
use crate::adapter::net::behavior::org_grant::{
DispatcherScope, GrantRights, GrantTargetScope,
};
fn org_a() -> OrgKeypair {
OrgKeypair::from_bytes([0x77u8; 32])
}
fn org_b() -> OrgKeypair {
OrgKeypair::from_bytes([0x42u8; 32])
}
fn caller() -> EntityKeypair {
EntityKeypair::from_bytes([0x24u8; 32])
}
fn provider() -> EntityId {
EntityId::from_bytes([0x99u8; 32])
}
fn cap() -> CapabilityAuthorityId {
CapabilityAuthorityId::for_tag("nrpc:oa2-echo")
}
fn build_cross_org_proof(call_id: u64, request_digest: [u8; 32]) -> (OrgCallProof, u64) {
let caller = caller();
let membership =
OrgMembershipCert::try_issue(&org_a(), caller.entity_id().clone(), 1, 3600)
.expect("cert");
let dispatcher = OrgDispatcherGrant::try_issue(
&org_a(),
caller.entity_id().clone(),
DispatcherScope::Exact(cap()),
3600,
)
.expect("dispatcher");
let (capability_grant, _secret) = OrgCapabilityGrant::try_issue(
&org_b(),
org_a().org_id(),
cap(),
GrantRights::INVOKE,
GrantTargetScope::ExactNode(provider()),
3600,
)
.expect("cap grant");
let expiry = (current_timestamp() + 20) * 1_000_000_000;
let proof = OrgCallProof::sign_for_call(
&caller,
membership,
dispatcher,
Some(capability_grant),
org_a().org_id(),
org_b().org_id(),
provider(),
call_id,
cap(),
expiry,
request_digest,
);
(proof, expiry)
}
#[test]
fn proof_binds_and_verifies_against_the_exact_call() {
let digest = [0x11u8; 32];
let (proof, _) = build_cross_org_proof(42, digest);
let binding = proof.binding_for_verify(org_b().org_id(), provider(), 42, cap(), digest);
binding
.verify(&proof.call_binding_sig)
.expect("binding verifies for the exact call");
proof.check_expiry(0).expect("live");
}
#[test]
fn encoded_call_proof_carries_no_discovery_key() {
use crate::adapter::net::behavior::org_grant::audience_key_commitment;
let caller = caller();
let membership =
OrgMembershipCert::try_issue(&org_a(), caller.entity_id().clone(), 1, 3600)
.expect("cert");
let dispatcher = OrgDispatcherGrant::try_issue(
&org_a(),
caller.entity_id().clone(),
DispatcherScope::Exact(cap()),
3600,
)
.expect("dispatcher");
let (capability_grant, secret) = OrgCapabilityGrant::try_issue(
&org_b(),
org_a().org_id(),
cap(),
GrantRights::INVOKE.union(GrantRights::DISCOVER),
GrantTargetScope::ExactNode(provider()),
3600,
)
.expect("discover cap grant");
let secret = secret.expect("a DISCOVER grant mints an audience secret");
let discovery_key = *secret.discovery_key();
let expiry = (current_timestamp() + 20) * 1_000_000_000;
let proof = OrgCallProof::sign_for_call(
&caller,
membership,
dispatcher,
Some(capability_grant),
org_a().org_id(),
org_b().org_id(),
provider(),
7,
cap(),
expiry,
[0x11u8; 32],
);
let encoded = proof.encode().expect("encode proof");
assert!(
!encoded.windows(32).any(|w| w == discovery_key),
"the raw discovery key leaked into the encoded call proof",
);
let commitment = audience_key_commitment(&discovery_key);
assert!(
encoded.windows(32).any(|w| w == commitment),
"the grant's discovery-key commitment should ride in the proof",
);
}
#[test]
fn binding_transplant_matrix_every_bound_field_is_load_bearing() {
let digest = [0x11u8; 32];
let (proof, _) = build_cross_org_proof(42, digest);
assert!(proof
.binding_for_verify(org_b().org_id(), provider(), 43, cap(), digest)
.verify(&proof.call_binding_sig)
.is_err());
assert!(proof
.binding_for_verify(
org_b().org_id(),
EntityId::from_bytes([7u8; 32]),
42,
cap(),
digest
)
.verify(&proof.call_binding_sig)
.is_err());
assert!(proof
.binding_for_verify(org_a().org_id(), provider(), 42, cap(), digest)
.verify(&proof.call_binding_sig)
.is_err());
assert!(proof
.binding_for_verify(org_b().org_id(), provider(), 42, cap(), [0x22u8; 32])
.verify(&proof.call_binding_sig)
.is_err());
}
#[test]
fn tampering_a_carried_credential_breaks_the_binding() {
let digest = [0x11u8; 32];
let (mut proof, _) = build_cross_org_proof(42, digest);
let (other_grant, _) = OrgCapabilityGrant::try_issue(
&org_b(),
org_a().org_id(),
cap(),
GrantRights::INVOKE,
GrantTargetScope::AnyNodeOwnedBy(org_b().org_id()),
3600,
)
.expect("other grant");
proof.capability_grant = Some(other_grant);
assert!(proof
.binding_for_verify(org_b().org_id(), provider(), 42, cap(), digest)
.verify(&proof.call_binding_sig)
.is_err());
}
#[test]
fn wrong_caller_key_never_verifies() {
let digest = [0x11u8; 32];
let (proof, expiry) = build_cross_org_proof(42, digest);
let attacker = EntityKeypair::from_bytes([0xEEu8; 32]);
let forged_binding = CallBinding {
acting_org: org_a().org_id(),
caller: proof.caller_membership.member.clone(),
provider_org: org_b().org_id(),
callee: provider(),
call_id: 42,
capability: cap(),
proof_expires_at_unix_ns: expiry,
membership_digest: credential_digest(&proof.caller_membership.to_bytes()),
dispatcher_grant_digest: credential_digest(&proof.dispatcher_grant.to_bytes()),
capability_grant_digest: proof
.capability_grant
.as_ref()
.map(|g| credential_digest(&g.to_bytes()))
.unwrap(),
request_digest: digest,
};
let forged_sig = attacker.sign(&forged_binding.transcript_hash()).to_bytes();
assert!(forged_binding.verify(&forged_sig).is_err(), "wrong key");
}
#[test]
fn expiry_ceiling_and_expired_are_refused() {
let caller = caller();
let membership =
OrgMembershipCert::try_issue(&org_a(), caller.entity_id().clone(), 1, 3600)
.expect("cert");
let dispatcher = OrgDispatcherGrant::try_issue(
&org_a(),
caller.entity_id().clone(),
DispatcherScope::Any,
3600,
)
.expect("dispatcher");
let mk = |expiry_ns: u64| {
OrgCallProof::sign_for_call(
&caller,
membership.clone(),
dispatcher.clone(),
None,
org_a().org_id(),
org_b().org_id(),
provider(),
1,
cap(),
expiry_ns,
[0u8; 32],
)
};
let past = (current_timestamp().saturating_sub(10)) * 1_000_000_000;
assert!(matches!(mk(past).check_expiry(0), Err(OrgError::Expired)));
let far = (current_timestamp() + MAX_ORG_PROOF_TTL_SECS + 60) * 1_000_000_000;
assert!(matches!(mk(far).check_expiry(0), Err(OrgError::TtlTooLong)));
let ok = (current_timestamp() + 5) * 1_000_000_000;
mk(ok).check_expiry(0).expect("live");
assert!(matches!(
mk(ok).check_expiry(MAX_TOKEN_CLOCK_SKEW_SECS + 1),
Err(OrgError::ClockSkewTooLarge)
));
}
#[test]
fn proof_codec_roundtrips_and_stays_under_the_header_cap() {
let (proof, _) = build_cross_org_proof(42, [0x11u8; 32]);
let bytes = proof.encode().expect("encode");
assert!(
bytes.len() <= MAX_ORG_CALL_PROOF_BYTES,
"encoded proof {} exceeds cap",
bytes.len()
);
assert!(bytes.len() < 4096);
let decoded = OrgCallProof::decode(&bytes).expect("decode");
assert_eq!(decoded, proof);
decoded
.binding_for_verify(org_b().org_id(), provider(), 42, cap(), [0x11u8; 32])
.verify(&decoded.call_binding_sig)
.expect("decoded verifies");
}
#[test]
fn same_org_proof_carries_no_capability_grant() {
let caller = caller();
let membership =
OrgMembershipCert::try_issue(&org_a(), caller.entity_id().clone(), 1, 3600)
.expect("cert");
let dispatcher = OrgDispatcherGrant::try_issue(
&org_a(),
caller.entity_id().clone(),
DispatcherScope::Exact(cap()),
3600,
)
.expect("dispatcher");
let expiry = (current_timestamp() + 10) * 1_000_000_000;
let proof = OrgCallProof::sign_for_call(
&caller,
membership,
dispatcher,
None,
org_a().org_id(),
org_a().org_id(), provider(),
7,
cap(),
expiry,
[0u8; 32],
);
assert!(proof.capability_grant.is_none());
proof
.binding_for_verify(org_a().org_id(), provider(), 7, cap(), [0u8; 32])
.verify(&proof.call_binding_sig)
.expect("same-org binding verifies");
let bytes = proof.encode().expect("encode");
assert_eq!(OrgCallProof::decode(&bytes).expect("decode"), proof);
}
#[test]
fn decode_refuses_oversized_input() {
assert!(matches!(
OrgCallProof::decode(&vec![0u8; MAX_ORG_CALL_PROOF_BYTES + 1]),
Err(OrgError::InvalidFormat)
));
}
}