Skip to content

Commit

Permalink
Add expiration time to expected values
Browse files Browse the repository at this point in the history
This introduces (but does not yet use) the concept of validity for
expected values.

Other notes:
* rename `into_expected_digests` -> `to_expected_digests` by Rust
  convention.
* Add UnixTimestampMillis helper and use instead of inline 1000 *
* Added helper to convert between the Rust validity type and proto type.

Fixed: b/333580907

Change-Id: I3f0187668fccf63a4ee46c63a55e9de0dd709f22
  • Loading branch information
jblebrun committed Oct 11, 2024
1 parent d2490cf commit 82862eb
Show file tree
Hide file tree
Showing 10 changed files with 347 additions and 263 deletions.
9 changes: 6 additions & 3 deletions oak_attestation_verification/src/endorsement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ use time::OffsetDateTime;

use crate::{
rekor::{get_rekor_log_entry_body, verify_rekor_log_entry, verify_rekor_log_entry_ecdsa},
util::{convert_pem_to_raw, equal_keys, verify_signature, verify_signature_ecdsa},
util::{
convert_pem_to_raw, equal_keys, verify_signature, verify_signature_ecdsa,
UnixTimestampMillis,
},
};

/// URI representing in-toto statements. We only use V1, earlier and later
Expand Down Expand Up @@ -327,10 +330,10 @@ pub fn validate_statement(

match &statement.predicate.validity {
Some(validity) => {
if 1000 * validity.not_before.unix_timestamp() > now_utc_millis.into() {
if validity.not_before.unix_timestamp_millis() > now_utc_millis.into() {
anyhow::bail!("the claim is not yet applicable")
}
if 1000 * validity.not_after.unix_timestamp() < now_utc_millis.into() {
if validity.not_after.unix_timestamp_millis() < now_utc_millis.into() {
anyhow::bail!("the claim is no longer applicable")
}
}
Expand Down
66 changes: 45 additions & 21 deletions oak_attestation_verification/src/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ use prost::Message;

use crate::{
endorsement::{
get_digest, is_firmware_type, is_kernel_type, parse_statement, verify_binary_endorsement,
self, get_digest, is_firmware_type, is_kernel_type, parse_statement,
verify_binary_endorsement,
},
util::{hex_to_raw_digest, is_hex_digest_match, raw_digest_from_contents, raw_to_hex_digest},
};
Expand Down Expand Up @@ -389,12 +390,15 @@ pub(crate) fn get_expected_measurement_digest(
&public_keys.rekor_public_key,
)
.context("verifying binary endorsement")?;
Ok(into_expected_digests(&[hex_to_raw_digest(&get_digest(&parse_statement(
&endorsement.endorsement,
)?)?)?]))
let endorsement_statement = parse_statement(&endorsement.endorsement)
.context("parsing endorsement statement")?;
Ok(to_expected_digests(
&[hex_to_raw_digest(&get_digest(&endorsement_statement)?)?],
endorsement_statement.predicate.validity.as_ref(),
))
}
Some(binary_reference_value::Type::Digests(expected_digests)) => {
Ok(into_expected_digests(&expected_digests.digests))
Ok(to_expected_digests(&expected_digests.digests, None))
}
None => Err(anyhow::anyhow!("empty binary reference value")),
}
Expand Down Expand Up @@ -445,24 +449,28 @@ fn get_stage0_expected_values(
r#type: Some(expected_digests::Type::Skipped(VerificationSkipped {})),
}),
Some(binary_reference_value::Type::Endorsement(public_keys)) => {
let firmware_attachment = get_verified_stage0_attachment(
now_utc_millis,
endorsement.context("matching endorsement not found for reference value")?,
public_keys,
)
.context("getting verified stage0 attachment")?;
let endorsement =
endorsement.context("matching endorsement not found for reference value")?;

let firmware_attachment =
get_verified_stage0_attachment(now_utc_millis, endorsement, public_keys)
.context("getting verified stage0 attachment")?;

let endorsement_statement = parse_statement(&endorsement.endorsement)
.context("parsing endorsement statement")?;

Ok(into_expected_digests(
Ok(to_expected_digests(
firmware_attachment
.configs
.values()
.map(|digest| hex_to_raw_digest(digest).unwrap())
.collect::<Vec<RawDigest>>()
.as_slice(),
endorsement_statement.predicate.validity.as_ref(),
))
}
Some(binary_reference_value::Type::Digests(expected_digests)) => {
Ok(into_expected_digests(expected_digests.digests.as_slice()))
Ok(to_expected_digests(expected_digests.digests.as_slice(), None))
}

None => Err(anyhow::anyhow!("empty stage0 reference value")),
Expand Down Expand Up @@ -532,28 +540,38 @@ fn get_kernel_expected_values(
.setup_data
.ok_or_else(|| anyhow::anyhow!("no setup data digest in kernel attachment"))?;

let endorsement = endorsement.context("No endorsement provided")?;
let parsed_statement = parse_statement(&endorsement.endorsement)
.context("parsing endorsement statement")?;

Ok(KernelExpectedValues {
image: Some(into_expected_digests(&[hex_to_raw_digest(&expected_image)?])),
setup_data: Some(into_expected_digests(&[hex_to_raw_digest(
&expected_setup_data,
)?])),
image: Some(to_expected_digests(
&[hex_to_raw_digest(&expected_image)?],
parsed_statement.predicate.validity.as_ref(),
)),
setup_data: Some(to_expected_digests(
&[hex_to_raw_digest(&expected_setup_data)?],
parsed_statement.predicate.validity.as_ref(),
)),
})
}
Some(kernel_binary_reference_value::Type::Digests(expected_digests)) => {
Ok(KernelExpectedValues {
image: Some(into_expected_digests(
image: Some(to_expected_digests(
&expected_digests
.image
.as_ref()
.ok_or_else(|| anyhow::anyhow!("no image digests provided"))?
.digests,
None,
)),
setup_data: Some(into_expected_digests(
setup_data: Some(to_expected_digests(
&expected_digests
.setup_data
.as_ref()
.ok_or_else(|| anyhow::anyhow!("no setup_data digests provided"))?
.digests,
None,
)),
})
}
Expand Down Expand Up @@ -605,9 +623,15 @@ pub(crate) fn get_text_expected_values(
}
}

fn into_expected_digests(source: &[RawDigest]) -> ExpectedDigests {
fn to_expected_digests(
source: &[RawDigest],
claim_validity: Option<&endorsement::Validity>,
) -> ExpectedDigests {
ExpectedDigests {
r#type: Some(expected_digests::Type::Digests(RawDigests { digests: source.to_vec() })),
r#type: Some(expected_digests::Type::Digests(RawDigests {
digests: source.to_vec(),
validity: claim_validity.map(|cv| cv.into()),
})),
}
}

Expand Down
29 changes: 19 additions & 10 deletions oak_attestation_verification/src/expect/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ use oak_proto_rust::oak::{
use prost::Message;
use time::ext::NumericalDuration;

use crate::{test_util, util};
use crate::{
test_util::{self, GetValidity},
util::{self, UnixTimestampMillis},
};

#[test]
fn test_get_expected_measurement_digest_validity() {
// Create an endorsement of some arbitrary content.
let measured_content = b"Just some abitrary content";
let content_digests = util::raw_digest_from_contents(measured_content);
let endorsement = test_util::fake_endorsement(&content_digests, test_util::Usage::None);
let endorsement_validity = endorsement.predicate.validity.as_ref().expect("no validity");

// Now create the TR endorsement.
let (signing_key, public_key) = test_util::new_random_signing_keypair();
Expand All @@ -50,9 +54,9 @@ fn test_get_expected_measurement_digest_validity() {
// endorsement signing key.
let reference_value = test_util::binary_reference_value_for_endorser_pk(public_key);
// Pretend it's a week into validity.
let now =
endorsement.predicate.validity.as_ref().unwrap().not_before.saturating_add((7).days());
let now_utc_millis = 1000 * now.unix_timestamp();
let now_utc_millis =
endorsement.validity().not_before.saturating_add((7).days()).unix_timestamp_millis();

let expected_digests = super::get_expected_measurement_digest(
now_utc_millis,
Some(&tr_endorsement),
Expand All @@ -61,10 +65,12 @@ fn test_get_expected_measurement_digest_validity() {
.expect("failed to get digests");

// Assert that the expected digests are those from the verified endorsement.

assert_eq!(
expected_digests,
ExpectedDigests {
r#type: Some(expected_digests::Type::Digests(RawDigests {
validity: Some(endorsement_validity.into()),
digests: vec![content_digests],
})),
}
Expand All @@ -89,6 +95,7 @@ fn test_get_stage0_expected_values_validity() {
let subject_digests = util::raw_digest_from_contents(&serialized_subject);
let (signing_key, public_key) = test_util::new_random_signing_keypair();
let endorsement = test_util::fake_endorsement(&subject_digests, test_util::Usage::Firmware);
let endorsement_validity = endorsement.predicate.validity.as_ref().expect("no validity");
let (serialized_endorsement, endorsement_signature) =
test_util::serialize_and_sign_endorsement(&endorsement, signing_key);
let tr_endorsement = TransparentReleaseEndorsement {
Expand All @@ -103,9 +110,8 @@ fn test_get_stage0_expected_values_validity() {
// endorsement signing key.
let reference_value = test_util::binary_reference_value_for_endorser_pk(public_key);
// Pretend it's a week into the validity period.
let now =
endorsement.predicate.validity.as_ref().unwrap().not_before.saturating_add((7).days());
let now_utc_millis = 1000 * now.unix_timestamp();
let now_utc_millis =
endorsement.validity().not_before.saturating_add((7).days()).unix_timestamp_millis();
let expected_digests =
super::get_stage0_expected_values(now_utc_millis, Some(&tr_endorsement), &reference_value)
.expect("failed to get digests");
Expand All @@ -115,6 +121,7 @@ fn test_get_stage0_expected_values_validity() {
expected_digests,
ExpectedDigests {
r#type: Some(expected_digests::Type::Digests(RawDigests {
validity: Some(endorsement_validity.into()),
digests: vec![content_digests],
})),
}
Expand All @@ -140,6 +147,7 @@ fn test_get_kernel_expected_values_validity() {
let subject_digests = util::raw_digest_from_contents(&serialized_subject);
let (signing_key, public_key) = test_util::new_random_signing_keypair();
let endorsement = test_util::fake_endorsement(&subject_digests, test_util::Usage::Kernel);
let endorsement_validity = endorsement.predicate.validity.as_ref().expect("no validity");
let (serialized_endorsement, endorsement_signature) =
test_util::serialize_and_sign_endorsement(&endorsement, signing_key);
let tr_endorsement = TransparentReleaseEndorsement {
Expand All @@ -154,9 +162,8 @@ fn test_get_kernel_expected_values_validity() {
// endorsement signing key.
let reference_value = test_util::kernel_binary_reference_value_for_endorser_pk(public_key);
// Pretend it's a week into the validity period.
let now =
endorsement.predicate.validity.as_ref().unwrap().not_before.saturating_add((7).days());
let now_utc_millis = 1000 * now.unix_timestamp();
let now_utc_millis =
endorsement.validity().not_before.saturating_add((7).days()).unix_timestamp_millis();
let expected_digests =
super::get_kernel_expected_values(now_utc_millis, Some(&tr_endorsement), &reference_value)
.expect("failed to get digests");
Expand All @@ -166,6 +173,7 @@ fn test_get_kernel_expected_values_validity() {
expected_digests.image,
Some(ExpectedDigests {
r#type: Some(expected_digests::Type::Digests(RawDigests {
validity: Some(endorsement_validity.into()),
digests: vec![image_digests],
})),
})
Expand All @@ -174,6 +182,7 @@ fn test_get_kernel_expected_values_validity() {
expected_digests.setup_data,
Some(ExpectedDigests {
r#type: Some(expected_digests::Type::Digests(RawDigests {
validity: Some(endorsement_validity.into()),
digests: vec![setup_digests],
})),
})
Expand Down
5 changes: 3 additions & 2 deletions oak_attestation_verification/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use oak_proto_rust::oak::attestation::v1::{
};
pub use util::{
convert_pem_to_raw, hex_to_raw_digest, raw_to_hex_digest, reference_values_from_evidence,
UnixTimestampMillis,
};

/// Verifies a signed endorsement against a reference value.
Expand All @@ -57,8 +58,8 @@ pub fn verify_endorsement(
Ok(EndorsementDetails {
subject_digest: Some(digest),
validity: Some(Validity {
not_before: 1000 * validity.not_before.unix_timestamp(),
not_after: 1000 * validity.not_after.unix_timestamp(),
not_before: validity.not_before.unix_timestamp_millis(),
not_after: validity.not_after.unix_timestamp_millis(),
}),
})
}
16 changes: 12 additions & 4 deletions oak_attestation_verification/src/test_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ use oak_proto_rust::oak::{
use p256::{ecdsa::signature::Signer, pkcs8::EncodePublicKey, NistP256, PublicKey};
use time::macros::datetime;

use crate::endorsement::{
self, DefaultPredicate, DefaultStatement, Subject, Validity as EndorsementValidity,
};
use crate::endorsement::{self, DefaultPredicate, DefaultStatement, Statement, Subject};

pub enum Usage {
None,
Expand Down Expand Up @@ -56,7 +54,7 @@ pub fn fake_endorsement(digests: &RawDigest, usage: Usage) -> DefaultStatement {
predicate: DefaultPredicate {
usage: usage.to_string(),
issued_on: datetime!(2024-10-01 12:08 UTC),
validity: Some(EndorsementValidity {
validity: Some(endorsement::Validity {
not_before: datetime!(2024-09-01 12:00 UTC),
not_after: datetime!(2024-12-01 12:00 UTC),
}),
Expand Down Expand Up @@ -141,3 +139,13 @@ fn raw_digest_to_map(h: &RawDigest) -> BTreeMap<String, String> {

map
}

pub trait GetValidity {
fn validity(&self) -> &endorsement::Validity;
}

impl GetValidity for Statement<DefaultPredicate> {
fn validity(&self) -> &endorsement::Validity {
self.predicate.validity.as_ref().expect("missing validity")
}
}
32 changes: 31 additions & 1 deletion oak_attestation_verification/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ use oak_proto_rust::oak::{
KernelBinaryReferenceValue, KernelDigests, KernelLayerData, KernelLayerReferenceValues,
KeyType, OakContainersReferenceValues, OakRestrictedKernelReferenceValues, ReferenceValues,
RootLayerData, RootLayerReferenceValues, Signature, SkipVerification, StringLiterals,
SystemLayerReferenceValues, TextReferenceValue, VerifyingKeySet,
SystemLayerReferenceValues, TextReferenceValue, Validity, VerifyingKeySet,
},
HexDigest, RawDigest,
};
use p256::pkcs8::{der::Decode, DecodePublicKey};
use prost::Message;
use prost_types::Any;
use sha2::{Digest, Sha256, Sha384, Sha512};
use time::OffsetDateTime;

use crate::endorsement;

const PUBLIC_KEY_PEM_LABEL: &str = "PUBLIC KEY";

Expand Down Expand Up @@ -461,5 +464,32 @@ pub fn decode_protobuf_any<M: Message + Default>(
})
}

/// Return a milliseconds-since-the-epoch timestamp value.
/// ///
/// Endorsement validity structures in our JSON-based endorsements use
/// milliseconds resolution, but [`OffsetDateTime`] provides only seconds or
/// nanoseconds since the epoch.
///
/// This bridges a convenience gap, and helps with readability of code that
/// works with validity times.
pub trait UnixTimestampMillis {
fn unix_timestamp_millis(&self) -> i64;
}

impl UnixTimestampMillis for OffsetDateTime {
fn unix_timestamp_millis(&self) -> i64 {
self.unix_timestamp() * 1000
}
}

impl From<&endorsement::Validity> for Validity {
fn from(value: &endorsement::Validity) -> Validity {
Validity {
not_before: value.not_before.unix_timestamp_millis(),
not_after: value.not_after.unix_timestamp_millis(),
}
}
}

#[cfg(test)]
mod tests;
7 changes: 3 additions & 4 deletions oak_attestation_verification/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ pub fn to_attestation_results(

/// Verifies entire setup by forwarding to individual setup types.
///
/// This just fetches expected values using [get_expected_values], and then
/// calls [verify_with_expected_values] with those.
/// This just fetches expected values using [`expect::get_expected_values`],
/// and then call [`verify_with_expected_values`] providing those values.
///
/// If you'd like to cache and reuse the values, call those two methods
/// indepedently, and cache the results of the first.
/// independently, and cache the results of the first.
pub fn verify(
now_utc_millis: i64,
evidence: &Evidence,
Expand All @@ -101,7 +101,6 @@ pub fn verify(
}

/// Verifies entire setup by forwarding to individual setup types.
/// This variant returns expected values along with the extracted evidence.
pub fn verify_with_expected_values(
now_utc_millis: i64,
evidence: &Evidence,
Expand Down
Loading

0 comments on commit 82862eb

Please sign in to comment.