wascap/
jwt.rs

1//! Claims encoding, decoding, and validation for JSON Web Tokens (JWT)
2
3use 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
16// Current internal revision number that will go into embedded claims
17pub(crate) const WASCAP_INTERNAL_REVISION: u32 = 3;
18
19// Minimum revision number at which we verify module hashes
20pub(crate) const MIN_WASCAP_INTERNAL_REVISION: u32 = 3;
21
22/// A structure containing a JWT and its associated decoded claims
23#[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/// The metadata that corresponds to a component
47#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
48pub struct Component {
49    /// A descriptive name for this component, should not include version information or public key
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub name: Option<String>,
52
53    /// A hash of the module's bytes as they exist without the embedded signature. This is stored so wascap
54    /// can determine if a WebAssembly module's bytecode has been altered after it was signed
55    #[serde(rename = "hash")]
56    pub module_hash: String,
57
58    /// List of arbitrary string tags associated with the claims
59    #[serde(rename = "tags", skip_serializing_if = "Option::is_none")]
60    pub tags: Option<Vec<String>>,
61
62    /// Indicates a monotonically increasing revision number.  Optional.
63    #[serde(rename = "rev", skip_serializing_if = "Option::is_none")]
64    pub rev: Option<i32>,
65
66    /// Indicates a human-friendly version string
67    #[serde(rename = "ver", skip_serializing_if = "Option::is_none")]
68    pub ver: Option<String>,
69
70    /// An optional, code-friendly alias that can be used instead of a public key or
71    /// OCI reference for invocations
72    #[serde(rename = "call_alias", skip_serializing_if = "Option::is_none")]
73    pub call_alias: Option<String>,
74
75    /// Indicates whether this module is a capability provider
76    #[serde(rename = "prov", default = "default_as_false")]
77    pub provider: bool,
78}
79
80/// The claims metadata corresponding to a capability provider
81#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
82pub struct CapabilityProvider {
83    /// A descriptive name for the capability provider
84    pub name: Option<String>,
85    /// A human-readable string identifying the vendor of this provider (e.g. Redis or Cassandra or NATS etc)
86    pub vendor: String,
87    /// Indicates a monotonically increasing revision number.  Optional.
88    #[serde(rename = "rev", skip_serializing_if = "Option::is_none")]
89    pub rev: Option<i32>,
90    /// Indicates a human-friendly version string. Optional.
91    #[serde(rename = "ver", skip_serializing_if = "Option::is_none")]
92    pub ver: Option<String>,
93    /// The file hashes that correspond to the architecture-OS target triples for this provider.
94    pub target_hashes: HashMap<String, String>,
95    /// If the provider chooses, it can supply a JSON schma that describes its expected link configuration
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub config_schema: Option<serde_json::Value>,
98}
99
100/// The claims metadata corresponding to an account
101#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
102pub struct Account {
103    /// A descriptive name for this account
104    pub name: Option<String>,
105    /// A list of valid public keys that may appear as an `issuer` on
106    /// components signed by one of this account's multiple seed keys
107    pub valid_signers: Option<Vec<String>>,
108}
109
110/// The claims metadata corresponding to an operator
111#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
112pub struct Operator {
113    /// A descriptive name for the operator
114    pub name: Option<String>,
115    /// A list of valid public keys that may appear as an `issuer` on accounts
116    /// signed by one of this operator's multiple seed keys
117    pub valid_signers: Option<Vec<String>>,
118}
119
120/// The claims metadata corresponding to a cluster
121#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
122pub struct Cluster {
123    /// Optional friendly descriptive name for the cluster
124    pub name: Option<String>,
125    /// A list of valid public keys that may appear as an `issuer` on hosts
126    /// or anything else signed by one of the cluster's seed keys
127    pub valid_signers: Option<Vec<String>>,
128}
129
130#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
131pub struct Invocation {
132    /// Fully qualified bus URL indicating the target of the invocation
133    pub target_url: String,
134    /// Fully qualified bus URL indicating the origin of the invocation
135    pub origin_url: String,
136    /// Hash of the invocation to which these claims belong
137    #[serde(rename = "hash")]
138    pub invocation_hash: String,
139}
140
141#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
142pub struct Host {
143    /// Optional friendly descriptive name for the host
144    pub name: Option<String>,
145    /// Optional labels for the host
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub labels: Option<HashMap<String, String>>,
148}
149
150/// Represents a set of [RFC 7519](https://tools.ietf.org/html/rfc7519) compliant JSON Web Token
151/// claims.
152#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
153pub struct Claims<T> {
154    /// All timestamps in JWTs are stored in _seconds since the epoch_ format
155    /// as described as `NumericDate` in the RFC. Corresponds to the `exp` field in a JWT.
156    #[serde(rename = "exp", skip_serializing_if = "Option::is_none")]
157    pub expires: Option<u64>,
158
159    /// Corresponds to the `jti` field in a JWT.
160    #[serde(rename = "jti")]
161    pub id: String,
162
163    /// The `iat` field, stored in _seconds since the epoch_
164    #[serde(rename = "iat")]
165    pub issued_at: u64,
166
167    /// Issuer of the token, by convention usually the public key of the _account_ that
168    /// signed the token
169    #[serde(rename = "iss")]
170    pub issuer: String,
171
172    /// Subject of the token, usually the public key of the _module_ corresponding to the WebAssembly file
173    /// being signed
174    #[serde(rename = "sub")]
175    pub subject: String,
176
177    /// The `nbf` JWT field, indicates the time when the token becomes valid. If `None` token is valid immediately
178    #[serde(rename = "nbf", skip_serializing_if = "Option::is_none")]
179    pub not_before: Option<u64>,
180
181    /// Custom jwt claims in the `wascap` namespace
182    #[serde(rename = "wascap", skip_serializing_if = "Option::is_none")]
183    pub metadata: Option<T>,
184
185    /// Internal revision number used to aid in parsing and validating claims
186    #[serde(rename = "wascap_revision", skip_serializing_if = "Option::is_none")]
187    pub(crate) wascap_revision: Option<u32>,
188}
189
190/// The result of the validation process perform on a JWT
191#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
192pub struct TokenValidation {
193    /// Indicates whether or not this token has expired, as determined by the current OS system clock.
194    /// If `true`, you should treat the associated token as invalid
195    pub expired: bool,
196    /// Indicates whether this token is _not yet_ valid. If `true`, do not use this token
197    pub cannot_use_yet: bool,
198    /// A human-friendly (lowercase) description of the _relative_ expiration date (e.g. "in 3 hours").
199    /// If the token never expires, the value will be "never"
200    pub expires_human: String,
201    /// A human-friendly description of the relative time when this token will become valid (e.g. "in 2 weeks").
202    /// If the token has not had a "not before" date set, the value will be "immediately"
203    pub not_before_human: String,
204    /// Indicates whether the signature is valid according to a cryptographic comparison. If `false` you should
205    /// reject this token.
206    pub signature_valid: bool,
207}
208
209impl<T> Claims<T>
210where
211    T: Serialize + DeserializeOwned + WascapEntity,
212{
213    #[allow(clippy::missing_errors_doc)] // TODO: document
214    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)] // TODO: document
229    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    /// Creates a new non-expiring Claims wrapper for metadata representing an account
309    #[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    /// Creates a new Claims wrapper for metadata representing an account, with optional valid before and expiration dates
320    #[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    /// Creates a new non-expiring Claims wrapper for metadata representing a capability provider
347    #[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    /// Creates a new Claims non-expiring wrapper for metadata representing a capability provider, with optional valid before and expiration dates
362    #[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    /// Creates a new Claims wrapper for metadata representing a capability provider, with optional valid before and expiration dates
383    #[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    /// Creates a new non-expiring Claims wrapper for metadata representing an operator
418    #[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    /// Creates a new Claims wrapper for metadata representing an operator, with optional valid before and expiration dates
429    #[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    /// Creates a new non-expiring Claims wrapper for metadata representing a cluster
456    #[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    /// Creates a new Claims wrapper for metadata representing a cluster, with optional valid before and expiration dates
467    #[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    /// Creates a new non-expiring Claims wrapper for metadata representing an component
494    #[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    /// Creates a new Claims wrapper for metadata representing an component, with optional valid before and expiration dates
512    #[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    /// Creates a new non-expiring Claims wrapper for metadata representing an invocation
541    #[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    /// Creates a new Claims wrapper for metadata representing an invocation, with optional valid before and expiration dates
553    #[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    /// Creates a new non-expiring Claims wrapper for metadata representing a host
582    #[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    /// Creates a new builder
626    #[must_use]
627    pub fn new() -> Self {
628        ClaimsBuilder::default()
629    }
630
631    /// Sets the issuer for the claims
632    pub fn issuer(&mut self, issuer: &str) -> &mut Self {
633        self.claims.issuer = issuer.to_string();
634        self
635    }
636
637    /// Sets the subject for the claims
638    pub fn subject(&mut self, module: &str) -> &mut Self {
639        self.claims.subject = module.to_string();
640        self
641    }
642
643    /// Indicates how long this claim set will remain valid
644    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    /// Indicates how long until this claim set becomes valid
650    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    /// Sets the appropriate metadata for this claims type (e.g. `Component`, `Operator`, `Invocation`, `CapabilityProvider` or `Account`)
656    pub fn with_metadata(&mut self, metadata: T) -> &mut Self {
657        self.claims.metadata = Some(metadata);
658        self
659    }
660
661    // Produce a claims set from the builder
662    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/// Validates a signed JWT. This will check the signature, expiration time, and not-valid-before time
672#[allow(clippy::missing_errors_doc)] // TODO: document errors
673pub 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        // calculate roundoff
788        let diff_sec = if diff_sec >= 86400 {
789            // round to days
790            diff_sec - (diff_sec % 86400)
791        } else if diff_sec >= 3600 {
792            // round to hours
793            diff_sec - (diff_sec % 3600)
794        } else if diff_sec >= 60 {
795            // round to minutes
796            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        // Set the issuer to empty
1331        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        // Should be an error, but not because of the segment validation
1406        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}