-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add Jws2020 cryptosuite (#483)
* 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
1 parent
37474b9
commit d41c59f
Showing
39 changed files
with
2,392 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
...ypto-suite/src/main/java/org/eclipse/edc/security/signature/jws2020/ByteArrayAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
||
} |
78 changes: 78 additions & 0 deletions
78
...0-crypto-suite/src/main/java/org/eclipse/edc/security/signature/jws2020/IssuerCompat.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
54 changes: 54 additions & 0 deletions
54
...20-crypto-suite/src/main/java/org/eclipse/edc/security/signature/jws2020/JsonAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
78 changes: 78 additions & 0 deletions
78
...020-crypto-suite/src/main/java/org/eclipse/edc/security/signature/jws2020/JwkAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
Oops, something went wrong.