docker_registry/v2/
content_digest.rs

1use std::str;
2
3/// Implements types and methods for content verification
4use sha2::{self, Digest};
5
6/// DigestAlgorithm declares the supported algorithms
7#[derive(strum::Display, Clone, Debug)]
8pub enum DigestAlgorithm {
9  Sha256(sha2::Sha256),
10}
11
12impl std::str::FromStr for DigestAlgorithm {
13  type Err = ContentDigestError;
14
15  fn from_str(name: &str) -> Result<Self, Self::Err> {
16    match name {
17      "sha256" => Ok(DigestAlgorithm::Sha256(sha2::Sha256::new())),
18      _ => Err(ContentDigestError::AlgorithmUnknown(name.to_string())),
19    }
20  }
21}
22
23#[derive(Debug, thiserror::Error)]
24pub enum ContentDigestError {
25  #[error("digest {0} does not have algorithm prefix")]
26  BadDigest(String),
27  #[error("unknown algorithm: {0}")]
28  AlgorithmUnknown(String),
29  #[error("verification failed: expected '{expected}', got '{got}'")]
30  Verify { expected: String, got: String },
31}
32
33/// ContentDigest stores a digest and its DigestAlgorithm
34#[derive(Clone, Debug)]
35pub struct ContentDigest {
36  digest: String,
37  algorithm: DigestAlgorithm,
38}
39
40impl ContentDigest {
41  /// try_new attempts to parse the digest string and create a ContentDigest instance from it
42  ///
43  /// Success depends on
44  /// - the string having an "algorithm:" prefix
45  /// - the algorithm being supported by DigestAlgorithm
46  pub fn try_new(digest: &str) -> std::result::Result<Self, ContentDigestError> {
47    let digest_split = digest.split(':').collect::<Vec<&str>>();
48
49    if digest_split.len() != 2 {
50      return Err(ContentDigestError::BadDigest(digest.to_string()));
51    }
52
53    let algorithm = std::str::FromStr::from_str(digest_split[0])?;
54    Ok(ContentDigest {
55      digest: digest.to_string(),
56      algorithm,
57    })
58  }
59
60  pub fn update(&mut self, input: &[u8]) {
61    self.algorithm.update(input)
62  }
63
64  pub fn verify(self) -> std::result::Result<(), ContentDigestError> {
65    let digest = self.algorithm.digest();
66    if digest != self.digest {
67      return Err(ContentDigestError::Verify {
68        expected: self.digest,
69        got: digest,
70      });
71    }
72    Ok(())
73  }
74}
75
76impl std::fmt::Display for ContentDigest {
77  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
78    write!(f, "{}:{}", self.algorithm, self.digest)
79  }
80}
81
82impl DigestAlgorithm {
83  fn update(&mut self, input: &[u8]) {
84    match self {
85      DigestAlgorithm::Sha256(hash) => {
86        hash.update(input);
87      }
88    }
89  }
90
91  fn digest(self) -> String {
92    let (algo, digest) = match self {
93      DigestAlgorithm::Sha256(hash) => ("sha256", hash.finalize()),
94    };
95    format!("{}:{:x}", algo, &digest)
96  }
97}
98
99#[cfg(test)]
100mod tests {
101  use sha2;
102
103  use super::*;
104
105  type Fallible<T> = Result<T, crate::Error>;
106
107  #[test]
108  fn try_new_succeeds_with_correct_digest() -> Fallible<()> {
109    let correct_digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
110    ContentDigest::try_new(correct_digest)?;
111
112    Ok(())
113  }
114
115  #[test]
116  fn try_new_fails_with_incorrect_digest() {
117    for incorrect_digest in &[
118      "invalid",
119      "invalid:",
120      "invalid:0000000000000000000000000000000000000000000000000000000000000000",
121    ] {
122      if ContentDigest::try_new(incorrect_digest).is_ok() {
123        panic!("expected try_new to fail for incorrect digest {incorrect_digest}");
124      }
125    }
126  }
127
128  #[test]
129  fn verify_succeeds_with_same_content() -> Fallible<()> {
130    let blob: &[u8] = b"somecontent";
131    let mut content_digest =
132      ContentDigest::try_new("sha256:d5a3477d91583e65a7aba6f6db7a53e2de739bc7bf8f4a08f0df0457b637f1fb")?;
133    content_digest.update(blob);
134    content_digest.verify().map_err(Into::into)
135  }
136
137  #[test]
138  fn verify_chunked_succeeds_with_same_content() -> Fallible<()> {
139    let mut content_digest =
140      ContentDigest::try_new("sha256:d5a3477d91583e65a7aba6f6db7a53e2de739bc7bf8f4a08f0df0457b637f1fb")?;
141    content_digest.update(b"some");
142    content_digest.update(b"content");
143    content_digest.verify().map_err(Into::into)
144  }
145
146  #[test]
147  fn verify_fails_with_different_content() -> Fallible<()> {
148    let blob: &[u8] = b"somecontent";
149    let different_blob: &[u8] = b"someothercontent";
150
151    let mut expected_digest = DigestAlgorithm::Sha256(sha2::Sha256::new());
152    expected_digest.update(different_blob);
153    let expected_digest = expected_digest.digest();
154
155    let mut content_digest = ContentDigest::try_new(&expected_digest)?;
156    content_digest.update(blob);
157    if content_digest.verify().is_ok() {
158      panic!("expected try_verify to fail for a different blob");
159    }
160    Ok(())
161  }
162}