passport 0.4.0

Crate for signing and validating PASSporT (RFC8225) tokens
Documentation
//! passport-rs
//!
//! A library for generating JWT passports following [RFC-8225](https://tools.ietf.org/html/rfc8225)
//!
//! Usage:
//! ```rust
//! let passport_builder =
//!     passport::PassportBuilder::new(String::from("https://cert.example.org/passport.cer"), passport::Identity::URI(String::from("https://matrix.to/#/@alice:example.org")))
//!         .add_destination(passport::Identity::URI(String::from("https://matrix.to/#/@bob:example.org")))
//!         .set_expires_in(Some(512));
//!
//! let jwt = passport_builder.encode(
//!     &passport::EncodingKey::from_secret(b"test_secret"),
//!     passport::Algorithm::HS512,
//! ).unwrap();
//! ```

#![deny(trivial_casts, trivial_numeric_casts, unused_extern_crates, unused_qualifications)]
#![warn(
	missing_debug_implementations,
	missing_docs,
	unused_import_braces,
	dead_code,
	clippy::unwrap_used,
	clippy::expect_used,
	clippy::missing_docs_in_private_items,
	clippy::missing_panics_doc
)]

/// Structs for representing and parsing JWT data from configuration files
pub mod config;

use std::{collections::HashMap, fmt, time::SystemTime};

use jsonwebtoken::{encode, DecodingKey, Header};
pub use jsonwebtoken::{Algorithm, EncodingKey};
use serde::{Deserialize, Serialize, Serializer};

/// Wrapper for both JWT and Serde errors
pub enum Error {
	/// Wrapped [`jsonwebtoken::errors::Error`](https://docs.rs/jsonwebtoken/7.2.0/jsonwebtoken/errors/struct.Error.html)
	JWT(jsonwebtoken::errors::Error),
	/// Wrapped [`serde_json::Error`](https://docs.serde.rs/serde_json/struct.Error.html)
	Serde(serde_json::Error),
}

impl From<jsonwebtoken::errors::Error> for Error {
	fn from(err: jsonwebtoken::errors::Error) -> Self {
		Self::JWT(err)
	}
}

impl From<serde_json::Error> for Error {
	fn from(err: serde_json::Error) -> Self {
		Self::Serde(err)
	}
}

impl fmt::Debug for Error {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			Error::JWT(e) => e.fmt(f),
			Error::Serde(e) => e.fmt(f),
		}
	}
}

impl fmt::Display for Error {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			Error::JWT(e) => e.fmt(f),
			Error::Serde(e) => e.fmt(f),
		}
	}
}

/// Given the key and an algorithm it will return an [`EncodingKey`](https://docs.rs/jsonwebtoken/7.2.0/jsonwebtoken/struct.EncodingKey.html)
pub fn make_encoding_key(
	key: &[u8],
	algorithm: Algorithm,
) -> jsonwebtoken::errors::Result<EncodingKey> {
	match algorithm {
		Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => Ok(EncodingKey::from_secret(key)),
		Algorithm::ES256 | Algorithm::ES384 => EncodingKey::from_ec_pem(key),
		Algorithm::RS256
		| Algorithm::RS384
		| Algorithm::RS512
		| Algorithm::PS256
		| Algorithm::PS384
		| Algorithm::PS512 => EncodingKey::from_rsa_pem(key),
		Algorithm::EdDSA => EncodingKey::from_ed_pem(key),
	}
}

/// Given the key and an algorithm it will return a [`DecodingKey`](https://docs.rs/jsonwebtoken/7.2.0/jsonwebtoken/struct.DecodingKey.html)
pub fn make_decoding_key(
	key: &[u8],
	algorithm: Algorithm,
) -> jsonwebtoken::errors::Result<DecodingKey> {
	match algorithm {
		Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => Ok(DecodingKey::from_secret(key)),
		Algorithm::ES256 | Algorithm::ES384 => DecodingKey::from_ec_pem(key),
		Algorithm::RS256
		| Algorithm::RS384
		| Algorithm::RS512
		| Algorithm::PS256
		| Algorithm::PS384
		| Algorithm::PS512 => DecodingKey::from_rsa_pem(key),
		Algorithm::EdDSA => DecodingKey::from_ed_pem(key),
	}
}

/// Factory for building passports
#[derive(Debug, Clone)]
pub struct PassportBuilder {
	/// The JSON Web Signature `x5u` URI pointing to the X.509 public key of
	/// this JWT. See [section 4.3] of the RFC.
	///
	/// [section 4.3]: https://datatracker.ietf.org/doc/html/rfc8225#section-4.3
	certificate_url: String,
	/// The claims in the token payload.
	claims: PassportClaims,
	/// How many seconds the token should be valid for. `None` means
	/// indefinitely.
	expires_in: Option<u64>,
}

impl PassportBuilder {
	/// Creates a new builder
	pub fn new(certificate_url: String, origin: Identity) -> Self {
		Self { certificate_url, claims: PassportClaims::new(origin), expires_in: None }
	}

	/// Adds an entry to the `media_keys` claim for new passports
	pub fn add_media_key(mut self, key: MediaKey) -> Self {
		self.claims = self.claims.add_media_key(key);
		self
	}

	/// Adds a destination to new passports
	pub fn add_destination(mut self, identity: Identity) -> Self {
		self.claims = self.claims.add_destination(identity);
		self
	}

