1use 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#[derive(Debug, thiserror::Error)]
14pub enum DigestError {
15 #[error("Invalid digest header: {0}")]
17 InvalidHeader(#[from] http::header::ToStrError),
18 #[error("Unsupported digest algorithm: {0}")]
20 UnsupportedAlgorithm(String),
21 #[error("Missing digest algorithm")]
23 MissingAlgorithm,
24 #[error("Invalid digest. Expected {expected}, got {actual}")]
26 VerificationError {
27 expected: String,
29 actual: String,
31 },
32}
33
34#[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 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
52pub(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 _ => 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
94pub 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
103pub 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 (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 (None, None) => Ok(sha256_digest(body)),
127 }
128}
129
130fn 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 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 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 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 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 assert_eq!(
190 validate_digest(body, None, None)
191 .expect("Failed to validate digest with no digests provided"),
192 digest_sha256
193 );
194
195 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 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 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}