ear 0.5.0

EAT Attestation Results implementation
Documentation
<!-- cargo-rdme start -->

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.

<!-- cargo-rdme end -->