Skip to main content

oci_zero/
digest.rs

1//! OCI content digests and streaming verification.
2
3use core::fmt;
4
5use sha2::{Digest as _, Sha256};
6
7/// Number of bytes in a SHA-256 digest.
8pub const SHA256_SIZE: usize = 32;
9
10/// A supported OCI content digest.
11#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12pub struct Digest([u8; SHA256_SIZE]);
13
14impl Digest {
15    /// Parses a canonical `sha256:<64 lowercase hexadecimal digits>` digest.
16    pub fn parse(value: &str) -> Result<Self, DigestError> {
17        let encoded = value
18            .strip_prefix("sha256:")
19            .ok_or(DigestError::UnsupportedAlgorithm)?;
20        if encoded.len() != SHA256_SIZE * 2 {
21            return Err(DigestError::InvalidLength);
22        }
23        let mut bytes = [0; SHA256_SIZE];
24        for (index, pair) in encoded.as_bytes().chunks_exact(2).enumerate() {
25            bytes[index] = (hex(pair[0])? << 4) | hex(pair[1])?;
26        }
27        Ok(Self(bytes))
28    }
29
30    pub const fn from_bytes(bytes: [u8; SHA256_SIZE]) -> Self {
31        Self(bytes)
32    }
33
34    pub const fn as_bytes(&self) -> &[u8; SHA256_SIZE] {
35        &self.0
36    }
37}
38
39impl fmt::Display for Digest {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        formatter.write_str("sha256:")?;
42        for byte in self.0 {
43            write!(formatter, "{byte:02x}")?;
44        }
45        Ok(())
46    }
47}
48
49/// Incremental verifier for a descriptor payload.
50pub struct Verifier {
51    expected_digest: Digest,
52    expected_size: Option<u64>,
53    actual_size: u64,
54    hash: Sha256,
55}
56
57impl Verifier {
58    pub fn new(expected_digest: Digest, expected_size: u64) -> Self {
59        Self {
60            expected_digest,
61            expected_size: Some(expected_size),
62            actual_size: 0,
63            hash: Sha256::new(),
64        }
65    }
66
67    /// Creates a verifier for content whose size is not described separately.
68    pub fn digest_only(expected_digest: Digest) -> Self {
69        Self {
70            expected_digest,
71            expected_size: None,
72            actual_size: 0,
73            hash: Sha256::new(),
74        }
75    }
76
77    pub fn update(&mut self, bytes: &[u8]) -> Result<(), VerifyError> {
78        self.actual_size = self
79            .actual_size
80            .checked_add(bytes.len() as u64)
81            .ok_or(VerifyError::SizeOverflow)?;
82        self.hash.update(bytes);
83        Ok(())
84    }
85
86    pub const fn actual_size(&self) -> u64 {
87        self.actual_size
88    }
89
90    pub fn finish(&self) -> Result<(), VerifyError> {
91        if let Some(expected_size) = self.expected_size.filter(|size| *size != self.actual_size) {
92            return Err(VerifyError::SizeMismatch {
93                expected: expected_size,
94                actual: self.actual_size,
95            });
96        }
97        let actual = Digest::from_bytes(self.hash.clone().finalize().into());
98        if actual != self.expected_digest {
99            return Err(VerifyError::DigestMismatch {
100                expected: self.expected_digest,
101                actual,
102            });
103        }
104        Ok(())
105    }
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
109pub enum DigestError {
110    #[display("unsupported digest algorithm")]
111    UnsupportedAlgorithm,
112    #[display("invalid SHA-256 digest length")]
113    InvalidLength,
114    #[display("invalid SHA-256 digest encoding")]
115    InvalidEncoding,
116}
117
118#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
119pub enum VerifyError {
120    #[display("content size overflow")]
121    SizeOverflow,
122    #[display("size mismatch: expected {expected}, got {actual}")]
123    SizeMismatch { expected: u64, actual: u64 },
124    #[display("digest mismatch: expected {expected}, got {actual}")]
125    DigestMismatch { expected: Digest, actual: Digest },
126}
127
128fn hex(byte: u8) -> Result<u8, DigestError> {
129    match byte {
130        b'0'..=b'9' => Ok(byte - b'0'),
131        b'a'..=b'f' => Ok(byte - b'a' + 10),
132        _ => Err(DigestError::InvalidEncoding),
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use std::{format, string::ToString};
139
140    use super::{Digest, DigestError, Verifier, VerifyError};
141
142    #[test]
143    fn parses_and_formats_sha256() {
144        let text = "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
145        assert_eq!(Digest::parse(text).unwrap().to_string(), text);
146    }
147
148    #[test]
149    fn reports_digest_parse_errors() {
150        assert_eq!(
151            Digest::parse(
152                "sha512:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
153            ),
154            Err(DigestError::UnsupportedAlgorithm)
155        );
156        assert_eq!(Digest::parse("sha256:00"), Err(DigestError::InvalidLength));
157        assert_eq!(
158            Digest::parse(
159                "sha256:ga7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
160            ),
161            Err(DigestError::InvalidEncoding)
162        );
163
164        assert_eq!(
165            DigestError::UnsupportedAlgorithm.to_string(),
166            "unsupported digest algorithm"
167        );
168        assert_eq!(
169            DigestError::InvalidLength.to_string(),
170            "invalid SHA-256 digest length"
171        );
172        assert_eq!(
173            DigestError::InvalidEncoding.to_string(),
174            "invalid SHA-256 digest encoding"
175        );
176    }
177
178    #[test]
179    fn verifies_fragmented_content() {
180        let digest = Digest::parse(
181            "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
182        )
183        .unwrap();
184        let mut verifier = Verifier::new(digest, 3);
185        assert_eq!(verifier.actual_size(), 0);
186        verifier.update(b"a").unwrap();
187        assert_eq!(verifier.actual_size(), 1);
188        verifier.update(b"bc").unwrap();
189        assert_eq!(verifier.actual_size(), 3);
190        assert_eq!(verifier.finish(), Ok(()));
191
192        let mut verifier = Verifier::new(digest, 4);
193        verifier.update(b"abc").unwrap();
194        assert!(matches!(
195            verifier.finish(),
196            Err(VerifyError::SizeMismatch { .. })
197        ));
198    }
199
200    #[test]
201    fn formats_verification_errors() {
202        let expected = Digest::from_bytes([0x11; 32]);
203        let actual = Digest::from_bytes([0x22; 32]);
204
205        assert_eq!(
206            VerifyError::SizeOverflow.to_string(),
207            "content size overflow"
208        );
209        assert_eq!(
210            VerifyError::SizeMismatch {
211                expected: 7,
212                actual: 3,
213            }
214            .to_string(),
215            "size mismatch: expected 7, got 3"
216        );
217        assert_eq!(
218            VerifyError::DigestMismatch { expected, actual }.to_string(),
219            format!("digest mismatch: expected {expected}, got {actual}")
220        );
221    }
222}