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

feat: add Jws2020 cryptosuite #483

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
67 changes: 67 additions & 0 deletions edc-extensions/ssi/jws2020-crypto-suite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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
paullatzelsperger marked this conversation as resolved.
Show resolved Hide resolved
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 {
api(libs.edc.spi.jwt)
implementation(libs.nimbus.jwt)
implementation(libs.edc.spi.jsonld)
paullatzelsperger marked this conversation as resolved.
Show resolved Hide resolved
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
paullatzelsperger marked this conversation as resolved.
Show resolved Hide resolved
* @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