Skip to content

Commit

Permalink
Added merkle proof examples, fixed some bugs, reduced repetition and …
Browse files Browse the repository at this point in the history
…added some functionality.

Relates to: sigstore#283

Signed-off-by: Victor Embacher <victor@embacher.xyz>
  • Loading branch information
vembacher committed Mar 20, 2024
1 parent 0d3fc02 commit 7dd4224
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 55 deletions.
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,11 @@ path = "examples/rekor/search_log_query/main.rs"
[[example]]
name = "fulcio_cert"
path = "examples/fulcio/cert/main.rs"

[[example]]
name = "inclusion_proof"
path = "examples/rekor/merkle_proofs/inclusion.rs"

[[example]]
name = "consistency_proof"
path = "examples/rekor/merkle_proofs/consistency.rs"
46 changes: 46 additions & 0 deletions examples/rekor/merkle_proofs/consistency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use clap::Parser;
use sigstore::crypto::CosignVerificationKey;
use sigstore::rekor::apis::configuration::Configuration;
use sigstore::rekor::apis::tlog_api::{get_log_info, get_log_proof};
use std::fs::read_to_string;
use std::path::PathBuf;

#[derive(Parser)]
struct Args {
#[arg(long, value_name = "REKOR PUBLIC KEY")]
rekor_key: PathBuf,
#[arg(long, value_name = "HEX ENCODED HASH")]
old_root: String,
#[arg(long)]
old_size: usize,
#[arg(long, value_name = "TREE ID")]
tree_id: Option<String>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let tree_id = args.tree_id.as_ref().map(|s| s.as_str());
// read verification key
let rekor_key = read_to_string(&args.rekor_key)
.map_err(Into::into)
.and_then(|k| CosignVerificationKey::from_pem(k.as_bytes(), &Default::default()))?;

// fetch log info
let rekor_config = Configuration::default();
let log_info = get_log_info(&rekor_config).await?;

let proof = get_log_proof(
&rekor_config,
log_info.tree_size as _,
Some(&args.old_size.to_string()),
tree_id,
)
.await?;

log_info
.verify_consistency(args.old_size, &args.old_root, &proof, &rekor_key)
.expect("failed to verify log consistency");
println!("Successfully verified consistency");
Ok(())
}
35 changes: 35 additions & 0 deletions examples/rekor/merkle_proofs/inclusion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use clap::Parser;
use sigstore::crypto::CosignVerificationKey;
use sigstore::rekor::apis::configuration::Configuration;
use sigstore::rekor::apis::entries_api::get_log_entry_by_index;
use std::fs::read_to_string;
use std::path::PathBuf;

#[derive(Parser)]
struct Args {
#[arg(long, value_name = "INDEX")]
log_index: usize,
#[arg(long, value_name = "REKOR PUBLIC KEY")]
rekor_key: PathBuf,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();

// read verification key
let rekor_key = read_to_string(&args.rekor_key)
.map_err(Into::into)
.and_then(|k| CosignVerificationKey::from_pem(k.as_bytes(), &Default::default()))?;

// fetch entry from log
let rekor_config = Configuration::default();
let log_entry = get_log_entry_by_index(&rekor_config, args.log_index as i32).await?;

// verify inclusion with key
log_entry
.verify_inclusion(&rekor_key)
.expect("failed to verify log inclusion");
println!("Successfully verified inclusion.");
Ok(())
}
15 changes: 15 additions & 0 deletions src/crypto/merkle/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
pub mod proof_verification;
pub mod rfc6962;

use crate::errors::SigstoreError;
use crate::errors::SigstoreError::UnexpectedError;
use digest::Output;
pub use proof_verification::MerkleProofError;
pub(crate) use proof_verification::MerkleProofVerifier;
pub(crate) use rfc6962::{Rfc6269Default, Rfc6269HasherTrait};

/// Many rekor models have hex-encoded hashes, this functions helps to avoid repetition.
pub(crate) fn hex_to_hash_output(
h: impl AsRef<[u8]>,
) -> Result<Output<Rfc6269Default>, SigstoreError> {
hex::decode(h)
.map_err(Into::into)
.and_then(|h| {
<[u8; 32]>::try_from(h.as_slice()).map_err(|err| UnexpectedError(format!("{err:?}")))
})
.map(Into::into)
}
32 changes: 32 additions & 0 deletions src/rekor/models/checkpoint.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use crate::crypto::merkle::{MerkleProofVerifier, Rfc6269Default};
use crate::crypto::{CosignVerificationKey, Signature};
use crate::errors::SigstoreError;
use crate::errors::SigstoreError::{ConsistencyProofError, UnexpectedError};
use crate::rekor::models::checkpoint::ParseCheckpointError::*;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use digest::Output;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
Expand Down Expand Up @@ -153,6 +156,35 @@ impl SignedCheckpoint {
self.note.marshal().as_bytes(),
)
}

/// Checks if the checkpoint and inclusion proof are valid together.
pub(crate) fn valid_consistency_proof(
&self,
proof_root_hash: &Output<Rfc6269Default>,
proof_tree_size: u64,
) -> Result<(), SigstoreError> {
// Delegate implementation as trivial consistency proof.
Rfc6269Default::verify_consistency(
self.note.size as usize,
proof_tree_size as usize,
&[],
&self.note.hash.into(),
proof_root_hash,
)
.map_err(ConsistencyProofError)
}

/// Verifies that the checkpoint can be used for an inclusion proof with this root hash.
pub(crate) fn valid_inclusion_proof(
&self,
proof_root_hash: &Output<Rfc6269Default>,
) -> Result<(), SigstoreError> {
Rfc6269Default::verify_match(proof_root_hash, &self.note.hash.into()).map_err(|_| {
UnexpectedError(
"consistency proof root hash does not match checkpoint root hash".to_string(),
)
})
}
}

