diff --git a/Cargo.lock b/Cargo.lock index bc8f695..c5b06ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,55 +17,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "anstream" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is-terminal", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" - -[[package]] -name = "anstyle-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "anstyle-wincon" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" -dependencies = [ - "anstyle", - "windows-sys 0.48.0", -] - [[package]] name = "anyhow" version = "1.0.71" @@ -75,34 +26,30 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "ariadne-signature-profile-proto" version = "0.1.0" -dependencies = [ - "asp", - "clap", - "data-encoding", - "indoc", - "josekit", - "openssl", - "reqwest", - "serde", - "serde_json", - "sha2", - "tokio", -] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "asp" -version = "0.0.0" +version = "0.1.0" dependencies = [ "anyhow", "data-encoding", + "hex_color", "josekit", "openssl", "reqwest", "serde", + "serde-email", "serde_json", "sha2", "thiserror", "tokio", + "url", ] [[package]] @@ -156,54 +103,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "clap" -version = "4.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9394150f5b4273a1763355bd1c2ec54cc5a2593f790587bcd6b2c947cfa9211" -dependencies = [ - "clap_builder", - "clap_derive", - "once_cell", -] - -[[package]] -name = "clap_builder" -version = "4.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a78fbdd3cc2914ddf37ba444114bc7765bbdcb55ec9cbe6fa054f0137400717" -dependencies = [ - "anstream", - "anstyle", - "bitflags", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - [[package]] name = "core-foundation" version = "0.9.3" @@ -264,6 +163,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.32" @@ -398,6 +306,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "h2" version = "0.3.20" @@ -429,12 +348,6 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "hermit-abi" version = "0.2.6" @@ -450,6 +363,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex_color" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff917051cbc87800de93ddcf39b59c9f2a0a4d809411a341c0ac422771219808" +dependencies = [ + "arrayvec", + "rand", + "serde", +] + [[package]] name = "http" version = "0.2.9" @@ -551,12 +475,6 @@ dependencies = [ "hashbrown 0.14.0", ] -[[package]] -name = "indoc" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2cb48b81b1dc9f39676bf99f5499babfec7cd8fe14307f7b3d747208fb5690" - [[package]] name = "instant" version = "0.1.12" @@ -583,18 +501,6 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" -[[package]] -name = "is-terminal" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", -] - [[package]] name = "itoa" version = "1.0.6" @@ -786,6 +692,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.63" @@ -804,6 +716,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -928,6 +870,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-email" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4bbdc7ba85a57465d7e2947111caf279892eba3d4891bf770401d2a6ee5de4f" +dependencies = [ + "email_address", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.164" @@ -993,12 +945,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "syn" version = "2.0.22" @@ -1195,14 +1141,9 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index d30659c..9af7df1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,19 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -clap = { version = "4.3.8", features = ["derive"] } -data-encoding = "2.4.0" -indoc = "2.0.1" -josekit = "0.8.3" -openssl = "0.10.55" -reqwest = "0.11.18" -serde = { version = "1.0.164", features = ["derive"] } -serde_json = "1.0.99" -sha2 = "0.10.7" -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } - -asp = { path = "crates/asp" } - [workspace] members = ["crates/*"] diff --git a/crates/asp/Cargo.toml b/crates/asp/Cargo.toml index 5dc8180..90b1496 100644 --- a/crates/asp/Cargo.toml +++ b/crates/asp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "asp" -version = "0.0.0" +version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -8,11 +8,14 @@ edition = "2021" [dependencies] anyhow = "1.0.71" data-encoding = "2.4.0" +hex_color = { version = "2.0.0", features = ["serde"] } josekit = "0.8.3" openssl = "0.10.55" reqwest = "0.11.18" serde = { version = "1.0.164", features = ["derive"] } +serde-email = "2.0.0" serde_json = "1.0.99" sha2 = "0.10.7" thiserror = "1.0.40" tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +url = { version = "2.4.0", features = ["serde"] } diff --git a/crates/asp/src/keys/mod.rs b/crates/asp/src/keys/mod.rs new file mode 100644 index 0000000..691ed9c --- /dev/null +++ b/crates/asp/src/keys/mod.rs @@ -0,0 +1,248 @@ +use josekit::{ + jwk::{ + alg::{ec::EcCurve, ed::EdCurve}, + Jwk, + }, + jws::{ + alg::{ecdsa::EcdsaJwsAlgorithm::Es256, eddsa::EddsaJwsAlgorithm::Eddsa}, + JwsHeader, JwsSigner, JwsVerifier, + }, +}; +use thiserror::Error; + +use crate::utils::jwk::JwtExt; + +/// An enum representing the possible types of JWK for ASPs +#[derive(Debug)] +pub enum AspKeyType { + EdDSA, + ES256, +} + +/// A struct representing a key that can be used to create profiles or ASPE requests +#[derive(Debug)] +pub struct AspKey { + pub key_type: AspKeyType, + pub fingerprint: String, + pub jwk: Jwk, +} + +impl AspKey { + pub fn from_jwk(jwk: Jwk) -> Result { + match jwk.key_type() { + "OKP" => match jwk.curve() { + Some("Ed25519") => Ok(Self { + key_type: AspKeyType::EdDSA, + fingerprint: jwk + .get_fingerprint() + .or(Err(AspKeyError::FingerprintError))?, + jwk, + }), + _ => Err(AspKeyError::InvalidJwkType), + }, + "EC" => match jwk.curve() { + Some("P-256") => Ok(Self { + key_type: AspKeyType::ES256, + fingerprint: jwk + .get_fingerprint() + .or(Err(AspKeyError::FingerprintError))?, + jwk, + }), + _ => Err(AspKeyError::InvalidJwkType), + }, + _ => Err(AspKeyError::InvalidJwkType), + } + } + + pub fn from_pkcs8(key: &str) -> Result { + Self::from_jwk(Jwk::from_pkcs8(key.as_bytes()).or(Err(AspKeyError::Pkcs8ConversionError))?) + } + + pub fn into_pkcs8(&self) -> Result { + self.jwk + .to_pkcs8() + .or(Err(AspKeyError::Pkcs8ConversionError)) + } + + pub fn generate(key_type: AspKeyType) -> Result { + let result: anyhow::Result = try { + match key_type { + AspKeyType::EdDSA => { + let jwk = Jwk::generate_ed_key(EdCurve::Ed25519)?; + Self { + key_type, + fingerprint: jwk.get_fingerprint()?, + jwk, + } + } + AspKeyType::ES256 => { + let jwk = Jwk::generate_ec_key(EcCurve::P256)?; + Self { + key_type, + fingerprint: jwk.get_fingerprint()?, + jwk, + } + } + } + }; + result.or(Err(AspKeyError::GenerationError)) + } + + pub fn create_signer(&self) -> anyhow::Result> { + Ok(match self.key_type { + AspKeyType::EdDSA => Box::new(Eddsa.signer_from_jwk(&self.jwk)?), + AspKeyType::ES256 => Box::new(Es256.signer_from_jwk(&self.jwk)?), + }) + } + + pub fn create_verifier(&self) -> anyhow::Result> { + Ok(match self.key_type { + AspKeyType::EdDSA => Box::new(Eddsa.verifier_from_jwk(&self.jwk)?), + AspKeyType::ES256 => Box::new(Es256.verifier_from_jwk(&self.jwk)?), + }) + } + + pub fn export_encrypted(&self, secret: &[u8]) -> anyhow::Result { + self.jwk.encrypt(secret) + } + + pub fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result { + let jwk = Jwk::from_encrypted(secret, jwe)?; + Ok(Self::from_jwk(jwk)?) + } +} + +impl TryFrom for AspKey { + type Error = AspKeyError; + + fn try_from(value: Jwk) -> Result { + Self::from_jwk(value) + } +} + +pub trait JwsHeaderExt { + fn set_asp_key(&mut self, key: &AspKey) -> anyhow::Result<()>; +} + +impl JwsHeaderExt for JwsHeader { + fn set_asp_key(&mut self, key: &AspKey) -> anyhow::Result<()> { + self.set_algorithm(match key.key_type { + AspKeyType::ES256 => "ES256", + AspKeyType::EdDSA => "EdDSA", + }); + self.set_key_id(&key.fingerprint); + self.set_jwk(key.jwk.to_public_key()?); + Ok(()) + } +} + +#[derive(Error, Debug, PartialEq)] +pub enum AspKeyError { + #[error("provided jwk was not a valid type")] + InvalidJwkType, + #[error("unable to calculate fingerprint of key")] + FingerprintError, + #[error("an error occurred during key generation")] + GenerationError, + #[error("unable to convert PKCS#8 key to/from a jwt key")] + Pkcs8ConversionError, +} + +#[cfg(test)] +mod tests { + use josekit::jwk::{ + alg::{ec::EcCurve, ed::EdCurve}, + Jwk, + }; + + use crate::keys::{AspKey, AspKeyError, AspKeyType}; + + #[test] + fn generate_eddsa() { + let key = AspKey::generate(AspKeyType::EdDSA); + assert!(key.is_ok(), "key should generate successfully"); + let key = key.unwrap(); + assert_eq!(key.jwk.key_type(), "OKP", "key should have type of OKP"); + assert_eq!( + key.jwk.curve(), + Some("Ed25519"), + "key should have curve of Ed25519" + ); + } + + #[test] + fn generate_es256() { + let key = AspKey::generate(AspKeyType::ES256); + assert!(key.is_ok(), "key should generate successfully"); + let key = key.unwrap(); + assert_eq!(key.jwk.key_type(), "EC", "key should have type of EC"); + assert_eq!( + key.jwk.curve(), + Some("P-256"), + "key should have curve of P-256" + ); + } + + #[test] + fn convert_eddsa_jwk() { + let jwk = Jwk::generate_ed_key(EdCurve::Ed25519); + assert!(jwk.is_ok(), "jwk should generate successfully"); + let key = AspKey::from_jwk(jwk.unwrap()); + assert!(key.is_ok(), "key should generate successfully"); + let key = key.unwrap(); + assert_eq!(key.jwk.key_type(), "OKP", "key should have type of OKP"); + assert_eq!( + key.jwk.curve(), + Some("Ed25519"), + "key should have curve of Ed25519" + ); + } + + #[test] + fn convert_es256_jwk() { + let jwk = Jwk::generate_ec_key(EcCurve::P256); + assert!(jwk.is_ok(), "jwk should generate successfully"); + let key = AspKey::from_jwk(jwk.unwrap()); + assert!(key.is_ok(), "key should generate successfully"); + let key = key.unwrap(); + assert_eq!(key.jwk.key_type(), "EC", "key should have type of EC"); + assert_eq!( + key.jwk.curve(), + Some("P-256"), + "key should have curve of P-256" + ); + } + + #[test] + fn convert_invalid_jwk() { + let jwk = Jwk::generate_ec_key(EcCurve::P521); // Invalid curve type! + assert!(jwk.is_ok(), "jwk should generate successfully"); + let key = AspKey::from_jwk(jwk.unwrap()); + assert_eq!( + key.err(), + Some(AspKeyError::InvalidJwkType), + "key should fail to convert" + ); + } + + #[test] + fn export_encrypted() { + let mut secret = [0u8; 32]; + assert!(openssl::rand::rand_bytes(&mut secret).is_ok()); + let key = AspKey::generate(AspKeyType::EdDSA); + assert!(key.is_ok()); + let jwe = key.unwrap().export_encrypted(&secret); + assert!(jwe.is_ok()); + } + + #[test] + fn import_encrypted() { + let mut secret = [0u8; 32]; + assert!(openssl::rand::rand_bytes(&mut secret).is_ok()); + let key = AspKey::generate(AspKeyType::EdDSA).unwrap(); + let encrypted = key.export_encrypted(&secret); + assert!(encrypted.is_ok()); + let decrypted = AspKey::from_encrypted(&secret, &encrypted.unwrap()); + assert!(decrypted.is_ok()); + } +} diff --git a/crates/asp/src/lib.rs b/crates/asp/src/lib.rs index 703bc08..e052a39 100644 --- a/crates/asp/src/lib.rs +++ b/crates/asp/src/lib.rs @@ -1 +1,5 @@ +#![feature(try_blocks)] + pub mod keys; +pub mod profiles; +pub mod utils; diff --git a/crates/asp/src/profiles/mod.rs b/crates/asp/src/profiles/mod.rs new file mode 100644 index 0000000..b123851 --- /dev/null +++ b/crates/asp/src/profiles/mod.rs @@ -0,0 +1,135 @@ +use hex_color::HexColor; +use serde::{Deserialize, Serialize}; +use serde_email::Email; +use url::Url; + +use crate::utils::jwt::JwtSerializable; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum AspType { + Profile, + Request, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct AriadneSignatureProfile { + #[serde(rename = "http://ariadne.id/version")] + pub version: u8, + #[serde(rename = "http://ariadne.id/type")] + pub r#type: AspType, + #[serde(rename = "http://ariadne.id/name")] + pub name: String, + #[serde(rename = "http://ariadne.id/claims")] + pub claims: Vec, + #[serde( + rename = "http://ariadne.id/description", + skip_serializing_if = "Option::is_none" + )] + pub description: Option, + #[serde( + rename = "http://ariadne.id/avatar_url", + skip_serializing_if = "Option::is_none" + )] + pub avatar_url: Option, + #[serde( + rename = "http://ariadne.id/email", + skip_serializing_if = "Option::is_none" + )] + pub email: Option, + #[serde( + rename = "http://ariadne.id/color", + skip_serializing_if = "Option::is_none" + )] + pub color: Option, +} + +impl JwtSerializable for AriadneSignatureProfile {} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use hex_color::HexColor; + use josekit::jwk::Jwk; + + use crate::{keys::AspKey, utils::jwt::JwtSerialize}; + + use super::{AriadneSignatureProfile, AspType}; + + #[test] + fn serializing_profile_succeeds() { + // NOTE: This key is taken from the example keys in RFC 7517 + let key = TryInto::::try_into( + Jwk::from_bytes( + r#" + {"kty":"EC", + "crv":"P-256", + "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE"} + "#, + ) + .unwrap(), + ) + .unwrap(); + + let profile = AriadneSignatureProfile { + version: 0, + r#type: AspType::Profile, + name: "Example name".to_string(), + claims: vec![ + "dns:example.com?type=TXT".to_string(), + "https://git.example.com/example/forgejo_proof".to_string(), + ], + description: None, + avatar_url: None, + email: None, + color: Some(HexColor::from_str("#a434eb").unwrap()), + }; + + let jwt = profile.encode_and_sign(&key); + assert!(jwt.is_ok(), "Jwt should encode and sign successfully"); + } + + #[test] + fn deserializing_profile_succeeds() { + // NOTE: This key is taken from the example keys in RFC 7517 + let key = TryInto::::try_into( + Jwk::from_bytes( + r#" + {"kty":"EC", + "crv":"P-256", + "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE"} + "#, + ) + .unwrap(), + ) + .unwrap(); + + let jwt = r"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjQ1MkpGQUk2QjNLT0xLQkFVWDNNQzczREFVIiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiTUtCQ1ROSWNLVVNEaWkxMXlTczM1MjZpRFo4QWlUbzdUdTZLUEFxdjdENCIsInkiOiI0RXRsNlNSVzJZaUxVck41dmZ2Vkh1aHA3eDhQeGx0bVdXbGJiTTRJRnlNIn19.eyJodHRwOi8vYXJpYWRuZS5pZC92ZXJzaW9uIjowLCJodHRwOi8vYXJpYWRuZS5pZC90eXBlIjoicHJvZmlsZSIsImh0dHA6Ly9hcmlhZG5lLmlkL25hbWUiOiJFeGFtcGxlIG5hbWUiLCJodHRwOi8vYXJpYWRuZS5pZC9jbGFpbXMiOlsiZG5zOmV4YW1wbGUuY29tP3R5cGU9VFhUIiwiaHR0cHM6Ly9naXQuZXhhbXBsZS5jb20vZXhhbXBsZS9mb3JnZWpvX3Byb29mIl0sImh0dHA6Ly9hcmlhZG5lLmlkL2NvbG9yIjoiI0E0MzRFQiJ9.u5AbAqRpyXetXwU_QqpZrieNzwZGCRZ0tFTL4FoIwPRiZZ9iIGBnqs7PWbsd0iHQpYT_Q7s1GmwggGssM9ttxQ"; + + let profile = AriadneSignatureProfile::decode_and_verify(jwt, &key); + assert!(profile.is_ok(), "Profile should parse and verify correctly"); + let profile = profile.unwrap(); + assert_eq!( + *profile, + AriadneSignatureProfile { + version: 0, + r#type: AspType::Profile, + name: "Example name".to_string(), + claims: vec![ + "dns:example.com?type=TXT".to_string(), + "https://git.example.com/example/forgejo_proof".to_string() + ], + description: None, + avatar_url: None, + email: None, + color: Some(HexColor::from_str("#a434eb").unwrap()), + }, + "Profile should decode correctly" + ); + } +} diff --git a/crates/asp/src/keys.rs b/crates/asp/src/utils/jwk.rs similarity index 66% rename from crates/asp/src/keys.rs rename to crates/asp/src/utils/jwk.rs index afe0b94..8f8400a 100644 --- a/crates/asp/src/keys.rs +++ b/crates/asp/src/utils/jwk.rs @@ -1,30 +1,36 @@ +use anyhow::Context; use data_encoding::{BASE32_NOPAD, BASE64URL_NOPAD, BASE64_NOPAD}; use josekit::{ + jwe::JweHeader, + jwe::{self, alg::aesgcmkw::AesgcmkwJweAlgorithm::A256gcmkw}, jwk::{alg::ec::EcKeyPair, Jwk}, jws::ES256, }; use openssl::pkey::PKey; use sha2::{Digest, Sha512}; -use anyhow::Context; pub trait JwtExt { fn get_fingerprint(&self) -> anyhow::Result; fn to_pkcs8(&self) -> anyhow::Result; fn from_pkcs8(pkcs8: &[u8]) -> anyhow::Result; + fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result; + fn encrypt(&self, secret: &[u8]) -> anyhow::Result; } impl JwtExt for Jwk { fn get_fingerprint(&self) -> anyhow::Result { // Get the "x" value of the JWK - let fingerprint = self.parameter("x").context(r#"Jwk "x" parameter was not present"#)?; + let fingerprint = self + .parameter("x") + .context(r#"Jwk "x" parameter was not present"#)?; // Base64url decode the "x" value and use that as the public key value let fingerprint = BASE64URL_NOPAD .decode( // The as_str() can be unwrapped safely because it is impossible to create a Jwk struct where the "x" value is not a string - fingerprint.as_str().unwrap().as_bytes() + fingerprint.as_str().unwrap().as_bytes(), ) .unwrap(); // The decode() can be unwrapped safely because it is impossible to create a Jwk struct where the "x" value is not base64url decodable - // Sha512 hash the public key + // Sha512 hash the public key let fingerprint: Vec = { let mut hash = Sha512::new(); hash.update(fingerprint); @@ -46,9 +52,7 @@ impl JwtExt for Jwk { let key_pair = EcKeyPair::from_jwk(self)?; let pem_private = key_pair.to_pem_private_key(); let pkey = PKey::private_key_from_pem(&pem_private)?; - let pkcs8 = pkey - .as_ref() - .private_key_to_pkcs8()?; + let pkcs8 = pkey.as_ref().private_key_to_pkcs8()?; let encoded = BASE64_NOPAD.encode(&pkcs8); Ok(encoded) } @@ -58,4 +62,25 @@ impl JwtExt for Jwk { let key_pair = ES256.key_pair_from_der(decoded_pkcs8)?; Ok(key_pair.to_jwk_key_pair()) } -} \ No newline at end of file + + fn encrypt(&self, secret: &[u8]) -> anyhow::Result { + let mut header = JweHeader::new(); + header.set_content_type("jwt+json"); + header.set_content_encryption("A128CBC-HS256"); + + let payload = self.to_string(); + + let encrypter = A256gcmkw.encrypter_from_bytes(secret)?; + let jwt = jwe::serialize_compact(payload.as_bytes(), &header, &encrypter)?; + + Ok(jwt) + } + + fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result { + let decrypter = A256gcmkw.decrypter_from_bytes(secret)?; + let (deserialized, _) = jwe::deserialize_compact(jwe, &decrypter)?; + let jwk = Jwk::from_bytes(&deserialized)?; + + Ok(jwk) + } +} diff --git a/crates/asp/src/utils/jwt.rs b/crates/asp/src/utils/jwt.rs new file mode 100644 index 0000000..0f1eced --- /dev/null +++ b/crates/asp/src/utils/jwt.rs @@ -0,0 +1,104 @@ +use anyhow::Context; +use josekit::{ + jws::JwsHeader, + jwt::{self, JwtPayload}, +}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::keys::{AspKey, JwsHeaderExt}; + +pub trait JwtSerializable {} + +pub trait JwtSerialize { + fn encode_and_sign(&self, key: &AspKey) -> Result; + fn decode_and_verify(jwt: &str, key: &AspKey) -> Result, JwtDeserializationError> + where + Self: for<'de> Deserialize<'de> + JwtSerializable; +} + +#[derive(Error, Debug)] +pub enum JwtSerializationError { + #[error("provided jwk was unable to be used")] + JwkUsageError, + #[error("provided payload was unable to be serialized")] + PayloadSerializationError, + #[error("jwt was unable to be serialized for an unknown reason")] + SerializationError, +} + +#[derive(Error, Debug)] +pub enum JwtDeserializationError { + #[error("provided jwk was not the correct key for the provided jwt")] + WrongJwkError, + #[error("jwt header was unable to be decoded")] + HeaderDecodeError, + #[error("jwt was unable to be decoded and verified")] + JwtDecodeError, + #[error("provided jwk was unable to be used")] + JwkUsageError, +} + +impl Deserialize<'de>> JwtSerialize for O { + fn encode_and_sign(&self, key: &AspKey) -> Result { + // Construct the JWT header + let mut header = JwsHeader::new(); + header.set_token_type("JWT"); + header + .set_asp_key(&key) + .or(Err(JwtSerializationError::JwkUsageError))?; + + // Construct the payload + let value = + serde_json::to_value(self).or(Err(JwtSerializationError::PayloadSerializationError))?; + let map = value + .as_object() + .context("serialized struct was not a Map") + .or(Err(JwtSerializationError::PayloadSerializationError))?; + let payload = JwtPayload::from_map(map.clone()) + .or(Err(JwtSerializationError::PayloadSerializationError))?; + + // Sign it into a JWT + Ok(jwt::encode_with_signer( + &payload, + &header, + &*key + .create_signer() + .or(Err(JwtSerializationError::JwkUsageError))?, + ) + .or(Err(JwtSerializationError::SerializationError))?) + } + + fn decode_and_verify(jwt: &str, key: &AspKey) -> Result, JwtDeserializationError> + where + Self: for<'de> serde::Deserialize<'de> + JwtSerializable, + { + // Decode the header and check if the key id is correct + let header = jwt::decode_header(jwt).or(Err(JwtDeserializationError::HeaderDecodeError))?; + let key_id = header + .claim("kid") + .context("kid value on header was missing") + .or(Err(JwtDeserializationError::HeaderDecodeError))? + .as_str() + .context("kid value on header was not a string") + .or(Err(JwtDeserializationError::HeaderDecodeError))?; + + if key.fingerprint != key_id { + return Err(JwtDeserializationError::WrongJwkError); + }; + + // Decode the rest of the JWT + let (payload, _) = jwt::decode_with_verifier( + jwt, + &*key + .create_verifier() + .or(Err(JwtDeserializationError::JwkUsageError))?, + ) + .or(Err(JwtDeserializationError::JwtDecodeError))?; + let claims: Self = + serde_json::from_value(serde_json::Value::Object(payload.claims_set().clone())) + .or(Err(JwtDeserializationError::JwtDecodeError))?; + + Ok(Box::new(claims)) + } +} diff --git a/crates/asp/src/utils/mod.rs b/crates/asp/src/utils/mod.rs new file mode 100644 index 0000000..9332967 --- /dev/null +++ b/crates/asp/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod jwk; +pub mod jwt; diff --git a/src/main.rs b/src/main.rs index f52d1b1..e0a21f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,515 +1,3 @@ -mod structs; - -use std::{ - fs::File, - io::{Read, Write}, - time::SystemTime, -}; - -use clap::{Parser, Subcommand}; -use data_encoding::{BASE64_NOPAD}; -use indoc::printdoc; -use josekit::{ - jwk::{ - alg::ec::{EcCurve, EcKeyPair}, - Jwk, - }, - jws::{JwsHeader, ES256}, - jwt::{self, JwtPayload}, -}; -use openssl::pkey::PKey; -use reqwest::{header, redirect, StatusCode}; -use asp::keys::JwtExt; - -use crate::structs::{AspClaims, AspType, AspRequestAction, AspRequestClaims}; - -// Prototype implementation of ASPs -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -#[command(propagate_version = true)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum AspeCommand { - /// Uploads a profile to an aspe server - Upload { - /// The path of the key file (stored in JSON JWK format) to be used when signing the profile, or - to parse a JWK from stdin (but only this or the profile can be taken from stdin at once). This can be generated with the `create-key` subcommand. - #[arg(short, long)] - key: String, - /// The JWT profile to upload to the server (or - for stdin, but only this or the key can be taken from stdin at once) - profile: String, - }, - /// Deletes a profile on an aspe server - Delete { - /// The path of the key file (stored in JSON JWK format) to be used when signing the profile, or - to parse a JWK from stdin (but only this or the profile can be taken from stdin at once). This can be generated with the `create-key` subcommand. - #[arg(short, long)] - key: String, - }, -} - -#[derive(Subcommand)] -enum Commands { - /// Creates a new JWK (EC, p-256) - CreateKey { - /// The file to output to (or - for stdout) - #[arg(short, long)] - file: String, - }, - /// Converts a pkcs8 private key to a JWK (Ec, p-256) - ConvertFromPkcs8 { - /// The pkcs8-encoded private key - data: String, - /// The file to output to (or - for stdout) - #[arg(short, long)] - file: String, - }, - /// Converts a JWT (Ec, p-256) to a pkcs8 private key - ConvertToPkcs8 { - /// The file to input the JWT from (or - for stdin) - #[arg(short, long)] - key: String, - }, - /// Creates a profile with the specified claims - CreateProfile { - /// The path of the key file (stored in JSON JWK format) to be used when signing the profile, or - to parse a JWK from stdin. This can be generated with the `create-key` subcommand. - #[arg(short, long)] - key: String, - /// The claims to use on the profile - #[arg(short, long)] - claim: Vec, - /// The name to use for the profile - #[arg(short, long)] - name: String, - /// The description to use for the profile - #[arg(short, long)] - description: Option, - /// The url for the avatar of this profile - #[arg(short, long)] - avatar_url: Option, - /// The email to use for this profile - #[arg(short, long)] - email: Option, - /// The color to use for this profile - #[arg(long)] - color: Option, - }, - /// Parses and displays nicely a profile JWT - ParseProfile { - /// The file (or "-") that contains the JWT - profile: String, - }, - Aspe { - #[command(subcommand)] - command: AspeCommand, - /// The domain of the server to use for aspe operations - #[arg(short, long)] - server: String, - }, -} - -#[tokio::main] -async fn main() { - let http = reqwest::Client::builder() - .redirect(redirect::Policy::none()) - .user_agent(format!( - "{}/{}", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION") - )) - .build() - .expect("unable to create http client"); - let args = Cli::parse(); - - match &args.command { - Commands::CreateKey { file } => { - let key = Jwk::generate_ec_key(EcCurve::P256).expect("Unable to generate EC256 JWK"); - if file != "-" { - let mut file = File::create("key.jwk").unwrap(); - file.write_all(key.to_string().as_bytes()) - .expect("unable to write key to file"); - } else { - std::io::stdout() - .write_all(key.to_string().as_bytes()) - .expect("unable to write key to stdout"); - }; - } - Commands::ConvertFromPkcs8 { data, file } => { - let keypair = ES256 - .key_pair_from_der( - BASE64_NOPAD - .decode(data.as_bytes()) - .expect("unable to base64 decode input private key"), - ) - .expect("unable to parse private key into an ES256 key pair"); - let jwk = keypair.to_jwk_key_pair(); - - if file != "-" { - let mut file = File::create("key.jwk").unwrap(); - file.write_all(jwk.to_string().as_bytes()) - .expect("unable to write key to file"); - } else { - std::io::stdout() - .write_all(jwk.to_string().as_bytes()) - .expect("unable to write key to stdout"); - }; - } - Commands::ConvertToPkcs8 { key } => { - let mut jwk_bytes = Vec::new(); - if key == "-" { - std::io::stdin() - .read_to_end(&mut jwk_bytes) - .expect("unable to read from stdin"); - } else { - File::open(key) - .expect("unable to open keyfile") - .read_to_end(&mut jwk_bytes) - .expect("unable to read keyfile"); - }; - - let jwk = Jwk::from_bytes(jwk_bytes).expect("unable to parse jwk"); - - // This was done because I couldn't find an easy way to get a PKCS#8 encoded private key from the josekit Jwk struct, so I did the following as a workaround, directly using the openssl library: - // 1. Get the josekit EcKeyPair from the Jwk - // 2. Convert that to a PEM private key - // 3. Get the openssl Pkey struct by loading the PRM private key - // 4. Convert that Pkey into the PKCS#8 encoded private key (and then base64 encode it) - let key_pair = EcKeyPair::from_jwk(&jwk).expect("unable to create EcKeyPair from jwk"); - let pem_private = key_pair.to_pem_private_key(); - let pkey = PKey::private_key_from_pem(&pem_private).unwrap(); - println!( - "{}", - BASE64_NOPAD.encode(&pkey.as_ref().private_key_to_pkcs8().unwrap()) - ); - } - Commands::CreateProfile { - key, - claim, - name, - description, - avatar_url, - email, - color, - } => { - let mut jwk_bytes = Vec::new(); - if key == "-" { - std::io::stdin() - .read_to_end(&mut jwk_bytes) - .expect("unable to read from stdin"); - } else { - File::open(key) - .expect("unable to open keyfile") - .read_to_end(&mut jwk_bytes) - .expect("unable to read keyfile"); - }; - - let jwk = Jwk::from_bytes(jwk_bytes).expect("unable to parse jwk"); - - // Derive the key fingerprint from the JWT - let fingerprint = jwk.get_fingerprint().expect("unable to calculate jwk fingerprint"); - - // Construct the JWT header - let mut header = JwsHeader::new(); - header.set_token_type("JWT"); - header.set_algorithm("ES256"); - header.set_jwk( - jwk.to_public_key() - .expect("unable to convert jwk to public key"), - ); - header.set_key_id(fingerprint); - - // Construct the JWT payload - let payload = JwtPayload::from_map( - serde_json::to_value(AspClaims { - name: name.clone(), - version: 0, - r#type: AspType::Profile, - claims: claim.clone(), - description: description.clone(), - avatar_url: avatar_url.clone(), - email: email.clone(), - color: color.clone(), - }) - .unwrap() - .as_object() - .unwrap() - .clone(), - ) - .expect("unable to create payload from map of claims"); - - // Construct the JWT itself - let signer = ES256 - .signer_from_jwk(&jwk) - .expect("unable to convert jwk to jwt signer"); - let jwt = jwt::encode_with_signer(&payload, &header, &signer) - .expect("unable to create and sign jwt"); - println!("{jwt}"); - } - Commands::ParseProfile { profile } => { - // Get the profile JWT - let mut jwt_bytes = Vec::new(); - if profile == "-" { - std::io::stdin() - .read_to_end(&mut jwt_bytes) - .expect("unable to read from stdin"); - } else { - File::open(profile) - .expect("unable to open jwt file") - .read_to_end(&mut jwt_bytes) - .expect("unable to read jwt file"); - }; - // Strip newlines - let jwt_bytes = jwt_bytes.strip_suffix(&[10]).unwrap_or(&jwt_bytes[..]); - - // Parse the header of the JWT in order to fetch the JWK from it - let header = jwt::decode_header(jwt_bytes).expect("unable to decode header"); - let jwk = Jwk::from_map( - header - .claim("jwk") - .expect("jwt does not have key embedded") - .as_object() - .expect("embedded jwk is not object type") - .clone(), - ) - .expect("unable to parse embedded jwk"); - - // Parse and verify the JWT - let verifier = ES256 - .verifier_from_jwk(&jwk) - .expect("unable to create verifier from jwk"); - let (payload, _) = jwt::decode_with_verifier(&jwt_bytes, &verifier) - .expect("unable to parse and verify jwt"); - - let claims: AspClaims = - serde_json::from_value(serde_json::Value::Object(payload.claims_set().clone())) - .expect("unable to deserialize jwt payload claims"); - - printdoc! { - " - Profile fingerprint: {fingerprint} - Profile version: {version} - - Name: {name} - Email: {email} - Avatar url: {avatar_url} - Description: - | {description} - Color: {color} - Claims: - {claims_formatted} - ", - fingerprint = jwk.get_fingerprint().expect("unable to calculate jwt fingerprint"), - version = claims.version, - name = claims.name, - description = claims.description.unwrap_or("N/A".to_string()), - email = claims.email.unwrap_or("N/A".to_string()), - avatar_url = claims.avatar_url.unwrap_or("N/A".to_string()), - color = claims.color.unwrap_or("N/A".to_string()), - claims_formatted = claims.claims.iter().map(|claim| format!("- {claim}")).collect::>().join("\n") - } - } - Commands::Aspe { command, server } => { - match command { - AspeCommand::Upload { - key, - profile, - } => { - if key == "-" && profile == "-" { - panic!("Only one of `--key ` and `` can be `-` at a time") - } - - // Obtain the JWK w/ private key - let mut jwk_bytes = Vec::new(); - if key == "-" { - std::io::stdin() - .read_to_end(&mut jwk_bytes) - .expect("unable to read from stdin"); - } else { - File::open(key) - .expect("unable to open keyfile") - .read_to_end(&mut jwk_bytes) - .expect("unable to read keyfile"); - }; - - let jwk = Jwk::from_bytes(jwk_bytes).expect("unable to parse jwk"); - - // Obtain the JWT profile - let mut jwt_bytes = Vec::new(); - if profile == "-" { - std::io::stdin() - .read_to_end(&mut jwt_bytes) - .expect("unable to read from stdin"); - } else { - File::open(profile) - .expect("unable to open jwt file") - .read_to_end(&mut jwt_bytes) - .expect("unable to read jwt file"); - }; - // Strip newlines - let jwt_bytes = jwt_bytes.strip_suffix(&[10]).unwrap_or(&jwt_bytes[..]); - - // Construct a request JWT - - // Construct the JWT header - let mut header = JwsHeader::new(); - header.set_token_type("JWT"); - header.set_algorithm("ES256"); - header.set_jwk( - jwk.to_public_key() - .expect("unable to convert jwk to public key"), - ); - header.set_key_id(jwk.get_fingerprint().expect("unable to calculate jwk fingerprint")); - - // Construct the payload - let mut payload = JwtPayload::from_map( - serde_json::to_value(AspRequestClaims { - version: 0, - r#type: AspType::Request, - action: AspRequestAction::Create, - aspe_uri: None, - profile_jws: Some( - String::from_utf8(jwt_bytes.to_vec()) - .expect("unable to parse jwt bytes into String"), - ), - }) - .unwrap() - .as_object() - .unwrap() - .clone(), - ) - .expect("unable to create payload from map of claims"); - payload.set_issued_at(&SystemTime::now()); - - // Construct the signer - let signer = ES256 - .signer_from_jwk(&jwk) - .expect("unable to construct signer from jwk"); - - // Construct the actual jwt - let jwt = jwt::encode_with_signer(&payload, &header.into(), &signer) - .expect("unable to create and sign jwt"); - - // Send the request - let response = http - .post(format!("https://{server}/.well-known/aspe/post")) - .header(header::CONTENT_TYPE, "application/jose; charset=UTF-8") - .body(jwt) - .send() - .await - .expect("unable to send http request to server"); - - match response.status() { - StatusCode::CREATED => { - dbg!(response.text().await.unwrap()); - } - StatusCode::BAD_REQUEST => { - dbg!(response.text().await.unwrap()); - panic!("Request returned 400, did the request take over 60 seconds to send?") - } - StatusCode::TOO_MANY_REQUESTS => { - dbg!(response.text().await.unwrap()); - panic!("Ratelimited"); - } - _ => { - dbg!(response.status()); - dbg!(response.text().await.unwrap()); - - panic!("wtf") - } - } - }, - AspeCommand::Delete { - key, - } => { - // Obtain the JWK w/ private key - let mut jwk_bytes = Vec::new(); - if key == "-" { - std::io::stdin() - .read_to_end(&mut jwk_bytes) - .expect("unable to read from stdin"); - } else { - File::open(key) - .expect("unable to open keyfile") - .read_to_end(&mut jwk_bytes) - .expect("unable to read keyfile"); - }; - - let jwk = Jwk::from_bytes(jwk_bytes).expect("unable to parse jwk"); - - // Construct a request JWT - let fingerprint = jwk.get_fingerprint().expect("unable to calculate jwt fingerprint"); - - // Construct the JWT header - let mut header = JwsHeader::new(); - header.set_token_type("JWT"); - header.set_algorithm("ES256"); - header.set_jwk( - jwk.to_public_key() - .expect("unable to convert jwk to public key"), - ); - header.set_key_id(&fingerprint); - - // Construct the payload - let mut payload = JwtPayload::from_map( - serde_json::to_value(AspRequestClaims { - version: 0, - r#type: AspType::Request, - action: AspRequestAction::Delete, - aspe_uri: Some(format!("aspe:{server}:{fingerprint}")), - profile_jws: None, - }) - .unwrap() - .as_object() - .unwrap() - .clone(), - ) - .expect("unable to create payload from map of claims"); - payload.set_issued_at(&SystemTime::now()); - - // Construct the signer - let signer = ES256 - .signer_from_jwk(&jwk) - .expect("unable to construct signer from jwk"); - - // Construct the actual jwt - let jwt = jwt::encode_with_signer(&payload, &header.into(), &signer) - .expect("unable to create and sign jwt"); - dbg!(&jwt); - // Send the request - let response = http - .post(format!("https://{server}/.well-known/aspe/post")) - .header(header::CONTENT_TYPE, "application/jose; charset=UTF-8") - .body(jwt) - .send() - .await - .expect("unable to send http request to server"); - - match response.status() { - StatusCode::OK => { - dbg!(response.status()); - dbg!(response.text().await.unwrap()); - } - StatusCode::BAD_REQUEST => { - dbg!(response.status()); - dbg!(response.text().await.unwrap()); - panic!("Request returned 400, did the request take over 60 seconds to send?") - } - StatusCode::TOO_MANY_REQUESTS => { - dbg!(response.status()); - dbg!(response.text().await.unwrap()); - panic!("Ratelimited"); - } - _ => { - dbg!(response.status()); - dbg!(response.text().await.unwrap()); - - panic!("wtf") - } - } - } - } - } - } -} +fn main() { + todo!(); +} \ No newline at end of file diff --git a/src/structs.rs b/src/structs.rs deleted file mode 100644 index 7e6800e..0000000 --- a/src/structs.rs +++ /dev/null @@ -1,68 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub enum AspType { - Profile, - Request, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct AspClaims { - #[serde(rename = "http://ariadne.id/version")] - pub version: u8, - #[serde(rename = "http://ariadne.id/type")] - pub r#type: AspType, - #[serde(rename = "http://ariadne.id/name")] - pub name: String, - #[serde(rename = "http://ariadne.id/claims")] - pub claims: Vec, - #[serde( - rename = "http://ariadne.id/description", - skip_serializing_if = "Option::is_none" - )] - pub description: Option, - #[serde( - rename = "http://ariadne.id/avatar_url", - skip_serializing_if = "Option::is_none" - )] - pub avatar_url: Option, - #[serde( - rename = "http://ariadne.id/email", - skip_serializing_if = "Option::is_none" - )] - pub email: Option, - #[serde( - rename = "http://ariadne.id/color", - skip_serializing_if = "Option::is_none" - )] - pub color: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub enum AspRequestAction { - Create, - Update, - Delete, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct AspRequestClaims { - #[serde(rename = "http://ariadne.id/version")] - pub version: u8, - #[serde(rename = "http://ariadne.id/type")] - pub r#type: AspType, - #[serde(rename = "http://ariadne.id/action")] - pub action: AspRequestAction, - #[serde( - rename = "http://ariadne.id/profile_jws", - skip_serializing_if = "Option::is_none" - )] - pub profile_jws: Option, - #[serde( - rename = "http://ariadne.id/aspe_uri", - skip_serializing_if = "Option::is_none" - )] - pub aspe_uri: Option, -}