1use 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
34pub const PROTOCOL: &str = "1";
36
37pub const BUNDLE_VERSION: u8 = 1;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44#[non_exhaustive]
45pub enum Scope {
46 Meta,
48 Append,
50 Read,
52 Admin,
55 Config,
58 #[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 pub fn can_read_values(self) -> bool {
77 matches!(self, Scope::Read | Scope::Admin)
78 }
79
80 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 pub fn can_manage(self) -> bool {
99 self == Scope::Admin
100 }
101
102 pub fn can_read_audit(self) -> bool {
104 true
105 }
106
107 pub fn bundle_holds_vault_key(self) -> bool {
109 matches!(self, Scope::Read | Scope::Admin)
110 }
111
112 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 pub fn bundle_holds_secret_writer(self) -> bool {
122 matches!(self, Scope::Append | Scope::Admin)
123 }
124
125 pub fn bundle_holds_config_writer(self) -> bool {
127 matches!(self, Scope::Append | Scope::Admin | Scope::ConfigWrite)
128 }
129
130 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 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 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
186pub const OWNER_SCOPE_CODE: u8 = 0;
188pub const OWNER_TOKEN_ID: TokenId = TokenId([0; 16]);
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197#[non_exhaustive]
198pub struct SignedBundle {
199 pub sealed: B64,
200 pub sig: Sig64,
201}
202
203pub 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 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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292#[serde(deny_unknown_fields)]
293#[non_exhaustive]
294pub struct CreateVaultRequest {
295 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub challenge: Option<String>,
300 #[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 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 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 #[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
358pub mod feature {
363 pub const TOKEN_REPORT: &str = "token_report";
366 pub const RATE_LIMITS: &str = "rate_limits";
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381#[non_exhaustive]
382pub struct Capabilities {
383 #[serde(default = "protocols_before_the_field")]
385 pub protocols: Vec<String>,
386 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub formats: Option<Formats>,
390 #[serde(default)]
392 pub proof_of_work: Option<ProofOfWork>,
393 #[serde(default)]
396 pub idle_expiry_days: Option<u32>,
397 #[serde(default)]
399 pub limits: Limits,
400 #[serde(default)]
402 pub features: Vec<String>,
403 #[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 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 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 pub fn with_server(mut self, server: Option<String>) -> Self {
440 self.server = server;
441 self
442 }
443
444 pub fn speaks(&self, protocol: &str) -> bool {
446 self.protocols.iter().any(|p| p == protocol)
447 }
448}
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
453#[serde(default)]
454#[non_exhaustive]
455pub struct Formats {
456 pub descriptor: u32,
458 pub bundle: u32,
460 pub envelope: u32,
462 pub children: u32,
464 pub audit_row: u32,
466 pub request_signature: u32,
468}
469
470impl Formats {
471 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
484#[non_exhaustive]
485pub struct ProofOfWork {
486 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 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 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; const TOKEN_HEX: u64 = 32; const DIGITS: u64 = 20; const NAME_CT: u64 = 24 + 256 + 16; const SIG: u64 = 86; const BUNDLE: u64 = 48 + 198; const DESCRIPTOR: u64 = 189;
573 const VERSION_FRAME: u64 = 104;
576 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 pub scope: Tolerant<Scope>,
603 pub created_at: i64,
604 pub expires_at: i64,
605 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
632#[non_exhaustive]
633pub struct VaultStatus {
634 pub vault_id: VaultId,
635 pub generation: u32,
636 pub revision: u64,
639 pub owner_sign_pub: Key32,
641 pub owner_box_pub: Key32,
642 pub descriptor: SignedDescriptor,
644 #[serde(default)]
645 pub config_count: u32,
646 pub created_at: i64,
647 pub last_active_at: i64,
648 #[serde(default)]
651 pub expires_at: Option<i64>,
652 pub bytes_used: u64,
653 pub limits: Limits,
654 pub tokens: Option<Vec<TokenSummary>>,
656 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#[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#[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 pub descriptor: SignedDescriptor,
719 pub owner_bundle: SignedBundle,
720 pub secrets: Vec<RotatedVersion>,
721 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#[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 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#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
848#[serde(deny_unknown_fields)]
849#[non_exhaustive]
850pub struct RegisterTokenRequest {
851 pub token_id: TokenId,
852 pub auth_pub: Key32,
854 pub box_pub: Key32,
856 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
908#[non_exhaustive]
909pub struct TokenSelf {
910 pub token_id: TokenId,
911 pub vault_id: VaultId,
912 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#[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
964pub 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#[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 pub value_ct: Option<B64>,
1036 pub generation: u32,
1037 pub written_at: i64,
1038 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#[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#[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 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#[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 #[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
1251pub const AGE_HEADER: &[u8] = b"age-encryption.org/v1\n";
1253
1254#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1261#[serde(rename_all = "snake_case")]
1262#[non_exhaustive]
1263pub enum ErrorCode {
1264 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 BadSignature,
1295 VersionMismatch,
1297 KeyMismatch,
1299 VersionRollback,
1301 GenerationRollback,
1303}
1304
1305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1308#[non_exhaustive]
1309pub enum Retry {
1310 Yes,
1313 Refresh,
1316 No,
1319}
1320
1321impl Retry {
1322 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 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1458#[non_exhaustive]
1459pub struct ErrorBody {
1460 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 #[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 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 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 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 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 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 #[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 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}