compact_jwt 0.5.6

Minimal implementation of JWT for OIDC and other applications
Documentation
//! JWS Implementation

use crate::compact::{JwsCompact, ProtectedHeader};
use crate::error::JwtError;
use crate::traits::JwsSignable;
use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize};
use std::fmt;

/// A signed jwt which can be converted to a string.
pub struct JwsSigned {
    pub(crate) jwsc: JwsCompact,
}

/// A builder to create a new JWS that can be signed
pub struct JwsBuilder {
    pub(crate) header: ProtectedHeader,
    pub(crate) payload: Vec<u8>,
}

impl From<Vec<u8>> for JwsBuilder {
    fn from(payload: Vec<u8>) -> Self {
        JwsBuilder {
            header: ProtectedHeader::default(),
            payload,
        }
    }
}

impl JwsBuilder {
    /// Create a new builder from a serialisable type
    pub fn into_json<T: Serialize>(value: &T) -> Result<Self, serde_json::Error> {
        serde_json::to_vec(value).map(|payload| JwsBuilder {
            header: ProtectedHeader::default(),
            payload,
        })
    }

    /// Set the content type of this JWS
    pub fn set_typ(mut self, typ: Option<&str>) -> Self {
        self.header.typ = typ.map(|s| s.to_string());
        self
    }

    /// Set the content type of the payload
    pub fn set_cty(mut self, cty: Option<&str>) -> Self {
        self.header.cty = cty.map(|s| s.to_string());
        self
    }

    #[cfg(test)]
    /// Test function : Set the algorithm to use for this JWS
    pub fn set_alg(mut self, alg: crate::compact::JwaAlg) -> Self {
        self.header.alg = alg;
        self
    }

    /// Set the kid (required for Windows Hello/MS Extensions)
    pub fn set_kid(mut self, kid: Option<&str>) -> Self {
        self.header.kid = kid.map(|s| s.to_string());
        self
    }

    /// Set the chain of certificates
    pub fn set_x5c(mut self, x5c: Option<Vec<Vec<u8>>>) -> Self {
        self.header.x5c = x5c.map(|v| {
            v.into_iter()
                .map(|c| general_purpose::STANDARD.encode(c))
                .collect()
        });
        self
    }

    /// Set the content use header
    #[cfg(feature = "msextensions")]
    pub fn set_use(mut self, r#use: Option<&str>) -> Self {
        self.header.r#use = r#use.map(|s| s.to_string());
        self
    }

    /// Set the certificate thumbprint
    pub fn set_x5t(mut self, thumbprint: &str) -> Self {
        self.header.x5t = Some(thumbprint.to_string());
        self
    }

    /// Set the OAuth2 Client ID
    pub fn set_client_id(mut self, client_id: Option<&str>) -> Self {
        self.header.client_id = client_id.map(|s| s.to_string());
        self
    }

    /// Finalise this builder
    pub fn build(self) -> Jws {
        let JwsBuilder { header, payload } = self;
        Jws { header, payload }
    }
}

/// A Jws that is being created or has succeeded in being validated
#[derive(Debug, Clone, PartialEq)]
pub struct Jws {
    pub(crate) header: ProtectedHeader,
    pub(crate) payload: Vec<u8>,
}

impl Jws {
    /// Get the bytes of the payload of this JWS
    pub fn payload(&self) -> &[u8] {
        &self.payload
    }

    /// View the authenticated header content
    pub fn header(&self) -> &ProtectedHeader {
        &self.header
    }

    /// Create a JWS from a serialisable type. This assumes you want to encode
    /// the input value with json.
    pub fn into_json<T: Serialize>(value: &T) -> Result<Jws, serde_json::Error> {
        serde_json::to_vec(value).map(|payload| Jws {
            header: ProtectedHeader::default(),
            payload,
        })
    }

    /// Deserialise the inner payload of this JWS assuming it contains json.
    pub fn from_json<'a, T: Deserialize<'a>>(&'a self) -> Result<T, serde_json::Error> {
        serde_json::from_slice(self.payload())
    }

    pub(crate) fn set_typ(&mut self, typ: Option<&str>) {
        self.header.typ = typ.map(|s| s.to_string());
    }
}

impl JwsSignable for Jws {
    type Signed = JwsCompact;

    fn data(&self) -> Result<JwsCompactSign2Data, JwtError> {
        let payload_b64 = general_purpose::URL_SAFE_NO_PAD.encode(&self.payload);
        Ok(JwsCompactSign2Data {
            header: self.header.clone(),
            payload_b64,
        })
    }

