Skip to main content

reqsign_oracle/
sign_request.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// RequestSigner that implements Oracle Cloud Infrastructure API signing.
34///
35/// - [Oracle Cloud Infrastructure API Signing](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm)
36#[derive(Debug)]
37pub struct RequestSigner {}
38
39impl RequestSigner {
40    /// Create a new builder for Oracle signer.
41    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        // Construct string to sign
89        let string_to_sign = build_string_to_sign(&signing_req, &request_target, now)?;
90
91        debug!("string to sign: {}", string_to_sign);
92
93        // Read private key from file
94        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        // Sign the string
100        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        // Set headers
107        signing_req
108            .headers
109            .insert(DATE, now.format_http_date().parse()?);
110
111        // Build authorization header
112        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&amp=%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}