reqsign_oracle/
sign_request.rs1use crate::Credential;
19use base64::{Engine as _, engine::general_purpose};
20use http::header::{AUTHORIZATION, DATE};
21use http::request::Parts;
22use log::debug;
23use reqsign_core::Result;
24use reqsign_core::time::Timestamp;
25use reqsign_core::{Context, SignRequest, SigningCredential, SigningRequest};
26use rsa::pkcs1v15::SigningKey;
27use rsa::sha2::Sha256;
28use rsa::signature::{SignatureEncoding, Signer};
29use rsa::{RsaPrivateKey, pkcs8::DecodePrivateKey};
30use std::fmt::Write;
31use std::time::Duration;
32
33#[derive(Debug)]
37pub struct RequestSigner {}
38
39impl RequestSigner {
40 pub fn new() -> Self {
42 Self {}
43 }
44}
45
46impl Default for RequestSigner {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51impl SignRequest for RequestSigner {
52 type Credential = Credential;
53
54 fn required_valid_until(
55 &self,
56 _credential: &Self::Credential,
57 _expires_in: Option<Duration>,
58 ) -> Timestamp {
59 Timestamp::now()
60 }
61
62 async fn sign_request(
63 &self,
64 ctx: &Context,
65 req: &mut Parts,
66 credential: Option<&Self::Credential>,
67 _expires_in: Option<Duration>,
68 ) -> Result<()> {
69 let Some(cred) = credential else {
70 return Ok(());
71 };
72
73 let now = Timestamp::now();
74 if !cred.is_valid_at(now) {
75 return Err(reqsign_core::Error::credential_invalid(
76 "credential expires before the requested signing operation deadline",
77 ));
78 }
79
80 let request_target = req
81 .uri
82 .path_and_query()
83 .map(|value| value.as_str())
84 .unwrap_or("/")
85 .to_string();
86 let mut signing_req = SigningRequest::build(req)?;
87
88 let string_to_sign = build_string_to_sign(&signing_req, &request_target, now)?;
90
91 debug!("string to sign: {}", string_to_sign);
92
93 let private_key_content = ctx.file_read_as_string(&cred.key_file).await?;
95 let private_key = RsaPrivateKey::from_pkcs8_pem(&private_key_content).map_err(|e| {
96 reqsign_core::Error::credential_invalid(format!("Failed to read private key: {e}"))
97 })?;
98
99 let signing_key = SigningKey::<Sha256>::new(private_key);
101 let signature = signing_key
102 .try_sign(string_to_sign.as_bytes())
103 .map_err(|e| reqsign_core::Error::unexpected(format!("Failed to sign: {e}")))?;
104 let encoded_signature = general_purpose::STANDARD.encode(signature.to_bytes());
105
106 signing_req
108 .headers
109 .insert(DATE, now.format_http_date().parse()?);
110
111 let mut auth_value = String::new();
113 write!(auth_value, "Signature version=\"1\",")?;
114 write!(auth_value, "headers=\"date (request-target) host\",")?;
115 write!(
116 auth_value,
117 "keyId=\"{}/{}/{}\",",
118 cred.tenancy, cred.user, cred.fingerprint
119 )?;
120 write!(auth_value, "algorithm=\"rsa-sha256\",")?;
121 write!(auth_value, "signature=\"{encoded_signature}\"")?;
122
123 signing_req
124 .headers
125 .insert(AUTHORIZATION, auth_value.parse()?);
126
127 signing_req.apply(req)
128 }
129}
130
131fn build_string_to_sign(
132 request: &SigningRequest,
133 request_target: &str,
134 now: Timestamp,
135) -> Result<String> {
136 let mut value = String::new();
137 writeln!(value, "date: {}", now.format_http_date())?;
138 writeln!(
139 value,
140 "(request-target): {} {}",
141 request.method.as_str().to_lowercase(),
142 request_target
143 )?;
144 write!(value, "host: {}", request.authority)?;
145 Ok(value)
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use reqsign_core::FileRead;
152 use rsa::pkcs8::{EncodePrivateKey, LineEnding};
153 use rsa::rand_core::OsRng;
154
155 const RAW_QUERY: &str = "slash=%2F&hash=%23&=%26&equals=%3D&space=%20&encoded-plus=%2B&literal-plus=+&double=%252F&dup=first&dup=second&=empty-key&empty=&flag&flag=&";
156
157 #[derive(Debug)]
158 struct StaticFileRead(Vec<u8>);
159
160 impl FileRead for StaticFileRead {
161 async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
162 Ok(self.0.clone())
163 }
164 }
165
166 fn credential() -> Credential {
167 Credential {
168 tenancy: "tenancy".to_string(),
169 user: "user".to_string(),
170 key_file: "key.pem".to_string(),
171 fingerprint: "fingerprint".to_string(),
172 expires_in: None,
173 }
174 }
175
176 #[tokio::test]
177 async fn request_target_and_wire_uri_preserve_raw_query() -> Result<()> {
178 let original_uri = format!("https://example.com/object%2Fname?{RAW_QUERY}");
179 let mut canonical_parts = http::Request::get(&original_uri).body(())?.into_parts().0;
180 let signing_req = SigningRequest::build(&mut canonical_parts)?;
181 let now: Timestamp = "2026-07-22T00:00:00Z".parse()?;
182 let string_to_sign = build_string_to_sign(
183 &signing_req,
184 canonical_parts.uri.path_and_query().unwrap().as_str(),
185 now,
186 )?;
187 assert!(
188 string_to_sign.contains(&format!("(request-target): get /object%2Fname?{RAW_QUERY}"))
189 );
190
191 let private_key = RsaPrivateKey::new(&mut OsRng, 1024).expect("key generation must work");
192 let private_key = private_key
193 .to_pkcs8_pem(LineEnding::LF)
194 .expect("private key must encode")
195 .as_bytes()
196 .to_vec();
197 let ctx = Context::new().with_file_read(StaticFileRead(private_key));
198 let mut parts = http::Request::get(&original_uri).body(())?.into_parts().0;
199
200 RequestSigner::new()
201 .sign_request(&ctx, &mut parts, Some(&credential()), None)
202 .await?;
203
204 assert_eq!(parts.uri.to_string(), original_uri);
205 assert!(parts.headers.contains_key(AUTHORIZATION));
206 Ok(())
207 }
208
209 #[tokio::test]
210 async fn invalid_private_key_leaves_request_unchanged() -> Result<()> {
211 let original_uri = format!("https://example.com/object?{RAW_QUERY}");
212 let ctx = Context::new().with_file_read(StaticFileRead(b"invalid".to_vec()));
213 let mut parts = http::Request::get(&original_uri)
214 .header("x-original", "value")
215 .body(())?
216 .into_parts()
217 .0;
218 let original = parts.clone();
219
220 assert!(
221 RequestSigner::new()
222 .sign_request(&ctx, &mut parts, Some(&credential()), None)
223 .await
224 .is_err()
225 );
226 assert_eq!(parts.method, original.method);
227 assert_eq!(parts.uri, original.uri);
228 assert_eq!(parts.version, original.version);
229 assert_eq!(parts.headers, original.headers);
230 Ok(())
231 }
232}