impl Serialize for SignedCheckpoint {
Expand Down
30 changes: 6 additions & 24 deletions src/rekor/models/consistency_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
* Generated by: https://openapi-generator.tech
*/

use crate::crypto::merkle::hex_to_hash_output;
use crate::crypto::merkle::{MerkleProofVerifier, Rfc6269Default};
use crate::errors::SigstoreError;
use crate::errors::SigstoreError::{ConsistencyProofError, UnexpectedError};
use crate::errors::SigstoreError::ConsistencyProofError;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
Expand All @@ -32,36 +34,16 @@ impl ConsistencyProof {
old_root: &str,
new_size: usize,
) -> Result<(), SigstoreError> {
use crate::crypto::merkle::{MerkleProofVerifier, Rfc6269Default};

// decode hashes from hex and convert them to the required data structure
// immediately return an error when conversion fails
let proof_hashes = self
.hashes
.iter()
.map(|h| {
hex::decode(h)
.map_err(Into::into) // failed to decode from hex
.and_then(|h| {
<[u8; 32]>::try_from(h).map_err(|err| UnexpectedError(format!("{err:?}")))
})
.map(Into::into)
})
.map(hex_to_hash_output)
.collect::<Result<Vec<_>, _>>()?;

let old_root = hex::decode(old_root)
.map_err(Into::into)
.and_then(|h| {
<[u8; 32]>::try_from(h).map_err(|err| UnexpectedError(format!("{err:?}")))
})
.map(Into::into)?;

let new_root = hex::decode(&self.root_hash)
.map_err(Into::into)
.and_then(|h| {
<[u8; 32]>::try_from(h).map_err(|err| UnexpectedError(format!("{err:?}")))
})
.map(Into::into)?;
let old_root = hex_to_hash_output(old_root)?;
let new_root = hex_to_hash_output(&self.root_hash)?;

Rfc6269Default::verify_consistency(old_size, new_size, &proof_hashes, &old_root, &new_root)
.map_err(ConsistencyProofError)
Expand Down
39 changes: 9 additions & 30 deletions src/rekor/models/inclusion_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
* Generated by: https://openapi-generator.tech
*/

use crate::crypto::merkle::{MerkleProofVerifier, Rfc6269Default, Rfc6269HasherTrait};
use crate::crypto::merkle::{
hex_to_hash_output, MerkleProofVerifier, Rfc6269Default, Rfc6269HasherTrait,
};
use crate::crypto::CosignVerificationKey;
use crate::errors::SigstoreError;
use crate::errors::SigstoreError::{InclusionProofError, UnexpectedError};
use crate::rekor::models::checkpoint::{CheckpointNote, SignedCheckpoint};
use crate::rekor::models::checkpoint::SignedCheckpoint;
use crate::rekor::TreeSize;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -62,9 +64,6 @@ impl InclusionProof {
"inclusion proof misses checkpoint".to_string(),
))?;

// make sure we don't just accept any random checkpoint
self.verify_checkpoint_sanity(&checkpoint.note)?;

// verify the checkpoint signature
checkpoint.verify_signature(rekor_key)?;

Expand All @@ -75,28 +74,13 @@ impl InclusionProof {
let proof_hashes = self
.hashes
.iter()
.map(|h| {
hex::decode(h)
.map_err(Into::into)
.and_then(|h| {
<[u8; 32]>::try_from(h).map_err(|err| UnexpectedError(format!("{err:?}")))
})
.map(Into::into)
})
.map(hex_to_hash_output)
.collect::<Result<Vec<_>, _>>()?;

let entry_hash = hex::decode(entry_hash)
.map_err(Into::into)
.and_then(|h| {
<[u8; 32]>::try_from(h).map_err(|err| UnexpectedError(format!("{err:?}")))
})
.map(Into::into)?;
let root_hash = hex::decode(&self.root_hash)
.map_err(Into::into)
.and_then(|h| {
<[u8; 32]>::try_from(h).map_err(|err| UnexpectedError(format!("{err:?}")))
})
.map(Into::into)?;
let root_hash = hex_to_hash_output(&self.root_hash)?;

// check if the inclusion and checkpoint match
checkpoint.valid_inclusion_proof(&root_hash)?;

Rfc6269Default::verify_inclusion(
self.log_index as usize,
Expand All @@ -107,9 +91,4 @@ impl InclusionProof {
)
.map_err(InclusionProofError)
}

/// verify that the checkpoint actually can be used to verify this inclusion proof
fn verify_checkpoint_sanity(&self, _note: &CheckpointNote) -> Result<(), SigstoreError> {
todo!()
}
}
2 changes: 1 addition & 1 deletion src/rekor/models/log_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ impl LogEntry {
&mut encoded_entry,
CanonicalFormatter::new(),
);
self.serialize(&mut ser)?;
self.body.serialize(&mut ser)?;
proof.verify(&encoded_entry, rekor_key)
})
}
Expand Down
22 changes: 22 additions & 0 deletions src/rekor/models/log_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
* Generated by: https://openapi-generator.tech
*/

use crate::crypto::merkle::hex_to_hash_output;
use crate::crypto::CosignVerificationKey;
use crate::errors::SigstoreError;
use crate::rekor::models::checkpoint::SignedCheckpoint;
use crate::rekor::models::ConsistencyProof;
use crate::rekor::TreeSize;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -44,4 +48,22 @@ impl LogInfo {
inactive_shards: None,
}
}

pub fn verify_consistency(
&self,
old_size: usize,
old_root: &str,
consistency_proof: &ConsistencyProof,
rekor_key: &CosignVerificationKey,
) -> Result<(), SigstoreError> {
// verify checkpoint is signed by log
self.signed_tree_head.verify_signature(rekor_key)?;

self.signed_tree_head.valid_consistency_proof(
&hex_to_hash_output(&self.root_hash)?,
self.tree_size as u64,
)?;
consistency_proof.verify(old_size, old_root, self.tree_size as _)?;
Ok(())
}
}

0 comments on commit 7dd4224

Please sign in to comment.