Skip to main content

monetize_embed/
ticket.rs

1//! **The actor ticket: the appliance's word that this human may renew this
2//! tenant** — minted by the product's appliance, verified here.
3//!
4//! # Which way this one points, and why that is the whole point
5//!
6//! Every other signed thing in this crate travels monetize → appliance: a
7//! [`crate::signing::verify_fact`] is monetize's verdict about a tenant, and a
8//! [`crate::signing::GoAhead`] is monetize's permission to grow a data set.
9//! Both are verified on the appliance, against monetize's key.
10//!
11//! **A ticket travels the other way.** monetize authenticates exactly one
12//! caller on its gRPC surface — a shared bearer, and an empty one means open
13//! (`monetize-server/src/auth.rs`) — so `Purchase.Start` cannot tell the
14//! product's console from the product's operator, and certainly cannot tell one
15//! browser session from another. When a *customer* renews their own
16//! subscription, the thing monetize needs and does not have is evidence that
17//! the human driving the console holds the tenant they are paying for.
18//!
19//! The appliance is the one party that knows. It holds the identity table, the
20//! keys and the grants; on gunnar a principal's id **is** the namespace it owns
21//! (`AuthTable::owns_account`, a string equality with no row to get wrong). So
22//! the appliance decides, and says so in a ticket monetize can check without
23//! asking anybody.
24//!
25//! # Why monetize verifies a signature instead of calling the appliance back
26//!
27//! The obvious alternative — monetize asks the appliance "is this ticket
28//! yours?" — needs no new key material at all, because a product plugin already
29//! holds an authenticated client to its own appliance. It was rejected for one
30//! reason, and it is the reason this whole path exists:
31//!
32//! > **It puts the appliance in the critical path of its own renewal.**
33//!
34//! A renewal is what a customer does when the box is full, suspended, or down.
35//! A design in which the box must be reachable before it can be paid for cannot
36//! serve the case it was built for. A signature is checked offline, so a
37//! ticket minted while the box was up is still good when it is not.
38//!
39//! # What a ticket is NOT
40//!
41//! A ticket is **permission for one verb, never a widening of any other**. It
42//! authorises `purpose: "renew"` on exactly the tenant it names. It cannot
43//! authorise an order (new caps), it cannot name a second tenant, and its
44//! absence changes nothing — a request with no ticket is the operator path,
45//! unchanged.
46//!
47//! # Shape
48//!
49//! ```json
50//! {"v":1,"issuer":"gunnar","product":"gunnar","tenant":"alice",
51//!  "purpose":"renew","nonce":"<one line, ≤256 B>",
52//!  "issued_unix_ms":<i64>,"expires_unix_ms":<i64>,
53//!  "signature":"<base64, standard alphabet, padded>"}
54//! ```
55//!
56//! Ed25519 over [`ticket_message`]: the canonical JSON of every field except
57//! `signature`. The same canonical form and the same base64 alphabet a
58//! [`crate::signing::GoAhead`] uses, so there is one of each in this crate and
59//! not two.
60
61use ed25519_dalek::VerifyingKey;
62
63use crate::signing::{base64_decode, canonical_json, check, SignatureError};
64
65/// **Another period of what the tenant already holds.** Names no caps.
66///
67/// A word rather than a bool, and the reason is now visible rather than
68/// hypothetical: [`PURPOSE_ORDER`] exists, and a verifier must be able to
69/// refuse a renewal ticket presented on an order — and the reverse — instead of
70/// waving either through because the signature was good. [`verify_ticket`]
71/// takes the purpose it wants and compares.
72pub const PURPOSE_RENEW: &str = "renew";
73
74/// **An order: more room, at a cap the customer chose.**
75///
76/// The difference from [`PURPOSE_RENEW`] is not the word, it is
77/// [`ActorTicket::caps`]. A renewal sells a known thing — another period of
78/// what is already held — so the appliance vouching for the tenant is the whole
79/// of what monetize needs. An order names an AMOUNT, and an amount the ticket
80/// did not cover is an amount nobody vouched for: a ticket that said only "this
81/// human may buy" would be a blank cheque, signed by the appliance, spendable
82/// by whatever sent it for whatever it liked.
83///
84/// So an order ticket carries the caps it covers, [`verify_ticket`] compares
85/// them against the order they arrived with, and a mismatch is **refused and
86/// never ignored** — the same rule the tenant, product and purpose fields are
87/// already held to.
88pub const PURPOSE_ORDER: &str = "order";
89
90/// The longest life a ticket may be minted with.
91///
92/// A ticket is fetched inside the request that spends it, so its life covers
93/// one round trip and a clock disagreement, not a session. Ten minutes is two
94/// orders of magnitude more than the path needs and still short enough that a
95/// ticket captured off the wire is worthless by the time it is read out of a
96/// log. [`verify_ticket`] refuses a longer one **even when it verifies**: the
97/// cap is this crate's rule, not the issuer's, so an appliance that is talked
98/// into minting an eternal ticket still cannot spend one here.
99pub const MAX_LIFE_MS: i64 = 10 * 60 * 1000;
100
101/// How far ahead of the verifier's clock a ticket may claim to have been
102/// issued. Two boxes, two clocks; gunnar's own gRPC credential allows the same
103/// order of slack.
104pub const MAX_CLOCK_SKEW_MS: i64 = 60 * 1000;
105
106/// **The appliance's word that one principal may act for one tenant.**
107#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
108pub struct ActorTicket {
109    /// Format version. `1`, and a verifier refuses anything else rather than
110    /// guessing at a field it does not know.
111    pub v: u32,
112    /// Which appliance minted it, as a product id (`gunnar`). Decorative for a
113    /// verifier that holds one key — the key IS the issuer — and load-bearing
114    /// the day a monetize holds two.
115    pub issuer: String,
116    /// The product this ticket may be spent on, as `ProductInfo.id`. Checked
117    /// against the order, so a ticket from one product's appliance cannot buy
118    /// another product's period.
119    pub product: String,
120    /// The tenant the bearer may act for. On gunnar this is the namespace,
121    /// which is the principal's own id.
122    pub tenant: String,
123    /// The one verb this ticket authorises: [`PURPOSE_RENEW`] or
124    /// [`PURPOSE_ORDER`].
125    pub purpose: String,
126    /// **The caps this ticket covers**, for [`PURPOSE_ORDER`] — the target caps
127    /// the order will name, meter by meter.
128    ///
129    /// **Empty for [`PURPOSE_RENEW`], and required to be**: a renewal names no
130    /// caps at all (they come off the tenant's stored fact), so a renewal
131    /// ticket carrying any is either a client that has confused the two paths
132    /// or an order wearing a renewal's word. [`verify_ticket`] refuses both.
133    ///
134    /// A `BTreeMap` because it is serialised into the signed message and the
135    /// canonical form sorts keys anyway; the ordered map makes the Rust value
136    /// and the signed bytes agree by construction rather than by the
137    /// serialiser's mood.
138    #[serde(default)]
139    pub caps: std::collections::BTreeMap<String, u64>,
140    /// Single-use, chosen by the issuer. The verifier keeps a ring and refuses
141    /// a repeat — see [`SeenNonces`].
142    pub nonce: String,
143    pub issued_unix_ms: i64,
144    pub expires_unix_ms: i64,
145    /// Base64, standard alphabet, padded. Not part of [`ticket_message`].
146    #[serde(default)]
147    pub signature: String,
148}
149
150impl ActorTicket {
151    /// Build an unsigned ticket. The issuer signs [`ticket_message`] of it and
152    /// fills in [`ActorTicket::signature`].
153    ///
154    /// Not a constructor that signs, because this crate holds no key: it is
155    /// compiled into products, and a product must be able to CHECK a ticket
156    /// without being able to MINT one. That asymmetry is the same one
157    /// [`crate::signing`] has for facts, and it is deliberate.
158    pub fn unsigned(
159        issuer: impl Into<String>,
160        product: impl Into<String>,
161        tenant: impl Into<String>,
162        purpose: impl Into<String>,
163        nonce: impl Into<String>,
164        issued_unix_ms: i64,
165        life_ms: i64,
166    ) -> ActorTicket {
167        ActorTicket {
168            v: 1,
169            issuer: issuer.into(),
170            product: product.into(),
171            tenant: tenant.into(),
172            purpose: purpose.into(),
173            caps: std::collections::BTreeMap::new(),
174            nonce: nonce.into(),
175            issued_unix_ms,
176            expires_unix_ms: issued_unix_ms.saturating_add(life_ms),
177            signature: String::new(),
178        }
179    }
180
181    /// **The caps this order ticket covers.** Builder rather than an eighth
182    /// positional argument, because every renewal would otherwise pass an empty
183    /// map at a call site where the emptiness is the whole point and reads as
184    /// noise.
185    pub fn covering(mut self, caps: std::collections::BTreeMap<String, u64>) -> ActorTicket {
186        self.caps = caps;
187        self
188    }
189}
190
191/// The bytes a ticket's signature covers: every field except `signature`.
192pub fn ticket_message(ticket: &ActorTicket) -> Vec<u8> {
193    let mut v = serde_json::to_value(ticket).expect("a ticket serializes");
194    v.as_object_mut()
195        .expect("a ticket is an object")
196        .remove("signature");
197    let mut s = String::new();
198    canonical_json(&v, &mut s);
199    s.into_bytes()
200}
201
202/// **Parse, verify and admit a ticket — or refuse it, saying which rule it
203/// broke.**
204///
205/// Every argument is a thing the caller must supply rather than a default this
206/// function could pick, because each one is a check that a verifier which
207/// "just checked the signature" would have skipped:
208///
209/// * `key` — the appliance's Ed25519 verifying key. A ticket is only ever as
210///   good as the key it is checked against, and that key is configuration.
211/// * `product` / `tenant` — what the ORDER says. A valid ticket for another
212///   tenant is the whole attack, and comparing it here means no caller can
213///   forget to.
214/// * `purpose` — [`PURPOSE_RENEW`] or [`PURPOSE_ORDER`]. Named by the caller so
215///   a ticket minted for a verb this build has never heard of is refused, and
216///   so a renewal ticket can never be spent on an order.
217/// * `caps` — what the ORDER actually names. For [`PURPOSE_ORDER`] this must
218///   equal the ticket's own caps exactly; for [`PURPOSE_RENEW`] both must be
219///   empty. This is the argument that stops an order ticket being a blank
220///   cheque: without it the appliance would be vouching for "this human may
221///   buy", and the amount would be whatever the thing holding the ticket
222///   decided to ask for.
223/// * `now_unix_ms` — the verifier's clock, so expiry is not optional.
224///
225/// Order: structure, then signature, then the claims. The claims are only
226/// compared once the signature says the appliance really wrote them, so a
227/// refusal about a tenant or an expiry is a statement about a genuine ticket
228/// and not about an attacker's JSON.
229pub fn verify_ticket(
230    bytes: &[u8],
231    key: &VerifyingKey,
232    product: &str,
233    tenant: &str,
234    purpose: &str,
235    caps: &std::collections::BTreeMap<String, u64>,
236    now_unix_ms: i64,
237) -> Result<ActorTicket, SignatureError> {
238    let ticket: ActorTicket =
239        serde_json::from_slice(bytes).map_err(|_| SignatureError::Malformed)?;
240    if ticket.v != 1
241        || ticket.nonce.trim().is_empty()
242        || ticket.nonce.len() > 256
243        || ticket.tenant.trim().is_empty()
244        || ticket.product.trim().is_empty()
245    {
246        return Err(SignatureError::Malformed);
247    }
248    let sig = base64_decode(&ticket.signature).ok_or(SignatureError::Malformed)?;
249    if !check(key, &ticket_message(&ticket), &sig)? {
250        return Err(SignatureError::Ticket(
251            "the signature does not verify against this appliance's key".to_owned(),
252        ));
253    }
254    // ── The claims, now that they are known to be the appliance's ──────────
255    //
256    // `Ticket` carries one sentence and it is deliberately the same shape for
257    // every one of these: the CALLER decides what a customer is told. A
258    // verifier that returned "…for tenant beta, not alpha" and let that reach a
259    // browser would answer, for any name a stranger cares to type, whether a
260    // ticket for it exists — which is the enumeration the tenant path exists to
261    // avoid.
262    if ticket.purpose != purpose {
263        return Err(SignatureError::Ticket(format!(
264            "this ticket authorises {:?} and the call is {purpose:?}",
265            ticket.purpose
266        )));
267    }
268    if ticket.product != product {
269        return Err(SignatureError::Ticket(format!(
270            "this ticket was minted for product {:?}",
271            ticket.product
272        )));
273    }
274    if ticket.tenant != tenant {
275        return Err(SignatureError::Ticket(
276            "this ticket names another tenant".to_owned(),
277        ));
278    }
279    // ── The amount, for an order ───────────────────────────────────────────
280    //
281    // **Refused, never ignored**, which is the rule every other field here is
282    // held to. A ticket whose caps do not match the order it arrived with is
283    // not a ticket for this order, and quietly running the order anyway would
284    // charge for something the appliance never vouched for.
285    if ticket.caps != *caps {
286        return Err(SignatureError::Ticket(format!(
287            "this ticket covers {} meter(s) and the order names {}",
288            ticket.caps.len(),
289            caps.len()
290        )));
291    }
292    // A renewal names no caps on either side. Stated separately from the
293    // comparison above, because `{} == {}` would pass an order-shaped renewal
294    // silently and the two mistakes have different remedies.
295    if ticket.purpose == PURPOSE_RENEW && !ticket.caps.is_empty() {
296        return Err(SignatureError::Ticket(
297            "a renewal ticket names no caps: a renewal sells another period of what is already \
298             held, and the caps come off the stored fact"
299                .to_owned(),
300        ));
301    }
302    if ticket.expires_unix_ms <= ticket.issued_unix_ms
303        || ticket.expires_unix_ms.saturating_sub(ticket.issued_unix_ms) > MAX_LIFE_MS
304    {
305        return Err(SignatureError::Ticket(format!(
306            "a ticket may live at most {MAX_LIFE_MS} ms and this one claims {} ms",
307            ticket.expires_unix_ms.saturating_sub(ticket.issued_unix_ms)
308        )));
309    }
310    if now_unix_ms >= ticket.expires_unix_ms {
311        return Err(SignatureError::Ticket("this ticket has expired".to_owned()));
312    }
313    if ticket.issued_unix_ms.saturating_sub(now_unix_ms) > MAX_CLOCK_SKEW_MS {
314        return Err(SignatureError::Ticket(
315            "this ticket is issued further in the future than two clocks explain".to_owned(),
316        ));
317    }
318    Ok(ticket)
319}
320
321/// **The nonces already spent, so a ticket is single use.**
322///
323/// A ring and not a set: the thing that writes to it is a verified ticket with
324/// a bounded life, so what has to be remembered is one life's worth of them and
325/// never the whole history. [`MAX_LIFE_MS`] is ten minutes; a box selling a
326/// renewal a second for ten minutes fills six hundred slots.
327///
328/// **In memory, and that is a stated limit rather than an oversight.** A
329/// monetize that restarts forgets, and a ticket replayed across that restart
330/// would be admitted — within its ten-minute life, by somebody who had already
331/// captured it, to buy the tenant it already names another period of the same
332/// subscription. The exposure is one duplicate renewal of the attacker's own
333/// account, and `Purchase.Start` is idempotent on its reference anyway
334/// (`<product>/<tenant>/<date>`), so the second one is the same order. Making
335/// it durable would mean a table, and a table is worth its cost when the thing
336/// it prevents is worth more than a repeated no-op.
337#[derive(Debug)]
338pub struct SeenNonces {
339    ring: std::sync::Mutex<std::collections::VecDeque<String>>,
340    cap: usize,
341}
342
343impl Default for SeenNonces {
344    fn default() -> SeenNonces {
345        SeenNonces::with_capacity(4096)
346    }
347}
348
349impl SeenNonces {
350    pub fn with_capacity(cap: usize) -> SeenNonces {
351        SeenNonces {
352            ring: std::sync::Mutex::new(std::collections::VecDeque::with_capacity(cap.min(1024))),
353            cap: cap.max(1),
354        }
355    }
356
357    /// Record this nonce and say whether it was **new**. `false` means it has
358    /// been spent and the ticket must be refused.
359    pub fn admit(&self, nonce: &str) -> bool {
360        let mut ring = self.ring.lock().expect("the nonce ring is not poisoned");
361        if ring.iter().any(|seen| seen == nonce) {
362            return false;
363        }
364        if ring.len() >= self.cap {
365            ring.pop_front();
366        }
367        ring.push_back(nonce.to_owned());
368        true
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    use ed25519_dalek::{Signer, SigningKey};
377
378    use crate::signing::base64_encode;
379
380    const NOW: i64 = 1_760_000_000_000;
381
382    fn appliance() -> SigningKey {
383        SigningKey::from_bytes(&[7u8; 32])
384    }
385
386    /// Mint the way a real appliance does: build unsigned, sign the canonical
387    /// message, fill in the signature, serialise.
388    fn mint(key: &SigningKey, ticket: ActorTicket) -> Vec<u8> {
389        let mut ticket = ticket;
390        let sig = key.sign(&ticket_message(&ticket));
391        ticket.signature = base64_encode(&sig.to_bytes());
392        serde_json::to_vec(&ticket).expect("a ticket serialises")
393    }
394
395    /// No caps: what a renewal names, and what every renewal assertion passes.
396    fn none() -> std::collections::BTreeMap<String, u64> {
397        std::collections::BTreeMap::new()
398    }
399
400    /// The caps an order names. One meter, a number no other fixture uses.
401    fn ten_gib() -> std::collections::BTreeMap<String, u64> {
402        std::collections::BTreeMap::from([("pack_bytes".to_owned(), 10_737_418_240u64)])
403    }
404
405    fn order_for(tenant: &str) -> ActorTicket {
406        ActorTicket::unsigned("gunnar", "gunnar", tenant, PURPOSE_ORDER, "n-1", NOW, 60_000)
407            .covering(ten_gib())
408    }
409
410    fn renew_for(tenant: &str) -> ActorTicket {
411        ActorTicket::unsigned("gunnar", "gunnar", tenant, PURPOSE_RENEW, "n-1", NOW, 60_000)
412    }
413
414    #[test]
415    fn a_ticket_the_appliance_signed_is_admitted_for_the_tenant_it_names() {
416        let key = appliance();
417        let bytes = mint(&key, renew_for("alice"));
418
419        let ticket = verify_ticket(
420            &bytes,
421            &key.verifying_key(),
422            "gunnar",
423            "alice",
424            PURPOSE_RENEW,
425            &none(),
426            NOW + 1_000,
427        )
428        .expect("the appliance's own ticket must verify");
429
430        assert_eq!(ticket.tenant, "alice");
431        assert_eq!(ticket.purpose, PURPOSE_RENEW);
432    }
433
434    /// ★ **The attack this whole mechanism exists to refuse.**
435    ///
436    /// A perfectly valid ticket, signed by the real appliance, for the caller's
437    /// OWN tenant — spent on somebody else's. Nothing about the signature is
438    /// wrong; the ticket is genuine. What refuses it is that `verify_ticket`
439    /// compares the tenant in the signed claims against the tenant in the
440    /// ORDER, and takes both as arguments so that no call site can forget one.
441    #[test]
442    fn a_genuine_ticket_cannot_be_spent_on_another_tenant() {
443        let key = appliance();
444        let bytes = mint(&key, renew_for("alice"));
445
446        let refused = verify_ticket(
447            &bytes,
448            &key.verifying_key(),
449            "gunnar",
450            "beta",
451            PURPOSE_RENEW,
452            &none(),
453            NOW + 1_000,
454        )
455        .expect_err("alice's ticket must not renew beta");
456
457        let said = refused.to_string();
458        assert!(
459            !said.contains("alice"),
460            "the refusal names the ticket's own tenant to whoever asked about another: {said}"
461        );
462    }
463
464    /// A ticket from an appliance this monetize does not hold the key for.
465    #[test]
466    fn a_ticket_signed_by_another_key_is_refused() {
467        let bytes = mint(&SigningKey::from_bytes(&[9u8; 32]), renew_for("alice"));
468
469        assert!(verify_ticket(
470            &bytes,
471            &appliance().verifying_key(),
472            "gunnar",
473            "alice",
474            PURPOSE_RENEW,
475            &none(),
476            NOW + 1_000,
477        )
478        .is_err());
479    }
480
481    /// **The signature covers every field**, so editing one after the fact
482    /// breaks it — the tenant most of all.
483    #[test]
484    fn rewriting_the_tenant_after_signing_breaks_the_signature() {
485        let key = appliance();
486        let bytes = mint(&key, renew_for("alice"));
487        let mut ticket: ActorTicket = serde_json::from_slice(&bytes).expect("parses");
488        ticket.tenant = "beta".to_owned();
489        let forged = serde_json::to_vec(&ticket).expect("serialises");
490
491        assert!(verify_ticket(
492            &forged,
493            &key.verifying_key(),
494            "gunnar",
495            "beta",
496            PURPOSE_RENEW,
497            &none(),
498            NOW + 1_000,
499        )
500        .is_err());
501    }
502
503    /// A ticket for one product must not buy another product's period, even
504    /// from an appliance whose key this monetize holds.
505    #[test]
506    fn a_ticket_is_bound_to_its_product() {
507        let key = appliance();
508        let bytes = mint(&key, renew_for("alice"));
509
510        assert!(verify_ticket(
511            &bytes,
512            &key.verifying_key(),
513            "holger",
514            "alice",
515            PURPOSE_RENEW,
516            &none(),
517            NOW + 1_000,
518        )
519        .is_err());
520    }
521
522    /// The purpose is compared, so a ticket minted for a verb invented later
523    /// is refused by a build that predates it rather than waved through.
524    #[test]
525    fn a_ticket_for_another_purpose_is_refused() {
526        let key = appliance();
527        let bytes = mint(
528            &key,
529            ActorTicket::unsigned("gunnar", "gunnar", "alice", "order", "n-1", NOW, 60_000),
530        );
531
532        assert!(verify_ticket(
533            &bytes,
534            &key.verifying_key(),
535            "gunnar",
536            "alice",
537            PURPOSE_RENEW,
538            &none(),
539            NOW + 1_000,
540        )
541        .is_err());
542    }
543
544    #[test]
545    fn an_expired_ticket_is_refused() {
546        let key = appliance();
547        let bytes = mint(&key, renew_for("alice"));
548
549        assert!(verify_ticket(
550            &bytes,
551            &key.verifying_key(),
552            "gunnar",
553            "alice",
554            PURPOSE_RENEW,
555            &none(),
556            NOW + 60_001,
557        )
558        .is_err());
559    }
560
561    /// ★ **The cap is this crate's rule and not the issuer's.**
562    ///
563    /// An appliance talked into minting a ticket that lives a year signs it
564    /// perfectly well, and every claim in it is genuine. It is still refused,
565    /// because the life a verifier will accept is not a thing the signer gets
566    /// to choose.
567    #[test]
568    fn a_ticket_that_outlives_the_cap_is_refused_even_though_it_verifies() {
569        let key = appliance();
570        let bytes = mint(
571            &key,
572            ActorTicket::unsigned(
573                "gunnar",
574                "gunnar",
575                "alice",
576                PURPOSE_RENEW,
577                "n-1",
578                NOW,
579                365 * 24 * 60 * 60 * 1000,
580            ),
581        );
582
583        assert!(verify_ticket(
584            &bytes,
585            &key.verifying_key(),
586            "gunnar",
587            "alice",
588            PURPOSE_RENEW,
589            &none(),
590            NOW + 1_000,
591        )
592        .is_err());
593    }
594
595    /// A ticket dated far enough ahead to outlive its own expiry check is
596    /// refused rather than trusted — otherwise an issuer with a wrong clock
597    /// mints something that never expires from the verifier's point of view.
598    #[test]
599    fn a_ticket_from_the_far_future_is_refused() {
600        let key = appliance();
601        let bytes = mint(&key, renew_for("alice"));
602
603        assert!(verify_ticket(
604            &bytes,
605            &key.verifying_key(),
606            "gunnar",
607            "alice",
608            PURPOSE_RENEW,
609            &none(),
610            NOW - MAX_CLOCK_SKEW_MS - 1,
611        )
612        .is_err());
613    }
614
615    /// An order ticket verifies for the caps it names.
616    #[test]
617    fn an_order_ticket_is_admitted_for_the_caps_it_covers() {
618        let key = appliance();
619        let bytes = mint(&key, order_for("alice"));
620
621        let ticket = verify_ticket(
622            &bytes,
623            &key.verifying_key(),
624            "gunnar",
625            "alice",
626            PURPOSE_ORDER,
627            &ten_gib(),
628            NOW + 1_000,
629        )
630        .expect("the appliance's own order ticket must verify");
631
632        assert_eq!(ticket.caps, ten_gib());
633    }
634
635    /// ★ **An order ticket is not a blank cheque.**
636    ///
637    /// The ticket is genuine, signed by the real appliance, for the right
638    /// tenant, the right product and the right verb. Only the AMOUNT differs —
639    /// the order asks for a hundred times what the customer agreed to. Without
640    /// the caps in the signed message and compared here, the appliance would be
641    /// vouching for "this human may buy" and whatever held the ticket would
642    /// choose how much.
643    #[test]
644    fn an_order_ticket_cannot_be_spent_on_a_bigger_order() {
645        let key = appliance();
646        let bytes = mint(&key, order_for("alice"));
647        let hundredfold = std::collections::BTreeMap::from([(
648            "pack_bytes".to_owned(),
649            1_073_741_824_000u64,
650        )]);
651
652        assert!(verify_ticket(
653            &bytes,
654            &key.verifying_key(),
655            "gunnar",
656            "alice",
657            PURPOSE_ORDER,
658            &hundredfold,
659            NOW + 1_000,
660        )
661        .is_err());
662    }
663
664    /// An order ticket must not be spendable as a renewal, nor the reverse.
665    /// Both directions, because they fail for different reasons and a verifier
666    /// that got one right could still wave the other through.
667    #[test]
668    fn a_renewal_ticket_and_an_order_ticket_are_not_interchangeable() {
669        let key = appliance();
670
671        let renewal = mint(&key, renew_for("alice"));
672        assert!(
673            verify_ticket(
674                &renewal,
675                &key.verifying_key(),
676                "gunnar",
677                "alice",
678                PURPOSE_ORDER,
679                &ten_gib(),
680                NOW + 1_000,
681            )
682            .is_err(),
683            "a renewal ticket bought room"
684        );
685
686        let order = mint(&key, order_for("alice"));
687        assert!(
688            verify_ticket(
689                &order,
690                &key.verifying_key(),
691                "gunnar",
692                "alice",
693                PURPOSE_RENEW,
694                &none(),
695                NOW + 1_000,
696            )
697            .is_err(),
698            "an order ticket renewed a period"
699        );
700    }
701
702    /// A renewal ticket that carries caps is refused — an order wearing a
703    /// renewal's word, or a client that has confused the two paths.
704    #[test]
705    fn a_renewal_ticket_carrying_caps_is_refused() {
706        let key = appliance();
707        let bytes = mint(&key, renew_for("alice").covering(ten_gib()));
708
709        assert!(verify_ticket(
710            &bytes,
711            &key.verifying_key(),
712            "gunnar",
713            "alice",
714            PURPOSE_RENEW,
715            &ten_gib(),
716            NOW + 1_000,
717        )
718        .is_err());
719    }
720
721    /// The signature covers the caps, so editing them after signing breaks it.
722    #[test]
723    fn rewriting_the_caps_after_signing_breaks_the_signature() {
724        let key = appliance();
725        let bytes = mint(&key, order_for("alice"));
726        let mut ticket: ActorTicket = serde_json::from_slice(&bytes).expect("parses");
727        ticket.caps = std::collections::BTreeMap::from([("pack_bytes".to_owned(), 1u64)]);
728        let forged = serde_json::to_vec(&ticket).expect("serialises");
729
730        assert!(verify_ticket(
731            &forged,
732            &key.verifying_key(),
733            "gunnar",
734            "alice",
735            PURPOSE_ORDER,
736            &ticket.caps,
737            NOW + 1_000,
738        )
739        .is_err());
740    }
741
742    #[test]
743    fn a_nonce_is_admitted_once() {
744        let seen = SeenNonces::with_capacity(4);
745        assert!(seen.admit("n-1"));
746        assert!(!seen.admit("n-1"), "a nonce must be spendable once");
747        assert!(seen.admit("n-2"));
748    }
749
750    #[test]
751    fn the_nonce_ring_does_not_grow_without_bound() {
752        let seen = SeenNonces::with_capacity(2);
753        assert!(seen.admit("a"));
754        assert!(seen.admit("b"));
755        assert!(seen.admit("c"));
756        // `a` fell off the front, so it is admitted again. That is the ring's
757        // stated trade and the reason its capacity covers one ticket life.
758        assert!(seen.admit("a"));
759        assert!(!seen.admit("c"));
760    }
761
762    #[test]
763    fn rubbish_is_refused_without_panicking() {
764        let key = appliance().verifying_key();
765        for bytes in [&b""[..], b"{", b"{}", b"null", b"[1,2,3]"] {
766            assert!(
767                verify_ticket(bytes, &key, "gunnar", "alice", PURPOSE_RENEW, &none(), NOW).is_err(),
768                "{bytes:?} was not refused"
769            );
770        }
771    }
772}