Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt EAR in Attestation Service #353

Open
fitzthum opened this issue Mar 21, 2024 · 5 comments · May be fixed by #516
Open

Adopt EAR in Attestation Service #353

fitzthum opened this issue Mar 21, 2024 · 5 comments · May be fixed by #516

Comments

@fitzthum
Copy link
Member

Following @thomas-fossati's excellent presentation about EAR in the community meeting, I think we should move towards replacing/reformating our attestation token to use EAR. There are a few pieces that we'll need to think about.

Currently the AS returns a JWT with these claims inside of it. Code for token issueing is here.

let token_claims = json!({
    "tee": to_variant_name(&tee)?,
     "evaluation-reports": policies,
      "tcb-status": flattened_claims,
      "reference-data": reference_data_map,
      "customized_claims": {
          "init_data": init_data_claims,
           "runtime_data": runtime_data_claims,
      },
});

Part of the question is how we can map these claims onto EAR. I don't have a thorough knowledge of the EAR format yet, so I don't have a lot to say about this. I do wonder if we should drop the reference-data field. I am also not sure if there is something like the TEE field. I guess EAR is supposed to be platform independent, but it is reasonable for a KBS to have policies based on this info (i.e. don't release secrets to a guest with the sample attester).

There are two deeper questions that I am concerned about. First, how do we produce the EAR trustworthiness claims. Currently evidence validation in the AS is based on OPA. Each verifier plugin extracts a claims map (also stored in the tcb-status field of attestation token) which is fed into the OPA engine. The output of the policy evaluation is stored in the policies field. Note that we support evaluation of multiple different policies. I'm not sure if we should continue with that or how to work it into the EAR format.

The bigger issue is that we currently have very loose requirements for what the AS validation produces. The OPA policies can essentially return anything. It is up to the client to synchronize their AS policies with their resource policies in the KBS. This is quite different from the EAR approach where the format of the attestation results are more strongly structured. I'm not sure how we should account for this. Maybe we can provide OPA policies that transform claims maps into trustworthiness claims. Maybe we should get rid of the OPA engine in the AS entirely. This is related to some of the discussion here.

Another issue is how we handle init_data. The init_data expresses the configuration of many components in the guests. This reflects crucial information about the guest TCB and potentially about the workload as well. Inidata is not hardware specific. Currently the AS ensures a binding between the hardware evidence and the initdata, then it is up to the policy to validate the initdata. Even without EAR I think we're a little uncertain whether the inidata should be validated in the AS or the KBS. Since it is closely related to the resources that are allowed, this should probably be handled by the KBS. I guess this means that it would not be part of the EAR? This flow is still unclear to me.

Overall I think the EAR is a good approach, so let's see if we can get the ball rolling here.

cc: @Xynnn007 @mkulke @mythi @sameo

@Xynnn007
Copy link
Member

cc @jialez0

@fitzthum
Copy link
Member Author

Another thing is that if we move to EAR, we can probably change the way we do the integration with Veraison.

@Xynnn007
Copy link
Member

Xynnn007 commented Mar 30, 2024

Well, let me share some thoughts after reading EAR Document

Let me divide into four parts, s.t.

  1. What we are doing now in CoCo-AS
  2. What the idea of EAR
  3. How to integrate EAR into CoCo-AS and challenges.
  4. Potential influence upon KBS

What we are doing now in CoCo-AS

To sum up, CoCo-AS does two things upon an evidence

  1. Verify the endorsement of the evidence, s.t. ensure it is generated by a legal TEE platform manufactured by a specified HW vendor.
  2. Verify the firmware/software status running inside is as expected.

Point 1 is achieved by verifying the signature of the evidence, together the claimed pubkey, to check if
the signature is for the whole evidence, and if the public key is accepted by the HW vendor's root of trust. For example,
TDX the PC(C)S service is involved to ensure this, and for SNP an AMD key chain.

Point 2 is achieved by verifying the so-called "claims", s.t. the fields in evidence with specific semantics. For example,
TDX has mr_td and SNP has measurement fields.

The part 1 is natural and I think almost all the remote attestation services should firstly ensure this.
The part 2 is somehow a straightforward thing, as it is easiest for us to compare, field to field. BTW, the current
reference value provider service (RVPS) implementation follows the principle, s.t. providing a way to make claim to claim comparation easier.

What the idea of EAR

To my understanding, the core part of EAR is the ar4si.trustworthiness-vector map, s.t. submods. The items inside the map is decided by concrete
software components. For example, the example in section 3.3.1 obviously shows this. The claim PSA (platform firmware for ARM CCA) has different properties
to represent the verification result.

The difference between what CoCo-AS is doing now and EAR is how thing 2 as mentioned. Concretely,

  1. it is based on software component entities as the granularity.
  2. Not only a yes-or-no status, but different tiers.

What does this mean? The submods explicitly shows every concrete software/firmware components and the verification result
shows concretely the attributes (like configuration, executables) and usages (like storage-opaque).

How to integrate EAR into CoCo-AS and challenges.

Let's see the EAR definition

EAR = {
  eat.profile => "tag:github.com,2023:veraison/ear"
  iat => int                                                // issure time
  ear.verifier-id => ar4si.verifier-id                      // builder and developer of CoCo-AS
  ? ear.raw-evidence => ear-bytes                           // raw evidence bytes
  eat.submods => { + text => EAR-appraisal }                // submods
  ? eat.nonce => eat.nonce-type                             // provided by requester
  * $$ear-extension
}

iat can be generated runtimely; ear.verifier-id can be speficied when building and the launch time. nonce can be specified by adding
an extra optional parameter to API of CoCo-AS.

The only one thing left is EAR-appraisal, defined as following

EAR-appraisal = {
  ear.status => $ar4si.trust-tier
  ? ear.trustworthiness-vector => ar4si.trustworthiness-vector
  ? ear.appraisal-policy-id => text
  * $$ear-appraisal-extension
}

submods of EAR is for component. ear.appraisal-policy-id could be the policy id defined by CoCo-AS.

So the core problem is to mapping evidence claims of CoCo-AS (so-called parsed-claims) to the ear.status and ear.trustworthiness-vector.

However, we know that different software components will be reflected by one (or more) parsed-claims directly (or indirectly).
For example:

  1. Directly: TDX's mr_td usually reflects the initialized guest memory layout, s.t. guest-firmware.
  2. Indirectly: mostly are in this case. For example, the guest kernel is reflected by an eventlog entry on TDX, and the eventlog's
    integrity is ensured by claims rt_mr. What's more, on SNP (without vTPM support), all guest firmware, kernel and rootfs are reflected
    by one claim measurement.

So, to handle this, we should handle the following problems:

  1. The software producer/builder side (like CoCo community, or kata artifect GHA & ORAS) should provide the following information
    • 1.1 the software component artifects
    • 1.2 the reference value manifests, which at least include the digest to be matched, and how to be matched (in CoCo it is relatively easy as this could be specified by a claim key)
    • 1.3 what AR4SI vector is the thing related to, e.g. initdata could be configuration, file-system could the rootfs hash by dm-verity. Two places to set this: in the reference value manifest, or in the CoCo-AS policy. The score is relatively easy to apply, e.g. if the hash is not matched, we can always mark as 99.
  2. Consumer, including
    • 2.1 Policy implementation inside CoCo-AS. This is mentioned in 1.3. If AR4SI vector mapping is defined in CoCo-AS Policy, an intermediate policy language/expression might help. And a final logic inside AS will calculate automatically the ear.status by select the min value of all ear.trustworthiness-vectors.

Potential influence upon KBS

If we have a good CoCo-AS implemented as mentioned, which would receice (evidence, policy_ids) and reply a EAR, we should also make some changes upon KBS.

Currently, KBS would leverage CoCo-AS' attestation result to decide whether to authorize the KBS-client. If yes, a CoCo-AS token with a public key will be delivered to the KBS client. However, if we apply EAR on CoCo-AS, KBS would only care abstract result of the request and will not response the coupled CoCo-AS token thing. The policy design inside KBS would still break in some way. (Now the access to a resource is determined usually by some specific claims, which is somehow coupled with AS ability but works fine temporarily)

So, we might need to decouple the things. That means there should be two parts of KBS:

  1. Authentication and Authorization service part. The API of which is register(). This is a 4-pass protocol, include current Request-Challenge-Attestation-Response passes. The result is to deliver a "credential" defined by KBS. The design should be plug-in implemented. The KBS client would bring a specific id defined by KBS, and this id is for KBS to decide which policy_ids are used to request CoCo-AS, what ear.trustworthiness-vector are cared, what is the minimum acceptable level of ear.status and which concrete "credential" would be return. If the EAR returned by CoCoAS matches the expectation of the id, KBS will return the "credential" to the client. The "credential" is only related to KBS and should not have anything coupled with the EAR (I mean embed into the EAR).
  2. Permission control service part. Currently KBS provides a simple permission control system facilited by an OPA engine. This could be one concrete implementation of a simple permission control service, but more abstractly, we should provide a more universal API to connect different permission control service. This means the API would be get_resource(resource_uri, credential). Here the credential is the one mentioned in part 1. For an opensource reference implementation, KBS would have its own credential like the current JWT. It could also be an OIDC token issured by another existing AuthN&AuthZ service, which only wants to leverage KBS to provide the ability to authenticate a requester by remote attestation, or an AK (Access Key) for some cloud service. The permission control service is a out-of-band system aligned with the "credential", and the code in KBS could work just as a forwarder to the real permission control service.

@fitzthum
Copy link
Member Author

fitzthum commented Jul 30, 2024

Ok, I finally read @Xynnn007's response above and I have looked through the spec.

I think we can integrate EAR, but there is one potential problem. First, the good news.

EAR support can be separate from CoRIM support. The EAR crate can replace our existing token generation code. This is probably a good simplification.

We can generate an EAR token in Rust like this

  let token = Ear{
        profile: "test".to_string(),
        iat: 1,
        vid: VerifierID {
            build: "vsts 0.0.1".to_string(),
            developer: "https://veraison-project.org".to_string(),
        },
        raw_evidence: None,
        nonce: None,
        submods: BTreeMap::from([("test".to_string(), Appraisal::new())]),
    };

It's easy enough to populate these fields. The only one that really matters is the submods field.

The submods contains a map of keys (e.g. "CPU") and appraisals. The appraisal struct looks like this.

pub struct Appraisal {
    /// The overall status of the appraisal represented by an AR4SI trustworthiness tier
    ///
    /// This is typically the lowest tier of all the claims that have been made (who's values have
    /// been set), though a verifier may chose to set it to a lower value.
    pub status: TrustTier,
    /// Contains the trustworthiness claims made in the appraisal
    pub trust_vector: TrustVector,
    /// Identifier of the policy applied by the verifier
    pub policy_id: Option<String>,
    /// Evidence claims extracted and annotated by the verifier from the evidence supplied by the
    /// attester
    pub annotated_evidence: BTreeMap<String, RawValue>,
    /// Addition claims made as part of the appraisal based on the policy indicated by `policy_id`
    pub policy_claims: BTreeMap<String, RawValue>,
    /// Claims about the public key that is being attested
    pub key_attestation: Option<KeyAttestation>,
}

The most important field here is the TrustVector.

As Ding points out the question is really how do we generate a TrustVector from the claims map. We discussed using a DSL in the past, but this wasn't very popular. We should be able to generate a TrustVector from an OPA policy.

The TrustVector struct looks like this.

pub struct TrustVector {
    pub instance_identity: TrustClaim,
    pub configuration: TrustClaim,
    pub executables: TrustClaim,
    pub file_system: TrustClaim,
    pub hardware: TrustClaim,
    pub runtime_opaque: TrustClaim,
    pub storage_opaque: TrustClaim,
    pub sourced_data: TrustClaim,
}

One possible approach would be to expect the OPA Policy to have at least one claim that matches the fields of this struct. We would fill in the others with default values. We'll have to think carefully about what these defaults should be. Another question is whether we should only set one submodule (the CPU one, presumably) or if we should somehow support mapping multiple policies onto different submodules.

There are a few other values to figure out, but in general this seems relatively straightforward. We would keep the KBS policy engine roughly the same, although we would have to rework our default and example policies.

Here's the not-so-great news.

Currently our token contains the following fields.

        let token_claims = json!({
            "tee": to_variant_name(&tee)?,
            "evaluation-reports": policies,
            "tcb-status": flattened_claims,
            "reference-data": reference_data_map,
            "customized_claims": {
                "init_data": init_data_claims,
                "runtime_data": runtime_data_claims,
            },
        });

The EAR spec is apparently extensible, but I don't think the Rust implementation supports adding arbitrary fields to the token. If not, we don't really have any equivalent of several of these fields. In fact, the only thing that is really represented by the EAR tokens is the evaluation-reports field. Is this a problem? Currently we do use the TEE field in several of our KBS policies. We would need to rethink things if we remove it. We may want to expose the init_data to the KBS policy as well.

@fitzthum
Copy link
Member Author

Ok, I've actually started to work on this. PR should be ready in a few days.

@fitzthum fitzthum linked a pull request Oct 4, 2024 that will close this issue
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants