use crate::error::{Error, Result};
use futures_io::AsyncRead;
use sigstore_crypto::{hash_async_reader, hash_reader, ArtifactHasher, SigningScheme};
use sigstore_types::{
Artifact, ArtifactDigest, HashAlgorithm, Sha256Hash, Sha512Hash, SignatureContent, Statement,
};
use std::io::Read;
pub(crate) struct ArtifactRequirements {
algorithms: Vec<HashAlgorithm>,
statement: Option<Statement>,
message_scheme: Option<SigningScheme>,
}
impl ArtifactRequirements {
pub(crate) fn new(content: &SignatureContent, scheme: SigningScheme) -> Result<Self> {
match content {
SignatureContent::MessageSignature(signature) => {
let mut algorithms = vec![HashAlgorithm::Sha2256]; for algorithm in signature
.message_digest
.as_ref()
.map(|d| d.algorithm)
.into_iter()
.chain(scheme.hash_algorithm())
{
if !algorithms.contains(&algorithm) {
algorithms.push(algorithm);
}
}
Ok(Self {
algorithms,
statement: None,
message_scheme: Some(scheme),
})
}
SignatureContent::DsseEnvelope(envelope) => {
if envelope.payload_type != "application/vnd.in-toto+json" {
return Err(Error::UnsupportedBundle(format!(
"unsupported DSSE payload type {:?}: cannot bind artifact to attestation",
envelope.payload_type
)));
}
let statement: Statement = serde_json::from_slice(envelope.payload.as_bytes())
.map_err(|e| {
Error::InvalidBundle(format!("failed to parse in-toto statement: {e}"))
})?;
if statement.subject.is_empty() {
return Err(Error::InvalidBundle(
"in-toto statement has no subjects: cannot bind artifact to attestation"
.into(),
));
}
let algorithms = statement.subject_algorithms();
if algorithms.is_empty() {
return Err(Error::UnsupportedBundle(
"in-toto statement has no supported subject digest algorithms".into(),
));
}
Ok(Self {
algorithms,
statement: Some(statement),
message_scheme: None,
})
}
_ => Err(Error::UnsupportedBundle(
"unsupported bundle signature content".into(),
)),
}
}
fn hashers(&self) -> Result<Vec<ArtifactHasher>> {
self.algorithms
.iter()
.copied()
.map(|algorithm| ArtifactHasher::new(algorithm).map_err(Error::from))
.collect()
}
fn check_reader(&self) -> Result<()> {
if let Some(scheme) = self.message_scheme {
if !scheme.supports_prehashed() {
return Err(Error::UnsupportedArtifact(format!("cannot verify signature from a digest or reader - scheme {} does not support prehashed mode", scheme.name())));
}
}
Ok(())
}
pub(crate) fn verify_binding(&self, artifact: &PreparedArtifact<'_>) -> Result<()> {
if let Some(statement) = &self.statement {
let matches = artifact
.sha256()
.is_ok_and(|digest| statement.matches_sha256(&digest))
|| artifact
.sha512()
.is_ok_and(|digest| statement.matches_sha512(&digest));
if !matches {
return Err(Error::ArtifactMismatch(
"artifact hash does not match any subject in attestation".into(),
));
}
}
Ok(())
}
}
pub(crate) struct PreparedArtifact<'a> {
blob: Option<&'a [u8]>,
digests: Vec<ArtifactDigest>,
}
impl<'a> PreparedArtifact<'a> {
pub(crate) fn from_artifact(
artifact: Artifact<'a>,
requirements: &ArtifactRequirements,
) -> Result<Self> {
Ok(match artifact {
Artifact::Blob(blob) => {
let mut hashers = requirements.hashers()?;
for hasher in &mut hashers {
hasher.update(blob);
}
Self {
blob: Some(blob),
digests: Self::finalize(hashers),
}
}
Artifact::Digest(digest) => Self {
blob: None,
digests: vec![digest],
},
other => {
return Err(Error::UnsupportedArtifact(format!(
"unsupported artifact input: {other:?}"
)))
}
})
}
pub(crate) fn from_reader(
reader: impl Read,
requirements: &ArtifactRequirements,
) -> Result<PreparedArtifact<'static>> {
requirements.check_reader()?;
let mut hashers = requirements.hashers()?;
hash_reader(reader, &mut hashers).map_err(Error::ArtifactRead)?;
Ok(Self::from_digests(Self::finalize(hashers)))
}
pub(crate) async fn from_async_reader(
reader: impl AsyncRead + Unpin,
requirements: &ArtifactRequirements,
) -> Result<PreparedArtifact<'static>> {
requirements.check_reader()?;
let mut hashers = requirements.hashers()?;
hash_async_reader(reader, &mut hashers)
.await
.map_err(Error::ArtifactRead)?;
Ok(Self::from_digests(Self::finalize(hashers)))
}
fn finalize(hashers: Vec<ArtifactHasher>) -> Vec<ArtifactDigest> {
hashers.into_iter().map(ArtifactHasher::finalize).collect()
}
fn from_digests(digests: Vec<ArtifactDigest>) -> PreparedArtifact<'static> {
PreparedArtifact {
blob: None,
digests,
}
}
pub(crate) fn blob(&self) -> Option<&[u8]> {
self.blob
}
pub(crate) fn digest(&self, algorithm: HashAlgorithm) -> Result<ArtifactDigest> {
self.digests
.iter()
.find(|digest| digest.algorithm() == algorithm)
.cloned()
.ok_or_else(|| {
let supplied = self
.digests
.iter()
.map(|digest| digest.algorithm().to_string())
.collect::<Vec<_>>()
.join(", ");
Error::UnsupportedArtifact(format!(
"verification requires an {algorithm} artifact digest; supplied: {supplied}"
))
})
}
pub(crate) fn sha256(&self) -> Result<Sha256Hash> {
Sha256Hash::try_from(self.digest(HashAlgorithm::Sha2256)?.as_bytes())
.map_err(|e| Error::UnsupportedArtifact(e.to_string()))
}
pub(crate) fn sha512(&self) -> Result<Sha512Hash> {
Sha512Hash::try_from(self.digest(HashAlgorithm::Sha2512)?.as_bytes())
.map_err(|e| Error::UnsupportedArtifact(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use sigstore_types::{DsseEnvelope, DsseSignature, PayloadBytes, SignatureBytes};
#[test]
fn scheme_and_subjects_select_the_required_digests() {
let message = SignatureContent::MessageSignature(sigstore_types::MessageSignature::new(
SignatureBytes::from_bytes(b"unused"),
));
let requirements =
ArtifactRequirements::new(&message, SigningScheme::EcdsaP384Sha384).unwrap();
let artifact = PreparedArtifact::from_reader(&b"hello"[..], &requirements).unwrap();
assert!(artifact.digest(HashAlgorithm::Sha2256).is_ok());
assert!(artifact.digest(HashAlgorithm::Sha2384).is_ok());
assert!(artifact.digest(HashAlgorithm::Sha2512).is_err());
let requirements = ArtifactRequirements::new(&message, SigningScheme::Ed25519).unwrap();
let mut reader = std::io::Cursor::new(b"do not read");
assert!(PreparedArtifact::from_reader(&mut reader, &requirements).is_err());
assert_eq!(reader.position(), 0);
let hash = sigstore_crypto::sha512(b"hello").to_hex();
let content = SignatureContent::DsseEnvelope(DsseEnvelope::new("application/vnd.in-toto+json", PayloadBytes::new(format!(r#"{{"_type":"https://in-toto.io/Statement/v1","subject":[{{"digest":{{"sha512":"{hash}"}}}}],"predicateType":"p","predicate":{{}}}}"#).into_bytes()), DsseSignature::new(SignatureBytes::from_bytes(b"unused"), Default::default())));
let requirements = ArtifactRequirements::new(&content, SigningScheme::Ed25519).unwrap();
assert_eq!(requirements.algorithms, vec![HashAlgorithm::Sha2512]);
let artifact = PreparedArtifact::from_reader(&b"hello"[..], &requirements).unwrap();
requirements.verify_binding(&artifact).unwrap();
let wrong = PreparedArtifact::from_reader(&b"other"[..], &requirements).unwrap();
assert!(requirements.verify_binding(&wrong).is_err());
}
}