use anyhow::{bail, Context, Result};
use std::fs::{metadata, set_permissions, OpenOptions};
use std::io::{self, Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::process::{Child, Command, Stdio};
use std::thread::{self, JoinHandle};
use tempfile::{self, TempDir};
#[derive(Debug)]
pub enum VerifyKeys {
Production,
#[cfg(test)]
InsecureTest,
}
#[derive(Debug)]
enum VerifyReport {
Stderr,
StderrOnSuccess,
Ignore,
}
pub struct VerifyReader<R: Read> {
typ: VerifyType<R>,
}
enum VerifyType<R: Read> {
None(R),
Gpg(GpgReader<R>),
}
impl<R: Read> VerifyReader<R> {
pub fn new(source: R, gpg_signature: Option<&[u8]>, keys: VerifyKeys) -> Result<Self> {
let typ = if let Some(signature) = gpg_signature {
VerifyType::Gpg(GpgReader::new(source, signature, keys)?)
} else {
VerifyType::None(source)
};
Ok(VerifyReader { typ })
}
pub fn verify(&mut self) -> Result<()> {
match &mut self.typ {
VerifyType::None(_) => (),
VerifyType::Gpg(reader) => reader.finish(VerifyReport::Stderr)?,
}
Ok(())
}
pub fn verify_without_logging_failure(&mut self) -> Result<()> {
match &mut self.typ {
VerifyType::None(_) => (),
VerifyType::Gpg(reader) => reader.finish(VerifyReport::StderrOnSuccess)?,
}
Ok(())
}
}
impl<R: Read> Read for VerifyReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match &mut self.typ {
VerifyType::None(reader) => reader.read(buf),
VerifyType::Gpg(reader) => reader.read(buf),
}
}
}
struct GpgReader<R: Read> {
_gpgdir: TempDir,
source: R,
child: Child,
stderr_thread: Option<JoinHandle<io::Result<Vec<u8>>>>,
}
impl<R: Read> GpgReader<R> {
fn new(source: R, signature: &[u8], keys: VerifyKeys) -> Result<Self> {
let gpgdir = tempfile::Builder::new()
.prefix("coreos-installer-")
.tempdir()
.context("creating temporary directory")?;
let meta = metadata(gpgdir.path()).context("getting metadata for temporary directory")?;
let mut permissions = meta.permissions();
permissions.set_mode(0o700);
set_permissions(gpgdir.path(), permissions)
.context("setting mode for temporary directory")?;
let keys = match keys {
VerifyKeys::Production => &include_bytes!("../signing-keys.asc")[..],
#[cfg(test)]
VerifyKeys::InsecureTest => {
&include_bytes!("../../fixtures/verify/test-key.pub.asc")[..]
}
};
let mut import = Command::new("gpg")
.arg("--homedir")
.arg(gpgdir.path())
.arg("--batch")
.arg("--quiet")
.arg("--import")
.stdin(Stdio::piped())
.spawn()
.context("running gpg --import")?;
import
.stdin
.as_mut()
.unwrap()
.write_all(keys)
.context("importing GPG keys")?;
if !import.wait().context("waiting for gpg --import")?.success() {
bail!("gpg --import failed");
}
let mut list = Command::new("gpg")
.arg("--homedir")
.arg(gpgdir.path())
.arg("--batch")
.arg("--list-keys")
.arg("--with-colons")
.stdout(Stdio::piped())
.spawn()
.context("running gpg --list-keys")?;
let mut list_output = String::new();
list.stdout
.as_mut()
.unwrap()
.read_to_string(&mut list_output)
.context("listing GPG keys")?;
if !list
.wait()
.context("waiting for gpg --list-keys")?
.success()
{
bail!("gpg --list-keys failed");
}
let mut trust: Vec<&str> = Vec::new();
for line in list_output.lines() {
let fields: Vec<&str> = line.split(':').collect();
if fields[0] != "pub" {
continue;
}
if fields.len() >= 5 {
trust.append(&mut vec!["--trusted-key", fields[4]]);
}
}
let trustdb = Command::new("gpg")
.arg("--homedir")
.arg(gpgdir.path())
.arg("--batch")
.arg("--check-trustdb")
.args(trust)
.output()
.context("running gpg --check-trustdb")?;
if !trustdb.status.success() {
eprint!("{}", String::from_utf8_lossy(&trustdb.stderr));
bail!("gpg --check-trustdb failed");
}
let mut signature_path = gpgdir.path().to_path_buf();
signature_path.push("signature");
let mut signature_file = OpenOptions::new()
.create_new(true)
.write(true)
.open(&signature_path)
.context("creating signature file")?;
signature_file
.write_all(signature)
.context("writing signature file")?;
let mut verify = Command::new("gpg")
.arg("--homedir")
.arg(gpgdir.path())
.arg("--batch")
.arg("--verify")
.arg(&signature_path)
.arg("-")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("running gpg --verify")?;
let mut stderr = verify.stderr.take().unwrap();
let stderr_thread = thread::Builder::new()
.name("gpg-stderr".into())
.spawn(move || -> io::Result<Vec<u8>> {
let mut buf = Vec::new();
stderr.read_to_end(&mut buf)?;
Ok(buf)
})
.context("spawning GPG stderr reader")?;
Ok(GpgReader {
_gpgdir: gpgdir,
source,
child: verify,
stderr_thread: Some(stderr_thread),
})
}
fn finish(&mut self, report: VerifyReport) -> io::Result<()> {
let wait_result = self.child.wait();
let join_result = self.stderr_thread.take().map(|t| t.join());
let success = wait_result?.success();
match join_result {
Some(Ok(Ok(stderr))) => match report {
VerifyReport::StderrOnSuccess if !success => (),
VerifyReport::Stderr | VerifyReport::StderrOnSuccess => {
eprint!("{}", String::from_utf8_lossy(&stderr))
}
VerifyReport::Ignore => (),
},
Some(Ok(Err(e))) => return Err(e),
Some(Err(e)) => std::panic::resume_unwind(e),
None => (),
}
if !success {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"GPG verification failure",
));
}
Ok(())
}
}
impl<R: Read> Read for GpgReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let count = self.source.read(buf)?;
if count > 0 {
self.child
.stdin
.as_mut()
.unwrap()
.write_all(&buf[0..count])?;
}
Ok(count)
}
}
impl<R: Read> Drop for GpgReader<R> {
fn drop(&mut self) {
self.finish(VerifyReport::Ignore).ok();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_good_signature() {
let data = include_bytes!("../../fixtures/verify/test-key.priv.asc");
let sig = include_bytes!("../../fixtures/verify/test-key.priv.asc.sig");
let mut reader =
VerifyReader::new(&data[..], Some(&sig[..]), VerifyKeys::InsecureTest).unwrap();
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
reader.verify().unwrap();
reader.verify().unwrap();
reader.verify_without_logging_failure().unwrap();
assert_eq!(&buf[..], &data[..]);
}
#[test]
fn test_bad_signature() {
let mut data = *include_bytes!("../../fixtures/verify/test-key.priv.asc");
let sig = include_bytes!("../../fixtures/verify/test-key.priv.asc.sig");
data[data.len() - 1] = b'!';
let mut reader =
VerifyReader::new(&data[..], Some(&sig[..]), VerifyKeys::InsecureTest).unwrap();
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
reader.verify().unwrap_err();
reader.verify().unwrap_err();
reader.verify_without_logging_failure().unwrap_err();
assert_eq!(&buf[..], &data[..]);
}
#[test]
fn test_truncated_data() {
let data = include_bytes!("../../fixtures/verify/test-key.priv.asc");
let sig = include_bytes!("../../fixtures/verify/test-key.priv.asc.sig");
let mut reader =
VerifyReader::new(&data[..1000], Some(&sig[..]), VerifyKeys::InsecureTest).unwrap();
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
reader.verify().unwrap_err();
reader.verify().unwrap_err();
reader.verify_without_logging_failure().unwrap_err();
assert_eq!(&buf[..], &data[..1000]);
}
#[test]
fn test_no_pubkey() {
let data = include_bytes!("../../fixtures/verify/test-key.priv.asc");
let sig = include_bytes!("../../fixtures/verify/test-key.priv.asc.random.sig");
let mut reader =
VerifyReader::new(&data[..], Some(&sig[..]), VerifyKeys::InsecureTest).unwrap();
let mut buf = Vec::new();
reader.read_to_end(&mut buf).unwrap();
reader.verify().unwrap_err();
reader.verify().unwrap_err();
reader.verify_without_logging_failure().unwrap_err();
assert_eq!(&buf[..], &data[..]);
}
}