#![forbid(unsafe_code)]
#![forbid(missing_docs)]
use super::shasums::Shasums;
use anyhow::{
anyhow,
Result,
};
use bytes::{
Buf,
Bytes,
};
use pgp::composed::{
Deserializable,
StandaloneSignature,
};
use pgp::composed::signed_key::public::SignedPublicKey;
use std::io::BufReader;
use std::io::Cursor;
#[cfg(any(test, not(feature = "embed_gpg_key")))]
use std::io::prelude::*;
#[cfg(any(test, not(feature = "embed_gpg_key")))]
use std::fs::File;
#[cfg(any(test, not(feature = "embed_gpg_key")))]
use std::path::PathBuf;
#[cfg(any(test, not(feature = "embed_gpg_key")))]
const HASHICORP_GPG_KEY_FILENAME: &str = "hashicorp.asc";
#[cfg(feature = "embed_gpg_key")]
const HASHICORP_GPG_KEY: &str = include_str!("../gpg/hashicorp.asc");
#[derive(Debug)]
pub struct Signature {
public_key: SignedPublicKey,
signature: StandaloneSignature,
}
#[cfg(test)]
impl PartialEq for Signature {
fn eq(&self, other: &Self) -> bool {
let public_key_match = self.public_key == other.public_key;
let signature_match = self.signature.signature == other.signature.signature;
public_key_match && signature_match
}
}
impl Signature {
pub fn new(signature: Bytes) -> Result<Self> {
let public_key = get_public_key()?;
let signature = Self::with_public_key(
signature,
public_key,
)?;
Ok(signature)
}
pub fn with_public_key(signature: Bytes, public_key: String) -> Result<Self> {
let mut cursor = Cursor::new(public_key.as_bytes());
let public_key = SignedPublicKey::from_armor_single(&mut cursor)?;
let public_key = public_key.0;
let reader = BufReader::new(signature.reader());
let signature = StandaloneSignature::from_bytes(reader)?;
let signature = Self {
signature: signature,
public_key: public_key,
};
Ok(signature)
}
pub fn check(&self, shasums: &Shasums) -> Result<()> {
let shasums = shasums.content().as_bytes();
for subkey in &self.public_key.public_subkeys {
match self.signature.verify(&subkey, shasums) {
Err(_) => continue,
Ok(()) => return Ok(()),
}
}
match self.signature.verify(&self.public_key, shasums) {
Err(_) => Err(anyhow!("Couldn't verify signature")),
Ok(()) => Ok(()),
}
}
}
#[cfg(any(test, not(feature = "embed_gpg_key")))]
fn read_file_content(path: &PathBuf) -> Result<String> {
let file = File::open(&path)?;
let mut reader = BufReader::new(file);
let mut contents = String::new();
reader.read_to_string(&mut contents)?;
Ok(contents)
}
#[cfg(not(feature = "embed_gpg_key"))]
fn get_public_key_path() -> Result<PathBuf> {
let path = if cfg!(test) {
let test_data_dir = concat!(
env!("CARGO_MANIFEST_DIR"),
"/gpg/",
);
let mut path = PathBuf::new();
path.push(test_data_dir);
path.push(HASHICORP_GPG_KEY_FILENAME);
path
}
else {
let mut path = dirs::data_dir()
.ok_or_else(|| anyhow!("Couldn't find shared data directory"))?;
if !path.exists() || !path.is_dir() {
let msg = anyhow!(
"Data directory {} does not exist or is not a directory",
path.display(),
);
return Err(msg);
}
path = path.join(env!("CARGO_PKG_NAME"));
path = path.join(HASHICORP_GPG_KEY_FILENAME);
if !path.exists() || !path.is_file() {
let msg = format!(
"GPG key file {} does not exist or it not a file.\n\
Check https://www.hashicorp.com/security to find the GPG key",
path.display(),
);
return Err(anyhow!(msg));
}
path
};
Ok(path)
}
#[cfg(not(feature = "embed_gpg_key"))]
fn get_public_key() -> Result<String> {
let path = get_public_key_path()?;
let public_key = read_file_content(&path)?;
Ok(public_key)
}
#[cfg(feature = "embed_gpg_key")]
#[allow(clippy::unnecessary_wraps)]
fn get_public_key() -> Result<String> {
let public_key = HASHICORP_GPG_KEY.to_string();
Ok(public_key)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn read_file_bytes(path: &PathBuf) -> Result<Bytes> {
let file = File::open(&path)?;
let mut reader = BufReader::new(file);
let mut contents = Vec::new();
reader.read_to_end(&mut contents)?;
Ok(Bytes::from(contents))
}
#[test]
fn test_signature_check_ok() {
let gpg_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/gpg/",
);
let test_data_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/test-data/",
);
let gpg_key_file_path = Path::new(&format!(
"{}{}",
gpg_path,
HASHICORP_GPG_KEY_FILENAME,
)).to_path_buf();
let signature_file_path = Path::new(&format!(
"{}{}",
test_data_path,
"terraform_0.15.1_SHA256SUMS.sig",
)).to_path_buf();
let gpg_key_content = read_file_content(&gpg_key_file_path).unwrap();
let signature_content = read_file_bytes(&signature_file_path).unwrap();
let signature = Signature::with_public_key(
Bytes::from(signature_content),
gpg_key_content,
).unwrap();
let shasums_file_path = Path::new(&format!(
"{}{}",
test_data_path,
"terraform_0.15.1_SHA256SUMS",
)).to_path_buf();
let shasums_content = read_file_content(&shasums_file_path).unwrap();
let shasums = Shasums::new(shasums_content);
let res = signature.check(&shasums);
assert!(res.is_ok())
}
#[test]
fn test_signature_check_bad_public_key() {
let test_data_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/test-data/",
);
let signature_file_path = Path::new(&format!(
"{}{}",
test_data_path,
"terraform_0.15.1_SHA256SUMS.sig",
)).to_path_buf();
let signature_content = read_file_bytes(&signature_file_path).unwrap();
let signature = Signature::with_public_key(
Bytes::from(signature_content),
"bad".into(),
);
assert_eq!(
signature.unwrap_err().to_string(),
"io error: Custom { kind: Interrupted, error: \"incomplete parse\" }",
)
}
#[test]
fn test_signature_check_bad_signature() {
let gpg_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/gpg/",
);
let test_data_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/test-data/",
);
let gpg_key_file_path = Path::new(&format!(
"{}{}",
gpg_path,
HASHICORP_GPG_KEY_FILENAME,
)).to_path_buf();
let signature_file_path = Path::new(&format!(
"{}{}",
test_data_path,
"terraform_0.15.1_SHA256SUMS.sig",
)).to_path_buf();
let gpg_key_content = read_file_content(&gpg_key_file_path).unwrap();
let signature_content = read_file_bytes(&signature_file_path).unwrap();
let signature = Signature::with_public_key(
Bytes::from(signature_content),
gpg_key_content,
).unwrap();
let shasums_file_path = Path::new(&format!(
"{}{}",
test_data_path,
"test.txt",
)).to_path_buf();
let shasums_content = read_file_content(&shasums_file_path).unwrap();
let shasums = Shasums::new(shasums_content);
let res = signature.check(&shasums);
assert_eq!(
res.unwrap_err().to_string(),
"Couldn't verify signature",
)
}
#[test]
fn test_signature_check_known_bad_signature() {
let gpg_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/gpg/",
);
let test_data_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/test-data/",
);
let gpg_key_file_path = Path::new(&format!(
"{}{}",
gpg_path,
HASHICORP_GPG_KEY_FILENAME,
)).to_path_buf();
let signature_file_path = Path::new(&format!(
"{}{}",
test_data_path,
"terraform_0.12.26_SHA256SUMS.sig",
)).to_path_buf();
let gpg_key_content = read_file_content(&gpg_key_file_path).unwrap();
let signature_content = read_file_bytes(&signature_file_path).unwrap();
let signature = Signature::with_public_key(
Bytes::from(signature_content),
gpg_key_content,
).unwrap();
let shasums_file_path = Path::new(&format!(
"{}{}",
test_data_path,
"terraform_0.12.26_SHA256SUMS",
)).to_path_buf();
let shasums_content = read_file_content(&shasums_file_path).unwrap();
let shasums = Shasums::new(shasums_content);
let res = signature.check(&shasums);
assert_eq!(
res.unwrap_err().to_string(),
"Couldn't verify signature",
)
}
}