-
Notifications
You must be signed in to change notification settings - Fork 88
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
EAR Support #516
base: main
Are you sure you want to change the base?
EAR Support #516
Conversation
a6068df
to
fe6af93
Compare
This is ready for review. I tried to make this as simple as possible, but there are a lot of interlocking pieces. Please read the commit messages. They will hopefully explain what is happening and highlight a couple of significant changes (like not flattening the tcb claims anymore, not supporting multiple policies, etc) We still have some issues to resolve in the underlying crate before we can merge.
|
007f110
to
e3fa251
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Tobin. Nice work about code, tests and documents. I am excited to see that the design that leaves the trusted anchor calculation to users.
I have not dived into the code too deeply, but only for a quick catch for the architecture design.
This PR abondons former AS token format. Maybe we could leave the token plugins still. Probably the difficulty is where should OPA lie. My suggestion is to make OPA a common lib for both legacy AS token and EAR.
In this way, the token type could be configured in the launch toml of AS. Such as
[token]
type = "ear"
# other configs for ear
or
[token]
type = "simple"
# other configs for simple
This can be implemented by
#[serde(tag = "type")]
pub enum AttestationTokenConfig {
Ear(EarConfig),
Simple(SimpleConfig),
}
pub type AttestationTokenBroker = Arc<dyn AttestationTokenBrokerTrait + Send + Sync>;
impl TryFrom<AttestationTokenConfig> for AttestationTokenBroker {
type Error = Error;
fn try_from(value: AttestationTokenConfig) -> Result<Self> {
match value {
AttestationTokenConfig::Ear(config) => {
let ear_broker = Arc::new(EarBroker::new(config)?);
Ok(ear_broker as _)
}
AttestationTokenConfig::Simple(config) => {
let simple_broker = Arc::new(SimpleBroker::new(config)?);
Ok(simple_broker as _)
}
}
}
}
This would give enough flexibility to downstream users to define their different token format, although they might not be standard as EAR. But for CoCo, we can support EAR by default.
I concur that the format for generating tokens should be modular. We can default to supporting EAR, but simultaneously, retain the previous simple AS token mode as a plugin option. This approach will provide ample room for customization to the users of this project. |
@Xynnn007 @jialez0 You are both right that this PR makes our token provisioning less generic. In fact, it's no longer generic at all. There are some reasons for this. First, with EAR tokens the policy is fundamentally tied to the token. Our CoCo token supports just about any policy claims, but with EAR we need to generate an Appraisal. This means that 1) the policy is specific to the type of token we use. 2) A bunch of the code surrounding the policy is specific to the type of token we are using. The first thing isn't a huge problem. It's not a great user experience to require totally different policies for different tokens, but at least that is configurable at runtime. The second thing is a bit more of a problem. There is code in both the policy engine and One option would be to move most of the logic into the token generation code. The policy engine would always process all the claims and return the full results. The results would be provided as-is to the token broker. The token broker would also get the TCB claims and any other pieces of the token. I don't really like broadening the scope of the token broker like this. If we want to keep the token broker abstraction the same, we would probably have to add a bunch of conditional logic to the policy engine and It's possible to keep both tokens, but there would be costs. Keep in mind that we would need to create more tests, more examples, extend the docs, etc. Besides the costs, I actually think there are advantages to supporting fewer things. So far we have tried to make Trustee as generic as possible. In general I think this is good, but currently I think we are in danger of giving users too many ways to configure things without showing them any particular way that actually works end-to-end. I think part of the reason why we have some gaps with policies, the RVPS, etc is that we haven't been prescriptive enough. I think EAR tokens are a good fit because they are supposed to be generic, but they are more opinionated. Anyway, this change wasn't an accident, but I understand that it is kind of scary. I am open to trying to support both, but my question for you is what is the value of keeping the CoCo token? |
Another way we could potentially compromise is by keeping the CoCo token but populating the policy results with an EAR-like Appraisal. I'm not sure if this really makes sense tho. |
Hi @fitzthum
Actually we are trying to define some own token format in downstream pre-productions benefits from the original plugin design. Let's go back to your worries. The core issues are 1. token generation code and 2. user interface. For 1,
{
"work_dir": "/opt/confidential-containers/attestation-service",
"policy_engine": "opa",
"rvps_config": {
"remote_addr":"http://rvps:50003"
},
"attestation_token_broker": "Simple",
"attestation_token_config": {
"duration_min": 5
}
} After some change, it would be {
"work_dir": "/opt/confidential-containers/attestation-service",
"policy_engine": "opa",
"rvps_config": {
"remote_addr":"http://rvps:50003"
},
"attestation_token_broker": {
"type": "XXX",
...// Other options
}
"attestation_token_config": {
"duration_min": 5
}
} The default one we could set as EAR, together with all other e2e examples. Although we provide "too" many ways to run trustee, I believe most users will directly use the default configuration of our e2e test (probably EAR or Simple token), which needs to be ensured to run normally by us. Some advanced users might want do extension, then the value of extensibility values. |
Ok. This is a valid consideration. Ideally we could converge on one token, but I'm not sure what your requirements are and they might not be public. Keep in mind that the EAR token is in some ways very similar to our CoCo token. For instance, they are both JWT. The EAR token stores a public key in a similar way but with a different path. Consumers can somewhat ignore parsing the AR4SI Trust Claims by just looking at the status field of the Appraisal.
I think we should try to keep the policy engine modular. My opinion of rego/opa is declining and I would love to provide another option (as long as we don't invent it ourselves). Btw, this PR also switches from evaluating multiple policies with the AS, to only taking one policy id. If we keep support for the CoCo token, can it just take one policy or does it need to do multiple? |
One policy is ok for now. |
f7349c5
to
51654bc
Compare
I will rebase this again next week. All of the underlying issues with the EAR crate have been fixed and updated here. PTAL (at least at the high-level) @Xynnn007 @mkulke @mythi et al I'd like to either merge this soonish or abandon it and continue working on fixing some issues with RVPS and other stuff that I noticed while working on this. |
This commit allows the AS to issue EAR tokens with the help of the rust-ear crate. EAR tokens require particular claims. This creates a binding between the AS policy and the EAR token. Specifically, the policy engine must return an EAR appraisal. The policy engine is still generic. Multiple policy engines could be implemented as long as they create an appraisal. Since policy evaluation is now closely tied to the type of token we are going to generate, make the policy engine more generic and move the logic around calling the policy engine out of lib.rs and into the token broker. There are a few other changes, including that the policy engine no longer takes multiple policies. For now, we only evaluate the first policy in the policy list, but future commits will change this convention so that we only ever think about one policy for the attestation service (until we introduce support for validating multiple devices at once). EAR Tokens also do not use flattened claims. The TCB claims are currently flattened so that we can use the key names as the input to the RVPS. This commit breaks this functionality, but a future commit will change the way the RVPS works to accomodate. There isn't a direct pairing between claim names and reference values, so there is no reason to keep flattening all the claims, especially because the flattening code has some corner cases that it does not support. This commit also adds the init_data_claims and runtime_data_claims to the tcb claims as long as the corresponding claims about the hashes are already there. This will allow the init_data to travel with the token, which will be convenient except if the init_data is too big. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]> Signed-off-by: Xynnn007 <[email protected]>
For EAR tokens we require the public key to be set. There is no option to deserialize a token without validating the signature. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
A keypair is required to sign and validate the attestation token. In the past this was optional, but now it is not. Update the docker-compose manifest and configs to pass in this new keypair and update the docs to tell people how to generate it. This does complicate the user experience, but things are not secure without it. That said, we may be able to implement this automatically in a future PR. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
Now we need to provision a keypair for signing and validatig the attestation tokens. Add this keypair to the docker e2e test Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
The sample attester is enabled by default. Remove setting the environment variable that used to enable it. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
We now require a keypair for signing/validating the attestation token. Add this keypair to our k8s deployment tooling. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
We now require a keypair to sign/validate the attestation token. Add this keypair to the e2e test. Interestingly, we were using a keypair for validating the old CoCo token in this test, but only for the passport mode. Even in background check mode, this keypair is required or the token won't be validated at all. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
Previously we expected the caller of the RVPS to provide a name for the reference value that they wanted. In the AS we were flattening the TCB claims to get this name. Ultimately, the names of the TCB claims do not map directly onto the names of the required reference values. This changes the interface to have the RVPS determine which reference values to send. At the moment, it simply sends all of them. This allows the reference values that are used to mostly be set within the policy itself, which is probably a good idea. In the future, the RVPS should be improved to include a context abtraction that allows groups of reference values to be provided to the AS. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
THe skeleton for a policy that can be used to validate the TCB claims of all platforms in the context of confidential containers. Only sample and snp are supported currently, but this should give a good idea of how to extend the policy to other platforms. There are a few tweaks we can make later, such as supporting `>` or `<` comparisons. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
Update the attestestion service policy docs to describe the requirements for policies that will generate EAR tokens. Also update various example and default policies. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
EAR tokens do not support an expiration claim by default, but fortunately we can use the Extension framework to add an `exp` field that will match what we would expect in a JWT. Add this extension and check it when we validate the token. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
Now that the as policy engine is more generic, re-enable and update the test. This test is slightly tied to EAR. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
The rust-ear crate now supports null json value so we can remove the workaround we had to make sure null value didn't cause an exception. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more nits and let's see what if a rebase comes
env_logger = { workspace = true, optional = true } | ||
futures = "0.3.17" | ||
hex.workspace = true | ||
jsonwebtoken = "9.3.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jsonwebtoken
is imported in workspace Cargo.toml. We can directly jsonwebtoken.workspace = true
here
@@ -59,6 +61,7 @@ serde_variant = "0.1.2" | |||
sha2.workspace = true | |||
shadow-rs.workspace = true | |||
strum.workspace = true | |||
tempfile = "3.13.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also imported
tempfile = "3.13.0" | |
tempfile.workspace = true |
"attestation_token_broker": "Simple", | ||
"attestation_token_config": { | ||
"attestation_token_broker": { | ||
"type": "Simple", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to mark this as Ear
to align with what we changed in README.md
? s.t.
If the policy is not updated, the AS will use the default policy.
@@ -1,6 +1,29 @@ | |||
# Attestation Policy (Developing) | |||
|
|||
CoCo AS provides a flexible policy support based on Rego to facilitate the customized verification rules. | |||
The policies are used to general an EAR appraisal, which is part of the EAR token returned by the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might need to claim that different token type needs different policy formattings. The added part is for Ear
let policy_ids = if request.policy_ids.is_empty() { | ||
info!("no policy specified, use `default`"); | ||
vec!["default".into()] | ||
} else { | ||
request.policy_ids | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noticed that https://github.com/confidential-containers/trustee/pull/516/files#diff-a4f0031656042cef2f9c6da58178868fb7f80de2d2f094d33cc38196135e2d1aR199 we will raise an error if no policy is selected. Do we need to keep this part of code?
#[derive(Deserialize, Debug, Clone, PartialEq)] | ||
pub struct TokenSignerConfig { | ||
pub key_path: String, | ||
pub cert_url: Option<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add
#[serde(default = "Option::default")]
pub cert_url: Option<String>, | ||
|
||
// PEM format certificate chain. | ||
pub cert_path: Option<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add
#[serde(default = "Option::default")]
/// Configuration for signing the token. | ||
/// If this is not specified, the token | ||
/// will be signed with an ephemeral private key. | ||
pub signer: Option<TokenSignerConfig>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add
#[serde(default = "Option::default")]
extensions, | ||
}; | ||
|
||
let signed_ear = ear.sign_jwt_pem(Algorithm::ES256, &self.private_key_bytes)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We now supports to embed jwk
part or kid
part into the JWT with veraison/rust-ear#29 / https://docs.rs/ear/0.3.0/ear/struct.Ear.html#method.sign_jwt_pem_with_header. Thus we can embed the public key part of self.private_key_bytes
. In this way
- if KBS side set
insecure_key = true
. This public jwk can be used to verify the integrity of the jwt. - if KBS side set
insecure_key = false
. Some extra trusted root certificate should be set on KBS config side and this public jwk can be used to verify the integrity of the jwt
pub enum AttestationTokenVerifierType { | ||
Jwk, | ||
#[default] | ||
Ear, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see the previous comment of signing token part of AS' EAR token side. Now EAR token generation will result in a same format of JWT that Simple/ITA uses. Thus we do not the extra token verifier kind but they can share a common one.
@@ -5,7 +5,8 @@ private_key = "./work/https.key" | |||
certificate = "./work/https.crt" | |||
|
|||
[attestation_token_config] | |||
trusted_certs_paths = ["./work/token-cert.pem"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we can remove the TOKEN_* code from the Makefile?
let policy_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD | ||
.decode(policy) | ||
.map_err(RegoError::Base64DecodeFailed)?; | ||
.map_err(PolicyError::Base64DecodeFailed)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if a library returns an error, i.e. not a generic anyhow, like base64::DecodeError
we can just use a #from annotation in the error enum and don't have to map the error explicitly.
##### TDX TODO | ||
##### AZ SNP TODO | ||
##### AZ TDX TODO | ||
##### SE TODO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will all of those TEE fail to verify if EAR tokens are used?
This is the very beginning of this PR. I still need to fix some issues with the underlying crate, add support for verifying tokens in the KBS, and fixup all the tests, examples, and default values.
See commit messages for more info.
Fixes: #353
Progress so far: