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:
parent
409fbe5f1d
commit
ea388904c2
11 changed files with 621 additions and 753 deletions
235
Cargo.lock
generated
235
Cargo.lock
generated
|
@ -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"
|
||||
|
|
14
Cargo.toml
14
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/*"]
|
||||
|
|
|
@ -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
248
crates/asp/src/keys/mod.rs
Normal 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());
|
||||
}
|
||||
}
|
|
@ -1 +1,5 @@
|
|||
#![feature(try_blocks)]
|
||||
|
||||
pub mod keys;
|
||||
pub mod profiles;
|
||||
pub mod utils;
|
||||
|
|
135
crates/asp/src/profiles/mod.rs
Normal file
135
crates/asp/src/profiles/mod.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<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
|
||||
// Sha512 hash the public key
|
||||
let fingerprint: Vec<u8> = {
|
||||
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())
|
||||
}
|
||||
|
||||
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
104
crates/asp/src/utils/jwt.rs
Normal 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))
|
||||
}
|
||||
}
|
2
crates/asp/src/utils/mod.rs
Normal file
2
crates/asp/src/utils/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod jwk;
|
||||
pub mod jwt;
|
516
src/main.rs
516
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<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!();
|
||||
}
|
|
@ -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>,
|
||||
}
|
Loading…
Reference in a new issue