objectstore_types/
presign.rs1use base64::Engine as _;
37use base64::engine::general_purpose::URL_SAFE_NO_PAD;
38use ed25519_dalek::{Signature, Signer};
39use http::Method;
40
41pub use ed25519_dalek::{SigningKey as DalekSigningKey, VerifyingKey as DalekVerifyingKey};
42
43pub const PARAM_TIMESTAMP: &str = "os_timestamp";
45
46pub const PARAM_DURATION: &str = "os_duration";
48
49pub const PARAM_KID: &str = "os_kid";
51
52pub const PARAM_SIG: &str = "os_sig";
54
55#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
57pub enum Error {
58 #[error("invalid key")]
60 InvalidKey,
61 #[error("invalid signature encoding")]
64 InvalidSignatureEncoding,
65 #[error("signature verification failed")]
67 VerificationFailed,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct CanonicalRequest(String);
73
74pub type SigningKey = [u8; 32];
76
77pub type VerifyingKey = [u8; 32];
79
80impl CanonicalRequest {
81 pub fn new(method: &Method, path: &str, query: Option<&str>) -> Self {
93 let normalized_method = if *method == Method::HEAD {
94 String::from("GET")
95 } else {
96 method.as_str().to_uppercase()
97 };
98
99 let mut pairs: Vec<String> = query
100 .unwrap_or_default()
101 .split('&')
102 .filter(|pair| !pair.is_empty())
103 .filter_map(|pair| {
104 if let Some((key, value)) = pair.split_once('=') {
105 let key = key.to_ascii_lowercase();
106 (key != PARAM_SIG).then(|| format!("{key}={value}"))
107 } else {
108 let key = pair.to_ascii_lowercase();
109 (key != PARAM_SIG).then_some(key)
110 }
111 })
112 .collect();
113 pairs.sort_unstable();
114 let canonical_query = pairs.join("&");
115
116 Self(format!("{normalized_method}\n{path}\n{canonical_query}"))
117 }
118
119 pub fn sign(&self, key: &SigningKey) -> String {
124 let key = DalekSigningKey::from_bytes(key);
125 let signature = key.sign(self.0.as_bytes());
126 URL_SAFE_NO_PAD.encode(signature.to_bytes())
127 }
128
129 pub fn verify(&self, key: &VerifyingKey, signature_b64: &str) -> Result<(), Error> {
138 let key = DalekVerifyingKey::from_bytes(key).map_err(|_| Error::InvalidKey)?;
139 let bytes = URL_SAFE_NO_PAD
140 .decode(signature_b64)
141 .map_err(|_| Error::InvalidSignatureEncoding)?;
142 let signature =
143 Signature::from_slice(&bytes).map_err(|_| Error::InvalidSignatureEncoding)?;
144 key.verify_strict(self.0.as_bytes(), &signature)
145 .map_err(|_| Error::VerificationFailed)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn test_signing_key() -> DalekSigningKey {
154 DalekSigningKey::from_bytes(&[0x42; 32])
155 }
156
157 fn sample_query() -> &'static str {
160 "os_timestamp=1985-04-12T23:20:50.52Z\
161 &os_kid=relay\
162 &os_duration=3600\
163 &os_sig=should-be-excluded"
164 }
165
166 #[test]
167 fn canonical_form_is_stable() {
168 let canonical = CanonicalRequest::new(
171 &Method::GET,
172 "/v1/objects/testing/org=17;project=42/foo/bar",
173 Some(sample_query()),
174 );
175 assert_eq!(
176 canonical.0,
177 "GET\n\
178 /v1/objects/testing/org=17;project=42/foo/bar\n\
179 os_duration=3600&\
180 os_kid=relay&\
181 os_timestamp=1985-04-12T23:20:50.52Z"
182 );
183
184 let canonical = CanonicalRequest::new(&Method::GET, "/v1/objects/testing/_/key", None);
186 assert_eq!(canonical.0, "GET\n/v1/objects/testing/_/key\n");
187
188 let canonical = CanonicalRequest::new(
191 &Method::GET,
192 "/v1/objects/testing/_/key",
193 Some("dup=b&dup=a&x=1"),
194 );
195 assert_eq!(
196 canonical.0,
197 "GET\n/v1/objects/testing/_/key\ndup=a&dup=b&x=1"
198 );
199
200 let canonical = CanonicalRequest::new(
201 &Method::GET,
202 "/v1/objects/testing/_/key",
203 Some(sample_query()),
204 );
205 assert_eq!(
206 canonical.0,
207 "GET\n\
208 /v1/objects/testing/_/key\n\
209 os_duration=3600&\
210 os_kid=relay&\
211 os_timestamp=1985-04-12T23:20:50.52Z"
212 );
213 }
214
215 #[test]
216 fn head_is_normalized_to_get() {
217 let path = "/v1/objects/testing/_/key";
218 assert_eq!(
219 CanonicalRequest::new(&Method::HEAD, path, Some(sample_query()),),
220 CanonicalRequest::new(&Method::GET, path, Some(sample_query()),),
221 );
222 }
223
224 #[test]
225 fn sign_and_verify_roundtrip() {
226 let sk = test_signing_key();
227 let vk = sk.verifying_key();
228
229 let canonical = CanonicalRequest::new(
230 &Method::GET,
231 "/v1/objects/testing/_/key",
232 Some(sample_query()),
233 );
234 let signature = canonical.sign(sk.as_bytes());
235
236 assert_eq!(canonical.verify(vk.as_bytes(), &signature), Ok(()));
237 }
238
239 #[test]
240 fn verify_rejects_tampered_canonical() {
241 let sk = test_signing_key();
242 let vk = sk.verifying_key();
243
244 let canonical = CanonicalRequest::new(
245 &Method::GET,
246 "/v1/objects/testing/_/key",
247 Some(sample_query()),
248 );
249 let signature = canonical.sign(sk.as_bytes());
250
251 let tampered = CanonicalRequest::new(
252 &Method::GET,
253 "/v1/objects/testing/_/other",
254 Some(sample_query()),
255 );
256 assert_eq!(
257 tampered.verify(vk.as_bytes(), &signature),
258 Err(Error::VerificationFailed)
259 );
260 }
261
262 #[test]
263 fn verify_rejects_wrong_key() {
264 let sk = test_signing_key();
265 let other_vk = DalekSigningKey::from_bytes(&[0x01; 32]).verifying_key();
266
267 let canonical = CanonicalRequest::new(
268 &Method::GET,
269 "/v1/objects/testing/_/key",
270 Some(sample_query()),
271 );
272 let signature = canonical.sign(sk.as_bytes());
273
274 assert_eq!(
275 canonical.verify(other_vk.as_bytes(), &signature),
276 Err(Error::VerificationFailed)
277 );
278 }
279
280 #[test]
281 fn verify_rejects_bad_signature_encoding() {
282 let vk = test_signing_key().verifying_key();
283 let canonical = CanonicalRequest::new(
284 &Method::GET,
285 "/v1/objects/testing/_/key",
286 Some(sample_query()),
287 );
288
289 assert_eq!(
291 canonical.verify(vk.as_bytes(), "not valid base64!!"),
292 Err(Error::InvalidSignatureEncoding)
293 );
294 assert_eq!(
296 canonical.verify(vk.as_bytes(), &URL_SAFE_NO_PAD.encode([0u8; 10])),
297 Err(Error::InvalidSignatureEncoding)
298 );
299 }
300}