From dd1e4fe49f511f01554bfaeb1275f03f90452913 Mon Sep 17 00:00:00 2001 From: Ernesto J Ocampo Date: Wed, 10 Jan 2024 15:09:25 +0000 Subject: [PATCH] Remove serde_jcs dependency and make Rekor signature verification more robust. --- Cargo.lock | 35 ++-- oak_attestation_verification/Cargo.toml | 2 +- oak_attestation_verification/src/rekor.rs | 179 ++++++++++++------ .../testdata/logentry.json | 36 +++- 4 files changed, 169 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e050fab10d..8258d6a116 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1868,7 +1868,7 @@ dependencies = [ "prost", "prost-build", "serde", - "serde_jcs", + "serde_canonical_json", "serde_json", "sha2", "time", @@ -3275,12 +3275,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" -[[package]] -name = "ryu-js" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" - [[package]] name = "s2" version = "0.0.12" @@ -3414,6 +3408,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_canonical_json" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef94ee2661f3ce924fa936258393d02155fa22c5a81125016a24069e23a0465" +dependencies = [ + "itoa", + "lazy_static", + "regex", + "serde_json", +] + [[package]] name = "serde_derive" version = "1.0.188" @@ -3425,22 +3431,11 @@ dependencies = [ "syn 2.0.41", ] -[[package]] -name = "serde_jcs" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cacecf649bc1a7c5f0e299cc813977c6a78116abda2b93b1ee01735b71ead9a8" -dependencies = [ - "ryu-js", - "serde", - "serde_json", -] - [[package]] name = "serde_json" -version = "1.0.93" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9" dependencies = [ "itoa", "ryu", diff --git a/oak_attestation_verification/Cargo.toml b/oak_attestation_verification/Cargo.toml index d0ccdebe02..49a88ae7ce 100644 --- a/oak_attestation_verification/Cargo.toml +++ b/oak_attestation_verification/Cargo.toml @@ -16,7 +16,7 @@ oak_dice = { workspace = true } prost = { workspace = true } p256 = { version = "*", features = ["ecdsa-core", "ecdsa", "pem"] } serde = { version = "*", features = ["derive"] } -serde_jcs = "*" +serde_canonical_json = "*" serde_json = "*" sha2 = { version = "*", default-features = false } time = { version = "0.3.28", features = ["serde", "parsing", "formatting"] } diff --git a/oak_attestation_verification/src/rekor.rs b/oak_attestation_verification/src/rekor.rs index d3663aae77..65ee5fe633 100644 --- a/oak_attestation_verification/src/rekor.rs +++ b/oak_attestation_verification/src/rekor.rs @@ -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, + // #[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, } /// Struct representing the body in a Rekor LogEntry. @@ -118,10 +120,11 @@ 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 { + // 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, + pub signed_data: Vec, /// The signature over the canonicalized JSON document. pub signature: Vec, @@ -129,41 +132,41 @@ pub struct RekorSignatureBundle { /// Converter for creating a RekorSignatureBundle from a Rekor LogEntry as described in /// . -impl TryFrom<&LogEntry> for RekorSignatureBundle { - type Error = anyhow::Error; - - fn try_from(log_entry: &LogEntry) -> anyhow::Result { - // 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 { +// // 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, +// }) +// } +// } /// Verifies a Rekor LogEntry. This includes verifying: /// @@ -202,12 +205,14 @@ pub fn get_rekor_log_entry_body(log_entry: &[u8]) -> anyhow::Result { /// 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 { - let parsed: BTreeMap = - 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 { let parsed: BTreeMap = +// 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 { + let mut log_entry_json = serde_json::from_slice::(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 + .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 + .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 + + 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"); + + let signed_json_bytes = serializer.into_inner(); + + let signed_entry_timestamp_base64_encoded = verification + .as_object() + .context("Expected 'verification' entry to contain a JSON object")?["signedEntryTimestamp"] + .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, + }) } diff --git a/oak_attestation_verification/testdata/logentry.json b/oak_attestation_verification/testdata/logentry.json index 6ecb7bc079..dab31d1201 100644 --- a/oak_attestation_verification/testdata/logentry.json +++ b/oak_attestation_verification/testdata/logentry.json @@ -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" + } + } +}