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: snapshot.request,
270 context: snapshot.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
323struct SigningSnapshot<'request, 'context, 'headers, 'digest, 'nonce> {
324 request: TransportRequest<'request>,
325 context: SigningContext<'context>,
326 selected_headers: SigningHeaders<'headers>,
327 body_digest: SigningBodyDigest<'digest>,
328 nonce: SigningNonce<'nonce>,
329 time: UnixTime,
330}
331
332fn validate_bounded(value: &[u8], maximum: usize) -> Result<(), SigningValueError> {
333 if value.is_empty() {
334 return Err(SigningValueError::Empty);
335 }
336 if value.len() > maximum {
337 return Err(SigningValueError::TooLong);
338 }
339 Ok(())
340}
341
342fn validate_selected_headers(
343 request: TransportRequest<'_>,
344 selected: SigningHeaders<'_>,
345) -> Result<(), SigningInputError> {
346 for expected in selected.as_slice() {
347 let Some(actual) = request.headers().get(expected.name().as_str()) else {
348 return Err(SigningInputError::HeaderMismatch);
349 };
350 if actual.value().as_str().as_bytes() != expected.value().as_str().as_bytes()
351 || actual.sensitivity() != expected.sensitivity()
352 {
353 return Err(SigningInputError::HeaderMismatch);
354 }
355 }
356 Ok(())
357}
358
359fn encode_signing_snapshot(
360 snapshot: &SigningSnapshot<'_, '_, '_, '_, '_>,
361 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
362) -> Result<(), SigningInputError> {
363 encoder.bytes(SIGNING_DOMAIN)?;
364 encode_u8_len(encoder, snapshot.context.provider().as_str().as_bytes())?;
365 encode_u8_len(encoder, snapshot.context.service().as_str().as_bytes())?;
366 let endpoint = snapshot.context.endpoint();
367 let scheme = match endpoint.scheme() {
368 EndpointScheme::Http => b"http".as_slice(),
369 EndpointScheme::Https => b"https".as_slice(),
370 };
371 encode_u8_len(encoder, scheme)?;
372 encode_canonical_host(encoder, endpoint.canonical_host())?;
373 encoder.bytes(&endpoint.effective_port().to_be_bytes())?;
374 encode_u16_len(encoder, endpoint.base_path().as_bytes())?;
375 encode_optional_scope(encoder, snapshot.context.audience())?;
376 encode_optional_scope(encoder, snapshot.context.account())?;
377 encode_optional_scope(encoder, snapshot.context.tenant())?;
378 encode_u16_len(encoder, snapshot.context.key_id().as_str().as_bytes())?;
379 encode_u16_len(
380 encoder,
381 snapshot.context.digest_algorithm().as_str().as_bytes(),
382 )?;
383 encode_u16_len(
384 encoder,
385 snapshot.context.signature_algorithm().as_str().as_bytes(),
386 )?;
387 encode_u8_len(encoder, snapshot.request.method().as_str().as_bytes())?;
388 encode_u16_len(encoder, snapshot.request.target().as_str().as_bytes())?;
389 let count = u8::try_from(snapshot.selected_headers.as_slice().len())
390 .map_err(|_| SigningInputError::LengthOverflow)?;
391 encoder.byte(count)?;
392 for header in snapshot.selected_headers.as_slice() {
393 encode_lowercase_name(encoder, header.name().as_str())?;
394 encode_u16_len(encoder, header.value().as_str().as_bytes())?;
395 }
396 encode_u8_len(encoder, snapshot.body_digest.as_bytes())?;
397 encode_u16_len(encoder, snapshot.nonce.as_bytes())?;
398 encoder.bytes(&snapshot.time.as_seconds().to_be_bytes())
399}
400
401fn encode_canonical_host(
402 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
403 host: CanonicalHost<'_>,
404) -> Result<(), SigningInputError> {
405 match host {
406 CanonicalHost::Dns(value) => {
407 encoder.byte(0)?;
408 encode_u16_len(encoder, value.as_bytes())
409 }
410 CanonicalHost::Ipv4(octets) => {
411 encoder.byte(1)?;
412 encoder.bytes(&octets)
413 }
414 CanonicalHost::Ipv6(octets) => {
415 encoder.byte(2)?;
416 encoder.bytes(&octets)
417 }
418 }
419}
420
421fn encode_optional_scope(
422 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
423 value: Option<ScopeValue<'_>>,
424) -> Result<(), SigningInputError> {
425 match value {
426 Some(value) => {
427 encoder.byte(1)?;
428 encode_u16_len(encoder, value.as_str().as_bytes())
429 }
430 None => encoder.byte(0),
431 }
432}
433
434fn encode_lowercase_name(
435 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
436 name: &str,
437) -> Result<(), SigningInputError> {
438 let len = u8::try_from(name.len()).map_err(|_| SigningInputError::LengthOverflow)?;
439 encoder.byte(len)?;
440 for byte in name.bytes() {
441 encoder.byte(byte.to_ascii_lowercase())?;
442 }
443 Ok(())
444}
445
446fn encode_u8_len(
447 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
448 value: &[u8],
449) -> Result<(), SigningInputError> {
450 let len = u8::try_from(value.len()).map_err(|_| SigningInputError::LengthOverflow)?;
451 encoder.byte(len)?;
452 encoder.bytes(value)
453}
454
455fn encode_u16_len(
456 encoder: &mut SnapshotEncoder<'_, SigningInputError>,
457 value: &[u8],
458) -> Result<(), SigningInputError> {
459 let len = u16::try_from(value.len()).map_err(|_| SigningInputError::LengthOverflow)?;
460 encoder.bytes(&len.to_be_bytes())?;
461 encoder.bytes(value)
462}
463
464#[cfg(test)]
465mod tests;