    fn post_process(&self, value: JwsCompact) -> Result<Self::Signed, JwtError> {
        Ok(value)
    }
}

/// Data that will be signed
pub struct JwsCompactSign2Data {
    #[allow(dead_code)]
    pub(crate) header: ProtectedHeader,
    #[allow(dead_code)]
    pub(crate) payload_b64: String,
}

impl JwsSigned {
    /// Invalidate this signed jwt, causing it to require validation before you can use it
    /// again.
    pub fn invalidate(self) -> JwsCompact {
        self.jwsc
    }
}

impl fmt::Display for JwsSigned {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.jwsc.fmt(f)
    }
}

#[cfg(test)]
mod tests {
    use super::JwsBuilder;
    use crate::compact::JwaAlg;
    use crate::crypto::{
        JwsEs256Signer, JwsEs256Verifier, JwsHs256Signer, JwsX509Signer, JwsX509VerifierBuilder,
    };
    use crate::traits::*;
    use crypto_glue::{ecdsa_p384::EcdsaP384PrivateKey, traits::DecodePem, x509::Certificate};
    use serde::{Deserialize, Serialize};
    use std::convert::TryFrom;
    use std::time::{Duration, SystemTime};

    #[derive(Default, Debug, Serialize, Clone, Deserialize, PartialEq)]
    struct CustomExtension {
        my_exten: String,
    }

    #[test]
    fn test_sign_and_validate_es256() {
        let _ = tracing_subscriber::fmt::try_init();
        let jws_es256_signer =
            JwsEs256Signer::generate_es256().expect("failed to construct signer.");
        let jwk_es256_verifier = jws_es256_signer
            .get_verifier()
            .expect("failed to get verifier from signer");

        let inner = CustomExtension {
            my_exten: "Hello".to_string(),
        };

        let payload = serde_json::to_vec(&inner).expect("Unable to serialise");

        let jwt = JwsBuilder::from(payload)
            .set_typ(Some("JWT"))
            .set_kid(Some(jws_es256_signer.get_kid()))
            .set_alg(JwaAlg::ES256)
            .build();

        let jwts = jws_es256_signer.sign(&jwt).expect("failed to sign jwt");

        let jwt_str = jwts.to_string();
        trace!("{}", jwt_str);

        let released = jwk_es256_verifier
            .verify(&jwts)
            .expect("Unable to validate jwt");

        trace!(?released);
        trace!(?jwt);

        assert!(released == jwt);
    }

    #[test]
    fn test_sign_and_validate_hs256() {
        let _ = tracing_subscriber::fmt::try_init();
        let jws_hs256_verifier =
            JwsHs256Signer::generate_hs256().expect("failed to construct signer.");

        let inner = CustomExtension {
            my_exten: "Hello".to_string(),
        };

        let payload = serde_json::to_vec(&inner).expect("Unable to serialise");

        let jwt = JwsBuilder::from(payload)
            .set_typ(Some("JWT"))
            .set_kid(Some(JwsSigner::get_kid(&jws_hs256_verifier)))
            .set_alg(JwaAlg::HS256)
            .build();

        let jwts = jws_hs256_verifier.sign(&jwt).expect("failed to sign jwt");

        let released = jws_hs256_verifier
            .verify(&jwts)
            .expect("Unable to validate jwt");

        trace!(?released);
        trace!(?jwt);

        assert!(released == jwt);
    }

    #[test]
    fn test_verification_jws_x5c() {
        let current_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1770691587);

        const ROOT_CERT: &str = r#"-----BEGIN CERTIFICATE-----
MIICLjCCAbSgAwIBAgIRAU8Kb4rRT0mUsQf9rVbLAhowCgYIKoZIzj0EAwMwQzEL
MAkGA1UEBhMCQVUxETAPBgNVBAoMCFBscyBIZWxwMSEwHwYDVQQDDBhPaCBubyBo
ZSBpcyB3cml0aW5nIGEgQ0EwHhcNMjYwMjEwMDI0NTQyWhcNMjYwMjEwMDM0NTQy
WjBDMQswCQYDVQQGEwJBVTERMA8GA1UECgwIUGxzIEhlbHAxITAfBgNVBAMMGE9o
IG5vIGhlIGlzIHdyaXRpbmcgYSBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABFPu
aQn2tzNSL6ooAmYXhcqHbY8pBNis0LckMMytB1bsCdbS9JWGvxZnSSaDVgENFNvf
64G5Sh+eicjrUcAJLGfmdc7YBdZ3o5NcCWh/C6XiBw3XgnGWw/ZDj4OchlFbNqNs
MGowHQYDVR0OBBYEFAXszmYwxKgrhDYVy/GVodMtQNGhMA8GA1UdEwEB/wQFMAMB
Af8wDgYDVR0PAQH/BAQDAgEGMCgGA1UdHwQhMB8wHaAboBmGF2h0dHBzOi8vZXhh
bXBsZS5jb20vY3JsMAoGCCqGSM49BAMDA2gAMGUCMHQ6Nef4ENwiudXQMcH4DzuB
0zzgF9RIH5GvhcYNZ+13pZtHbiAO8AnHuSYquLFvdgIxAMgLk9X9KxTPcym41pGt
L9Ytm2ZPCfNpWL9qBqy3qPyGtfxpMclA3ck3JKlkGepD/g==
-----END CERTIFICATE-----"#;
        const INT_CERT: &str = r#"-----BEGIN CERTIFICATE-----
MIICZjCCAeugAwIBAgIRAQ1mUzLJHEbjtJrFkwzt2xMwCgYIKoZIzj0EAwMwQzEL
MAkGA1UEBhMCQVUxETAPBgNVBAoMCFBscyBIZWxwMSEwHwYDVQQDDBhPaCBubyBo
ZSBpcyB3cml0aW5nIGEgQ0EwHhcNMjYwMjEwMDI0NTQyWhcNMjYwMjEwMDM0NTQy
WjAxMQswCQYDVQQGEwJBVTEiMCAGA1UEAwwZT2ggbm8gaXRzIGFuIGludGVybWVk
aWF0ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABI198gNal5U9+eMmEs8dlUdURuJU
nn33eysYelGNuWYDYiuPjYlsucHTKkdsNZuY9FicElQewzK7Cr47WZwChoRpOS9B
2r8i10tI2/ipmPIAlqWshSIlPN2UZ+NZ7EDGqqOBtDCBsTAdBgNVHQ4EFgQU7gqP
RZ82etk0XzGfbLWnZa5NEIowHwYDVR0jBBgwFoAUBezOZjDEqCuENhXL8ZWh0y1A
0aEwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwLAYDVR0fBCUw
IzAhoB+gHYYbaHR0cHM6Ly9leGFtcGxlLmNvbS9pbnQvY3JsMB0GA1UdHgEB/wQT
MBGgDzANggtleGFtcGxlLmNvbTAKBggqhkjOPQQDAwNpADBmAjEA9VvAuOTw9192
/Djj7/iOAHbGmjk6a2PUS8CDUwjxL4qBjyRB3dmZNnF2wHPLE/oKAjEAmqhFqxWb
wtlOkqPmniGR103gurd3/alkHcxkqyU0KeQLAJ6gjVtm9wD3qhbDlJpX
-----END CERTIFICATE-----"#;
        const LEAF_CERT: &str = r#"-----BEGIN CERTIFICATE-----
MIICEjCCAZegAwIBAgIRAV1zkh2XSE0wiN+axloUuo0wCgYIKoZIzj0EAwMwMTEL
MAkGA1UEBhMCQVUxIjAgBgNVBAMMGU9oIG5vIGl0cyBhbiBpbnRlcm1lZGlhdGUw
HhcNMjYwMjEwMDI0NTQyWhcNMjYwMjEwMDM0NTQyWjAUMRIwEAYDVQQDDAlsb2Nh
bGhvc3QwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARpZJM5KB3fqb4q9wJLZWq+JH5i
JbM5yZMVKJ6GS7XZDw/cRizVtcDPYiAaiIk76Cg83IPeAZmdOruulkRb5QmMiBJW
PSS2jxMcdE5pwLVvL0p7CS/hOmUS/WHhuS5Pop+jgY8wgYwwHQYDVR0OBBYEFO5A
oPdFRbBrkkBZpzl1k1Aps6FLMB8GA1UdIwQYMBaAFO4Kj0WfNnrZNF8xn2y1p2Wu
TRCKMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgPoMBYGA1UdJQEB/wQMMAoG
CCsGAQUFBwMBMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAKBggqhkjOPQQDAwNpADBm
AjEAy1u1GsYeRUnJxSbuUveiN0rubXgk/y0EnESIM8f2lXe84ofElUXTozNFdWm9
Sr4mAjEAvklqw0mWwQKgr5ypJFj6duCe1QMO0ff1Jv+6Z3NpbFABNquuyi+3H6vV
xH0SEy1Z
-----END CERTIFICATE-----"#;
        const LEAF_KEY: &str = r#"-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDA949NqEFqlXON4L0/8AQuILa53B2JOE14xkWsp3lg0ywWPJ0FzXA0O
UWwCFT564PmgBwYFK4EEACKhZANiAARpZJM5KB3fqb4q9wJLZWq+JH5iJbM5yZMV
KJ6GS7XZDw/cRizVtcDPYiAaiIk76Cg83IPeAZmdOruulkRb5QmMiBJWPSS2jxMc
dE5pwLVvL0p7CS/hOmUS/WHhuS5Pop8=
-----END EC PRIVATE KEY-----"#;

