use core::fmt;
use cloud_sdk_sanitization::sanitize_bytes;
use subtle::ConstantTimeEq;
use crate::operation::PreparedRequest;
use crate::transport::EndpointIdentity;
mod encoding;
mod writer;
use encoding::{encode, encoded_len};
use writer::Writer;
const DOMAIN: &[u8] = b"cloud-sdk/retry-fingerprint/v2\0";
pub const MAX_FINGERPRINT_SCOPE_BYTES: usize = 1024;
pub const MAX_FINGERPRINT_DIGEST_BYTES: usize = 64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FingerprintScope<'a> {
Absent,
Value(&'a [u8]),
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum DigestAlgorithm {
Sha256,
Sha384,
Sha512,
Blake3,
}
impl DigestAlgorithm {
pub(crate) const fn output_len(self) -> usize {
match self {
Self::Sha256 | Self::Blake3 => 32,
Self::Sha384 => 48,
Self::Sha512 => 64,
}
}
}
pub trait FingerprintHasher {
type Error;
fn algorithm(&self) -> DigestAlgorithm;
fn digest(&self, input: &[u8], output: &mut [u8]) -> Result<usize, Self::Error>;
}
pub enum FingerprintBuildError<E> {
MissingOperationId,
ScopeTooLong,
LengthOverflow,
OutputTooSmall,
EndpointNotAdmitted,
SensitiveBodyRequiresDigest,
Hasher(E),
InvalidDigestLength,
}
impl<E> fmt::Debug for FingerprintBuildError<E> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::MissingOperationId => "FingerprintBuildError::MissingOperationId",
Self::ScopeTooLong => "FingerprintBuildError::ScopeTooLong",
Self::LengthOverflow => "FingerprintBuildError::LengthOverflow",
Self::OutputTooSmall => "FingerprintBuildError::OutputTooSmall",
Self::EndpointNotAdmitted => "FingerprintBuildError::EndpointNotAdmitted",
Self::SensitiveBodyRequiresDigest => {
"FingerprintBuildError::SensitiveBodyRequiresDigest"
}
Self::Hasher(_) => "FingerprintBuildError::Hasher([redacted])",
Self::InvalidDigestLength => "FingerprintBuildError::InvalidDigestLength",
})
}
}
impl<E> fmt::Display for FingerprintBuildError<E> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::MissingOperationId => "retry fingerprint requires an operation identifier",
Self::ScopeTooLong => "retry fingerprint scope exceeds the length limit",
Self::LengthOverflow => "retry fingerprint length overflowed",
Self::OutputTooSmall => "retry fingerprint output is too small",
Self::EndpointNotAdmitted => "retry fingerprint endpoint is not admitted",
Self::SensitiveBodyRequiresDigest => {
"sensitive request body requires a collision-resistant retry digest"
}
Self::Hasher(_) => "retry fingerprint hashing failed",
Self::InvalidDigestLength => "retry fingerprint digest length is invalid",
})
}
}
impl<E> core::error::Error for FingerprintBuildError<E>
where
E: core::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::Hasher(error) => Some(error),
_ => None,
}
}
}
pub struct CanonicalFingerprint<'output, 'request> {
storage: &'output mut [u8],
len: usize,
prepared: PreparedRequest<'request>,
}
impl<'output, 'request> CanonicalFingerprint<'output, 'request> {
#[must_use]
pub fn as_ref(&self) -> FingerprintRef<'_> {
FingerprintRef(FingerprintKind::Exact(self.as_bytes()))
}
#[must_use]
pub fn subject(&self) -> RetrySubject<'request, '_> {
RetrySubject {
prepared: &self.prepared,
fingerprint: self.as_ref(),
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
false
}
fn as_bytes(&self) -> &[u8] {
self.storage.get(..self.len).unwrap_or_default()
}
}
impl fmt::Debug for CanonicalFingerprint<'_, '_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CanonicalFingerprint")
.field("len", &self.len)
.field("bytes", &"[redacted]")
.finish()
}
}
impl Drop for CanonicalFingerprint<'_, '_> {
fn drop(&mut self) {
sanitize_bytes(self.storage);
}
}
pub struct FingerprintDigest<'output, 'request> {
algorithm: DigestAlgorithm,
storage: &'output mut [u8],
len: usize,
prepared: PreparedRequest<'request>,
}
impl<'output, 'request> FingerprintDigest<'output, 'request> {
#[must_use]
pub const fn algorithm(&self) -> DigestAlgorithm {
self.algorithm
}
#[must_use]
pub fn as_ref(&self) -> FingerprintRef<'_> {
FingerprintRef(FingerprintKind::Digest {
algorithm: self.algorithm,
bytes: self.storage.get(..self.len).unwrap_or_default(),
})
}
#[must_use]
pub fn subject(&self) -> RetrySubject<'request, '_> {
RetrySubject {
prepared: &self.prepared,
fingerprint: self.as_ref(),
}
}
}
impl fmt::Debug for FingerprintDigest<'_, '_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("FingerprintDigest")
.field("algorithm", &self.algorithm)
.field("bytes", &"[redacted]")
.finish()
}
}
impl Drop for FingerprintDigest<'_, '_> {
fn drop(&mut self) {
sanitize_bytes(self.storage);
self.len = 0;
}
}
#[derive(Clone, Copy)]
pub struct FingerprintRef<'a>(FingerprintKind<'a>);
#[derive(Clone, Copy)]
pub struct RetrySubject<'request, 'fingerprint> {
prepared: &'fingerprint PreparedRequest<'request>,
fingerprint: FingerprintRef<'fingerprint>,
}
impl<'request, 'fingerprint> RetrySubject<'request, 'fingerprint> {
pub(crate) const fn prepared(self) -> &'fingerprint PreparedRequest<'request> {
self.prepared
}
pub(crate) const fn fingerprint(self) -> FingerprintRef<'fingerprint> {
self.fingerprint
}
}
impl fmt::Debug for RetrySubject<'_, '_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("RetrySubject")
.field("prepared", &self.prepared)
.field("fingerprint", &"[redacted]")
.finish()
}
}
#[derive(Clone, Copy)]
enum FingerprintKind<'a> {
Exact(&'a [u8]),
Digest {
algorithm: DigestAlgorithm,
bytes: &'a [u8],
},
}
impl<'a> FingerprintRef<'a> {
pub(crate) fn matches(self, other: Self) -> bool {
match (self.0, other.0) {
(FingerprintKind::Exact(left), FingerprintKind::Exact(right)) => {
constant_time_eq(left, right)
}
(
FingerprintKind::Digest {
algorithm: left_algorithm,
bytes: left,
},
FingerprintKind::Digest {
algorithm: right_algorithm,
bytes: right,
},
) => left_algorithm == right_algorithm && constant_time_eq(left, right),
_ => false,
}
}
}
impl fmt::Debug for FingerprintRef<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("FingerprintRef([redacted])")
}
}
pub fn build_canonical_fingerprint<'output, 'request>(
request: PreparedRequest<'request>,
endpoint: EndpointIdentity<'_>,
scope: FingerprintScope<'_>,
output: &'output mut [u8],
) -> Result<CanonicalFingerprint<'output, 'request>, FingerprintBuildError<core::convert::Infallible>>
{
sanitize_bytes(output);
if request.body_sensitivity().requires_digest() {
return Err(FingerprintBuildError::SensitiveBodyRequiresDigest);
}
build_canonical_fingerprint_inner(request, endpoint, scope, output)
}
#[allow(
clippy::large_types_passed_by_value,
reason = "the returned fingerprint must own the complete prepared request"
)]
fn build_canonical_fingerprint_inner<'output, 'request>(
request: PreparedRequest<'request>,
endpoint: EndpointIdentity<'_>,
scope: FingerprintScope<'_>,
output: &'output mut [u8],
) -> Result<CanonicalFingerprint<'output, 'request>, FingerprintBuildError<core::convert::Infallible>>
{
sanitize_bytes(output);
if !request.service().endpoint_policy().admits(endpoint) {
return Err(FingerprintBuildError::EndpointNotAdmitted);
}
let required = encoded_len(&request, endpoint, scope)?;
if output.len() < required {
return Err(FingerprintBuildError::OutputTooSmall);
}
let encoded = {
let mut writer = Writer::new(output);
encode(&request, endpoint, scope, &mut writer)
};
if let Err(error) = encoded {
sanitize_bytes(output);
return Err(error);
}
Ok(CanonicalFingerprint {
storage: output,
len: required,
prepared: request,
})
}
pub fn build_fingerprint_digest<'output, 'request, H: FingerprintHasher>(
request: PreparedRequest<'request>,
endpoint: EndpointIdentity<'_>,
scope: FingerprintScope<'_>,
scratch: &mut [u8],
output: &'output mut [u8],
hasher: &H,
) -> Result<FingerprintDigest<'output, 'request>, FingerprintBuildError<H::Error>> {
sanitize_bytes(output);
let canonical = build_canonical_fingerprint_inner(request, endpoint, scope, scratch)
.map_err(map_infallible_error)?;
let algorithm = hasher.algorithm();
let expected = algorithm.output_len();
let mut digest = FingerprintDigest {
algorithm,
storage: output,
len: 0,
prepared: request,
};
let output = digest
.storage
.get_mut(..expected)
.ok_or(FingerprintBuildError::OutputTooSmall)?;
let len = hasher
.digest(canonical.as_bytes(), output)
.map_err(FingerprintBuildError::Hasher)?;
if len != expected {
return Err(FingerprintBuildError::InvalidDigestLength);
}
digest.len = len;
Ok(digest)
}
fn map_infallible_error<E>(
error: FingerprintBuildError<core::convert::Infallible>,
) -> FingerprintBuildError<E> {
match error {
FingerprintBuildError::MissingOperationId => FingerprintBuildError::MissingOperationId,
FingerprintBuildError::ScopeTooLong => FingerprintBuildError::ScopeTooLong,
FingerprintBuildError::LengthOverflow => FingerprintBuildError::LengthOverflow,
FingerprintBuildError::OutputTooSmall => FingerprintBuildError::OutputTooSmall,
FingerprintBuildError::EndpointNotAdmitted => FingerprintBuildError::EndpointNotAdmitted,
FingerprintBuildError::SensitiveBodyRequiresDigest => {
FingerprintBuildError::SensitiveBodyRequiresDigest
}
FingerprintBuildError::InvalidDigestLength => FingerprintBuildError::InvalidDigestLength,
FingerprintBuildError::Hasher(never) => match never {},
}
}
fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
left.len() == right.len() && bool::from(left.ct_eq(right))
}
#[cfg(test)]
mod tests;