1
0
Fork 0
mirror of https://codeberg.org/tyy/aspm synced 2024-12-22 20:39:29 -07:00

A whole bunch of changes, I forgor to split it up

This commit is contained in:
Tyler Beckman 2024-03-10 20:25:30 -06:00
parent 561cc3fb42
commit 8af322b2ad
Signed by: Ty
GPG key ID: 2813440C772555A4
20 changed files with 210 additions and 63 deletions

View file

@ -4,7 +4,7 @@ use reqwest::{
header::{self, HeaderValue},
StatusCode,
};
pub use url::Host;
use url::Host;
/// An ASPE-compatible server
pub struct AspeServer {
@ -100,14 +100,14 @@ impl AspeServer {
pub async fn fetch_profile(
&self,
fingerprint: impl Into<String>,
fingerprint: impl AsRef<str>,
) -> Result<String, AspeFetchFailure> {
let res = self
.client
.get(format!(
"https://{host}/.well-known/aspe/id/{fingerprint}",
host = self.host,
fingerprint = fingerprint.into()
fingerprint = fingerprint.as_ref()
))
.header(
header::ACCEPT,

View file

@ -72,12 +72,14 @@ mod tests {
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
.unwrap();
let decoded = AspeRequest::decode_and_verify(crate::test_constants::CREATE_REQUEST, &key);
let decoded = AspeRequest::decode_and_verify(crate::test_constants::CREATE_REQUEST, Some(&key.fingerprint));
assert!(
decoded.is_ok(),
"ASPE request JWS should verify and decode successfully"
);
let decoded = decoded.unwrap();
let (parsed_key, decoded) = decoded.unwrap();
assert_eq!(parsed_key.fingerprint, key.fingerprint); // Comparison by fingerprint makes public == private comparison succeed
assert_eq!(
*decoded,
@ -105,7 +107,7 @@ mod tests {
aspe_uri: crate::test_constants::ASPE_URI.to_string(),
},
};
let encoded = dbg!(request.encode_and_sign(&key));
let encoded = request.encode_and_sign(&key);
assert!(
encoded.is_ok(),
"ASPE request JWS should sign and encode successfully"
@ -117,12 +119,14 @@ mod tests {
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
.unwrap();
let decoded = AspeRequest::decode_and_verify(crate::test_constants::UPDATE_REQUEST, &key);
let decoded = AspeRequest::decode_and_verify(crate::test_constants::UPDATE_REQUEST, Some(&key.fingerprint));
assert!(
decoded.is_ok(),
"ASPE request JWS should verify and decode successfully"
);
let decoded = decoded.unwrap();
let (parsed_key, decoded) = decoded.unwrap();
assert_eq!(parsed_key.fingerprint, key.fingerprint); // Comparison by fingerprint makes public == private comparison succeed
assert_eq!(
*decoded,
@ -150,7 +154,7 @@ mod tests {
aspe_uri: crate::test_constants::ASPE_URI.to_string(),
},
};
let encoded = dbg!(request.encode_and_sign(&key));
let encoded = request.encode_and_sign(&key);
assert!(
encoded.is_ok(),
"ASPE request JWS should sign and encode successfully"
@ -162,12 +166,14 @@ mod tests {
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
.unwrap();
let decoded = AspeRequest::decode_and_verify(crate::test_constants::DELETE_REQUEST, &key);
let decoded = AspeRequest::decode_and_verify(crate::test_constants::DELETE_REQUEST, Some(&key.fingerprint));
assert!(
decoded.is_ok(),
"ASPE request JWS should verify and decode successfully"
);
let decoded = decoded.unwrap();
let (parsed_key, decoded) = decoded.unwrap();
assert_eq!(parsed_key.fingerprint, key.fingerprint); // Comparison by fingerprint makes public == private comparison succeed
assert_eq!(
*decoded,

View file

@ -18,7 +18,7 @@ use serde_json::Map;
use sha2::{Digest, Sha512};
/// An enum representing the possible types of JWK for ASPs
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AspKeyType {
Ed25519,
ES256,
@ -54,7 +54,7 @@ impl TryFrom<i32> for AspKeyType {
}
/// A struct representing a key that can be used to create profiles or ASPE requests
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub struct AspKey {
pub key_type: AspKeyType,
pub fingerprint: String,

View file

@ -3,20 +3,18 @@ pub mod keys;
pub mod profiles;
pub mod utils;
pub use hex_color;
pub use serde_email;
pub use url;
#[cfg(test)]
pub(crate) mod test_constants {
// NOTE: This key is taken from the example keys in RFC 7517
pub(crate) static KEY: &str = r#"
{"kty":"EC",
"crv":"P-256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE"}
"#;
pub(crate) static ASPE_URI: &str = "aspe:example.com:452JFAI6B3KOLKBAUX3MC73DAU";
pub(crate) static KEY: &str = r#"{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM","d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE"}"#;
pub(crate) static ASPE_URI: &str = "aspe:example.com:6O6CWLNM66Z7CYONKDONKLYPAQ";
pub(crate) static PROFILE: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjQ1MkpGQUk2QjNLT0xLQkFVWDNNQzczREFVIiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiTUtCQ1ROSWNLVVNEaWkxMXlTczM1MjZpRFo4QWlUbzdUdTZLUEFxdjdENCIsInkiOiI0RXRsNlNSVzJZaUxVck41dmZ2Vkh1aHA3eDhQeGx0bVdXbGJiTTRJRnlNIn19.eyJodHRwOi8vYXJpYWRuZS5pZC92ZXJzaW9uIjowLCJodHRwOi8vYXJpYWRuZS5pZC90eXBlIjoicHJvZmlsZSIsImh0dHA6Ly9hcmlhZG5lLmlkL25hbWUiOiJFeGFtcGxlIG5hbWUiLCJodHRwOi8vYXJpYWRuZS5pZC9jbGFpbXMiOlsiZG5zOmV4YW1wbGUuY29tP3R5cGU9VFhUIiwiaHR0cHM6Ly9naXQuZXhhbXBsZS5jb20vZXhhbXBsZS9mb3JnZWpvX3Byb29mIl0sImh0dHA6Ly9hcmlhZG5lLmlkL2NvbG9yIjoiI0E0MzRFQiJ9.u5AbAqRpyXetXwU_QqpZrieNzwZGCRZ0tFTL4FoIwPRiZZ9iIGBnqs7PWbsd0iHQpYT_Q7s1GmwggGssM9ttxQ";
pub(crate) static CREATE_REQUEST: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjQ1MkpGQUk2QjNLT0xLQkFVWDNNQzczREFVIiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiTUtCQ1ROSWNLVVNEaWkxMXlTczM1MjZpRFo4QWlUbzdUdTZLUEFxdjdENCIsInkiOiI0RXRsNlNSVzJZaUxVck41dmZ2Vkh1aHA3eDhQeGx0bVdXbGJiTTRJRnlNIn19.eyJodHRwOi8vYXJpYWRuZS5pZC92ZXJzaW9uIjowLCJodHRwOi8vYXJpYWRuZS5pZC90eXBlIjoicmVxdWVzdCIsImh0dHA6Ly9hcmlhZG5lLmlkL2FjdGlvbiI6ImNyZWF0ZSIsImh0dHA6Ly9hcmlhZG5lLmlkL3Byb2ZpbGVfandzIjoiZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKRlV6STFOaUlzSW10cFpDSTZJalExTWtwR1FVazJRak5MVDB4TFFrRlZXRE5OUXpjelJFRlZJaXdpYW5kcklqcDdJbXQwZVNJNklrVkRJaXdpWTNKMklqb2lVQzB5TlRZaUxDSjRJam9pVFV0Q1ExUk9TV05MVlZORWFXa3hNWGxUY3pNMU1qWnBSRm80UVdsVWJ6ZFVkVFpMVUVGeGRqZEVOQ0lzSW5raU9pSTBSWFJzTmxOU1Z6SlphVXhWY2s0MWRtWjJWa2gxYUhBM2VEaFFlR3gwYlZkWGJHSmlUVFJKUm5sTkluMTkuZXlKb2RIUndPaTh2WVhKcFlXUnVaUzVwWkM5MlpYSnphVzl1SWpvd0xDSm9kSFJ3T2k4dllYSnBZV1J1WlM1cFpDOTBlWEJsSWpvaWNISnZabWxzWlNJc0ltaDBkSEE2THk5aGNtbGhaRzVsTG1sa0wyNWhiV1VpT2lKRmVHRnRjR3hsSUc1aGJXVWlMQ0pvZEhSd09pOHZZWEpwWVdSdVpTNXBaQzlqYkdGcGJYTWlPbHNpWkc1ek9tVjRZVzF3YkdVdVkyOXRQM1I1Y0dVOVZGaFVJaXdpYUhSMGNITTZMeTluYVhRdVpYaGhiWEJzWlM1amIyMHZaWGhoYlhCc1pTOW1iM0puWldwdlgzQnliMjltSWwwc0ltaDBkSEE2THk5aGNtbGhaRzVsTG1sa0wyTnZiRzl5SWpvaUkwRTBNelJGUWlKOS51NUFiQXFScHlYZXRYd1VfUXFwWnJpZU56d1pHQ1JaMHRGVEw0Rm9Jd1BSaVpaOWlJR0JucXM3UFdic2QwaUhRcFlUX1E3czFHbXdnZ0dzc005dHR4USJ9.f8NdVzrjCZKT2R5MzUZkgcnNIJWo6ftQj6MCvXF5cgpjYt3suTqOGoBs6EKvtsgVGs12uS4ZxNnVAnFMsKKGlQ";
pub(crate) static UPDATE_REQUEST: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjQ1MkpGQUk2QjNLT0xLQkFVWDNNQzczREFVIiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiTUtCQ1ROSWNLVVNEaWkxMXlTczM1MjZpRFo4QWlUbzdUdTZLUEFxdjdENCIsInkiOiI0RXRsNlNSVzJZaUxVck41dmZ2Vkh1aHA3eDhQeGx0bVdXbGJiTTRJRnlNIn19.eyJodHRwOi8vYXJpYWRuZS5pZC92ZXJzaW9uIjowLCJodHRwOi8vYXJpYWRuZS5pZC90eXBlIjoicmVxdWVzdCIsImh0dHA6Ly9hcmlhZG5lLmlkL2FjdGlvbiI6InVwZGF0ZSIsImh0dHA6Ly9hcmlhZG5lLmlkL3Byb2ZpbGVfandzIjoiZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKRlV6STFOaUlzSW10cFpDSTZJalExTWtwR1FVazJRak5MVDB4TFFrRlZXRE5OUXpjelJFRlZJaXdpYW5kcklqcDdJbXQwZVNJNklrVkRJaXdpWTNKMklqb2lVQzB5TlRZaUxDSjRJam9pVFV0Q1ExUk9TV05MVlZORWFXa3hNWGxUY3pNMU1qWnBSRm80UVdsVWJ6ZFVkVFpMVUVGeGRqZEVOQ0lzSW5raU9pSTBSWFJzTmxOU1Z6SlphVXhWY2s0MWRtWjJWa2gxYUhBM2VEaFFlR3gwYlZkWGJHSmlUVFJKUm5sTkluMTkuZXlKb2RIUndPaTh2WVhKcFlXUnVaUzVwWkM5MlpYSnphVzl1SWpvd0xDSm9kSFJ3T2k4dllYSnBZV1J1WlM1cFpDOTBlWEJsSWpvaWNISnZabWxzWlNJc0ltaDBkSEE2THk5aGNtbGhaRzVsTG1sa0wyNWhiV1VpT2lKRmVHRnRjR3hsSUc1aGJXVWlMQ0pvZEhSd09pOHZZWEpwWVdSdVpTNXBaQzlqYkdGcGJYTWlPbHNpWkc1ek9tVjRZVzF3YkdVdVkyOXRQM1I1Y0dVOVZGaFVJaXdpYUhSMGNITTZMeTluYVhRdVpYaGhiWEJzWlM1amIyMHZaWGhoYlhCc1pTOW1iM0puWldwdlgzQnliMjltSWwwc0ltaDBkSEE2THk5aGNtbGhaRzVsTG1sa0wyTnZiRzl5SWpvaUkwRTBNelJGUWlKOS51NUFiQXFScHlYZXRYd1VfUXFwWnJpZU56d1pHQ1JaMHRGVEw0Rm9Jd1BSaVpaOWlJR0JucXM3UFdic2QwaUhRcFlUX1E3czFHbXdnZ0dzc005dHR4USIsImh0dHA6Ly9hcmlhZG5lLmlkL2FzcGVfdXJpIjoiYXNwZTpleGFtcGxlLmNvbTo0NTJKRkFJNkIzS09MS0JBVVgzTUM3M0RBVSJ9.044vzbhefes8bFJFrXLwU2RNYhNvK_rNDDqM7NjEaC8alyFl-5Fh_Obj-pIKUkcxD-HL27y2objt_-lbDqvw4g";
pub(crate) static DELETE_REQUEST: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjQ1MkpGQUk2QjNLT0xLQkFVWDNNQzczREFVIiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiTUtCQ1ROSWNLVVNEaWkxMXlTczM1MjZpRFo4QWlUbzdUdTZLUEFxdjdENCIsInkiOiI0RXRsNlNSVzJZaUxVck41dmZ2Vkh1aHA3eDhQeGx0bVdXbGJiTTRJRnlNIn19.eyJodHRwOi8vYXJpYWRuZS5pZC92ZXJzaW9uIjowLCJodHRwOi8vYXJpYWRuZS5pZC90eXBlIjoicmVxdWVzdCIsImh0dHA6Ly9hcmlhZG5lLmlkL2FjdGlvbiI6ImRlbGV0ZSIsImh0dHA6Ly9hcmlhZG5lLmlkL2FzcGVfdXJpIjoiYXNwZTpleGFtcGxlLmNvbTo0NTJKRkFJNkIzS09MS0JBVVgzTUM3M0RBVSJ9.DJNuN-wTXxOW3VZHcN_tlUIFOHfI0GeD_uzs1RplwsGTBe0Z4KpIojEQ85N7tSnuLxuGlsR8kd1SrbcvxhkWaw";
pub(crate) static PROFILE: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjZPNkNXTE5NNjZaN0NZT05LRE9OS0xZUEFRIiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiTUtCQ1ROSWNLVVNEaWkxMXlTczM1MjZpRFo4QWlUbzdUdTZLUEFxdjdENCIsInkiOiI0RXRsNlNSVzJZaUxVck41dmZ2Vkh1aHA3eDhQeGx0bVdXbGJiTTRJRnlNIn19.eyJodHRwOi8vYXJpYWRuZS5pZC92ZXJzaW9uIjowLCJodHRwOi8vYXJpYWRuZS5pZC90eXBlIjoicHJvZmlsZSIsImh0dHA6Ly9hcmlhZG5lLmlkL25hbWUiOiJFeGFtcGxlIG5hbWUiLCJodHRwOi8vYXJpYWRuZS5pZC9jbGFpbXMiOlsiZG5zOmV4YW1wbGUuY29tP3R5cGU9VFhUIiwiaHR0cHM6Ly9naXQuZXhhbXBsZS5jb20vZXhhbXBsZS9mb3JnZWpvX3Byb29mIl0sImh0dHA6Ly9hcmlhZG5lLmlkL2Rlc2NyaXB0aW9uIjoiVGhpcyBpcyBhbiBleGFtcGxlIHByb2ZpbGUiLCJodHRwOi8vYXJpYWRuZS5pZC9lbWFpbCI6ImV4YW1wbGVAZXhhbXBsZS5jb20iLCJodHRwOi8vYXJpYWRuZS5pZC9hdmF0YXJfdXJsIjoiaHR0cHM6Ly9wbGFjZWhvbGQuY28vMjU2IiwiaHR0cDovL2FyaWFkbmUuaWQvY29sb3IiOiIjQTQzNEVCIn0.aVdOWTIjdRo8riTiepNIadLNXJxPnOnmKzKzv6C8Abt7hQSlRYrEFg42PpF7R7juz5INuJauAv-xKj2s9kweDA";
pub(crate) static CREATE_REQUEST: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjZPNkNXTE5NNjZaN0NZT05LRE9OS0xZUEFRIiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiTUtCQ1ROSWNLVVNEaWkxMXlTczM1MjZpRFo4QWlUbzdUdTZLUEFxdjdENCIsInkiOiI0RXRsNlNSVzJZaUxVck41dmZ2Vkh1aHA3eDhQeGx0bVdXbGJiTTRJRnlNIn19.eyJodHRwOi8vYXJpYWRuZS5pZC92ZXJzaW9uIjowLCJodHRwOi8vYXJpYWRuZS5pZC90eXBlIjoicmVxdWVzdCIsImh0dHA6Ly9hcmlhZG5lLmlkL2FjdGlvbiI6ImNyZWF0ZSIsImh0dHA6Ly9hcmlhZG5lLmlkL3Byb2ZpbGVfandzIjoiZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKRlV6STFOaUlzSW10cFpDSTZJalpQTmtOWFRFNU5OalphTjBOWlQwNUxSRTlPUzB4WlVFRlJJaXdpYW5kcklqcDdJbXQwZVNJNklrVkRJaXdpWTNKMklqb2lVQzB5TlRZaUxDSjRJam9pVFV0Q1ExUk9TV05MVlZORWFXa3hNWGxUY3pNMU1qWnBSRm80UVdsVWJ6ZFVkVFpMVUVGeGRqZEVOQ0lzSW5raU9pSTBSWFJzTmxOU1Z6SlphVXhWY2s0MWRtWjJWa2gxYUhBM2VEaFFlR3gwYlZkWGJHSmlUVFJKUm5sTkluMTkuZXlKb2RIUndPaTh2WVhKcFlXUnVaUzVwWkM5MlpYSnphVzl1SWpvd0xDSm9kSFJ3T2k4dllYSnBZV1J1WlM1cFpDOTBlWEJsSWpvaWNISnZabWxzWlNJc0ltaDBkSEE2THk5aGNtbGhaRzVsTG1sa0wyNWhiV1VpT2lKRmVHRnRjR3hsSUc1aGJXVWlMQ0pvZEhSd09pOHZZWEpwWVdSdVpTNXBaQzlqYkdGcGJYTWlPbHNpWkc1ek9tVjRZVzF3YkdVdVkyOXRQM1I1Y0dVOVZGaFVJaXdpYUhSMGNITTZMeTluYVhRdVpYaGhiWEJzWlM1amIyMHZaWGhoYlhCc1pTOW1iM0puWldwdlgzQnliMjltSWwwc0ltaDBkSEE2THk5aGNtbGhaRzVsTG1sa0wyUmxjMk55YVhCMGFXOXVJam9pVkdocGN5QnBjeUJoYmlCbGVHRnRjR3hsSUhCeWIyWnBiR1VpTENKb2RIUndPaTh2WVhKcFlXUnVaUzVwWkM5bGJXRnBiQ0k2SW1WNFlXMXdiR1ZBWlhoaGJYQnNaUzVqYjIwaUxDSm9kSFJ3T2k4dllYSnBZV1J1WlM1cFpDOWhkbUYwWVhKZmRYSnNJam9pYUhSMGNITTZMeTl3YkdGalpXaHZiR1F1WTI4dk1qVTJJaXdpYUhSMGNEb3ZMMkZ5YVdGa2JtVXVhV1F2WTI5c2IzSWlPaUlqUVRRek5FVkNJbjAuYVZkT1dUSWpkUm84cmlUaWVwTklhZExOWEp4UG5Pbm1Lekt6djZDOEFidDdoUVNsUllyRUZnNDJQcEY3UjdqdXo1SU51SmF1QXYteEtqMnM5a3dlREEifQ.G5Asc4gswiU0iw8oBZGz4dmPREKmUmHykKfjXY-89sVm_HJPfO4XRmODIqaNkonlehsS23jgry5_Dt4X6fO6PA";
pub(crate) static UPDATE_REQUEST: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjZPNkNXTE5NNjZaN0NZT05LRE9OS0xZUEFRIiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiTUtCQ1ROSWNLVVNEaWkxMXlTczM1MjZpRFo4QWlUbzdUdTZLUEFxdjdENCIsInkiOiI0RXRsNlNSVzJZaUxVck41dmZ2Vkh1aHA3eDhQeGx0bVdXbGJiTTRJRnlNIn19.eyJodHRwOi8vYXJpYWRuZS5pZC92ZXJzaW9uIjowLCJodHRwOi8vYXJpYWRuZS5pZC90eXBlIjoicmVxdWVzdCIsImh0dHA6Ly9hcmlhZG5lLmlkL2FjdGlvbiI6InVwZGF0ZSIsImh0dHA6Ly9hcmlhZG5lLmlkL3Byb2ZpbGVfandzIjoiZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKRlV6STFOaUlzSW10cFpDSTZJalpQTmtOWFRFNU5OalphTjBOWlQwNUxSRTlPUzB4WlVFRlJJaXdpYW5kcklqcDdJbXQwZVNJNklrVkRJaXdpWTNKMklqb2lVQzB5TlRZaUxDSjRJam9pVFV0Q1ExUk9TV05MVlZORWFXa3hNWGxUY3pNMU1qWnBSRm80UVdsVWJ6ZFVkVFpMVUVGeGRqZEVOQ0lzSW5raU9pSTBSWFJzTmxOU1Z6SlphVXhWY2s0MWRtWjJWa2gxYUhBM2VEaFFlR3gwYlZkWGJHSmlUVFJKUm5sTkluMTkuZXlKb2RIUndPaTh2WVhKcFlXUnVaUzVwWkM5MlpYSnphVzl1SWpvd0xDSm9kSFJ3T2k4dllYSnBZV1J1WlM1cFpDOTBlWEJsSWpvaWNISnZabWxzWlNJc0ltaDBkSEE2THk5aGNtbGhaRzVsTG1sa0wyNWhiV1VpT2lKRmVHRnRjR3hsSUc1aGJXVWlMQ0pvZEhSd09pOHZZWEpwWVdSdVpTNXBaQzlqYkdGcGJYTWlPbHNpWkc1ek9tVjRZVzF3YkdVdVkyOXRQM1I1Y0dVOVZGaFVJaXdpYUhSMGNITTZMeTluYVhRdVpYaGhiWEJzWlM1amIyMHZaWGhoYlhCc1pTOW1iM0puWldwdlgzQnliMjltSWwwc0ltaDBkSEE2THk5aGNtbGhaRzVsTG1sa0wyUmxjMk55YVhCMGFXOXVJam9pVkdocGN5QnBjeUJoYmlCbGVHRnRjR3hsSUhCeWIyWnBiR1VpTENKb2RIUndPaTh2WVhKcFlXUnVaUzVwWkM5bGJXRnBiQ0k2SW1WNFlXMXdiR1ZBWlhoaGJYQnNaUzVqYjIwaUxDSm9kSFJ3T2k4dllYSnBZV1J1WlM1cFpDOWhkbUYwWVhKZmRYSnNJam9pYUhSMGNITTZMeTl3YkdGalpXaHZiR1F1WTI4dk1qVTJJaXdpYUhSMGNEb3ZMMkZ5YVdGa2JtVXVhV1F2WTI5c2IzSWlPaUlqUVRRek5FVkNJbjAuYVZkT1dUSWpkUm84cmlUaWVwTklhZExOWEp4UG5Pbm1Lekt6djZDOEFidDdoUVNsUllyRUZnNDJQcEY3UjdqdXo1SU51SmF1QXYteEtqMnM5a3dlREEiLCJodHRwOi8vYXJpYWRuZS5pZC9hc3BlX3VyaSI6ImFzcGU6ZXhhbXBsZS5jb206Nk82Q1dMTk02Nlo3Q1lPTktET05LTFlQQVEifQ.jGAmCQwGZ82iBleew2E1QNohe29HFgnSJO-UxwwYNI4g1n-_wRKVhE2Xw3Tah5UMBByEAbViLmlCMTS_hg6hPA";
pub(crate) static DELETE_REQUEST: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IjZPNkNXTE5NNjZaN0NZT05LRE9OS0xZUEFRIiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiTUtCQ1ROSWNLVVNEaWkxMXlTczM1MjZpRFo4QWlUbzdUdTZLUEFxdjdENCIsInkiOiI0RXRsNlNSVzJZaUxVck41dmZ2Vkh1aHA3eDhQeGx0bVdXbGJiTTRJRnlNIn19.eyJodHRwOi8vYXJpYWRuZS5pZC92ZXJzaW9uIjowLCJodHRwOi8vYXJpYWRuZS5pZC90eXBlIjoicmVxdWVzdCIsImh0dHA6Ly9hcmlhZG5lLmlkL2FjdGlvbiI6ImRlbGV0ZSIsImh0dHA6Ly9hcmlhZG5lLmlkL2FzcGVfdXJpIjoiYXNwZTpleGFtcGxlLmNvbTo2TzZDV0xOTTY2WjdDWU9OS0RPTktMWVBBUSJ9.Z_cZOKHK-QwpejGMU_iAl_5a2teJUmtqwxVfElP-fGL25qZWhFRL5cG-B08YrwBc1vGS5rO31RY1RKtqUiiuKQ";
}

View file

@ -1,7 +1,7 @@
pub use hex_color::HexColor;
use hex_color::HexColor;
use serde::{Deserialize, Serialize};
pub use serde_email::Email;
pub use url::Url;
use serde_email::Email;
use url::Url;
use crate::utils::jwt::{AspJwsType, JwtSerializable};
@ -45,6 +45,8 @@ mod tests {
use hex_color::HexColor;
use josekit::jwk::Jwk;
use serde_email::Email;
use url::Url;
use crate::{
keys::AspKey,
@ -82,9 +84,9 @@ mod tests {
.unwrap();
let profile =
AriadneSignatureProfile::decode_and_verify(crate::test_constants::PROFILE, &key);
AriadneSignatureProfile::decode_and_verify(crate::test_constants::PROFILE, Some(&key.fingerprint));
assert!(profile.is_ok(), "Profile should parse and verify correctly");
let profile = profile.unwrap();
let (_, profile) = profile.unwrap();
assert_eq!(
*profile,
AriadneSignatureProfile {
@ -95,9 +97,9 @@ mod tests {
"dns:example.com?type=TXT".to_string(),
"https://git.example.com/example/forgejo_proof".to_string()
],
description: None,
avatar_url: None,
email: None,
description: Some("This is an example profile".to_string()),
avatar_url: Some(Url::from_str("https://placehold.co/256").unwrap()),
email: Some(Email::from_str("example@example.com").unwrap()),
color: Some(HexColor::from_str("#a434eb").unwrap()),
},
"Profile should decode correctly"

View file

@ -1,5 +1,6 @@
use anyhow::Context;
use josekit::{
jwk::Jwk,
jws::JwsHeader,
jwt::{self, JwtPayload},
};
@ -19,9 +20,10 @@ 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>
fn decode_and_verify<S>(jwt: &str, expected_fingerprint: Option<S>) -> Result<(AspKey, Box<Self>), JwtDeserializationError>
where
Self: for<'de> Deserialize<'de> + JwtSerializable;
Self: for<'de> Deserialize<'de> + JwtSerializable,
S: AsRef<str>;
}
#[derive(Error, Debug)]
@ -36,13 +38,15 @@ pub enum JwtSerializationError {
#[derive(Error, Debug)]
pub enum JwtDeserializationError {
#[error("provided jwk was not the correct key for the provided jwt")]
#[error("nested jwk had an incorrect kid")]
MalformedJwkError,
#[error("nested jwk fingerprint did not match expected fingerprint")]
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")]
#[error("nested jwk was unable to be used for verification")]
JwkUsageError,
}
@ -76,9 +80,10 @@ impl<O: JwtSerializable + Serialize + for<'de> Deserialize<'de>> JwtSerialize fo
.or(Err(JwtSerializationError::SerializationError))
}
fn decode_and_verify(jwt: &str, key: &AspKey) -> Result<Box<Self>, JwtDeserializationError>
fn decode_and_verify<S>(jwt: &str, expected_fingerprint: Option<S>) -> Result<(AspKey, Box<Self>), JwtDeserializationError>
where
Self: for<'de> serde::Deserialize<'de> + JwtSerializable,
S: AsRef<str>
{
// Decode the header and check if the key id is correct
let header = jwt::decode_header(jwt).or(Err(JwtDeserializationError::HeaderDecodeError))?;
@ -89,8 +94,26 @@ impl<O: JwtSerializable + Serialize + for<'de> Deserialize<'de>> JwtSerialize fo
.as_str()
.context("kid value on header was not a string")
.or(Err(JwtDeserializationError::HeaderDecodeError))?;
let key = AspKey::from_jwk(
Jwk::from_map(
header
.claim("jwk")
.context("jwk value on header was missing")
.or(Err(JwtDeserializationError::HeaderDecodeError))?
.as_object()
.context("kid value on header was not an object")
.or(Err(JwtDeserializationError::HeaderDecodeError))?
.clone(),
)
.context("jwk value on header was not a valid jwk")
.or(Err(JwtDeserializationError::HeaderDecodeError))?,
)
.or(Err(JwtDeserializationError::HeaderDecodeError))?;
if key.fingerprint != key_id {
return Err(JwtDeserializationError::MalformedJwkError);
}
if expected_fingerprint.is_some_and(|e| e.as_ref() != key_id) {
return Err(JwtDeserializationError::WrongJwkError);
};
@ -106,6 +129,6 @@ impl<O: JwtSerializable + Serialize + for<'de> Deserialize<'de>> JwtSerialize fo
serde_json::from_value(serde_json::Value::Object(payload.claims_set().clone()))
.or(Err(JwtDeserializationError::JwtDecodeError))?;
Ok(Box::new(claims))
Ok((key, Box::new(claims)))
}
}

View file

@ -25,7 +25,7 @@ pub struct KeysDeleteCommand {
#[async_trait::async_trait]
impl AspmSubcommand for KeysDeleteCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
// Fetch key from db
let entry = Keys::query_key(&state.db, &self.key)
.await

View file

@ -61,7 +61,7 @@ pub struct KeysExportCommand {
#[async_trait::async_trait]
impl AspmSubcommand for KeysExportCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
// Fetch key from db
let entry = Keys::query_key(&state.db, &self.key)
.await

View file

@ -34,7 +34,7 @@ pub struct KeysGenerateCommand {
#[async_trait::async_trait]
impl AspmSubcommand for KeysGenerateCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
if self.key_type == KeyGenerationType::Ed25519 {
let confirmation = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("You are creating an Ed25519 key. Before confirming, please make sure you are aware that this may not be supported in browser environments, such as being viewed on https://keyoxide.org. Are you sure you want to create an Ed25519 key?")

View file

@ -27,9 +27,10 @@ pub enum KeyImportFormat {
/// Imports an ASP from raw JWK format. This only will import JWKs that have supported curves.
#[derive(Parser, Debug)]
pub struct KeysImportCommand {
/// The format of key to import
/// The format of key to import.
///
format: KeyImportFormat,
/// The key to import, as a file or "-" for stdin. This must be a valid JWK
/// The key to import, as a string.
key: String,
/// The alias of the key to import. This can be anything, and it can also be omitted to prompt interactively. This has no purpose other than providing a way to nicely name keys, rather than having to remember a fingerprint.
#[arg(short = 'n', long)]
@ -38,7 +39,7 @@ pub struct KeysImportCommand {
#[async_trait::async_trait]
impl AspmSubcommand for KeysImportCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let alias = if let Some(alias) = &self.key_alias {
alias.clone()
} else {

View file

@ -15,7 +15,7 @@ pub struct KeysListCommand;
#[async_trait::async_trait]
impl AspmSubcommand for KeysListCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let entries = Keys::find()
.all(&state.db)
.await

View file

@ -9,11 +9,11 @@ use crate::entities::keys::{Column as KeysColumn, Entity as KeysEntity, Model as
use crate::AspmState;
#[async_trait::async_trait]
pub trait AspmSubcommand: Parser + Sync {
async fn execute(&self, _state: AspmState) -> Result<(), anyhow::Error> {
pub trait AspmSubcommand: Parser + Sync + Send {
async fn execute(self, _state: AspmState) -> Result<(), anyhow::Error> {
panic!("Not implemented")
}
fn execute_sync(&self, state: AspmState, runtime: Runtime) -> Result<(), anyhow::Error> {
fn execute_sync(self, state: AspmState, runtime: Runtime) -> Result<(), anyhow::Error> {
runtime.block_on(self.execute(state))
}
}

View file

@ -1,7 +1,10 @@
use std::str::FromStr;
use anyhow::Context;
use asp::{keys::AspKeyType, profiles::*, utils::jwt::AspJwsType};
use asp::{
hex_color::HexColor, keys::AspKeyType, profiles::*, serde_email::Email, url::Url,
utils::jwt::AspJwsType,
};
use clap::Parser;
use dialoguer::{theme::ColorfulTheme, Input, Select};
use sea_orm::{ActiveValue, EntityTrait};
@ -27,7 +30,7 @@ pub struct ProfilesCreateCommand {
#[async_trait::async_trait]
impl AspmSubcommand for ProfilesCreateCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let theme = ColorfulTheme::default();
let alias = if let Some(alias) = &self.profile_alias {

View file

@ -21,7 +21,7 @@ pub struct ProfilesDeleteCommand {
#[async_trait::async_trait]
impl AspmSubcommand for ProfilesDeleteCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let profile = match &self.profile {
Some(key) => Profiles::find_by_id(key)
.one(&state.db)
@ -101,7 +101,8 @@ impl AspmSubcommand for ProfilesDeleteCommand {
}
}
profile.delete(&state.db)
profile
.delete(&state.db)
.await
.context("Unable to delete profile and claims from database")?;

View file

@ -1,6 +1,6 @@
use anstyle::{AnsiColor, Reset, Style as Anstyle};
use anyhow::{bail, Context};
use asp::profiles::{Email, HexColor, Url};
use asp::{hex_color::HexColor, serde_email::Email, url::Url};
use clap::Parser;
use dialoguer::{theme::ColorfulTheme, Input, Select};
use indoc::writedoc;
@ -22,7 +22,7 @@ pub struct ProfilesEditCommand {
#[async_trait::async_trait]
impl AspmSubcommand for ProfilesEditCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let (profile, claims) = match &self.profile {
Some(fingerprint) => {
let mut profiles = Profiles::find_by_id(fingerprint)

View file

@ -2,8 +2,11 @@ use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit as _};
use anyhow::{anyhow, bail, Context};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher as _};
use asp::{
hex_color::HexColor,
keys::AspKey,
profiles::{AriadneSignatureProfile, Email, HexColor, Url},
profiles::AriadneSignatureProfile,
serde_email::Email,
url::Url,
utils::jwt::{AspJwsType, JwtSerialize},
};
use clap::Parser;
@ -23,7 +26,7 @@ pub struct ProfilesExportCommand {
#[async_trait::async_trait]
impl AspmSubcommand for ProfilesExportCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let Some(profile) = Profiles::find_by_id(&self.fingerprint)
.one(&state.db)
.await

View file

@ -0,0 +1,106 @@
use anyhow::Context;
use asp::{
aspe::{self, AspeFetchFailure},
profiles::AriadneSignatureProfile,
url::{Host, Url},
utils::jwt::JwtSerialize,
};
use clap::Parser;
use sea_orm::{EntityTrait, IntoActiveValue, SqlErr};
use crate::{
commands::AspmSubcommand,
entities::{claims, prelude::*, profiles},
};
/// Imports a profile either directly or from an ASPE server. To import from an aspe server, pass a valid ASPE URI. The relevant key must be imported seperately beforehand.
#[derive(Parser, Debug)]
pub struct ProfilesImportCommand {
/// Either the encoded profile, or an ASPE URI to fetch from
profile: String,
/// The alias to give to the imported profile
#[clap(trailing_var_arg = true)]
alias: Vec<String>,
}
#[async_trait::async_trait]
impl AspmSubcommand for ProfilesImportCommand {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
let (profile, fingerprint) = match Url::parse(&self.profile).ok().and_then(|url| {
Some({
let (host, fingerprint) = url.path().split_once(':')?;
(host.to_string(), fingerprint.to_string())
})
}) {
Some((host, fingerprint)) => match aspe::AspeServer::new(Host::parse(&host).unwrap())?
.fetch_profile(&fingerprint)
.await
{
Ok(profile) => (profile, Some(fingerprint)),
Err(AspeFetchFailure::NotFound) => {
eprintln!(
"A profile could not be found with the specified host and fingerprint"
);
std::process::exit(1);
}
Err(AspeFetchFailure::RateLimited) => {
eprintln!("The requested profile could not be fetched due to ratelimiting, try again later");
std::process::exit(1);
}
Err(AspeFetchFailure::TooLarge) => {
eprintln!("The server rejected the request as it was deemed too large");
std::process::exit(1);
}
Err(AspeFetchFailure::Unknown(e)) => return Err(e.into()),
},
_ => (self.profile, None),
};
let (key, profile) = AriadneSignatureProfile::decode_and_verify(&profile, fingerprint)
.context("The provided or fetched profile was invalid and unable to be imported")?;
match Profiles::insert(profiles::ActiveModel {
alias: self.alias.join(" ").into_active_value(),
key: key.fingerprint.clone().into_active_value(),
name: profile.name.into_active_value(),
description: profile.description.into_active_value(),
email: profile
.email
.map(|email| email.to_string())
.into_active_value(),
avatar_url: profile
.avatar_url
.map(|avatar_url| avatar_url.to_string())
.into_active_value(),
color: profile
.color
.map(|color| color.to_string())
.into_active_value(),
})
.exec(&state.db)
.await
{
Ok(_) => {
Claims::insert_many(profile.claims.into_iter().map(|claim| claims::ActiveModel {
profile: key.fingerprint.clone().into_active_value(),
uri: claim.into_active_value(),
..Default::default()
}))
.exec(&state.db)
.await
.context("Unable to insert claims into database")?;
println!(
"Successfully imported profile with fingerprint {}",
key.fingerprint
);
return Ok(());
}
Err(e) if matches!(e.sql_err(), Some(SqlErr::ForeignKeyConstraintViolation(_))) => {
eprintln!("Unable to import profile as the key has not first been imported. Import the secret key and then try again.");
std::process::exit(1);
}
Err(e) => return Err(e.into()),
};
}
}

View file

@ -14,7 +14,7 @@ pub struct ProfilesListCommand {}
#[async_trait::async_trait]
impl AspmSubcommand for ProfilesListCommand {
async fn execute(&self, state: crate::AspmState) -> Result<(), anyhow::Error> {
async fn execute(self, state: crate::AspmState) -> Result<(), anyhow::Error> {
// Fetch data
let profiles = Profiles::find()
.find_with_related(Claims)

View file

@ -1,10 +1,11 @@
use clap::{Parser, Subcommand};
pub mod create;
pub mod delete;
pub mod edit;
pub mod export;
pub mod import;
pub mod list;
pub mod delete;
/// A subcommand to allow the management of keys, which can then be used to create, modify, or delete profiles.
#[derive(Parser)]
@ -20,4 +21,5 @@ pub enum ProfilesSubcommands {
List(list::ProfilesListCommand),
Edit(edit::ProfilesEditCommand),
Delete(delete::ProfilesDeleteCommand),
Import(import::ProfilesImportCommand),
}

View file

@ -23,6 +23,7 @@ const DATABASE_URL: &str = "sqlite://DB_PATH?mode=rwc";
pub struct AspmState {
pub data_dir: PathBuf,
pub db: DatabaseConnection,
pub verbose: bool,
}
#[derive(Parser)]
@ -142,26 +143,27 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> {
.context("Unable to check database for keys table")?);
// Make the state
let state = AspmState { data_dir, db };
let state = AspmState { data_dir, db, verbose: parsed.verbose };
Ok::<AspmState, anyhow::Error>(state)
})?;
// Call the subcommand
match &parsed.subcommand {
AspmSubcommands::Keys(subcommand) => match &subcommand.subcommand {
match parsed.subcommand {
AspmSubcommands::Keys(subcommand) => match subcommand.subcommand {
KeysSubcommands::Generate(subcommand) => subcommand.execute_sync(state, runtime),
KeysSubcommands::List(subcommand) => subcommand.execute_sync(state, runtime),
KeysSubcommands::Export(subcommand) => subcommand.execute_sync(state, runtime),
KeysSubcommands::Delete(subcommand) => subcommand.execute_sync(state, runtime),
KeysSubcommands::Import(subcommand) => subcommand.execute_sync(state, runtime),
},
AspmSubcommands::Profiles(subcommand) => match &subcommand.subcommand {
AspmSubcommands::Profiles(subcommand) => match subcommand.subcommand {
ProfilesSubcommands::Create(subcommand) => subcommand.execute_sync(state, runtime),
ProfilesSubcommands::Export(subcommand) => subcommand.execute_sync(state, runtime),
ProfilesSubcommands::List(subcommand) => subcommand.execute_sync(state, runtime),
ProfilesSubcommands::Edit(subcommand) => subcommand.execute_sync(state, runtime),
ProfilesSubcommands::Delete(subcommand) => subcommand.execute_sync(state, runtime),
ProfilesSubcommands::Import(subcommand) => subcommand.execute_sync(state, runtime),
},
}
}