	/// Optionally passports expire `expires_in` seconds after creation
	pub fn set_expires_in(mut self, expires_in: Option<u64>) -> Self {
		self.expires_in = expires_in;
		self
	}

	/// Creates and encodes a new passport
	pub fn encode(mut self, key: &EncodingKey, algorithm: Algorithm) -> Result<String, Error> {
		let header = Header {
			typ: Some(String::from("passport")),
			alg: algorithm,
			cty: None,
			jku: None,
			kid: None,
			x5u: Some(self.certificate_url.clone()),
			x5t: None,
			jwk: None,
			x5c: None,
			x5t_s256: None,
		};

		self.claims = self.claims.set_issuing_time(self.expires_in);
		Ok(encode(&header, &self.claims, key)?)
	}
}

/// Extended [JWT Claims](https://tools.ietf.org/html/rfc8225#section-5) for passports
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PassportClaims {
	/// The destination identity claim. See [section 5.2.1] of the RFC.
	///
	/// [section 5.2.1](https://datatracker.ietf.org/doc/html/rfc8225#section-5.2.1)
	#[serde(default, skip_serializing_if = "HashMap::is_empty", rename = "dest")]
	pub destination: HashMap<IdentityForms, Vec<String>>,

	/// The origin identity claim. See [section 5.2.1] of the RFC.
	///
	/// [section 5.2.1](https://datatracker.ietf.org/doc/html/rfc8225#section-5.2.1)
	#[serde(rename = "orig")]
	pub origin: Identity,

	/// The unix timestamp for when the claim was issued.
	#[serde(rename = "iat")]
	pub issued_at: Option<u64>,

	/// The unix timestamp for when the claim expires.
	#[serde(default, skip_serializing_if = "Option::is_none", rename = "exp")]
	pub expires_at: Option<u64>,

	/// Media security key digests. See [section 5.2.2] of the RFC.
	///
	/// [section 5.2.2](https://datatracker.ietf.org/doc/html/rfc8225#section-5.2.2)
	#[serde(default, skip_serializing_if = "Vec::is_empty", rename = "mky")]
	pub media_keys: Vec<MediaKey>,
}

impl PassportClaims {
	/// Construct a new set of passport claims with the given origin identity.
	fn new(origin: Identity) -> Self {
		Self {
			destination: HashMap::new(),
			origin,
			issued_at: None,
			expires_at: None,
			media_keys: Vec::new(),
		}
	}

	/// Add a media key.
	fn add_media_key(mut self, key: MediaKey) -> Self {
		self.media_keys.push(key);
		self
	}

	/// Add a destination identity.
	fn add_destination(mut self, identity: Identity) -> Self {
		let inner = identity.clone().into_inner();
		let key = IdentityForms::from(&identity);
		self.destination.entry(key).or_insert_with(Vec::new).push(inner);
		self
	}

	/// Set the issuing time based on the system clock, and optionally also the
	/// expiration time based on the amount of seconds the token should be
	/// valid.
	fn set_issuing_time(mut self, expires_in: Option<u64>) -> Self {
		#[allow(clippy::expect_used)]
		let now = SystemTime::UNIX_EPOCH.elapsed().expect("System time is before unix epoch").as_secs();
		self.issued_at = Some(now);
		self.expires_at = expires_in.map(|t| t + now);
		self
	}
}

/// Ways of repsenting [identities](https://tools.ietf.org/html/rfc8225#section-5.2.1)
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Clone, Copy)]
pub enum IdentityForms {
	/// [Telephone Number](https://tools.ietf.org/html/rfc8225#section-5.2.1.1)
	#[serde(rename = "tn")]
	TelephoneNumber,
	/// [URI](https://tools.ietf.org/html/rfc8225#section-5.2.1.2)
	#[serde(rename = "uri")]
	URI,
}

/// Represents and holds the different forms of [identification](https://tools.ietf.org/html/rfc8225#section-5.2.1)
#[derive(Deserialize, Clone, Debug)]
pub enum Identity {
	/// [Telephone Number](https://tools.ietf.org/html/rfc8225#section-5.2.1.1)
	#[serde(rename = "tn")]
	TelephoneNumber(String),
	/// [URI](https://tools.ietf.org/html/rfc8225#section-5.2.1.2)
	#[serde(rename = "uri")]
	URI(String),
}

impl Serialize for Identity {
	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
	where
		S: Serializer,
	{
		let mut data: HashMap<IdentityForms, String> = HashMap::new();
		data.insert(IdentityForms::from(self), self.clone().into_inner());
		serializer.serialize_newtype_struct("Identity", &data)
	}
}

impl From<&Identity> for IdentityForms {
	fn from(identity: &Identity) -> Self {
		match identity {
			Identity::TelephoneNumber(_) => IdentityForms::TelephoneNumber,
			Identity::URI(_) => IdentityForms::URI,
		}
	}
}

impl Identity {
	/// Convert the identity into its string represtentation.
	fn into_inner(self) -> String {
		match self {
			Self::TelephoneNumber(num) => num,
			Self::URI(uri) => uri,
		}
	}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
/// Represents the [Media Key](https://tools.ietf.org/html/rfc8225#section-5.2.2) claim
pub struct MediaKey {
	/// The hashing algorithm applied to the fingerprint.
	algorithm: String,
	/// The digest of the media key's fingerprint.
	digest: String,
}