libcoreinst/io/
hash.rs

1// Copyright 2019 CoreOS, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use anyhow::{bail, ensure, Context, Error, Result};
16use openssl::hash::{Hasher, MessageDigest};
17use openssl::sha;
18use serde::{Deserialize, Serialize};
19use serde_with::{DeserializeFromStr, SerializeDisplay};
20use std::fmt;
21use std::fs::OpenOptions;
22use std::io::{self, Read, Write};
23use std::os::unix::io::AsRawFd;
24use std::path::Path;
25use std::str::FromStr;
26
27/// Ignition-style message digests
28#[derive(Clone, Debug, DeserializeFromStr, SerializeDisplay, PartialEq, Eq)]
29pub enum IgnitionHash {
30    /// SHA-256 digest.
31    Sha256(Vec<u8>),
32    /// SHA-512 digest.
33    Sha512(Vec<u8>),
34}
35
36/// Digest implementation.  Helpfully, each digest in openssl::sha has a
37/// different type.
38enum IgnitionHasher {
39    Sha256(sha::Sha256),
40    Sha512(sha::Sha512),
41}
42
43impl FromStr for IgnitionHash {
44    type Err = Error;
45
46    /// Try to parse an hash-digest argument.
47    ///
48    /// This expects an input value following the `ignition.config.verification.hash`
49    /// spec, i.e. `<type>-<value>` format.
50    fn from_str(input: &str) -> Result<Self, Self::Err> {
51        let parts: Vec<_> = input.splitn(2, '-').collect();
52        if parts.len() != 2 {
53            bail!("failed to detect hash-type and digest in '{}'", input);
54        }
55        let (hash_kind, hex_digest) = (parts[0], parts[1]);
56
57        let hash = match hash_kind {
58            "sha256" => {
59                let digest = hex::decode(hex_digest).context("decoding hex digest")?;
60                ensure!(
61                    digest.len().saturating_mul(8) == 256,
62                    "wrong digest length ({})",
63                    digest.len().saturating_mul(8)
64                );
65                IgnitionHash::Sha256(digest)
66            }
67            "sha512" => {
68                let digest = hex::decode(hex_digest).context("decoding hex digest")?;
69                ensure!(
70                    digest.len().saturating_mul(8) == 512,
71                    "wrong digest length ({})",
72                    digest.len().saturating_mul(8)
73                );
74                IgnitionHash::Sha512(digest)
75            }
76            x => bail!("unknown hash type '{}'", x),
77        };
78
79        Ok(hash)
80    }
81}
82
83impl fmt::Display for IgnitionHash {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        let (kind, value) = match self {
86            Self::Sha256(v) => ("sha256", v),
87            Self::Sha512(v) => ("sha512", v),
88        };
89        write!(f, "{}-{}", kind, hex::encode(value))
90    }
91}
92
93impl IgnitionHash {
94    /// Digest and validate input data.
95    pub fn validate(&self, input: &mut impl Read) -> Result<()> {
96        let (mut hasher, digest) = match self {
97            IgnitionHash::Sha256(val) => (IgnitionHasher::Sha256(sha::Sha256::new()), val),
98            IgnitionHash::Sha512(val) => (IgnitionHasher::Sha512(sha::Sha512::new()), val),
99        };
100        let mut buf = [0u8; 128 * 1024];
101        loop {
102            match input.read(&mut buf) {
103                Ok(0) => break,
104                Ok(n) => match hasher {
105                    IgnitionHasher::Sha256(ref mut h) => h.update(&buf[..n]),
106                    IgnitionHasher::Sha512(ref mut h) => h.update(&buf[..n]),
107                },
108                Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
109                Err(e) => return Err(e).context("reading input"),
110            };
111        }
112        let computed = match hasher {
113            IgnitionHasher::Sha256(h) => h.finish().to_vec(),
114            IgnitionHasher::Sha512(h) => h.finish().to_vec(),
115        };
116
117        if &computed != digest {
118            bail!(
119                "hash mismatch, computed '{}' but expected '{}'",
120                hex::encode(computed),
121                hex::encode(digest),
122            );
123        }
124
125        Ok(())
126    }
127}
128
129#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
130pub struct Sha256Digest(pub [u8; 32]);
131
132impl TryFrom<Hasher> for Sha256Digest {
133    type Error = Error;
134
135    fn try_from(mut hasher: Hasher) -> std::result::Result<Self, Self::Error> {
136        let digest = hasher.finish().context("finishing hash")?;
137        Ok(Sha256Digest(
138            digest.as_ref().try_into().context("converting to SHA256")?,
139        ))
140    }
141}
142
143impl Sha256Digest {
144    /// Calculates the SHA256 of a file.
145    pub fn from_path(path: &Path) -> Result<Self> {
146        let mut f = OpenOptions::new()
147            .read(true)
148            .open(path)
149            .with_context(|| format!("opening {path:?}"))?;
150
151        Self::from_file(&mut f)
152    }
153
154    /// Calculates the SHA256 of an opened file. Note that the underlying file descriptor will have
155    /// `posix_fadvise` called on it to optimize for sequential reading.
156    pub fn from_file(f: &mut std::fs::File) -> Result<Self> {
157        // tell kernel to optimize for sequential reading
158        if unsafe { libc::posix_fadvise(f.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL) } < 0 {
159            eprintln!(
160                "posix_fadvise(SEQUENTIAL) failed (errno {}) -- ignoring...",
161                nix::errno::Errno::last_raw()
162            );
163        }
164
165        Self::from_reader(f)
166    }
167
168    /// Calculates the SHA256 of a reader.
169    pub fn from_reader(r: &mut impl Read) -> Result<Self> {
170        let mut hasher = Hasher::new(MessageDigest::sha256()).context("creating SHA256 hasher")?;
171        std::io::copy(r, &mut hasher)?;
172        hasher.try_into()
173    }
174
175    pub fn to_hex_string(&self) -> Result<String> {
176        let mut buf: Vec<u8> = Vec::with_capacity(64);
177        for i in 0..32 {
178            write!(buf, "{:02x}", self.0[i])?;
179        }
180        Ok(String::from_utf8(buf)?)
181    }
182}
183
184pub struct WriteHasher<W: Write> {
185    writer: W,
186    hasher: Hasher,
187}
188
189impl<W: Write> WriteHasher<W> {
190    pub fn new(writer: W, hasher: Hasher) -> Self {
191        WriteHasher { writer, hasher }
192    }
193
194    pub fn new_sha256(writer: W) -> Result<Self> {
195        let hasher = Hasher::new(MessageDigest::sha256()).context("creating SHA256 hasher")?;
196        Ok(WriteHasher { writer, hasher })
197    }
198}
199
200impl<W: Write> Write for WriteHasher<W> {
201    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
202        if buf.is_empty() {
203            return Ok(0);
204        }
205
206        let n = self.writer.write(buf)?;
207        self.hasher.write_all(&buf[..n])?;
208
209        Ok(n)
210    }
211
212    fn flush(&mut self) -> io::Result<()> {
213        self.writer.flush()?;
214        self.hasher.flush()?;
215        Ok(())
216    }
217}
218
219impl<W: Write> TryFrom<WriteHasher<W>> for Sha256Digest {
220    type Error = Error;
221
222    fn try_from(wrapper: WriteHasher<W>) -> std::result::Result<Self, Self::Error> {
223        Sha256Digest::try_from(wrapper.hasher)
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_ignition_hash_cli_parse() {
233        let err_cases = vec!["", "foo-bar", "-bar", "sha512", "sha512-", "sha512-00"];
234        for arg in err_cases {
235            IgnitionHash::from_str(arg).expect_err(&format!("input: {arg}"));
236        }
237
238        let null_digest = "sha512-cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e";
239        IgnitionHash::from_str(null_digest).unwrap();
240    }
241
242    #[test]
243    fn test_ignition_hash_validate() {
244        let input = vec![b'a', b'b', b'c'];
245        let hash_args = [
246            (true, "sha256-ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"),
247            (true, "sha512-ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"),
248            (false, "sha256-aa7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"),
249            (false, "sha512-cdaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f")
250        ];
251        for (valid, hash_arg) in &hash_args {
252            let hasher = IgnitionHash::from_str(hash_arg).unwrap();
253            let mut rd = std::io::Cursor::new(&input);
254            assert!(hasher.validate(&mut rd).is_ok() == *valid);
255        }
256    }
257}