From 99dd502faff954138341884bf88d4464999ec824 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. --- oak_attestation_verification/src/rekor.rs | 65 +++++++++++++++++-- .../testdata/logentry.json | 36 +++++++++- 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/oak_attestation_verification/src/rekor.rs b/oak_attestation_verification/src/rekor.rs index d3663aae77c..79ca7fbdcfb 100644 --- a/oak_attestation_verification/src/rekor.rs +++ b/oak_attestation_verification/src/rekor.rs @@ -21,6 +21,7 @@ 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 crate::util::{convert_pem_to_raw, hash_sha2_256, verify_signature_raw}; @@ -118,10 +119,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, @@ -144,7 +146,7 @@ impl TryFrom<&LogEntry> for RekorSignatureBundle { // Canonicalized JSON document that is signed. Canonicalization should follow the RFC 8785 // rules. - let canonicalized = serde_jcs::to_vec(&entry_subset) + let signed_data = serde_jcs::to_vec(&entry_subset) .context("couldn't create canonicalized json string")?; // Extract the signature from the LogEntry. @@ -159,7 +161,7 @@ impl TryFrom<&LogEntry> for RekorSignatureBundle { .context("couldn't decode Base64 signature")?; Ok(Self { - canonicalized, + signed_data: signed_data, signature, }) } @@ -202,12 +204,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 +259,57 @@ 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")?; +// 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 signed_json_bytes = + serde_json::to_vec(log_entry_artifact_object).context("Could not serialize to JSON")?; + + 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 6ecb7bc079b..dab31d1201c 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" + } + } +}