use super::TrustSet;
use crate::crypto::{EventVerificationStatus, verify_ed25519_strict};
use crate::error::Result;
use crate::session::event::{
EventSignatureRecordedPayload, EventToBeSigned, ShoreEvent,
event_signature_pre_authentication_encoding,
};
const ED25519_SIGNATURE_ALG: &str = "ed25519";
const EVENT_SIGNATURE_VERSION: u32 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CosignatureVerification {
Attested(EventVerificationStatus),
BindingMismatch,
}
pub fn verify_cosignature(
carrier: &EventSignatureRecordedPayload,
target: &ShoreEvent,
trust: &TrustSet,
) -> Result<CosignatureVerification> {
if target.event_record_hash()? != carrier.target_event_record_hash {
return Ok(CosignatureVerification::BindingMismatch);
}
if carrier.attestation.alg != ED25519_SIGNATURE_ALG
|| carrier.attestation.sig_version != EVENT_SIGNATURE_VERSION
{
return Ok(CosignatureVerification::Attested(
EventVerificationStatus::Invalid,
));
}
let tbs = match EventToBeSigned::from_event(target, &carrier.attesting_signer) {
Ok(tbs) => tbs,
Err(_) => {
return Ok(CosignatureVerification::Attested(
EventVerificationStatus::Invalid,
));
}
};
let message = match event_signature_pre_authentication_encoding(&tbs) {
Ok(message) => message,
Err(_) => {
return Ok(CosignatureVerification::Attested(
EventVerificationStatus::Invalid,
));
}
};
let status = verify_ed25519_strict(
&carrier.attesting_signer,
&message,
carrier.attestation.sig.as_str(),
)?;
if status != EventVerificationStatus::Valid {
return Ok(CosignatureVerification::Attested(status));
}
if trust.authorizes(
&target.writer.actor_id,
&carrier.attesting_signer,
target.occurred_at.as_str(),
) {
Ok(CosignatureVerification::Attested(
EventVerificationStatus::Valid,
))
} else {
Ok(CosignatureVerification::Attested(
EventVerificationStatus::UntrustedKey,
))
}
}
pub const COSIGNATURE_INVALID_CODE: &str = "cosignature_invalid";
pub const COSIGNATURE_TARGET_PENDING_CODE: &str = "cosignature_target_pending";
pub const COSIGNATURE_BINDING_MISMATCH_CODE: &str = "cosignature_binding_mismatch";
pub const COSIGNATURE_UNTRUSTED_SIGNER_CODE: &str = "cosignature_untrusted_signer";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CosignatureGateDecision {
Store(EventVerificationStatus),
DropInvalid,
TargetPending,
BindingMismatch,
}
impl CosignatureGateDecision {
pub fn stores(&self) -> bool {
matches!(self, Self::Store(_))
}
pub fn drop_diagnostic_code(&self) -> Option<&'static str> {
match self {
Self::Store(_) => None,
Self::DropInvalid => Some(COSIGNATURE_INVALID_CODE),
Self::TargetPending => Some(COSIGNATURE_TARGET_PENDING_CODE),
Self::BindingMismatch => Some(COSIGNATURE_BINDING_MISMATCH_CODE),
}
}
}
pub fn gate_cosignature_for_store(
carrier: &EventSignatureRecordedPayload,
target: Option<&ShoreEvent>,
trust: &TrustSet,
) -> Result<CosignatureGateDecision> {
let Some(target) = target else {
return Ok(CosignatureGateDecision::TargetPending);
};
let decision = match verify_cosignature(carrier, target, trust)? {
CosignatureVerification::BindingMismatch => CosignatureGateDecision::BindingMismatch,
CosignatureVerification::Attested(EventVerificationStatus::Invalid) => {
CosignatureGateDecision::DropInvalid
}
CosignatureVerification::Attested(status @ EventVerificationStatus::Valid)
| CosignatureVerification::Attested(status @ EventVerificationStatus::UntrustedKey) => {
CosignatureGateDecision::Store(status)
}
CosignatureVerification::Attested(EventVerificationStatus::Unsigned) => {
CosignatureGateDecision::DropInvalid
}
};
Ok(decision)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::{EventSignatureBytes, EventSigner};
use crate::session::event::{EventSignature, ShoreEvent};
use crate::session::signing::test_support::{DeterministicSigner, trust_for_actor};
fn target_event() -> ShoreEvent {
serde_json::from_str(include_str!(
"../../../tests/fixtures/event_signatures/friendly-valid-event.json"
))
.expect("fixture event decodes")
}
fn cosign(target: &ShoreEvent, signer: &DeterministicSigner) -> EventSignatureRecordedPayload {
let attesting_signer = signer.signer_id().clone();
let tbs = EventToBeSigned::from_event(target, &attesting_signer).unwrap();
let pae = event_signature_pre_authentication_encoding(&tbs).unwrap();
let sig = signer.sign_event_message(&pae).unwrap();
EventSignatureRecordedPayload {
target_event_id: target.event_id.clone(),
target_event_record_hash: target.event_record_hash().unwrap(),
attesting_signer,
attestation: EventSignature::ed25519_v1(sig),
inclusion_proof: None,
}
}
#[test]
fn well_formed_attestation_by_trusted_signer_is_valid() {
let target = target_event();
let signer = DeterministicSigner::from_seed([11u8; 32]);
let trust = trust_for_actor(&target.writer.actor_id, &signer);
let carrier = cosign(&target, &signer);
assert_eq!(
verify_cosignature(&carrier, &target, &trust).unwrap(),
CosignatureVerification::Attested(EventVerificationStatus::Valid)
);
}
#[test]
fn well_formed_attestation_by_unknown_signer_is_untrusted_key() {
let target = target_event();
let signer = DeterministicSigner::from_seed([12u8; 32]);
let carrier = cosign(&target, &signer);
assert_eq!(
verify_cosignature(&carrier, &target, &TrustSet::default()).unwrap(),
CosignatureVerification::Attested(EventVerificationStatus::UntrustedKey)
);
}
#[test]
fn tampered_attestation_signature_is_invalid() {
let target = target_event();
let signer = DeterministicSigner::from_seed([13u8; 32]);
let trust = trust_for_actor(&target.writer.actor_id, &signer);
let mut carrier = cosign(&target, &signer);
carrier.attestation =
EventSignature::ed25519_v1(EventSignatureBytes::from_bytes(&[0u8; 64]));
assert_eq!(
verify_cosignature(&carrier, &target, &trust).unwrap(),
CosignatureVerification::Attested(EventVerificationStatus::Invalid)
);
}
#[test]
fn malformed_attesting_signer_key_is_invalid() {
let target = target_event();
let signer = DeterministicSigner::from_seed([14u8; 32]);
let mut carrier = cosign(&target, &signer);
carrier.attesting_signer =
serde_json::from_value(serde_json::json!("did:key:zNotARealEd25519Key")).unwrap();
assert_eq!(
verify_cosignature(&carrier, &target, &TrustSet::default()).unwrap(),
CosignatureVerification::Attested(EventVerificationStatus::Invalid)
);
}
#[test]
fn wrong_alg_or_sig_version_on_attestation_is_invalid() {
let target = target_event();
let signer = DeterministicSigner::from_seed([15u8; 32]);
let trust = trust_for_actor(&target.writer.actor_id, &signer);
let mut wrong_alg = cosign(&target, &signer);
wrong_alg.attestation.alg = "rsa".to_owned();
assert_eq!(
verify_cosignature(&wrong_alg, &target, &trust).unwrap(),
CosignatureVerification::Attested(EventVerificationStatus::Invalid)
);
let mut wrong_version = cosign(&target, &signer);
wrong_version.attestation.sig_version = 2;
assert_eq!(
verify_cosignature(&wrong_version, &target, &trust).unwrap(),
CosignatureVerification::Attested(EventVerificationStatus::Invalid)
);
}
#[test]
fn carrier_target_record_hash_mismatch_is_binding_mismatch_not_invalid() {
let target = target_event();
let signer = DeterministicSigner::from_seed([16u8; 32]);
let trust = trust_for_actor(&target.writer.actor_id, &signer);
let mut carrier = cosign(&target, &signer);
carrier.target_event_record_hash =
"sha256:0000000000000000000000000000000000000000000000000000000000000000".to_owned();
let result = verify_cosignature(&carrier, &target, &trust).unwrap();
assert_eq!(result, CosignatureVerification::BindingMismatch);
assert_ne!(
result,
CosignatureVerification::Attested(EventVerificationStatus::Invalid),
"a bad binding is a different fact, not a malformed signature of this fact"
);
}
#[test]
fn signer_inclusive_view_is_what_is_signed() {
let target = target_event();
let attesting = DeterministicSigner::from_seed([17u8; 32]);
let other = DeterministicSigner::from_seed([18u8; 32]);
let trust = trust_for_actor(&target.writer.actor_id, &attesting);
let genuine = cosign(&target, &attesting);
assert_eq!(
verify_cosignature(&genuine, &target, &trust).unwrap(),
CosignatureVerification::Attested(EventVerificationStatus::Valid)
);
let other_tbs = EventToBeSigned::from_event(&target, other.signer_id()).unwrap();
let other_pae = event_signature_pre_authentication_encoding(&other_tbs).unwrap();
let replayed_sig = attesting.sign_event_message(&other_pae).unwrap();
let mut replay = cosign(&target, &attesting);
replay.attestation = EventSignature::ed25519_v1(replayed_sig);
assert_eq!(
verify_cosignature(&replay, &target, &trust).unwrap(),
CosignatureVerification::Attested(EventVerificationStatus::Invalid)
);
}
#[test]
fn gate_stores_valid_and_untrusted_key() {
let target = target_event();
let trusted = DeterministicSigner::from_seed([20u8; 32]);
let trust = trust_for_actor(&target.writer.actor_id, &trusted);
let valid = cosign(&target, &trusted);
assert_eq!(
gate_cosignature_for_store(&valid, Some(&target), &trust).unwrap(),
CosignatureGateDecision::Store(EventVerificationStatus::Valid)
);
let unknown = DeterministicSigner::from_seed([21u8; 32]);
let untrusted = cosign(&target, &unknown);
let decision = gate_cosignature_for_store(&untrusted, Some(&target), &trust).unwrap();
assert_eq!(
decision,
CosignatureGateDecision::Store(EventVerificationStatus::UntrustedKey)
);
assert!(decision.stores());
assert_eq!(decision.drop_diagnostic_code(), None);
}
#[test]
fn gate_drops_invalid_with_code() {
let target = target_event();
let signer = DeterministicSigner::from_seed([22u8; 32]);
let trust = trust_for_actor(&target.writer.actor_id, &signer);
let mut carrier = cosign(&target, &signer);
carrier.attestation =
EventSignature::ed25519_v1(EventSignatureBytes::from_bytes(&[0u8; 64]));
let decision = gate_cosignature_for_store(&carrier, Some(&target), &trust).unwrap();
assert_eq!(decision, CosignatureGateDecision::DropInvalid);
assert!(!decision.stores());
assert_eq!(
decision.drop_diagnostic_code(),
Some(COSIGNATURE_INVALID_CODE)
);
}
#[test]
fn gate_target_pending_when_absent_then_stores_after_backfill() {
let target = target_event();
let signer = DeterministicSigner::from_seed([23u8; 32]);
let trust = trust_for_actor(&target.writer.actor_id, &signer);
let carrier = cosign(&target, &signer);
let pending = gate_cosignature_for_store(&carrier, None, &trust).unwrap();
assert_eq!(pending, CosignatureGateDecision::TargetPending);
assert_eq!(
pending.drop_diagnostic_code(),
Some(COSIGNATURE_TARGET_PENDING_CODE)
);
assert_eq!(
gate_cosignature_for_store(&carrier, Some(&target), &trust).unwrap(),
CosignatureGateDecision::Store(EventVerificationStatus::Valid)
);
}
#[test]
fn gate_binding_mismatch_distinct_from_invalid() {
let target = target_event();
let signer = DeterministicSigner::from_seed([24u8; 32]);
let trust = trust_for_actor(&target.writer.actor_id, &signer);
let mut carrier = cosign(&target, &signer);
carrier.target_event_record_hash =
"sha256:0000000000000000000000000000000000000000000000000000000000000000".to_owned();
let decision = gate_cosignature_for_store(&carrier, Some(&target), &trust).unwrap();
assert_eq!(decision, CosignatureGateDecision::BindingMismatch);
assert_ne!(decision, CosignatureGateDecision::DropInvalid);
assert_eq!(
decision.drop_diagnostic_code(),
Some(COSIGNATURE_BINDING_MISMATCH_CODE)
);
}
}