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

Add JSON Object Signing capabilities #1105

Merged
merged 42 commits into from
Feb 6, 2023
Merged

Conversation

PhilippGackstatter
Copy link
Contributor

@PhilippGackstatter PhilippGackstatter commented Jan 23, 2023

Description of change

This PR introduces JOSE (JSON Object Signing and Encryption) capabilities in the identity_jose crate. This is in preparation to adding support for signing credentials with VC-JWT. See also #1103 for more details.

The primary source of the implementation is our (unused) libjose crate, from which the JWK, JWS and JWT implementations were extracted. JWE is not required for now and so was not added to the new crate.

The biggest change to the libjose crate is how signatures are crated and how they are verified. In particular, libjose included signing capabilities for RSA, P256, HMAC and Ed25519 using in-memory keys. Because we want to use our more secure architecture that delegates signing to some KMS (like Stronghold, a cloud KMS or potentially a hardware-secure module), the API was changed accordingly.
The included capabilities were removed to remove the dependency on various cryptographic crates. Instead the Encoder and Decoder API essentially take closures that are expected to sign and verify, respectively.

A full roundtrip example:

#[tokio::test]
  async fn test_encoder_decoder_roundtrip() {
    let secret_key = Arc::new(SecretKey::generate().unwrap());
    let public_key = secret_key.public_key();

    let sign_fn = move |protected, unprotected, msg: Vec<u8>| {
      let sk = secret_key.clone();
      async move {
        let header_set: JwtHeaderSet<JwsHeader> = JwtHeaderSet::new().protected(&protected).unprotected(&unprotected);
        if header_set.try_alg().map_err(|_| "missing `alg` parameter")? != JwsAlgorithm::EdDSA {
          return Err("incompatible `alg` parameter");
        }
        let sig: _ = sk.sign(msg.as_slice()).to_bytes();
        Ok(jwu::encode_b64(sig))
      }
    };

    let verify_fn = |protected: Option<&JwsHeader>, unprotected: Option<&JwsHeader>, msg: &[u8], sig: &[u8]| {
      let header_set: JwtHeaderSet<JwsHeader> = JwtHeaderSet::new().protected(protected).unprotected(unprotected);
      if header_set.try_alg().map_err(|_| "missing `alg` parameter")? != JwsAlgorithm::EdDSA {
        return Err("incompatible `alg` parameter");
      }

      let signature_arr = <[u8; crypto::signatures::ed25519::SIGNATURE_LENGTH]>::try_from(sig)
        .map_err(|err| err.to_string())
        .unwrap();

      let signature = crypto::signatures::ed25519::Signature::from_bytes(signature_arr);
      if public_key.verify(&signature, msg) {
        Ok(())
      } else {
        Err("invalid signature")
      }
    };

    let mut header: JwsHeader = JwsHeader::new(JwsAlgorithm::EdDSA);
    header.set_kid("did:iota:0x123#signing-key");

    let mut claims: JwtClaims<serde_json::Value> = JwtClaims::new();
    claims.set_iss("issuer");
    claims.set_iat(
      SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64,
    );
    claims.set_custom(serde_json::json!({"num": 42u64}));

    let token: String = Encoder::new(sign_fn)
      .recipient(Recipient::new().protected(&header))
      .encode_serde(&claims)
      .await
      .unwrap();

    let token: _ = Decoder::new(verify_fn).decode(token.as_bytes()).unwrap();

    let recovered_claims: JwtClaims<serde_json::Value> = serde_json::from_slice(&token.claims).unwrap();

    assert_eq!(claims, recovered_claims);
  }

Links to any relevant issues

part of #1103.

Type of change

Add an x to the boxes that are relevant to your changes.

  • Bug fix (a non-breaking change which fixes an issue)
  • Enhancement (a non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Fix

How the change has been tested

Tests with test vectors from various RFCs are included.

Change checklist

Add an x to the boxes that are relevant to your changes.

  • I have followed the contribution guidelines for this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

@PhilippGackstatter PhilippGackstatter added Added A new feature that requires a minor release. Part of "Added" section in changelog Rust Related to the core Rust code. Becomes part of the Rust changelog. labels Jan 24, 2023
@PhilippGackstatter PhilippGackstatter added this to the v0.7 Features milestone Jan 26, 2023
@PhilippGackstatter PhilippGackstatter marked this pull request as ready for review January 26, 2023 15:11
@PhilippGackstatter PhilippGackstatter changed the title WIP: JOSE Add JSON Object Signing and Encryption capabilities Jan 26, 2023
Copy link
Contributor

@olivereanderson olivereanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this!

I understand that it is hard to know exactly what to bring in here as this crate is intended to be a building block for higher level functionality which we have yet to flesh out. Taking that into account I think this is already pretty good!

What I am missing is some more documentation for public items, perhaps especially in the jws module. In fact it would be really nice with some module level documentation for encoder and decoder.

I also wish it was a bit easier to get an overview over where the tests for a given function can be found since the tests are not in the same file (in this case understandably so). Maybe the cov_mark crate can help with that? (Matklad has written some pretty good blog posts about that crate here and here).

On the architecture side I am not sure whether we want to include the possibility to attach private key parameters in a JWK at all as we want to discourage in-memory keys, but that is perhaps something we can revisit in a later PR.

This was my first read through. I will go through the tests more carefully tomorrow.

identity_jose/src/jwk/key.rs Show resolved Hide resolved
identity_jose/src/jws/decoder.rs Show resolved Hide resolved
identity_jose/src/jws/encoder.rs Show resolved Hide resolved
identity_jose/src/jws/encoder.rs Show resolved Hide resolved
identity_jose/src/jws/recipient.rs Show resolved Hide resolved
identity_jose/src/jwt/claims.rs Outdated Show resolved Hide resolved
@PhilippGackstatter
Copy link
Contributor Author

I also wish it was a bit easier to get an overview over where the tests for a given function can be found since the tests are not in the same file (in this case understandably so). Maybe the cov_mark crate can help with that?

There are only high-level tests of the encoder/decoder API for now, most based on test vectors, so even with cov_mark you could not find a test for a certain function. Coverage is certainly not great right now. We can eventually add more unit tests in the respective modules, and then we will be able to find tests in the same file as the definition.

On the architecture side I am not sure whether we want to include the possibility to attach private key parameters in a JWK at all as we want to discourage in-memory keys, but that is perhaps something we can revisit in a later PR.

I think we should leave it as-is for having a complete JWK implementation but mostly because the test vectors include the private keys as JWKs, so we couldn't decode those, or would have to do so manually.

Copy link
Contributor

@olivereanderson olivereanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the improved documentation!

I approve this with the understanding that we will iterate heavily upon this crate before our next release as there are still a lot of missing docs and there should be more tests (also for the unhappy path).

identity_jose/src/jws/decoder.rs Outdated Show resolved Hide resolved
identity_jose/src/jws/encoder.rs Show resolved Hide resolved
@PhilippGackstatter PhilippGackstatter merged commit ef8be50 into main Feb 6, 2023
@PhilippGackstatter PhilippGackstatter deleted the feat/identity_jose branch February 6, 2023 11:12
@PhilippGackstatter PhilippGackstatter changed the title Add JSON Object Signing and Encryption capabilities Add JSON Object Signing capabilities Feb 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Added A new feature that requires a minor release. Part of "Added" section in changelog Rust Related to the core Rust code. Becomes part of the Rust changelog.
Projects
Development

Successfully merging this pull request may close these issues.

2 participants