1use crate::{errors, errors::ErrorKind, jwt, Result};
4
5use data_encoding::BASE64URL_NOPAD;
6use nkeys::KeyPair;
7use serde::{de::DeserializeOwned, Deserialize, Serialize};
8use serde_json::{from_str, to_string};
9use std::{
10 collections::HashMap,
11 time::{Duration, SystemTime, UNIX_EPOCH},
12};
13const HEADER_TYPE: &str = "jwt";
14const HEADER_ALGORITHM: &str = "Ed25519";
15
16pub(crate) const WASCAP_INTERNAL_REVISION: u32 = 3;
18
19pub(crate) const MIN_WASCAP_INTERNAL_REVISION: u32 = 3;
21
22#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
24pub struct Token<T> {
25 pub jwt: String,
26 pub claims: Claims<T>,
27}
28
29#[derive(Debug, Serialize, Deserialize)]
30struct ClaimsHeader {
31 #[serde(rename = "typ")]
32 header_type: String,
33
34 #[serde(rename = "alg")]
35 algorithm: String,
36}
37
38fn default_as_false() -> bool {
39 false
40}
41
42pub trait WascapEntity: Clone {
43 fn name(&self) -> String;
44}
45
46#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
48pub struct Component {
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub name: Option<String>,
52
53 #[serde(rename = "hash")]
56 pub module_hash: String,
57
58 #[serde(rename = "tags", skip_serializing_if = "Option::is_none")]
60 pub tags: Option<Vec<String>>,
61
62 #[serde(rename = "rev", skip_serializing_if = "Option::is_none")]
64 pub rev: Option<i32>,
65
66 #[serde(rename = "ver", skip_serializing_if = "Option::is_none")]
68 pub ver: Option<String>,
69
70 #[serde(rename = "call_alias", skip_serializing_if = "Option::is_none")]
73 pub call_alias: Option<String>,
74
75 #[serde(rename = "prov", default = "default_as_false")]
77 pub provider: bool,
78}
79
80#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
82pub struct CapabilityProvider {
83 pub name: Option<String>,
85 pub vendor: String,
87 #[serde(rename = "rev", skip_serializing_if = "Option::is_none")]
89 pub rev: Option<i32>,
90 #[serde(rename = "ver", skip_serializing_if = "Option::is_none")]
92 pub ver: Option<String>,
93 pub target_hashes: HashMap<String, String>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub config_schema: Option<serde_json::Value>,
98}
99
100#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
102pub struct Account {
103 pub name: Option<String>,
105 pub valid_signers: Option<Vec<String>>,
108}
109
110#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
112pub struct Operator {
113 pub name: Option<String>,
115 pub valid_signers: Option<Vec<String>>,
118}
119
120#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
122pub struct Cluster {
123 pub name: Option<String>,
125 pub valid_signers: Option<Vec<String>>,
128}
129
130#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
131pub struct Invocation {
132 pub target_url: String,
134 pub origin_url: String,
136 #[serde(rename = "hash")]
138 pub invocation_hash: String,
139}
140
141#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
142pub struct Host {
143 pub name: Option<String>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub labels: Option<HashMap<String, String>>,
148}
149
150#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
153pub struct Claims<T> {
154 #[serde(rename = "exp", skip_serializing_if = "Option::is_none")]
157 pub expires: Option<u64>,
158
159 #[serde(rename = "jti")]
161 pub id: String,
162
163 #[serde(rename = "iat")]
165 pub issued_at: u64,
166
167 #[serde(rename = "iss")]
170 pub issuer: String,
171
172 #[serde(rename = "sub")]
175 pub subject: String,
176
177 #[serde(rename = "nbf", skip_serializing_if = "Option::is_none")]
179 pub not_before: Option<u64>,
180
181 #[serde(rename = "wascap", skip_serializing_if = "Option::is_none")]
183 pub metadata: Option<T>,
184
185 #[serde(rename = "wascap_revision", skip_serializing_if = "Option::is_none")]
187 pub(crate) wascap_revision: Option<u32>,
188}
189
190#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
192pub struct TokenValidation {
193 pub expired: bool,
196 pub cannot_use_yet: bool,
198 pub expires_human: String,
201 pub not_before_human: String,
204 pub signature_valid: bool,
207}
208
209impl<T> Claims<T>
210where
211 T: Serialize + DeserializeOwned + WascapEntity,
212{
213 #[allow(clippy::missing_errors_doc)] pub fn encode(&self, kp: &KeyPair) -> Result<String> {
215 let header = ClaimsHeader {
216 header_type: HEADER_TYPE.to_string(),
217 algorithm: HEADER_ALGORITHM.to_string(),
218 };
219 let header = to_jwt_segment(&header)?;
220 let claims = to_jwt_segment(self)?;
221
222 let head_and_claims = format!("{header}.{claims}");
223 let sig = kp.sign(head_and_claims.as_bytes())?;
224 let sig64 = BASE64URL_NOPAD.encode(&sig);
225 Ok(format!("{head_and_claims}.{sig64}"))
226 }
227
228 #[allow(clippy::missing_errors_doc)] pub fn decode(input: &str) -> Result<Claims<T>> {
230 let segments: Vec<&str> = input.split('.').collect();
231 if segments.len() != 3 {
232 return Err(errors::new(errors::ErrorKind::Token(
233 "invalid token format".into(),
234 )));
235 }
236 let claims: Claims<T> = from_jwt_segment(segments[1])?;
237
238 Ok(claims)
239 }
240
241 pub fn name(&self) -> String {
242 self.metadata
243 .as_ref()
244 .map_or("Anonymous".to_string(), jwt::WascapEntity::name)
245 }
246}
247
248impl WascapEntity for Component {
249 fn name(&self) -> String {
250 self.name
251 .as_ref()
252 .unwrap_or(&"Anonymous".to_string())
253 .to_string()
254 }
255}
256
257impl WascapEntity for CapabilityProvider {
258 fn name(&self) -> String {
259 self.name
260 .as_ref()
261 .unwrap_or(&"Unnamed Provider".to_string())
262 .to_string()
263 }
264}
265
266impl WascapEntity for Account {
267 fn name(&self) -> String {
268 self.name
269 .as_ref()
270 .unwrap_or(&"Anonymous".to_string())
271 .to_string()
272 }
273}
274
275impl WascapEntity for Operator {
276 fn name(&self) -> String {
277 self.name
278 .as_ref()
279 .unwrap_or(&"Anonymous".to_string())
280 .to_string()
281 }
282}
283
284impl WascapEntity for Cluster {
285 fn name(&self) -> String {
286 self.name
287 .as_ref()
288 .unwrap_or(&"Anonymous Cluster".to_string())
289 .to_string()
290 }
291}
292impl WascapEntity for Invocation {
293 fn name(&self) -> String {
294 self.target_url.to_string()
295 }
296}
297
298impl WascapEntity for Host {
299 fn name(&self) -> String {
300 self.name
301 .as_ref()
302 .unwrap_or(&"Unnamed Host".to_string())
303 .to_string()
304 }
305}
306
307impl Claims<Account> {
308 #[must_use]
310 pub fn new(
311 name: String,
312 issuer: String,
313 subject: String,
314 additional_keys: Vec<String>,
315 ) -> Claims<Account> {
316 Self::with_dates(name, issuer, subject, None, None, additional_keys)
317 }
318
319 #[must_use]
321 pub fn with_dates(
322 name: String,
323 issuer: String,
324 subject: String,
325 not_before: Option<u64>,
326 expires: Option<u64>,
327 additional_keys: Vec<String>,
328 ) -> Claims<Account> {
329 Claims {
330 metadata: Some(Account {
331 name: Some(name),
332 valid_signers: Some(additional_keys),
333 }),
334 expires,
335 id: nuid::next().to_string(),
336 issued_at: since_the_epoch().as_secs(),
337 issuer,
338 subject,
339 not_before,
340 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
341 }
342 }
343}
344
345impl Claims<CapabilityProvider> {
346 #[allow(clippy::too_many_arguments)]
348 #[must_use]
349 pub fn new(
350 name: String,
351 issuer: String,
352 subject: String,
353 vendor: String,
354 rev: Option<i32>,
355 ver: Option<String>,
356 hashes: HashMap<String, String>,
357 ) -> Claims<CapabilityProvider> {
358 Self::with_dates(name, issuer, subject, vendor, rev, ver, hashes, None, None)
359 }
360
361 #[must_use]
363 pub fn with_provider(
364 issuer: String,
365 subject: String,
366 not_before: Option<u64>,
367 expires: Option<u64>,
368 provider: CapabilityProvider,
369 ) -> Claims<CapabilityProvider> {
370 Claims {
371 metadata: Some(provider),
372 expires,
373 id: nuid::next().to_string(),
374 issued_at: since_the_epoch().as_secs(),
375 issuer,
376 subject,
377 not_before,
378 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
379 }
380 }
381
382 #[allow(clippy::too_many_arguments)]
384 #[must_use]
385 pub fn with_dates(
386 name: String,
387 issuer: String,
388 subject: String,
389 vendor: String,
390 rev: Option<i32>,
391 ver: Option<String>,
392 hashes: HashMap<String, String>,
393 not_before: Option<u64>,
394 expires: Option<u64>,
395 ) -> Claims<CapabilityProvider> {
396 Claims {
397 metadata: Some(CapabilityProvider {
398 name: Some(name),
399 rev,
400 ver,
401 target_hashes: hashes,
402 vendor,
403 config_schema: None,
404 }),
405 expires,
406 id: nuid::next().to_string(),
407 issued_at: since_the_epoch().as_secs(),
408 issuer,
409 subject,
410 not_before,
411 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
412 }
413 }
414}
415
416impl Claims<Operator> {
417 #[must_use]
419 pub fn new(
420 name: String,
421 issuer: String,
422 subject: String,
423 additional_keys: Vec<String>,
424 ) -> Claims<Operator> {
425 Self::with_dates(name, issuer, subject, None, None, additional_keys)
426 }
427
428 #[must_use]
430 pub fn with_dates(
431 name: String,
432 issuer: String,
433 subject: String,
434 not_before: Option<u64>,
435 expires: Option<u64>,
436 additional_keys: Vec<String>,
437 ) -> Claims<Operator> {
438 Claims {
439 metadata: Some(Operator {
440 name: Some(name),
441 valid_signers: Some(additional_keys),
442 }),
443 expires,
444 id: nuid::next().to_string(),
445 issued_at: since_the_epoch().as_secs(),
446 issuer,
447 subject,
448 not_before,
449 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
450 }
451 }
452}
453
454impl Claims<Cluster> {
455 #[must_use]
457 pub fn new(
458 name: String,
459 issuer: String,
460 subject: String,
461 additional_keys: Vec<String>,
462 ) -> Claims<Cluster> {
463 Self::with_dates(name, issuer, subject, None, None, additional_keys)
464 }
465
466 #[must_use]
468 pub fn with_dates(
469 name: String,
470 issuer: String,
471 subject: String,
472 not_before: Option<u64>,
473 expires: Option<u64>,
474 additional_keys: Vec<String>,
475 ) -> Claims<Cluster> {
476 Claims {
477 metadata: Some(Cluster {
478 name: Some(name),
479 valid_signers: Some(additional_keys),
480 }),
481 expires,
482 id: nuid::next().to_string(),
483 issued_at: since_the_epoch().as_secs(),
484 issuer,
485 subject,
486 not_before,
487 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
488 }
489 }
490}
491
492impl Claims<Component> {
493 #[allow(clippy::too_many_arguments)]
495 #[must_use]
496 pub fn new(
497 name: String,
498 issuer: String,
499 subject: String,
500 tags: Option<Vec<String>>,
501 provider: bool,
502 rev: Option<i32>,
503 ver: Option<String>,
504 call_alias: Option<String>,
505 ) -> Self {
506 Self::with_dates(
507 name, issuer, subject, tags, None, None, provider, rev, ver, call_alias,
508 )
509 }
510
511 #[allow(clippy::too_many_arguments)]
513 #[must_use]
514 pub fn with_dates(
515 name: String,
516 issuer: String,
517 subject: String,
518 tags: Option<Vec<String>>,
519 not_before: Option<u64>,
520 expires: Option<u64>,
521 provider: bool,
522 rev: Option<i32>,
523 ver: Option<String>,
524 call_alias: Option<String>,
525 ) -> Claims<Component> {
526 Claims {
527 metadata: Some(Component::new(name, tags, provider, rev, ver, call_alias)),
528 expires,
529 id: nuid::next().to_string(),
530 issued_at: since_the_epoch().as_secs(),
531 issuer,
532 subject,
533 not_before,
534 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
535 }
536 }
537}
538
539impl Claims<Invocation> {
540 #[must_use]
542 pub fn new(
543 issuer: String,
544 subject: String,
545 target_url: &str,
546 origin_url: &str,
547 hash: &str,
548 ) -> Claims<Invocation> {
549 Self::with_dates(issuer, subject, None, None, target_url, origin_url, hash)
550 }
551
552 #[must_use]
554 pub fn with_dates(
555 issuer: String,
556 subject: String,
557 not_before: Option<u64>,
558 expires: Option<u64>,
559 target_url: &str,
560 origin_url: &str,
561 hash: &str,
562 ) -> Claims<Invocation> {
563 Claims {
564 metadata: Some(Invocation {
565 target_url: target_url.to_string(),
566 origin_url: origin_url.to_string(),
567 invocation_hash: hash.to_string(),
568 }),
569 expires,
570 id: nuid::next().to_string(),
571 issued_at: since_the_epoch().as_secs(),
572 issuer,
573 subject,
574 not_before,
575 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
576 }
577 }
578}
579
580impl Claims<Host> {
581 #[must_use]
583 pub fn new(
584 name: String,
585 issuer: String,
586 subject: String,
587 tags: Option<HashMap<String, String>>,
588 ) -> Self {
589 Self::with_dates(name, issuer, subject, None, None, tags)
590 }
591
592 pub fn with_dates(
593 name: String,
594 issuer: String,
595 subject: String,
596 not_before: Option<u64>,
597 expires: Option<u64>,
598 tags: Option<HashMap<String, String>>,
599 ) -> Claims<Host> {
600 Claims {
601 metadata: Some(Host {
602 name: Some(name),
603 labels: tags,
604 }),
605 expires,
606 id: nuid::next().to_string(),
607 issued_at: since_the_epoch().as_secs(),
608 issuer,
609 subject,
610 not_before,
611 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
612 }
613 }
614}
615
616#[derive(Default)]
617pub struct ClaimsBuilder<T> {
618 claims: Claims<T>,
619}
620
621impl<T> ClaimsBuilder<T>
622where
623 T: Default + WascapEntity,
624{
625 #[must_use]
627 pub fn new() -> Self {
628 ClaimsBuilder::default()
629 }
630
631 pub fn issuer(&mut self, issuer: &str) -> &mut Self {
633 self.claims.issuer = issuer.to_string();
634 self
635 }
636
637 pub fn subject(&mut self, module: &str) -> &mut Self {
639 self.claims.subject = module.to_string();
640 self
641 }
642
643 pub fn expires_in(&mut self, d: Duration) -> &mut Self {
645 self.claims.expires = Some(d.as_secs() + since_the_epoch().as_secs());
646 self
647 }
648
649 pub fn valid_in(&mut self, d: Duration) -> &mut Self {
651 self.claims.not_before = Some(d.as_secs() + since_the_epoch().as_secs());
652 self
653 }
654
655 pub fn with_metadata(&mut self, metadata: T) -> &mut Self {
657 self.claims.metadata = Some(metadata);
658 self
659 }
660
661 pub fn build(&self) -> Claims<T> {
663 Claims {
664 id: nuid::next().to_string(),
665 issued_at: since_the_epoch().as_secs(),
666 ..self.claims.clone()
667 }
668 }
669}
670
671#[allow(clippy::missing_errors_doc)] pub fn validate_token<T>(input: &str) -> Result<TokenValidation>
674where
675 T: Serialize + DeserializeOwned + WascapEntity,
676{
677 let segments: Vec<&str> = input.split('.').collect();
678 if segments.len() != 3 {
679 return Err(crate::errors::new(ErrorKind::Token(format!(
680 "invalid token format, expected 3 segments, found {}",
681 segments.len()
682 ))));
683 }
684
685 let header_and_claims = format!("{}.{}", segments[0], segments[1]);
686 let sig = BASE64URL_NOPAD.decode(segments[2].as_bytes())?;
687
688 let header: ClaimsHeader = from_jwt_segment(segments[0])?;
689 validate_header(&header)?;
690
691 let claims = Claims::<T>::decode(input)?;
692 validate_issuer(&claims.issuer)?;
693 validate_subject(&claims.subject)?;
694
695 let kp = KeyPair::from_public_key(&claims.issuer)?;
696 let sigverify = kp.verify(header_and_claims.as_bytes(), &sig);
697
698 let validation = TokenValidation {
699 signature_valid: sigverify.is_ok(),
700 expired: validate_expiration(claims.expires).is_err(),
701 expires_human: stamp_to_human(claims.expires).unwrap_or_else(|| "never".to_string()),
702 not_before_human: stamp_to_human(claims.not_before)
703 .unwrap_or_else(|| "immediately".to_string()),
704 cannot_use_yet: validate_notbefore(claims.not_before).is_err(),
705 };
706
707 Ok(validation)
708}
709
710fn validate_notbefore(nb: Option<u64>) -> Result<()> {
711 if let Some(nbf) = nb {
712 let nbf_secs = Duration::from_secs(nbf);
713 if since_the_epoch() < nbf_secs {
714 Err(errors::new(ErrorKind::TokenTooEarly))
715 } else {
716 Ok(())
717 }
718 } else {
719 Ok(())
720 }
721}
722
723fn validate_expiration(exp: Option<u64>) -> Result<()> {
724 if let Some(exp) = exp {
725 let exp_secs = Duration::from_secs(exp);
726 if exp_secs < since_the_epoch() {
727 Err(errors::new(ErrorKind::ExpiredToken))
728 } else {
729 Ok(())
730 }
731 } else {
732 Ok(())
733 }
734}
735
736fn validate_issuer(iss: &str) -> Result<()> {
737 if iss.is_empty() {
738 Err(errors::new(ErrorKind::MissingIssuer))
739 } else {
740 Ok(())
741 }
742}
743
744fn validate_subject(sub: &str) -> Result<()> {
745 if sub.is_empty() {
746 Err(errors::new(ErrorKind::MissingSubject))
747 } else {
748 Ok(())
749 }
750}
751
752fn since_the_epoch() -> Duration {
753 let start = SystemTime::now();
754 start
755 .duration_since(UNIX_EPOCH)
756 .expect("A timey wimey problem has occurred!")
757}
758
759fn validate_header(h: &ClaimsHeader) -> Result<()> {
760 if h.algorithm != HEADER_ALGORITHM {
761 Err(errors::new(ErrorKind::InvalidAlgorithm))
762 } else if h.header_type != HEADER_TYPE {
763 Err(errors::new(ErrorKind::Token("Invalid header".to_string())))
764 } else {
765 Ok(())
766 }
767}
768
769fn to_jwt_segment<T: Serialize>(input: &T) -> Result<String> {
770 let encoded = to_string(input)?;
771 Ok(BASE64URL_NOPAD.encode(encoded.as_bytes()))
772}
773
774fn from_jwt_segment<B: AsRef<str>, T: DeserializeOwned>(encoded: B) -> Result<T> {
775 let decoded = BASE64URL_NOPAD.decode(encoded.as_ref().as_bytes())?;
776 let s = String::from_utf8(decoded)?;
777
778 Ok(from_str(&s)?)
779}
780
781fn stamp_to_human(stamp: Option<u64>) -> Option<String> {
782 stamp.and_then(|s| {
783 let now: i64 = since_the_epoch().as_secs().try_into().ok()?;
784 let s: i64 = s.try_into().ok()?;
785 let diff_sec = (now - s).abs();
786
787 let diff_sec = if diff_sec >= 86400 {
789 diff_sec - (diff_sec % 86400)
791 } else if diff_sec >= 3600 {
792 diff_sec - (diff_sec % 3600)
794 } else if diff_sec >= 60 {
795 diff_sec - (diff_sec % 60)
797 } else {
798 diff_sec
799 };
800 let diff_sec = diff_sec.try_into().ok()?;
801 let ht = humantime::format_duration(Duration::from_secs(diff_sec));
802
803 let now: u64 = now.try_into().ok()?;
804 let s: u64 = s.try_into().ok()?;
805 if now > s {
806 Some(format!("{ht} ago"))
807 } else {
808 Some(format!("in {ht}"))
809 }
810 })
811}
812
813fn normalize_call_alias(alias: Option<String>) -> Option<String> {
814 alias.map(|a| {
815 let mut n = a.to_lowercase();
816 n = n.trim().to_string();
817 n = n.replace(|c: char| !c.is_ascii(), "");
818 n = n.replace(' ', "_");
819 n = n.replace('-', "_");
820 n = n.replace('.', "_");
821 n
822 })
823}
824
825impl Component {
826 #[must_use]
827 pub fn new(
828 name: String,
829 tags: Option<Vec<String>>,
830 provider: bool,
831 rev: Option<i32>,
832 ver: Option<String>,
833 call_alias: Option<String>,
834 ) -> Self {
835 Self {
836 name: Some(name),
837 module_hash: String::new(),
838 tags,
839 provider,
840 rev,
841 ver,
842 call_alias: normalize_call_alias(call_alias),
843 }
844 }
845}
846
847impl CapabilityProvider {
848 #[must_use]
849 pub fn new(
850 name: String,
851 vendor: String,
852 rev: Option<i32>,
853 ver: Option<String>,
854 hashes: HashMap<String, String>,
855 ) -> CapabilityProvider {
856 CapabilityProvider {
857 target_hashes: hashes,
858 name: Some(name),
859 vendor,
860 rev,
861 ver,
862 config_schema: None,
863 }
864 }
865}
866
867impl Account {
868 #[must_use]
869 pub fn new(name: String, additional_keys: Vec<String>) -> Account {
870 Account {
871 name: Some(name),
872 valid_signers: Some(additional_keys),
873 }
874 }
875}
876
877impl Operator {
878 #[must_use]
879 pub fn new(name: String, additional_keys: Vec<String>) -> Operator {
880 Operator {
881 name: Some(name),
882 valid_signers: Some(additional_keys),
883 }
884 }
885}
886
887impl Cluster {
888 #[must_use]
889 pub fn new(name: String, additional_keys: Vec<String>) -> Cluster {
890 Cluster {
891 name: Some(name),
892 valid_signers: Some(additional_keys),
893 }
894 }
895}
896
897impl Invocation {
898 #[must_use]
899 pub fn new(target_url: &str, origin_url: &str, hash: &str) -> Invocation {
900 Invocation {
901 target_url: target_url.to_string(),
902 origin_url: origin_url.to_string(),
903 invocation_hash: hash.to_string(),
904 }
905 }
906}
907
908impl Host {
909 #[must_use]
910 pub fn new(name: String, labels: HashMap<String, String>) -> Host {
911 Host {
912 name: Some(name),
913 labels: Some(labels),
914 }
915 }
916}
917
918#[cfg(test)]
919mod test {
920 use super::{Account, Claims, Component, ErrorKind, Host, KeyPair, Operator};
921 use crate::jwt::{
922 since_the_epoch, validate_token, CapabilityProvider, ClaimsBuilder, Cluster,
923 WASCAP_INTERNAL_REVISION,
924 };
925 use std::collections::HashMap;
926 use std::io::Read;
927
928 #[test]
929 fn full_validation_nbf() {
930 let kp = KeyPair::new_account();
931 let claims = Claims {
932 metadata: Some(Component::new(
933 "test".to_string(),
934 Some(vec![]),
935 false,
936 Some(0),
937 Some(String::new()),
938 None,
939 )),
940 expires: None,
941 id: nuid::next().to_string(),
942 issued_at: 0,
943 issuer: kp.public_key(),
944 subject: "test.wasm".to_string(),
945 not_before: Some(since_the_epoch().as_secs() + 1000),
946 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
947 };
948
949 let encoded = claims.encode(&kp).unwrap();
950 let vres = validate_token::<Component>(&encoded);
951 assert!(vres.is_ok());
952 if let Ok(v) = vres {
953 assert!(!v.expired);
954 assert!(v.cannot_use_yet);
955 assert_eq!(v.not_before_human, "in 16m");
956 }
957 }
958
959 #[test]
960 fn full_validation_expires() {
961 let kp = KeyPair::new_account();
962 let claims = Claims {
963 metadata: Some(Component::new(
964 "test".to_string(),
965 Some(vec![]),
966 false,
967 Some(1),
968 Some(String::new()),
969 None,
970 )),
971 expires: Some(since_the_epoch().as_secs() - 30000),
972 id: nuid::next().to_string(),
973 issued_at: 0,
974 issuer: kp.public_key(),
975 subject: "test.wasm".to_string(),
976 not_before: None,
977 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
978 };
979
980 let encoded = claims.encode(&kp).unwrap();
981 let vres = validate_token::<Component>(&encoded);
982 assert!(vres.is_ok());
983 if let Ok(v) = vres {
984 assert!(v.expired);
985 assert!(!v.cannot_use_yet);
986 assert_eq!(v.expires_human, "8h ago");
987 }
988 }
989
990 #[test]
991 fn validate_account() {
992 let issuer = KeyPair::new_operator();
993 let claims = Claims {
994 metadata: Some(Account::new("test account".to_string(), vec![])),
995 expires: Some(since_the_epoch().as_secs() - 30000),
996 id: nuid::next().to_string(),
997 issued_at: 0,
998 issuer: issuer.public_key(),
999 subject: "foo".to_string(),
1000 not_before: None,
1001 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
1002 };
1003 let encoded = claims.encode(&issuer).unwrap();
1004 let vres = validate_token::<Account>(&encoded);
1005 assert!(vres.is_ok());
1006 if let Ok(v) = vres {
1007 assert!(v.expired);
1008 assert!(!v.cannot_use_yet);
1009 assert_eq!(v.expires_human, "8h ago");
1010 }
1011 }
1012
1013 #[test]
1014 fn full_validation() {
1015 let kp = KeyPair::new_account();
1016 let claims = Claims {
1017 metadata: Some(Component::new(
1018 "test".to_string(),
1019 Some(vec![]),
1020 false,
1021 Some(1),
1022 Some(String::new()),
1023 None,
1024 )),
1025 expires: None,
1026 id: nuid::next().to_string(),
1027 issued_at: 0,
1028 issuer: kp.public_key(),
1029 subject: "test.wasm".to_string(),
1030 not_before: None,
1031 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
1032 };
1033
1034 let encoded = claims.encode(&kp).unwrap();
1035 let vres = validate_token::<Component>(&encoded);
1036 assert!(vres.is_ok());
1037 }
1038
1039 #[test]
1040 fn encode_decode_mismatch() {
1041 let issuer = KeyPair::new_operator();
1042 let claims = Claims {
1043 metadata: Some(Account::new("test account".to_string(), vec![])),
1044 expires: None,
1045 id: nuid::next().to_string(),
1046 issued_at: 0,
1047 issuer: "foo".to_string(),
1048 subject: "test".to_string(),
1049 not_before: None,
1050 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
1051 };
1052 let encoded = claims.encode(&issuer).unwrap();
1053 let decoded = Claims::<Component>::decode(&encoded);
1054 assert!(decoded.is_err());
1055 }
1056
1057 #[test]
1058 fn decode_component_as_operator() {
1059 let kp = KeyPair::new_account();
1060 let claims = Claims {
1061 metadata: Some(Component::new(
1062 "test".to_string(),
1063 Some(vec![]),
1064 false,
1065 Some(1),
1066 Some(String::new()),
1067 None,
1068 )),
1069 expires: None,
1070 id: nuid::next().to_string(),
1071 issued_at: 0,
1072 issuer: kp.public_key(),
1073 subject: "test.wasm".to_string(),
1074 not_before: None,
1075 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
1076 };
1077 let encoded = claims.encode(&kp).unwrap();
1078 let decoded = Claims::<Operator>::decode(&encoded);
1079 assert!(decoded.is_ok());
1080 assert_eq!(decoded.unwrap().metadata.unwrap().name.unwrap(), "test");
1081 }
1082
1083 #[test]
1084 fn encode_decode_roundtrip() {
1085 let kp = KeyPair::new_account();
1086 let claims = Claims {
1087 metadata: Some(Component::new(
1088 "test".to_string(),
1089 Some(vec![]),
1090 false,
1091 Some(1),
1092 Some(String::new()),
1093 None,
1094 )),
1095 expires: None,
1096 id: nuid::next().to_string(),
1097 issued_at: 0,
1098 issuer: kp.public_key(),
1099 subject: "test.wasm".to_string(),
1100 not_before: None,
1101 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
1102 };
1103
1104 let encoded = claims.encode(&kp).unwrap();
1105
1106 let decoded = Claims::decode(&encoded).unwrap();
1107 assert!(validate_token::<Component>(&encoded).is_ok());
1108
1109 assert_eq!(claims, decoded);
1110 }
1111
1112 #[test]
1113 fn provider_round_trip() {
1114 let account = KeyPair::new_account();
1115 let provider = KeyPair::new_service();
1116
1117 let mut hashes = HashMap::new();
1118 hashes.insert("aarch64-linux".to_string(), "abc12345".to_string());
1119 let claims = ClaimsBuilder::new()
1120 .subject(&provider.public_key())
1121 .issuer(&account.public_key())
1122 .with_metadata(CapabilityProvider::new(
1123 "Test Provider".to_string(),
1124 "wasmCloud Internal".to_string(),
1125 Some(1),
1126 Some("v0.0.1".to_string()),
1127 hashes,
1128 ))
1129 .build();
1130
1131 let encoded = claims.encode(&account).unwrap();
1132 let decoded: Claims<CapabilityProvider> = Claims::decode(&encoded).unwrap();
1133 assert!(validate_token::<CapabilityProvider>(&encoded).is_ok());
1134 assert_eq!(decoded.issuer, account.public_key());
1135 assert_eq!(decoded.subject, provider.public_key());
1136 assert_eq!(
1137 decoded.metadata.as_ref().unwrap().vendor,
1138 "wasmCloud Internal"
1139 );
1140 }
1141
1142 #[test]
1143 fn provider_round_trip_with_schema() {
1144 let raw_schema = r#"
1145 {
1146 "$id": "https://wasmcloud.com/httpserver.schema.json",
1147 "$schema": "https://json-schema.org/draft/2020-12/schema",
1148 "title": "HTTP server provider schema",
1149 "type": "object",
1150 "properties": {
1151 "port": {
1152 "type": "integer",
1153 "description": "The port number to use for the web server",
1154 "minimum": 4000,
1155 "maximum": 10000
1156 },
1157 "lastName": {
1158 "type": "string",
1159 "description": "Someone's last name."
1160 },
1161 "easterEgg": {
1162 "description": "Indicates whether or not the easter egg should be displayed",
1163 "type": "boolean"
1164 }
1165 }
1166 }
1167 "#;
1168 let account = KeyPair::new_account();
1169 let provider = KeyPair::new_service();
1170
1171 let mut hashes = HashMap::new();
1172 hashes.insert("aarch64-linux".to_string(), "abc12345".to_string());
1173
1174 let schema = serde_json::from_str::<serde_json::Value>(raw_schema).unwrap();
1175 let mut hashes = HashMap::new();
1176 hashes.insert("aarch64-linux".to_string(), "abc12345".to_string());
1177 let claims = ClaimsBuilder::new()
1178 .subject(&provider.public_key())
1179 .issuer(&account.public_key())
1180 .with_metadata(CapabilityProvider {
1181 name: Some("Test Provider".to_string()),
1182 vendor: "wasmCloud Internal".to_string(),
1183 rev: Some(1),
1184 ver: Some("v0.0.1".to_string()),
1185 target_hashes: hashes,
1186 config_schema: Some(schema),
1187 })
1188 .build();
1189
1190 let encoded = claims.encode(&account).unwrap();
1191 let decoded: Claims<CapabilityProvider> = Claims::decode(&encoded).unwrap();
1192 assert!(validate_token::<CapabilityProvider>(&encoded).is_ok());
1193 assert_eq!(decoded.issuer, account.public_key());
1194 assert_eq!(decoded.subject, provider.public_key());
1195 assert_eq!(
1196 decoded
1197 .metadata
1198 .as_ref()
1199 .unwrap()
1200 .config_schema
1201 .as_ref()
1202 .unwrap()["properties"]["port"]["minimum"],
1203 4000
1204 );
1205 }
1206
1207 #[test]
1208 fn encode_decode_logging_roundtrip() {
1209 let kp = KeyPair::new_account();
1210 let claims = Claims {
1211 metadata: Some(Component::new(
1212 "test".to_string(),
1213 Some(vec![]),
1214 false,
1215 Some(1),
1216 Some(String::new()),
1217 None,
1218 )),
1219 expires: None,
1220 id: nuid::next().to_string(),
1221 issued_at: 0,
1222 issuer: kp.public_key(),
1223 subject: "test.wasm".to_string(),
1224 not_before: None,
1225 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
1226 };
1227
1228 let encoded = claims.encode(&kp).unwrap();
1229
1230 let decoded = Claims::decode(&encoded).unwrap();
1231 assert!(validate_token::<Component>(&encoded).is_ok());
1232
1233 assert_eq!(claims, decoded);
1234 }
1235 #[test]
1236 fn account_extra_signers() {
1237 let op = KeyPair::new_operator();
1238 let kp1 = KeyPair::new_account();
1239 let kp2 = KeyPair::new_account();
1240 let claims = Claims::<Account>::new(
1241 "test account".to_string(),
1242 op.public_key(),
1243 kp1.public_key(),
1244 vec![kp2.public_key()],
1245 );
1246 let encoded = claims.encode(&kp1).unwrap();
1247 let decoded = Claims::<Account>::decode(&encoded).unwrap();
1248 assert!(validate_token::<Account>(&encoded).is_ok());
1249 assert_eq!(claims, decoded);
1250 assert_eq!(claims.metadata.unwrap().valid_signers.unwrap().len(), 1);
1251 }
1252
1253 #[test]
1254 fn cluster_extra_signers() {
1255 let op = KeyPair::new_operator();
1256 let kp1 = KeyPair::new_cluster();
1257 let kp2 = KeyPair::new_cluster();
1258 let claims = Claims::<Cluster>::new(
1259 "test cluster".to_string(),
1260 op.public_key(),
1261 kp1.public_key(),
1262 vec![kp2.public_key()],
1263 );
1264 let encoded = claims.encode(&kp1).unwrap();
1265 let decoded = Claims::<Cluster>::decode(&encoded).unwrap();
1266 assert!(validate_token::<Cluster>(&encoded).is_ok());
1267 assert_eq!(claims, decoded);
1268 assert_eq!(claims.metadata.unwrap().valid_signers.unwrap().len(), 1);
1269 }
1270
1271 #[test]
1272 fn encode_decode_bad_token() {
1273 let kp = KeyPair::new_account();
1274 let claims = Claims {
1275 metadata: Some(Component::new(
1276 "test".to_string(),
1277 Some(vec![]),
1278 false,
1279 Some(1),
1280 Some(String::new()),
1281 None,
1282 )),
1283 expires: None,
1284 id: nuid::next().to_string(),
1285 issued_at: 0,
1286 issuer: kp.public_key(),
1287 subject: "test.wasm".to_string(),
1288 not_before: None,
1289 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
1290 };
1291
1292 let encoded_nosep = claims.encode(&kp).unwrap().replace('.', "");
1293
1294 let decoded = Claims::<Account>::decode(&encoded_nosep);
1295 assert!(decoded.is_err());
1296 if let Err(e) = decoded {
1297 match e.kind() {
1298 ErrorKind::Token(s) => assert_eq!(s, "invalid token format"),
1299 _ => {
1300 panic!("failed to assert errors::ErrorKind::Token");
1301 }
1302 }
1303 }
1304 }
1305
1306 #[test]
1307 fn ensure_issuer_on_token() {
1308 let kp = KeyPair::new_account();
1309 let mut claims = Claims {
1310 metadata: Some(Component::new(
1311 "test".to_string(),
1312 Some(vec![]),
1313 false,
1314 Some(1),
1315 Some(String::new()),
1316 None,
1317 )),
1318 expires: None,
1319 id: nuid::next().to_string(),
1320 issued_at: 0,
1321 issuer: kp.public_key(),
1322 subject: "test.wasm".to_string(),
1323 not_before: None,
1324 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
1325 };
1326
1327 let encoded = claims.encode(&kp).unwrap();
1328 assert!(validate_token::<Account>(&encoded).is_ok());
1329
1330 claims.issuer = String::new();
1332 let bad_encode = claims.encode(&kp).unwrap();
1333 let issuer_err = validate_token::<Account>(&bad_encode);
1334 assert!(issuer_err.is_err());
1335 if let Err(e) = issuer_err {
1336 match e.kind() {
1337 ErrorKind::MissingIssuer => (),
1338 _ => panic!("failed to assert errors::ErrorKind::MissingIssuer"),
1339 }
1340 }
1341 }
1342
1343 #[test]
1344 fn ensure_subject_on_token() {
1345 let kp = KeyPair::new_account();
1346 let mut claims = Claims {
1347 metadata: Some(Component::new(
1348 "test".to_string(),
1349 Some(vec![]),
1350 false,
1351 Some(1),
1352 Some(String::new()),
1353 None,
1354 )),
1355 expires: None,
1356 id: nuid::next().to_string(),
1357 issued_at: 0,
1358 issuer: kp.public_key(),
1359 subject: "test.wasm".to_string(),
1360 not_before: None,
1361 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
1362 };
1363
1364 claims.subject = String::new();
1365 let bad_subject = claims.encode(&kp).unwrap();
1366 let subject_err = validate_token::<Account>(&bad_subject);
1367 assert!(subject_err.is_err());
1368 assert!(subject_err.is_err());
1369 if let Err(e) = subject_err {
1370 match e.kind() {
1371 ErrorKind::MissingSubject => (),
1372 _ => panic!("failed to assert errors::ErrorKind::MissingSubject"),
1373 }
1374 }
1375 }
1376
1377 #[test]
1378 fn ensure_backwards_compatible() {
1379 let mut echo_messaging_v9 = vec![];
1380 let mut file = std::fs::File::open("./fixtures/echo_messaging_v0.9.0.wasm").unwrap();
1381 file.read_to_end(&mut echo_messaging_v9).unwrap();
1382
1383 let extracted = crate::wasm::extract_claims(&echo_messaging_v9)
1384 .unwrap()
1385 .unwrap();
1386
1387 let vres = validate_token::<Component>(&extracted.jwt);
1388 assert!(vres.is_ok());
1389 }
1390
1391 #[test]
1392 fn ensure_jwt_valid_segments() {
1393 let valid = "eyJ0eXAiOiJqd3QiLCJhbGciOiJFZDI1NTE5In0.eyJqdGkiOiJTakI1Zm05NzRTanU5V01nVFVjaHNiIiwiaWF0IjoxNjQ0ODQzNzQzLCJpc3MiOiJBQ09KSk42V1VQNE9ERDc1WEVCS0tUQ0NVSkpDWTVaS1E1NlhWS1lLNEJFSldHVkFPT1FIWk1DVyIsInN1YiI6Ik1CQ0ZPUE02SlcyQVBKTFhKRDNaNU80Q043Q1BZSjJCNEZUS0xKVVI1WVI1TUlUSVU3SEQzV0Q1Iiwid2FzY2FwIjp7Im5hbWUiOiJFY2hvIiwiaGFzaCI6IjRDRUM2NzNBN0RDQ0VBNkE0MTY1QkIxOTU4MzJDNzkzNjQ3MUNGN0FCNDUwMUY4MzdGOEQ2NzlGNDQwMEJDOTciLCJ0YWdzIjpbXSwiY2FwcyI6WyJ3YXNtY2xvdWQ6aHR0cHNlcnZlciJdLCJyZXYiOjQsInZlciI6IjAuMy40IiwicHJvdiI6ZmFsc2V9fQ.ZWyD6VQqzaYM1beD2x9Fdw4o_Bavy3ZG703Eg4cjhyJwUKLDUiVPVhqHFE6IXdV4cW6j93YbMT6VGq5iBDWmAg";
1394 let too_few = "asd.123123123";
1395 let too_many = "asd.123.abc.easy";
1396 let correct_but_wrong = "ddd.123.notajwt";
1397
1398 assert!(validate_token::<Component>(valid).is_ok());
1399 assert!(validate_token::<Component>(too_few)
1400 .is_err_and(|e| e.to_string()
1401 == "JWT error: invalid token format, expected 3 segments, found 2"));
1402 assert!(validate_token::<Component>(too_many)
1403 .is_err_and(|e| e.to_string()
1404 == "JWT error: invalid token format, expected 3 segments, found 4"));
1405 assert!(
1407 validate_token::<Component>(correct_but_wrong).is_err_and(|e| !e
1408 .to_string()
1409 .contains("invalid token format, expected 3 segments"))
1410 );
1411 }
1412
1413 #[test]
1414 fn ensure_host_validation() {
1415 let signer = KeyPair::new_account();
1416 let host = KeyPair::new_server();
1417 let mut claims = Claims {
1418 metadata: Some(Host::new("test".to_string(), HashMap::new())),
1419 expires: None,
1420 id: nuid::next().to_string(),
1421 issued_at: 0,
1422 issuer: signer.public_key(),
1423 subject: host.public_key().to_string(),
1424 not_before: None,
1425 wascap_revision: Some(WASCAP_INTERNAL_REVISION),
1426 };
1427
1428 let encoded = claims.encode(&signer).unwrap();
1429 let decoded = Claims::<Host>::decode(&encoded);
1430 assert!(decoded.is_ok());
1431 let decoded_claims = decoded.unwrap();
1432 assert_eq!(
1433 decoded_claims.metadata.unwrap().labels,
1434 Some(HashMap::new())
1435 );
1436
1437 let validation = validate_token::<Host>(&encoded);
1438 assert!(validation.is_ok());
1439 assert!(validation.unwrap().signature_valid);
1440
1441 claims.metadata.as_mut().unwrap().labels =
1442 Some(HashMap::from([("test".to_string(), "value".to_string())]));
1443 let encoded = claims.encode(&signer).unwrap();
1444 let decoded = Claims::<Host>::decode(&encoded).unwrap();
1445 assert_eq!(
1446 decoded.metadata.unwrap().labels,
1447 Some(HashMap::from([("test".to_string(), "value".to_string())]))
1448 );
1449 }
1450}