1use http::HeaderMap;
4use sha2::Digest as _;
5use std::fmt::{Display, Formatter};
6
7use crate::sha256_digest;
8
9pub const DOCKER_DIGEST_HEADER: &str = "Docker-Content-Digest";
10
11pub type Result<T> = std::result::Result<T, DigestError>;
12
13#[derive(Debug, thiserror::Error)]
15pub enum DigestError {
16 #[error("Invalid digest header: {0}")]
18 InvalidHeader(#[from] http::header::ToStrError),
19 #[error("Unsupported digest algorithm: {0}")]
21 UnsupportedAlgorithm(String),
22 #[error("Missing digest algorithm")]
24 MissingAlgorithm,
25 #[error("Invalid digest. Expected {expected}, got {actual}")]
27 VerificationError {
28 expected: String,
30 actual: String,
32 },
33}
34
35#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
37pub struct Digest<'a> {
38 pub algorithm: &'a str,
39 pub digest: &'a str,
40}
41
42impl<'a> Display for Digest<'a> {
43 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
44 let Digest { algorithm, digest } = self;
45 write!(f, "{algorithm}:{digest}")
46 }
47}
48
49impl<'a> Digest<'a> {
50 pub fn new(digest: &'a str) -> Result<Self> {
53 let (algorithm, digest) = digest
54 .split_once(':')
55 .ok_or(DigestError::MissingAlgorithm)?;
56 Ok(Self { algorithm, digest })
57 }
58}
59
60pub(crate) enum Digester {
64 Sha256(sha2::Sha256),
65 Sha384(sha2::Sha384),
66 Sha512(sha2::Sha512),
67}
68
69impl Digester {
70 pub fn new(digest: &str) -> Result<Self> {
71 let parsed_digest = Digest::new(digest)?;
72
73 match parsed_digest.algorithm {
74 "sha256" => Ok(Digester::Sha256(sha2::Sha256::new())),
75 "sha384" => Ok(Digester::Sha384(sha2::Sha384::new())),
76 "sha512" => Ok(Digester::Sha512(sha2::Sha512::new())),
77 _ => Err(DigestError::UnsupportedAlgorithm(
80 parsed_digest.algorithm.to_string(),
81 )),
82 }
83 }
84
85 pub fn update(&mut self, data: impl AsRef<[u8]>) {
86 match self {
87 Self::Sha256(d) => d.update(data),
88 Self::Sha384(d) => d.update(data),
89 Self::Sha512(d) => d.update(data),
90 }
91 }
92
93 pub fn finalize(&mut self) -> String {
94 match self {
95 Self::Sha256(d) => format!("sha256:{:x}", d.finalize_reset()),
96 Self::Sha384(d) => format!("sha384:{:x}", d.finalize_reset()),
97 Self::Sha512(d) => format!("sha512:{:x}", d.finalize_reset()),
98 }
99 }
100}
101
102pub fn digest_header_value(headers: HeaderMap) -> Result<Option<String>> {
104 headers
105 .get(DOCKER_DIGEST_HEADER)
106 .and_then(|hv| {
107 hv.to_str()
108 .map(|s| (!s.is_empty()).then(|| s.to_string()))
110 .transpose()
111 })
112 .transpose()
113 .map_err(DigestError::from)
114}
115
116pub fn validate_digest(
123 body: &[u8],
124 digest_header: Option<String>,
125 reference_digest: Option<&str>,
126) -> Result<String> {
127 let digest_header = digest_header.as_ref().map(|s| Digest::new(s)).transpose()?;
128 let reference_digest = reference_digest.map(Digest::new).transpose()?;
129 match (digest_header, reference_digest) {
130 (Some(digest), Some(reference)) if digest == reference => {
132 calculate_and_validate(body, digest)
133 }
134 (Some(digest), Some(reference)) => {
135 calculate_and_validate(body, reference)?;
136 calculate_and_validate(body, digest)
137 }
138 (Some(digest), None) => calculate_and_validate(body, digest),
139 (None, Some(reference)) => calculate_and_validate(body, reference),
140 (None, None) => Ok(sha256_digest(body)),
142 }
143}
144
145fn calculate_and_validate(content: &[u8], parsed_digest: Digest) -> Result<String> {
147 let digest_calculated = match parsed_digest.algorithm {
148 "sha256" => format!("{:x}", sha2::Sha256::digest(content)),
149 "sha384" => format!("{:x}", sha2::Sha384::digest(content)),
150 "sha512" => format!("{:x}", sha2::Sha512::digest(content)),
151 other => return Err(DigestError::UnsupportedAlgorithm(other.to_string())),
152 };
153 let hex = Digest {
154 algorithm: parsed_digest.algorithm,
155 digest: &digest_calculated,
156 };
157 tracing::debug!(%hex, "Computed digest of payload");
158 if hex != parsed_digest {
159 return Err(DigestError::VerificationError {
160 expected: parsed_digest.to_string(),
161 actual: Digest {
162 algorithm: parsed_digest.algorithm,
163 digest: &digest_calculated,
164 }
165 .to_string(),
166 });
167 }
168 Ok(hex.to_string())
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_validate_digest() {
177 let body = b"hello world";
178 let digest_sha256 = format!("sha256:{:x}", sha2::Sha256::digest(body));
179 let digest_sha384 = format!("sha384:{:x}", sha2::Sha384::digest(body));
180
181 assert_eq!(
183 validate_digest(body, Some(digest_sha256.clone()), Some(&digest_sha256))
184 .expect("Failed to validate digest with matching header and reference"),
185 digest_sha256
186 );
187
188 assert_eq!(
190 validate_digest(body, Some(digest_sha256.clone()), Some(&digest_sha384))
191 .expect("Failed to validate digest with different header and reference"),
192 digest_sha256
193 );
194
195 assert_eq!(
197 validate_digest(body, Some(digest_sha256.clone()), None)
198 .expect("Failed to validate digest with only header"),
199 digest_sha256
200 );
201
202 assert_eq!(
204 validate_digest(body, None, Some(&digest_sha384))
205 .expect("Failed to validate digest with only reference"),
206 digest_sha384
207 );
208
209 assert_eq!(
211 validate_digest(body, None, None)
212 .expect("Failed to validate digest with no digests provided"),
213 digest_sha256
214 );
215
216 let invalid_digest = "sha256:invalid";
218 validate_digest(body, Some(invalid_digest.to_string()), None)
219 .expect_err("Expected error for invalid digest");
220
221 let invalid_layer_digest = "sha512:invalid";
223 validate_digest(
224 body,
225 Some(digest_sha256.clone()),
226 Some(invalid_layer_digest),
227 )
228 .expect_err("Expected error for invalid layer digest");
229
230 let unsupported_digest = "md5:d41d8cd98f00b204e9800998ecf8427e";
232 validate_digest(body, Some(unsupported_digest.to_string()), None)
233 .expect_err("Expected error for unsupported algorithm");
234 }
235}