1
0
Fork 0
mirror of https://codeberg.org/tyy/aspm synced 2024-12-22 21:49:28 -07:00

Scrapped the entire thing lol

I was going to refactor slowly, but it is easier just to scrap the entire binary and make a new one
This commit is contained in:
TymanWasTaken 2023-06-29 00:24:42 -04:00
parent 409fbe5f1d
commit ea388904c2
Signed by: Ty
GPG key ID: 2813440C772555A4
11 changed files with 621 additions and 753 deletions

235
Cargo.lock generated
View file

@ -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"

View file

@ -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/*"]

View file

@ -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"] }

248
crates/asp/src/keys/mod.rs Normal file
View file

@ -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<Self, AspKeyError> {
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, AspKeyError> {
Self::from_jwk(Jwk::from_pkcs8(key.as_bytes()).or(Err(AspKeyError::Pkcs8ConversionError))?)
}
pub fn into_pkcs8(&self) -> Result<String, AspKeyError> {
self.jwk
.to_pkcs8()
.or(Err(AspKeyError::Pkcs8ConversionError))
}
pub fn generate(key_type: AspKeyType) -> Result<Self, AspKeyError> {
let result: anyhow::Result<Self> = 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<Box<dyn JwsSigner>> {
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<Box<dyn JwsVerifier>> {
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<String> {
self.jwk.encrypt(secret)
}
pub fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result<Self> {
let jwk = Jwk::from_encrypted(secret, jwe)?;
Ok(Self::from_jwk(jwk)?)
}
}
impl TryFrom<Jwk> for AspKey {
type Error = AspKeyError;
fn try_from(value: Jwk) -> Result<Self, Self::Error> {
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());
}
}

View file

@ -1 +1,5 @@
#![feature(try_blocks)]
pub mod keys;
pub mod profiles;
pub mod utils;

View file

@ -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<String>,
#[serde(
rename = "http://ariadne.id/description",
skip_serializing_if = "Option::is_none"
)]
pub description: Option<String>,
#[serde(
rename = "http://ariadne.id/avatar_url",
skip_serializing_if = "Option::is_none"
)]
pub avatar_url: Option<Url>,
#[serde(
rename = "http://ariadne.id/email",
skip_serializing_if = "Option::is_none"
)]
pub email: Option<Email>,
#[serde(
rename = "http://ariadne.id/color",
skip_serializing_if = "Option::is_none"
)]
pub color: Option<HexColor>,
}
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::<AspKey>::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::<AspKey>::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"
);
}
}

View file

@ -1,27 +1,33 @@
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<String>;
fn to_pkcs8(&self) -> anyhow::Result<String>;
fn from_pkcs8(pkcs8: &[u8]) -> anyhow::Result<Jwk>;
fn from_encrypted(secret: &[u8], jwe: &str) -> anyhow::Result<Jwk>;
fn encrypt(&self, secret: &[u8]) -> anyhow::Result<String>;
}
impl JwtExt for Jwk {
fn get_fingerprint(&self) -> anyhow::Result<String> {
// 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
@ -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())
}
fn encrypt(&self, secret: &[u8]) -> anyhow::Result<String> {
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<Jwk> {
let decrypter = A256gcmkw.decrypter_from_bytes(secret)?;
let (deserialized, _) = jwe::deserialize_compact(jwe, &decrypter)?;
let jwk = Jwk::from_bytes(&deserialized)?;
Ok(jwk)
}
}

104
crates/asp/src/utils/jwt.rs Normal file
View file

@ -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<String, JwtSerializationError>;
fn decode_and_verify(jwt: &str, key: &AspKey) -> Result<Box<Self>, 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<O: JwtSerializable + Serialize + for<'de> Deserialize<'de>> JwtSerialize for O {
fn encode_and_sign(&self, key: &AspKey) -> Result<String, JwtSerializationError> {
// 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<Box<Self>, 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))
}
}

View file

@ -0,0 +1,2 @@
pub mod jwk;
pub mod jwt;

View file

@ -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<String>,
/// The name to use for the profile
#[arg(short, long)]
name: String,
/// The description to use for the profile
#[arg(short, long)]
description: Option<String>,
/// The url for the avatar of this profile
#[arg(short, long)]
avatar_url: Option<String>,
/// The email to use for this profile
#[arg(short, long)]
email: Option<String>,
/// The color to use for this profile
#[arg(long)]
color: Option<String>,
},
/// 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::<Vec<String>>().join("\n")
}
}
Commands::Aspe { command, server } => {
match command {
AspeCommand::Upload {
key,
profile,
} => {
if key == "-" && profile == "-" {
panic!("Only one of `--key <key>` and `<profile>` 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!();
}

View file

@ -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<String>,
#[serde(
rename = "http://ariadne.id/description",
skip_serializing_if = "Option::is_none"
)]
pub description: Option<String>,
#[serde(
rename = "http://ariadne.id/avatar_url",
skip_serializing_if = "Option::is_none"
)]
pub avatar_url: Option<String>,
#[serde(
rename = "http://ariadne.id/email",
skip_serializing_if = "Option::is_none"
)]
pub email: Option<String>,
#[serde(
rename = "http://ariadne.id/color",
skip_serializing_if = "Option::is_none"
)]
pub color: Option<String>,
}
#[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<String>,
#[serde(
rename = "http://ariadne.id/aspe_uri",
skip_serializing_if = "Option::is_none"
)]
pub aspe_uri: Option<String>,
}