1use std::fmt;
14
15pub const SECURE_MESSAGE_VERSION: u32 = 1;
17pub const DEFAULT_EXPIRES_AFTER_SECS: u32 = 60;
19
20pub const CONTENT_TYPE_SIGN_REQUEST: &str = "application/basil.sign-request";
26pub const CONTENT_TYPE_SIGN_RESPONSE: &str = "application/basil.sign-response";
28pub const CONTENT_TYPE_MINT_JWT_REQUEST: &str = "application/basil.mint-jwt-request";
33pub const CONTENT_TYPE_MINT_JWT_RESPONSE: &str = "application/basil.mint-jwt-response";
37pub const CONTENT_TYPE_MINT_NATS_USER_REQUEST: &str = "application/basil.mint-nats-user-request";
42pub const CONTENT_TYPE_MINT_NATS_USER_RESPONSE: &str = "application/basil.mint-nats-user-response";
46
47pub const INVOCATION_CONTENT_TYPES: [&str; 6] = [
51 CONTENT_TYPE_SIGN_REQUEST,
52 CONTENT_TYPE_SIGN_RESPONSE,
53 CONTENT_TYPE_MINT_JWT_REQUEST,
54 CONTENT_TYPE_MINT_JWT_RESPONSE,
55 CONTENT_TYPE_MINT_NATS_USER_REQUEST,
56 CONTENT_TYPE_MINT_NATS_USER_RESPONSE,
57];
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum InvocationError {
62 MissingField(&'static str),
64 InvalidBody(&'static str),
66}
67
68impl fmt::Display for InvocationError {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 Self::MissingField(field) => write!(f, "missing required header field `{field}`"),
72 Self::InvalidBody(reason) => write!(f, "invalid invocation body: {reason}"),
73 }
74 }
75}
76
77impl std::error::Error for InvocationError {}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum InvocationStatusCode {
82 Ok = 1,
84 Denied = 2,
86 InvalidRequest = 3,
88 InternalError = 4,
90}
91
92impl InvocationStatusCode {
93 #[must_use]
95 pub const fn from_u64(value: u64) -> Option<Self> {
96 match value {
97 1 => Some(Self::Ok),
98 2 => Some(Self::Denied),
99 3 => Some(Self::InvalidRequest),
100 4 => Some(Self::InternalError),
101 _ => None,
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct InvocationStatus {
109 pub code: InvocationStatusCode,
111 pub reason: String,
113 pub message: Option<String>,
115 pub retryable: bool,
117}
118
119impl InvocationStatus {
120 #[must_use]
122 pub fn ok() -> Self {
123 Self {
124 code: InvocationStatusCode::Ok,
125 reason: "OK".to_string(),
126 message: None,
127 retryable: false,
128 }
129 }
130
131 #[must_use]
133 pub fn denied() -> Self {
134 Self {
135 code: InvocationStatusCode::Denied,
136 reason: "UNAUTHORIZED".to_string(),
137 message: Some("not authorized".to_string()),
138 retryable: false,
139 }
140 }
141
142 #[must_use]
144 pub fn invalid_request(reason: impl Into<String>) -> Self {
145 Self {
146 code: InvocationStatusCode::InvalidRequest,
147 reason: reason.into(),
148 message: None,
149 retryable: false,
150 }
151 }
152
153 #[must_use]
155 pub fn internal_error() -> Self {
156 Self {
157 code: InvocationStatusCode::InternalError,
158 reason: "INTERNAL_ERROR".to_string(),
159 message: None,
160 retryable: true,
161 }
162 }
163
164 fn encode_cbor(&self, out: &mut Vec<u8>) {
165 cbor_map(out, 4);
166 cbor_u64(out, 1);
167 cbor_u64(out, self.code as u64);
168 cbor_u64(out, 2);
169 cbor_text(out, &self.reason);
170 cbor_u64(out, 3);
171 cbor_optional_text(out, self.message.as_deref());
172 cbor_u64(out, 4);
173 cbor_bool(out, self.retryable);
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct SignInvocationRequest {
180 pub key_id: String,
182 pub message: Vec<u8>,
184 pub algorithm: i32,
186}
187
188impl SignInvocationRequest {
189 #[must_use]
191 pub fn to_cbor_bytes(&self) -> Vec<u8> {
192 let mut out = Vec::new();
193 cbor_map(&mut out, 3);
194 cbor_u64(&mut out, 1);
195 cbor_text(&mut out, &self.key_id);
196 cbor_u64(&mut out, 2);
197 cbor_bytes(&mut out, &self.message);
198 cbor_u64(&mut out, 3);
199 cbor_i64(&mut out, i64::from(self.algorithm));
200 out
201 }
202
203 pub fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, InvocationError> {
209 let mut decoder = CborDecoder::new(bytes);
210 decoder.map_len(3, "SignInvocationRequest")?;
211 decoder.key(1)?;
212 let key_id = decoder.text("key_id")?;
213 decoder.key(2)?;
214 let message = decoder.bytes("message")?;
215 decoder.key(3)?;
216 let algorithm = decoder.i64("algorithm")?;
217 decoder.finish()?;
218 let algorithm = i32::try_from(algorithm)
219 .map_err(|_| InvocationError::InvalidBody("algorithm out of range"))?;
220 Ok(Self {
221 key_id,
222 message,
223 algorithm,
224 })
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct SignInvocationResponse {
231 pub status: InvocationStatus,
233 pub policy_generation: u64,
235 pub signature: Option<Vec<u8>>,
237}
238
239impl SignInvocationResponse {
240 #[must_use]
242 pub fn to_cbor_bytes(&self) -> Vec<u8> {
243 let mut out = Vec::new();
244 cbor_map(&mut out, 3);
245 cbor_u64(&mut out, 1);
246 self.status.encode_cbor(&mut out);
247 cbor_u64(&mut out, 2);
248 cbor_u64(&mut out, self.policy_generation);
249 cbor_u64(&mut out, 3);
250 cbor_optional_bytes(&mut out, self.signature.as_deref());
251 out
252 }
253
254 pub fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, InvocationError> {
260 let mut decoder = CborDecoder::new(bytes);
261 decoder.map_len(3, "SignInvocationResponse")?;
262 decoder.key(1)?;
263 let status = decoder.status()?;
264 decoder.key(2)?;
265 let policy_generation = decoder.u64("policy_generation")?;
266 decoder.key(3)?;
267 let signature = decoder.optional_bytes("signature")?;
268 decoder.finish()?;
269 if status.code == InvocationStatusCode::Ok && signature.is_none() {
270 return Err(InvocationError::InvalidBody(
271 "successful sign response missing signature",
272 ));
273 }
274 if status.code != InvocationStatusCode::Ok && signature.is_some() {
275 return Err(InvocationError::InvalidBody(
276 "non-success sign response carries signature",
277 ));
278 }
279 Ok(Self {
280 status,
281 policy_generation,
282 signature,
283 })
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct MintJwtInvocationResponse {
294 pub status: InvocationStatus,
296 pub policy_generation: u64,
298 pub jwt: Option<String>,
300 pub expires_at_unix: Option<u64>,
302}
303
304impl MintJwtInvocationResponse {
305 #[must_use]
307 pub fn to_cbor_bytes(&self) -> Vec<u8> {
308 let mut out = Vec::new();
309 cbor_map(&mut out, 4);
310 cbor_u64(&mut out, 1);
311 self.status.encode_cbor(&mut out);
312 cbor_u64(&mut out, 2);
313 cbor_u64(&mut out, self.policy_generation);
314 cbor_u64(&mut out, 3);
315 cbor_optional_text(&mut out, self.jwt.as_deref());
316 cbor_u64(&mut out, 4);
317 cbor_optional_u64(&mut out, self.expires_at_unix);
318 out
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
328pub struct MintNatsUserInvocationResponse {
329 pub status: InvocationStatus,
331 pub policy_generation: u64,
333 pub jwt: Option<String>,
335 pub expires_at_unix: Option<u64>,
337}
338
339impl MintNatsUserInvocationResponse {
340 #[must_use]
342 pub fn to_cbor_bytes(&self) -> Vec<u8> {
343 let mut out = Vec::new();
344 cbor_map(&mut out, 4);
345 cbor_u64(&mut out, 1);
346 self.status.encode_cbor(&mut out);
347 cbor_u64(&mut out, 2);
348 cbor_u64(&mut out, self.policy_generation);
349 cbor_u64(&mut out, 3);
350 cbor_optional_text(&mut out, self.jwt.as_deref());
351 cbor_u64(&mut out, 4);
352 cbor_optional_u64(&mut out, self.expires_at_unix);
353 out
354 }
355}
356
357#[derive(Debug, Clone, PartialEq, Eq)]
363pub struct MintJwtInvocationRequest {
364 pub key_id: String,
366 pub subject: Option<String>,
368 pub ttl_secs: Option<u64>,
370 pub claims_json: Vec<u8>,
372}
373
374impl MintJwtInvocationRequest {
375 #[must_use]
377 pub fn to_cbor_bytes(&self) -> Vec<u8> {
378 let mut out = Vec::new();
379 cbor_map(&mut out, 4);
380 cbor_u64(&mut out, 1);
381 cbor_text(&mut out, &self.key_id);
382 cbor_u64(&mut out, 2);
383 cbor_optional_text(&mut out, self.subject.as_deref());
384 cbor_u64(&mut out, 3);
385 cbor_optional_u64(&mut out, self.ttl_secs);
386 cbor_u64(&mut out, 4);
387 cbor_bytes(&mut out, &self.claims_json);
388 out
389 }
390}
391
392#[derive(Debug, Clone, PartialEq, Eq)]
398pub struct MintNatsUserInvocationRequest {
399 pub account_key_id: String,
401 pub user_nkey: String,
403 pub name: String,
405 pub ttl_secs: Option<u64>,
407 pub issuer_account: Option<String>,
411}
412
413impl MintNatsUserInvocationRequest {
414 #[must_use]
416 pub fn to_cbor_bytes(&self) -> Vec<u8> {
417 let mut out = Vec::new();
418 cbor_map(&mut out, 5);
419 cbor_u64(&mut out, 1);
420 cbor_text(&mut out, &self.account_key_id);
421 cbor_u64(&mut out, 2);
422 cbor_text(&mut out, &self.user_nkey);
423 cbor_u64(&mut out, 3);
424 cbor_text(&mut out, &self.name);
425 cbor_u64(&mut out, 4);
426 cbor_optional_u64(&mut out, self.ttl_secs);
427 cbor_u64(&mut out, 5);
428 cbor_optional_text(&mut out, self.issuer_account.as_deref());
429 out
430 }
431
432 pub fn from_cbor_bytes(bytes: &[u8]) -> Result<Self, InvocationError> {
438 let mut decoder = CborDecoder::new(bytes);
439 decoder.map_len(5, "MintNatsUserInvocationRequest")?;
440 decoder.key(1)?;
441 let account_key_id = decoder.text("account_key_id")?;
442 decoder.key(2)?;
443 let user_nkey = decoder.text("user_nkey")?;
444 decoder.key(3)?;
445 let name = decoder.text("name")?;
446 decoder.key(4)?;
447 let ttl_secs = decoder.optional_u64("ttl_secs")?;
448 decoder.key(5)?;
449 let issuer_account = decoder.optional_text("issuer_account")?;
450 decoder.finish()?;
451 Ok(Self {
452 account_key_id,
453 user_nkey,
454 name,
455 ttl_secs,
456 issuer_account,
457 })
458 }
459}
460
461fn cbor_map(out: &mut Vec<u8>, len: u64) {
462 cbor_type_len(out, 5, len);
463}
464
465fn cbor_text(out: &mut Vec<u8>, value: &str) {
466 cbor_type_len(out, 3, u64::try_from(value.len()).unwrap_or(u64::MAX));
467 out.extend_from_slice(value.as_bytes());
468}
469
470fn cbor_bytes(out: &mut Vec<u8>, value: &[u8]) {
471 cbor_type_len(out, 2, u64::try_from(value.len()).unwrap_or(u64::MAX));
472 out.extend_from_slice(value);
473}
474
475fn cbor_optional_text(out: &mut Vec<u8>, value: Option<&str>) {
476 match value {
477 Some(value) => cbor_text(out, value),
478 None => cbor_null(out),
479 }
480}
481
482fn cbor_optional_bytes(out: &mut Vec<u8>, value: Option<&[u8]>) {
483 match value {
484 Some(value) => cbor_bytes(out, value),
485 None => cbor_null(out),
486 }
487}
488
489fn cbor_optional_u64(out: &mut Vec<u8>, value: Option<u64>) {
490 match value {
491 Some(value) => cbor_u64(out, value),
492 None => cbor_null(out),
493 }
494}
495
496fn cbor_bool(out: &mut Vec<u8>, value: bool) {
497 out.push(if value { 0xf5 } else { 0xf4 });
498}
499
500fn cbor_null(out: &mut Vec<u8>) {
501 out.push(0xf6);
502}
503
504fn cbor_i64(out: &mut Vec<u8>, value: i64) {
505 if value >= 0 {
506 cbor_u64(out, value.unsigned_abs());
507 } else {
508 cbor_type_len(out, 1, value.unsigned_abs() - 1);
509 }
510}
511
512fn cbor_u64(out: &mut Vec<u8>, value: u64) {
513 cbor_type_len(out, 0, value);
514}
515
516fn cbor_type_len(out: &mut Vec<u8>, major: u8, value: u64) {
517 let head = major << 5;
518 if value <= 23 {
519 out.push(head | u8::try_from(value).unwrap_or(23));
520 } else if let Ok(v) = u8::try_from(value) {
521 out.extend_from_slice(&[head | 0x18, v]);
522 } else if let Ok(v) = u16::try_from(value) {
523 out.push(head | 0x19);
524 out.extend_from_slice(&v.to_be_bytes());
525 } else if let Ok(v) = u32::try_from(value) {
526 out.push(head | 0x1a);
527 out.extend_from_slice(&v.to_be_bytes());
528 } else {
529 out.push(head | 0x1b);
530 out.extend_from_slice(&value.to_be_bytes());
531 }
532}
533
534#[derive(Debug)]
535struct CborDecoder<'a> {
536 bytes: &'a [u8],
537 offset: usize,
538}
539
540impl<'a> CborDecoder<'a> {
541 const fn new(bytes: &'a [u8]) -> Self {
542 Self { bytes, offset: 0 }
543 }
544
545 const fn finish(&self) -> Result<(), InvocationError> {
546 if self.offset == self.bytes.len() {
547 Ok(())
548 } else {
549 Err(InvocationError::InvalidBody("trailing CBOR bytes"))
550 }
551 }
552
553 fn map_len(&mut self, expected: u64, schema: &'static str) -> Result<(), InvocationError> {
554 let len = self.type_len(5, schema)?;
555 if len == expected {
556 Ok(())
557 } else {
558 Err(InvocationError::InvalidBody("unexpected CBOR map length"))
559 }
560 }
561
562 fn key(&mut self, expected: u64) -> Result<(), InvocationError> {
563 let actual = self.u64("map key")?;
564 if actual == expected {
565 Ok(())
566 } else {
567 Err(InvocationError::InvalidBody("unexpected CBOR map key"))
568 }
569 }
570
571 fn status(&mut self) -> Result<InvocationStatus, InvocationError> {
572 self.map_len(4, "InvocationStatus")?;
573 self.key(1)?;
574 let code = self.u64("status.code")?;
575 let code = InvocationStatusCode::from_u64(code).ok_or(InvocationError::InvalidBody(
576 "invalid invocation status code",
577 ))?;
578 self.key(2)?;
579 let reason = self.text("status.reason")?;
580 if reason.is_empty() {
581 return Err(InvocationError::InvalidBody(
582 "empty invocation status reason",
583 ));
584 }
585 self.key(3)?;
586 let message = self.optional_text("status.message")?;
587 self.key(4)?;
588 let retryable = self.bool("status.retryable")?;
589 Ok(InvocationStatus {
590 code,
591 reason,
592 message,
593 retryable,
594 })
595 }
596
597 fn u64(&mut self, field: &'static str) -> Result<u64, InvocationError> {
598 self.type_len(0, field)
599 }
600
601 fn i64(&mut self, field: &'static str) -> Result<i64, InvocationError> {
602 let initial = self.take(field)?;
603 let major = initial >> 5;
604 let additional = initial & 0x1f;
605 let value = self.len_value(additional, field)?;
606 match major {
607 0 => i64::try_from(value)
608 .map_err(|_| InvocationError::InvalidBody("positive integer out of range")),
609 1 => {
610 let magnitude = i64::try_from(value)
611 .map_err(|_| InvocationError::InvalidBody("negative integer out of range"))?;
612 Ok(-1 - magnitude)
613 }
614 _ => Err(InvocationError::InvalidBody("expected CBOR integer")),
615 }
616 }
617
618 fn text(&mut self, field: &'static str) -> Result<String, InvocationError> {
619 let len = self.type_len(3, field)?;
620 let bytes = self.take_n(len, field)?;
621 std::str::from_utf8(bytes)
622 .map(str::to_string)
623 .map_err(|_| InvocationError::InvalidBody("invalid UTF-8 text"))
624 }
625
626 fn optional_text(&mut self, field: &'static str) -> Result<Option<String>, InvocationError> {
627 if self.peek() == Some(0xf6) {
628 self.offset += 1;
629 return Ok(None);
630 }
631 self.text(field).map(Some)
632 }
633
634 fn bytes(&mut self, field: &'static str) -> Result<Vec<u8>, InvocationError> {
635 let len = self.type_len(2, field)?;
636 Ok(self.take_n(len, field)?.to_vec())
637 }
638
639 fn optional_bytes(&mut self, field: &'static str) -> Result<Option<Vec<u8>>, InvocationError> {
640 if self.peek() == Some(0xf6) {
641 self.offset += 1;
642 return Ok(None);
643 }
644 self.bytes(field).map(Some)
645 }
646
647 fn optional_u64(&mut self, field: &'static str) -> Result<Option<u64>, InvocationError> {
648 if self.peek() == Some(0xf6) {
649 self.offset += 1;
650 return Ok(None);
651 }
652 self.u64(field).map(Some)
653 }
654
655 fn bool(&mut self, field: &'static str) -> Result<bool, InvocationError> {
656 match self.take(field)? {
657 0xf4 => Ok(false),
658 0xf5 => Ok(true),
659 _ => Err(InvocationError::InvalidBody("expected CBOR bool")),
660 }
661 }
662
663 fn type_len(
664 &mut self,
665 expected_major: u8,
666 field: &'static str,
667 ) -> Result<u64, InvocationError> {
668 let initial = self.take(field)?;
669 let major = initial >> 5;
670 if major != expected_major {
671 return Err(InvocationError::InvalidBody("unexpected CBOR type"));
672 }
673 self.len_value(initial & 0x1f, field)
674 }
675
676 fn len_value(&mut self, additional: u8, field: &'static str) -> Result<u64, InvocationError> {
679 let (value, minimum) = match additional {
680 value @ 0..=23 => return Ok(u64::from(value)),
681 24 => (self.take(field).map(u64::from)?, 24),
682 25 => (
683 self.take_array::<2>(field)
684 .map(u16::from_be_bytes)
685 .map(u64::from)?,
686 u64::from(u8::MAX) + 1,
687 ),
688 26 => (
689 self.take_array::<4>(field)
690 .map(u32::from_be_bytes)
691 .map(u64::from)?,
692 u64::from(u16::MAX) + 1,
693 ),
694 27 => (
695 self.take_array::<8>(field).map(u64::from_be_bytes)?,
696 u64::from(u32::MAX) + 1,
697 ),
698 _ => {
699 return Err(InvocationError::InvalidBody(
700 "unsupported CBOR additional info",
701 ));
702 }
703 };
704 if value < minimum {
705 return Err(InvocationError::InvalidBody(
706 "non-minimal CBOR integer or length encoding",
707 ));
708 }
709 Ok(value)
710 }
711
712 fn peek(&self) -> Option<u8> {
713 self.bytes.get(self.offset).copied()
714 }
715
716 fn take(&mut self, field: &'static str) -> Result<u8, InvocationError> {
717 let byte = self
718 .bytes
719 .get(self.offset)
720 .copied()
721 .ok_or(InvocationError::InvalidBody(field))?;
722 self.offset += 1;
723 Ok(byte)
724 }
725
726 fn take_array<const N: usize>(
727 &mut self,
728 field: &'static str,
729 ) -> Result<[u8; N], InvocationError> {
730 self.take_n(u64::try_from(N).unwrap_or(u64::MAX), field)?
731 .try_into()
732 .map_err(|_| InvocationError::InvalidBody("short CBOR integer"))
733 }
734
735 fn take_n(&mut self, len: u64, field: &'static str) -> Result<&'a [u8], InvocationError> {
736 let len = usize::try_from(len)
737 .map_err(|_| InvocationError::InvalidBody("CBOR length out of range"))?;
738 let end = self
739 .offset
740 .checked_add(len)
741 .ok_or(InvocationError::InvalidBody("CBOR length overflow"))?;
742 let out = self
743 .bytes
744 .get(self.offset..end)
745 .ok_or(InvocationError::InvalidBody(field))?;
746 self.offset = end;
747 Ok(out)
748 }
749}
750
751#[cfg(test)]
752#[allow(clippy::unwrap_used)]
753mod tests {
754 use super::*;
755
756 fn hex(bytes: &[u8]) -> String {
757 let mut out = String::with_capacity(bytes.len() * 2);
758 for byte in bytes {
759 out.push(hex_nibble(byte >> 4));
760 out.push(hex_nibble(byte & 0x0f));
761 }
762 out
763 }
764
765 const fn hex_nibble(nibble: u8) -> char {
766 match nibble {
767 0 => '0',
768 1 => '1',
769 2 => '2',
770 3 => '3',
771 4 => '4',
772 5 => '5',
773 6 => '6',
774 7 => '7',
775 8 => '8',
776 9 => '9',
777 10 => 'a',
778 11 => 'b',
779 12 => 'c',
780 13 => 'd',
781 14 => 'e',
782 15 => 'f',
783 _ => '?',
784 }
785 }
786
787 #[test]
788 fn content_type_registry_values_are_media_type_shaped() {
789 for value in INVOCATION_CONTENT_TYPES {
790 let (kind, subtype) = value.split_once('/').unwrap();
791 assert_eq!(kind, "application", "{value}");
792 assert!(subtype.starts_with("basil."), "{value}");
793 assert!(!subtype.contains('/'), "{value}");
794 assert_eq!(value.trim(), value, "{value}");
795 }
796 }
797
798 #[test]
799 fn request_bodies_have_deterministic_cbor() {
800 let request = SignInvocationRequest {
801 key_id: "publisher.signing.2026q3".to_string(),
802 message: b"payload".to_vec(),
803 algorithm: 1,
804 };
805 assert_eq!(
806 SignInvocationRequest::from_cbor_bytes(&request.to_cbor_bytes()).unwrap(),
807 request
808 );
809 assert_eq!(
810 hex(&request.to_cbor_bytes()),
811 "a30178187075626c69736865722e7369676e696e672e32303236713302477061796c6f61640301"
812 );
813
814 let response = SignInvocationResponse {
815 status: InvocationStatus::ok(),
816 policy_generation: 42,
817 signature: Some(vec![0xAB; 64]),
818 };
819 assert_eq!(
820 SignInvocationResponse::from_cbor_bytes(&response.to_cbor_bytes()).unwrap(),
821 response
822 );
823 assert_eq!(
824 hex(&response.to_cbor_bytes()),
825 "a301a4010102624f4b03f604f402182a035840abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab"
826 );
827
828 let denied = SignInvocationResponse {
829 status: InvocationStatus::denied(),
830 policy_generation: 42,
831 signature: None,
832 };
833 assert_eq!(
834 SignInvocationResponse::from_cbor_bytes(&denied.to_cbor_bytes()).unwrap(),
835 denied
836 );
837 }
838
839 #[test]
840 fn non_minimal_cbor_integer_and_length_forms_are_rejected() {
841 let minimal = [0xa3, 0x01, 0x61, 0x6b, 0x02, 0x41, 0x01, 0x03, 0x01];
844 let decoded = SignInvocationRequest::from_cbor_bytes(&minimal).unwrap();
845 assert_eq!(decoded.to_cbor_bytes(), minimal);
846
847 let non_minimal: [&[u8]; 8] = [
848 &[0xb8, 0x03, 0x01, 0x61, 0x6b, 0x02, 0x41, 0x01, 0x03, 0x01],
850 &[0xa3, 0x18, 0x01, 0x61, 0x6b, 0x02, 0x41, 0x01, 0x03, 0x01],
852 &[0xa3, 0x01, 0x78, 0x01, 0x6b, 0x02, 0x41, 0x01, 0x03, 0x01],
854 &[0xa3, 0x01, 0x61, 0x6b, 0x02, 0x58, 0x01, 0x01, 0x03, 0x01],
856 &[0xa3, 0x01, 0x61, 0x6b, 0x02, 0x41, 0x01, 0x03, 0x18, 0x01],
858 &[
860 0xa3, 0x01, 0x61, 0x6b, 0x02, 0x41, 0x01, 0x03, 0x19, 0x00, 0x01,
861 ],
862 &[
864 0xa3, 0x01, 0x61, 0x6b, 0x02, 0x41, 0x01, 0x03, 0x1a, 0x00, 0x00, 0x00, 0x01,
865 ],
866 &[
868 0xa3, 0x01, 0x61, 0x6b, 0x02, 0x41, 0x01, 0x03, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00,
869 0x00, 0x00, 0x01,
870 ],
871 ];
872 for bytes in non_minimal {
873 assert_eq!(
874 SignInvocationRequest::from_cbor_bytes(bytes),
875 Err(InvocationError::InvalidBody(
876 "non-minimal CBOR integer or length encoding"
877 )),
878 "{}",
879 hex(bytes)
880 );
881 }
882 }
883
884 #[test]
885 fn mint_nats_user_request_carries_issuer_account() {
886 let with_issuer = MintNatsUserInvocationRequest {
887 account_key_id: "nats.account.signing".to_string(),
888 user_nkey: "UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4".to_string(),
889 name: "svc-a".to_string(),
890 ttl_secs: Some(300),
891 issuer_account: Some(
892 "ADXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4".to_string(),
893 ),
894 };
895 let decoded =
896 MintNatsUserInvocationRequest::from_cbor_bytes(&with_issuer.to_cbor_bytes()).unwrap();
897 assert_eq!(decoded, with_issuer);
898 assert_eq!(
899 decoded.issuer_account.as_deref(),
900 Some("ADXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4")
901 );
902
903 let without_issuer = MintNatsUserInvocationRequest {
905 issuer_account: None,
906 ..with_issuer
907 };
908 let decoded =
909 MintNatsUserInvocationRequest::from_cbor_bytes(&without_issuer.to_cbor_bytes())
910 .unwrap();
911 assert_eq!(decoded, without_issuer);
912 assert_eq!(decoded.issuer_account, None);
913 }
914}