1use core::fmt;
4
5use cloud_sdk_sanitization::sanitize_bytes;
6
7use crate::buffer::{SnapshotEncoder, encode_snapshot_bounded, measure_snapshot_bounded};
8use crate::transport::{CanonicalHost, EndpointScheme, RequestHeader, TransportRequest};
9
10use super::ScopeValue;
11
12mod build;
13mod context;
14mod output;
15
16pub use build::SigningBuildError;
17use build::{DigestScratch, SigningBodyDigest};
18pub use context::{
19 MAX_SIGNING_ALGORITHM_BYTES, MAX_SIGNING_DIGEST_ALGORITHM_BYTES, MAX_SIGNING_KEY_ID_BYTES,
20 SigningAlgorithm, SigningContext, SigningContextValueError, SigningDigestAlgorithm,
21 SigningKeyId,
22};
23pub use output::{RequestSigner, SignedRequest, SigningOutputError};
24
25pub const MAX_SIGNING_BODY_DIGEST_BYTES: usize = 128;
27pub const MAX_SIGNING_NONCE_BYTES: usize = 256;
29pub const MAX_SIGNING_HEADERS: usize = 32;
31pub const MAX_CANONICAL_SIGNING_INPUT_BYTES: usize = 12_288;
33
34const SIGNING_DOMAIN: &[u8] = b"cloud-sdk-signing-v2\0";
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum SigningValueError {
39 Empty,
41 TooLong,
43}
44
45impl_static_error!(SigningValueError,
46 Self::Empty => "signing value is empty",
47 Self::TooLong => "signing value exceeds the length limit",
48);
49
50#[derive(Clone, Copy, Eq, PartialEq)]
52pub struct SigningNonce<'a>(&'a [u8]);
53
54impl<'a> SigningNonce<'a> {
55 pub fn new(value: &'a [u8]) -> Result<Self, SigningValueError> {
57 validate_bounded(value, MAX_SIGNING_NONCE_BYTES)?;
58 Ok(Self(value))
59 }
60
61 #[must_use]
63 pub const fn as_bytes(self) -> &'a [u8] {
64 self.0
65 }
66}
67
68impl fmt::Debug for SigningNonce<'_> {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 formatter.write_str("SigningNonce([redacted])")
71 }
72}
73
74#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
76pub struct UnixTime(u64);
77
78impl UnixTime {
79 #[must_use]
81 pub const fn from_seconds(seconds: u64) -> Self {
82 Self(seconds)
83 }
84
85 #[must_use]
87 pub const fn as_seconds(self) -> u64 {
88 self.0
89 }
90}
91
92#[derive(Clone, Copy)]
94pub struct SigningFreshness<'a> {
95 nonce: SigningNonce<'a>,
96 time: UnixTime,
97}
98
99impl<'a> SigningFreshness<'a> {
100 #[must_use]
102 pub const fn new(nonce: SigningNonce<'a>, time: UnixTime) -> Self {
103 Self { nonce, time }
104 }
105
106 const fn nonce(self) -> SigningNonce<'a> {
107 self.nonce
108 }
109
110 const fn time(self) -> UnixTime {
111 self.time
112 }
113}
114
115impl fmt::Debug for SigningFreshness<'_> {
116 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
117 formatter
118 .debug_struct("SigningFreshness")
119 .field("nonce", &"[redacted]")
120 .field("time", &self.time)
121 .finish()
122 }
123}
124
125#[derive(Clone, Copy)]
127pub struct SigningHeaders<'a> {
128 entries: &'a [RequestHeader<'a>],
129}
130
131impl<'a> SigningHeaders<'a> {
132 pub fn new(entries: &'a [RequestHeader<'a>]) -> Result<Self, SigningInputError> {
134 if entries.len() > MAX_SIGNING_HEADERS {
135 return Err(SigningInputError::TooManyHeaders);
136 }
137 for pair in entries.windows(2) {
138 let Some(left) = pair.first() else {
139 return Err(SigningInputError::HeaderOrder);
140 };
141 let Some(right) = pair.get(1) else {
142 return Err(SigningInputError::HeaderOrder);
143 };
144 if left.name() >= right.name() {
145 return Err(SigningInputError::HeaderOrder);
146 }
147 }
148 Ok(Self { entries })
149 }
150
151 #[must_use]
153 pub const fn as_slice(self) -> &'a [RequestHeader<'a>] {
154 self.entries
155 }
156}
157
158impl fmt::Debug for SigningHeaders<'_> {
159 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160 formatter
161 .debug_struct("SigningHeaders")
162 .field("count", &self.entries.len())
163 .field("values", &"[redacted]")
164 .finish()
165 }
166}
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170pub enum SigningInputError {
171 TooManyHeaders,
173 HeaderOrder,
175 HeaderMismatch,
177 LengthOverflow,
179 InputTooLarge,
181 OutputTooSmall,
183 SnapshotChanged,
185}
186
187impl_static_error!(SigningInputError,
188 Self::TooManyHeaders => "too many signing headers were selected",
189 Self::HeaderOrder => "signing headers are not in canonical order",
190 Self::HeaderMismatch => "signing header does not match the request",
191 Self::LengthOverflow => "signing field length cannot be represented",
192 Self::InputTooLarge => "canonical signing input exceeds the length limit",
193 Self::OutputTooSmall => "canonical signing output is too small",
194 Self::SnapshotChanged => "canonical signing snapshot changed during encoding",
195);
196
197pub trait RequestBodyHasher {
199 type Error;
201
202 fn digest_algorithm(&self) -> SigningDigestAlgorithm<'_>;
204
205 fn hash_body(&self, body: &[u8], output: &mut [u8]) -> Result<usize, Self::Error>;
207}
208
209pub struct CanonicalSigningInput<'storage, 'request> {
211 request: TransportRequest<'request>,
212 context: SigningContext<'request>,
213 storage: &'storage mut [u8],
214 len: usize,
215}
216
217impl<'storage, 'request> CanonicalSigningInput<'storage, 'request> {
218 pub fn new_hashed<H: RequestBodyHasher + ?Sized>(
220 request: TransportRequest<'request>,
221 context: SigningContext<'request>,
222 selected_headers: SigningHeaders<'_>,
223 freshness: SigningFreshness<'_>,
224 hasher: &H,
225 digest_storage: &mut [u8],
226 output: &'storage mut [u8],
227 ) -> Result<Self, SigningBuildError<H::Error>> {
228 let mut digest_storage = DigestScratch::new(digest_storage);
229 validate_selected_headers(request, selected_headers).map_err(SigningBuildError::Input)?;
230 if hasher.digest_algorithm() != context.digest_algorithm() {
231 return Err(SigningBuildError::DigestAlgorithmMismatch);
232 }
233 let digest_len = hasher
234 .hash_body(request.body(), digest_storage.as_mut())
235 .map_err(SigningBuildError::Hasher)?;
236 let digest_bytes = digest_storage
237 .as_slice()
238 .get(..digest_len)
239 .ok_or(SigningBuildError::Digest(SigningValueError::TooLong))?;
240 let body_digest =
241 SigningBodyDigest::new(digest_bytes).map_err(SigningBuildError::Digest)?;
242 let snapshot = SigningSnapshot {
243 request,
244 context,
245 selected_headers,
246 body_digest,
247 nonce: freshness.nonce(),
248 time: freshness.time(),
249 };
250 let required = measure_snapshot_bounded(
251 snapshot,
252 MAX_CANONICAL_SIGNING_INPUT_BYTES,
253 SigningInputError::InputTooLarge,
254 encode_signing_snapshot,
255 )
256 .map_err(SigningBuildError::Input)?;
257 if output.len() < required {
258 return Err(SigningBuildError::Input(SigningInputError::OutputTooSmall));
259 }
260 let len = encode_snapshot_bounded(
261 snapshot,
262 output,
263 MAX_CANONICAL_SIGNING_INPUT_BYTES,
264 SigningInputError::SnapshotChanged,
265 encode_signing_snapshot,
266 )
267 .map_err(SigningBuildError::Input)?;
268 Ok(Self {
269 request,
270 context,
271 storage: output,
272 len,
273 })
274 }
275
276 #[must_use]
278 pub fn as_bytes(&self) -> &[u8] {
279 self.storage.get(..self.len).unwrap_or_default()
280 }
281
282 #[must_use]
284 pub const fn request(&self) -> TransportRequest<'request> {
285 self.request
286 }
287
288 #[must_use]
290 pub const fn context(&self) -> SigningContext<'request> {
291 self.context
292 }
293
294 pub fn sign_into<'signature, S: RequestSigner>(
296 self,
297 signer: &S,
298 output: &'signature mut [u8],
299 ) -> Result<SignedRequest<'signature, 'request>, SigningOutputError<S::Error>> {
300 let result =
301 SignedRequest::sign(self.request, self.context, self.as_bytes(), signer, output);
302 drop(self);
303 result
304 }
305}
306
307impl fmt::Debug for CanonicalSigningInput<'_, '_> {
308 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
309 formatter
310 .debug_struct("CanonicalSigningInput")
311 .field("len", &self.len)
312 .field("input", &"[redacted]")
313 .finish()
314 }
315}
316
317impl Drop for CanonicalSigningInput<'_, '_> {
318 fn drop(&mut self) {
319 sanitize_bytes(self.storage);
320 }
321}
322
323#[derive(Clone, Copy)]
324struct SigningSnapshot<'request, 'context, 'headers, 'digest, 'nonce> {
325 request: TransportRequest<'request>,
326 context: SigningContext<'context>,
327 selected_headers: SigningHeaders<'headers>,
328 body_digest: SigningBodyDigest<'digest>,
329 nonce: SigningNonce<'nonce>,
330 time: UnixTime,
331}
332
333fn validate_bounded(value: &[u8], maximum: usize) -> Result<(), SigningValueError> {
334 if value.is_empty() {
335 return Err(SigningValueError::Empty);
336 }
337 if value.len() > maximum {
338 return Err(SigningValueError::TooLong);
339 }
340 Ok(())
341}
342
343fn validate_selected_headers(
344 request: TransportRequest<'_>,
345 selected: SigningHeaders<'_>,
346) -> Result<(), SigningInputError> {
347 for expected in selected.as_slice() {
348 let Some(actual) = request.headers().get(expected.name().as_str()) else {
349 return Err(SigningInputError::HeaderMismatch);
350 };
351 if actual.value().as_str().as_bytes() != expected.value().as_str().as_bytes()
352 || actual.sensitivity() != expected.sensitivity()
353 {
354 return Err(SigningInputError::HeaderMismatch);
355 }
356 }
357 Ok(())
358}
359
360fn encode_signing_snapshot(
361 snapshot: SigningSnapshot<'_, '_, '_, '_, '_>,
362 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
363) -> Result<(), SigningInputError> {
364 encoder.bytes(SIGNING_DOMAIN)?;
365 encode_u8_len(encoder, snapshot.context.provider().as_str().as_bytes())?;
366 encode_u8_len(encoder, snapshot.context.service().as_str().as_bytes())?;
367 let endpoint = snapshot.context.endpoint();
368 let scheme = match endpoint.scheme() {
369 EndpointScheme::Http => b"http".as_slice(),
370 EndpointScheme::Https => b"https".as_slice(),
371 };
372 encode_u8_len(encoder, scheme)?;
373 encode_canonical_host(encoder, endpoint.canonical_host())?;
374 encoder.bytes(&endpoint.effective_port().to_be_bytes())?;
375 encode_u16_len(encoder, endpoint.base_path().as_bytes())?;
376 encode_optional_scope(encoder, snapshot.context.audience())?;
377 encode_optional_scope(encoder, snapshot.context.account())?;
378 encode_optional_scope(encoder, snapshot.context.tenant())?;
379 encode_u16_len(encoder, snapshot.context.key_id().as_str().as_bytes())?;
380 encode_u16_len(
381 encoder,
382 snapshot.context.digest_algorithm().as_str().as_bytes(),
383 )?;
384 encode_u16_len(
385 encoder,
386 snapshot.context.signature_algorithm().as_str().as_bytes(),
387 )?;
388 encode_u8_len(encoder, snapshot.request.method().as_str().as_bytes())?;
389 encode_u16_len(encoder, snapshot.request.target().as_str().as_bytes())?;
390 let count = u8::try_from(snapshot.selected_headers.as_slice().len())
391 .map_err(|_| SigningInputError::LengthOverflow)?;
392 encoder.byte(count)?;
393 for header in snapshot.selected_headers.as_slice() {
394 encode_lowercase_name(encoder, header.name().as_str())?;
395 encode_u16_len(encoder, header.value().as_str().as_bytes())?;
396 }
397 encode_u8_len(encoder, snapshot.body_digest.as_bytes())?;
398 encode_u16_len(encoder, snapshot.nonce.as_bytes())?;
399 encoder.bytes(&snapshot.time.as_seconds().to_be_bytes())
400}
401
402fn encode_canonical_host(
403 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
404 host: CanonicalHost<'_>,
405) -> Result<(), SigningInputError> {
406 match host {
407 CanonicalHost::Dns(value) => {
408 encoder.byte(0)?;
409 encode_u16_len(encoder, value.as_bytes())
410 }
411 CanonicalHost::Ipv4(octets) => {
412 encoder.byte(1)?;
413 encoder.bytes(&octets)
414 }
415 CanonicalHost::Ipv6(octets) => {
416 encoder.byte(2)?;
417 encoder.bytes(&octets)
418 }
419 }
420}
421
422fn encode_optional_scope(
423 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
424 value: Option<ScopeValue<'_>>,
425) -> Result<(), SigningInputError> {
426 match value {
427 Some(value) => {
428 encoder.byte(1)?;
429 encode_u16_len(encoder, value.as_str().as_bytes())
430 }
431 None => encoder.byte(0),
432 }
433}
434
435fn encode_lowercase_name(
436 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
437 name: &str,
438) -> Result<(), SigningInputError> {
439 let len = u8::try_from(name.len()).map_err(|_| SigningInputError::LengthOverflow)?;
440 encoder.byte(len)?;
441 for byte in name.bytes() {
442 encoder.byte(byte.to_ascii_lowercase())?;
443 }
444 Ok(())
445}
446
447fn encode_u8_len(
448 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
449 value: &[u8],
450) -> Result<(), SigningInputError> {
451 let len = u8::try_from(value.len()).map_err(|_| SigningInputError::LengthOverflow)?;
452 encoder.byte(len)?;
453 encoder.bytes(value)
454}
455
456fn encode_u16_len(
457 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
458 value: &[u8],
459) -> Result<(), SigningInputError> {
460 let len = u16::try_from(value.len()).map_err(|_| SigningInputError::LengthOverflow)?;
461 encoder.bytes(&len.to_be_bytes())?;
462 encoder.bytes(value)
463}
464
465#[cfg(test)]
466mod tests;