#![cfg(feature = "consent")]
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
use crate::{Sealable, WireError};
pub const SIGNATURE_LEN: usize = 64;
pub const PUBLIC_KEY_LEN: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum ConsentScope {
ScreenOnly = 0,
ScreenAndInput = 1,
ScreenInputFiles = 2,
Interactive = 3,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CausalPredicate {
pub description: String,
pub opaque: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsentRequestCore {
pub request_id: u64,
pub requester_pubkey: [u8; PUBLIC_KEY_LEN],
#[serde(with = "BigArray")]
pub session_fingerprint: [u8; 32],
pub valid_until: u64,
pub scope: ConsentScope,
pub reason: String,
pub causal_binding: Option<CausalPredicate>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsentRequest {
pub core: ConsentRequestCore,
#[serde(with = "BigArray")]
pub signature: [u8; SIGNATURE_LEN],
}
impl ConsentRequest {
pub fn sign(core: ConsentRequestCore, signing_key: &SigningKey) -> Self {
let bytes = bincode::serialize(&core).expect("consent core serializes");
let signature = signing_key.sign(&bytes);
Self {
core,
signature: signature.to_bytes(),
}
}
pub fn verify(&self, expected_pubkey: Option<&[u8; PUBLIC_KEY_LEN]>) -> bool {
if let Some(exp) = expected_pubkey {
if exp != &self.core.requester_pubkey {
return false;
}
}
let Ok(pk) = VerifyingKey::from_bytes(&self.core.requester_pubkey) else {
return false;
};
let Ok(sig) = Signature::from_slice(&self.signature) else {
return false;
};
let bytes = match bincode::serialize(&self.core) {
Ok(b) => b,
Err(_) => return false,
};
pk.verify(&bytes, &sig).is_ok()
}
}
impl Sealable for ConsentRequest {
fn to_bin(&self) -> Result<Vec<u8>, WireError> {
bincode::serialize(self).map_err(WireError::encode)
}
fn from_bin(bytes: &[u8]) -> Result<Self, WireError> {
bincode::deserialize(bytes).map_err(WireError::decode)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsentResponseCore {
pub request_id: u64,
pub responder_pubkey: [u8; PUBLIC_KEY_LEN],
#[serde(with = "BigArray")]
pub session_fingerprint: [u8; 32],
pub approved: bool,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsentResponse {
pub core: ConsentResponseCore,
#[serde(with = "BigArray")]
pub signature: [u8; SIGNATURE_LEN],
}
impl ConsentResponse {
pub fn sign(core: ConsentResponseCore, signing_key: &SigningKey) -> Self {
let bytes = bincode::serialize(&core).expect("consent response core serializes");
let signature = signing_key.sign(&bytes);
Self {
core,
signature: signature.to_bytes(),
}
}
pub fn verify(&self, expected_pubkey: Option<&[u8; PUBLIC_KEY_LEN]>) -> bool {
if let Some(exp) = expected_pubkey {
if exp != &self.core.responder_pubkey {
return false;
}
}
let Ok(pk) = VerifyingKey::from_bytes(&self.core.responder_pubkey) else {
return false;
};
let Ok(sig) = Signature::from_slice(&self.signature) else {
return false;
};
let bytes = match bincode::serialize(&self.core) {
Ok(b) => b,
Err(_) => return false,
};
pk.verify(&bytes, &sig).is_ok()
}
}
impl Sealable for ConsentResponse {
fn to_bin(&self) -> Result<Vec<u8>, WireError> {
bincode::serialize(self).map_err(WireError::encode)
}
fn from_bin(bytes: &[u8]) -> Result<Self, WireError> {
bincode::deserialize(bytes).map_err(WireError::decode)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsentRevocationCore {
pub request_id: u64,
pub revoker_pubkey: [u8; PUBLIC_KEY_LEN],
#[serde(with = "BigArray")]
pub session_fingerprint: [u8; 32],
pub issued_at: u64,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsentRevocation {
pub core: ConsentRevocationCore,
#[serde(with = "BigArray")]
pub signature: [u8; SIGNATURE_LEN],
}
impl ConsentRevocation {
pub fn sign(core: ConsentRevocationCore, signing_key: &SigningKey) -> Self {
let bytes = bincode::serialize(&core).expect("consent revocation core serializes");
let signature = signing_key.sign(&bytes);
Self {
core,
signature: signature.to_bytes(),
}
}
pub fn verify(&self, expected_pubkey: Option<&[u8; PUBLIC_KEY_LEN]>) -> bool {
if let Some(exp) = expected_pubkey {
if exp != &self.core.revoker_pubkey {
return false;
}
}
let Ok(pk) = VerifyingKey::from_bytes(&self.core.revoker_pubkey) else {
return false;
};
let Ok(sig) = Signature::from_slice(&self.signature) else {
return false;
};
let bytes = match bincode::serialize(&self.core) {
Ok(b) => b,
Err(_) => return false,
};
pk.verify(&bytes, &sig).is_ok()
}
}
impl Sealable for ConsentRevocation {
fn to_bin(&self) -> Result<Vec<u8>, WireError> {
bincode::serialize(self).map_err(WireError::encode)
}
fn from_bin(bytes: &[u8]) -> Result<Self, WireError> {
bincode::deserialize(bytes).map_err(WireError::decode)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConsentState {
LegacyBypass,
AwaitingRequest,
Requested,
Approved,
Denied,
Revoked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConsentEvent {
Request {
request_id: u64,
},
ResponseApproved {
request_id: u64,
},
ResponseDenied {
request_id: u64,
},
Revocation {
request_id: u64,
},
}
impl ConsentEvent {
pub fn request_id(&self) -> u64 {
match self {
ConsentEvent::Request { request_id }
| ConsentEvent::ResponseApproved { request_id }
| ConsentEvent::ResponseDenied { request_id }
| ConsentEvent::Revocation { request_id } => *request_id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ConsentViolation {
#[error("revocation observed before any approval (request_id={request_id})")]
RevocationBeforeApproval {
request_id: u64,
},
#[error(
"contradictory response for request_id={request_id}: prior approved={prior_approved}, new approved={new_approved}"
)]
ContradictoryResponse {
request_id: u64,
prior_approved: bool,
new_approved: bool,
},
#[error("response for unknown request_id={request_id} (no prior Request)")]
StaleResponseForUnknownRequest {
request_id: u64,
},
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
fn test_key_pair() -> (SigningKey, [u8; 32]) {
let sk = SigningKey::generate(&mut OsRng);
let pk_bytes = sk.verifying_key().to_bytes();
(sk, pk_bytes)
}
const TEST_FP: [u8; 32] = [0xAA; 32];
#[test]
fn consent_request_signs_and_verifies() {
let (sk, pk) = test_key_pair();
let core = ConsentRequestCore {
request_id: 42,
requester_pubkey: pk,
session_fingerprint: TEST_FP,
valid_until: 1_700_000_000,
scope: ConsentScope::ScreenAndInput,
reason: "ticket #1234 password reset".to_string(),
causal_binding: None,
};
let req = ConsentRequest::sign(core, &sk);
assert!(req.verify(None));
assert!(req.verify(Some(&pk)));
}
#[test]
fn consent_request_rejects_wrong_pubkey() {
let (sk, _) = test_key_pair();
let (_, other_pk) = test_key_pair();
let core = ConsentRequestCore {
request_id: 1,
requester_pubkey: sk.verifying_key().to_bytes(),
session_fingerprint: TEST_FP,
valid_until: 1,
scope: ConsentScope::ScreenOnly,
reason: "".into(),
causal_binding: None,
};
let req = ConsentRequest::sign(core, &sk);
assert!(!req.verify(Some(&other_pk)));
}
#[test]
fn consent_request_rejects_tampered_body() {
let (sk, pk) = test_key_pair();
let core = ConsentRequestCore {
request_id: 1,
requester_pubkey: pk,
session_fingerprint: TEST_FP,
valid_until: 100,
scope: ConsentScope::ScreenOnly,
reason: "original".into(),
causal_binding: None,
};
let mut req = ConsentRequest::sign(core, &sk);
req.core.reason = "tampered".into();
assert!(!req.verify(None));
}
#[test]
fn consent_request_rejects_tampered_fingerprint() {
let (sk, pk) = test_key_pair();
let core = ConsentRequestCore {
request_id: 1,
requester_pubkey: pk,
session_fingerprint: TEST_FP,
valid_until: 100,
scope: ConsentScope::ScreenOnly,
reason: "".into(),
causal_binding: None,
};
let mut req = ConsentRequest::sign(core, &sk);
req.core.session_fingerprint[0] ^= 0x01;
assert!(!req.verify(None));
}
#[test]
fn consent_response_signs_and_verifies() {
let (sk, pk) = test_key_pair();
let core = ConsentResponseCore {
request_id: 42,
responder_pubkey: pk,
session_fingerprint: TEST_FP,
approved: true,
reason: "".into(),
};
let resp = ConsentResponse::sign(core, &sk);
assert!(resp.verify(Some(&pk)));
}
#[test]
fn consent_revocation_signs_and_verifies() {
let (sk, pk) = test_key_pair();
let core = ConsentRevocationCore {
request_id: 42,
revoker_pubkey: pk,
session_fingerprint: TEST_FP,
issued_at: 1_700_000_500,
reason: "session complete".into(),
};
let rev = ConsentRevocation::sign(core, &sk);
assert!(rev.verify(Some(&pk)));
}
#[test]
fn consent_messages_are_sealable() {
let (sk, pk) = test_key_pair();
let req = ConsentRequest::sign(
ConsentRequestCore {
request_id: 1,
requester_pubkey: pk,
session_fingerprint: TEST_FP,
valid_until: 1,
scope: ConsentScope::ScreenOnly,
reason: "".into(),
causal_binding: None,
},
&sk,
);
let bytes = req.to_bin().unwrap();
let decoded = ConsentRequest::from_bin(&bytes).unwrap();
assert_eq!(decoded, req);
}
}