mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-23 00:09:28 -07:00
A bit of refactoring, and added aspe delete
command
This commit is contained in:
parent
463d4eb459
commit
409fbe5f1d
9 changed files with 319 additions and 131 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"Aspe",
|
||||||
"josekit",
|
"josekit",
|
||||||
"PKCS",
|
"PKCS",
|
||||||
"Pkey"
|
"Pkey"
|
||||||
|
|
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -76,6 +76,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||||
name = "ariadne-signature-profile-proto"
|
name = "ariadne-signature-profile-proto"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"asp",
|
||||||
"clap",
|
"clap",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"indoc",
|
"indoc",
|
||||||
|
@ -88,6 +89,22 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asp"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"data-encoding",
|
||||||
|
"josekit",
|
||||||
|
"openssl",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
|
@ -18,3 +18,8 @@ serde = { version = "1.0.164", features = ["derive"] }
|
||||||
serde_json = "1.0.99"
|
serde_json = "1.0.99"
|
||||||
sha2 = "0.10.7"
|
sha2 = "0.10.7"
|
||||||
tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] }
|
||||||
|
|
||||||
|
asp = { path = "crates/asp" }
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["crates/*"]
|
||||||
|
|
18
crates/asp/Cargo.toml
Normal file
18
crates/asp/Cargo.toml
Normal 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
61
crates/asp/src/keys.rs
Normal 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
1
crates/asp/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod keys;
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
157
src/main.rs
157
src/main.rs
|
@ -7,7 +7,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use data_encoding::{BASE32_NOPAD, BASE64URL_NOPAD, BASE64_NOPAD};
|
use data_encoding::{BASE64_NOPAD};
|
||||||
use indoc::printdoc;
|
use indoc::printdoc;
|
||||||
use josekit::{
|
use josekit::{
|
||||||
jwk::{
|
jwk::{
|
||||||
|
@ -19,10 +19,9 @@ use josekit::{
|
||||||
};
|
};
|
||||||
use openssl::pkey::PKey;
|
use openssl::pkey::PKey;
|
||||||
use reqwest::{header, redirect, StatusCode};
|
use reqwest::{header, redirect, StatusCode};
|
||||||
use sha2::{Digest, Sha512};
|
use asp::keys::JwtExt;
|
||||||
use structs::{AspRequestAction, AspRequestClaims, JwkExt};
|
|
||||||
|
|
||||||
use crate::structs::{AspClaims, AspType};
|
use crate::structs::{AspClaims, AspType, AspRequestAction, AspRequestClaims};
|
||||||
|
|
||||||
// Prototype implementation of ASPs
|
// Prototype implementation of ASPs
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -33,6 +32,24 @@ struct Cli {
|
||||||
command: Commands,
|
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)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Creates a new JWK (EC, p-256)
|
/// Creates a new JWK (EC, p-256)
|
||||||
|
@ -84,37 +101,15 @@ enum Commands {
|
||||||
/// The file (or "-") that contains the JWT
|
/// The file (or "-") that contains the JWT
|
||||||
profile: String,
|
profile: String,
|
||||||
},
|
},
|
||||||
UploadProfile {
|
Aspe {
|
||||||
/// 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.
|
#[command(subcommand)]
|
||||||
#[arg(short, long)]
|
command: AspeCommand,
|
||||||
key: String,
|
/// The domain of the server to use for aspe operations
|
||||||
/// The domain of the server to upload the profile to
|
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
server: String,
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let http = reqwest::Client::builder()
|
let http = reqwest::Client::builder()
|
||||||
|
@ -213,7 +208,7 @@ async fn main() {
|
||||||
let jwk = Jwk::from_bytes(jwk_bytes).expect("unable to parse jwk");
|
let jwk = Jwk::from_bytes(jwk_bytes).expect("unable to parse jwk");
|
||||||
|
|
||||||
// Derive the key fingerprint from the JWT
|
// 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
|
// Construct the JWT header
|
||||||
let mut header = JwsHeader::new();
|
let mut header = JwsHeader::new();
|
||||||
|
@ -305,7 +300,7 @@ async fn main() {
|
||||||
Claims:
|
Claims:
|
||||||
{claims_formatted}
|
{claims_formatted}
|
||||||
",
|
",
|
||||||
fingerprint = jwk.fingerprint(),
|
fingerprint = jwk.get_fingerprint().expect("unable to calculate jwt fingerprint"),
|
||||||
version = claims.version,
|
version = claims.version,
|
||||||
name = claims.name,
|
name = claims.name,
|
||||||
description = claims.description.unwrap_or("N/A".to_string()),
|
description = claims.description.unwrap_or("N/A".to_string()),
|
||||||
|
@ -315,9 +310,10 @@ async fn main() {
|
||||||
claims_formatted = claims.claims.iter().map(|claim| format!("- {claim}")).collect::<Vec<String>>().join("\n")
|
claims_formatted = claims.claims.iter().map(|claim| format!("- {claim}")).collect::<Vec<String>>().join("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::UploadProfile {
|
Commands::Aspe { command, server } => {
|
||||||
|
match command {
|
||||||
|
AspeCommand::Upload {
|
||||||
key,
|
key,
|
||||||
server,
|
|
||||||
profile,
|
profile,
|
||||||
} => {
|
} => {
|
||||||
if key == "-" && profile == "-" {
|
if key == "-" && profile == "-" {
|
||||||
|
@ -364,7 +360,7 @@ async fn main() {
|
||||||
jwk.to_public_key()
|
jwk.to_public_key()
|
||||||
.expect("unable to convert jwk to public key"),
|
.expect("unable to convert jwk to public key"),
|
||||||
);
|
);
|
||||||
header.set_key_id(jwk.fingerprint());
|
header.set_key_id(jwk.get_fingerprint().expect("unable to calculate jwk fingerprint"));
|
||||||
|
|
||||||
// Construct the payload
|
// Construct the payload
|
||||||
let mut payload = JwtPayload::from_map(
|
let mut payload = JwtPayload::from_map(
|
||||||
|
@ -423,6 +419,97 @@ async fn main() {
|
||||||
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,3 @@ pub struct AspRequestClaims {
|
||||||
)]
|
)]
|
||||||
pub aspe_uri: Option<String>,
|
pub aspe_uri: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait JwkExt {
|
|
||||||
fn fingerprint(&self) -> String;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue