-
Notifications
You must be signed in to change notification settings - Fork 107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove serde_jcs dependency #4641
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,10 @@ use alloc::{collections::BTreeMap, string::String, vec::Vec}; | |
use anyhow::Context; | ||
use base64::{prelude::BASE64_STANDARD, Engine as _}; | ||
use serde::{Deserialize, Serialize}; | ||
use serde_json::Value; | ||
|
||
use serde_canonical_json::CanonicalFormatter; | ||
use serde_json::Serializer; | ||
|
||
use crate::util::{convert_pem_to_raw, hash_sha2_256, verify_signature_raw}; | ||
|
||
|
@@ -31,24 +35,22 @@ pub struct LogEntry { | |
/// We cannot directly use the type `Body` here, since body is Base64-encoded. | ||
#[serde(rename = "body")] | ||
pub body: String, | ||
|
||
#[serde(rename = "integratedTime")] | ||
pub integrated_time: usize, | ||
|
||
/// This is the SHA256 hash of the DER-encoded public key for the log at the time the entry was | ||
/// included in the log | ||
/// Pattern: ^[0-9a-fA-F]{64}$ | ||
#[serde(rename = "logID")] | ||
pub log_id: String, | ||
|
||
/// Minimum: 0 | ||
#[serde(rename = "logIndex")] | ||
pub log_index: u64, | ||
|
||
/// Includes a signature over the body, integratedTime, logID, and logIndex. | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
#[serde(rename = "verification")] | ||
pub verification: Option<LogEntryVerification>, | ||
// #[serde(rename = "integratedTime")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is interesting, I guess these fields were just here for the canonicalization but we didn't actually need them for anything? In that case, do we think we may need any of them in the future (I cannot think of a reason, apart from perhaps logging the decoded log entry somewhere as part of the verification, which may actually be nice) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree that a little more convenience is better, be it only for logging. The trouble with the canonicalisation shouldn't lead to removing this fields. |
||
// pub integrated_time: usize, | ||
// This is the SHA256 hash of the DER-encoded public key for the log at the time the entry was | ||
// included in the log | ||
// Pattern: ^[0-9a-fA-F]{64}$ | ||
// #[serde(rename = "logID")] | ||
// pub log_id: String, | ||
|
||
// Minimum: 0 | ||
// #[serde(rename = "logIndex")] | ||
// pub log_index: u64, | ||
|
||
// Includes a signature over the body, integratedTime, logID, and logIndex. | ||
// #[serde(skip_serializing_if = "Option::is_none")] | ||
// #[serde(rename = "verification")] | ||
// pub verification: Option<LogEntryVerification>, | ||
} | ||
|
||
/// Struct representing the body in a Rekor LogEntry. | ||
|
@@ -118,52 +120,53 @@ pub struct LogEntryVerification { | |
/// be obtained from the `/api/v1/log/publicKey` Rest API. For `sigstore.dev`, it is a PEM-encoded | ||
/// x509/PKIX public key. | ||
pub struct RekorSignatureBundle { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The type RekorSignatureBundle is dispensable, it is just a pair of two relatively unrelated things. Also not used outside this file. NB: This is just my personal taste, keep if you think differently. |
||
// TODO: doc here | ||
/// Canonicalized JSON representation, based on RFC 8785 rules, of a subset of a Rekor LogEntry | ||
/// fields that are signed to generate `signedEntryTimestamp` (also a field in the Rekor | ||
/// LogEntry). These fields include body, integratedTime, logID and logIndex. | ||
pub canonicalized: Vec<u8>, | ||
pub signed_data: Vec<u8>, | ||
|
||
/// The signature over the canonicalized JSON document. | ||
pub signature: Vec<u8>, | ||
} | ||
|
||
/// Converter for creating a RekorSignatureBundle from a Rekor LogEntry as described in | ||
/// <https://github.com/sigstore/rekor/blob/4fcdcaa58fd5263560a82978d781eb64f5c5f93c/openapi.yaml#L433-L476>. | ||
impl TryFrom<&LogEntry> for RekorSignatureBundle { | ||
type Error = anyhow::Error; | ||
|
||
fn try_from(log_entry: &LogEntry) -> anyhow::Result<Self> { | ||
// Create a copy of the LogEntry, but skip the verification. | ||
let entry_subset = LogEntry { | ||
body: log_entry.body.clone(), | ||
integrated_time: log_entry.integrated_time, | ||
log_id: log_entry.log_id.clone(), | ||
log_index: log_entry.log_index, | ||
verification: None, | ||
}; | ||
|
||
// Canonicalized JSON document that is signed. Canonicalization should follow the RFC 8785 | ||
// rules. | ||
let canonicalized = serde_jcs::to_vec(&entry_subset) | ||
.context("couldn't create canonicalized json string")?; | ||
|
||
// Extract the signature from the LogEntry. | ||
let sig_base64 = log_entry | ||
.verification | ||
.as_ref() | ||
.ok_or_else(|| anyhow::anyhow!("no verification field in the log entry"))? | ||
.signed_entry_timestamp | ||
.clone(); | ||
let signature = BASE64_STANDARD | ||
.decode(sig_base64) | ||
.context("couldn't decode Base64 signature")?; | ||
|
||
Ok(Self { | ||
canonicalized, | ||
signature, | ||
}) | ||
} | ||
} | ||
// impl TryFrom<&LogEntry> for RekorSignatureBundle { | ||
// type Error = anyhow::Error; | ||
|
||
// fn try_from(log_entry: &LogEntry) -> anyhow::Result<Self> { | ||
// // Create a copy of the LogEntry, but skip the verification. | ||
// let entry_subset = LogEntry { | ||
// body: log_entry.body.clone(), | ||
// integrated_time: log_entry.integrated_time, | ||
// log_id: log_entry.log_id.clone(), | ||
// log_index: log_entry.log_index, | ||
// verification: None, | ||
// }; | ||
|
||
// // Canonicalized JSON document that is signed. Canonicalization should follow the RFC | ||
// 8785 // rules. | ||
// let signed_data = serde_jcs::to_vec(&entry_subset) | ||
// .context("couldn't create canonicalized json string")?; | ||
|
||
// // Extract the signature from the LogEntry. | ||
// let sig_base64 = log_entry | ||
// .verification | ||
// .as_ref() | ||
// .ok_or_else(|| anyhow::anyhow!("no verification field in the log entry"))? | ||
// .signed_entry_timestamp | ||
// .clone(); | ||
// let signature = BASE64_STANDARD | ||
// .decode(sig_base64) | ||
// .context("couldn't decode Base64 signature")?; | ||
|
||
// Ok(Self { | ||
// signed_data: signed_data, | ||
// signature, | ||
// }) | ||
// } | ||
// } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove commented code (assuming it's just the original version kept around for reference). |
||
|
||
/// Verifies a Rekor LogEntry. This includes verifying: | ||
/// | ||
|
@@ -202,12 +205,14 @@ pub fn get_rekor_log_entry_body(log_entry: &[u8]) -> anyhow::Result<Body> { | |
|
||
/// Parses a blob into a Rekor log entry and verifies the signature in | ||
/// `signedEntryTimestamp`` using Rekor's public key. | ||
/// | ||
/// TODO: specify what log_entry needs to contain. | ||
pub fn verify_rekor_signature(log_entry: &[u8], rekor_public_key: &[u8]) -> anyhow::Result<()> { | ||
let signature_bundle = rekor_signature_bundle(log_entry)?; | ||
|
||
verify_signature_raw( | ||
&signature_bundle.signature, | ||
&signature_bundle.canonicalized, | ||
&signature_bundle.signed_data, | ||
rekor_public_key, | ||
) | ||
.context("couldn't verify signedEntryTimestamp of the Rekor LogEntry") | ||
|
@@ -255,10 +260,62 @@ pub fn verify_rekor_body(body: &Body, contents_bytes: &[u8]) -> anyhow::Result<( | |
.context("couldn't verify signature over the endorsement") | ||
} | ||
|
||
fn rekor_signature_bundle(log_entry: &[u8]) -> anyhow::Result<RekorSignatureBundle> { | ||
let parsed: BTreeMap<String, LogEntry> = | ||
serde_json::from_slice(log_entry).context("couldn't parse bytes into a LogEntry object")?; | ||
let entry = parsed.values().next().context("no entry in the map")?; | ||
|
||
RekorSignatureBundle::try_from(entry) | ||
// TODO remove after tests. Keeping around for now to compare outputs from old and new. | ||
// fn rekor_signature_bundle_old(log_entry_json_bytes: &[u8]) -> | ||
// anyhow::Result<RekorSignatureBundle> { let parsed: BTreeMap<String, LogEntry> = | ||
// serde_json::from_slice(log_entry_json_bytes) .context("couldn't parse bytes into a | ||
// LogEntry object")?; let entry = parsed.values().next().context("no entry in the map")?; | ||
|
||
// RekorSignatureBundle::try_from(entry) | ||
// } | ||
|
||
/// For a sample of expected JSON structure, see ../testdata/logentry.json . | ||
fn rekor_signature_bundle(log_entry_json_bytes: &[u8]) -> anyhow::Result<RekorSignatureBundle> { | ||
let mut log_entry_json = serde_json::from_slice::<Value>(log_entry_json_bytes) | ||
.context("Couldn't parse bytes as JSON")?; | ||
|
||
let log_entry_root_object = log_entry_json | ||
.as_object_mut() | ||
.context("JSON root expected to be a JSON object")?; | ||
|
||
anyhow::ensure!( | ||
log_entry_root_object.len() == 1, | ||
"Expected exactly 1 entry in log entry root JSON object" | ||
); | ||
|
||
let log_entry_artifact_object = log_entry_root_object | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the method chain is quite long here, could you add a type annotation to help readers understand what type this is? |
||
.values_mut() | ||
.next() | ||
.unwrap() // Already ensured one item must exist. | ||
.as_object_mut() | ||
.context("Artifact metadata expected to be a JSON object")?; | ||
|
||
let verification: Value = log_entry_artifact_object | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you did the hard work of figuring out the actual logic upstream, it may be useful to document that more clearly here. |
||
.remove("verification") | ||
.context("'verification' key not found in artifact JSON")?; | ||
|
||
// TODO does this always equal canonicalized? | ||
// Note on preserving order: https://users.rust-lang.org/t/how-to-keep-order-after-using-serde-json-from-str-to-deserialize-a-string-to-struct/97727 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good question -- if the fields are reordered, does this still work? Either way, it may be useful to add a few more test cases to show that (some valid , some not) |
||
|
||
let mut serializer = Serializer::with_formatter(Vec::new(), CanonicalFormatter::new()); | ||
log_entry_artifact_object | ||
.serialize(&mut serializer) | ||
.expect("Failed to serialize Rekor signed payload to JSON"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. expect panics and does not return an error. Here the message should be returned as Result. |
||
|
||
let signed_json_bytes = serializer.into_inner(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make a private function which encapsulates Serializer and CanonicalFormatter? |
||
|
||
let signed_entry_timestamp_base64_encoded = verification | ||
.as_object() | ||
.context("Expected 'verification' entry to contain a JSON object")?["signedEntryTimestamp"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this will panic if it does not contain that field, which is probably undesirable There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I stumbled over the naming There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, SG! It's signed entry timestamp as in signed (entry timestamp), not (signed entry) timestamp 😂 . I'll add a comment. |
||
.as_str() | ||
.context("Expected 'signedEntryTimestamp' entry to contain a JSON string")?; | ||
|
||
let signed_entry_timestamp_bytes = BASE64_STANDARD | ||
.decode(signed_entry_timestamp_base64_encoded) | ||
.context("Couldn't Base64 decode signedEntryTimestap")?; | ||
|
||
Ok(RekorSignatureBundle { | ||
signed_data: signed_json_bytes, | ||
signature: signed_entry_timestamp_bytes, | ||
}) | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to use the pretty printed JSON I think (so that's good). Saving a few bytes of whitespace is of no importance. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,35 @@ | ||
{"24296fb24b8ad77a51d549703a3a1c2dd2639ba49617fc563854031cb93e6d354e7b005065c334a8":{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4ZTA2Nzk1ODA5NjM3ZTI3MjNmNjQzODE5MTQ3NzU4NGRhOTI2MjQ2MTZmMTI2MDViODIwZjg1NjUzMDcyYzA5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUQvaHJ3OWVjVlpHRE0zMUIycXE1dEdaNFZtSGNuRytDVml0NW93VURjK2RRSWdUQVI2V2FuY0ZaZUtuNzgwRmRTNkIxZ0cxakNlejFsTXZsTHFtdFBVc28wPSIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlVqUmlOUzlNWlZsNE9WZHpOMm93TVVZelNEUlJRVk5rYm1sVVF3cHhaakpJV1cxNEsyOVNLeklyVms1SmRtRllWRTVtVEU1WldHUTVTMVo0YW5OcllqRlVhMHMzU0VjeGVFVTVSMXA0ZW1waWQwWkRkWGxCUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19","integratedTime":1691754247,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d","logIndex":30891523,"verification":{"inclusionProof":{"checkpoint":"rekor.sigstore.dev - 2605736670972794746\n26728094\nUMgdlhClBSzBbqefL5wyKzDrSb/Wf1yic2lWuqc490o=\nTimestamp: 1691754248454344594\n\n— rekor.sigstore.dev wNI9ajBFAiEAuhZGCMHAXSu1zARzPjKeUQF4i1jY+45EmKodcfQofE4CICxsQ/OxAP0r+9wLT+1PzDuF/kZ5LJ44k6f0oueozZBf\n","hashes":["030b9e9fb6a20219790c620da0677ae9a9d551300d5d53677d2e889b18f93408","665e92da8bb3ecfec55d7084c2e680da9627140ceebd5b90443194974478aaae","d493bd198b0273aaadd90b15daae59f8c437ad7669b1ef0f35ee3bdfcccb0c1c","1c4f5d27f667cf8fdfab11719cb2700c43b6ec0699c0e906582f81cc5bbe627f","6f77ba99b9061179f7cf7d94ad3fafe88137ec4939f7a2855caf7a25b6b4c3eb","f72f831d5e9f5c86157f56bc850d0c505d0baa8af389c91689b5c002b83e47c3","85bbefe750579844c4ef01ba7e50ee147867768adf376df59a7d46d9061a0529","e9f1cc1f52ef6fafa3c87d2c2031f14ef16da2ac47c267601a97c1671307c313","91e4eaeb84796946c5ad1570ea06f4fd07ee9261526b696c251119d888e641e2","e4a5b55c06b38419780a1a1b34b7e1ab1329b55948a105df191ec58325ae6220","1127441051032e9e2b9a7ff43ac6a8d4133438354f8b295c37b23f6292569ff5","fda678e668dd9896f1cdbf160943a690da123917de48afe85edd6c494d08e1b9","5cf299a407ce2c41b16dc87bd3bc7396ba9426d1b5e43ba70bbd979e417c45e6","ba60819f9a3f9ddabeb6ec73d1ba79d04fd2ad69ce8d95c777ab485a9fadb36b","8d152ae03f0ef85238ed66f0f7ab3bc870aee2acd6531a4855fc5011ea6b0e67","ad712c98424de0f1284d4f144b8a95b5d22c181d4c0a246518e7a9a220bdf643"],"logIndex":26728092,"rootHash":"50c81d9610a5052cc16ea79f2f9c322b30eb49bfd67f5ca2736956baa738f74a","treeSize":26728094},"signedEntryTimestamp":"MEYCIQCCN9ip/cW7QfS4EbLyigCs4OKz4wcWUQThuQY00i3PZAIhAKsTz7epe3Gh/9XGLzh4L1yPqcGUCETPPckPvMIZbL/7"}}} | ||
{ | ||
"24296fb24b8ad77a51d549703a3a1c2dd2639ba49617fc563854031cb93e6d354e7b005065c334a8": { | ||
"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4ZTA2Nzk1ODA5NjM3ZTI3MjNmNjQzODE5MTQ3NzU4NGRhOTI2MjQ2MTZmMTI2MDViODIwZjg1NjUzMDcyYzA5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUQvaHJ3OWVjVlpHRE0zMUIycXE1dEdaNFZtSGNuRytDVml0NW93VURjK2RRSWdUQVI2V2FuY0ZaZUtuNzgwRmRTNkIxZ0cxakNlejFsTXZsTHFtdFBVc28wPSIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlVqUmlOUzlNWlZsNE9WZHpOMm93TVVZelNEUlJRVk5rYm1sVVF3cHhaakpJV1cxNEsyOVNLeklyVms1SmRtRllWRTVtVEU1WldHUTVTMVo0YW5OcllqRlVhMHMzU0VjeGVFVTVSMXA0ZW1waWQwWkRkWGxCUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19", | ||
"integratedTime": 1691754247, | ||
"logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", | ||
"logIndex": 30891523, | ||
"verification": { | ||
"inclusionProof": { | ||
"checkpoint": "rekor.sigstore.dev - 2605736670972794746\n26728094\nUMgdlhClBSzBbqefL5wyKzDrSb/Wf1yic2lWuqc490o=\nTimestamp: 1691754248454344594\n\n— rekor.sigstore.dev wNI9ajBFAiEAuhZGCMHAXSu1zARzPjKeUQF4i1jY+45EmKodcfQofE4CICxsQ/OxAP0r+9wLT+1PzDuF/kZ5LJ44k6f0oueozZBf\n", | ||
"hashes": [ | ||
"030b9e9fb6a20219790c620da0677ae9a9d551300d5d53677d2e889b18f93408", | ||
"665e92da8bb3ecfec55d7084c2e680da9627140ceebd5b90443194974478aaae", | ||
"d493bd198b0273aaadd90b15daae59f8c437ad7669b1ef0f35ee3bdfcccb0c1c", | ||
"1c4f5d27f667cf8fdfab11719cb2700c43b6ec0699c0e906582f81cc5bbe627f", | ||
"6f77ba99b9061179f7cf7d94ad3fafe88137ec4939f7a2855caf7a25b6b4c3eb", | ||
"f72f831d5e9f5c86157f56bc850d0c505d0baa8af389c91689b5c002b83e47c3", | ||
"85bbefe750579844c4ef01ba7e50ee147867768adf376df59a7d46d9061a0529", | ||
"e9f1cc1f52ef6fafa3c87d2c2031f14ef16da2ac47c267601a97c1671307c313", | ||
"91e4eaeb84796946c5ad1570ea06f4fd07ee9261526b696c251119d888e641e2", | ||
"e4a5b55c06b38419780a1a1b34b7e1ab1329b55948a105df191ec58325ae6220", | ||
"1127441051032e9e2b9a7ff43ac6a8d4133438354f8b295c37b23f6292569ff5", | ||
"fda678e668dd9896f1cdbf160943a690da123917de48afe85edd6c494d08e1b9", | ||
"5cf299a407ce2c41b16dc87bd3bc7396ba9426d1b5e43ba70bbd979e417c45e6", | ||
"ba60819f9a3f9ddabeb6ec73d1ba79d04fd2ad69ce8d95c777ab485a9fadb36b", | ||
"8d152ae03f0ef85238ed66f0f7ab3bc870aee2acd6531a4855fc5011ea6b0e67", | ||
"ad712c98424de0f1284d4f144b8a95b5d22c181d4c0a246518e7a9a220bdf643" | ||
], | ||
"logIndex": 26728092, | ||
"rootHash": "50c81d9610a5052cc16ea79f2f9c322b30eb49bfd67f5ca2736956baa738f74a", | ||
"treeSize": 26728094 | ||
}, | ||
"signedEntryTimestamp": "MEYCIQCCN9ip/cW7QfS4EbLyigCs4OKz4wcWUQThuQY00i3PZAIhAKsTz7epe3Gh/9XGLzh4L1yPqcGUCETPPckPvMIZbL/7" | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nit) remove blank line