ostree_ext/container/
skopeo.rs

1//! Fork skopeo as a subprocess
2
3use super::ImageReference;
4use anyhow::{Context, Result};
5use cap_std_ext::cmdext::CapStdExtCommandExt;
6use containers_image_proxy::oci_spec::image as oci_image;
7use fn_error_context::context;
8use io_lifetimes::OwnedFd;
9use serde::Deserialize;
10use std::io::Read;
11use std::path::Path;
12use std::process::Stdio;
13use std::str::FromStr;
14use tokio::process::Command;
15
16// See `man containers-policy.json` and
17// https://github.com/containers/image/blob/main/signature/policy_types.go
18// Ideally we add something like `skopeo pull --disallow-insecure-accept-anything`
19// but for now we parse the policy.
20const POLICY_PATH: &str = "/etc/containers/policy.json";
21const INSECURE_ACCEPT_ANYTHING: &str = "insecureAcceptAnything";
22
23#[derive(Deserialize)]
24struct PolicyEntry {
25    #[serde(rename = "type")]
26    ty: String,
27}
28#[derive(Deserialize)]
29struct ContainerPolicy {
30    default: Option<Vec<PolicyEntry>>,
31}
32
33impl ContainerPolicy {
34    fn is_default_insecure(&self) -> bool {
35        if let Some(default) = self.default.as_deref() {
36            match default.split_first() {
37                Some((v, &[])) => v.ty == INSECURE_ACCEPT_ANYTHING,
38                _ => false,
39            }
40        } else {
41            false
42        }
43    }
44}
45
46pub(crate) fn container_policy_is_default_insecure() -> Result<bool> {
47    let r = std::io::BufReader::new(std::fs::File::open(POLICY_PATH)?);
48    let policy: ContainerPolicy = serde_json::from_reader(r)?;
49    Ok(policy.is_default_insecure())
50}
51
52/// Create a Command builder for skopeo.
53pub(crate) fn new_cmd() -> std::process::Command {
54    let mut cmd = std::process::Command::new("skopeo");
55    cmd.stdin(Stdio::null());
56    cmd
57}
58
59/// Spawn the child process
60pub(crate) fn spawn(mut cmd: Command) -> Result<tokio::process::Child> {
61    let cmd = cmd.stdin(Stdio::null()).stderr(Stdio::piped());
62    cmd.spawn().context("Failed to exec skopeo")
63}
64
65/// Use skopeo to copy a container image.
66#[context("Skopeo copy")]
67pub async fn copy(
68    src: &ImageReference,
69    dest: &ImageReference,
70    authfile: Option<&Path>,
71    add_fd: Option<(std::sync::Arc<OwnedFd>, i32)>,
72    progress: bool,
73) -> Result<oci_image::Digest> {
74    let digestfile = tempfile::NamedTempFile::new()?;
75    let mut cmd = new_cmd();
76    cmd.arg("copy");
77    if !progress {
78        cmd.stdout(std::process::Stdio::null());
79    }
80    cmd.arg("--digestfile");
81    cmd.arg(digestfile.path());
82    if let Some((add_fd, n)) = add_fd {
83        cmd.take_fd_n(add_fd, n);
84    }
85    if let Some(authfile) = authfile {
86        cmd.arg("--authfile");
87        cmd.arg(authfile);
88    }
89    cmd.args(&[src.to_string(), dest.to_string()]);
90    let mut cmd = tokio::process::Command::from(cmd);
91    cmd.kill_on_drop(true);
92    let proc = super::skopeo::spawn(cmd)?;
93    let output = proc.wait_with_output().await?;
94    if !output.status.success() {
95        let stderr = String::from_utf8_lossy(&output.stderr);
96        return Err(anyhow::anyhow!("skopeo failed: {}\n", stderr));
97    }
98    let mut digestfile = digestfile.into_file();
99    let mut r = String::new();
100    digestfile.read_to_string(&mut r)?;
101    Ok(oci_image::Digest::from_str(r.trim())?)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    // Default value as of the Fedora 34 containers-common-1-21.fc34.noarch package.
109    const DEFAULT_POLICY: &str = indoc::indoc! {r#"
110    {
111        "default": [
112            {
113                "type": "insecureAcceptAnything"
114            }
115        ],
116        "transports":
117            {
118                "docker-daemon":
119                    {
120                        "": [{"type":"insecureAcceptAnything"}]
121                    }
122            }
123    }
124    "#};
125
126    // Stripped down copy from the manual.
127    const REASONABLY_LOCKED_DOWN: &str = indoc::indoc! { r#"
128    {
129        "default": [{"type": "reject"}],
130        "transports": {
131            "dir": {
132                "": [{"type": "insecureAcceptAnything"}]
133            },
134            "atomic": {
135                "hostname:5000/myns/official": [
136                    {
137                        "type": "signedBy",
138                        "keyType": "GPGKeys",
139                        "keyPath": "/path/to/official-pubkey.gpg"
140                    }
141                ]
142            }
143        }
144    }
145    "#};
146
147    #[test]
148    fn policy_is_insecure() {
149        let p: ContainerPolicy = serde_json::from_str(DEFAULT_POLICY).unwrap();
150        assert!(p.is_default_insecure());
151        for &v in &["{}", REASONABLY_LOCKED_DOWN] {
152            let p: ContainerPolicy = serde_json::from_str(v).unwrap();
153            assert!(!p.is_default_insecure());
154        }
155    }
156}