Skip to content

Commit d69f992

Browse files
committed
docs(changeset): allow kid (when not a did) to be combined with x5c/jwk header params in JWT/JWS. This is a pattern commonly used and breaks interop with Credo.
Signed-off-by: Timo Glastra <[email protected]>
1 parent 1a4182e commit d69f992

File tree

6 files changed

+188
-16
lines changed

6 files changed

+188
-16
lines changed

.changeset/cuddly-deers-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@credo-ts/core': patch
3+
---
4+
5+
allow kid (when not a did) to be combined with x5c/jwk header params in JWT/JWS. This is a pattern commonly used and breaks interop with Credo.

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
},
2727
"dependencies": {
2828
"@animo-id/mdoc": "0.4.0",
29-
"@animo-id/pex": "4.1.1-alpha.0",
29+
"@animo-id/pex": "4.1.1-alpha.1",
3030
"@astronautlabs/jsonpath": "^1.1.2",
3131
"@digitalcredentials/jsonld": "^6.0.0",
3232
"@digitalcredentials/jsonld-signatures": "^9.4.0",

packages/core/src/crypto/JwsService.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,17 @@ export class JwsService {
211211
}
212212

213213
private buildProtected(options: JwsProtectedHeaderOptions) {
214-
if ([options.jwk, options.kid, options.x5c].filter(Boolean).length != 1) {
215-
throw new CredoError('Only one of JWK, kid or x5c can and must be provided.')
214+
const x5cJwkCount = [options.jwk, options.x5c].filter(Boolean).length
215+
216+
// FIXME: checking for kid starting with '#' is not good.
217+
// but now we don't really limit that kid (did key reference)
218+
// cannot be combined with x5c/jwk
219+
if (options.kid?.startsWith('did:')) {
220+
if (x5cJwkCount > 0) {
221+
throw new CredoError("When 'kid' is a did, 'jwk' and 'x5c' cannot be provided.")
222+
}
223+
} else if (x5cJwkCount > 1 || (x5cJwkCount === 0 && !options.kid)) {
224+
throw new CredoError("Header must contain one of 'kid' with a did value, 'x5c', or 'jwk'.")
216225
}
217226

218227
return {
@@ -241,16 +250,25 @@ export class JwsService {
241250
trustedCertificates: trustedCertificatesFromOptions = [],
242251
} = options
243252

244-
if ([protectedHeader.jwk, protectedHeader.kid, protectedHeader.x5c].filter(Boolean).length > 1) {
245-
throw new CredoError('Only one of jwk, kid and x5c headers can and must be provided.')
253+
const x5cJwkCount = [protectedHeader.jwk, protectedHeader.x5c].filter(Boolean)
254+
255+
// FIXME: checking for kid starting with '#' is not good.
256+
// but now we don't really limit that kid (did key reference)
257+
// cannot be combined with x5c/jwk
258+
if (typeof protectedHeader.kid === 'string' && protectedHeader.kid?.startsWith('did:')) {
259+
if (x5cJwkCount.length > 0) {
260+
throw new CredoError("When 'kid' is a did, 'jwk' and 'x5c' cannot be provided.")
261+
}
262+
} else if (x5cJwkCount.length > 1) {
263+
throw new CredoError("Header must contain one of 'kid' with a did value, 'x5c', or 'jwk'.")
246264
}
247265

248266
if (protectedHeader.x5c) {
249267
if (
250268
!Array.isArray(protectedHeader.x5c) ||
251269
protectedHeader.x5c.some((certificate) => typeof certificate !== 'string')
252270
) {
253-
throw new CredoError('x5c header is not a valid JSON array of string.')
271+
throw new CredoError('x5c header is not a valid JSON array of strings.')
254272
}
255273

256274
const trustedCertificatesFromConfig =
@@ -278,7 +296,9 @@ export class JwsService {
278296
}
279297

280298
if (!jwkResolver) {
281-
throw new CredoError(`jwkResolver is required when the JWS protected header does not contain a 'jwk' property.`)
299+
throw new CredoError(
300+
`jwkResolver is required when the JWS protected header does not contain a 'jwk' or 'x5c' property.`
301+
)
282302
}
283303

284304
try {

packages/core/src/crypto/__tests__/JwsService.test.ts

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AgentContext } from '../../agent'
2-
import type { Key, Wallet } from '@credo-ts/core'
2+
import { X509Certificate, X509ModuleConfig, X509Service, type Key, type Wallet } from '@credo-ts/core'
33

44
import { InMemoryWallet } from '../../../../../tests/InMemoryWallet'
55
import { getAgentConfig, getAgentContext } from '../../../tests/helpers'
@@ -19,6 +19,7 @@ describe('JwsService', () => {
1919
let agentContext: AgentContext
2020
let jwsService: JwsService
2121
let didJwsz6MkfKey: Key
22+
let didJwsz6MkfCertificate: X509Certificate
2223
let didJwsz6MkvKey: Key
2324
let didJwszDnaeyKey: Key
2425

@@ -27,14 +28,22 @@ describe('JwsService', () => {
2728
wallet = new InMemoryWallet()
2829
agentContext = getAgentContext({
2930
wallet,
31+
registerInstances: [[X509ModuleConfig, new X509ModuleConfig()]],
3032
})
3133
await wallet.createAndOpen(config.walletConfig)
3234

3335
jwsService = new JwsService()
36+
3437
didJwsz6MkfKey = await wallet.createKey({
3538
privateKey: TypedArrayEncoder.fromString(didJwsz6Mkf.SEED),
3639
keyType: KeyType.Ed25519,
3740
})
41+
didJwsz6MkfCertificate = await X509Service.createCertificate(agentContext, {
42+
authorityKey: didJwsz6MkfKey,
43+
issuer: {
44+
countryName: 'NL',
45+
},
46+
})
3847

3948
didJwsz6MkvKey = await wallet.createKey({
4049
privateKey: TypedArrayEncoder.fromString(didJwsz6Mkv.SEED),
@@ -102,6 +111,80 @@ describe('JwsService', () => {
102111
)
103112
})
104113

114+
it('allows both x5c/jwk and kid (no did) to be present', async () => {
115+
const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON)
116+
117+
const signed1 = await jwsService.createJwsCompact(agentContext, {
118+
payload,
119+
key: didJwsz6MkfKey,
120+
protectedHeaderOptions: {
121+
alg: JwaSignatureAlgorithm.EdDSA,
122+
jwk: getJwkFromKey(didJwsz6MkfKey),
123+
kid: 'something',
124+
},
125+
})
126+
const { isValid: isValid1 } = await jwsService.verifyJws(agentContext, {
127+
jws: signed1,
128+
})
129+
expect(isValid1).toEqual(true)
130+
131+
const signed2 = await jwsService.createJwsCompact(agentContext, {
132+
payload,
133+
key: didJwsz6MkfKey,
134+
protectedHeaderOptions: {
135+
alg: JwaSignatureAlgorithm.EdDSA,
136+
x5c: [didJwsz6MkfCertificate.toString('base64url')],
137+
kid: 'something',
138+
},
139+
})
140+
141+
const { isValid: isValid2 } = await jwsService.verifyJws(agentContext, {
142+
jws: signed2,
143+
trustedCertificates: [didJwsz6MkfCertificate.toString('base64url')],
144+
})
145+
expect(isValid2).toEqual(true)
146+
})
147+
148+
it('throws error whens signing jws with more than one of x5c, jwk, kid (with did)', async () => {
149+
const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON)
150+
151+
await expect(
152+
jwsService.createJwsCompact(agentContext, {
153+
payload,
154+
key: didJwsz6MkfKey,
155+
protectedHeaderOptions: {
156+
alg: JwaSignatureAlgorithm.EdDSA,
157+
jwk: getJwkFromKey(didJwsz6MkfKey),
158+
kid: 'did:example:123',
159+
},
160+
})
161+
).rejects.toThrow("When 'kid' is a did, 'jwk' and 'x5c' cannot be provided.")
162+
163+
await expect(
164+
jwsService.createJwsCompact(agentContext, {
165+
payload,
166+
key: didJwsz6MkfKey,
167+
protectedHeaderOptions: {
168+
alg: JwaSignatureAlgorithm.EdDSA,
169+
jwk: getJwkFromKey(didJwsz6MkfKey),
170+
x5c: [didJwsz6MkfCertificate.toString('base64url')],
171+
},
172+
})
173+
).rejects.toThrow("Header must contain one of 'kid' with a did value, 'x5c', or 'jwk'.")
174+
175+
await expect(
176+
jwsService.createJwsCompact(agentContext, {
177+
payload,
178+
key: didJwsz6MkfKey,
179+
protectedHeaderOptions: {
180+
alg: JwaSignatureAlgorithm.EdDSA,
181+
kid: 'did:example:123',
182+
x5c: [didJwsz6MkfCertificate.toString('base64url')],
183+
},
184+
})
185+
).rejects.toThrow("When 'kid' is a did, 'jwk' and 'x5c' cannot be provided.")
186+
})
187+
105188
describe('verifyJws', () => {
106189
it('returns true if the jws signature matches the payload', async () => {
107190
const { isValid, signerKeys } = await jwsService.verifyJws(agentContext, {
@@ -149,5 +232,69 @@ describe('JwsService', () => {
149232
})
150233
).rejects.toThrow('Unable to verify JWS, no signatures present in JWS.')
151234
})
235+
236+
it('throws error when verifying jws with more than one of x5c, jwk, kid (with did)', async () => {
237+
const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON)
238+
239+
await expect(
240+
jwsService.verifyJws(agentContext, {
241+
jws: {
242+
header: {},
243+
protected: JsonEncoder.toBase64URL({
244+
alg: JwaSignatureAlgorithm.EdDSA,
245+
jwk: getJwkFromKey(didJwsz6MkfKey).toJson(),
246+
kid: 'did:example:123',
247+
}),
248+
payload: '',
249+
signature: '',
250+
},
251+
})
252+
).rejects.toThrow("When 'kid' is a did, 'jwk' and 'x5c' cannot be provided.")
253+
254+
await expect(
255+
jwsService.verifyJws(agentContext, {
256+
jws: {
257+
header: {},
258+
protected: JsonEncoder.toBase64URL({
259+
alg: JwaSignatureAlgorithm.EdDSA,
260+
jwk: getJwkFromKey(didJwsz6MkfKey).toJson(),
261+
kid: 'did:example:123',
262+
}),
263+
payload: '',
264+
signature: '',
265+
},
266+
})
267+
).rejects.toThrow("When 'kid' is a did, 'jwk' and 'x5c' cannot be provided.")
268+
269+
await expect(
270+
jwsService.verifyJws(agentContext, {
271+
jws: {
272+
header: {},
273+
protected: JsonEncoder.toBase64URL({
274+
alg: JwaSignatureAlgorithm.EdDSA,
275+
jwk: getJwkFromKey(didJwsz6MkfKey).toJson(),
276+
x5c: [didJwsz6MkfCertificate.toString('base64url')],
277+
}),
278+
payload: '',
279+
signature: '',
280+
},
281+
})
282+
).rejects.toThrow("Header must contain one of 'kid' with a did value, 'x5c', or 'jwk'.")
283+
284+
await expect(
285+
jwsService.verifyJws(agentContext, {
286+
jws: {
287+
header: {},
288+
protected: JsonEncoder.toBase64URL({
289+
alg: JwaSignatureAlgorithm.EdDSA,
290+
kid: 'did:example:123',
291+
x5c: [didJwsz6MkfCertificate.toString('base64url')],
292+
}),
293+
payload: '',
294+
signature: '',
295+
},
296+
})
297+
).rejects.toThrow("When 'kid' is a did, 'jwk' and 'x5c' cannot be provided.")
298+
})
152299
})
153300
})

packages/didcomm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"rxjs": "^7.8.0"
3535
},
3636
"devDependencies": {
37-
"@animo-id/pex": "4.1.1-alpha.0",
37+
"@animo-id/pex": "4.1.1-alpha.1",
3838
"@sphereon/pex-models": "^2.3.1",
3939
"@types/luxon": "^3.2.0",
4040
"reflect-metadata": "^0.1.13",

pnpm-lock.yaml

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)