An implementation of EAT Attestation Results token.
This crate provides an implementation of attestation results tokens that conforms to EAT
Attestation Results [draft-fv-rats-ear] specification. This defines a token intended to
communicate a set of appraisals of attested evidence produced by a verifier. Each appraisal is
based around a set of trust claims defined by Attestation Results for Secure Interactions
(AR4SI) [draft-ietf-rats-ar4si].
The attestation result may be serialized as a signed JSON or CBOR token (using JWT and COSE,
respectively).
[draft-fv-rats-ear]: https://datatracker.ietf.org/doc/draft-fv-rats-ear/
[draft-ietf-rats-ar4si]: https://datatracker.ietf.org/doc/draft-ietf-rats-ar4si/
# Examples
## Signing
```rust
use std::collections::BTreeMap;
use ear::{Ear, VerifierID, Algorithm, Appraisal, Extensions};
const SIGNING_KEY: &str = "-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPp4XZRnRHSMhGg0t
6yjQCRV35J4TUY4idLgiCu6EyLqhRANCAAQbx8C533c2AKDwL/RtjVipVnnM2WRv
5w2wZNCJrubSK0StYKJ71CikDgkhw8M90ojfRIowqpl0uLA3kW3PEZy9
-----END PRIVATE KEY-----
";
fn main() {
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())]),
extensions: Extensions::new(),
};
let signed = token.sign_jwt_pem(Algorithm::ES256, SIGNING_KEY.as_bytes()).unwrap();
}
```
## Verification
```rust
use ear::{Ear, Algorithm};
const VERIF_KEY: &str = r#"
{
"kty":"EC",
"crv":"P-256",
"x":"G8fAud93NgCg8C_0bY1YqVZ5zNlkb-cNsGTQia7m0is",
"y":"RK1gonvUKKQOCSHDwz3SiN9EijCqmXS4sDeRbc8RnL0"
}
"#;
fn main() {
let signed = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlYXRfcHJvZmlsZSI6InRlc3QiLCJpYXQiOjEsImVhci52ZXJpZmllci1pZCI6eyJkZXZlbG9wZXIiOiJodHRwczovL3ZlcmFpc29uLXByb2plY3Qub3JnIiwiYnVpbGQiOiJ2c3RzIDAuMC4xIn0sInN1Ym1vZHMiOnsidGVzdCI6eyJlYXIuc3RhdHVzIjoibm9uZSJ9fX0.G25v0j0NDQhSOcK3Jtfq5vqVxnoWuWf-Q0DCNkCwpyB03DGr25ZDJ3IDSAHVPZrr6TVMwj8RcGEzQnCrucem4Q";
let token = Ear::from_jwt_jwk(signed, Algorithm::ES256, VERIF_KEY.as_bytes()).unwrap();
println!("EAR profiles: {}", token.profile);
}
```
# Extensions and Profiles
EAR supports extension at top level (i.e. within the [`Ear`] struct), and also within
[`Appraisal`]s. An extension is an additional field definition. Extensions can be defined by
registering them with the `extensions` field of the corresponding struct. When registering an
extension, you must provide a string name (used in JSON), an integer key (used in CBOR), and an
[`ExtensionKind`] indicating which [`ExtensionValue`]s are valid.
## Registering individual extensions
Extensions can be registered directly with the corresponding struct's `extensions` field. Once
they have been registered, their values can be set and queried
```rust
use ear::{Ear, Appraisal, ExtensionKind, ExtensionValue};
let mut ear = Ear::new();
ear.extensions.register("ext.company-name", -65537, ExtensionKind::String).unwrap();
let mut appraisal = Appraisal::new();
// extensions for Ear's and Appraisal's have their own namespaces, so it is
// to use the same key in both.
appraisal.extensions.register("ext.timestamp", -65537, ExtensionKind::Integer).unwrap();
ear.extensions.set_by_name(
"ext.company-name",
ExtensionValue::String("Acme Inc.".to_string()),
).unwrap();
appraisal.extensions.set_by_key(
-65537,
ExtensionValue::Integer(1723534859),
).unwrap();
ear.submods.insert("road-runner-trap".to_string(), appraisal);
assert_eq!(
ear.extensions.get_by_key(&-65537).unwrap(),
ExtensionValue::String("Acme Inc.".to_string()),
);
assert_eq!(
ear.submods["road-runner-trap"].extensions.get_by_name("ext.timestamp").unwrap(),
ExtensionValue::Integer(1723534859),
);
```
Note: if you've obtained the [`Ear`] by deserializing from CBOR/JSON, [`Extensions`] struct
will cache any values for any unexpected fields, so that when you register extensions
afterwards, the corresponding unmarshaled values will be accessible.
## Using Profiles
Sets of extensions can be associated together within [`Profile`]s. A [`Profile`] can be
registered, and can then be retrieved by its `id` when creating a new [`Ear`] or [`Appraisal`]
```rust
use ear::{Ear, Appraisal, ExtensionKind, ExtensionValue, Profile, register_profile};
fn init_profile() {
let mut profile = Profile::new("tag:github.com,2023:veraison/ear#acme-profile");
profile.register_ear_extension(
"ext.company-name", -65537, ExtensionKind::String).unwrap();
profile.register_appraisal_extension(
"ext.timestamp", -65537, ExtensionKind::Integer).unwrap();
register_profile(&profile);
}
fn main() {
init_profile();
let mut ear = Ear::new_with_profile(
"tag:github.com,2023:veraison/ear#acme-profile").unwrap();
// these will apply to all submods/appraisals within a profiled EAR
let mut appraisal = Appraisal::new_with_profile(
"tag:github.com,2023:veraison/ear#acme-profile").unwrap();
ear.extensions.set_by_name(
"ext.company-name",
ExtensionValue::String("Acme Inc.".to_string()),
).unwrap();
appraisal.extensions.set_by_key(
-65537,
ExtensionValue::Integer(1723534859),
).unwrap();
ear.submods.insert("road-runner-trap".to_string(), appraisal);
assert_eq!(
ear.extensions.get_by_key(&-65537).unwrap(),
ExtensionValue::String("Acme Inc.".to_string()),
);
assert_eq!(
ear.submods["road-runner-trap"]
.extensions.get_by_name("ext.timestamp").unwrap(),
ExtensionValue::Integer(1723534859),
);
}
```
When deserializing an [`Ear`], its `profile` field will automatically be used to look up a
registred profile and add the associated extensions.
# JWT/CWT common claims
The only common JWT/CWT claim specified by EAR spec is "iat" (issued at). Other claims (e.g.
"iss" or "exp") are not expected to be present inside a valid EAR. It is, however, possible
to define them for a particular profile and include them as extensions via mechanisms described
above.
The following example shows how to include and then verify expiration time ("exp" JWT claim)
inside an EAR.
```rust
use ear::{Ear, Algorithm, Appraisal, ExtensionKind, ExtensionValue};
use std::time::{SystemTime, Duration, UNIX_EPOCH};
const VERIF_KEY: &str = r#"
{
"kty":"EC",
"crv":"P-256",
"x":"G8fAud93NgCg8C_0bY1YqVZ5zNlkb-cNsGTQia7m0is",
"y":"RK1gonvUKKQOCSHDwz3SiN9EijCqmXS4sDeRbc8RnL0"
}
"#;
const SIGNING_KEY: &str = "-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPp4XZRnRHSMhGg0t
6yjQCRV35J4TUY4idLgiCu6EyLqhRANCAAQbx8C533c2AKDwL/RtjVipVnnM2WRv
5w2wZNCJrubSK0StYKJ71CikDgkhw8M90ojfRIowqpl0uLA3kW3PEZy9
-----END PRIVATE KEY-----
";
let mut ear = Ear::new();
ear.profile = "tag:github.com,2023:veraison/ear#acme-profile".to_string();
ear.vid.build = "vsts 0.0.1".to_string();
ear.vid.developer = "https://veraison-project.org".to_string();
ear.submods.insert("road-runner-trap".to_string(), Appraisal::new());
ear.extensions.register("exp", 4, ExtensionKind::Integer).unwrap();
// expire 10 days from now
let exp = SystemTime::now().checked_add(Duration::from_secs(60*60*24*10)).unwrap()
.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
ear.extensions.set_by_name("exp", ExtensionValue::Integer(exp)).unwrap();
let signed = ear
.sign_jwt_pem(Algorithm::ES256, SIGNING_KEY.as_bytes())
.unwrap();
let mut ear2 =
Ear::from_jwt_jwk(signed.as_str(), Algorithm::ES256, VERIF_KEY.as_bytes()).unwrap();
ear2.extensions.register("exp", 4, ExtensionKind::Integer).unwrap();
// Verify the token has not expired.
let exp2 = match ear2.extensions.get_by_name("exp").unwrap() {
ExtensionValue::Integer(v) => Duration::from_secs(v as u64),
_ => panic!(),
};
assert!(SystemTime::now().duration_since(UNIX_EPOCH).unwrap() < exp2);
```
# JWT/CWT headers
When signing with `sign_jwt_pem`/`sign_jwk_der`, only the `alg` header is set in the resulting
JWT based on the the specified algorithm. If other headers need to be specified, then
`sign_jwt_pem_with_header` and `sign_jwk_der_with_header` can be used instead; these take a
`jwt::Header` instead of an algorithm. A new header can be created from an algorithm using
`new_jwt_header`.
The same goes when signing as COSE_Sign1Message. Only `alg` header is set by default, however,
`_with_header` signing methods can be used to specify a custom `cose::headers::CoseHeader`,
which can be reating from an algorithm using `new_cwt_header`.
```rust
use std::collections::BTreeMap;
use ear::{Ear, VerifierID, Algorithm, Appraisal, Extensions, new_jwt_header, new_cose_header};
const SIGNING_KEY: &str = "-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPp4XZRnRHSMhGg0t
6yjQCRV35J4TUY4idLgiCu6EyLqhRANCAAQbx8C533c2AKDwL/RtjVipVnnM2WRv
5w2wZNCJrubSK0StYKJ71CikDgkhw8M90ojfRIowqpl0uLA3kW3PEZy9
-----END PRIVATE KEY-----
";
fn main() {
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())]),
extensions: Extensions::new(),
};
// JWT
let mut jwt_header = new_jwt_header(&Algorithm::ES256).unwrap();
// set additional header(s)
jwt_header.kid = Some("key-ident".to_string());
let signed_jwt = token.sign_jwt_pem_with_header(&jwt_header, SIGNING_KEY.as_bytes()).unwrap();
// CWT
let mut cwt_header = new_cose_header(&Algorithm::ES256).unwrap();
// set additional header(s)
cwt_header.kid("key-ident".as_bytes().to_vec(), true, false);
let signed_cwt = token.sign_cose_pem_with_header(cwt_header, SIGNING_KEY.as_bytes()).unwrap();
}
```
# Limitations
- Signing supports PEM and DER keys; verification currently only supports JWK
keys.
- JWT signing currently only supports ES256, ES384, EdDSA, PS256, PS384, and
PS512.
- COSE signing currently only supports ES256, ES384, ES512, and EdDSA.