Skip to content
This repository was archived by the owner on Feb 13, 2024. It is now read-only.

Commit f31b647

Browse files
author
Philipp Etschel
authored
Merge pull request #840 from boschresearch/feature/self-attested-prover
Answer presentation request with self attested attributes (FE)
2 parents 9097f5c + 4006a50 commit f31b647

File tree

9 files changed

+284
-39
lines changed

9 files changed

+284
-39
lines changed

backend/business-partner-agent/src/main/java/org/hyperledger/bpa/impl/util/Converter.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,9 @@ public AriesProofExchange toAPIObject(@NonNull PartnerProof p) {
252252
.proofType(e.getValue().getType())
253253
.revealedAttributes(e.getValue().getRevealedAttributes())
254254
.requestedPredicates(e.getValue().getRequestedPredicates())
255-
.identifier(credentialInfoResolver.populateIdentifier(e.getValue().getIdentifier()))
255+
.identifier(e.getValue().getIdentifier() != null ?
256+
credentialInfoResolver.populateIdentifier(e.getValue().getIdentifier())
257+
: null)
256258
.build()));
257259
proofData = mapper.convertValue(collect, JsonNode.class);
258260
} else if (p.typeIsJsonLd()) {
@@ -263,8 +265,9 @@ public AriesProofExchange toAPIObject(@NonNull PartnerProof p) {
263265
AriesProofExchange.RevealedAttributeGroup ag = AriesProofExchange.RevealedAttributeGroup
264266
.builder()
265267
.revealedAttributes(vc.subjectToFlatMap())
266-
.identifier(credentialInfoResolver
267-
.populateIdentifier(LDContextHelper.findSchemaId(vc), vc.getIssuer()))
268+
.identifier(vc.getIssuer() != null ? credentialInfoResolver
269+
.populateIdentifier(LDContextHelper.findSchemaId(vc), vc.getIssuer())
270+
: null)
268271
.build();
269272
return new AbstractMap.SimpleEntry<>(sub.getId(), ag);
270273
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

backend/pom.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,19 @@
5454
<skip.docker.build>true</skip.docker.build>
5555
<!-- Dependency Versions -->
5656
<jackson.version>2.13.4</jackson.version>
57+
<jackson.databind.version>2.13.4.2</jackson.databind.version>
5758
<log4j2.version>2.19.0</log4j2.version>
5859
<lombok.version>1.18.24</lombok.version>
5960
<lombok.maven.plugin.version>1.18.20.1</lombok.maven.plugin.version>
60-
<micronaut.version>3.7.1</micronaut.version>
61+
<micronaut.version>3.7.2</micronaut.version>
6162
<micronaut.data.version>3.8.1</micronaut.data.version>
6263
<micronaut.openapi.version>4.5.2</micronaut.openapi.version>
6364
<micronaut.security.version>3.8.0</micronaut.security.version>
64-
<mockito.version>4.8.0</mockito.version>
65+
<mockito.version>4.8.1</mockito.version>
6566
<okhttp.version>4.10.0</okhttp.version>
6667
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
6768
<pmd.version>6.50.0</pmd.version>
68-
<spotbugs.version>4.7.2</spotbugs.version>
69+
<spotbugs.version>4.7.3</spotbugs.version>
6970
<testcontainers.version>1.17.5</testcontainers.version>
7071
<!-- Plugin Versions -->
7172
<license-maven-plugin.version>4.1</license-maven-plugin.version>
@@ -136,7 +137,7 @@
136137
<dependency>
137138
<groupId>com.fasterxml.jackson.core</groupId>
138139
<artifactId>jackson-databind</artifactId>
139-
<version>${jackson.version}</version>
140+
<version>${jackson.databind.version}</version>
140141
</dependency>
141142
<dependency>
142143
<groupId>com.fasterxml.jackson.core</groupId>
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# JWT Token Validation
2+
3+
JSON Web Token (JWT) is a term standing for two implementations: JSON Web Signature (JWS) and JSON Web Encryption (JWE).
4+
JWE is not very common, and most of the time you will see JWS.
5+
6+
## JWS Basics
7+
8+
A token consists of three parts:
9+
10+
1. Header
11+
2. Payload
12+
3. Signature
13+
14+
Header and Payload are base64 encoded JSON documents whose contents are based on [RFC7519](https://tools.ietf.org/html/rfc7519).
15+
The signature is created by hashing header and payload and then signing the result with a private key and base64 encoding the result.
16+
The JWS presentation is built by concatenating all three parts with dots in between, so in short the result looks like:
17+
Concat(Base64(Header) + '.' + Base64(Payload) + '.' + Base64(Signature).
18+
19+
Note: To restrict parsing effort the CA only allows EdDSA as the signature algorithm with an Ed25519 key pair.
20+
21+
## JWS in a regular (web-) service context
22+
23+
To verify the token, you need the public key of the signing party.
24+
In a web service context this key is usually statically configured and only changes when the key is rotated or expired.
25+
As both header and payload are part of the signature any manipulation of the header or the payload becomes evident immediately.
26+
27+
## JWS in the context of business-partner-agent:web
28+
29+
Here we have the issue that the public key is delivered separately from the token, this opens possibilities for manipulation.
30+
In this context we have the following token retrieval and validation process:
31+
32+
* Step 1. Resolve the public profile service endpoint via the did document
33+
34+
This depends on the did method, so the did document can be written on a ledger or made available by a web service.
35+
As did document presentations are not always the same, resolution happens with the help of the universal resolver,
36+
that (hopefully) returns a normalised presentation of the did document.
37+
38+
Example did document with a profile type endpoint within the service section:
39+
```json
40+
{
41+
"@context": [
42+
"https://www.w3.org/ns/did/v1"
43+
],
44+
"id": "did:sov:F6dB7dMVHUQSC64qemnBi7",
45+
"verificationMethod": [
46+
{
47+
"id": "did:sov:F6dB7dMVHUQSC64qemnBi7#key-1",
48+
"type": "Ed25519VerificationKey2018",
49+
"controller": "did:sov:F6dB7dMVHUQSC64qemnBi7",
50+
"publicKeyBase58": "8gdhRLtvJHzKoJGyuEqgdN1QZGYfai4wMHFGgtfDXg3D"
51+
}
52+
],
53+
"service": [
54+
{
55+
"id": "did:sov:F6dB7dMVHUQSC64qemnBi7#did-communication",
56+
"type": "did-communication",
57+
"serviceEndpoint": "http://localhost:8080",
58+
"recipientKeys": [
59+
"did:sov:F6dB7dMVHUQSC64qemnBi7#key-1"
60+
],
61+
"routingKeys": [],
62+
"priority": 1
63+
},
64+
{
65+
"id": "did:sov:F6dB7dMVHUQSC64qemnBi7#profile",
66+
"type": "profile",
67+
"serviceEndpoint": "http://localhost:8080/profile.jsonld"
68+
}
69+
]
70+
}
71+
```
72+
73+
* Step 2. Resolve the public profile
74+
75+
In this case the profile service endpoint returns a JSON structure that contains the JWT.
76+
77+
```
78+
{
79+
"decoded": {},
80+
"jwt": ""
81+
}
82+
```
83+
84+
* Step 3. Decode token and verify signature
85+
86+
Decoding just means breaking the token down into the three parts mentioned above and verifying its integrity.
87+
Verification is the tricky part though.
88+
89+
* Step 4. Resolve the public key
90+
91+
If the token is self-signed the key is already part of the did document.
92+
If the token was signed by a third party the public key needs to be resolved by pulling the did document of the third party.
93+
94+
### Token verification scenarios
95+
96+
A token can be either self-signed or signed by a third party.
97+
Self-signed in the context of verifiable credentials means subject (sub) == issuer (iss).
98+
In JWT terms iss === sub. In general, we have to decide if we want to trust an issuer to make specific claim about a subject.
99+
In the self-signed public profile data case we have (human) trust if issuer == subject.
100+
But in some cases we would probably not trust did:indy:1 making claims about did:indy:2.
101+
102+
The second step is cryptographic trust, to check if the issuer authenticated the credential.
103+
This is the part where we have to check that the public key which verifies the JWT is authorized by the issuer.
104+
Therefore, we have to check if the verifying public key is part of the did document of the issuer
105+
106+
Therefore, we can state that:
107+
108+
1. If there is no kid and iss == sub then the token is considered self-signed
109+
2. If kid && iss == sub then the token is also considered self-signed, and kid must be part of the issuers did document
110+
3. If sub != iss the token is signed by a third party
111+
4. If kid && sub != iss, the token is signed by a third party, and kid must be part of the issuers did document
112+
113+
114+
#### Self-signed - no key id in the header
115+
116+
If the JWT is in the following form:
117+
118+
```jsom
119+
{
120+
"typ": "JWT",
121+
"alg": "EdDSA"
122+
},
123+
{
124+
"sub": "did:web:faber.iil.network",
125+
"iss": "did:web:faber.iil.network"
126+
}
127+
```
128+
129+
The did document must only contain a single key. If there is more than one key element the token is considered invalid.
130+
If the structure is valid the token signature is validated with the value of "publicKeyBase58".
131+
132+
```json
133+
{
134+
"id": "did:web:faber.iil.network",
135+
"publicKey": [
136+
{
137+
"id": "did:web:faber.iil.network#key-1",
138+
"type": "Ed25519VerificationKey2018",
139+
"publicKeyBase58": "24j9iYZTPRE3L4W5kRixBAEQLHJzfzuHsqiQyCnVEbKZ"
140+
}
141+
]
142+
}
143+
```
144+
145+
#### Self-signed - key id in the header
146+
147+
If the header contains a kid...
148+
149+
```
150+
{
151+
"kid": "did:web:faber.iil.network#key-1",
152+
"typ": "JWT",
153+
"alg": "EdDSA"
154+
},
155+
{
156+
"sub": "did:web:faber.iil.network",
157+
"iss": "did:web:faber.iil.network"
158+
}
159+
```
160+
161+
...the did document is allowed to have multiple keys. If the did document is missing a matching publicKey:id the token is considered invalid.
162+
163+
```json
164+
{
165+
"id": "did:web:faber.iil.network",
166+
"publicKey": [
167+
{
168+
"id": "did:web:faber.iil.network#key-1",
169+
"type": "Ed25519VerificationKey2018",
170+
"publicKeyBase58": "24j9iYZTPRE3L4W5kRixBAEQLHJzfzuHsqiQyCnVEbKZ"
171+
}
172+
]
173+
}
174+
```
175+
176+
#### Signed by third party
177+
178+
Like mentioned above, the public key needs to be resolved by yet another call to the universal resolver to retrieve
179+
the did document and public key of the third party. The `did` used for resolution is taken from the iss field of the Payload.
180+
If there is a kid then the same mechanism as above is applied.
181+
182+
This use-case is currently out of scope and validation will fail.

frontend/src/components/CredentialCard.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
height="40"
1313
class="light-blue darken-3 card-title text-subtitle-1 white--text font-weight-medium"
1414
style="text-transform: capitalize"
15-
>{{ this.document.proofData.identifier.schemaLabel }}</v-card-title
15+
>{{
16+
hasIdentifiers ? this.document.proofData.identifier.schemaLabel : ""
17+
}}</v-card-title
1618
>
1719
<v-card-subtitle
1820
class="light-blue darken-3 card-title text-subtitle-2 white--text font-weight-thin"
@@ -51,7 +53,7 @@
5153
</v-col>
5254
</v-row>
5355
</v-container>
54-
<v-expansion-panels flat>
56+
<v-expansion-panels flat v-if="hasIdentifiers">
5557
<v-expansion-panel>
5658
<v-expansion-panel-header class="font-weight-medium">
5759
{{ $t("component.credentialCard.details") }}
@@ -105,6 +107,9 @@ export default {
105107
predicates() {
106108
return this.document.proofData.requestedPredicates;
107109
},
110+
hasIdentifiers() {
111+
return this.document.proofData.identifier;
112+
},
108113
unrevealedAttributes() {
109114
return (
110115
RequestedProofType.UNREVEALED_ATTRS ===

frontend/src/components/PresentationExList.vue

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
class="justify-start"
9797
v-else
9898
v-bind:record="record"
99+
:isReadyToApprove.sync="isReadyToApprove"
99100
></PresentationRecord>
100101
<v-alert
101102
v-if="
@@ -198,11 +199,12 @@ export default {
198199
dialog: false,
199200
isBusy: false,
200201
isLoadingPresExRecords: true,
202+
isWaitingForMatchingCreds: false,
203+
isReadyToApprove: false,
201204
presentationExchangeRecords: new Array<AriesProofExchange>(),
202205
options: {},
203206
totalNumberOfElements: 0,
204207
hideFooter: false,
205-
isWaitingForMatchingCreds: false,
206208
declineReasonText: "",
207209
};
208210
},
@@ -267,17 +269,6 @@ export default {
267269
this.record.state === PresentationExchangeStates.REQUEST_RECEIVED
268270
);
269271
},
270-
isReadyToApprove() {
271-
if (Object.hasOwnProperty.call(this.record, "proofRequest")) {
272-
const groupsWithCredentials = RequestTypes.map((type) => {
273-
return Object.values(this.record.proofRequest[type]).map((group) => {
274-
return Object.hasOwnProperty.call(group, "selectedCredential");
275-
});
276-
});
277-
// eslint-disable-next-line unicorn/no-array-reduce
278-
return groupsWithCredentials.flat().reduce((x, y) => x && y);
279-
} else return false;
280-
},
281272
typeIsIndy() {
282273
return this.record.type === CredentialTypes.INDY.type;
283274
},
@@ -422,10 +413,20 @@ export default {
422413
RequestTypes.map((type) => {
423414
Object.entries(this.record.proofRequest[type]).map(
424415
([groupName, group]: [string, any]) => {
425-
referents[groupName] = {
426-
referent: group.selectedCredential?.credentialInfo?.referent,
427-
revealed: !!group.revealed,
428-
};
416+
if (
417+
group.selectedCredential &&
418+
typeof group.selectedCredential === "object"
419+
) {
420+
referents[groupName] = {
421+
referent: group.selectedCredential?.credentialInfo?.referent,
422+
revealed: !!group.revealed,
423+
};
424+
} else {
425+
referents[groupName] = {
426+
selfAttestedValue: group.selectedCredential,
427+
revealed: true,
428+
};
429+
}
429430
}
430431
);
431432
});

0 commit comments

Comments
 (0)