Skip to main content

open_payments/http_signature/
validation.rs

1use crate::http_signature::error::{HttpSignatureError, Result};
2use base64::{engine::general_purpose::STANDARD, Engine};
3use ed25519_dalek::{Signature, Verifier, VerifyingKey};
4use http::{HeaderMap, Request};
5
6pub struct ValidationOptions<'a> {
7    pub request: &'a Request<Option<String>>,
8    pub headers: &'a HeaderMap,
9    pub public_key: &'a VerifyingKey,
10}
11
12impl<'a> ValidationOptions<'a> {
13    pub fn new(
14        request: &'a Request<Option<String>>,
15        headers: &'a HeaderMap,
16        public_key: &'a VerifyingKey,
17    ) -> Self {
18        Self {
19            request,
20            headers,
21            public_key,
22        }
23    }
24}
25
26fn create_signature_base_string(
27    request: &Request<Option<String>>,
28    components: &[&str],
29    created: i64,
30    keyid: &str,
31) -> String {
32    let mut parts = Vec::new();
33
34    for component in components {
35        let value = match *component {
36            "@method" => request.method().as_str(),
37            "@target-uri" => &request.uri().to_string(),
38            "authorization" => request
39                .headers()
40                .get("Authorization")
41                .and_then(|v| v.to_str().ok())
42                .unwrap_or(""),
43            "content-digest" => request
44                .headers()
45                .get("Content-Digest")
46                .and_then(|v| v.to_str().ok())
47                .unwrap_or(""),
48            "content-length" => request
49                .headers()
50                .get("Content-Length")
51                .and_then(|v| v.to_str().ok())
52                .unwrap_or(""),
53            "content-type" => request
54                .headers()
55                .get("Content-Type")
56                .and_then(|v| v.to_str().ok())
57                .unwrap_or(""),
58            _ => "",
59        };
60        parts.push(format!("\"{}\": {}", component, value));
61    }
62
63    let sig_params = format!(
64        "({});created={};keyid=\"{}\"",
65        components.join(" "),
66        created,
67        keyid
68    );
69    parts.push(format!("\"@signature-params\": {}", sig_params));
70
71    parts.join("\n")
72}
73
74#[allow(clippy::manual_strip)]
75fn parse_signature_input(signature_input: &str) -> Result<(Vec<&str>, i64, String)> {
76    let mut components = Vec::new();
77    let mut created = None;
78    let mut keyid = None;
79
80    // Remove the sig1= prefix if present
81    let signature_input = if signature_input.starts_with("sig1=") {
82        &signature_input[5..]
83    } else {
84        signature_input
85    };
86
87    for part in signature_input.split(';') {
88        if part.starts_with('(') {
89            components = part[1..part.len() - 1]
90                .split(' ')
91                .map(|s| s.trim())
92                .collect();
93        } else if part.starts_with("created=") {
94            created = Some(part[8..].parse::<i64>().unwrap_or(0));
95        } else if part.starts_with("keyid=") {
96            keyid = Some(part[7..].trim_matches('"').to_string());
97        }
98    }
99
100    let created = created
101        .ok_or_else(|| HttpSignatureError::Validation("Missing created field".to_string()))?;
102    let keyid =
103        keyid.ok_or_else(|| HttpSignatureError::Validation("Missing keyid field".to_string()))?;
104
105    Ok((components, created, keyid))
106}
107
108pub fn validate_signature(options: ValidationOptions<'_>) -> Result<()> {
109    let signature_input = options
110        .headers
111        .get("Signature-Input")
112        .and_then(|v| v.to_str().ok())
113        .ok_or_else(|| {
114            HttpSignatureError::Validation("Missing Signature-Input header".to_string())
115        })?;
116
117    let (components, created, keyid) = parse_signature_input(signature_input)?;
118
119    let signature = options
120        .headers
121        .get("Signature")
122        .and_then(|v| v.to_str().ok())
123        .ok_or_else(|| HttpSignatureError::Validation("Missing Signature header".to_string()))?;
124
125    let signature_base =
126        create_signature_base_string(options.request, &components, created, &keyid);
127
128    let signature_bytes = STANDARD
129        .decode(signature)
130        .map_err(|_| HttpSignatureError::Validation("Base64 decode failed".to_string()))?;
131
132    let signature_bytes: [u8; 64] = signature_bytes
133        .try_into()
134        .map_err(|_| HttpSignatureError::Validation("Invalid signature length".to_string()))?;
135    let signature = Signature::from_bytes(&signature_bytes);
136
137    options
138        .public_key
139        .verify(signature_base.as_bytes(), &signature)
140        .map_err(|_| HttpSignatureError::Validation("Signature verification failed".to_string()))?;
141
142    Ok(())
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::http_signature::{create_signature_headers, SignOptions};
149    use ed25519_dalek::{SigningKey, VerifyingKey};
150    use http::{HeaderMap, Method, Request, Uri};
151    use rand::rngs::OsRng;
152
153    #[test]
154    fn test_signature_validation() {
155        let mut request = Request::new(Some("test body".to_string()));
156        *request.method_mut() = Method::POST;
157        *request.uri_mut() = Uri::from_static("http://example.com");
158        request
159            .headers_mut()
160            .insert("Content-Type", "application/json".parse().unwrap());
161
162        let signing_key = SigningKey::generate(&mut OsRng);
163        let verifying_key = VerifyingKey::from(&signing_key);
164
165        let options = SignOptions::new(&request, &signing_key, "test-key".to_string());
166        let signature_headers = create_signature_headers(options).unwrap();
167
168        let mut headers = HeaderMap::new();
169        headers.insert("Signature", signature_headers.signature.parse().unwrap());
170        headers.insert(
171            "Signature-Input",
172            signature_headers.signature_input.parse().unwrap(),
173        );
174
175        let options = ValidationOptions::new(&request, &headers, &verifying_key);
176        assert!(validate_signature(options).is_ok());
177    }
178}