use ed25519_dalek::VerifyingKey;
use crate::signing::{base64_decode, canonical_json, check, SignatureError};
pub const PURPOSE_RENEW: &str = "renew";
pub const PURPOSE_ORDER: &str = "order";
pub const MAX_LIFE_MS: i64 = 10 * 60 * 1000;
pub const MAX_CLOCK_SKEW_MS: i64 = 60 * 1000;
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct ActorTicket {
pub v: u32,
pub issuer: String,
pub product: String,
pub tenant: String,
pub purpose: String,
#[serde(default)]
pub caps: std::collections::BTreeMap<String, u64>,
pub nonce: String,
pub issued_unix_ms: i64,
pub expires_unix_ms: i64,
#[serde(default)]
pub signature: String,
}
impl ActorTicket {
pub fn unsigned(
issuer: impl Into<String>,
product: impl Into<String>,
tenant: impl Into<String>,
purpose: impl Into<String>,
nonce: impl Into<String>,
issued_unix_ms: i64,
life_ms: i64,
) -> ActorTicket {
ActorTicket {
v: 1,
issuer: issuer.into(),
product: product.into(),
tenant: tenant.into(),
purpose: purpose.into(),
caps: std::collections::BTreeMap::new(),
nonce: nonce.into(),
issued_unix_ms,
expires_unix_ms: issued_unix_ms.saturating_add(life_ms),
signature: String::new(),
}
}
pub fn covering(mut self, caps: std::collections::BTreeMap<String, u64>) -> ActorTicket {
self.caps = caps;
self
}
}
pub fn ticket_message(ticket: &ActorTicket) -> Vec<u8> {
let mut v = serde_json::to_value(ticket).expect("a ticket serializes");
v.as_object_mut()
.expect("a ticket is an object")
.remove("signature");
let mut s = String::new();
canonical_json(&v, &mut s);
s.into_bytes()
}
pub fn verify_ticket(
bytes: &[u8],
key: &VerifyingKey,
product: &str,
tenant: &str,
purpose: &str,
caps: &std::collections::BTreeMap<String, u64>,
now_unix_ms: i64,
) -> Result<ActorTicket, SignatureError> {
let ticket: ActorTicket =
serde_json::from_slice(bytes).map_err(|_| SignatureError::Malformed)?;
if ticket.v != 1
|| ticket.nonce.trim().is_empty()
|| ticket.nonce.len() > 256
|| ticket.tenant.trim().is_empty()
|| ticket.product.trim().is_empty()
{
return Err(SignatureError::Malformed);
}
let sig = base64_decode(&ticket.signature).ok_or(SignatureError::Malformed)?;
if !check(key, &ticket_message(&ticket), &sig)? {
return Err(SignatureError::Ticket(
"the signature does not verify against this appliance's key".to_owned(),
));
}
if ticket.purpose != purpose {
return Err(SignatureError::Ticket(format!(
"this ticket authorises {:?} and the call is {purpose:?}",
ticket.purpose
)));
}
if ticket.product != product {
return Err(SignatureError::Ticket(format!(
"this ticket was minted for product {:?}",
ticket.product
)));
}
if ticket.tenant != tenant {
return Err(SignatureError::Ticket(
"this ticket names another tenant".to_owned(),
));
}
if ticket.caps != *caps {
return Err(SignatureError::Ticket(format!(
"this ticket covers {} meter(s) and the order names {}",
ticket.caps.len(),
caps.len()
)));
}
if ticket.purpose == PURPOSE_RENEW && !ticket.caps.is_empty() {
return Err(SignatureError::Ticket(
"a renewal ticket names no caps: a renewal sells another period of what is already \
held, and the caps come off the stored fact"
.to_owned(),
));
}
if ticket.expires_unix_ms <= ticket.issued_unix_ms
|| ticket.expires_unix_ms.saturating_sub(ticket.issued_unix_ms) > MAX_LIFE_MS
{
return Err(SignatureError::Ticket(format!(
"a ticket may live at most {MAX_LIFE_MS} ms and this one claims {} ms",
ticket.expires_unix_ms.saturating_sub(ticket.issued_unix_ms)
)));
}
if now_unix_ms >= ticket.expires_unix_ms {
return Err(SignatureError::Ticket("this ticket has expired".to_owned()));
}
if ticket.issued_unix_ms.saturating_sub(now_unix_ms) > MAX_CLOCK_SKEW_MS {
return Err(SignatureError::Ticket(
"this ticket is issued further in the future than two clocks explain".to_owned(),
));
}
Ok(ticket)
}
#[derive(Debug)]
pub struct SeenNonces {
ring: std::sync::Mutex<std::collections::VecDeque<String>>,
cap: usize,
}
impl Default for SeenNonces {
fn default() -> SeenNonces {
SeenNonces::with_capacity(4096)
}
}
impl SeenNonces {
pub fn with_capacity(cap: usize) -> SeenNonces {
SeenNonces {
ring: std::sync::Mutex::new(std::collections::VecDeque::with_capacity(cap.min(1024))),
cap: cap.max(1),
}
}
pub fn admit(&self, nonce: &str) -> bool {
let mut ring = self.ring.lock().expect("the nonce ring is not poisoned");
if ring.iter().any(|seen| seen == nonce) {
return false;
}
if ring.len() >= self.cap {
ring.pop_front();
}
ring.push_back(nonce.to_owned());
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
use crate::signing::base64_encode;
const NOW: i64 = 1_760_000_000_000;
fn appliance() -> SigningKey {
SigningKey::from_bytes(&[7u8; 32])
}
fn mint(key: &SigningKey, ticket: ActorTicket) -> Vec<u8> {
let mut ticket = ticket;
let sig = key.sign(&ticket_message(&ticket));
ticket.signature = base64_encode(&sig.to_bytes());
serde_json::to_vec(&ticket).expect("a ticket serialises")
}
fn none() -> std::collections::BTreeMap<String, u64> {
std::collections::BTreeMap::new()
}
fn ten_gib() -> std::collections::BTreeMap<String, u64> {
std::collections::BTreeMap::from([("pack_bytes".to_owned(), 10_737_418_240u64)])
}
fn order_for(tenant: &str) -> ActorTicket {
ActorTicket::unsigned("gunnar", "gunnar", tenant, PURPOSE_ORDER, "n-1", NOW, 60_000)
.covering(ten_gib())
}
fn renew_for(tenant: &str) -> ActorTicket {
ActorTicket::unsigned("gunnar", "gunnar", tenant, PURPOSE_RENEW, "n-1", NOW, 60_000)
}
#[test]
fn a_ticket_the_appliance_signed_is_admitted_for_the_tenant_it_names() {
let key = appliance();
let bytes = mint(&key, renew_for("alice"));
let ticket = verify_ticket(
&bytes,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_RENEW,
&none(),
NOW + 1_000,
)
.expect("the appliance's own ticket must verify");
assert_eq!(ticket.tenant, "alice");
assert_eq!(ticket.purpose, PURPOSE_RENEW);
}
#[test]
fn a_genuine_ticket_cannot_be_spent_on_another_tenant() {
let key = appliance();
let bytes = mint(&key, renew_for("alice"));
let refused = verify_ticket(
&bytes,
&key.verifying_key(),
"gunnar",
"beta",
PURPOSE_RENEW,
&none(),
NOW + 1_000,
)
.expect_err("alice's ticket must not renew beta");
let said = refused.to_string();
assert!(
!said.contains("alice"),
"the refusal names the ticket's own tenant to whoever asked about another: {said}"
);
}
#[test]
fn a_ticket_signed_by_another_key_is_refused() {
let bytes = mint(&SigningKey::from_bytes(&[9u8; 32]), renew_for("alice"));
assert!(verify_ticket(
&bytes,
&appliance().verifying_key(),
"gunnar",
"alice",
PURPOSE_RENEW,
&none(),
NOW + 1_000,
)
.is_err());
}
#[test]
fn rewriting_the_tenant_after_signing_breaks_the_signature() {
let key = appliance();
let bytes = mint(&key, renew_for("alice"));
let mut ticket: ActorTicket = serde_json::from_slice(&bytes).expect("parses");
ticket.tenant = "beta".to_owned();
let forged = serde_json::to_vec(&ticket).expect("serialises");
assert!(verify_ticket(
&forged,
&key.verifying_key(),
"gunnar",
"beta",
PURPOSE_RENEW,
&none(),
NOW + 1_000,
)
.is_err());
}
#[test]
fn a_ticket_is_bound_to_its_product() {
let key = appliance();
let bytes = mint(&key, renew_for("alice"));
assert!(verify_ticket(
&bytes,
&key.verifying_key(),
"holger",
"alice",
PURPOSE_RENEW,
&none(),
NOW + 1_000,
)
.is_err());
}
#[test]
fn a_ticket_for_another_purpose_is_refused() {
let key = appliance();
let bytes = mint(
&key,
ActorTicket::unsigned("gunnar", "gunnar", "alice", "order", "n-1", NOW, 60_000),
);
assert!(verify_ticket(
&bytes,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_RENEW,
&none(),
NOW + 1_000,
)
.is_err());
}
#[test]
fn an_expired_ticket_is_refused() {
let key = appliance();
let bytes = mint(&key, renew_for("alice"));
assert!(verify_ticket(
&bytes,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_RENEW,
&none(),
NOW + 60_001,
)
.is_err());
}
#[test]
fn a_ticket_that_outlives_the_cap_is_refused_even_though_it_verifies() {
let key = appliance();
let bytes = mint(
&key,
ActorTicket::unsigned(
"gunnar",
"gunnar",
"alice",
PURPOSE_RENEW,
"n-1",
NOW,
365 * 24 * 60 * 60 * 1000,
),
);
assert!(verify_ticket(
&bytes,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_RENEW,
&none(),
NOW + 1_000,
)
.is_err());
}
#[test]
fn a_ticket_from_the_far_future_is_refused() {
let key = appliance();
let bytes = mint(&key, renew_for("alice"));
assert!(verify_ticket(
&bytes,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_RENEW,
&none(),
NOW - MAX_CLOCK_SKEW_MS - 1,
)
.is_err());
}
#[test]
fn an_order_ticket_is_admitted_for_the_caps_it_covers() {
let key = appliance();
let bytes = mint(&key, order_for("alice"));
let ticket = verify_ticket(
&bytes,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_ORDER,
&ten_gib(),
NOW + 1_000,
)
.expect("the appliance's own order ticket must verify");
assert_eq!(ticket.caps, ten_gib());
}
#[test]
fn an_order_ticket_cannot_be_spent_on_a_bigger_order() {
let key = appliance();
let bytes = mint(&key, order_for("alice"));
let hundredfold = std::collections::BTreeMap::from([(
"pack_bytes".to_owned(),
1_073_741_824_000u64,
)]);
assert!(verify_ticket(
&bytes,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_ORDER,
&hundredfold,
NOW + 1_000,
)
.is_err());
}
#[test]
fn a_renewal_ticket_and_an_order_ticket_are_not_interchangeable() {
let key = appliance();
let renewal = mint(&key, renew_for("alice"));
assert!(
verify_ticket(
&renewal,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_ORDER,
&ten_gib(),
NOW + 1_000,
)
.is_err(),
"a renewal ticket bought room"
);
let order = mint(&key, order_for("alice"));
assert!(
verify_ticket(
&order,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_RENEW,
&none(),
NOW + 1_000,
)
.is_err(),
"an order ticket renewed a period"
);
}
#[test]
fn a_renewal_ticket_carrying_caps_is_refused() {
let key = appliance();
let bytes = mint(&key, renew_for("alice").covering(ten_gib()));
assert!(verify_ticket(
&bytes,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_RENEW,
&ten_gib(),
NOW + 1_000,
)
.is_err());
}
#[test]
fn rewriting_the_caps_after_signing_breaks_the_signature() {
let key = appliance();
let bytes = mint(&key, order_for("alice"));
let mut ticket: ActorTicket = serde_json::from_slice(&bytes).expect("parses");
ticket.caps = std::collections::BTreeMap::from([("pack_bytes".to_owned(), 1u64)]);
let forged = serde_json::to_vec(&ticket).expect("serialises");
assert!(verify_ticket(
&forged,
&key.verifying_key(),
"gunnar",
"alice",
PURPOSE_ORDER,
&ticket.caps,
NOW + 1_000,
)
.is_err());
}
#[test]
fn a_nonce_is_admitted_once() {
let seen = SeenNonces::with_capacity(4);
assert!(seen.admit("n-1"));
assert!(!seen.admit("n-1"), "a nonce must be spendable once");
assert!(seen.admit("n-2"));
}
#[test]
fn the_nonce_ring_does_not_grow_without_bound() {
let seen = SeenNonces::with_capacity(2);
assert!(seen.admit("a"));
assert!(seen.admit("b"));
assert!(seen.admit("c"));
assert!(seen.admit("a"));
assert!(!seen.admit("c"));
}
#[test]
fn rubbish_is_refused_without_panicking() {
let key = appliance().verifying_key();
for bytes in [&b""[..], b"{", b"{}", b"null", b"[1,2,3]"] {
assert!(
verify_ticket(bytes, &key, "gunnar", "alice", PURPOSE_RENEW, &none(), NOW).is_err(),
"{bytes:?} was not refused"
);
}
}
}