1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
//! Facilities for [signed URLs](https://cloud.google.com/storage/docs/access-control/signed-urls),

use crate::{error::Error, signing, types::ObjectIdentifier};
use percent_encoding as perc_enc;
use std::borrow::Cow;
use url::Url;

/// A generator for [signed URLs](https://cloud.google.com/storage/docs/access-control/signed-urls),
/// which can be used to grant temporary access to specific storage
/// resources even if the client making the request is not otherwise
/// logged in or normally able to access to the storage resources in question.
///
/// This implements the [V4 signing process](https://cloud.google.com/storage/docs/access-control/signing-urls-manually)
pub struct UrlSigner<D, S> {
    digester: D,
    signer: S,
}

#[cfg(feature = "signing")]
impl UrlSigner<signing::RingDigest, signing::RingSigner> {
    /// Creates a UrlSigner implemented via `ring`
    pub fn with_ring() -> UrlSigner<signing::RingDigest, signing::RingSigner> {
        UrlSigner::new(signing::RingDigest, signing::RingSigner)
    }
}

impl<D, S> UrlSigner<D, S>
where
    D: signing::DigestCalulator,
    S: signing::Signer,
{
    /// Creates a new UrlSigner from a `DigestCalculator` implementation
    /// capable of generating SHA256 digests of buffers, and a `Signer`
    /// capable of doing RSA-SHA256 encryption. You may implement these
    /// on your own using whatever crates you prefer, or you can use the
    /// `signing` feature which will use the excellent `ring` crate
    /// to provide implementations.
    pub fn new(digester: D, signer: S) -> Self {
        Self { digester, signer }
    }

    /// Generates a new signed url for the specified resource, using a key
    /// provider. Note that this operation is entirely local, so though this
    /// may succeed in generating a url, the actual operation using it may fail
    /// if the account used to sign the URL does not have sufficient permissions
    /// for the resource. For example, if you provided a GCP service account
    /// that had devstorage.read_only permissions for the bucket/object, this method
    /// would succeed in generating a signed url for a `POST` operation, but the actual
    /// `POST` using that url would fail as the account does not itself have permissions
    /// for the `POST` operation.
    pub fn generate<'a, K, OID>(
        &self,
        key_provider: &K,
        id: &OID,
        optional: SignedUrlOptional<'_>,
    ) -> Result<Url, Error>
    where
        K: signing::KeyProvider,
        OID: ObjectIdentifier<'a>,
    {
        // This is apparently the maximum expiration duration
        const SEVEN_DAYS: u64 = 7 * 24 * 60 * 60;
        if optional.duration.as_secs() > SEVEN_DAYS {
            return Err(Error::TooLongExpiration {
                requested: optional.duration.as_secs(),
                max: SEVEN_DAYS,
            });
        }

        // First, create the canonical request, as described here
        // https://cloud.google.com/storage/docs/authentication/canonical-requests
        //
        // HTTP_VERB
        // PATH_TO_RESOURCE
        // CANONICAL_QUERY_STRING
        // CANONICAL_HEADERS
        let mut signed_url =
            Url::parse("https://storage.googleapis.com").map_err(Error::UrlParse)?;

        // https://cloud.google.com/storage/docs/authentication/canonical-requests#about-resource-path
        let resource_path = format!(
            "/{}/{}",
            perc_enc::percent_encode(id.bucket().as_ref(), crate::util::PATH_ENCODE_SET),
            perc_enc::percent_encode(id.object().as_ref(), crate::util::PATH_ENCODE_SET),
        );

        signed_url.set_path(&resource_path);

        let mut headers = optional.headers;

        // `host` is always required
        headers.insert(
            http::header::HOST,
            http::header::HeaderValue::from_static("storage.googleapis.com"),
        );

        // Eliminate duplicate header names by creating one header name with a comma-separated list of values.
        // Be sure there is no whitespace between the values, and be sure that the order of the comma-separated
        // list matches the order that the headers appear in your request.
        let headers = {
            let mut hdrs = Vec::with_capacity(headers.keys_len());
            for key in headers.keys() {
                let vals_size = headers
                    .get_all(key)
                    .iter()
                    .fold(0, |acc, v| acc + v.len() + 1)
                    - 1;
                let mut key_vals = String::with_capacity(vals_size);
                for (i, val) in headers.get_all(key).iter().enumerate() {
                    if i > 0 {
                        key_vals.push(',');
                    }

                    key_vals.push_str(
                        val.to_str()
                            .map_err(|_| Error::OpaqueHeaderValue(val.clone()))?,
                    );
                }

                // Make all header names lowercase.
                hdrs.push((key.as_str().to_lowercase(), key_vals));
            }

            // Sort all headers by header name using a lexicographical sort by code point value.
            hdrs.sort();
            hdrs
        };

        let signed_headers = {
            let signed_size =
                headers.iter().fold(0, |acc, (name, _)| acc + name.len()) + headers.len() - 1;
            let mut names = String::with_capacity(signed_size);

            for (i, name) in headers.iter().map(|(name, _)| name).enumerate() {
                if i > 0 {
                    names.push(';');
                }

                names.push_str(name);
            }

            assert_eq!(signed_size, names.capacity());
            names
        };

        let timestamp = chrono::Utc::now();

        // YYYYMMDD'T'HHMMSS'Z'
        let request_timestamp = timestamp.format("%Y%m%dT%H%M%SZ").to_string();
        // YYYYMMDD
        let datestamp = &request_timestamp[..8];

        // https://cloud.google.com/storage/docs/access-control/signed-urls#credential-scope
        // [DATE]/[LOCATION]/storage/goog4_request
        let credential_scope = format!("{}/{}/storage/goog4_request", datestamp, optional.region);
        // service account email (or HMAC key)/scope
        let credential_param = format!("{}/{}", key_provider.authorizer(), credential_scope);

        let expiration = optional.duration.as_secs().to_string();

        let mut query_params = optional.query_params;

        query_params.extend(
            [
                ("X-Goog-Algorithm", "GOOG4-RSA-SHA256"),
                ("X-Goog-Credential", &credential_param),
                ("X-Goog-Date", &request_timestamp),
                ("X-Goog-Expires", &expiration),
                ("X-Goog-SignedHeaders", &signed_headers),
            ]
            .iter()
            .map(|(k, v)| (Cow::Borrowed(*k), Cow::Borrowed(*v))),
        );

        // The parameters in the query string must be sorted by name using a lexicographical sort by code point value.
        query_params.sort();

        // Fake it till you make it!
        let canonical_query = {
            {
                let mut query_pairs = signed_url.query_pairs_mut();
                query_pairs.clear(); // Shouldn't be anything here but trust nothing

                for (key, value) in &query_params {
                    query_pairs.append_pair(key, value);
                }
            }

            signed_url.query().unwrap().to_owned()
        };

        let canonical_headers = {
            let canonical_size = headers
                .iter()
                .fold(0, |acc, kv| acc + kv.0.len() + kv.1.len())
                + headers.len() * 2;
            let mut hdrs = String::with_capacity(canonical_size);

            for (k, v) in &headers {
                hdrs.push_str(k);
                hdrs.push(':');
                hdrs.push_str(v);
                hdrs.push('\n');
            }

            assert_eq!(canonical_size, hdrs.capacity());
            hdrs
        };

        // https://cloud.google.com/storage/docs/access-control/signing-urls-manually#algorithm
        // 1. Construct canonical request
        let canonical_request = format!(
            "{verb}\n{resource}\n{query}\n{headers}\n{signed_headers}\nUNSIGNED-PAYLOAD",
            verb = optional.method,
            resource = resource_path,
            query = canonical_query,
            headers = canonical_headers,
            signed_headers = signed_headers,
        );

        // 2. Use a SHA-256 hashing function to create a hex-encoded hash value of the canonical request.
        let mut digest = [0u8; 32];
        self.digester.digest(
            signing::DigestAlgorithm::Sha256,
            canonical_request.as_bytes(),
            &mut digest,
        );

        let mut digest_str_bytes = [0u8; 64];
        let digest_str = crate::util::to_hex(&digest, &mut digest_str_bytes)
            .expect("unable to construct digest string");

        // 3. Construct the string-to-sign.
        // SIGNING_ALGORITHM
        // CURRENT_DATETIME
        // CREDENTIAL_SCOPE
        // HASHED_CANONICAL_REQUEST
        let string_to_sign = format!(
            "GOOG4-RSA-SHA256\n{timestamp}\n{scope}\n{hash}",
            timestamp = request_timestamp,
            scope = credential_scope,
            hash = digest_str,
        );

        let signature = self.signer.sign(
            signing::SigningAlgorithm::RsaSha256,
            key_provider.key(),
            string_to_sign.as_bytes(),
        )?;

        let mut signature_str_bytes = vec![0; signature.len() * 2];
        let signature_str = crate::util::to_hex(&signature, &mut signature_str_bytes)
            .expect("unable to construct signature string");

        signed_url
            .query_pairs_mut()
            .append_pair("X-Goog-Signature", signature_str);

        // 4. Profit!
        Ok(signed_url)
    }
}

/// Optional parameters that can be used to tweak url signing
pub struct SignedUrlOptional<'a> {
    /// The HTTP method for the request to sign. Defaults to 'GET'.
    pub method: http::Method,
    /// The lifetime of the signed URL, as measured from the DateTime of the
    /// signed URL creation. Defaults to 1 hour.
    pub duration: std::time::Duration,
    /// Additional headers in the request
    pub headers: http::HeaderMap,
    /// The region where the resource for which the signed url is being
    /// created is for. Defaults to "auto".
    pub region: &'a str,
    /// Additional query paramters in the request
    pub query_params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
}

impl<'a> Default for SignedUrlOptional<'a> {
    fn default() -> Self {
        Self {
            method: http::Method::GET,
            duration: std::time::Duration::from_secs(60 * 60),
            headers: http::HeaderMap::default(),
            region: "auto",
            query_params: Vec::new(),
        }
    }
}