From e2faff18782f4fd808cddce04faf5e4828f31229 Mon Sep 17 00:00:00 2001 From: Parvathi Bhogaraju Date: Tue, 16 Dec 2025 12:19:59 -0800 Subject: [PATCH 01/28] Add basic framework for the EAT verifier --- .gitignore | 3 +- ocp-eat-verifier/Cargo.toml | 12 ++++++++ ocp-eat-verifier/ocptoken-rs/Cargo.toml | 11 +++++++ ocp-eat-verifier/ocptoken-rs/src/error.rs | 21 ++++++++++++++ ocp-eat-verifier/ocptoken-rs/src/lib.rs | 4 +++ .../ocptoken-rs/src/token/evidence.rs | 29 +++++++++++++++++++ ocp-eat-verifier/ocptoken-rs/src/token/mod.rs | 3 ++ 7 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 ocp-eat-verifier/Cargo.toml create mode 100644 ocp-eat-verifier/ocptoken-rs/Cargo.toml create mode 100644 ocp-eat-verifier/ocptoken-rs/src/error.rs create mode 100644 ocp-eat-verifier/ocptoken-rs/src/lib.rs create mode 100644 ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs create mode 100644 ocp-eat-verifier/ocptoken-rs/src/token/mod.rs diff --git a/.gitignore b/.gitignore index 2ce236dcf..da83e56d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -/target*/ +*target*/ +**/target/ test_key # By default, ignore Cargo.lock files in non-workspace directories. diff --git a/ocp-eat-verifier/Cargo.toml b/ocp-eat-verifier/Cargo.toml new file mode 100644 index 000000000..5c2c4ce7d --- /dev/null +++ b/ocp-eat-verifier/Cargo.toml @@ -0,0 +1,12 @@ +# Licensed under the Apache-2.0 license + +[workspace] +members = [ + "ocptoken-rs", +] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +authors = ["Caliptra contributors"] \ No newline at end of file diff --git a/ocp-eat-verifier/ocptoken-rs/Cargo.toml b/ocp-eat-verifier/ocptoken-rs/Cargo.toml new file mode 100644 index 000000000..ded4f0b10 --- /dev/null +++ b/ocp-eat-verifier/ocptoken-rs/Cargo.toml @@ -0,0 +1,11 @@ +# Licensed under the Apache-2.0 license + +[package] +name = "ocptoken" +version.workspace = true +edition.workspace = true +authors.workspace = true + +[dependencies] +coset = "0.4.0" +thiserror = "2.0.17" diff --git a/ocp-eat-verifier/ocptoken-rs/src/error.rs b/ocp-eat-verifier/ocptoken-rs/src/error.rs new file mode 100644 index 000000000..eac20a8e7 --- /dev/null +++ b/ocp-eat-verifier/ocptoken-rs/src/error.rs @@ -0,0 +1,21 @@ +// Licensed under the Apache-2.0 license + +use thiserror::Error; + +/// Errors that can occur when working with OCP EAT tokens +#[derive(Error, Debug)] +pub enum OcpEatError { + /// COSE parsing or validation error + #[error("COSE error: {0:?}")] + CoseSign1(coset::CoseError), + // Other error variants can be added here as needed +} + +impl From for OcpEatError { + fn from(err: coset::CoseError) -> Self { + OcpEatError::CoseSign1(err) + } +} + +/// Result type for OCP EAT operations +pub type OcpEatResult = std::result::Result; \ No newline at end of file diff --git a/ocp-eat-verifier/ocptoken-rs/src/lib.rs b/ocp-eat-verifier/ocptoken-rs/src/lib.rs new file mode 100644 index 000000000..0fe6feae4 --- /dev/null +++ b/ocp-eat-verifier/ocptoken-rs/src/lib.rs @@ -0,0 +1,4 @@ +// Licensed under the Apache-2.0 license + +pub mod token; +pub mod error; \ No newline at end of file diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs new file mode 100644 index 000000000..93eb25d41 --- /dev/null +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license + + +use coset::{CborSerializable, CoseSign1, Header}; +use crate::error::{OcpEatError, OcpEatResult}; + +pub struct Evidence { + pub signed_eat: Option, + +} + +impl Default for Evidence { + fn default() -> Self { + Evidence { signed_eat: None } + } +} + +impl Evidence { + pub fn new(signed_eat: CoseSign1) -> Self { + Evidence { signed_eat: Some(signed_eat) } + } + pub fn decode(slice: &[u8]) -> OcpEatResult { + //1. Implement decoding logic here + //2. After decoding, parse the certificate from x5chain field in the unprotected header + // and extract public key + //3. Verify the signature of the CoseSign1 object using the public key + todo!("Implement decode logic here and set ocp_cwt in Evidence struct"); + } +} \ No newline at end of file diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/mod.rs b/ocp-eat-verifier/ocptoken-rs/src/token/mod.rs new file mode 100644 index 000000000..a44f65dfd --- /dev/null +++ b/ocp-eat-verifier/ocptoken-rs/src/token/mod.rs @@ -0,0 +1,3 @@ +// Licensed under the Apache-2.0 license + +pub mod evidence; From 752c1b0d8ba96d792dc5332f04af75dbefaeb90b Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Thu, 18 Dec 2025 14:22:52 -0800 Subject: [PATCH 02/28] first version change --- ocp-eat-verifier/ocptoken-rs/Cargo.toml | 1 + ocp-eat-verifier/ocptoken-rs/src/error.rs | 18 +++- .../ocptoken-rs/src/token/evidence.rs | 93 +++++++++++++++++-- 3 files changed, 100 insertions(+), 12 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/Cargo.toml b/ocp-eat-verifier/ocptoken-rs/Cargo.toml index ded4f0b10..74ae5e2ca 100644 --- a/ocp-eat-verifier/ocptoken-rs/Cargo.toml +++ b/ocp-eat-verifier/ocptoken-rs/Cargo.toml @@ -9,3 +9,4 @@ authors.workspace = true [dependencies] coset = "0.4.0" thiserror = "2.0.17" +openssl = { version = "0.10", features = ["vendored"] } diff --git a/ocp-eat-verifier/ocptoken-rs/src/error.rs b/ocp-eat-verifier/ocptoken-rs/src/error.rs index eac20a8e7..baf8d3ab9 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/error.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/error.rs @@ -1,14 +1,26 @@ // Licensed under the Apache-2.0 license -use thiserror::Error; +// error.rs +use thiserror::Error; /// Errors that can occur when working with OCP EAT tokens #[derive(Error, Debug)] pub enum OcpEatError { /// COSE parsing or validation error #[error("COSE error: {0:?}")] CoseSign1(coset::CoseError), - // Other error variants can be added here as needed + + /// Certificate parsing error + #[error("Certificate error: {0}")] + Certificate(String), + + /// Signature verification failure + #[error("Signature verification failed")] + SignatureVerification, + + /// Crypto backend error + #[error("Crypto error: {0}")] + Crypto(String), } impl From for OcpEatError { @@ -18,4 +30,4 @@ impl From for OcpEatError { } /// Result type for OCP EAT operations -pub type OcpEatResult = std::result::Result; \ No newline at end of file +pub type OcpEatResult = std::result::Result; diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index 93eb25d41..d2021e0bc 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -1,12 +1,12 @@ // Licensed under the Apache-2.0 license +use coset::{cbor::value::Value, CborSerializable, CoseSign1, Header}; +use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier, x509::X509}; -use coset::{CborSerializable, CoseSign1, Header}; use crate::error::{OcpEatError, OcpEatResult}; pub struct Evidence { pub signed_eat: Option, - } impl Default for Evidence { @@ -17,13 +17,88 @@ impl Default for Evidence { impl Evidence { pub fn new(signed_eat: CoseSign1) -> Self { - Evidence { signed_eat: Some(signed_eat) } + Evidence { + signed_eat: Some(signed_eat), + } } + + /// Decode a CBOR-encoded COSE_Sign1, extract the signing certificate + /// from unprotected header (label 33), and verify the ECDSA-P384 signature. pub fn decode(slice: &[u8]) -> OcpEatResult { - //1. Implement decoding logic here - //2. After decoding, parse the certificate from x5chain field in the unprotected header - // and extract public key - //3. Verify the signature of the CoseSign1 object using the public key - todo!("Implement decode logic here and set ocp_cwt in Evidence struct"); + // 1️ Decode CBOR → COSE_Sign1 + let cose = CoseSign1::from_slice(slice)?; + + // Payload must be present + cose.payload.as_deref().ok_or_else(|| { + OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem("nil", "bstr payload")) + })?; + + // 2 Extract certificate from unprotected header + let cert_der = extract_cert(&cose.unprotected)?; + + // Parse cert → public key + let pubkey = extract_pubkey_from_cert(cert_der)?; + + // 3 Verify signature using COSE API + cose.verify_signature(&[], |sig_structure, signature| { + verify_ecdsa_p384(&pubkey, sig_structure, signature) + })?; + + Ok(Evidence::new(cose)) } -} \ No newline at end of file +} + +/// Extract DER-encoded certificate from COSE x5chain header (label 33) +fn extract_cert(unprotected: &Header) -> OcpEatResult<&[u8]> { + unprotected + .rest + .iter() + .find_map(|(label, value)| { + if *label == coset::Label::Int(33) { + match value { + Value::Bytes(bytes) => Some(bytes.as_slice()), + _ => None, + } + } else { + None + } + }) + .ok_or_else(|| { + OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( + "x5chain missing", + "x5chain (label 33) in unprotected header", + )) + }) +} + +/// Parse X.509 cert and extract public key +fn extract_pubkey_from_cert(cert_der: &[u8]) -> OcpEatResult> { + let cert = X509::from_der(cert_der).map_err(|e| OcpEatError::Certificate(e.to_string()))?; + + cert.public_key() + .map_err(|e| OcpEatError::Certificate(e.to_string())) +} + +/// OpenSSL-backed ECDSA-P384 verification +fn verify_ecdsa_p384( + pubkey: &PKey, + sig_structure: &[u8], + signature: &[u8], +) -> OcpEatResult<()> { + let mut verifier = Verifier::new(MessageDigest::sha384(), pubkey) + .map_err(|e| OcpEatError::Crypto(e.to_string()))?; + + verifier + .update(sig_structure) + .map_err(|e| OcpEatError::Crypto(e.to_string()))?; + + let verified = verifier + .verify(signature) + .map_err(|e| OcpEatError::Crypto(e.to_string()))?; + + if verified { + Ok(()) + } else { + Err(OcpEatError::SignatureVerification) + } +} From 49bb2db6b21b7389cd42414006afb0105478dbd1 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Thu, 18 Dec 2025 14:30:52 -0800 Subject: [PATCH 03/28] Revert "first version change" This reverts commit 4b9b46dd14a175758efc6c0ee580ff1785663e1f. --- ocp-eat-verifier/ocptoken-rs/Cargo.toml | 1 - ocp-eat-verifier/ocptoken-rs/src/error.rs | 18 +--- .../ocptoken-rs/src/token/evidence.rs | 93 ++----------------- 3 files changed, 12 insertions(+), 100 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/Cargo.toml b/ocp-eat-verifier/ocptoken-rs/Cargo.toml index 74ae5e2ca..ded4f0b10 100644 --- a/ocp-eat-verifier/ocptoken-rs/Cargo.toml +++ b/ocp-eat-verifier/ocptoken-rs/Cargo.toml @@ -9,4 +9,3 @@ authors.workspace = true [dependencies] coset = "0.4.0" thiserror = "2.0.17" -openssl = { version = "0.10", features = ["vendored"] } diff --git a/ocp-eat-verifier/ocptoken-rs/src/error.rs b/ocp-eat-verifier/ocptoken-rs/src/error.rs index baf8d3ab9..eac20a8e7 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/error.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/error.rs @@ -1,26 +1,14 @@ // Licensed under the Apache-2.0 license -// error.rs - use thiserror::Error; + /// Errors that can occur when working with OCP EAT tokens #[derive(Error, Debug)] pub enum OcpEatError { /// COSE parsing or validation error #[error("COSE error: {0:?}")] CoseSign1(coset::CoseError), - - /// Certificate parsing error - #[error("Certificate error: {0}")] - Certificate(String), - - /// Signature verification failure - #[error("Signature verification failed")] - SignatureVerification, - - /// Crypto backend error - #[error("Crypto error: {0}")] - Crypto(String), + // Other error variants can be added here as needed } impl From for OcpEatError { @@ -30,4 +18,4 @@ impl From for OcpEatError { } /// Result type for OCP EAT operations -pub type OcpEatResult = std::result::Result; +pub type OcpEatResult = std::result::Result; \ No newline at end of file diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index d2021e0bc..93eb25d41 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -1,12 +1,12 @@ // Licensed under the Apache-2.0 license -use coset::{cbor::value::Value, CborSerializable, CoseSign1, Header}; -use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier, x509::X509}; +use coset::{CborSerializable, CoseSign1, Header}; use crate::error::{OcpEatError, OcpEatResult}; pub struct Evidence { pub signed_eat: Option, + } impl Default for Evidence { @@ -17,88 +17,13 @@ impl Default for Evidence { impl Evidence { pub fn new(signed_eat: CoseSign1) -> Self { - Evidence { - signed_eat: Some(signed_eat), - } + Evidence { signed_eat: Some(signed_eat) } } - - /// Decode a CBOR-encoded COSE_Sign1, extract the signing certificate - /// from unprotected header (label 33), and verify the ECDSA-P384 signature. pub fn decode(slice: &[u8]) -> OcpEatResult { - // 1️ Decode CBOR → COSE_Sign1 - let cose = CoseSign1::from_slice(slice)?; - - // Payload must be present - cose.payload.as_deref().ok_or_else(|| { - OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem("nil", "bstr payload")) - })?; - - // 2 Extract certificate from unprotected header - let cert_der = extract_cert(&cose.unprotected)?; - - // Parse cert → public key - let pubkey = extract_pubkey_from_cert(cert_der)?; - - // 3 Verify signature using COSE API - cose.verify_signature(&[], |sig_structure, signature| { - verify_ecdsa_p384(&pubkey, sig_structure, signature) - })?; - - Ok(Evidence::new(cose)) + //1. Implement decoding logic here + //2. After decoding, parse the certificate from x5chain field in the unprotected header + // and extract public key + //3. Verify the signature of the CoseSign1 object using the public key + todo!("Implement decode logic here and set ocp_cwt in Evidence struct"); } -} - -/// Extract DER-encoded certificate from COSE x5chain header (label 33) -fn extract_cert(unprotected: &Header) -> OcpEatResult<&[u8]> { - unprotected - .rest - .iter() - .find_map(|(label, value)| { - if *label == coset::Label::Int(33) { - match value { - Value::Bytes(bytes) => Some(bytes.as_slice()), - _ => None, - } - } else { - None - } - }) - .ok_or_else(|| { - OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( - "x5chain missing", - "x5chain (label 33) in unprotected header", - )) - }) -} - -/// Parse X.509 cert and extract public key -fn extract_pubkey_from_cert(cert_der: &[u8]) -> OcpEatResult> { - let cert = X509::from_der(cert_der).map_err(|e| OcpEatError::Certificate(e.to_string()))?; - - cert.public_key() - .map_err(|e| OcpEatError::Certificate(e.to_string())) -} - -/// OpenSSL-backed ECDSA-P384 verification -fn verify_ecdsa_p384( - pubkey: &PKey, - sig_structure: &[u8], - signature: &[u8], -) -> OcpEatResult<()> { - let mut verifier = Verifier::new(MessageDigest::sha384(), pubkey) - .map_err(|e| OcpEatError::Crypto(e.to_string()))?; - - verifier - .update(sig_structure) - .map_err(|e| OcpEatError::Crypto(e.to_string()))?; - - let verified = verifier - .verify(signature) - .map_err(|e| OcpEatError::Crypto(e.to_string()))?; - - if verified { - Ok(()) - } else { - Err(OcpEatError::SignatureVerification) - } -} +} \ No newline at end of file From ceceeb2053ab0b609e4a838cbb69e2d8f4ced3dd Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Thu, 18 Dec 2025 14:39:34 -0800 Subject: [PATCH 04/28] first version of decode --- ocp-eat-verifier/ocptoken-rs/Cargo.toml | 1 + ocp-eat-verifier/ocptoken-rs/src/error.rs | 18 +++- .../ocptoken-rs/src/token/evidence.rs | 93 +++++++++++++++++-- 3 files changed, 100 insertions(+), 12 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/Cargo.toml b/ocp-eat-verifier/ocptoken-rs/Cargo.toml index ded4f0b10..74ae5e2ca 100644 --- a/ocp-eat-verifier/ocptoken-rs/Cargo.toml +++ b/ocp-eat-verifier/ocptoken-rs/Cargo.toml @@ -9,3 +9,4 @@ authors.workspace = true [dependencies] coset = "0.4.0" thiserror = "2.0.17" +openssl = { version = "0.10", features = ["vendored"] } diff --git a/ocp-eat-verifier/ocptoken-rs/src/error.rs b/ocp-eat-verifier/ocptoken-rs/src/error.rs index eac20a8e7..baf8d3ab9 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/error.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/error.rs @@ -1,14 +1,26 @@ // Licensed under the Apache-2.0 license -use thiserror::Error; +// error.rs +use thiserror::Error; /// Errors that can occur when working with OCP EAT tokens #[derive(Error, Debug)] pub enum OcpEatError { /// COSE parsing or validation error #[error("COSE error: {0:?}")] CoseSign1(coset::CoseError), - // Other error variants can be added here as needed + + /// Certificate parsing error + #[error("Certificate error: {0}")] + Certificate(String), + + /// Signature verification failure + #[error("Signature verification failed")] + SignatureVerification, + + /// Crypto backend error + #[error("Crypto error: {0}")] + Crypto(String), } impl From for OcpEatError { @@ -18,4 +30,4 @@ impl From for OcpEatError { } /// Result type for OCP EAT operations -pub type OcpEatResult = std::result::Result; \ No newline at end of file +pub type OcpEatResult = std::result::Result; diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index 93eb25d41..9f8d15d98 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -1,12 +1,12 @@ // Licensed under the Apache-2.0 license +use coset::{cbor::value::Value, CborSerializable, CoseSign1, Header}; +use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier, x509::X509}; -use coset::{CborSerializable, CoseSign1, Header}; use crate::error::{OcpEatError, OcpEatResult}; pub struct Evidence { pub signed_eat: Option, - } impl Default for Evidence { @@ -17,13 +17,88 @@ impl Default for Evidence { impl Evidence { pub fn new(signed_eat: CoseSign1) -> Self { - Evidence { signed_eat: Some(signed_eat) } + Evidence { + signed_eat: Some(signed_eat), + } } + + /// Decode a CBOR-encoded COSE_Sign1, extract the signing certificate + /// from unprotected header (label 33), and verify the signature. pub fn decode(slice: &[u8]) -> OcpEatResult { - //1. Implement decoding logic here - //2. After decoding, parse the certificate from x5chain field in the unprotected header - // and extract public key - //3. Verify the signature of the CoseSign1 object using the public key - todo!("Implement decode logic here and set ocp_cwt in Evidence struct"); + // 1️ Decode CBOR → COSE_Sign1 + let cose = CoseSign1::from_slice(slice)?; + + // Payload must be present + cose.payload.as_deref().ok_or_else(|| { + OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem("nil", "bstr payload")) + })?; + + // 2 Extract certificate from unprotected header + let cert_der = extract_cert(&cose.unprotected)?; + + // Parse cert → public key + let pubkey = extract_pubkey_from_cert(cert_der)?; + + // 3 Verify signature using COSE API + cose.verify_signature(&[], |sig_structure, signature| { + verify_ecdsa_p384(&pubkey, sig_structure, signature) + })?; + + Ok(Evidence::new(cose)) } -} \ No newline at end of file +} + +/// Extract DER-encoded certificate from COSE x5chain header (label 33) +fn extract_cert(unprotected: &Header) -> OcpEatResult<&[u8]> { + unprotected + .rest + .iter() + .find_map(|(label, value)| { + if *label == coset::Label::Int(33) { + match value { + Value::Bytes(bytes) => Some(bytes.as_slice()), + _ => None, + } + } else { + None + } + }) + .ok_or_else(|| { + OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( + "x5chain missing", + "x5chain (label 33) in unprotected header", + )) + }) +} + +/// Parse X.509 cert and extract public key +fn extract_pubkey_from_cert(cert_der: &[u8]) -> OcpEatResult> { + let cert = X509::from_der(cert_der).map_err(|e| OcpEatError::Certificate(e.to_string()))?; + + cert.public_key() + .map_err(|e| OcpEatError::Certificate(e.to_string())) +} + +/// OpenSSL-backed ECDSA-P384 verification +fn verify_ecdsa_p384( + pubkey: &PKey, + sig_structure: &[u8], + signature: &[u8], +) -> OcpEatResult<()> { + let mut verifier = Verifier::new(MessageDigest::sha384(), pubkey) + .map_err(|e| OcpEatError::Crypto(e.to_string()))?; + + verifier + .update(sig_structure) + .map_err(|e| OcpEatError::Crypto(e.to_string()))?; + + let verified = verifier + .verify(signature) + .map_err(|e| OcpEatError::Crypto(e.to_string()))?; + + if verified { + Ok(()) + } else { + Err(OcpEatError::SignatureVerification) + } +} From 3279339a30248862616bea8a0188fb5b01122248 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Fri, 19 Dec 2025 09:19:02 -0800 Subject: [PATCH 05/28] add unit test --- .../ocptoken-rs/src/token/evidence.rs | 33 +++++--- .../ocptoken-rs/tests/test_evidence.rs | 79 +++++++++++++++++++ 2 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index 9f8d15d98..2c96c0625 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -4,11 +4,12 @@ use coset::{cbor::value::Value, CborSerializable, CoseSign1, Header}; use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier, x509::X509}; use crate::error::{OcpEatError, OcpEatResult}; +use openssl::bn::BigNum; +use openssl::ecdsa::EcdsaSig; pub struct Evidence { pub signed_eat: Option, } - impl Default for Evidence { fn default() -> Self { Evidence { signed_eat: None } @@ -24,27 +25,32 @@ impl Evidence { /// Decode a CBOR-encoded COSE_Sign1, extract the signing certificate /// from unprotected header (label 33), and verify the signature. + pub fn decode(slice: &[u8]) -> OcpEatResult { - // 1️ Decode CBOR → COSE_Sign1 + // 1. Decode COSE_Sign1 let cose = CoseSign1::from_slice(slice)?; - // Payload must be present + // Payload cose.payload.as_deref().ok_or_else(|| { OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem("nil", "bstr payload")) })?; - // 2 Extract certificate from unprotected header + // 2. Extract certificate let cert_der = extract_cert(&cose.unprotected)?; - // Parse cert → public key + // Extract public key let pubkey = extract_pubkey_from_cert(cert_der)?; - // 3 Verify signature using COSE API + // 3. Verify signature via COSE API cose.verify_signature(&[], |sig_structure, signature| { - verify_ecdsa_p384(&pubkey, sig_structure, signature) + verify_ecdsa_p384(&pubkey, sig_structure, signature).map_err(|_| { + coset::CoseError::UnexpectedItem("invalid signature", "valid COSE_Sign1 signature") + }) })?; - Ok(Evidence::new(cose)) + Ok(Evidence { + signed_eat: Some(cose), + }) } } @@ -85,6 +91,15 @@ fn verify_ecdsa_p384( sig_structure: &[u8], signature: &[u8], ) -> OcpEatResult<()> { + if signature.len() != 96 { + return Err(OcpEatError::SignatureVerification); + } + + let r = BigNum::from_slice(&signature[..48]).unwrap(); + let s = BigNum::from_slice(&signature[48..]).unwrap(); + let sig = EcdsaSig::from_private_components(r, s).unwrap(); + let der_sig = sig.to_der().unwrap(); + let mut verifier = Verifier::new(MessageDigest::sha384(), pubkey) .map_err(|e| OcpEatError::Crypto(e.to_string()))?; @@ -93,7 +108,7 @@ fn verify_ecdsa_p384( .map_err(|e| OcpEatError::Crypto(e.to_string()))?; let verified = verifier - .verify(signature) + .verify(&der_sig) .map_err(|e| OcpEatError::Crypto(e.to_string()))?; if verified { diff --git a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs new file mode 100644 index 000000000..6c4ed6415 --- /dev/null +++ b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs @@ -0,0 +1,79 @@ +// Licensed under the Apache-2.0 license + +use coset::{iana::Algorithm, CborSerializable, CoseSign1Builder, HeaderBuilder}; + +use openssl::{ + ec::{EcGroup, EcKey}, + hash::MessageDigest, + nid::Nid, + pkey::PKey, + sign::Signer, + x509::{X509NameBuilder, X509}, +}; + +use ocptoken::token::evidence::Evidence; + +use coset::cbor::value::Value; + +use openssl::ecdsa::EcdsaSig; + +#[test] +fn decode_and_verify_cose_sign1() { + // 1️ Generate ECC P-384 key pair + let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap(); + let ec_key = EcKey::generate(&group).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + + // 2️ Create X.509 certificate from public key + let mut name = X509NameBuilder::new().unwrap(); + name.append_entry_by_text("CN", "test-cert").unwrap(); + let name = name.build(); + + let mut cert_builder = X509::builder().unwrap(); + cert_builder.set_version(2).unwrap(); + cert_builder.set_subject_name(&name).unwrap(); + cert_builder.set_issuer_name(&name).unwrap(); + cert_builder.set_pubkey(&pkey).unwrap(); + cert_builder.sign(&pkey, MessageDigest::sha384()).unwrap(); + + let cert = cert_builder.build(); + let cert_der = cert.to_der().unwrap(); + + // 3️ Dummy payload + let payload = b"dummy payload for COSE signature"; + + // 4️ Create COSE_Sign1 structure + let cose = CoseSign1Builder::new() + .payload(payload.to_vec()) + .protected(HeaderBuilder::new().algorithm(Algorithm::ES384).build()) + .unprotected( + HeaderBuilder::new() + // x5chain = label 33 + .value(33, Value::Array(vec![Value::Bytes(cert_der.clone())])) + .build(), + ) + .create_signature(&[], |msg| { + let mut signer = Signer::new(MessageDigest::sha384(), &pkey).unwrap(); + signer.update(msg).unwrap(); + + let der_sig = signer.sign_to_vec().unwrap(); + + // DER → raw r||s (COSE format) + let sig = EcdsaSig::from_der(&der_sig).unwrap(); + let r = sig.r().to_vec_padded(48).unwrap(); + let s = sig.s().to_vec_padded(48).unwrap(); + [r, s].concat() + }) + .build(); + + // 5️ Encode to CBOR + let encoded = cose.to_vec().unwrap(); + + // 6️cl Decode + verify + let evidence = Evidence::decode(&encoded); + + assert!( + evidence.is_ok(), + "COSE_Sign1 signature verification should succeed" + ); +} From dad97539e17b27e1e08dafa107c41b312a0090be Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Fri, 19 Dec 2025 09:22:55 -0800 Subject: [PATCH 06/28] unit test --- ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs index 6c4ed6415..09184304e 100644 --- a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs @@ -69,7 +69,7 @@ fn decode_and_verify_cose_sign1() { // 5️ Encode to CBOR let encoded = cose.to_vec().unwrap(); - // 6️cl Decode + verify + // 6️ Decode + verify let evidence = Evidence::decode(&encoded); assert!( From 95772d380fdc6c1a8abc7200bbcc274ce0484410 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Fri, 19 Dec 2025 14:32:01 -0800 Subject: [PATCH 07/28] some code for debugging --- ocp-eat-verifier/ocptoken-rs/Cargo.toml | 2 + .../ocptoken-rs/src/token/evidence.rs | 226 ++++++++++++++---- 2 files changed, 178 insertions(+), 50 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/Cargo.toml b/ocp-eat-verifier/ocptoken-rs/Cargo.toml index 74ae5e2ca..e665ceb8f 100644 --- a/ocp-eat-verifier/ocptoken-rs/Cargo.toml +++ b/ocp-eat-verifier/ocptoken-rs/Cargo.toml @@ -10,3 +10,5 @@ authors.workspace = true coset = "0.4.0" thiserror = "2.0.17" openssl = { version = "0.10", features = ["vendored"] } +x509-parser = "0.16" + diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index 2c96c0625..73636f84b 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -1,15 +1,24 @@ // Licensed under the Apache-2.0 license -use coset::{cbor::value::Value, CborSerializable, CoseSign1, Header}; -use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier, x509::X509}; +use coset::{ + cbor::value::Value, + iana::{Algorithm, CoapContentFormat}, + sig_structure_data, CborSerializable, CoseSign1, Header, RegisteredLabel, +}; +use openssl::{ + bn::{BigNum, BigNumContext}, + ec::{EcGroup, EcKey, EcPoint}, + nid::Nid, + x509::X509, +}; use crate::error::{OcpEatError, OcpEatResult}; -use openssl::bn::BigNum; -use openssl::ecdsa::EcdsaSig; +/// Parsed and verified EAT evidence pub struct Evidence { pub signed_eat: Option, } + impl Default for Evidence { fn default() -> Self { Evidence { signed_eat: None } @@ -23,30 +32,45 @@ impl Evidence { } } - /// Decode a CBOR-encoded COSE_Sign1, extract the signing certificate - /// from unprotected header (label 33), and verify the signature. - + /// Decode and verify a COSE_Sign1 pub fn decode(slice: &[u8]) -> OcpEatResult { - // 1. Decode COSE_Sign1 + // 1. Parse COSE_Sign1 let cose = CoseSign1::from_slice(slice)?; - // Payload - cose.payload.as_deref().ok_or_else(|| { + // 2. Verify protected header (ES384 + EatCwt) + + verify_protected_header(&cose.protected.header)?; + + // 3. Extract payload + + let payload = cose.payload.as_deref().ok_or_else(|| { OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem("nil", "bstr payload")) })?; - // 2. Extract certificate - let cert_der = extract_cert(&cose.unprotected)?; + // 4. Extract leaf cert DER from x5chain - // Extract public key - let pubkey = extract_pubkey_from_cert(cert_der)?; + // 5. Extract raw EC public key (x, y) - // 3. Verify signature via COSE API - cose.verify_signature(&[], |sig_structure, signature| { - verify_ecdsa_p384(&pubkey, sig_structure, signature).map_err(|_| { - coset::CoseError::UnexpectedItem("invalid signature", "valid COSE_Sign1 signature") - }) - })?; + let cert_der = extract_leaf_cert_der(&cose.unprotected)?; + println!("extract_pubkey_xy"); + + dump_cert_der(&cert_der); + + let (pubkey_x, pubkey_y) = extract_pubkey_xy(&cert_der)?; + + // 6. Reconstruct COSE Sig_structure + let message = sig_structure_data( + coset::SignatureContext::CoseSign1, + cose.protected.clone(), + None, + &[], + payload, + ); + + println!("verify_signature_es384"); + + // 7. Verify ES384 signature + verify_signature_es384(&cose.signature, pubkey_x, pubkey_y, &message)?; Ok(Evidence { signed_eat: Some(cose), @@ -54,61 +78,163 @@ impl Evidence { } } -/// Extract DER-encoded certificate from COSE x5chain header (label 33) -fn extract_cert(unprotected: &Header) -> OcpEatResult<&[u8]> { - unprotected +/* -------------------------------------------------------------------------- */ +/* Helper functions */ +/* -------------------------------------------------------------------------- */ + +use std::fs::File; +use std::io::Write; + +fn dump_cert_der(cert_der: &[u8]) { + let mut f = File::create("x5chain_cert.der").expect("failed to create x5chain_cert.der"); + f.write_all(cert_der).expect("failed to write cert DER"); + println!( + "Wrote x5chain cert to x5chain_cert.der ({} bytes)", + cert_der.len() + ); +} + +fn verify_protected_header(protected: &Header) -> OcpEatResult<()> { + if protected.alg + != Some(coset::RegisteredLabelWithPrivate::Assigned( + Algorithm::ES384, + )) + { + println!("alg is not ES384"); + return Err(OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( + "alg", "ES384", + ))); + } + + Ok(()) +} + +/// Extract leaf certificate DER from x5chain (label 33) +fn extract_leaf_cert_der(unprotected: &Header) -> OcpEatResult> { + let value = unprotected .rest .iter() .find_map(|(label, value)| { if *label == coset::Label::Int(33) { - match value { - Value::Bytes(bytes) => Some(bytes.as_slice()), - _ => None, - } + Some(value) } else { None } }) .ok_or_else(|| { - OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( - "x5chain missing", - "x5chain (label 33) in unprotected header", - )) - }) + OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem("x5chain", "present")) + })?; + + match value { + Value::Array(arr) => arr.first(), + Value::Bytes(_) => Some(value), + _ => None, + } + .and_then(|v| match v { + Value::Bytes(bytes) => Some(bytes.clone()), // 👈 CLONE + _ => None, + }) + .ok_or_else(|| { + OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( + "x5chain", + "DER cert bytes", + )) + }) } -/// Parse X.509 cert and extract public key -fn extract_pubkey_from_cert(cert_der: &[u8]) -> OcpEatResult> { - let cert = X509::from_der(cert_der).map_err(|e| OcpEatError::Certificate(e.to_string()))?; +use x509_parser::prelude::*; + +/// Extract raw P-384 public key coordinates (x, y) from DER X.509 cert +fn extract_pubkey_xy(cert_der: &[u8]) -> OcpEatResult<([u8; 48], [u8; 48])> { + // Parse X.509 certificate (pure Rust, lenient) + let cert = match X509Certificate::from_der(cert_der) { + Ok((rem, cert)) => { + println!("X509Certificate::from_der OK"); + println!("Remaining bytes after parse: {}", rem.len()); + cert + } + Err(e) => { + println!("X509Certificate::from_der FAILED"); + println!("Error: {:?}", e); + + return Err(OcpEatError::Certificate(format!( + "X509 parse error: {:?}", + e + ))); + } + }; + + println!(" X509Certificate::from_der"); - cert.public_key() - .map_err(|e| OcpEatError::Certificate(e.to_string())) + let spki = &cert.tbs_certificate.subject_pki; + + // subject_public_key is a BIT STRING + let pubkey_bytes = spki.subject_public_key.data.as_ref(); + + // Expect uncompressed EC point: 04 || X || Y (P-384) + if pubkey_bytes.len() != 1 + 48 + 48 { + return Err(OcpEatError::Certificate(format!( + "unexpected EC public key length: {}", + pubkey_bytes.len() + ))); + } + + if pubkey_bytes[0] != 0x04 { + return Err(OcpEatError::Certificate( + "EC public key is not uncompressed".into(), + )); + } + + let mut x = [0u8; 48]; + let mut y = [0u8; 48]; + + x.copy_from_slice(&pubkey_bytes[1..49]); + y.copy_from_slice(&pubkey_bytes[49..97]); + + println!("extract_pubkey_xy: success"); + + Ok((x, y)) } -/// OpenSSL-backed ECDSA-P384 verification -fn verify_ecdsa_p384( - pubkey: &PKey, - sig_structure: &[u8], +/// Verify ES384 COSE signature using raw EC public key +fn verify_signature_es384( signature: &[u8], + pubkey_x: [u8; 48], + pubkey_y: [u8; 48], + message: &[u8], ) -> OcpEatResult<()> { if signature.len() != 96 { return Err(OcpEatError::SignatureVerification); } - let r = BigNum::from_slice(&signature[..48]).unwrap(); - let s = BigNum::from_slice(&signature[48..]).unwrap(); - let sig = EcdsaSig::from_private_components(r, s).unwrap(); - let der_sig = sig.to_der().unwrap(); + let r = BigNum::from_slice(&signature[..48]).map_err(|_| OcpEatError::SignatureVerification)?; + let s = BigNum::from_slice(&signature[48..]).map_err(|_| OcpEatError::SignatureVerification)?; + + let sig = openssl::ecdsa::EcdsaSig::from_private_components(r, s) + .map_err(|_| OcpEatError::SignatureVerification)?; - let mut verifier = Verifier::new(MessageDigest::sha384(), pubkey) + let group = + EcGroup::from_curve_name(Nid::SECP384R1).map_err(|e| OcpEatError::Crypto(e.to_string()))?; + + let mut ctx = BigNumContext::new().map_err(|e| OcpEatError::Crypto(e.to_string()))?; + + let px = BigNum::from_slice(&pubkey_x).unwrap(); + let py = BigNum::from_slice(&pubkey_y).unwrap(); + + let mut point = EcPoint::new(&group).map_err(|e| OcpEatError::Crypto(e.to_string()))?; + + point + .set_affine_coordinates_gfp(&group, &px, &py, &mut ctx) .map_err(|e| OcpEatError::Crypto(e.to_string()))?; - verifier - .update(sig_structure) + let ec_key = + EcKey::from_public_key(&group, &point).map_err(|e| OcpEatError::Crypto(e.to_string()))?; + + let digest = openssl::hash::hash(openssl::hash::MessageDigest::sha384(), message) .map_err(|e| OcpEatError::Crypto(e.to_string()))?; - let verified = verifier - .verify(&der_sig) + let verified = sig + .verify(&digest, &ec_key) .map_err(|e| OcpEatError::Crypto(e.to_string()))?; if verified { From d48ce94eb049f2d839f281753d43460e7c39e49a Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Fri, 19 Dec 2025 15:28:56 -0800 Subject: [PATCH 08/28] this is a working version --- .../ocptoken-rs/src/token/evidence.rs | 5 +--- .../ocptoken-rs/tests/test_evidence.rs | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index 73636f84b..d1535bc44 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -1,15 +1,12 @@ // Licensed under the Apache-2.0 license use coset::{ - cbor::value::Value, - iana::{Algorithm, CoapContentFormat}, - sig_structure_data, CborSerializable, CoseSign1, Header, RegisteredLabel, + cbor::value::Value, iana::Algorithm, sig_structure_data, CborSerializable, CoseSign1, Header, }; use openssl::{ bn::{BigNum, BigNumContext}, ec::{EcGroup, EcKey, EcPoint}, nid::Nid, - x509::X509, }; use crate::error::{OcpEatError, OcpEatResult}; diff --git a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs index 09184304e..722f9cde8 100644 --- a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs @@ -25,15 +25,40 @@ fn decode_and_verify_cose_sign1() { let pkey = PKey::from_ec_key(ec_key).unwrap(); // 2️ Create X.509 certificate from public key + use openssl::asn1::Asn1Time; + use openssl::bn::BigNum; + + // 2️⃣ Create X.509 certificate from public key let mut name = X509NameBuilder::new().unwrap(); name.append_entry_by_text("CN", "test-cert").unwrap(); let name = name.build(); let mut cert_builder = X509::builder().unwrap(); + cert_builder.set_version(2).unwrap(); + + // REQUIRED: serial number + let mut serial = BigNum::new().unwrap(); + serial + .rand(64, openssl::bn::MsbOption::MAYBE_ZERO, false) + .unwrap(); + let serial = serial.to_asn1_integer().unwrap(); + cert_builder.set_serial_number(&serial).unwrap(); + + // Subject / issuer cert_builder.set_subject_name(&name).unwrap(); cert_builder.set_issuer_name(&name).unwrap(); + + // REQUIRED: validity + let not_before = Asn1Time::days_from_now(0).unwrap(); + let not_after = Asn1Time::days_from_now(365).unwrap(); + cert_builder.set_not_before(¬_before).unwrap(); + cert_builder.set_not_after(¬_after).unwrap(); + + // Public key cert_builder.set_pubkey(&pkey).unwrap(); + + // Sign certificate cert_builder.sign(&pkey, MessageDigest::sha384()).unwrap(); let cert = cert_builder.build(); From 201e8f8177a82b7b44a71e764e0d4ae2053e47b4 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Mon, 22 Dec 2025 09:11:53 -0800 Subject: [PATCH 09/28] intermi commit --- .../ocptoken-rs/src/token/evidence.rs | 10 ++++++++ .../ocptoken-rs/tests/test_evidence.rs | 25 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index d1535bc44..7ce456e1f 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -34,6 +34,16 @@ impl Evidence { // 1. Parse COSE_Sign1 let cose = CoseSign1::from_slice(slice)?; + /* use coset::cbor::value::Value; + use coset::CborSerializable; + + let value = Value::from_slice(slice).map_err(|e| OcpEatError::CoseSign1(e))?; + + let cose = match value { + Value::Tag(_, boxed) => CoseSign1::from_slice(&boxed.to_vec()?)?, + _ => CoseSign1::from_slice(slice)?, + };*/ + // 2. Verify protected header (ES384 + EatCwt) verify_protected_header(&cose.protected.header)?; diff --git a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs index 722f9cde8..8ee721afb 100644 --- a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs @@ -15,6 +15,8 @@ use ocptoken::token::evidence::Evidence; use coset::cbor::value::Value; +use std::fs; + use openssl::ecdsa::EcdsaSig; #[test] @@ -28,7 +30,7 @@ fn decode_and_verify_cose_sign1() { use openssl::asn1::Asn1Time; use openssl::bn::BigNum; - // 2️⃣ Create X.509 certificate from public key + // 3 Create X.509 certificate from public key let mut name = X509NameBuilder::new().unwrap(); name.append_entry_by_text("CN", "test-cert").unwrap(); let name = name.build(); @@ -102,3 +104,24 @@ fn decode_and_verify_cose_sign1() { "COSE_Sign1 signature verification should succeed" ); } + +#[test] +fn decode_measurement_block_fd_bin() { + // Read the bin generated by mctp spdm validator + let encoded = fs::read( + "/home/mei_lu/caliptra-mcu-sw/spdm-mctp-validator-test-results/measurement_block_fd.bin", + ) + .expect("failed to read measurement_block_fd.bin"); + + println!("encoded len: {}", encoded.len()); + + let evidence = match Evidence::decode(&encoded[3..]) { + Ok(ev) => ev, + Err(e) => { + panic!("Evidence::decode failed: {:?}", e); + } + }; + + // Basic sanity check + //assert!(evidence.signed_eat.is_some()); +} From 5253c88b3d64f5bdd26c96099e0707a3eb66657c Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Mon, 22 Dec 2025 16:23:39 -0800 Subject: [PATCH 10/28] take out tag and then use coset from main --- ocp-eat-verifier/ocptoken-rs/Cargo.toml | 5 +- .../ocptoken-rs/src/token/evidence.rs | 173 +++++++++++++----- .../ocptoken-rs/tests/test_evidence.rs | 20 +- 3 files changed, 144 insertions(+), 54 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/Cargo.toml b/ocp-eat-verifier/ocptoken-rs/Cargo.toml index e665ceb8f..1f4e7ed56 100644 --- a/ocp-eat-verifier/ocptoken-rs/Cargo.toml +++ b/ocp-eat-verifier/ocptoken-rs/Cargo.toml @@ -7,7 +7,10 @@ edition.workspace = true authors.workspace = true [dependencies] -coset = "0.4.0" +# coset = "0.4.0" +coset = { git = "https://github.com/google/coset", branch = "main" } +hex = "0.4" + thiserror = "2.0.17" openssl = { version = "0.10", features = ["vendored"] } x509-parser = "0.16" diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index 7ce456e1f..020c605e0 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -1,8 +1,6 @@ // Licensed under the Apache-2.0 license -use coset::{ - cbor::value::Value, iana::Algorithm, sig_structure_data, CborSerializable, CoseSign1, Header, -}; +use coset::{cbor::value::Value, sig_structure_data, CborSerializable, CoseSign1, Header}; use openssl::{ bn::{BigNum, BigNumContext}, ec::{EcGroup, EcKey, EcPoint}, @@ -11,6 +9,8 @@ use openssl::{ use crate::error::{OcpEatError, OcpEatResult}; +use x509_parser::prelude::*; + /// Parsed and verified EAT evidence pub struct Evidence { pub signed_eat: Option, @@ -31,53 +31,65 @@ impl Evidence { /// Decode and verify a COSE_Sign1 pub fn decode(slice: &[u8]) -> OcpEatResult { - // 1. Parse COSE_Sign1 - let cose = CoseSign1::from_slice(slice)?; - - /* use coset::cbor::value::Value; - use coset::CborSerializable; - - let value = Value::from_slice(slice).map_err(|e| OcpEatError::CoseSign1(e))?; - - let cose = match value { - Value::Tag(_, boxed) => CoseSign1::from_slice(&boxed.to_vec()?)?, - _ => CoseSign1::from_slice(slice)?, - };*/ - - // 2. Verify protected header (ES384 + EatCwt) + /* ---------------------------------------------------------- + * 1. Skip CBOR tags / bstr + * ---------------------------------------------------------- */ + let value = skip_cbor_tags(slice)?; + let cose_bytes = value.to_vec().map_err(OcpEatError::CoseSign1)?; + + /* ---------------------------------------------------------- + * 2. Strict COSE decode + * ---------------------------------------------------------- */ + let cose = match CoseSign1::from_slice(&cose_bytes) { + Ok(cose) => { + println!("coset strict decode succeeded"); + cose + } + Err(e) => { + println!("coset strict decode failed: {:?}", e); + println!("Dumping CBOR structure to identify offending tag/value:"); + debug_dump_cbor(slice); + return Err(OcpEatError::CoseSign1(e)); + } + }; + /* ---------------------------------------------------------- + * 3. Verify protected header + * ---------------------------------------------------------- */ verify_protected_header(&cose.protected.header)?; - // 3. Extract payload + // Extract payload let payload = cose.payload.as_deref().ok_or_else(|| { OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem("nil", "bstr payload")) })?; - // 4. Extract leaf cert DER from x5chain - - // 5. Extract raw EC public key (x, y) + /* ---------------------------------------------------------- + * 4. Extract leaf cert from unprotected header + * ---------------------------------------------------------- */ let cert_der = extract_leaf_cert_der(&cose.unprotected)?; - println!("extract_pubkey_xy"); - dump_cert_der(&cert_der); - let (pubkey_x, pubkey_y) = extract_pubkey_xy(&cert_der)?; - // 6. Reconstruct COSE Sig_structure - let message = sig_structure_data( + /* ---------------------------------------------------------- + * 5. Reconstruct Sig_structure (SPEC-CORRECT) + * ---------------------------------------------------------- */ + + let sig_structure = sig_structure_data( coset::SignatureContext::CoseSign1, - cose.protected.clone(), + cose.protected.clone(), // uses original_data internally None, &[], payload, ); - println!("verify_signature_es384"); + /* ---------------------------------------------------------- + * 6. Verify ES384 signature using pubkey and + * ---------------------------------------------------------- */ + verify_signature_es384(&cose.signature, pubkey_x, pubkey_y, &sig_structure)?; - // 7. Verify ES384 signature - verify_signature_es384(&cose.signature, pubkey_x, pubkey_y, &message)?; + println!("success decoded"); Ok(Evidence { signed_eat: Some(cose), @@ -92,6 +104,70 @@ impl Evidence { use std::fs::File; use std::io::Write; +fn debug_dump_cbor(slice: &[u8]) { + match Value::from_slice(slice) { + Ok(v) => { + println!("Top-level CBOR value: {:?}", v); + dump_cbor_value(&v, 0); + } + Err(e) => { + println!("CBOR parse failed before COSE: {:?}", e); + } + } +} + +fn dump_cbor_value(v: &Value, indent: usize) { + let pad = " ".repeat(indent); + + match v { + Value::Tag(tag, inner) => { + println!("{pad}CBOR Tag: {}", tag); + dump_cbor_value(inner, indent + 1); + } + Value::Map(map) => { + println!("{pad}CBOR Map:"); + for (k, v) in map { + println!("{pad} Key: {:?}", k); + println!("{pad} Value:"); + dump_cbor_value(v, indent + 2); + } + } + Value::Array(arr) => { + println!("{pad}CBOR Array (len={}):", arr.len()); + for (i, item) in arr.iter().enumerate() { + println!("{pad} [{i}]"); + dump_cbor_value(item, indent + 2); + } + } + other => { + println!("{pad}{:?}", other); + } + } +} + +fn skip_cbor_tags(slice: &[u8]) -> OcpEatResult { + let mut value = Value::from_slice(slice).map_err(OcpEatError::CoseSign1)?; + + loop { + match value { + Value::Tag(_, boxed) => value = *boxed, + Value::Bytes(bytes) => { + value = Value::from_slice(&bytes).map_err(OcpEatError::CoseSign1)? + } + Value::Array(_) => break, + _ => { + return Err(OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( + "tag / bstr / array", + "unexpected CBOR wrapper", + ))); + } + } + } + + println!("done Unwrap CBOR tags / bstr (Python skip_cbor_tags)"); + Ok(value) +} + fn dump_cert_der(cert_der: &[u8]) { let mut f = File::create("x5chain_cert.der").expect("failed to create x5chain_cert.der"); f.write_all(cert_der).expect("failed to write cert DER"); @@ -101,21 +177,6 @@ fn dump_cert_der(cert_der: &[u8]) { ); } -fn verify_protected_header(protected: &Header) -> OcpEatResult<()> { - if protected.alg - != Some(coset::RegisteredLabelWithPrivate::Assigned( - Algorithm::ES384, - )) - { - println!("alg is not ES384"); - return Err(OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( - "alg", "ES384", - ))); - } - - Ok(()) -} - /// Extract leaf certificate DER from x5chain (label 33) fn extract_leaf_cert_der(unprotected: &Header) -> OcpEatResult> { let value = unprotected @@ -149,8 +210,6 @@ fn extract_leaf_cert_der(unprotected: &Header) -> OcpEatResult> { }) } -use x509_parser::prelude::*; - /// Extract raw P-384 public key coordinates (x, y) from DER X.509 cert fn extract_pubkey_xy(cert_der: &[u8]) -> OcpEatResult<([u8; 48], [u8; 48])> { // Parse X.509 certificate (pure Rust, lenient) @@ -250,3 +309,23 @@ fn verify_signature_es384( Err(OcpEatError::SignatureVerification) } } + +fn verify_protected_header(protected: &Header) -> OcpEatResult<()> { + match &protected.alg { + Some(coset::RegisteredLabelWithPrivate::Assigned(alg)) => { + println!("protected.alg (IANA) = {:?}", alg); + } + Some(coset::RegisteredLabelWithPrivate::PrivateUse(v)) => { + println!("protected.alg (private-use) = {}", v); + } + Some(coset::RegisteredLabelWithPrivate::Text(t)) => { + println!("protected.alg (text) = {}", t); + } + None => { + return Err(OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( + "alg", "present", + ))); + } + } + Ok(()) +} diff --git a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs index 8ee721afb..0832385db 100644 --- a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs @@ -66,7 +66,7 @@ fn decode_and_verify_cose_sign1() { let cert = cert_builder.build(); let cert_der = cert.to_der().unwrap(); - // 3️ Dummy payload + // Dummy payload let payload = b"dummy payload for COSE signature"; // 4️ Create COSE_Sign1 structure @@ -105,9 +105,11 @@ fn decode_and_verify_cose_sign1() { ); } -#[test] +/*#[test] fn decode_measurement_block_fd_bin() { + // Read the bin generated by mctp spdm validator + // TODO change it to a cli app t let encoded = fs::read( "/home/mei_lu/caliptra-mcu-sw/spdm-mctp-validator-test-results/measurement_block_fd.bin", ) @@ -115,13 +117,19 @@ fn decode_measurement_block_fd_bin() { println!("encoded len: {}", encoded.len()); - let evidence = match Evidence::decode(&encoded[3..]) { + let _evidence = match Evidence::decode(&encoded[3..]) { Ok(ev) => ev, Err(e) => { - panic!("Evidence::decode failed: {:?}", e); + eprintln!("Evidence::decode failed"); + eprintln!("Error: {:#?}", e); + eprintln!("Input length: {}", encoded.len()); + eprintln!( + "First 32 bytes (hex): {}", + hex::encode(&encoded[..32.min(encoded.len())]) + ); + panic!("Evidence::decode failed"); } }; - // Basic sanity check //assert!(evidence.signed_eat.is_some()); -} +}*/ From 5d932f3300e0305dd2ae5c6bee66236fe7e8b2a7 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Mon, 29 Dec 2025 08:58:14 -0800 Subject: [PATCH 11/28] fix pipleline precheck error, add Clap cll to pass bin file ot decode. --- ocp-eat-verifier/Cargo.toml | 12 +++- ocp-eat-verifier/ocptoken-rs/Cargo.toml | 18 +++--- ocp-eat-verifier/ocptoken-rs/src/main.rs | 63 +++++++++++++++++++ .../ocptoken-rs/src/token/evidence.rs | 6 +- .../ocptoken-rs/tests/test_evidence.rs | 44 ++----------- 5 files changed, 92 insertions(+), 51 deletions(-) create mode 100644 ocp-eat-verifier/ocptoken-rs/src/main.rs diff --git a/ocp-eat-verifier/Cargo.toml b/ocp-eat-verifier/Cargo.toml index 5c2c4ce7d..135696f49 100644 --- a/ocp-eat-verifier/Cargo.toml +++ b/ocp-eat-verifier/Cargo.toml @@ -9,4 +9,14 @@ resolver = "2" [workspace.package] version = "0.1.0" edition = "2021" -authors = ["Caliptra contributors"] \ No newline at end of file +authors = ["Caliptra contributors"] + + + +[workspace.dependencies] +coset = { git = "https://github.com/google/coset", branch = "main" } +hex = "0.4" +thiserror = "2.0" +x509-parser = "0.16" +openssl = { version = "0.10", features = ["vendored"] } +clap = { version = "4", features = ["derive"] } \ No newline at end of file diff --git a/ocp-eat-verifier/ocptoken-rs/Cargo.toml b/ocp-eat-verifier/ocptoken-rs/Cargo.toml index 1f4e7ed56..0f88c8d6b 100644 --- a/ocp-eat-verifier/ocptoken-rs/Cargo.toml +++ b/ocp-eat-verifier/ocptoken-rs/Cargo.toml @@ -2,16 +2,16 @@ [package] name = "ocptoken" -version.workspace = true -edition.workspace = true -authors.workspace = true +version = "0.1.0" +edition = "2021" +authors = ["Caliptra Contributors"] [dependencies] -# coset = "0.4.0" -coset = { git = "https://github.com/google/coset", branch = "main" } -hex = "0.4" +coset.workspace = true +hex.workspace = true +thiserror.workspace = true +openssl.workspace = true +x509-parser.workspace = true +clap.workspace = true -thiserror = "2.0.17" -openssl = { version = "0.10", features = ["vendored"] } -x509-parser = "0.16" diff --git a/ocp-eat-verifier/ocptoken-rs/src/main.rs b/ocp-eat-verifier/ocptoken-rs/src/main.rs new file mode 100644 index 000000000..1a9516f93 --- /dev/null +++ b/ocp-eat-verifier/ocptoken-rs/src/main.rs @@ -0,0 +1,63 @@ +// Licensed under the Apache-2.0 license + +use clap::Parser; +use std::fs; +use std::path::PathBuf; + +use ocptoken::token::evidence::Evidence; + +#[derive(Parser, Debug)] +#[command( + name = "ocptoken", + author, + version, + about = "Decode and verify an OCP TOKEN COSE_Sign1 token" +)] +struct Cli { + /// Path to CBOR-encoded evidence + #[arg(short = 'e', long = "evidence", value_name = "FILE")] + evidence: PathBuf, +} + +fn main() { + let cli = Cli::parse(); + + // 1. Load the binary file + let encoded = match fs::read(&cli.evidence) { + Ok(b) => b, + Err(e) => { + eprintln!( + "Failed to read evidence file '{}': {}", + cli.evidence.display(), + e + ); + std::process::exit(1); + } + }; + + println!( + "Loaded evidence file '{}' ({} bytes)", + cli.evidence.display(), + encoded.len() + ); + + // 2. Decode the evidence + match Evidence::decode(&encoded) { + Ok(_ev) => { + println!("success decoded"); + } + Err(e) => { + eprintln!("Evidence::decode failed: {:?}", e); + + // Optional: show first few bytes to help debugging + let prefix_len = encoded.len().min(32); + eprintln!( + "First {} bytes of input: {:02x?}", + prefix_len, + &encoded[..prefix_len] + ); + + std::process::exit(1); + } + } +} diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index 020c605e0..cd8256ddd 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -1,14 +1,14 @@ // Licensed under the Apache-2.0 license +use crate::error::{OcpEatError, OcpEatResult}; use coset::{cbor::value::Value, sig_structure_data, CborSerializable, CoseSign1, Header}; + use openssl::{ bn::{BigNum, BigNumContext}, ec::{EcGroup, EcKey, EcPoint}, nid::Nid, }; -use crate::error::{OcpEatError, OcpEatResult}; - use x509_parser::prelude::*; /// Parsed and verified EAT evidence @@ -104,7 +104,7 @@ impl Evidence { use std::fs::File; use std::io::Write; -fn debug_dump_cbor(slice: &[u8]) { +pub fn debug_dump_cbor(slice: &[u8]) { match Value::from_slice(slice) { Ok(v) => { println!("Top-level CBOR value: {:?}", v); diff --git a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs index 0832385db..32fbcf8ef 100644 --- a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs @@ -1,9 +1,12 @@ // Licensed under the Apache-2.0 license -use coset::{iana::Algorithm, CborSerializable, CoseSign1Builder, HeaderBuilder}; +use coset::{ + cbor::value::Value, iana::Algorithm, CborSerializable, CoseSign1Builder, HeaderBuilder, +}; use openssl::{ ec::{EcGroup, EcKey}, + ecdsa::EcdsaSig, hash::MessageDigest, nid::Nid, pkey::PKey, @@ -13,12 +16,6 @@ use openssl::{ use ocptoken::token::evidence::Evidence; -use coset::cbor::value::Value; - -use std::fs; - -use openssl::ecdsa::EcdsaSig; - #[test] fn decode_and_verify_cose_sign1() { // 1️ Generate ECC P-384 key pair @@ -39,7 +36,7 @@ fn decode_and_verify_cose_sign1() { cert_builder.set_version(2).unwrap(); - // REQUIRED: serial number + // serial number let mut serial = BigNum::new().unwrap(); serial .rand(64, openssl::bn::MsbOption::MAYBE_ZERO, false) @@ -51,7 +48,7 @@ fn decode_and_verify_cose_sign1() { cert_builder.set_subject_name(&name).unwrap(); cert_builder.set_issuer_name(&name).unwrap(); - // REQUIRED: validity + // validity let not_before = Asn1Time::days_from_now(0).unwrap(); let not_after = Asn1Time::days_from_now(365).unwrap(); cert_builder.set_not_before(¬_before).unwrap(); @@ -104,32 +101,3 @@ fn decode_and_verify_cose_sign1() { "COSE_Sign1 signature verification should succeed" ); } - -/*#[test] -fn decode_measurement_block_fd_bin() { - - // Read the bin generated by mctp spdm validator - // TODO change it to a cli app t - let encoded = fs::read( - "/home/mei_lu/caliptra-mcu-sw/spdm-mctp-validator-test-results/measurement_block_fd.bin", - ) - .expect("failed to read measurement_block_fd.bin"); - - println!("encoded len: {}", encoded.len()); - - let _evidence = match Evidence::decode(&encoded[3..]) { - Ok(ev) => ev, - Err(e) => { - eprintln!("Evidence::decode failed"); - eprintln!("Error: {:#?}", e); - eprintln!("Input length: {}", encoded.len()); - eprintln!( - "First 32 bytes (hex): {}", - hex::encode(&encoded[..32.min(encoded.len())]) - ); - panic!("Evidence::decode failed"); - } - }; - // Basic sanity check - //assert!(evidence.signed_eat.is_some()); -}*/ From 0bf0e927961513361d02ebc5cefd46fd9d6a969b Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Mon, 29 Dec 2025 10:35:33 -0800 Subject: [PATCH 12/28] add steps to run cli tool to decode the bins --- ocp-eat-verifier/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/ocp-eat-verifier/Cargo.toml b/ocp-eat-verifier/Cargo.toml index 135696f49..26840431b 100644 --- a/ocp-eat-verifier/Cargo.toml +++ b/ocp-eat-verifier/Cargo.toml @@ -17,6 +17,5 @@ authors = ["Caliptra contributors"] coset = { git = "https://github.com/google/coset", branch = "main" } hex = "0.4" thiserror = "2.0" -x509-parser = "0.16" openssl = { version = "0.10", features = ["vendored"] } clap = { version = "4", features = ["derive"] } \ No newline at end of file From e47abaacfe74a31ddbd02ed606fcc374d6ff83ea Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Mon, 29 Dec 2025 10:52:29 -0800 Subject: [PATCH 13/28] add step to call cli tool to validate bin files --- .github/workflows/spdm-validator.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/spdm-validator.yml b/.github/workflows/spdm-validator.yml index db001b52b..d60dea943 100644 --- a/.github/workflows/spdm-validator.yml +++ b/.github/workflows/spdm-validator.yml @@ -135,6 +135,16 @@ jobs: exit 1 fi + - name: Decode SPDM measurement block using ocptoken + env: + SPDM_VALIDATOR_DIR: ${{ github.workspace }}/spdm-emu/build/bin + run: | + echo "Running ocptoken on measurement_block_fd.bin" + ls -l $SPDM_VALIDATOR_DIR/measurement_block_fd.bin + ./target/debug/ocptoken \ + --evidence $SPDM_VALIDATOR_DIR/measurement_block_fd.bin + + - name: Run SPDM validator tests on DOE transport env: SPDM_VALIDATOR_DIR: ${{ github.workspace }}/spdm-emu/build/bin From 234a995d6c87fde68923455b1111443afbbd69c5 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Mon, 29 Dec 2025 10:55:07 -0800 Subject: [PATCH 14/28] accidently checked cargo.toml that is workign in progress, roll back --- ocp-eat-verifier/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ocp-eat-verifier/Cargo.toml b/ocp-eat-verifier/Cargo.toml index 26840431b..135696f49 100644 --- a/ocp-eat-verifier/Cargo.toml +++ b/ocp-eat-verifier/Cargo.toml @@ -17,5 +17,6 @@ authors = ["Caliptra contributors"] coset = { git = "https://github.com/google/coset", branch = "main" } hex = "0.4" thiserror = "2.0" +x509-parser = "0.16" openssl = { version = "0.10", features = ["vendored"] } clap = { version = "4", features = ["derive"] } \ No newline at end of file From b915d7efa86e3e8b5a79676800db117c9b1e6399 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Mon, 29 Dec 2025 15:44:32 -0800 Subject: [PATCH 15/28] use Open SSL to parse pub key instead of 509 parser --- ocp-eat-verifier/Cargo.toml | 3 - ocp-eat-verifier/ocptoken-rs/Cargo.toml | 1 - .../ocptoken-rs/src/token/evidence.rs | 68 +++++++++---------- 3 files changed, 31 insertions(+), 41 deletions(-) diff --git a/ocp-eat-verifier/Cargo.toml b/ocp-eat-verifier/Cargo.toml index 135696f49..0a12b07ff 100644 --- a/ocp-eat-verifier/Cargo.toml +++ b/ocp-eat-verifier/Cargo.toml @@ -11,12 +11,9 @@ version = "0.1.0" edition = "2021" authors = ["Caliptra contributors"] - - [workspace.dependencies] coset = { git = "https://github.com/google/coset", branch = "main" } hex = "0.4" thiserror = "2.0" -x509-parser = "0.16" openssl = { version = "0.10", features = ["vendored"] } clap = { version = "4", features = ["derive"] } \ No newline at end of file diff --git a/ocp-eat-verifier/ocptoken-rs/Cargo.toml b/ocp-eat-verifier/ocptoken-rs/Cargo.toml index 0f88c8d6b..7ef1435a2 100644 --- a/ocp-eat-verifier/ocptoken-rs/Cargo.toml +++ b/ocp-eat-verifier/ocptoken-rs/Cargo.toml @@ -11,7 +11,6 @@ coset.workspace = true hex.workspace = true thiserror.workspace = true openssl.workspace = true -x509-parser.workspace = true clap.workspace = true diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index cd8256ddd..854065e90 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -7,10 +7,10 @@ use openssl::{ bn::{BigNum, BigNumContext}, ec::{EcGroup, EcKey, EcPoint}, nid::Nid, + pkey::PKey, + x509::X509, }; -use x509_parser::prelude::*; - /// Parsed and verified EAT evidence pub struct Evidence { pub signed_eat: Option, @@ -164,7 +164,6 @@ fn skip_cbor_tags(slice: &[u8]) -> OcpEatResult { } } - println!("done Unwrap CBOR tags / bstr (Python skip_cbor_tags)"); Ok(value) } @@ -212,50 +211,45 @@ fn extract_leaf_cert_der(unprotected: &Header) -> OcpEatResult> { /// Extract raw P-384 public key coordinates (x, y) from DER X.509 cert fn extract_pubkey_xy(cert_der: &[u8]) -> OcpEatResult<([u8; 48], [u8; 48])> { - // Parse X.509 certificate (pure Rust, lenient) - let cert = match X509Certificate::from_der(cert_der) { - Ok((rem, cert)) => { - println!("X509Certificate::from_der OK"); - println!("Remaining bytes after parse: {}", rem.len()); - cert - } - Err(e) => { - println!("X509Certificate::from_der FAILED"); - println!("Error: {:?}", e); + // Parse X.509 certificate using OpenSSL + let cert = X509::from_der(cert_der) + .map_err(|e| OcpEatError::Certificate(format!("OpenSSL X509 parse failed: {}", e)))?; - return Err(OcpEatError::Certificate(format!( - "X509 parse error: {:?}", - e - ))); - } - }; + // Extract public key + let pubkey: PKey = cert + .public_key() + .map_err(|e| OcpEatError::Certificate(format!("Failed to extract public key: {}", e)))?; + + // Ensure EC key + let ec_key = pubkey + .ec_key() + .map_err(|_| OcpEatError::Certificate("Public key is not an EC key".into()))?; - println!(" X509Certificate::from_der"); + let group = ec_key.group(); + let point = ec_key.public_key(); - let spki = &cert.tbs_certificate.subject_pki; + let mut ctx = BigNumContext::new().map_err(|e| OcpEatError::Crypto(e.to_string()))?; - // subject_public_key is a BIT STRING - let pubkey_bytes = spki.subject_public_key.data.as_ref(); + let mut ctx_x = BigNum::new().map_err(|e| OcpEatError::Crypto(e.to_string()))?; + let mut ctx_y = BigNum::new().map_err(|e| OcpEatError::Crypto(e.to_string()))?; - // Expect uncompressed EC point: 04 || X || Y (P-384) - if pubkey_bytes.len() != 1 + 48 + 48 { - return Err(OcpEatError::Certificate(format!( - "unexpected EC public key length: {}", - pubkey_bytes.len() - ))); - } + point + .affine_coordinates_gfp(group, &mut ctx_x, &mut ctx_y, &mut ctx) + .map_err(|e| OcpEatError::Crypto(e.to_string()))?; - if pubkey_bytes[0] != 0x04 { - return Err(OcpEatError::Certificate( - "EC public key is not uncompressed".into(), - )); - } + let x_bytes = ctx_x + .to_vec_padded(48) + .map_err(|_| OcpEatError::Certificate("Failed to pad X coordinate".into()))?; + + let y_bytes = ctx_y + .to_vec_padded(48) + .map_err(|_| OcpEatError::Certificate("Failed to pad Y coordinate".into()))?; let mut x = [0u8; 48]; let mut y = [0u8; 48]; - x.copy_from_slice(&pubkey_bytes[1..49]); - y.copy_from_slice(&pubkey_bytes[49..97]); + x.copy_from_slice(&x_bytes); + y.copy_from_slice(&y_bytes); println!("extract_pubkey_xy: success"); From 221d0e98f6d9178f88b7cd40ed364ede07e8682c Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Tue, 30 Dec 2025 09:11:48 -0800 Subject: [PATCH 16/28] build the binary before running the cli --- .github/workflows/spdm-validator.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/spdm-validator.yml b/.github/workflows/spdm-validator.yml index d60dea943..ef62dc1db 100644 --- a/.github/workflows/spdm-validator.yml +++ b/.github/workflows/spdm-validator.yml @@ -135,16 +135,23 @@ jobs: exit 1 fi + + - name: Build ocptoken CLI + working-directory: ocp-eat-verifier/ocptoken-rs + run: | + cargo build --release + - name: Decode SPDM measurement block using ocptoken env: SPDM_VALIDATOR_DIR: ${{ github.workspace }}/spdm-emu/build/bin run: | echo "Running ocptoken on measurement_block_fd.bin" ls -l $SPDM_VALIDATOR_DIR/measurement_block_fd.bin - ./target/debug/ocptoken \ + ./target/release/ocptoken \ --evidence $SPDM_VALIDATOR_DIR/measurement_block_fd.bin + - name: Run SPDM validator tests on DOE transport env: SPDM_VALIDATOR_DIR: ${{ github.workspace }}/spdm-emu/build/bin From 6010d6ea6647d1916eee5ee91bd9f09ba19e322a Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Tue, 30 Dec 2025 14:07:59 -0800 Subject: [PATCH 17/28] update the working dir --- .github/workflows/spdm-validator.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spdm-validator.yml b/.github/workflows/spdm-validator.yml index ef62dc1db..570ff9541 100644 --- a/.github/workflows/spdm-validator.yml +++ b/.github/workflows/spdm-validator.yml @@ -137,9 +137,9 @@ jobs: - name: Build ocptoken CLI - working-directory: ocp-eat-verifier/ocptoken-rs + working-directory: ocp-eat-verifier run: | - cargo build --release + cargo build --release -p ocptoken - name: Decode SPDM measurement block using ocptoken env: From 1ddeee2e334024e6b07198b808655039ebdba8e0 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Tue, 30 Dec 2025 14:56:13 -0800 Subject: [PATCH 18/28] one more try with working dir --- .github/workflows/spdm-validator.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/spdm-validator.yml b/.github/workflows/spdm-validator.yml index 570ff9541..78a04df59 100644 --- a/.github/workflows/spdm-validator.yml +++ b/.github/workflows/spdm-validator.yml @@ -138,20 +138,17 @@ jobs: - name: Build ocptoken CLI working-directory: ocp-eat-verifier - run: | - cargo build --release -p ocptoken + run: cargo build --release -p ocptoken - name: Decode SPDM measurement block using ocptoken + working-directory: ocp-eat-verifier env: SPDM_VALIDATOR_DIR: ${{ github.workspace }}/spdm-emu/build/bin run: | - echo "Running ocptoken on measurement_block_fd.bin" - ls -l $SPDM_VALIDATOR_DIR/measurement_block_fd.bin ./target/release/ocptoken \ --evidence $SPDM_VALIDATOR_DIR/measurement_block_fd.bin - - name: Run SPDM validator tests on DOE transport env: SPDM_VALIDATOR_DIR: ${{ github.workspace }}/spdm-emu/build/bin From 5979f07cf2d04ccb79d5db8ad1dbc1a379858117 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Wed, 31 Dec 2025 11:00:42 -0800 Subject: [PATCH 19/28] code review feedback --- ocp-eat-verifier/ocptoken-rs/src/error.rs | 5 +- .../ocptoken-rs/src/token/evidence.rs | 87 ++----------------- 2 files changed, 12 insertions(+), 80 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/src/error.rs b/ocp-eat-verifier/ocptoken-rs/src/error.rs index baf8d3ab9..7d160db48 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/error.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/error.rs @@ -1,7 +1,5 @@ // Licensed under the Apache-2.0 license -// error.rs - use thiserror::Error; /// Errors that can occur when working with OCP EAT tokens #[derive(Error, Debug)] @@ -10,6 +8,9 @@ pub enum OcpEatError { #[error("COSE error: {0:?}")] CoseSign1(coset::CoseError), + #[error("Invalid token: {0}")] + InvalidToken(&'static str), + /// Certificate parsing error #[error("Certificate error: {0}")] Certificate(String), diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index 854065e90..37b976b87 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -41,14 +41,8 @@ impl Evidence { * 2. Strict COSE decode * ---------------------------------------------------------- */ let cose = match CoseSign1::from_slice(&cose_bytes) { - Ok(cose) => { - println!("coset strict decode succeeded"); - cose - } + Ok(cose) => cose, Err(e) => { - println!("coset strict decode failed: {:?}", e); - println!("Dumping CBOR structure to identify offending tag/value:"); - debug_dump_cbor(slice); return Err(OcpEatError::CoseSign1(e)); } }; @@ -60,20 +54,20 @@ impl Evidence { // Extract payload - let payload = cose.payload.as_deref().ok_or_else(|| { - OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem("nil", "bstr payload")) - })?; + let payload = cose + .payload + .as_deref() + .ok_or_else(|| OcpEatError::InvalidToken("Payload missing"))?; /* ---------------------------------------------------------- * 4. Extract leaf cert from unprotected header * ---------------------------------------------------------- */ let cert_der = extract_leaf_cert_der(&cose.unprotected)?; - dump_cert_der(&cert_der); let (pubkey_x, pubkey_y) = extract_pubkey_xy(&cert_der)?; /* ---------------------------------------------------------- - * 5. Reconstruct Sig_structure (SPEC-CORRECT) + * 5. Reconstruct Sig_structure * ---------------------------------------------------------- */ let sig_structure = sig_structure_data( @@ -89,8 +83,6 @@ impl Evidence { * ---------------------------------------------------------- */ verify_signature_es384(&cose.signature, pubkey_x, pubkey_y, &sig_structure)?; - println!("success decoded"); - Ok(Evidence { signed_eat: Some(cose), }) @@ -101,50 +93,6 @@ impl Evidence { /* Helper functions */ /* -------------------------------------------------------------------------- */ -use std::fs::File; -use std::io::Write; - -pub fn debug_dump_cbor(slice: &[u8]) { - match Value::from_slice(slice) { - Ok(v) => { - println!("Top-level CBOR value: {:?}", v); - dump_cbor_value(&v, 0); - } - Err(e) => { - println!("CBOR parse failed before COSE: {:?}", e); - } - } -} - -fn dump_cbor_value(v: &Value, indent: usize) { - let pad = " ".repeat(indent); - - match v { - Value::Tag(tag, inner) => { - println!("{pad}CBOR Tag: {}", tag); - dump_cbor_value(inner, indent + 1); - } - Value::Map(map) => { - println!("{pad}CBOR Map:"); - for (k, v) in map { - println!("{pad} Key: {:?}", k); - println!("{pad} Value:"); - dump_cbor_value(v, indent + 2); - } - } - Value::Array(arr) => { - println!("{pad}CBOR Array (len={}):", arr.len()); - for (i, item) in arr.iter().enumerate() { - println!("{pad} [{i}]"); - dump_cbor_value(item, indent + 2); - } - } - other => { - println!("{pad}{:?}", other); - } - } -} - fn skip_cbor_tags(slice: &[u8]) -> OcpEatResult { let mut value = Value::from_slice(slice).map_err(OcpEatError::CoseSign1)?; @@ -167,15 +115,6 @@ fn skip_cbor_tags(slice: &[u8]) -> OcpEatResult { Ok(value) } -fn dump_cert_der(cert_der: &[u8]) { - let mut f = File::create("x5chain_cert.der").expect("failed to create x5chain_cert.der"); - f.write_all(cert_der).expect("failed to write cert DER"); - println!( - "Wrote x5chain cert to x5chain_cert.der ({} bytes)", - cert_der.len() - ); -} - /// Extract leaf certificate DER from x5chain (label 33) fn extract_leaf_cert_der(unprotected: &Header) -> OcpEatResult> { let value = unprotected @@ -251,8 +190,6 @@ fn extract_pubkey_xy(cert_der: &[u8]) -> OcpEatResult<([u8; 48], [u8; 48])> { x.copy_from_slice(&x_bytes); y.copy_from_slice(&y_bytes); - println!("extract_pubkey_xy: success"); - Ok((x, y)) } @@ -306,15 +243,9 @@ fn verify_signature_es384( fn verify_protected_header(protected: &Header) -> OcpEatResult<()> { match &protected.alg { - Some(coset::RegisteredLabelWithPrivate::Assigned(alg)) => { - println!("protected.alg (IANA) = {:?}", alg); - } - Some(coset::RegisteredLabelWithPrivate::PrivateUse(v)) => { - println!("protected.alg (private-use) = {}", v); - } - Some(coset::RegisteredLabelWithPrivate::Text(t)) => { - println!("protected.alg (text) = {}", t); - } + Some(coset::RegisteredLabelWithPrivate::Assigned(_alg)) => {} + Some(coset::RegisteredLabelWithPrivate::PrivateUse(_v)) => {} + Some(coset::RegisteredLabelWithPrivate::Text(_t)) => {} None => { return Err(OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( "alg", "present", From 048495643b63cf0502eae6ddd561692d98727080 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Fri, 2 Jan 2026 11:11:30 -0800 Subject: [PATCH 20/28] chamge the to add verify as a subcommand --- ocp-eat-verifier/ocptoken-rs/src/main.rs | 44 +++++++++++++++++++----- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/src/main.rs b/ocp-eat-verifier/ocptoken-rs/src/main.rs index 1a9516f93..e85d2688e 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/main.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/main.rs @@ -1,6 +1,6 @@ // Licensed under the Apache-2.0 license -use clap::Parser; +use clap::{Parser, Subcommand}; use std::fs; use std::path::PathBuf; @@ -11,24 +11,52 @@ use ocptoken::token::evidence::Evidence; name = "ocptoken", author, version, - about = "Decode and verify an OCP TOKEN COSE_Sign1 token" + about = "Verify an OCP TOKEN COSE_Sign1 token", + long_about = None )] struct Cli { - /// Path to CBOR-encoded evidence - #[arg(short = 'e', long = "evidence", value_name = "FILE")] + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Cryptographically verify the supplied OCP token using the EAT attestation key + Verify(VerifyArgs), +} + +#[derive(Parser, Debug)] +#[command( + author, + version, + about = "Cryptographically verify the supplied OCP token using the EAT attestation key" +)] +struct VerifyArgs { + #[arg( + short = 'e', + long = "evidence", + value_name = "EVIDENCE", + default_value = "ocp_eat.cbor" + )] evidence: PathBuf, } fn main() { let cli = Cli::parse(); + match cli.command { + Commands::Verify(args) => run_verify(&args), + } +} + +fn run_verify(args: &VerifyArgs) { // 1. Load the binary file - let encoded = match fs::read(&cli.evidence) { + let encoded = match fs::read(&args.evidence) { Ok(b) => b, Err(e) => { eprintln!( "Failed to read evidence file '{}': {}", - cli.evidence.display(), + args.evidence.display(), e ); std::process::exit(1); @@ -37,7 +65,7 @@ fn main() { println!( "Loaded evidence file '{}' ({} bytes)", - cli.evidence.display(), + args.evidence.display(), encoded.len() ); @@ -49,7 +77,7 @@ fn main() { Err(e) => { eprintln!("Evidence::decode failed: {:?}", e); - // Optional: show first few bytes to help debugging + // Optional debug dump let prefix_len = encoded.len().min(32); eprintln!( "First {} bytes of input: {:02x?}", From 1630e41b5752f15ba89d1f04feb02b6d195beb3c Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Tue, 6 Jan 2026 08:56:55 -0800 Subject: [PATCH 21/28] code review feedback --- ocp-eat-verifier/ocptoken-rs/src/main.rs | 18 ++- .../ocptoken-rs/src/token/evidence.rs | 114 ++++++++++++------ .../ocptoken-rs/tests/test_evidence.rs | 16 +-- 3 files changed, 103 insertions(+), 45 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/src/main.rs b/ocp-eat-verifier/ocptoken-rs/src/main.rs index e85d2688e..b0243f7fb 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/main.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/main.rs @@ -70,9 +70,10 @@ fn run_verify(args: &VerifyArgs) { ); // 2. Decode the evidence - match Evidence::decode(&encoded) { - Ok(_ev) => { - println!("success decoded"); + let ev = match Evidence::decode(&encoded) { + Ok(ev) => { + println!("Decode successful"); + ev } Err(e) => { eprintln!("Evidence::decode failed: {:?}", e); @@ -85,6 +86,17 @@ fn run_verify(args: &VerifyArgs) { &encoded[..prefix_len] ); + std::process::exit(1); + } + }; + + // 3. Cryptographically verify + match ev.verify() { + Ok(()) => { + println!("Signature verification successful"); + } + Err(e) => { + eprintln!("Evidence::verify failed: {:?}", e); std::process::exit(1); } } diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index 37b976b87..ea4f88b51 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -1,7 +1,9 @@ // Licensed under the Apache-2.0 license use crate::error::{OcpEatError, OcpEatResult}; -use coset::{cbor::value::Value, sig_structure_data, CborSerializable, CoseSign1, Header}; +use coset::{ + cbor::value::Value, iana::Algorithm, sig_structure_data, CborSerializable, CoseSign1, Header, +}; use openssl::{ bn::{BigNum, BigNumContext}, @@ -11,6 +13,8 @@ use openssl::{ x509::X509, }; +pub const OCP_EAT_CLAIMS_KEY_ID: &str = ""; + /// Parsed and verified EAT evidence pub struct Evidence { pub signed_eat: Option, @@ -29,63 +33,69 @@ impl Evidence { } } - /// Decode and verify a COSE_Sign1 + /// Decode and structurally validate a COSE_Sign1 + /// (Steps 1–3) pub fn decode(slice: &[u8]) -> OcpEatResult { - /* ---------------------------------------------------------- - * 1. Skip CBOR tags / bstr - * ---------------------------------------------------------- */ + /* ========================================================== + * Skip CBOR tags / bstr + * ========================================================== */ let value = skip_cbor_tags(slice)?; let cose_bytes = value.to_vec().map_err(OcpEatError::CoseSign1)?; - /* ---------------------------------------------------------- - * 2. Strict COSE decode - * ---------------------------------------------------------- */ - let cose = match CoseSign1::from_slice(&cose_bytes) { - Ok(cose) => cose, - Err(e) => { - return Err(OcpEatError::CoseSign1(e)); - } - }; + /* ========================================================== + * Strict COSE decode + * ========================================================== */ + let cose = CoseSign1::from_slice(&cose_bytes).map_err(OcpEatError::CoseSign1)?; - /* ---------------------------------------------------------- - * 3. Verify protected header - * ---------------------------------------------------------- */ + /* ========================================================== + * Verify protected header + * ========================================================== */ verify_protected_header(&cose.protected.header)?; - // Extract payload + Ok(Evidence { + signed_eat: Some(cose), + }) + } + /// Cryptographically verify the decoded COSE_Sign1 + + pub fn verify(&self) -> OcpEatResult<()> { + let cose = self + .signed_eat + .as_ref() + .ok_or_else(|| OcpEatError::InvalidToken("Missing COSE_Sign1"))?; + + /* ---------------------------------------------------------- + * Extract payload + * ---------------------------------------------------------- */ let payload = cose .payload .as_deref() .ok_or_else(|| OcpEatError::InvalidToken("Payload missing"))?; /* ---------------------------------------------------------- - * 4. Extract leaf cert from unprotected header + * Extract leaf cert from unprotected header * ---------------------------------------------------------- */ - let cert_der = extract_leaf_cert_der(&cose.unprotected)?; let (pubkey_x, pubkey_y) = extract_pubkey_xy(&cert_der)?; /* ---------------------------------------------------------- - * 5. Reconstruct Sig_structure + * Reconstruct Sig_structure * ---------------------------------------------------------- */ - let sig_structure = sig_structure_data( coset::SignatureContext::CoseSign1, - cose.protected.clone(), // uses original_data internally + cose.protected.clone(), // preserves original_data None, &[], payload, ); /* ---------------------------------------------------------- - * 6. Verify ES384 signature using pubkey and + * Verify ES384 signature * ---------------------------------------------------------- */ verify_signature_es384(&cose.signature, pubkey_x, pubkey_y, &sig_structure)?; - Ok(Evidence { - signed_eat: Some(cose), - }) + Ok(()) } } @@ -242,15 +252,51 @@ fn verify_signature_es384( } fn verify_protected_header(protected: &Header) -> OcpEatResult<()> { - match &protected.alg { - Some(coset::RegisteredLabelWithPrivate::Assigned(_alg)) => {} - Some(coset::RegisteredLabelWithPrivate::PrivateUse(_v)) => {} - Some(coset::RegisteredLabelWithPrivate::Text(_t)) => {} + /* ---------------------------------------------------------- + * Algorithm must be ES384 or ESP384 + * ---------------------------------------------------------- */ + + let alg_ok = matches!( + protected.alg, + Some(coset::RegisteredLabelWithPrivate::Assigned( + Algorithm::ES384 + )) | Some(coset::RegisteredLabelWithPrivate::Assigned( + Algorithm::ESP384 + )) + ); + + if !alg_ok { + return Err(OcpEatError::InvalidToken( + "Unexpected algorithm in protected header", + )); + } + + /* ---------------------------------------------------------- + * Content-Type must be EAT (CWT) + * ---------------------------------------------------------- */ + match &protected.content_type { None => { - return Err(OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( - "alg", "present", - ))); + // Accept missing content-type + } + Some(coset::RegisteredLabel::Assigned(coset::iana::CoapContentFormat::EatCwt)) => { + // Accept EAT CWT + } + _other => { + return Err(OcpEatError::InvalidToken( + "Content format mismatch in protected header", + )); } } + + /* ---------------------------------------------------------- + * Key ID must match expected EAT key ID + * ---------------------------------------------------------- */ + + if protected.key_id != OCP_EAT_CLAIMS_KEY_ID.as_bytes().to_vec() { + return Err(OcpEatError::InvalidToken( + "Key ID mismatch in protected header", + )); + } + Ok(()) } diff --git a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs index 32fbcf8ef..dcb222f67 100644 --- a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs @@ -48,7 +48,7 @@ fn decode_and_verify_cose_sign1() { cert_builder.set_subject_name(&name).unwrap(); cert_builder.set_issuer_name(&name).unwrap(); - // validity + // validity let not_before = Asn1Time::days_from_now(0).unwrap(); let not_after = Asn1Time::days_from_now(365).unwrap(); cert_builder.set_not_before(¬_before).unwrap(); @@ -63,7 +63,7 @@ fn decode_and_verify_cose_sign1() { let cert = cert_builder.build(); let cert_der = cert.to_der().unwrap(); - // Dummy payload + // Dummy payload let payload = b"dummy payload for COSE signature"; // 4️ Create COSE_Sign1 structure @@ -93,11 +93,11 @@ fn decode_and_verify_cose_sign1() { // 5️ Encode to CBOR let encoded = cose.to_vec().unwrap(); - // 6️ Decode + verify - let evidence = Evidence::decode(&encoded); + // 6️ Decode + let evidence = Evidence::decode(&encoded).expect("Evidence::decode should succeed"); - assert!( - evidence.is_ok(), - "COSE_Sign1 signature verification should succeed" - ); + // 7️ Verify + evidence + .verify() + .expect("COSE_Sign1 signature verification should succeed"); } From 15f07bf7afd98962b8c654a2bb704102a324d7ac Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Tue, 6 Jan 2026 08:57:04 -0800 Subject: [PATCH 22/28] clean up --- ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index ea4f88b51..f238bfa15 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -272,7 +272,7 @@ fn verify_protected_header(protected: &Header) -> OcpEatResult<()> { } /* ---------------------------------------------------------- - * Content-Type must be EAT (CWT) + * Content-Type * ---------------------------------------------------------- */ match &protected.content_type { None => { @@ -289,7 +289,7 @@ fn verify_protected_header(protected: &Header) -> OcpEatResult<()> { } /* ---------------------------------------------------------- - * Key ID must match expected EAT key ID + * Key ID * ---------------------------------------------------------- */ if protected.key_id != OCP_EAT_CLAIMS_KEY_ID.as_bytes().to_vec() { From 94f16cde39b5e122e80b4226ff4fa022b8764211 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Tue, 6 Jan 2026 16:00:01 -0800 Subject: [PATCH 23/28] code review feedback --- .../ocptoken-rs/src/token/evidence.rs | 55 ++++++------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index f238bfa15..6a6f00046 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -1,9 +1,7 @@ // Licensed under the Apache-2.0 license use crate::error::{OcpEatError, OcpEatResult}; -use coset::{ - cbor::value::Value, iana::Algorithm, sig_structure_data, CborSerializable, CoseSign1, Header, -}; +use coset::{cbor::value::Value, iana::Algorithm, CborSerializable, CoseSign1, Header}; use openssl::{ bn::{BigNum, BigNumContext}, @@ -15,6 +13,9 @@ use openssl::{ pub const OCP_EAT_CLAIMS_KEY_ID: &str = ""; +/// COSE header parameter: x5chain (label 33) +const COSE_HDR_PARAM_X5CHAIN: i64 = 33; + /// Parsed and verified EAT evidence pub struct Evidence { pub signed_eat: Option, @@ -65,35 +66,19 @@ impl Evidence { .as_ref() .ok_or_else(|| OcpEatError::InvalidToken("Missing COSE_Sign1"))?; - /* ---------------------------------------------------------- - * Extract payload - * ---------------------------------------------------------- */ - let payload = cose - .payload - .as_deref() - .ok_or_else(|| OcpEatError::InvalidToken("Payload missing"))?; - /* ---------------------------------------------------------- * Extract leaf cert from unprotected header * ---------------------------------------------------------- */ let cert_der = extract_leaf_cert_der(&cose.unprotected)?; let (pubkey_x, pubkey_y) = extract_pubkey_xy(&cert_der)?; - /* ---------------------------------------------------------- - * Reconstruct Sig_structure - * ---------------------------------------------------------- */ - let sig_structure = sig_structure_data( - coset::SignatureContext::CoseSign1, - cose.protected.clone(), // preserves original_data - None, - &[], - payload, - ); - /* ---------------------------------------------------------- * Verify ES384 signature * ---------------------------------------------------------- */ - verify_signature_es384(&cose.signature, pubkey_x, pubkey_y, &sig_structure)?; + // verify_signature_es384(&cose.signature, pubkey_x, pubkey_y, &sig_structure)?; + cose.verify_signature(&[], |signature, to_be_signed| { + verify_signature_es384(signature, pubkey_x, pubkey_y, to_be_signed) + })?; Ok(()) } @@ -114,10 +99,9 @@ fn skip_cbor_tags(slice: &[u8]) -> OcpEatResult { } Value::Array(_) => break, _ => { - return Err(OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( - "tag / bstr / array", - "unexpected CBOR wrapper", - ))); + return Err(OcpEatError::InvalidToken( + "Invalid COSE_Sign1 structure: expected CBOR tag, byte string, or array", + )); } } } @@ -131,15 +115,15 @@ fn extract_leaf_cert_der(unprotected: &Header) -> OcpEatResult> { .rest .iter() .find_map(|(label, value)| { - if *label == coset::Label::Int(33) { + if *label == coset::Label::Int(COSE_HDR_PARAM_X5CHAIN) { Some(value) } else { None } }) - .ok_or_else(|| { - OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem("x5chain", "present")) - })?; + .ok_or(OcpEatError::InvalidToken( + "Missing x5chain in COSE protected header", + ))?; match value { Value::Array(arr) => arr.first(), @@ -150,12 +134,9 @@ fn extract_leaf_cert_der(unprotected: &Header) -> OcpEatResult> { Value::Bytes(bytes) => Some(bytes.clone()), // 👈 CLONE _ => None, }) - .ok_or_else(|| { - OcpEatError::CoseSign1(coset::CoseError::UnexpectedItem( - "x5chain", - "DER cert bytes", - )) - }) + .ok_or(OcpEatError::InvalidToken( + "Missing or invalid x5chain: expected DER-encoded certificate bytes", + )) } /// Extract raw P-384 public key coordinates (x, y) from DER X.509 cert From 13944f72edce2420d017193f49505e3dc9799041 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Wed, 7 Jan 2026 11:00:26 -0800 Subject: [PATCH 24/28] update the cli due to foramt change --- .github/workflows/spdm-validator.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/spdm-validator.yml b/.github/workflows/spdm-validator.yml index 78a04df59..d897f7dd1 100644 --- a/.github/workflows/spdm-validator.yml +++ b/.github/workflows/spdm-validator.yml @@ -144,9 +144,9 @@ jobs: working-directory: ocp-eat-verifier env: SPDM_VALIDATOR_DIR: ${{ github.workspace }}/spdm-emu/build/bin - run: | - ./target/release/ocptoken \ - --evidence $SPDM_VALIDATOR_DIR/measurement_block_fd.bin + run: | + ./target/release/ocptoken verify \ + -e $SPDM_VALIDATOR_DIR/measurement_block_fd.bin - name: Run SPDM validator tests on DOE transport From 668bbfd2924760ff69a831d8047e21b55987df30 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Wed, 7 Jan 2026 11:03:14 -0800 Subject: [PATCH 25/28] fmt in yaml --- .github/workflows/spdm-validator.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/spdm-validator.yml b/.github/workflows/spdm-validator.yml index d897f7dd1..7b81d568d 100644 --- a/.github/workflows/spdm-validator.yml +++ b/.github/workflows/spdm-validator.yml @@ -144,9 +144,9 @@ jobs: working-directory: ocp-eat-verifier env: SPDM_VALIDATOR_DIR: ${{ github.workspace }}/spdm-emu/build/bin - run: | - ./target/release/ocptoken verify \ - -e $SPDM_VALIDATOR_DIR/measurement_block_fd.bin + run: | + ./target/release/ocptoken verify \ + -e $SPDM_VALIDATOR_DIR/measurement_block_fd.bin - name: Run SPDM validator tests on DOE transport From be1458f611530a2f315f22d25569e8fa14891192 Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Thu, 8 Jan 2026 13:39:54 -0800 Subject: [PATCH 26/28] code review feedback --- .github/workflows/spdm-validator.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/spdm-validator.yml b/.github/workflows/spdm-validator.yml index 7b81d568d..ebd08f44f 100644 --- a/.github/workflows/spdm-validator.yml +++ b/.github/workflows/spdm-validator.yml @@ -136,17 +136,14 @@ jobs: fi - - name: Build ocptoken CLI - working-directory: ocp-eat-verifier - run: cargo build --release -p ocptoken - - - name: Decode SPDM measurement block using ocptoken + - name: Verify EAT from measurement block for SPDM-MCTP working-directory: ocp-eat-verifier env: SPDM_VALIDATOR_DIR: ${{ github.workspace }}/spdm-emu/build/bin run: | + cargo build --release -p ocptoken ./target/release/ocptoken verify \ - -e $SPDM_VALIDATOR_DIR/measurement_block_fd.bin + -e $SPDM_VALIDATOR_DIR/measurement_block_fd.bin - name: Run SPDM validator tests on DOE transport From 9cb5be12890190bf7cfe69a1648deea1c04ede2d Mon Sep 17 00:00:00 2001 From: Mei Lu Date: Fri, 9 Jan 2026 15:44:53 -0800 Subject: [PATCH 27/28] code review feedback --- ocp-eat-verifier/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocp-eat-verifier/Cargo.toml b/ocp-eat-verifier/Cargo.toml index 0a12b07ff..85a174219 100644 --- a/ocp-eat-verifier/Cargo.toml +++ b/ocp-eat-verifier/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" authors = ["Caliptra contributors"] [workspace.dependencies] -coset = { git = "https://github.com/google/coset", branch = "main" } +coset = { git = "https://github.com/google/coset",rev = "3ebd2d7d0dafe2b6856934ea2b4fa28ea3d9a373"} hex = "0.4" thiserror = "2.0" openssl = { version = "0.10", features = ["vendored"] } From f2c47745aab6099ae1428ced7c25ec880a985db5 Mon Sep 17 00:00:00 2001 From: Parvathi Bhogaraju Date: Tue, 13 Jan 2026 16:17:48 -0800 Subject: [PATCH 28/28] address review comments --- ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs | 1 - ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs index 6a6f00046..f9bebec00 100644 --- a/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/src/token/evidence.rs @@ -75,7 +75,6 @@ impl Evidence { /* ---------------------------------------------------------- * Verify ES384 signature * ---------------------------------------------------------- */ - // verify_signature_es384(&cose.signature, pubkey_x, pubkey_y, &sig_structure)?; cose.verify_signature(&[], |signature, to_be_signed| { verify_signature_es384(signature, pubkey_x, pubkey_y, to_be_signed) })?; diff --git a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs index dcb222f67..ba3923594 100644 --- a/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs +++ b/ocp-eat-verifier/ocptoken-rs/tests/test_evidence.rs @@ -17,7 +17,7 @@ use openssl::{ use ocptoken::token::evidence::Evidence; #[test] -fn decode_and_verify_cose_sign1() { +fn decode_and_verify_ecc_p384_cose_sign1() { // 1️ Generate ECC P-384 key pair let group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap(); let ec_key = EcKey::generate(&group).unwrap();