|
| 1 | +//! Hosts a VSS protocol compliant [`Authorizer`] implementation that requires that every request |
| 2 | +//! come with a public key and proof of private key knowledge. Access is then granted to the user |
| 3 | +//! defined by the public key. |
| 4 | +//! |
| 5 | +//! There is no specific restriction of who is allowed to store data in VSS using this |
| 6 | +//! authentication scheme, only that each user is only allowed to store and access data for which |
| 7 | +//! they have a corresponding private key. Thus, you must ensure new user accounts are |
| 8 | +//! appropriately rate-limited or access to the VSS server is somehow limited. |
| 9 | +//! |
| 10 | +//! [`Authorizer`]: api::auth::Authorizer |
| 11 | +
|
| 12 | +use api::auth::{AuthResponse, Authorizer}; |
| 13 | +use api::error::VssError; |
| 14 | +use async_trait::async_trait; |
| 15 | +use bitcoin_hashes::HashEngine; |
| 16 | +use std::collections::HashMap; |
| 17 | +use std::time::SystemTime; |
| 18 | + |
| 19 | +/// A 64-byte constant which, after appending the public key, is signed in order to prove knowledge |
| 20 | +/// of the corresponding private key. |
| 21 | +pub const SIGNING_CONSTANT: &'static [u8] = |
| 22 | + b"VSS Signature Authorizer Signing Salt Constant.................."; |
| 23 | + |
| 24 | +/// An authorizer that requires that every request come with a public key and proof of private key |
| 25 | +/// knowledge. Access is then granted to the user defined by the public key. |
| 26 | +/// |
| 27 | +/// The proof of private key knowledge takes the form of an ECDSA signature over the |
| 28 | +/// [`SIGNING_CONSTANT`] followed by the public key followed by the current time since the UNIX |
| 29 | +/// epoch, encoded as a string. It is expected to appear in the `Authorization` header, in the form |
| 30 | +/// of the hex-encoded 33-byte secp256k1 public key in compressed form followed by the hex-encoded |
| 31 | +/// 64-byte secp256k1 ECDSA signature followed by the signing time since the UNIX epoch, encoded as |
| 32 | +/// a string. |
| 33 | +/// |
| 34 | +/// The proof will not be valid if the provided time is more than an hour from now. |
| 35 | +/// |
| 36 | +/// Because no rate-limiting of new user accounts is done, a higher-level service is required to |
| 37 | +/// ensure requests are not triggering excess new user registrations. |
| 38 | +pub struct SignatureValidatingAuthorizer; |
| 39 | + |
| 40 | +#[async_trait] |
| 41 | +impl Authorizer for SignatureValidatingAuthorizer { |
| 42 | + async fn verify( |
| 43 | + &self, headers_map: &HashMap<String, String>, |
| 44 | + ) -> Result<AuthResponse, VssError> { |
| 45 | + let auth_header = headers_map |
| 46 | + .get("Authorization") |
| 47 | + .ok_or_else(|| VssError::AuthError("Authorization header not found.".to_string()))?; |
| 48 | + |
| 49 | + if auth_header.len() <= (33 + 64) * 2 { |
| 50 | + return Err(VssError::AuthError("Authorization header has wrong length".to_string())); |
| 51 | + } |
| 52 | + if !auth_header.is_ascii() { |
| 53 | + return Err(VssError::AuthError("Authorization header has bogus chars".to_string())); |
| 54 | + } |
| 55 | + |
| 56 | + let pubkey_hex = &auth_header[..33 * 2]; |
| 57 | + let signat_hex = &auth_header[33 * 2..(33 + 64) * 2]; |
| 58 | + let time_strng = &auth_header[(33 + 64) * 2..]; |
| 59 | + |
| 60 | + let pubkey_bytes: [u8; 33] = hex_conservative::decode_to_array(pubkey_hex) |
| 61 | + .map_err(|_| VssError::AuthError("Authorization header is not hex".to_string()))?; |
| 62 | + let sig_bytes: [u8; 64] = hex_conservative::decode_to_array(signat_hex) |
| 63 | + .map_err(|_| VssError::AuthError("Authorization header is not hex".to_string()))?; |
| 64 | + let time: u64 = time_strng |
| 65 | + .parse() |
| 66 | + .map_err(|_| VssError::AuthError("Time is not an integer".to_string()))?; |
| 67 | + |
| 68 | + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); |
| 69 | + if now.as_secs() - 60 * 60 * 24 > time || now.as_secs() + 60 * 60 * 24 < time { |
| 70 | + return Err(VssError::AuthError("Time is too far from now".to_string()))?; |
| 71 | + } |
| 72 | + |
| 73 | + let pubkey = secp256k1::PublicKey::from_byte_array_compressed(pubkey_bytes) |
| 74 | + .map_err(|_| VssError::AuthError("Authorization header has bad pubkey".to_string()))?; |
| 75 | + let sig = secp256k1::ecdsa::Signature::from_compact(&sig_bytes) |
| 76 | + .map_err(|_| VssError::AuthError("Authorization header has bad sig".to_string()))?; |
| 77 | + |
| 78 | + let mut hash = bitcoin_hashes::Sha256::engine(); |
| 79 | + hash.input(&SIGNING_CONSTANT); |
| 80 | + hash.input(&pubkey_bytes); |
| 81 | + hash.input(time_strng.as_bytes()); |
| 82 | + let signed_hash = secp256k1::Message::from_digest(hash.finalize().to_byte_array()); |
| 83 | + sig.verify(signed_hash, &pubkey) |
| 84 | + .map_err(|_| VssError::AuthError("Signature was invalid".to_string()))?; |
| 85 | + |
| 86 | + Ok(AuthResponse { user_token: pubkey_hex.to_owned() }) |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | +#[cfg(test)] |
| 91 | +mod tests { |
| 92 | + use crate::signature::{SignatureValidatingAuthorizer, SIGNING_CONSTANT}; |
| 93 | + use api::auth::Authorizer; |
| 94 | + use api::error::VssError; |
| 95 | + use secp256k1::{Message, PublicKey, Secp256k1, SecretKey}; |
| 96 | + use std::collections::HashMap; |
| 97 | + use std::fmt::Write; |
| 98 | + use std::time::SystemTime; |
| 99 | + |
| 100 | + fn build_token(now: u64) -> (String, PublicKey) { |
| 101 | + let secret_key = SecretKey::from_byte_array([42; 32]).unwrap(); |
| 102 | + let pubkey = secret_key.public_key(secp256k1::SECP256K1); |
| 103 | + |
| 104 | + let mut bytes_to_sign = Vec::new(); |
| 105 | + bytes_to_sign.extend_from_slice(SIGNING_CONSTANT); |
| 106 | + bytes_to_sign.extend_from_slice(&pubkey.serialize()); |
| 107 | + bytes_to_sign.extend_from_slice(format!("{now}").as_bytes()); |
| 108 | + let hash = bitcoin_hashes::Sha256::hash(&bytes_to_sign); |
| 109 | + let sig = secret_key.sign_ecdsa(Message::from_digest(hash.to_byte_array())); |
| 110 | + let mut sig_hex = String::with_capacity(64 * 2); |
| 111 | + for c in sig.serialize_compact() { |
| 112 | + write!(&mut sig_hex, "{:02x}", c).unwrap(); |
| 113 | + } |
| 114 | + (format!("{pubkey:x}{sig_hex}{now}"), pubkey) |
| 115 | + } |
| 116 | + |
| 117 | + #[tokio::test] |
| 118 | + async fn test_sig() { |
| 119 | + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); |
| 120 | + let mut headers_map = HashMap::new(); |
| 121 | + let auth = SignatureValidatingAuthorizer; |
| 122 | + |
| 123 | + // Test a valid signature |
| 124 | + let (token, pubkey) = build_token(now); |
| 125 | + headers_map.insert("Authorization".to_string(), token); |
| 126 | + assert_eq!(auth.verify(&headers_map).await.unwrap().user_token, format!("{pubkey:x}")); |
| 127 | + |
| 128 | + // Test a signature too far in the future |
| 129 | + let (token, _) = build_token(now + 60 * 60 * 24 + 10); |
| 130 | + headers_map.insert("Authorization".to_string(), token); |
| 131 | + assert!(matches!(auth.verify(&headers_map).await.unwrap_err(), VssError::AuthError(_))); |
| 132 | + |
| 133 | + // Test a signature too far in the past |
| 134 | + let (token, _) = build_token(now - 60 * 60 * 24 - 10); |
| 135 | + headers_map.insert("Authorization".to_string(), token); |
| 136 | + assert!(matches!(auth.verify(&headers_map).await.unwrap_err(), VssError::AuthError(_))); |
| 137 | + |
| 138 | + // Test a token with an invalid signature |
| 139 | + let (mut token, _) = build_token(now); |
| 140 | + token = token |
| 141 | + .chars() |
| 142 | + .enumerate() |
| 143 | + .map(|(idx, c)| if idx == 33 * 2 + 10 || idx == 33 * 2 + 11 { '0' } else { c }) |
| 144 | + .collect(); |
| 145 | + headers_map.insert("Authorization".to_string(), token); |
| 146 | + assert!(matches!(auth.verify(&headers_map).await.unwrap_err(), VssError::AuthError(_))); |
| 147 | + |
| 148 | + // Test a token with the wrong public key |
| 149 | + let (mut token, _) = build_token(now); |
| 150 | + token = token |
| 151 | + .chars() |
| 152 | + .enumerate() |
| 153 | + .map(|(idx, c)| if idx == 10 || idx == 11 { '0' } else { c }) |
| 154 | + .collect(); |
| 155 | + headers_map.insert("Authorization".to_string(), token); |
| 156 | + assert!(matches!(auth.verify(&headers_map).await.unwrap_err(), VssError::AuthError(_))); |
| 157 | + } |
| 158 | +} |
0 commit comments