        let _ = tracing_subscriber::fmt::try_init();

        let root_cert =
            Certificate::from_pem(ROOT_CERT.as_bytes()).expect("Failed to parse trust root");
        let int_cert =
            Certificate::from_pem(INT_CERT.as_bytes()).expect("Failed to parse trust int root");
        let leaf_cert = Certificate::from_pem(LEAF_CERT.as_bytes()).expect("Failed to parse leaf");
        let leaf_key =
            EcdsaP384PrivateKey::from_sec1_pem(LEAF_KEY).expect("Failed to parse leaf key");

        let signer = JwsX509Signer::new(leaf_key, &leaf_cert, &[int_cert]);

        let claims_original: std::collections::BTreeMap<String, serde_json::value::Value> =
            std::collections::BTreeMap::from([("a".to_string(), serde_json::value::Value::Null)]);

        let jws = JwsBuilder::into_json(&claims_original)
            .expect("Failed to serialise json")
            .build();

        let jwsu: crate::JwsCompact = signer.sign(&jws).expect("Failed to sign");

        let (leaf, chain) = jwsu
            .get_x5c_chain()
            .expect("Failed to get x5c chain")
            .expect("x5c chain is empty");

        assert!(chain.len() == 1);

        let jws_x509_verifier = JwsX509VerifierBuilder::new(&leaf, &chain)
            .add_trust_root(root_cert)
            .build(current_time)
            .expect("Failed to construct verifier");

        let claims: std::collections::BTreeMap<String, serde_json::value::Value> =
            jws_x509_verifier
                .verify(&jwsu)
                .expect("Failed to verify")
                .from_json()
                .expect("Failed to deserialise contents");

        assert_eq!(claims_original, claims);
    }

    #[test]
    fn test_verification_jws_embedded() {
        use std::str::FromStr;

        let _ = tracing_subscriber::fmt::try_init();

        let jwsu = super::JwsCompact::from_str(
  "eyJhbGciOiJFUzI1NiIsImp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjhyaFRhVElJMHRzY1MyX2QtWUNYRm92RGpRUkxEUTEzbWhHV3d5UTBibWMiLCJ5IjoiYmoyakNkSXkxU3lpcHBkU2lEWmxHZEhMUTR0TG40NjMzTFk2dUJHUWU1NCIsImFsZyI6IkVTMjU2IiwidXNlIjoic2lnIn0sInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX2lkIjoiYTNkYjczYTctNzc3Zi00NzI2LTliZGUtNjBkMjEwOTJlNTFmIiwiYXV0aF90eXBlIjoiZ2VuZXJhdGVkcGFzc3dvcmQiLCJleHBpcnkiOlsyMDIyLDI2MCwyMTk5OCw2NTc4MDM0NjhdLCJ1dWlkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwiZGlzcGxheW5hbWUiOiJTeXN0ZW0gQWRtaW5pc3RyYXRvciIsInNwbiI6ImFkbWluQGlkbS5jb3JlZm9ybS5jb20iLCJtYWlsX3ByaW1hcnkiOm51bGwsImxpbV91aWR4IjpmYWxzZSwibGltX3JtYXgiOjEyOCwibGltX3BtYXgiOjI1NiwibGltX2ZtYXgiOjMyfQ.Y9CeMWwGX4xS4O2Yy9vlTjW-6dL_Ncoo-nWd2344O_SwWdBneDpUE35aA_kuLRg1ssVceyVvCDhlxYOyXwzAjQ"
        )
            .expect("Invalid jwsu");

        let jwk = jwsu.get_jwk_pubkey().expect("Failed to get JWK public key");

        let jws_es256_verifier =
            JwsEs256Verifier::try_from(jwk).expect("Failed to construct verifier");

        let _claims: std::collections::BTreeMap<String, serde_json::value::Value> =
            jws_es256_verifier
                .verify(&jwsu)
                .expect("Failed to verify")
                .from_json()
                .expect("Failed to deserialise contents");
    }
}