Skip to content

Commit

Permalink
feat: add Jws2020 cryptosuite (#483)
Browse files Browse the repository at this point in the history
* feat: add Jws2020 cryptosuite

* checkstyle

* copyright headers

* added fixed JWSAlgorithm for RSA keys, added some tests

* improve documentation

* more cleanup, more tests

* pr remarks
  • Loading branch information
paullatzelsperger authored Jun 16, 2023
1 parent 37474b9 commit d41c59f
Show file tree
Hide file tree
Showing 39 changed files with 2,392 additions and 1 deletion.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ allprojects {
configDirectory.set(rootProject.file("resources"))

//checkstyle violations are reported at the WARN level
this.isShowViolations = System.getProperty("checkstyle.verbose", "false").toBoolean()
this.isShowViolations = System.getProperty("checkstyle.verbose", "true").toBoolean()
}


Expand Down
70 changes: 70 additions & 0 deletions edc-extensions/ssi/jws2020-crypto-suite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# JsonWebSignature2020

This module extends the [iron-verifiable-credentials library](https://github.com/filip26/iron-verifiable-credentials),
which we use in conjunction with [titanium-ld](https://github.com/filip26/titanium-json-ld/) with an implementation for
the [JsonWebSignature2020](https://www.w3.org/community/reports/credentials/CG-FINAL-lds-jws2020-20220721) crypto suite.

## Technical aspects

This implementation is actually mostly glue code between the `iron-verifiable-credentials` lib and the
well-known [Nimbus JOSE lib](https://connect2id.com/products/nimbus-jose-jwt), as all cryptographic primitives are taken
from Nimbus.

VerifiableCredentials and VerifiablePresentations are processed as JSON(-LD) objects, so some familiarity with JSON-LD
is required.
The entrypoint into the cryptographic suite is the `Vc` class, which allows signing/issuing and verifying JSON-LD
structures. The following samples use explicit types for clarity. These are just some illustrative examples, please
check the `IssuerTests` and the `VerifierTests` for more comprehensive explanations.

### Sign a VC

```java
JwsSignature2020Suite suite = new JwsSignature2020Suite(JacksonJsonLd.createObjectMapper());
JsonObject vc = createVcAsJsonLd();
JWK keyPair = createKeyPairAsJwk();
JwkMethod signKeys = new JwkMethod(id,type,controller,keyPair);

var options = suite.createOptions()
.created(Instant.now())
.verificationMethod(signKeys) // embeds the proof
.purpose(URI.create("https://w3id.org/security#assertionMethod"));

Issuer signedVc = Vc.sign(vc, signKeys, options);

JsonObject compacted = IssuerCompat.compact(signedVc);
```

### Verify a VC

```java
JwsSignature2020Suite suite = new JwsSignature2020Suite(JacksonJsonLd.createObjectMapper());
JsonObject vc = readSignedVc();
Verifier result = Vc.verify(vc, suite);

try {
result.isValid();
} catch(VerificationError error) {
//handle
}
```

## Limitations & Known Issues

Java 17 [dropped support](https://connect2id.com/products/nimbus-jose-jwt/examples/jwt-with-es256k-signature) for
the `secp256k1` curve. Alternatively, the BouncyCastle JCA provider could be used.
For this implementation, we chose to forego this at the benefit of a smaller library footprint. There is plenty of other
curves to choose from.

On a similar note, support for Octet Keypairs (`"OKP"`) has not yet been added to the standard Java JCA, thus an
additional dependency `tink` is needed,
check [here](https://connect2id.com/products/nimbus-jose-jwt/examples/jwk-generation#okp) for details. If that is not
acceptable to you, please add a dependency exclusion to your build script.

`iron-verifiable-credentials` is not 100% agnostic toward its crypto suites, for example there is
a [hard-coded context](https://github.com/filip26/iron-verifiable-credentials/blob/82d13326c5f64a0f38c75d417ffc263febfd970d/src/main/java/com/apicatalog/vc/processor/Issuer.java#L122)
added to the compacted JSON-LD, which is incorrect. It doesn't negatively impact the resulting JSON-LD, other than
possibly affecting processing times, but unfortunately it also makes it impossible to add more contexts, such
as https://w3id.org/security/suites/jws-2020/v1. We mitigated this with
the [`IssuerCompat.java`](./src/main/java/org/eclipse/edc/security/signature/jws2020/IssuerCompat.java), which should be
used
for compaction.
31 changes: 31 additions & 0 deletions edc-extensions/ssi/jws2020-crypto-suite/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/
plugins {
`java-library`
}

dependencies {
implementation(libs.edc.spi.jwt)
implementation(libs.nimbus.jwt)
implementation(libs.edc.spi.jsonld)
implementation(libs.edc.jsonld)
implementation(libs.edc.util)
// used for the Ed25519 Verifier in conjunction with OctetKeyPairs (OKP)
runtimeOnly(libs.tink)
implementation(libs.jakartaJson)

implementation(libs.apicatalog.iron.vc) {
exclude("com.github.multiformats")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.security.signature.jws2020;

import com.apicatalog.jsonld.lang.Keywords;
import com.apicatalog.ld.schema.adapter.LdValueAdapter;
import jakarta.json.Json;
import jakarta.json.JsonValue;
import org.eclipse.edc.jsonld.spi.JsonLdKeywords;

class ByteArrayAdapter implements LdValueAdapter<JsonValue, byte[]> {
@Override
public byte[] read(JsonValue value) {
if (value.getValueType().equals(JsonValue.ValueType.OBJECT)) {
var obj = value.asJsonObject();
return obj.getString(JsonLdKeywords.VALUE).getBytes();
}
return value.toString().getBytes();
}

@Override
public JsonValue write(byte[] value) {
return Json.createObjectBuilder()
.add(Keywords.VALUE, new String(value))
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.security.signature.jws2020;

import com.apicatalog.jsonld.JsonLd;
import com.apicatalog.jsonld.JsonLdError;
import com.apicatalog.jsonld.document.Document;
import com.apicatalog.jsonld.document.JsonDocument;
import com.apicatalog.jsonld.loader.DocumentLoader;
import com.apicatalog.ld.DocumentError;
import com.apicatalog.ld.signature.SigningError;
import com.apicatalog.vc.processor.Issuer;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import org.eclipse.edc.util.reflection.ReflectionUtil;

import java.net.URI;
import java.util.Arrays;

/**
* The {@link Issuer} adds the context, but currently that adds hard-coded {@code "https://w3id.org/security/suites/ed25519-2020/v1"}.
* For the Jwk2020 suite we need that to be {@code "https://w3id.org/security/suites/jws-2020/v1"}, so as a temporary workaround we do <em>not</em>
* use {@link Issuer#getCompacted()}, but rather use {@link IssuerCompat#compact(Issuer, String...)}.
*/
public class IssuerCompat {
/**
* Compacts the JSON structure represented by the {@link Issuer} by delegating to {@link JsonLd#compact(Document, URI)}. Note that before compacting, the JSON-LD is expanded, signed, all additional contexts are added
* and then compacted.
* <p>
* By default, the following contexts are added automatically:
* <ul>
* <li>https://www.w3.org/2018/credentials/v1</li>
* <li>https://w3id.org/security/suites/jws-2020/v1</li>
* </ul>
*
* @param issuer The {@link Issuer}
* @param additionalContexts Any additional context URIs that should be used for compaction. For Jws2020 it is highly likely that
* @return a JSON-LD structure in compacted format that contains the signed content (e.g. a VC).
*/
public static JsonObject compact(Issuer issuer, String... additionalContexts) {
try {
var expanded = issuer.getExpanded();
var arrayBuilder = Json.createArrayBuilder();
Arrays.stream(additionalContexts).forEach(arrayBuilder::add);
var context = arrayBuilder
.add("https://www.w3.org/2018/credentials/v1")
.add("https://w3id.org/security/suites/jws-2020/v1")
.add("https://www.w3.org/ns/did/v1")
.build();
return JsonLd.compact(JsonDocument.of(expanded), JsonDocument.of(context)).loader(getLoader(issuer))
.get();

} catch (JsonLdError | SigningError | DocumentError e) {
throw new RuntimeException(e);
}
}

/**
* rather crude hack to obtain the {@link Issuer}'s loader. The EDC util we're using here basically fetches the declared field recursively.
*
* @see ReflectionUtil#getFieldValue(String, Object)
*/
private static DocumentLoader getLoader(Issuer issuer) {
return ReflectionUtil.getFieldValue("loader", issuer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.security.signature.jws2020;

import com.apicatalog.ld.schema.adapter.LdValueAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
import org.eclipse.edc.jsonld.spi.JsonLdKeywords;

import java.util.Map;

class JsonAdapter implements LdValueAdapter<JsonValue, Object> {
private final ObjectMapper mapper;

JsonAdapter(ObjectMapper mapper) {
this.mapper = mapper;
}

@Override
public Object read(JsonValue value) {
var input = value;
if (value instanceof JsonObject) {
var jo = value.asJsonObject();
input = jo.get(JsonLdKeywords.VALUE);
}
return mapper.convertValue(input, Object.class);
}

@Override
public JsonValue write(Object value) {
if (value instanceof Map) {
var jo = Json.createObjectBuilder();
jo.add(JsonLdKeywords.VALUE, Json.createObjectBuilder((Map) value));
jo.add(JsonLdKeywords.TYPE, JsonLdKeywords.JSON);
return mapper.convertValue(jo.build(), JsonValue.class);
}
return mapper.convertValue(value, JsonValue.class);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.security.signature.jws2020;

import com.apicatalog.ld.schema.LdObject;
import com.apicatalog.ld.schema.LdTerm;
import com.apicatalog.ld.schema.adapter.LdValueAdapter;
import com.apicatalog.ld.signature.method.VerificationMethod;
import com.apicatalog.vc.integrity.DataIntegrity;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static org.eclipse.edc.security.signature.jws2020.Jws2020Schema.JWK_PRIVATE_KEY;
import static org.eclipse.edc.security.signature.jws2020.Jws2020Schema.JWK_PUBLIC_KEY;

/**
* Adapter that converts between {@link LdObject} and {@link VerificationMethod}
*/
class JwkAdapter implements LdValueAdapter<LdObject, VerificationMethod> {

@Override
public VerificationMethod read(LdObject value) {
URI id = value.value(LdTerm.ID);
URI type = value.value(LdTerm.TYPE);
URI controller = value.value(DataIntegrity.CONTROLLER);
var keyProperty = getKeyProperty(value);
var jwk = KeyFactory.create(keyProperty);
return new JwkMethod(id, type, controller, jwk);
}


@Override
public LdObject write(VerificationMethod method) {
var result = new HashMap<String, Object>();
Objects.requireNonNull(method, "VerificationMethod cannot be null!");

if (method.id() != null) {
result.put(LdTerm.ID.uri(), method.id());
}
if (method.type() != null) {
result.put(LdTerm.TYPE.uri(), method.type());
}
if (method.controller() != null) {
result.put(DataIntegrity.CONTROLLER.uri(), method.controller());
}

if (method instanceof JwkMethod ecKeyPair) {
if (ecKeyPair.keyPair() != null) {
result.put(JWK_PUBLIC_KEY.uri(), ecKeyPair.keyPair().toPublicJWK().toJSONObject());
}
}

return new LdObject(result);
}

private Map<String, Object> getKeyProperty(LdObject value) {
if (value.contains(JWK_PRIVATE_KEY)) {
return value.value(JWK_PRIVATE_KEY);
}
return value.value(JWK_PUBLIC_KEY);
}

}
Loading

0 comments on commit d41c59f

Please sign in to comment.