about summary refs log tree commit diff
path: root/tvix/cli/src/derivation.rs
diff options
context:
space:
mode:
authorFlorian Klink <flokli@flokli.de>2023-01-26T13·18+0100
committerclbot <clbot@tvl.fyi>2023-01-27T14·06+0000
commita94a1434cc2a57b330a2ad6f310573fb70e15e8a (patch)
treebb9b4c32c03252ae32e409fe9bd893de19568f21 /tvix/cli/src/derivation.rs
parentbda5fc58d01d7513180da4456eb279a33f76bc1c (diff)
fix(tvix/cli): handle SRI hashes in outputHash r/5772
Instead of being called with `md5`, `sha1`, `sha256` or `sha512`,
`fetchurl.nix` (from corepkgs / `<nix`) can also be called with a `hash`
attribute, being an SRI hash.

In that case, `builtin.derivation` is called with `outputHashAlgo` being
an empty string, and `outputHash` being an SRI hash string.

In other cases, an SRI hash is passed as outputHash, but outputHashAlgo
is set too.

Nix does modify these values in (single, fixed) output specification it
serializes to ATerm, but keeps it unharmed in `env`.

Move this into a construct_output_hash helper function, that can be
tested better in isolation.

Change-Id: Id9d716a119664c44ea7747540399966752e20187
Reviewed-on: https://cl.tvl.fyi/c/depot/+/7933
Reviewed-by: tazjin <tazjin@tvl.su>
Autosubmit: flokli <flokli@flokli.de>
Tested-by: BuildkiteCI
Diffstat (limited to 'tvix/cli/src/derivation.rs')
-rw-r--r--tvix/cli/src/derivation.rs126
1 files changed, 109 insertions, 17 deletions
diff --git a/tvix/cli/src/derivation.rs b/tvix/cli/src/derivation.rs
index d57503a696d4..122330b963ef 100644
--- a/tvix/cli/src/derivation.rs
+++ b/tvix/cli/src/derivation.rs
@@ -1,5 +1,6 @@
 //! Implements `builtins.derivation`, the core of what makes Nix build packages.
 
+use data_encoding::BASE64;
 use std::cell::RefCell;
 use std::collections::{btree_map, BTreeSet};
 use std::rc::Rc;
@@ -79,6 +80,82 @@ fn populate_inputs<I: IntoIterator<Item = String>>(
     }
 }
 
+/// Due to the support for SRI hashes, and how these are passed along to
+/// builtins.derivation, outputHash and outputHashAlgo can have values which
+/// need to be further modified before constructing the Derivation struct.
+///
+/// If outputHashAlgo is an SRI hash, outputHashAlgo must either be an empty
+/// string, or the hash algorithm as specified in the (single) SRI (entry).
+/// SRI strings with multiple hash algorithms are not supported.
+///
+/// In case an SRI string was used, the (single) fixed output is populated
+/// with the hash algo name, and the hash digest is populated with the
+/// (lowercase) hex encoding of the digest.
+///
+/// These values are only rewritten for the outputs, not what's passed to env.
+fn construct_output_hash(digest: &str, algo: &str, hash_mode: Option<&str>) -> Result<Hash, Error> {
+    let sri_parsed = digest.parse::<ssri::Integrity>();
+    // SRI strings can embed multiple hashes with different algos, but that's probably not supported
+
+    let (digest, algo): (String, String) = match sri_parsed {
+        Err(e) => {
+            // unable to parse as SRI, but algo not set
+            if algo.is_empty() {
+                // InvalidSRIString doesn't implement PartialEq, but our error does
+                return Err(Error::InvalidSRIString(e.to_string()));
+            }
+
+            // algo is set. Assume the digest is set to some nixbase32.
+            // TODO: more validation here
+
+            (digest.to_string(), algo.to_string())
+        }
+        Ok(sri_parsed) => {
+            // We don't support more than one SRI hash
+            if sri_parsed.hashes.len() != 1 {
+                return Err(Error::UnsupportedSRIMultiple(sri_parsed.hashes.len()).into());
+            }
+
+            // grab the first (and only hash)
+            let sri_parsed_hash = &sri_parsed.hashes[0];
+
+            // ensure the algorithm in the SRI is supported
+            if !(sri_parsed_hash.algorithm == ssri::Algorithm::Sha1
+                || sri_parsed_hash.algorithm == ssri::Algorithm::Sha256
+                || sri_parsed_hash.algorithm == ssri::Algorithm::Sha512)
+            {
+                Error::UnsupportedSRIAlgo(sri_parsed_hash.algorithm.to_string());
+            }
+
+            // if algo is set, it needs to match what the SRI says
+            if !algo.is_empty() && algo != sri_parsed_hash.algorithm.to_string() {
+                return Err(Error::ConflictingSRIHashAlgo(
+                    algo.to_string(),
+                    sri_parsed_hash.algorithm.to_string(),
+                ));
+            }
+
+            // the digest comes base64-encoded. We need to decode, and re-encode as hexlower.
+            match BASE64.decode(sri_parsed_hash.digest.as_bytes()) {
+                Err(e) => return Err(Error::InvalidSRIDigest(e).into()),
+                Ok(sri_digest) => (
+                    data_encoding::HEXLOWER.encode(&sri_digest),
+                    sri_parsed_hash.algorithm.to_string(),
+                ),
+            }
+        }
+    };
+
+    // mutate the algo string a bit more, depending on hashMode
+    let algo = match hash_mode {
+        None | Some("flat") => algo,
+        Some("recursive") => format!("r:{}", algo),
+        Some(other) => return Err(Error::InvalidOutputHashMode(other.to_string()).into()),
+    };
+
+    Ok(Hash { algo, digest })
+}
+
 /// Populate the output configuration of a derivation based on the
 /// parameters passed to the call, flipping the required
 /// parameters for a fixed-output derivation if necessary.
