Skip to main content

galata_vault/proto/
api.rs

1//! Request and response bodies for every `/v1` endpoint, the token scopes
2//! and their policy, the capabilities document, and the stable error codes
3//! (`docs/spec/http-api.md`).
4//!
5//! The server knows nothing of projects or paths: every call addresses
6//! exactly one vault, chosen by the caller's credential. Everything a client
7//! must trust arrives signed:
8//! - descriptors and bundles by the owner (`descriptor`, [`SignedBundle`]);
9//! - records by the generation's writer keys (`record`);
10//! - the children record by the owner (`children`).
11//!
12//! **Requests are strict, responses tolerant** (`docs/spec/http-api.md#7`).
13//! Every request body refuses a field it does not know. No response body
14//! does: a newer server may add fields, and an older client ignores them.
15//! Response fields holding an enum a newer server may extend (a token's
16//! scope, an error code, a record's writer) are [`Tolerant`], so a value
17//! this build does not know is kept for display and never acted on.
18//!
19//! Every body and enum here is `#[non_exhaustive]`, so a later release can
20//! add a field or a variant without breaking a dependent crate. Crates that
21//! build a body use its `new` constructor.
22
23use serde::{Deserialize, Serialize};
24
25use crate::proto::audit::{Actor, AuditRow, ChainHead, RowStop};
26use crate::proto::descriptor::SignedDescriptor;
27use crate::proto::frame::{frame, label};
28use crate::proto::ids::{B64, Hash32, Key32, NameHmac, Sig64, TokenId, VaultId};
29use crate::proto::integrity::{IntegrityError, verify_ed25519};
30use crate::proto::tolerant::{Tolerant, Vocabulary};
31
32const DAY: u64 = 86_400;
33
34/// The protocol version this build speaks, as capabilities list it.
35pub const PROTOCOL: &str = "1";
36
37/// The bundle plaintext's version byte (`docs/spec/records.md#2`).
38pub const BUNDLE_VERSION: u8 = 1;
39
40// ---------------------------------------------------------------- scopes
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44#[non_exhaustive]
45pub enum Scope {
46    /// List, history metadata, audit, status.
47    Meta,
48    /// List, create and update secrets and configs; never read them.
49    Append,
50    /// List and read secrets and configs.
51    Read,
52    /// Read and write secrets and configs, list tokens, revoke without
53    /// rotating. Minting, rotation, rekey and deletion are owner-only.
54    Admin,
55    /// List, history metadata, audit, status, and read configs. Its bundle
56    /// has no field for the vault key: it cannot decrypt a secret.
57    Config,
58    /// Everything `Config` may, and create, update and delete configs. Its
59    /// bundle holds the config writer key and no secret writer key: it
60    /// cannot write a secret, by cryptography.
61    #[serde(rename = "config-write")]
62    ConfigWrite,
63}
64
65impl Scope {
66    pub const ALL: [Scope; 6] = [
67        Scope::Meta,
68        Scope::Append,
69        Scope::Read,
70        Scope::Admin,
71        Scope::Config,
72        Scope::ConfigWrite,
73    ];
74
75    /// Read secret values.
76    pub fn can_read_values(self) -> bool {
77        matches!(self, Scope::Read | Scope::Admin)
78    }
79
80    /// Create, update and delete secrets.
81    pub fn can_write_values(self) -> bool {
82        matches!(self, Scope::Append | Scope::Admin)
83    }
84
85    pub fn can_read_configs(self) -> bool {
86        matches!(
87            self,
88            Scope::Read | Scope::Admin | Scope::Config | Scope::ConfigWrite
89        )
90    }
91
92    pub fn can_write_configs(self) -> bool {
93        matches!(self, Scope::Append | Scope::Admin | Scope::ConfigWrite)
94    }
95
96    /// List every token and revoke without rotating. Minting and rotation
97    /// need the owner signature.
98    pub fn can_manage(self) -> bool {
99        self == Scope::Admin
100    }
101
102    /// Every scope may fetch and verify its vault's audit.
103    pub fn can_read_audit(self) -> bool {
104        true
105    }
106
107    /// Whether this scope's bundle carries the vault private key.
108    pub fn bundle_holds_vault_key(self) -> bool {
109        matches!(self, Scope::Read | Scope::Admin)
110    }
111
112    /// Whether this scope's bundle carries the config private key.
113    pub fn bundle_holds_config_key(self) -> bool {
114        matches!(
115            self,
116            Scope::Read | Scope::Admin | Scope::Config | Scope::ConfigWrite
117        )
118    }
119
120    /// Whether this scope's bundle carries the secret writer key.
121    pub fn bundle_holds_secret_writer(self) -> bool {
122        matches!(self, Scope::Append | Scope::Admin)
123    }
124
125    /// Whether this scope's bundle carries the config writer key.
126    pub fn bundle_holds_config_writer(self) -> bool {
127        matches!(self, Scope::Append | Scope::Admin | Scope::ConfigWrite)
128    }
129
130    /// The byte a bundle signature binds. 0 is the owner's own bundle.
131    pub fn code(self) -> u8 {
132        match self {
133            Scope::Meta => 1,
134            Scope::Append => 2,
135            Scope::Read => 3,
136            Scope::Admin => 4,
137            Scope::Config => 5,
138            Scope::ConfigWrite => 6,
139        }
140    }
141
142    pub fn default_ttl_secs(self) -> u64 {
143        match self {
144            Scope::Admin => DAY,
145            _ => 90 * DAY,
146        }
147    }
148
149    pub fn max_ttl_secs(self) -> u64 {
150        match self {
151            Scope::Admin => 30 * DAY,
152            _ => 365 * DAY,
153        }
154    }
155
156    /// The wire name.
157    pub fn as_str(self) -> &'static str {
158        match self {
159            Scope::Meta => "meta",
160            Scope::Append => "append",
161            Scope::Read => "read",
162            Scope::Admin => "admin",
163            Scope::Config => "config",
164            Scope::ConfigWrite => "config-write",
165        }
166    }
167
168    /// The inverse of `Display`.
169    pub fn parse(s: &str) -> Option<Scope> {
170        Scope::ALL.into_iter().find(|scope| scope.as_str() == s)
171    }
172}
173
174impl std::fmt::Display for Scope {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.write_str(self.as_str())
177    }
178}
179
180impl Vocabulary for Scope {
181    fn knows(name: &str) -> bool {
182        Scope::parse(name).is_some()
183    }
184}
185
186/// The scope byte of the owner's own bundle.
187pub const OWNER_SCOPE_CODE: u8 = 0;
188/// The token id of the owner's own bundle.
189pub const OWNER_TOKEN_ID: TokenId = TokenId([0; 16]);
190
191// ---------------------------------------------------------------- bundles
192
193/// A bundle as it travels: sealed to its holder's box key, signed by the
194/// owner. The server can verify the signature; only the holder
195/// can open the seal.
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197#[non_exhaustive]
198pub struct SignedBundle {
199    pub sealed: B64,
200    pub sig: Sig64,
201}
202
203/// What the owner signs for a bundle:
204/// `frame("gv/v1/bundle", vault_id ‖ token_id ‖ scope(1) ‖ generation(4) ‖ SHA-256(sealed))`.
205/// The owner's own bundle uses [`OWNER_TOKEN_ID`] and [`OWNER_SCOPE_CODE`].
206pub fn bundle_signing_input(
207    vault_id: &VaultId,
208    token_id: &TokenId,
209    scope_code: u8,
210    generation: u32,
211    sealed: &[u8],
212) -> Vec<u8> {
213    frame(
214        label::BUNDLE,
215        &[
216            &vault_id.0,
217            &token_id.0,
218            &[scope_code],
219            &generation.to_be_bytes(),
220            &Hash32::sha256(sealed).0,
221        ],
222    )
223}
224
225impl SignedBundle {
226    pub fn new(sealed: B64, sig: Sig64) -> SignedBundle {
227        SignedBundle { sealed, sig }
228    }
229
230    /// Check the owner's signature for this binding.
231    pub fn verify(
232        &self,
233        owner_sign_pub: &Key32,
234        vault_id: &VaultId,
235        token_id: &TokenId,
236        scope_code: u8,
237        generation: u32,
238    ) -> Result<(), IntegrityError> {
239        verify_ed25519(
240            owner_sign_pub,
241            &bundle_signing_input(vault_id, token_id, scope_code, generation, &self.sealed.0),
242            &self.sig,
243            "bundle",
244        )
245    }
246}
247
248// ---------------------------------------------------------------- vaults
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "snake_case")]
252#[non_exhaustive]
253pub enum ChallengePurpose {
254    CreateVault,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(deny_unknown_fields)]
259#[non_exhaustive]
260pub struct ChallengeRequest {
261    pub purpose: ChallengePurpose,
262}
263
264impl ChallengeRequest {
265    pub fn new(purpose: ChallengePurpose) -> ChallengeRequest {
266        ChallengeRequest { purpose }
267    }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
271#[non_exhaustive]
272pub struct ChallengeResponse {
273    pub challenge: String,
274    pub difficulty: u8,
275    pub expires_at: i64,
276}
277
278impl ChallengeResponse {
279    pub fn new(challenge: String, difficulty: u8, expires_at: i64) -> ChallengeResponse {
280        ChallengeResponse {
281            challenge,
282            difficulty,
283            expires_at,
284        }
285    }
286}
287
288/// `POST /v1/vaults`, owner-signed. The first descriptor names every public
289/// key of generation 1; the server checks its signature and that it is a
290/// first generation.
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292#[serde(deny_unknown_fields)]
293#[non_exhaustive]
294pub struct CreateVaultRequest {
295    /// A solved challenge from `POST /v1/challenges`, sent only to a server
296    /// whose capabilities ask for proof of work. A server that asks for none
297    /// does not check one.
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    pub challenge: Option<String>,
300    /// The nonce that solves `challenge`.
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub nonce: Option<u64>,
303    pub vault_id: VaultId,
304    pub owner_sign_pub: Key32,
305    pub owner_box_pub: Key32,
306    pub descriptor: SignedDescriptor,
307    pub owner_bundle: SignedBundle,
308}
309
310impl CreateVaultRequest {
311    /// A creation with no proof of work.
312    pub fn new(
313        vault_id: VaultId,
314        owner_sign_pub: Key32,
315        owner_box_pub: Key32,
316        descriptor: SignedDescriptor,
317        owner_bundle: SignedBundle,
318    ) -> CreateVaultRequest {
319        CreateVaultRequest {
320            challenge: None,
321            nonce: None,
322            vault_id,
323            owner_sign_pub,
324            owner_box_pub,
325            descriptor,
326            owner_bundle,
327        }
328    }
329
330    /// The same creation, carrying a solved challenge.
331    pub fn with_proof(mut self, challenge: Option<String>, nonce: Option<u64>) -> Self {
332        self.challenge = challenge;
333        self.nonce = nonce;
334        self
335    }
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339#[non_exhaustive]
340pub struct CreateVaultResponse {
341    pub vault_id: VaultId,
342    pub generation: u32,
343    /// `None` when the server expires no vault for inactivity.
344    #[serde(default)]
345    pub expires_at: Option<i64>,
346}
347
348impl CreateVaultResponse {
349    pub fn new(vault_id: VaultId, generation: u32, expires_at: Option<i64>) -> Self {
350        CreateVaultResponse {
351            vault_id,
352            generation,
353            expires_at,
354        }
355    }
356}
357
358// ---------------------------------------------------------------- capabilities
359
360/// The optional behaviours a server can advertise in its capabilities
361/// (`docs/spec/http-api.md#6`). A client ignores a name it does not know.
362pub mod feature {
363    /// `POST /v1/tokens/report` is served: a leaked token can be revoked by
364    /// whoever holds it.
365    pub const TOKEN_REPORT: &str = "token_report";
366    /// A request budget is enforced per token and per vault, so any
367    /// authenticated request may be answered 429 with `Retry-After`.
368    pub const RATE_LIMITS: &str = "rate_limits";
369}
370
371/// `GET /v1/capabilities`, unauthenticated: what a server asks of a client
372/// before the client acts (`docs/spec/http-api.md#6`). Identical for every
373/// caller, with no per-vault data.
374///
375/// `proof_of_work`, `idle_expiry_days` and `limits` are the fields of
376/// split-hosted-server and keep their names and meaning. A client requests a
377/// challenge only when `proof_of_work` is set, and keeps vaults alive only
378/// when `idle_expiry_days` is. A document without `protocols` comes from a
379/// server that predates the field, and speaks protocol 2 only.
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381#[non_exhaustive]
382pub struct Capabilities {
383    /// The protocol versions served, as strings: `["1"]`.
384    #[serde(default = "protocols_before_the_field")]
385    pub protocols: Vec<String>,
386    /// The version of each format this build implements; absent from a
387    /// server that predates the field.
388    #[serde(default, skip_serializing_if = "Option::is_none")]
389    pub formats: Option<Formats>,
390    /// What vault creation needs; `None` (`null`) when it needs no challenge.
391    #[serde(default)]
392    pub proof_of_work: Option<ProofOfWork>,
393    /// Days without an authenticated request before a vault expires; `None`
394    /// (`null`) when no vault ever expires for inactivity.
395    #[serde(default)]
396    pub idle_expiry_days: Option<u32>,
397    /// The per-vault quotas.
398    #[serde(default)]
399    pub limits: Limits,
400    /// Optional behaviours in force, from [`feature`].
401    #[serde(default)]
402    pub features: Vec<String>,
403    /// The server's name and version. Absent unless the operator turns it
404    /// on (`advertise_version`), to limit fingerprinting.
405    #[serde(default, skip_serializing_if = "Option::is_none")]
406    pub server: Option<String>,
407}
408
409fn protocols_before_the_field() -> Vec<String> {
410    vec![PROTOCOL.to_owned()]
411}
412
413impl Capabilities {
414    /// A protocol 2 document with this policy, the formats of this build,
415    /// no features and no server version.
416    pub fn new(
417        proof_of_work: Option<ProofOfWork>,
418        idle_expiry_days: Option<u32>,
419        limits: Limits,
420    ) -> Capabilities {
421        Capabilities {
422            protocols: vec![PROTOCOL.to_owned()],
423            formats: Some(Formats::CURRENT),
424            proof_of_work,
425            idle_expiry_days,
426            limits,
427            features: Vec::new(),
428            server: None,
429        }
430    }
431
432    /// The same document, advertising `features`.
433    pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
434        self.features = features.into_iter().map(Into::into).collect();
435        self
436    }
437
438    /// The same document, naming the server.
439    pub fn with_server(mut self, server: Option<String>) -> Self {
440        self.server = server;
441        self
442    }
443
444    /// Whether the server speaks `protocol` (`"1"`).
445    pub fn speaks(&self, protocol: &str) -> bool {
446        self.protocols.iter().any(|p| p == protocol)
447    }
448}
449
450/// The version of each stored or signed format, as a build implements it
451/// (`docs/spec/stability.md#2`). 0 is "not advertised".
452#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
453#[serde(default)]
454#[non_exhaustive]
455pub struct Formats {
456    /// The generation descriptor's version byte.
457    pub descriptor: u32,
458    /// The bundle plaintext's version byte.
459    pub bundle: u32,
460    /// The record envelope's version byte.
461    pub envelope: u32,
462    /// The children record's `v`.
463    pub children: u32,
464    /// The audit row format a server writes.
465    pub audit_row: u32,
466    /// The `GV-Sig` version, `v=`.
467    pub request_signature: u32,
468}
469
470impl Formats {
471    /// What this build writes.
472    pub const CURRENT: Formats = Formats {
473        descriptor: crate::proto::descriptor::DESCRIPTOR_VERSION as u32,
474        bundle: BUNDLE_VERSION as u32,
475        envelope: crate::proto::record::ENVELOPE_VERSION as u32,
476        children: crate::proto::children::CHILDREN_RECORD_VERSION,
477        audit_row: crate::proto::audit::ROW_FORMAT as u32,
478        request_signature: 1,
479    };
480}
481
482/// The proof of work vault creation needs.
483#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
484#[non_exhaustive]
485pub struct ProofOfWork {
486    /// Leading zero bits of `blake3(challenge ‖ nonce)`.
487    pub difficulty: u8,
488}
489
490impl ProofOfWork {
491    pub fn new(difficulty: u8) -> ProofOfWork {
492        ProofOfWork { difficulty }
493    }
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
497#[serde(default)]
498#[non_exhaustive]
499pub struct Limits {
500    pub max_names: u32,
501    pub max_value_bytes: u32,
502    pub max_versions: u32,
503    /// Secrets and configs together.
504    pub max_vault_bytes: u64,
505    pub max_tokens: u32,
506    pub max_configs: u32,
507    pub max_config_bytes: u32,
508    pub max_config_versions: u32,
509}
510
511impl Default for Limits {
512    fn default() -> Self {
513        Limits {
514            max_names: 200,
515            max_value_bytes: 16 * 1024,
516            max_versions: 20,
517            max_vault_bytes: 4 * 1024 * 1024,
518            max_tokens: 128,
519            max_configs: 64,
520            max_config_bytes: 256 * 1024,
521            max_config_versions: 20,
522        }
523    }
524}
525
526impl Limits {
527    #[allow(clippy::too_many_arguments)]
528    pub fn new(
529        max_names: u32,
530        max_value_bytes: u32,
531        max_versions: u32,
532        max_vault_bytes: u64,
533        max_tokens: u32,
534        max_configs: u32,
535        max_config_bytes: u32,
536        max_config_versions: u32,
537    ) -> Limits {
538        Limits {
539            max_names,
540            max_value_bytes,
541            max_versions,
542            max_vault_bytes,
543            max_tokens,
544            max_configs,
545            max_config_bytes,
546            max_config_versions,
547        }
548    }
549
550    /// The largest [`RotationRequest`] body, in bytes, that a vault at these
551    /// quotas can produce. A server's body limit must be at least this, or
552    /// the rotation that cuts off a revoked key fails on a full vault.
553    ///
554    /// Counted:
555    /// - the stored ciphertext `max_vault_bytes` covers (names and values),
556    ///   as unpadded base64;
557    /// - per retained secret and config version: two hex name indexes, the
558    ///   version and write time, a longest-name ciphertext, the record
559    ///   signature and the JSON framing;
560    /// - a signed, resealed bundle and a revoke entry for every permitted token;
561    /// - the signed descriptor, the signed owner bundle and a fixed margin.
562    pub fn max_rotation_body(&self) -> usize {
563        fn b64(n: u64) -> u64 {
564            (n * 4).div_ceil(3)
565        }
566        const INDEX_HEX: u64 = 64; // a NameHmac
567        const TOKEN_HEX: u64 = 32; // a TokenId
568        const DIGITS: u64 = 20; // a u64 or i64 in decimal
569        const NAME_CT: u64 = 24 + 256 + 16; // nonce, longest name, tag
570        const SIG: u64 = 86; // a Sig64, unpadded base64
571        const BUNDLE: u64 = 48 + 198; // sealed-box overhead, largest bundle plaintext
572        const DESCRIPTOR: u64 = 189;
573        // {"old_name_hmac":"","version":,"name_hmac":"","name_ct":"","value_ct":"","written_at":,"sig":""},
574        // is 97 bytes; the rest covers per-item base64 rounding.
575        const VERSION_FRAME: u64 = 104;
576        // {"token_id":"","bundle":{"sealed":"","sig":""}},
577        const TOKEN_FRAME: u64 = 56;
578        const MARGIN: u64 = 4096;
579
580        let versions = u64::from(self.max_names) * u64::from(self.max_versions)
581            + u64::from(self.max_configs) * u64::from(self.max_config_versions);
582        let tokens = u64::from(self.max_tokens);
583        let total = b64(self.max_vault_bytes)
584            + versions * (2 * INDEX_HEX + 2 * DIGITS + b64(NAME_CT) + SIG + VERSION_FRAME)
585            + tokens * (TOKEN_HEX + b64(BUNDLE) + SIG + TOKEN_FRAME)
586            + tokens * (TOKEN_HEX + 3)
587            + b64(DESCRIPTOR)
588            + SIG
589            + b64(BUNDLE)
590            + SIG
591            + 2 * DIGITS
592            + MARGIN;
593        usize::try_from(total).unwrap_or(usize::MAX)
594    }
595}
596
597#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
598#[non_exhaustive]
599pub struct TokenSummary {
600    pub token_id: TokenId,
601    /// A scope a newer server may name and this build not know.
602    pub scope: Tolerant<Scope>,
603    pub created_at: i64,
604    pub expires_at: i64,
605    /// Needed by a rotating owner to reseal this token's bundle.
606    pub box_pub: Key32,
607    pub allow_list: Option<Vec<NameHmac>>,
608}
609
610impl TokenSummary {
611    pub fn new(
612        token_id: TokenId,
613        scope: impl Into<Tolerant<Scope>>,
614        created_at: i64,
615        expires_at: i64,
616        box_pub: Key32,
617        allow_list: Option<Vec<NameHmac>>,
618    ) -> TokenSummary {
619        TokenSummary {
620            token_id,
621            scope: scope.into(),
622            created_at,
623            expires_at,
624            box_pub,
625            allow_list,
626        }
627    }
628}
629
630/// `GET /v1/vault`.
631#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
632#[non_exhaustive]
633pub struct VaultStatus {
634    pub vault_id: VaultId,
635    pub generation: u32,
636    /// Increments on every state change; a rotation names the revision it
637    /// was built from, so a concurrent write makes it fail rather than lose data.
638    pub revision: u64,
639    /// Checked by every client: it must hash to the pinned vault id.
640    pub owner_sign_pub: Key32,
641    pub owner_box_pub: Key32,
642    /// The current generation's owner-signed descriptor.
643    pub descriptor: SignedDescriptor,
644    #[serde(default)]
645    pub config_count: u32,
646    pub created_at: i64,
647    pub last_active_at: i64,
648    /// When the vault expires if nothing touches it; `None` when the server
649    /// expires no vault for inactivity.
650    #[serde(default)]
651    pub expires_at: Option<i64>,
652    pub bytes_used: u64,
653    pub limits: Limits,
654    /// Present only for owner-signed and admin requests.
655    pub tokens: Option<Vec<TokenSummary>>,
656    /// Present only for owner-signed requests.
657    pub owner_bundle: Option<SignedBundle>,
658}
659
660impl VaultStatus {
661    #[allow(clippy::too_many_arguments)]
662    pub fn new(
663        vault_id: VaultId,
664        generation: u32,
665        revision: u64,
666        owner_sign_pub: Key32,
667        owner_box_pub: Key32,
668        descriptor: SignedDescriptor,
669        config_count: u32,
670        created_at: i64,
671        last_active_at: i64,
672        expires_at: Option<i64>,
673        bytes_used: u64,
674        limits: Limits,
675        tokens: Option<Vec<TokenSummary>>,
676        owner_bundle: Option<SignedBundle>,
677    ) -> VaultStatus {
678        VaultStatus {
679            vault_id,
680            generation,
681            revision,
682            owner_sign_pub,
683            owner_box_pub,
684            descriptor,
685            config_count,
686            created_at,
687            last_active_at,
688            expires_at,
689            bytes_used,
690            limits,
691            tokens,
692            owner_bundle,
693        }
694    }
695}
696
697/// `GET /v1/vault/descriptors?after={generation}`: the chain, ascending.
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699#[non_exhaustive]
700pub struct DescriptorList {
701    pub descriptors: Vec<SignedDescriptor>,
702}
703
704impl DescriptorList {
705    pub fn new(descriptors: Vec<SignedDescriptor>) -> DescriptorList {
706        DescriptorList { descriptors }
707    }
708}
709
710/// `POST /v1/vault/rotations`, owner-signed: one atomic batch.
711#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
712#[serde(deny_unknown_fields)]
713#[non_exhaustive]
714pub struct RotationRequest {
715    pub from_generation: u32,
716    pub from_revision: u64,
717    /// The next generation's descriptor, linked by `prev_hash`.
718    pub descriptor: SignedDescriptor,
719    pub owner_bundle: SignedBundle,
720    pub secrets: Vec<RotatedVersion>,
721    /// Every retained config version, re-encrypted and re-signed.
722    pub configs: Vec<RotatedVersion>,
723    pub tokens: Vec<ResealedBundle>,
724    pub revoke: Vec<TokenId>,
725}
726
727impl RotationRequest {
728    #[allow(clippy::too_many_arguments)]
729    pub fn new(
730        from_generation: u32,
731        from_revision: u64,
732        descriptor: SignedDescriptor,
733        owner_bundle: SignedBundle,
734        secrets: Vec<RotatedVersion>,
735        configs: Vec<RotatedVersion>,
736        tokens: Vec<ResealedBundle>,
737        revoke: Vec<TokenId>,
738    ) -> RotationRequest {
739        RotationRequest {
740            from_generation,
741            from_revision,
742            descriptor,
743            owner_bundle,
744            secrets,
745            configs,
746            tokens,
747            revoke,
748        }
749    }
750}
751
752/// One retained version, re-encrypted to the new keys and re-signed with the
753/// new writer key, keeping its number and write time.
754#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
755#[serde(deny_unknown_fields)]
756#[non_exhaustive]
757pub struct RotatedVersion {
758    pub old_name_hmac: NameHmac,
759    pub version: u64,
760    pub name_hmac: NameHmac,
761    pub name_ct: B64,
762    /// `None` for a tombstone version.
763    pub value_ct: Option<B64>,
764    pub written_at: i64,
765    pub sig: Sig64,
766}
767
768impl RotatedVersion {
769    pub fn new(
770        old_name_hmac: NameHmac,
771        version: u64,
772        name_hmac: NameHmac,
773        name_ct: B64,
774        value_ct: Option<B64>,
775        written_at: i64,
776        sig: Sig64,
777    ) -> RotatedVersion {
778        RotatedVersion {
779            old_name_hmac,
780            version,
781            name_hmac,
782            name_ct,
783            value_ct,
784            written_at,
785            sig,
786        }
787    }
788}
789
790#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
791#[serde(deny_unknown_fields)]
792#[non_exhaustive]
793pub struct ResealedBundle {
794    pub token_id: TokenId,
795    pub bundle: SignedBundle,
796}
797
798impl ResealedBundle {
799    pub fn new(token_id: TokenId, bundle: SignedBundle) -> ResealedBundle {
800        ResealedBundle { token_id, bundle }
801    }
802}
803
804#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
805#[non_exhaustive]
806pub struct RotationResponse {
807    pub generation: u32,
808}
809
810impl RotationResponse {
811    pub fn new(generation: u32) -> RotationResponse {
812        RotationResponse { generation }
813    }
814}
815
816/// `DELETE /v1/vault`, owner-signed.
817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
818#[non_exhaustive]
819pub struct DeleteVaultResponse {
820    pub vault_id: VaultId,
821}
822
823impl DeleteVaultResponse {
824    pub fn new(vault_id: VaultId) -> DeleteVaultResponse {
825        DeleteVaultResponse { vault_id }
826    }
827}
828
829/// `DELETE /v1/tokens/{id}` and `POST /v1/tokens/report`. Revocation is
830/// forward-only: key material a holder already unwrapped is not recalled.
831#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
832#[non_exhaustive]
833pub struct RevokeResponse {
834    pub revoked: Vec<TokenId>,
835}
836
837impl RevokeResponse {
838    pub fn new(revoked: Vec<TokenId>) -> RevokeResponse {
839        RevokeResponse { revoked }
840    }
841}
842
843// ---------------------------------------------------------------- tokens
844
845/// `POST /v1/tokens`, owner-signed: register a client-minted token. The
846/// server stores the two public keys; nothing it stores derives the box key.
847#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
848#[serde(deny_unknown_fields)]
849#[non_exhaustive]
850pub struct RegisterTokenRequest {
851    pub token_id: TokenId,
852    /// Verifies the token's request signatures.
853    pub auth_pub: Key32,
854    /// The token's bundle is sealed to this.
855    pub box_pub: Key32,
856    /// A request is strict: a scope the server does not know is refused.
857    pub scope: Scope,
858    pub ttl_secs: u64,
859    pub allow_list: Option<Vec<NameHmac>>,
860    pub generation: u32,
861    pub bundle: SignedBundle,
862}
863
864impl RegisterTokenRequest {
865    #[allow(clippy::too_many_arguments)]
866    pub fn new(
867        token_id: TokenId,
868        auth_pub: Key32,
869        box_pub: Key32,
870        scope: Scope,
871        ttl_secs: u64,
872        allow_list: Option<Vec<NameHmac>>,
873        generation: u32,
874        bundle: SignedBundle,
875    ) -> RegisterTokenRequest {
876        RegisterTokenRequest {
877            token_id,
878            auth_pub,
879            box_pub,
880            scope,
881            ttl_secs,
882            allow_list,
883            generation,
884            bundle,
885        }
886    }
887}
888
889#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
890#[non_exhaustive]
891pub struct RegisterTokenResponse {
892    pub token_id: TokenId,
893    pub expires_at: i64,
894}
895
896impl RegisterTokenResponse {
897    pub fn new(token_id: TokenId, expires_at: i64) -> RegisterTokenResponse {
898        RegisterTokenResponse {
899            token_id,
900            expires_at,
901        }
902    }
903}
904
905/// `GET /v1/tokens/self`: everything a token holder needs to verify its
906/// vault from the vault id in its token string.
907#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
908#[non_exhaustive]
909pub struct TokenSelf {
910    pub token_id: TokenId,
911    pub vault_id: VaultId,
912    /// A scope a newer server may name and this build not know: such a
913    /// token's bundle cannot be checked, so the client refuses to open it.
914    pub scope: Tolerant<Scope>,
915    pub expires_at: i64,
916    pub allow_list: Option<Vec<NameHmac>>,
917    pub owner_sign_pub: Key32,
918    pub descriptor: SignedDescriptor,
919    pub bundle: SignedBundle,
920}
921
922impl TokenSelf {
923    #[allow(clippy::too_many_arguments)]
924    pub fn new(
925        token_id: TokenId,
926        vault_id: VaultId,
927        scope: impl Into<Tolerant<Scope>>,
928        expires_at: i64,
929        allow_list: Option<Vec<NameHmac>>,
930        owner_sign_pub: Key32,
931        descriptor: SignedDescriptor,
932        bundle: SignedBundle,
933    ) -> TokenSelf {
934        TokenSelf {
935            token_id,
936            vault_id,
937            scope: scope.into(),
938            expires_at,
939            allow_list,
940            owner_sign_pub,
941            descriptor,
942            bundle,
943        }
944    }
945}
946
947/// `POST /v1/tokens/report`: a leaked token proves possession with its
948/// token-auth key. The token string itself is never sent.
949#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
950#[serde(deny_unknown_fields)]
951#[non_exhaustive]
952pub struct ReportTokenRequest {
953    pub token_id: TokenId,
954    pub ts: i64,
955    pub sig: Sig64,
956}
957
958impl ReportTokenRequest {
959    pub fn new(token_id: TokenId, ts: i64, sig: Sig64) -> ReportTokenRequest {
960        ReportTokenRequest { token_id, ts, sig }
961    }
962}
963
964/// What a token-auth key signs to report its token:
965/// `frame("gv/v1/report", token_id(16) ‖ ts(8))`.
966pub fn report_signing_input(token_id: &TokenId, ts: i64) -> Vec<u8> {
967    frame(label::REPORT, &[&token_id.0, &ts.to_be_bytes()])
968}
969
970// ---------------------------------------------------------------- records
971
972/// One live record in a listing. The hashes let a holder that cannot decrypt
973/// verify the record signature.
974#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
975#[non_exhaustive]
976pub struct SecretListItem {
977    pub name_hmac: NameHmac,
978    pub name_ct: B64,
979    pub version: u64,
980    pub written_at: i64,
981    pub size: u32,
982    pub tombstone: bool,
983    pub generation: u32,
984    pub value_ct_hash: Hash32,
985    pub sig: Sig64,
986}
987
988impl SecretListItem {
989    #[allow(clippy::too_many_arguments)]
990    pub fn new(
991        name_hmac: NameHmac,
992        name_ct: B64,
993        version: u64,
994        written_at: i64,
995        size: u32,
996        tombstone: bool,
997        generation: u32,
998        value_ct_hash: Hash32,
999        sig: Sig64,
1000    ) -> SecretListItem {
1001        SecretListItem {
1002            name_hmac,
1003            name_ct,
1004            version,
1005            written_at,
1006            size,
1007            tombstone,
1008            generation,
1009            value_ct_hash,
1010            sig,
1011        }
1012    }
1013}
1014
1015#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1016#[non_exhaustive]
1017pub struct SecretList {
1018    pub items: Vec<SecretListItem>,
1019    pub next_cursor: Option<NameHmac>,
1020}
1021
1022impl SecretList {
1023    pub fn new(items: Vec<SecretListItem>, next_cursor: Option<NameHmac>) -> SecretList {
1024        SecretList { items, next_cursor }
1025    }
1026}
1027
1028#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1029#[non_exhaustive]
1030pub struct SecretVersion {
1031    pub name_hmac: NameHmac,
1032    pub version: u64,
1033    pub name_ct: B64,
1034    /// `None` for a tombstone.
1035    pub value_ct: Option<B64>,
1036    pub generation: u32,
1037    pub written_at: i64,
1038    /// Who the server says wrote it: an actor kind a newer server may name
1039    /// and this build not know. Display only; the writer signature is what
1040    /// a client trusts.
1041    pub written_by: Tolerant<Actor>,
1042    pub tombstone: bool,
1043    pub sig: Sig64,
1044}
1045
1046impl SecretVersion {
1047    #[allow(clippy::too_many_arguments)]
1048    pub fn new(
1049        name_hmac: NameHmac,
1050        version: u64,
1051        name_ct: B64,
1052        value_ct: Option<B64>,
1053        generation: u32,
1054        written_at: i64,
1055        written_by: impl Into<Tolerant<Actor>>,
1056        tombstone: bool,
1057        sig: Sig64,
1058    ) -> SecretVersion {
1059        SecretVersion {
1060            name_hmac,
1061            version,
1062            name_ct,
1063            value_ct,
1064            generation,
1065            written_at,
1066            written_by: written_by.into(),
1067            tombstone,
1068            sig,
1069        }
1070    }
1071}
1072
1073/// `PUT /v1/secrets/{name_hmac}` or `/v1/configs/{name_hmac}`, with
1074/// `If-None-Match: *` (version 1) or `If-Match: v` (version v + 1). The
1075/// signature binds the version the writer intends to create.
1076#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1077#[serde(deny_unknown_fields)]
1078#[non_exhaustive]
1079pub struct PutSecretRequest {
1080    pub name_ct: B64,
1081    pub value_ct: B64,
1082    pub generation: u32,
1083    pub version: u64,
1084    pub written_at: i64,
1085    pub sig: Sig64,
1086}
1087
1088impl PutSecretRequest {
1089    pub fn new(
1090        name_ct: B64,
1091        value_ct: B64,
1092        generation: u32,
1093        version: u64,
1094        written_at: i64,
1095        sig: Sig64,
1096    ) -> PutSecretRequest {
1097        PutSecretRequest {
1098            name_ct,
1099            value_ct,
1100            generation,
1101            version,
1102            written_at,
1103            sig,
1104        }
1105    }
1106}
1107
1108/// `DELETE /v1/secrets/{name_hmac}` or `/v1/configs/{name_hmac}` with
1109/// `If-Match: v`: a signed tombstone as version v + 1.
1110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1111#[serde(deny_unknown_fields)]
1112#[non_exhaustive]
1113pub struct DeleteRecordRequest {
1114    pub name_ct: B64,
1115    pub generation: u32,
1116    pub version: u64,
1117    pub written_at: i64,
1118    pub sig: Sig64,
1119}
1120
1121impl DeleteRecordRequest {
1122    pub fn new(
1123        name_ct: B64,
1124        generation: u32,
1125        version: u64,
1126        written_at: i64,
1127        sig: Sig64,
1128    ) -> DeleteRecordRequest {
1129        DeleteRecordRequest {
1130            name_ct,
1131            generation,
1132            version,
1133            written_at,
1134            sig,
1135        }
1136    }
1137}
1138
1139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1140#[non_exhaustive]
1141pub struct PutSecretResponse {
1142    pub version: u64,
1143}
1144
1145impl PutSecretResponse {
1146    pub fn new(version: u64) -> PutSecretResponse {
1147        PutSecretResponse { version }
1148    }
1149}
1150
1151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1152#[non_exhaustive]
1153pub struct VersionMeta {
1154    pub version: u64,
1155    pub written_at: i64,
1156    /// Display only, as in [`SecretVersion`].
1157    pub written_by: Tolerant<Actor>,
1158    pub size: u32,
1159    pub tombstone: bool,
1160    pub generation: u32,
1161    pub value_ct_hash: Hash32,
1162    pub name_ct_hash: Hash32,
1163    pub sig: Sig64,
1164}
1165
1166impl VersionMeta {
1167    #[allow(clippy::too_many_arguments)]
1168    pub fn new(
1169        version: u64,
1170        written_at: i64,
1171        written_by: impl Into<Tolerant<Actor>>,
1172        size: u32,
1173        tombstone: bool,
1174        generation: u32,
1175        value_ct_hash: Hash32,
1176        name_ct_hash: Hash32,
1177        sig: Sig64,
1178    ) -> VersionMeta {
1179        VersionMeta {
1180            version,
1181            written_at,
1182            written_by: written_by.into(),
1183            size,
1184            tombstone,
1185            generation,
1186            value_ct_hash,
1187            name_ct_hash,
1188            sig,
1189        }
1190    }
1191}
1192
1193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1194#[non_exhaustive]
1195pub struct VersionList {
1196    pub versions: Vec<VersionMeta>,
1197}
1198
1199impl VersionList {
1200    pub fn new(versions: Vec<VersionMeta>) -> VersionList {
1201        VersionList { versions }
1202    }
1203}
1204
1205/// `GET /v1/audit?after={seq}`.
1206///
1207/// Read tolerantly (`docs/spec/audit.md#4`): `rows` holds the rows up to the
1208/// first one this build cannot verify, and `stop` says where and why. A row
1209/// in a newer row format, or one naming an action or actor this build does
1210/// not know, ends the rows a client can check; the ones after it are not
1211/// kept. A server never sets `stop`, and it is not serialized.
1212#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1213#[non_exhaustive]
1214pub struct AuditPage {
1215    pub rows: Vec<AuditRow>,
1216    pub head: Option<ChainHead>,
1217    #[serde(skip)]
1218    pub stop: Option<RowStop>,
1219}
1220
1221impl AuditPage {
1222    pub fn new(rows: Vec<AuditRow>, head: Option<ChainHead>) -> AuditPage {
1223        AuditPage {
1224            rows,
1225            head,
1226            stop: None,
1227        }
1228    }
1229}
1230
1231impl<'de> Deserialize<'de> for AuditPage {
1232    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1233        /// The page as sent. Unknown page fields are ignored, like any
1234        /// response's.
1235        #[derive(Deserialize)]
1236        struct Raw {
1237            rows: Vec<serde_json::Value>,
1238            head: Option<ChainHead>,
1239        }
1240        let raw = Raw::deserialize(d)?;
1241        let (rows, stop) =
1242            crate::proto::audit::read_rows(raw.rows).map_err(serde::de::Error::custom)?;
1243        Ok(AuditPage {
1244            rows,
1245            head: raw.head,
1246            stop,
1247        })
1248    }
1249}
1250
1251/// The age v1 header every value ciphertext must begin with.
1252pub const AGE_HEADER: &[u8] = b"age-encryption.org/v1\n";
1253
1254// ---------------------------------------------------------------- errors
1255
1256/// The stable error codes (`docs/spec/http-api.md#5`). Codes are only ever
1257/// added: none is renamed, removed while a supported client may receive it,
1258/// or reused with a different meaning. A client reads an [`ErrorBody`]'s code
1259/// as a [`Tolerant`], so a code added later keeps its string.
1260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1261#[serde(rename_all = "snake_case")]
1262#[non_exhaustive]
1263pub enum ErrorCode {
1264    /// The one body for every authentication failure: unknown token, revoked
1265    /// token, expired vault, bad or replayed signature, a bearer or v1
1266    /// credential.
1267    Unauthorized,
1268    TokenExpired,
1269    Forbidden,
1270    NotFound,
1271    Conflict,
1272    StaleGeneration,
1273    ChallengeReused,
1274    PreconditionFailed,
1275    ValueTooLarge,
1276    InvalidRequest,
1277    CredentialInQuery,
1278    ChallengeExpired,
1279    InvalidProofOfWork,
1280    VaultIdMismatch,
1281    NotAgeCiphertext,
1282    IncompleteRotation,
1283    TtlTooLong,
1284    VaultQuotaExceeded,
1285    NameQuotaExceeded,
1286    TokenQuotaExceeded,
1287    ConfigTooLarge,
1288    ConfigQuotaExceeded,
1289    PreconditionRequired,
1290    RateLimited,
1291    Unavailable,
1292    Internal,
1293    /// A descriptor, bundle, record or children signature does not verify.
1294    BadSignature,
1295    /// A record's signed version is not the version the server would assign.
1296    VersionMismatch,
1297    /// A key does not match the vault's descriptor or pinned id.
1298    KeyMismatch,
1299    /// The server shows an older record version than the client saw.
1300    VersionRollback,
1301    /// The server shows an older or different generation than the client saw.
1302    GenerationRollback,
1303}
1304
1305/// Whether a request refused with a code may be sent again
1306/// (`docs/spec/http-api.md#5`).
1307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1308#[non_exhaustive]
1309pub enum Retry {
1310    /// Nothing changed; the same operation, signed afresh, may succeed later
1311    /// (after `Retry-After`, where given).
1312    Yes,
1313    /// Nothing changed; it may succeed once rebuilt from fresh state (a new
1314    /// version, generation, revision or challenge).
1315    Refresh,
1316    /// It fails again as sent: the request, the credential or the vault
1317    /// must change.
1318    No,
1319}
1320
1321impl Retry {
1322    /// The error table's word for it.
1323    pub fn as_str(self) -> &'static str {
1324        match self {
1325            Retry::Yes => "yes",
1326            Retry::Refresh => "refresh",
1327            Retry::No => "no",
1328        }
1329    }
1330}
1331
1332impl ErrorCode {
1333    pub const ALL: [ErrorCode; 31] = [
1334        ErrorCode::Unauthorized,
1335        ErrorCode::TokenExpired,
1336        ErrorCode::Forbidden,
1337        ErrorCode::NotFound,
1338        ErrorCode::Conflict,
1339        ErrorCode::StaleGeneration,
1340        ErrorCode::ChallengeReused,
1341        ErrorCode::PreconditionFailed,
1342        ErrorCode::ValueTooLarge,
1343        ErrorCode::InvalidRequest,
1344        ErrorCode::CredentialInQuery,
1345        ErrorCode::ChallengeExpired,
1346        ErrorCode::InvalidProofOfWork,
1347        ErrorCode::VaultIdMismatch,
1348        ErrorCode::NotAgeCiphertext,
1349        ErrorCode::IncompleteRotation,
1350        ErrorCode::TtlTooLong,
1351        ErrorCode::VaultQuotaExceeded,
1352        ErrorCode::NameQuotaExceeded,
1353        ErrorCode::TokenQuotaExceeded,
1354        ErrorCode::ConfigTooLarge,
1355        ErrorCode::ConfigQuotaExceeded,
1356        ErrorCode::PreconditionRequired,
1357        ErrorCode::RateLimited,
1358        ErrorCode::Unavailable,
1359        ErrorCode::Internal,
1360        ErrorCode::BadSignature,
1361        ErrorCode::VersionMismatch,
1362        ErrorCode::KeyMismatch,
1363        ErrorCode::VersionRollback,
1364        ErrorCode::GenerationRollback,
1365    ];
1366
1367    pub fn status(self) -> u16 {
1368        use ErrorCode::*;
1369        match self {
1370            InvalidRequest | CredentialInQuery | ChallengeExpired | InvalidProofOfWork => 400,
1371            Unauthorized | TokenExpired => 401,
1372            Forbidden => 403,
1373            NotFound => 404,
1374            Conflict | StaleGeneration | ChallengeReused | VersionMismatch | VersionRollback
1375            | GenerationRollback => 409,
1376            PreconditionFailed => 412,
1377            ValueTooLarge | ConfigTooLarge => 413,
1378            VaultIdMismatch | NotAgeCiphertext | IncompleteRotation | TtlTooLong
1379            | VaultQuotaExceeded | NameQuotaExceeded | TokenQuotaExceeded | ConfigQuotaExceeded
1380            | BadSignature | KeyMismatch => 422,
1381            PreconditionRequired => 428,
1382            RateLimited => 429,
1383            Internal => 500,
1384            Unavailable => 503,
1385        }
1386    }
1387
1388    /// Whether a request refused with this code may be sent again.
1389    pub fn retry(self) -> Retry {
1390        use ErrorCode::*;
1391        match self {
1392            RateLimited | Unavailable | Internal => Retry::Yes,
1393            Conflict | StaleGeneration | ChallengeReused | ChallengeExpired
1394            | PreconditionFailed | VersionMismatch => Retry::Refresh,
1395            _ => Retry::No,
1396        }
1397    }
1398
1399    /// The wire string.
1400    pub fn as_str(self) -> &'static str {
1401        use ErrorCode::*;
1402        match self {
1403            Unauthorized => "unauthorized",
1404            TokenExpired => "token_expired",
1405            Forbidden => "forbidden",
1406            NotFound => "not_found",
1407            Conflict => "conflict",
1408            StaleGeneration => "stale_generation",
1409            ChallengeReused => "challenge_reused",
1410            PreconditionFailed => "precondition_failed",
1411            ValueTooLarge => "value_too_large",
1412            InvalidRequest => "invalid_request",
1413            CredentialInQuery => "credential_in_query",
1414            ChallengeExpired => "challenge_expired",
1415            InvalidProofOfWork => "invalid_proof_of_work",
1416            VaultIdMismatch => "vault_id_mismatch",
1417            NotAgeCiphertext => "not_age_ciphertext",
1418            IncompleteRotation => "incomplete_rotation",
1419            TtlTooLong => "ttl_too_long",
1420            VaultQuotaExceeded => "vault_quota_exceeded",
1421            NameQuotaExceeded => "name_quota_exceeded",
1422            TokenQuotaExceeded => "token_quota_exceeded",
1423            ConfigTooLarge => "config_too_large",
1424            ConfigQuotaExceeded => "config_quota_exceeded",
1425            PreconditionRequired => "precondition_required",
1426            RateLimited => "rate_limited",
1427            Unavailable => "unavailable",
1428            Internal => "internal",
1429            BadSignature => "bad_signature",
1430            VersionMismatch => "version_mismatch",
1431            KeyMismatch => "key_mismatch",
1432            VersionRollback => "version_rollback",
1433            GenerationRollback => "generation_rollback",
1434        }
1435    }
1436
1437    /// The inverse of [`ErrorCode::as_str`].
1438    pub fn parse(s: &str) -> Option<ErrorCode> {
1439        ErrorCode::ALL.into_iter().find(|c| c.as_str() == s)
1440    }
1441}
1442
1443impl std::fmt::Display for ErrorCode {
1444    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1445        f.write_str(self.as_str())
1446    }
1447}
1448
1449impl Vocabulary for ErrorCode {
1450    fn knows(name: &str) -> bool {
1451        ErrorCode::parse(name).is_some()
1452    }
1453}
1454
1455/// Every error response. Never contains ciphertext, token material or a
1456/// client address.
1457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1458#[non_exhaustive]
1459pub struct ErrorBody {
1460    /// A code a newer server may add and this build not know.
1461    pub error: Tolerant<ErrorCode>,
1462    pub message: String,
1463}
1464
1465impl ErrorBody {
1466    pub fn new(code: ErrorCode, message: impl Into<String>) -> ErrorBody {
1467        ErrorBody {
1468            error: Tolerant::Known(code),
1469            message: message.into(),
1470        }
1471    }
1472}
1473
1474#[cfg(test)]
1475mod tests {
1476    use super::*;
1477    use crate::proto::audit::{AuditAction, AuditEvent, AuditResult, GENESIS};
1478
1479    /// The bound covers a worst-case batch serialized for real, and is not
1480    /// much looser than it.
1481    #[test]
1482    fn the_rotation_bound_covers_a_real_worst_case_batch() {
1483        let limits = Limits {
1484            max_names: 3,
1485            max_value_bytes: 4096,
1486            max_versions: 2,
1487            max_vault_bytes: 10_000,
1488            max_tokens: 4,
1489            max_configs: 2,
1490            max_config_bytes: 4096,
1491            max_config_versions: 2,
1492        };
1493        let versions = (limits.max_names * limits.max_versions
1494            + limits.max_configs * limits.max_config_versions) as usize;
1495        let value_len = limits.max_vault_bytes as usize / versions;
1496        let version = |_| RotatedVersion {
1497            old_name_hmac: NameHmac([0xff; 32]),
1498            version: u64::MAX,
1499            name_hmac: NameHmac([0xee; 32]),
1500            name_ct: B64(vec![0xab; 24 + 256 + 16]),
1501            value_ct: Some(B64(vec![0xcd; value_len])),
1502            written_at: i64::MIN,
1503            sig: Sig64([0x11; 64]),
1504        };
1505        let bundle = || SignedBundle {
1506            sealed: B64(vec![2; 48 + 198]),
1507            sig: Sig64([0x22; 64]),
1508        };
1509        let secret_count = (limits.max_names * limits.max_versions) as usize;
1510        let config_count = (limits.max_configs * limits.max_config_versions) as usize;
1511        let request = RotationRequest {
1512            from_generation: u32::MAX,
1513            from_revision: u64::MAX,
1514            descriptor: SignedDescriptor {
1515                descriptor: B64(vec![3; 189]),
1516                sig: Sig64([0x33; 64]),
1517            },
1518            owner_bundle: bundle(),
1519            secrets: (0..secret_count).map(version).collect(),
1520            configs: (0..config_count).map(version).collect(),
1521            tokens: (0..limits.max_tokens)
1522                .map(|_| ResealedBundle {
1523                    token_id: TokenId([0xaa; 16]),
1524                    bundle: bundle(),
1525                })
1526                .collect(),
1527            revoke: vec![TokenId([0xbb; 16]); limits.max_tokens as usize],
1528        };
1529        let len = serde_json::to_vec(&request).unwrap().len();
1530        let bound = limits.max_rotation_body();
1531        assert!(len <= bound, "{len} > {bound}");
1532        assert!(bound < len + 8 * 1024, "{bound} is loose against {len}");
1533    }
1534
1535    #[test]
1536    fn scope_policy_matches_the_spec() {
1537        use Scope::*;
1538        // Order: meta, append, read, admin, config, config-write.
1539        assert_eq!(
1540            Scope::ALL.map(Scope::bundle_holds_vault_key),
1541            [false, false, true, true, false, false]
1542        );
1543        assert_eq!(
1544            Scope::ALL.map(Scope::bundle_holds_config_key),
1545            [false, false, true, true, true, true]
1546        );
1547        assert_eq!(
1548            Scope::ALL.map(Scope::bundle_holds_secret_writer),
1549            [false, true, false, true, false, false]
1550        );
1551        assert_eq!(
1552            Scope::ALL.map(Scope::bundle_holds_config_writer),
1553            [false, true, false, true, false, true]
1554        );
1555        // Server permissions agree with the key material.
1556        for s in Scope::ALL {
1557            assert_eq!(s.can_read_values(), s.bundle_holds_vault_key(), "{s}");
1558            assert_eq!(s.can_read_configs(), s.bundle_holds_config_key(), "{s}");
1559            assert_eq!(s.can_write_values(), s.bundle_holds_secret_writer(), "{s}");
1560            assert_eq!(s.can_write_configs(), s.bundle_holds_config_writer(), "{s}");
1561            assert!(s.can_read_audit(), "{s}");
1562        }
1563        assert_eq!(
1564            Scope::ALL.map(Scope::can_manage),
1565            [false, false, false, true, false, false]
1566        );
1567        let codes: std::collections::BTreeSet<u8> = Scope::ALL.map(Scope::code).into();
1568        assert_eq!(codes.len(), 6);
1569        assert!(!codes.contains(&OWNER_SCOPE_CODE));
1570        for s in Scope::ALL {
1571            assert_eq!(Scope::parse(&s.to_string()), Some(s));
1572            assert_eq!(serde_json::to_string(&s).unwrap(), format!("\"{s}\""));
1573        }
1574        assert_eq!(Admin.default_ttl_secs(), DAY);
1575        assert_eq!(Admin.max_ttl_secs(), 30 * DAY);
1576        for s in [Meta, Append, Read, Config, ConfigWrite] {
1577            assert_eq!(s.default_ttl_secs(), 90 * DAY);
1578            assert_eq!(s.max_ttl_secs(), 365 * DAY);
1579        }
1580    }
1581
1582    #[test]
1583    fn error_codes_are_snake_case_with_stable_statuses() {
1584        let body = ErrorBody::new(ErrorCode::VaultQuotaExceeded, "m");
1585        assert_eq!(
1586            serde_json::to_string(&body).unwrap(),
1587            r#"{"error":"vault_quota_exceeded","message":"m"}"#
1588        );
1589        assert_eq!(ErrorCode::Unauthorized.status(), 401);
1590        assert_eq!(ErrorCode::PreconditionRequired.status(), 428);
1591        assert_eq!(ErrorCode::PreconditionFailed.status(), 412);
1592        assert_eq!(ErrorCode::Unavailable.status(), 503);
1593        assert_eq!(ErrorCode::VersionMismatch.status(), 409);
1594        assert_eq!(ErrorCode::BadSignature.status(), 422);
1595        assert_eq!(
1596            serde_json::to_string(&ErrorCode::GenerationRollback).unwrap(),
1597            "\"generation_rollback\""
1598        );
1599        // `as_str` is exactly what serde writes, for every code, and codes
1600        // are unique.
1601        let mut seen = std::collections::BTreeSet::new();
1602        for c in ErrorCode::ALL {
1603            assert_eq!(serde_json::to_string(&c).unwrap(), format!("\"{c}\""));
1604            assert_eq!(ErrorCode::parse(c.as_str()), Some(c));
1605            assert!(seen.insert(c.as_str()), "{c} twice");
1606        }
1607    }
1608
1609    #[test]
1610    fn an_error_body_keeps_a_code_this_build_does_not_know() {
1611        let body: ErrorBody =
1612            serde_json::from_str(r#"{"error":"future_conflict","message":"later"}"#).unwrap();
1613        assert_eq!(body.error.unknown(), Some("future_conflict"));
1614        assert_eq!(body.message, "later");
1615        let body: ErrorBody =
1616            serde_json::from_str(r#"{"error":"conflict","message":"now","hint":1}"#).unwrap();
1617        assert_eq!(body.error, ErrorCode::Conflict);
1618    }
1619
1620    #[test]
1621    fn requests_refuse_unknown_fields() {
1622        let json = format!(
1623            r#"{{"name_ct":"AA","value_ct":"AA","generation":1,"version":1,"written_at":0,"sig":"{}","plaintext":"x"}}"#,
1624            B64::encode_str(&[0; 64])
1625        );
1626        assert!(serde_json::from_str::<PutSecretRequest>(&json).is_err());
1627        let ok = json.replace(r#","plaintext":"x""#, "");
1628        assert!(serde_json::from_str::<PutSecretRequest>(&ok).is_ok());
1629        // And a scope a server does not know is a refusal, not a guess.
1630        let register = serde_json::to_value(RegisterTokenRequest::new(
1631            TokenId([1; 16]),
1632            Key32([2; 32]),
1633            Key32([3; 32]),
1634            Scope::Read,
1635            0,
1636            None,
1637            1,
1638            SignedBundle::new(B64(vec![1]), Sig64([0; 64])),
1639        ))
1640        .unwrap();
1641        let mut future = register.clone();
1642        future["scope"] = "future".into();
1643        assert!(serde_json::from_value::<RegisterTokenRequest>(register).is_ok());
1644        assert!(serde_json::from_value::<RegisterTokenRequest>(future).is_err());
1645    }
1646
1647    #[test]
1648    fn bundle_and_report_inputs_are_framed_and_binding() {
1649        let base = bundle_signing_input(&VaultId([1; 16]), &TokenId([2; 16]), 3, 4, b"sealed");
1650        assert!(base.starts_with(b"gv/v1/bundle\0"));
1651        for other in [
1652            bundle_signing_input(&VaultId([9; 16]), &TokenId([2; 16]), 3, 4, b"sealed"),
1653            bundle_signing_input(&VaultId([1; 16]), &TokenId([9; 16]), 3, 4, b"sealed"),
1654            bundle_signing_input(&VaultId([1; 16]), &TokenId([2; 16]), 4, 4, b"sealed"),
1655            bundle_signing_input(&VaultId([1; 16]), &TokenId([2; 16]), 3, 5, b"sealed"),
1656            bundle_signing_input(&VaultId([1; 16]), &TokenId([2; 16]), 3, 4, b"sealeD"),
1657        ] {
1658            assert_ne!(other, base);
1659        }
1660        let report = report_signing_input(&TokenId([1; 16]), 42);
1661        assert!(report.starts_with(b"gv/v1/report\0"));
1662        assert_ne!(report, report_signing_input(&TokenId([1; 16]), 43));
1663    }
1664
1665    #[test]
1666    fn capabilities_extend_split_hosted_servers_document() {
1667        // A document from before `protocols`, `formats` and `features`.
1668        let old: Capabilities = serde_json::from_str(
1669            r#"{"proof_of_work":{"difficulty":8},"idle_expiry_days":null,"limits":{}}"#,
1670        )
1671        .unwrap();
1672        assert!(old.speaks(PROTOCOL));
1673        assert_eq!(old.formats, None);
1674        assert!(old.features.is_empty() && old.server.is_none());
1675        assert_eq!(old.proof_of_work, Some(ProofOfWork::new(8)));
1676
1677        let now =
1678            Capabilities::new(None, None, Limits::default()).with_features([feature::TOKEN_REPORT]);
1679        let json = serde_json::to_value(&now).unwrap();
1680        assert_eq!(json["protocols"], serde_json::json!(["1"]));
1681        assert!(json["proof_of_work"].is_null() && json["idle_expiry_days"].is_null());
1682        assert_eq!(json["formats"]["audit_row"], 1);
1683        assert_eq!(json["formats"]["envelope"], 1);
1684        assert!(json.get("server").is_none(), "the version is opt-in");
1685        let newer: Capabilities =
1686            serde_json::from_str(r#"{"protocols":["2"],"limits":{},"quantum":true}"#).unwrap();
1687        assert!(!newer.speaks(PROTOCOL));
1688    }
1689
1690    /// Every response body parses with an unknown field added at every level
1691    /// (`docs/spec/http-api.md#7`); only the audit row, whose fields are all
1692    /// hashed, refuses one, and the page still reads up to it.
1693    #[test]
1694    fn every_response_tolerates_an_unknown_field() {
1695        fn inject(v: &mut serde_json::Value) {
1696            match v {
1697                serde_json::Value::Object(m) => {
1698                    for (_, child) in m.iter_mut() {
1699                        inject(child);
1700                    }
1701                    m.insert("zz_added_later".into(), serde_json::json!({"x": [1, 2]}));
1702                }
1703                serde_json::Value::Array(items) => items.iter_mut().for_each(inject),
1704                _ => {}
1705            }
1706        }
1707        fn check<T: Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug>(
1708            value: T,
1709        ) {
1710            let mut json = serde_json::to_value(&value).unwrap();
1711            inject(&mut json);
1712            let back: T =
1713                serde_json::from_value(json.clone()).unwrap_or_else(|e| panic!("{e}: {json}"));
1714            assert_eq!(back, value);
1715        }
1716        let sig = Sig64([7; 64]);
1717        let descriptor = SignedDescriptor::from_parts(B64(vec![2; 189]), sig);
1718        let bundle = SignedBundle::new(B64(vec![5; 80]), sig);
1719        let token = TokenSummary::new(TokenId([1; 16]), Scope::Read, 1, 2, Key32([3; 32]), None);
1720        let status = VaultStatus::new(
1721            VaultId([1; 16]),
1722            1,
1723            2,
1724            Key32([1; 32]),
1725            Key32([2; 32]),
1726            descriptor.clone(),
1727            0,
1728            1,
1729            2,
1730            None,
1731            3,
1732            Limits::default(),
1733            Some(vec![token.clone()]),
1734            Some(bundle.clone()),
1735        );
1736        check(ChallengeResponse::new("c".into(), 8, 9));
1737        check(CreateVaultResponse::new(VaultId([1; 16]), 1, None));
1738        check(Capabilities::new(None, Some(90), Limits::default()));
1739        check(status);
1740        check(DescriptorList::new(vec![descriptor.clone()]));
1741        check(RotationResponse::new(2));
1742        check(DeleteVaultResponse::new(VaultId([4; 16])));
1743        check(RevokeResponse::new(vec![TokenId([4; 16])]));
1744        check(RegisterTokenResponse::new(TokenId([4; 16]), 5));
1745        check(TokenSelf::new(
1746            TokenId([1; 16]),
1747            VaultId([2; 16]),
1748            Scope::Meta,
1749            3,
1750            None,
1751            Key32([4; 32]),
1752            descriptor.clone(),
1753            bundle.clone(),
1754        ));
1755        let item = SecretListItem::new(
1756            NameHmac([1; 32]),
1757            B64(vec![1]),
1758            1,
1759            2,
1760            3,
1761            false,
1762            1,
1763            Hash32([2; 32]),
1764            sig,
1765        );
1766        check(SecretList::new(vec![item], None));
1767        check(SecretVersion::new(
1768            NameHmac([1; 32]),
1769            1,
1770            B64(vec![1]),
1771            Some(B64(vec![2])),
1772            1,
1773            2,
1774            Actor::Token(TokenId([9; 16])),
1775            false,
1776            sig,
1777        ));
1778        check(PutSecretResponse::new(3));
1779        check(VersionList::new(vec![VersionMeta::new(
1780            1,
1781            2,
1782            Actor::Owner,
1783            3,
1784            false,
1785            1,
1786            Hash32([1; 32]),
1787            Hash32([2; 32]),
1788            sig,
1789        )]));
1790        check(crate::proto::children::ChildrenBlob::new(
1791            1,
1792            B64(vec![1]),
1793            sig,
1794        ));
1795        check(ErrorBody::new(ErrorCode::Forbidden, "m"));
1796
1797        // The audit page: fields added to the page and its head are fine. A
1798        // row in a known format with an added field is malformed: a new row
1799        // field comes with a new row format (`docs/spec/audit.md#4`).
1800        let row = crate::proto::audit::AuditRow::from_event(
1801            1,
1802            5,
1803            GENESIS,
1804            AuditEvent::simple(
1805                Actor::Owner,
1806                AuditAction::VaultCreate,
1807                None,
1808                AuditResult::Ok,
1809            ),
1810        );
1811        let page = AuditPage::new(vec![row.clone()], Some(row.head()));
1812        let mut json = serde_json::to_value(&page).unwrap();
1813        json["zz_added_later"] = 1.into();
1814        json["head"]["zz_added_later"] = 1.into();
1815        assert_eq!(
1816            serde_json::from_value::<AuditPage>(json.clone()).unwrap(),
1817            page
1818        );
1819        json["rows"][0]["zz_added_later"] = 1.into();
1820        assert!(serde_json::from_value::<AuditPage>(json).is_err());
1821    }
1822}