Skip to content

Commit fb88c3a

Browse files
Merge pull request #1854 from walt-id/feature/wal-1119
Fix x5c header location for CWT TSL
2 parents ee47a56 + dc44726 commit fb88c3a

2 files changed

Lines changed: 22 additions & 14 deletions

File tree

waltid-libraries/credentials/waltid-verification-policies2/src/commonMain/kotlin/id/walt/policies2/vc/policies/status/signature/StatusListSignatureVerifier.kt

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package id.walt.policies2.vc.policies.status.signature
22

3+
import id.walt.cose.CoseHeaders
34
import id.walt.cose.CoseSign1
5+
import id.walt.cose.coseCompliantCbor
46
import id.walt.cose.toCoseVerifier
57
import id.walt.crypto.keys.Key
68
import id.walt.crypto.keys.jwk.JWKKey
79
import id.walt.crypto.utils.JwsUtils.decodeJws
810
import id.walt.did.dids.DidService
911
import id.walt.did.dids.DidUtils
1012
import io.github.oshai.kotlinlogging.KotlinLogging
13+
import kotlinx.serialization.ExperimentalSerializationApi
14+
import kotlinx.serialization.decodeFromByteArray
1115
import kotlinx.serialization.json.JsonArray
1216
import kotlinx.serialization.json.JsonObject
1317
import kotlinx.serialization.json.jsonArray
1418
import kotlinx.serialization.json.jsonPrimitive
1519

20+
@OptIn(ExperimentalSerializationApi::class)
1621
class StatusListSignatureVerifier {
1722

1823
private val logger = KotlinLogging.logger {}
@@ -117,37 +122,41 @@ class StatusListSignatureVerifier {
117122

118123
/**
119124
* Resolves the signing key from COSE headers.
125+
*
120126
* Resolution order:
121-
* 1. x5chain header - import from certificate chain
122-
* 2. kid header with DID URL - resolve via DID service
127+
* 1. x5chain header in PROTECTED headers (ISO 18013-5 Second Edition §12.3.6.3) - import from certificate chain
128+
* 2. kid header with DID URL in PROTECTED headers - resolve via DID service
129+
*
130+
* Note: ISO 18013-5 Second Edition mandates x5chain in protected headers.
123131
*/
124132
private suspend fun resolveKeyFromCoseHeaders(coseSign1: CoseSign1): Result<Key> = runCatching {
125-
val unprotectedHeaders = coseSign1.unprotected
133+
// Decode protected headers
134+
val protectedHeaders = coseCompliantCbor.decodeFromByteArray<CoseHeaders>(coseSign1.protected)
126135

127-
// Try x5chain first (X.509 certificate chain in COSE)
128-
unprotectedHeaders.x5chain?.let { x5chain ->
136+
// Try x5chain in PROTECTED headers (ISO 18013-5 Second Edition compliant)
137+
protectedHeaders.x5chain?.let { x5chain ->
129138
if (x5chain.isNotEmpty()) {
130-
logger.debug { "Resolving key from x5chain header" }
139+
logger.debug { "Resolving key from x5chain in PROTECTED headers (ISO 18013-5 compliant)" }
131140
val leafCertBytes = x5chain.first().rawBytes
132141
return@runCatching JWKKey.importFromDerCertificate(leafCertBytes).getOrElse {
133142
throw KeyResolutionFailedException("Failed to import key from x5chain: ${it.message}")
134143
}
135144
}
136145
}
137146

138-
// Try kid with DID URL
139-
unprotectedHeaders.kid?.let { kidBytes ->
147+
// Try kid with DID URL in PROTECTED headers
148+
protectedHeaders.kid?.let { kidBytes ->
140149
val kid = kidBytes.decodeToString()
141150
if (DidUtils.isDidUrl(kid)) {
142-
logger.debug { "Resolving key from DID in CWT kid: $kid" }
151+
logger.debug { "Resolving key from DID in CWT kid (protected): $kid" }
143152
val didWithoutFragment = kid.substringBefore("#")
144153
return@runCatching DidService.resolveToKey(didWithoutFragment).getOrElse {
145154
throw KeyResolutionFailedException("Failed to resolve DID $kid: ${it.message}")
146155
}
147156
}
148157
}
149158

150-
throw KeyResolutionFailedException("No resolvable key information in CWT headers (expected x5chain or kid with DID)")
159+
throw KeyResolutionFailedException("No resolvable key information in CWT protected headers (expected x5chain or kid with DID)")
151160
}
152161

153162
/**

waltid-libraries/credentials/waltid-verification-policies2/src/jvmTest/kotlin/id/walt/policies2/vc/status/signature/StatusListSignatureVerifierTest.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,11 @@ class StatusListSignatureVerifierTest {
258258
private suspend fun createSignedCwt(key: JWKKey, did: String, payload: ByteArray): ByteArray {
259259
val protectedHeaders = CoseHeaders(
260260
algorithm = -8, // EdDSA
261-
contentType = id.walt.cose.CoseContentType.AsString("statuslist+cwt")
261+
contentType = id.walt.cose.CoseContentType.AsString("statuslist+cwt"),
262+
kid = "$did#${key.getKeyId()}".encodeToByteArray() // ISO 18013-5 Second Edition: kid in protected headers
262263
)
263264

264-
val unprotectedHeaders = CoseHeaders(
265-
kid = "$did#${key.getKeyId()}".encodeToByteArray()
266-
)
265+
val unprotectedHeaders = CoseHeaders()
267266

268267
val signer = key.toCoseSigner()
269268

0 commit comments

Comments
 (0)