diff --git a/crates/asp/src/aspe/mod.rs b/crates/asp/src/aspe/mod.rs index 53362df..350f77f 100644 --- a/crates/asp/src/aspe/mod.rs +++ b/crates/asp/src/aspe/mod.rs @@ -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, + fingerprint: impl AsRef, ) -> Result { 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, diff --git a/crates/asp/src/aspe/requests.rs b/crates/asp/src/aspe/requests.rs index 3e04759..8503025 100644 --- a/crates/asp/src/aspe/requests.rs +++ b/crates/asp/src/aspe/requests.rs @@ -72,12 +72,14 @@ mod tests { let key = TryInto::::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::::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::::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, diff --git a/crates/asp/src/keys/mod.rs b/crates/asp/src/keys/mod.rs index 7e2d796..f788290 100644 --- a/crates/asp/src/keys/mod.rs +++ b/crates/asp/src/keys/mod.rs @@ -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 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, diff --git a/crates/asp/src/lib.rs b/crates/asp/src/lib.rs index 9accdc7..070a0b4 100644 --- a/crates/asp/src/lib.rs +++ b/crates/asp/src/lib.rs @@ -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"; } diff --git a/crates/asp/src/profiles/mod.rs b/crates/asp/src/profiles/mod.rs index 2e26524..95c4059 100644 --- a/crates/asp/src/profiles/mod.rs +++ b/crates/asp/src/profiles/mod.rs @@ -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" diff --git a/crates/asp/src/utils/jwt.rs b/crates/asp/src/utils/jwt.rs index dc871ce..a93de58 100644 --- a/crates/asp/src/utils/jwt.rs +++ b/crates/asp/src/utils/jwt.rs @@ -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; - fn decode_and_verify(jwt: &str, key: &AspKey) -> Result, JwtDeserializationError> + fn decode_and_verify(jwt: &str, expected_fingerprint: Option) -> Result<(AspKey, Box), JwtDeserializationError> where - Self: for<'de> Deserialize<'de> + JwtSerializable; + Self: for<'de> Deserialize<'de> + JwtSerializable, + S: AsRef; } #[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 Deserialize<'de>> JwtSerialize fo .or(Err(JwtSerializationError::SerializationError)) } - fn decode_and_verify(jwt: &str, key: &AspKey) -> Result, JwtDeserializationError> + fn decode_and_verify(jwt: &str, expected_fingerprint: Option) -> Result<(AspKey, Box), JwtDeserializationError> where Self: for<'de> serde::Deserialize<'de> + JwtSerializable, + S: AsRef { // 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 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 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))) } } diff --git a/src/commands/keys/delete.rs b/src/commands/keys/delete.rs index e7c08bf..b130a07 100644 --- a/src/commands/keys/delete.rs +++ b/src/commands/keys/delete.rs @@ -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 diff --git a/src/commands/keys/export.rs b/src/commands/keys/export.rs index ca5a563..0367dff 100644 --- a/src/commands/keys/export.rs +++ b/src/commands/keys/export.rs @@ -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 diff --git a/src/commands/keys/generate.rs b/src/commands/keys/generate.rs index 527f5ee..0d0a492 100644 --- a/src/commands/keys/generate.rs +++ b/src/commands/keys/generate.rs @@ -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?") diff --git a/src/commands/keys/import.rs b/src/commands/keys/import.rs index 93b6d50..40ae311 100644 --- a/src/commands/keys/import.rs +++ b/src/commands/keys/import.rs @@ -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 { diff --git a/src/commands/keys/list.rs b/src/commands/keys/list.rs index 1bb5da8..ef2e6e4 100644 --- a/src/commands/keys/list.rs +++ b/src/commands/keys/list.rs @@ -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 diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b58f85b..2990362 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -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)) } } diff --git a/src/commands/profiles/create.rs b/src/commands/profiles/create.rs index 37ce8b1..d6b6969 100644 --- a/src/commands/profiles/create.rs +++ b/src/commands/profiles/create.rs @@ -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 { diff --git a/src/commands/profiles/delete.rs b/src/commands/profiles/delete.rs index 9b31600..5b04813 100644 --- a/src/commands/profiles/delete.rs +++ b/src/commands/profiles/delete.rs @@ -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")?; diff --git a/src/commands/profiles/edit.rs b/src/commands/profiles/edit.rs index 29068d4..5bbafc9 100644 --- a/src/commands/profiles/edit.rs +++ b/src/commands/profiles/edit.rs @@ -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) diff --git a/src/commands/profiles/export.rs b/src/commands/profiles/export.rs index 3440a08..2b753e2 100644 --- a/src/commands/profiles/export.rs +++ b/src/commands/profiles/export.rs @@ -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 diff --git a/src/commands/profiles/import.rs b/src/commands/profiles/import.rs new file mode 100644 index 0000000..72ba7ec --- /dev/null +++ b/src/commands/profiles/import.rs @@ -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, +} + +#[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()), + }; + } +} diff --git a/src/commands/profiles/list.rs b/src/commands/profiles/list.rs index 35c14d7..ea8dbef 100644 --- a/src/commands/profiles/list.rs +++ b/src/commands/profiles/list.rs @@ -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) diff --git a/src/commands/profiles/mod.rs b/src/commands/profiles/mod.rs index 8cf8ca5..5ece472 100644 --- a/src/commands/profiles/mod.rs +++ b/src/commands/profiles/mod.rs @@ -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), } diff --git a/src/main.rs b/src/main.rs index 5922489..a13a8b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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::(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), }, } }