oci_client/
digest.rs

1//! Errors and functions for validating digests
2
3use http::HeaderMap;
4use sha2::Digest as _;
5
6use crate::sha256_digest;
7
8pub const DOCKER_DIGEST_HEADER: &str = "Docker-Content-Digest";
9
10pub type Result<T> = std::result::Result<T, DigestError>;
11
12/// Errors that can occur when validating digests
13#[derive(Debug, thiserror::Error)]
14pub enum DigestError {
15    /// Invalid digest header
16    #[error("Invalid digest header: {0}")]
17    InvalidHeader(#[from] http::header::ToStrError),
18    /// Invalid digest algorithm found
19    #[error("Unsupported digest algorithm: {0}")]
20    UnsupportedAlgorithm(String),
21    /// Missing digest algorithm
22    #[error("Missing digest algorithm")]
23    MissingAlgorithm,
24    /// Digest verification failed
25    #[error("Invalid digest. Expected {expected}, got {actual}")]
26    VerificationError {
27        /// Expected digest
28        expected: String,
29        /// Actual digest
30        actual: String,
31    },
32}
33
34/// A convenience struct for parsing a digest value with an algorithm
35#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
36pub struct Digest<'a> {
37    pub algorithm: &'a str,
38    pub digest: &'a str,
39}
40
41impl<'a> Digest<'a> {
42    /// Create a new digest from a str. This isn't using `FromStr` because we can't use lifetimes
43    /// properly when implementing the trait
44    pub fn new(digest: &'a str) -> Result<Self> {
45        let (algorithm, digest) = digest
46            .split_once(':')
47            .ok_or(DigestError::MissingAlgorithm)?;
48        Ok(Self { algorithm, digest })
49    }
50}
51
52/// Helper wrapper around various digest algorithms to make it easier to use them with our blob
53/// utils. This has to be an enum because the digest algorithms aren't object safe so we can't box
54/// dynner them
55pub(crate) enum Digester {
56    Sha256(sha2::Sha256),
57    Sha384(sha2::Sha384),
58    Sha512(sha2::Sha512),
59}
60
61impl Digester {
62    pub fn new(digest: &str) -> Result<Self> {
63        let parsed_digest = Digest::new(digest)?;
64
65        match parsed_digest.algorithm {
66            "sha256" => Ok(Digester::Sha256(sha2::Sha256::new())),
67            "sha384" => Ok(Digester::Sha384(sha2::Sha384::new())),
68            "sha512" => Ok(Digester::Sha512(sha2::Sha512::new())),
69            // We already check this above when parsing, but just in case, we return the error as
70            // well here
71            _ => Err(DigestError::UnsupportedAlgorithm(
72                parsed_digest.algorithm.to_string(),
73            )),
74        }
75    }
76
77    pub fn update(&mut self, data: impl AsRef<[u8]>) {
78        match self {
79            Self::Sha256(d) => d.update(data),
80            Self::Sha384(d) => d.update(data),
81            Self::Sha512(d) => d.update(data),
82        }
83    }
84
85    pub fn finalize(&mut self) -> String {
86        match self {
87            Self::Sha256(d) => format!("sha256:{:x}", d.finalize_reset()),
88            Self::Sha384(d) => format!("sha384:{:x}", d.finalize_reset()),
89            Self::Sha512(d) => format!("sha512:{:x}", d.finalize_reset()),
90        }
91    }
92}
93
94/// Helper for extracting `Docker-Content-Digest` header from manifest GET or HEAD request.
95pub fn digest_header_value(headers: HeaderMap) -> Result<Option<String>> {
96    headers
97        .get(DOCKER_DIGEST_HEADER)
98        .map(|hv| hv.to_str().map(|s| s.to_string()))
99        .transpose()
100        .map_err(DigestError::from)
101}
102
103/// Given the optional digest header value and digest of the reference, returns the digest of the
104/// content, validating that the digest of the content matches the proper digest. If neither a
105/// header digest or a reference digest is provided, then the body is digested and returned as the
106/// digest. If both digests are provided, but they use different algorithms, then the header digest
107/// is returned after validation as according to the spec it is the "canonical" digest for the given
108/// content.
109pub fn validate_digest(
110    body: &[u8],
111    digest_header: Option<String>,
112    reference_digest: Option<&str>,
113) -> Result<String> {
114    match (digest_header, reference_digest) {
115        // If both digests are equal, then just calculate once
116        (Some(digest), Some(reference)) if digest == reference => {
117            calculate_and_validate(body, &digest)
118        }
119        (Some(digest), Some(reference)) => {
120            calculate_and_validate(body, reference)?;
121            calculate_and_validate(body, &digest)
122        }
123        (Some(digest), None) => calculate_and_validate(body, &digest),
124        (None, Some(reference)) => calculate_and_validate(body, reference),
125        // If we have neither, just digest the body
126        (None, None) => Ok(sha256_digest(body)),
127    }
128}
129
130/// Helper for calculating and validating the digest of the given content
131fn calculate_and_validate(content: &[u8], digest: &str) -> Result<String> {
132    let parsed_digest = Digest::new(digest)?;
133    let digest_calculated = match parsed_digest.algorithm {
134        "sha256" => format!("{:x}", sha2::Sha256::digest(content)),
135        "sha384" => format!("{:x}", sha2::Sha384::digest(content)),
136        "sha512" => format!("{:x}", sha2::Sha512::digest(content)),
137        other => return Err(DigestError::UnsupportedAlgorithm(other.to_string())),
138    };
139    let hex = format!("{}:{digest_calculated}", parsed_digest.algorithm);
140    tracing::debug!(%hex, "Computed digest of payload");
141    if hex != digest {
142        return Err(DigestError::VerificationError {
143            expected: digest.to_owned(),
144            actual: hex,
145        });
146    }
147    Ok(hex)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_validate_digest() {
156        let body = b"hello world";
157        let digest_sha256 = format!("sha256:{:x}", sha2::Sha256::digest(body));
158        let digest_sha384 = format!("sha384:{:x}", sha2::Sha384::digest(body));
159
160        // Test case 1: Both digests are equal
161        assert_eq!(
162            validate_digest(body, Some(digest_sha256.clone()), Some(&digest_sha256))
163                .expect("Failed to validate digest with matching header and reference"),
164            digest_sha256
165        );
166
167        // Test case 2: Different digests
168        assert_eq!(
169            validate_digest(body, Some(digest_sha256.clone()), Some(&digest_sha384))
170                .expect("Failed to validate digest with different header and reference"),
171            digest_sha256
172        );
173
174        // Test case 3: Only digest_header
175        assert_eq!(
176            validate_digest(body, Some(digest_sha256.clone()), None)
177                .expect("Failed to validate digest with only header"),
178            digest_sha256
179        );
180
181        // Test case 4: Only reference_digest
182        assert_eq!(
183            validate_digest(body, None, Some(&digest_sha384))
184                .expect("Failed to validate digest with only reference"),
185            digest_sha384
186        );
187
188        // Test case 5: No digests provided
189        assert_eq!(
190            validate_digest(body, None, None)
191                .expect("Failed to validate digest with no digests provided"),
192            digest_sha256
193        );
194
195        // Test case 6: Invalid digest
196        let invalid_digest = "sha256:invalid";
197        validate_digest(body, Some(invalid_digest.to_string()), None)
198            .expect_err("Expected error for invalid digest");
199
200        // Test case 7: Valid header digest and invalid layer digest
201        let invalid_layer_digest = "sha512:invalid";
202        validate_digest(
203            body,
204            Some(digest_sha256.clone()),
205            Some(invalid_layer_digest),
206        )
207        .expect_err("Expected error for invalid layer digest");
208
209        // Test case 8: Unsupported algorithm
210        let unsupported_digest = "md5:d41d8cd98f00b204e9800998ecf8427e";
211        validate_digest(body, Some(unsupported_digest.to_string()), None)
212            .expect_err("Expected error for unsupported algorithm");
213    }
214}