@@ -102,6 +179,12 @@ fn populate_output_configuration(
                     .as_str()
                     .to_string();
 
+                let digest_str = hash
+                    .force(vm)?
+                    .coerce_to_string(CoercionKind::Strong, vm)?
+                    .as_str()
+                    .to_string();
+
                 let hash_mode = match hash_mode {
                     None => None,
                     Some(mode) => Some(
@@ -112,23 +195,12 @@ fn populate_output_configuration(
                     ),
                 };
 
-                let algo = match hash_mode.as_deref() {
-                    None | Some("flat") => algo,
-                    Some("recursive") => format!("r:{}", algo),
-                    Some(other) => {
-                        return Err(Error::InvalidOutputHashMode(other.to_string()).into())
-                    }
-                };
-
-                out.hash = Some(Hash {
-                    algo,
-
-                    digest: hash
-                        .force(vm)?
-                        .coerce_to_string(CoercionKind::Strong, vm)?
-                        .as_str()
-                        .to_string(),
-                });
+                // construct out.hash
+                out.hash = Some(construct_output_hash(
+                    &digest_str,
+                    &algo,
+                    hash_mode.as_deref(),
+                )?);
             }
         },
 
@@ -371,6 +443,7 @@ pub use derivation_builtins::builtins as derivation_builtins;
 #[cfg(test)]
 mod tests {
     use super::*;
+    use test_case::test_case;
     use tvix_eval::observer::NoOpObserver;
 
     static mut OBSERVER: NoOpObserver = NoOpObserver {};
@@ -576,4 +649,23 @@ mod tests {
             vec!["--foo".to_string(), "42".to_string(), "--bar".to_string()]
         );
     }
+
+    #[test_case(
+        "sha256-swapHA/ZO8QoDPwumMt6s5gf91oYe+oyk4EfRSyJqMg=", "sha256", Some("flat"),
+        Ok(Hash { algo: "sha256".to_string(), digest: "b306a91c0fd93bc4280cfc2e98cb7ab3981ff75a187bea3293811f452c89a8c8".to_string() });
+        "sha256 and SRI"
+    )]
+    #[test_case(
+        "sha256-s6JN6XqP28g1uYMxaVAQMLiXcDG8tUs7OsE3QPhGqzA=", "", Some("flat"),
+        Ok(Hash { algo: "sha256".to_string(), digest: "b3a24de97a8fdbc835b9833169501030b8977031bcb54b3b3ac13740f846ab30".to_string() });
+        "SRI only"
+    )]
+    fn test_construct_output_hash(
+        digest: &str,
+        algo: &str,
+        hash_mode: Option<&str>,
+        result: Result<Hash, Error>,
+    ) {
+        assert_eq!(construct_output_hash(digest, algo, hash_mode), result);
+    }
 }