|
1 | 1 | package id.walt.policies2.vc.policies.status.signature |
2 | 2 |
|
| 3 | +import id.walt.cose.CoseHeaders |
3 | 4 | import id.walt.cose.CoseSign1 |
| 5 | +import id.walt.cose.coseCompliantCbor |
4 | 6 | import id.walt.cose.toCoseVerifier |
5 | 7 | import id.walt.crypto.keys.Key |
6 | 8 | import id.walt.crypto.keys.jwk.JWKKey |
7 | 9 | import id.walt.crypto.utils.JwsUtils.decodeJws |
8 | 10 | import id.walt.did.dids.DidService |
9 | 11 | import id.walt.did.dids.DidUtils |
10 | 12 | import io.github.oshai.kotlinlogging.KotlinLogging |
| 13 | +import kotlinx.serialization.ExperimentalSerializationApi |
| 14 | +import kotlinx.serialization.decodeFromByteArray |
11 | 15 | import kotlinx.serialization.json.JsonArray |
12 | 16 | import kotlinx.serialization.json.JsonObject |
13 | 17 | import kotlinx.serialization.json.jsonArray |
14 | 18 | import kotlinx.serialization.json.jsonPrimitive |
15 | 19 |
|
| 20 | +@OptIn(ExperimentalSerializationApi::class) |
16 | 21 | class StatusListSignatureVerifier { |
17 | 22 |
|
18 | 23 | private val logger = KotlinLogging.logger {} |
@@ -117,37 +122,41 @@ class StatusListSignatureVerifier { |
117 | 122 |
|
118 | 123 | /** |
119 | 124 | * Resolves the signing key from COSE headers. |
| 125 | + * |
120 | 126 | * 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. |
123 | 131 | */ |
124 | 132 | 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) |
126 | 135 |
|
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 -> |
129 | 138 | 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)" } |
131 | 140 | val leafCertBytes = x5chain.first().rawBytes |
132 | 141 | return@runCatching JWKKey.importFromDerCertificate(leafCertBytes).getOrElse { |
133 | 142 | throw KeyResolutionFailedException("Failed to import key from x5chain: ${it.message}") |
134 | 143 | } |
135 | 144 | } |
136 | 145 | } |
137 | 146 |
|
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 -> |
140 | 149 | val kid = kidBytes.decodeToString() |
141 | 150 | 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" } |
143 | 152 | val didWithoutFragment = kid.substringBefore("#") |
144 | 153 | return@runCatching DidService.resolveToKey(didWithoutFragment).getOrElse { |
145 | 154 | throw KeyResolutionFailedException("Failed to resolve DID $kid: ${it.message}") |
146 | 155 | } |
147 | 156 | } |
148 | 157 | } |
149 | 158 |
|
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)") |
151 | 160 | } |
152 | 161 |
|
153 | 162 | /** |
|
0 commit comments