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

A bit of refactoring, and added aspe delete command

This commit is contained in:
TymanWasTaken 2023-06-26 21:15:34 -04:00
parent 463d4eb459
commit 409fbe5f1d
Signed by: Ty
GPG key ID: 2813440C772555A4
9 changed files with 319 additions and 131 deletions

View file

@ -1,5 +1,6 @@
{
"cSpell.words": [
"Aspe",
"josekit",
"PKCS",
"Pkey"

17
Cargo.lock generated
View file

@ -76,6 +76,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
name = "ariadne-signature-profile-proto"
version = "0.1.0"
dependencies = [
"asp",
"clap",
"data-encoding",
"indoc",
@ -88,6 +89,22 @@ dependencies = [
"tokio",
]
[[package]]
name = "asp"
version = "0.0.0"
dependencies = [
"anyhow",
"data-encoding",
"josekit",
"openssl",
"reqwest",
"serde",
"serde_json",
"sha2",
"thiserror",
"tokio",
]
[[package]]
name = "autocfg"
version = "1.1.0"

View file

@ -18,3 +18,8 @@ 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/*"]

18
crates/asp/Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "asp"
version = "0.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.71"
data-encoding = "2.4.0"
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"
thiserror = "1.0.40"
tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] }

61
crates/asp/src/keys.rs Normal file
View file

@ -0,0 +1,61 @@
use data_encoding::{BASE32_NOPAD, BASE64URL_NOPAD, BASE64_NOPAD};
use josekit::{
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>;
}
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"#)?;
// 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()
)
.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
let fingerprint: Vec<u8> = {
let mut hash = Sha512::new();
hash.update(fingerprint);
hash.finalize().to_vec()
};
// Get the first 16 bytes of the hash
let fingerprint = &fingerprint[0..16];
// Base32 encode the first 16 bytes, and that is the fingerprint
let fingerprint = BASE32_NOPAD.encode(fingerprint);
Ok(fingerprint)
}
fn to_pkcs8(&self) -> anyhow::Result<String> {
// 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(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 encoded = BASE64_NOPAD.encode(&pkcs8);
Ok(encoded)
}
fn from_pkcs8(pkcs8: &[u8]) -> anyhow::Result<Self> {
let decoded_pkcs8 = BASE64_NOPAD.decode(pkcs8)?;
let key_pair = ES256.key_pair_from_der(decoded_pkcs8)?;
Ok(key_pair.to_jwk_key_pair())
}
}

1
crates/asp/src/lib.rs Normal file
View file

@ -0,0 +1 @@
pub mod keys;

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View file

@ -7,7 +7,7 @@ use std::{
};
use clap::{Parser, Subcommand};
use data_encoding::{BASE32_NOPAD, BASE64URL_NOPAD, BASE64_NOPAD};
use data_encoding::{BASE64_NOPAD};
use indoc::printdoc;
use josekit::{
jwk::{
@ -19,10 +19,9 @@ use josekit::{
};
use openssl::pkey::PKey;
use reqwest::{header, redirect, StatusCode};
use sha2::{Digest, Sha512};
use structs::{AspRequestAction, AspRequestClaims, JwkExt};
use asp::keys::JwtExt;
use crate::structs::{AspClaims, AspType};
use crate::structs::{AspClaims, AspType, AspRequestAction, AspRequestClaims};
// Prototype implementation of ASPs
#[derive(Parser)]
@ -33,6 +32,24 @@ struct Cli {
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)
@ -84,37 +101,15 @@ enum Commands {
/// The file (or "-") that contains the JWT
profile: String,
},
UploadProfile {
/// 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 domain of the server to upload the profile to
Aspe {
#[command(subcommand)]
command: AspeCommand,
/// The domain of the server to use for aspe operations
#[arg(short, long)]
server: 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,
},
}
impl JwkExt for Jwk {
fn fingerprint(&self) -> String {
let fingerprint = self
.parameter("x")
.expect("jwk did not contain an \"x\" value");
let fingerprint = BASE64URL_NOPAD
.decode(fingerprint.as_str().unwrap().as_bytes())
.expect("unable to base64 decode JWK \"x\" value");
let fingerprint: Vec<u8> = {
let mut hash = Sha512::new();
hash.update(fingerprint);
hash.finalize().to_vec()
};
let fingerprint = &fingerprint[0..16];
let fingerprint = BASE32_NOPAD.encode(fingerprint);
fingerprint
}
}
#[tokio::main]
async fn main() {
let http = reqwest::Client::builder()
@ -213,7 +208,7 @@ async fn main() {
let jwk = Jwk::from_bytes(jwk_bytes).expect("unable to parse jwk");
// Derive the key fingerprint from the JWT
let fingerprint = jwk.fingerprint();
let fingerprint = jwk.get_fingerprint().expect("unable to calculate jwk fingerprint");
// Construct the JWT header
let mut header = JwsHeader::new();
@ -305,7 +300,7 @@ async fn main() {
Claims:
{claims_formatted}
",
fingerprint = jwk.fingerprint(),
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()),
@ -315,112 +310,204 @@ async fn main() {
claims_formatted = claims.claims.iter().map(|claim| format!("- {claim}")).collect::<Vec<String>>().join("\n")
}
}
Commands::UploadProfile {
key,
server,
profile,
} => {
if key == "-" && profile == "-" {
panic!("Only one of `--key <key>` and `<profile>` can be `-` at a time")
}
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");
};
// 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");
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[..]);
// 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 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.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(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 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 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");
// 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");
// 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());
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")
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")
}
}
}
}
}

View file

@ -66,7 +66,3 @@ pub struct AspRequestClaims {
)]
pub aspe_uri: Option<String>,
}
pub trait JwkExt {
fn fingerprint(&self) -> String;
}