use crate::checkpoint::Checkpoint;
use crate::dsse::DsseEnvelope;
use crate::encoding::{
string_i64, CanonicalizedBody, DerCertificate, LogIndex, LogKeyId, Sha256Hash, SignatureBytes,
SignedTimestamp, TimestampToken,
};
use crate::error::{Error, Result};
use crate::hash::HashAlgorithm;
use serde::{Deserialize, Deserializer, Serialize};
use std::str::FromStr;
fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
where
D: Deserializer<'de>,
T: Default + Deserialize<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
fn is_zero(value: &i64) -> bool {
*value == 0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaType {
Bundle0_1,
Bundle0_2,
Bundle0_3,
}
impl MediaType {
pub fn as_str(&self) -> &'static str {
match self {
MediaType::Bundle0_1 => "application/vnd.dev.sigstore.bundle+json;version=0.1",
MediaType::Bundle0_2 => "application/vnd.dev.sigstore.bundle+json;version=0.2",
MediaType::Bundle0_3 => "application/vnd.dev.sigstore.bundle.v0.3+json",
}
}
}
impl FromStr for MediaType {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"application/vnd.dev.sigstore.bundle+json;version=0.1" => Ok(MediaType::Bundle0_1),
"application/vnd.dev.sigstore.bundle+json;version=0.2" => Ok(MediaType::Bundle0_2),
"application/vnd.dev.sigstore.bundle.v0.3+json" => Ok(MediaType::Bundle0_3),
"application/vnd.dev.sigstore.bundle+json;version=0.3" => Ok(MediaType::Bundle0_3),
_ => Err(Error::InvalidMediaType(s.to_string())),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BundleVersion {
#[serde(rename = "0.1")]
V0_1,
#[serde(rename = "0.2")]
V0_2,
#[serde(rename = "0.3")]
V0_3,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Bundle {
pub media_type: String,
pub verification_material: VerificationMaterial,
#[serde(flatten)]
pub content: SignatureContent,
}
impl Bundle {
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(Error::Json)
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(self).map_err(Error::Json)
}
pub fn to_json_pretty(&self) -> Result<String> {
serde_json::to_string_pretty(self).map_err(Error::Json)
}
pub fn version(&self) -> Result<MediaType> {
MediaType::from_str(&self.media_type)
}
pub fn signing_certificate(&self) -> Option<&DerCertificate> {
match &self.verification_material.content {
VerificationMaterialContent::Certificate(cert) => Some(&cert.raw_bytes),
VerificationMaterialContent::X509CertificateChain { certificates } => {
certificates.first().map(|c| &c.raw_bytes)
}
VerificationMaterialContent::PublicKey { .. } => None,
}
}
pub fn has_inclusion_proof(&self) -> bool {
self.verification_material
.tlog_entries
.iter()
.any(|e| e.inclusion_proof.is_some())
}
pub fn has_inclusion_promise(&self) -> bool {
self.verification_material
.tlog_entries
.iter()
.any(|e| e.inclusion_promise.is_some())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SignatureContent {
MessageSignature(MessageSignature),
DsseEnvelope(DsseEnvelope),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageSignature {
#[serde(skip_serializing_if = "Option::is_none")]
pub message_digest: Option<MessageDigest>,
pub signature: SignatureBytes,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageDigest {
pub algorithm: HashAlgorithm,
pub digest: Sha256Hash,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationMaterial {
#[serde(flatten)]
pub content: VerificationMaterialContent,
#[serde(default)]
pub tlog_entries: Vec<TransparencyLogEntry>,
#[serde(default, deserialize_with = "deserialize_null_as_default")]
pub timestamp_verification_data: TimestampVerificationData,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum VerificationMaterialContent {
Certificate(CertificateContent),
X509CertificateChain {
certificates: Vec<X509Certificate>,
},
PublicKey {
hint: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CertificateContent {
pub raw_bytes: DerCertificate,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct X509Certificate {
pub raw_bytes: DerCertificate,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransparencyLogEntry {
pub log_index: LogIndex,
pub log_id: LogId,
pub kind_version: KindVersion,
#[serde(default, with = "string_i64", skip_serializing_if = "is_zero")]
pub integrated_time: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub inclusion_promise: Option<InclusionPromise>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inclusion_proof: Option<InclusionProof>,
pub canonicalized_body: CanonicalizedBody,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogId {
pub key_id: LogKeyId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KindVersion {
pub kind: String,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InclusionPromise {
pub signed_entry_timestamp: SignedTimestamp,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InclusionProof {
pub log_index: LogIndex,
pub root_hash: Sha256Hash,
#[serde(with = "string_i64")]
pub tree_size: i64,
#[serde(with = "sha256_hash_vec")]
pub hashes: Vec<Sha256Hash>,
#[serde(default, skip_serializing_if = "CheckpointData::is_empty")]
pub checkpoint: CheckpointData,
}
mod sha256_hash_vec {
use super::Sha256Hash;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(hashes: &[Sha256Hash], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
hashes.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Sha256Hash>, D::Error>
where
D: Deserializer<'de>,
{
Vec::<Sha256Hash>::deserialize(deserializer)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CheckpointData {
#[serde(default)]
pub envelope: String,
}
impl CheckpointData {
pub fn parse(&self) -> Result<Checkpoint> {
Checkpoint::from_text(&self.envelope)
}
pub fn is_empty(&self) -> bool {
self.envelope.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TimestampVerificationData {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rfc3161_timestamps: Vec<Rfc3161Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Rfc3161Timestamp {
pub signed_timestamp: TimestampToken,
}
fn default_media_type() -> String {
"application/vnd.dev.sigstore.bundle+json;version=0.1".to_string()
}
impl<'de> Deserialize<'de> for Bundle {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct BundleHelper {
#[serde(default = "default_media_type")]
media_type: String,
verification_material: VerificationMaterial,
#[serde(flatten)]
content: SignatureContent,
}
let helper = BundleHelper::deserialize(deserializer)?;
Ok(Bundle {
media_type: helper.media_type,
verification_material: helper.verification_material,
content: helper.content,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_media_type_parsing() {
assert_eq!(
MediaType::from_str("application/vnd.dev.sigstore.bundle+json;version=0.1").unwrap(),
MediaType::Bundle0_1
);
assert_eq!(
MediaType::from_str("application/vnd.dev.sigstore.bundle+json;version=0.2").unwrap(),
MediaType::Bundle0_2
);
assert_eq!(
MediaType::from_str("application/vnd.dev.sigstore.bundle.v0.3+json").unwrap(),
MediaType::Bundle0_3
);
}
#[test]
fn test_media_type_invalid() {
assert!(MediaType::from_str("invalid").is_err());
}
}