Skip to content

Commit

Permalink
SIOPv2: Fix ISO structure in verifiable presentation
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh committed Oct 16, 2024
1 parent 8667c1b commit ddb6023
Show file tree
Hide file tree
Showing 12 changed files with 129 additions and 87 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Release 5.1.0:
- Update JsonPath4K to 2.4.0
- Fix XCF export with transitive dependencies

Release 5.0.1:
- Fix verifiable presentation of ISO credentials to contain `DeviceResponse` instead of a `Document`
- Data classes for verification result of ISO structures now may contain more than one document

Release 5.0.0:
- Remove `OidcSiopWallet.newDefaultInstance()` and replace it with a constructor
- Remove `OidcSiopVerifier.newInstance()` methods and replace them with constructors
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ kotlin.mpp.enableCInteropCommonization=true
kotlin.mpp.stability.nowarn=true
kotlin.native.ignoreDisabledTargets=true

artifactVersion = 5.1.0-SNAPSHOT
artifactVersion = 5.0.1-SNAPSHOT
jdk.version=17


Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ class OidcSiopVerifier private constructor(
/**
* Successfully decoded and validated the response from the Wallet (ISO credential)
*/
data class SuccessIso(val document: IsoDocumentParsed, val state: String?) :
data class SuccessIso(val documents: Collection<IsoDocumentParsed>, val state: String?) :
AuthnResponseResult()
}

Expand Down Expand Up @@ -656,7 +656,7 @@ class OidcSiopVerifier private constructor(
.also { Napier.i("VP success: $this") }

is Verifier.VerifyPresentationResult.SuccessIso ->
AuthnResponseResult.SuccessIso(document, state)
AuthnResponseResult.SuccessIso(documents, state)
.also { Napier.i("VP success: $this") }

is Verifier.VerifyPresentationResult.SuccessSdJwt ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import at.asitplus.openid.OpenIdConstants.SCOPE_OPENID
import at.asitplus.openid.OpenIdConstants.URN_TYPE_JWK_THUMBPRINT
import at.asitplus.openid.OpenIdConstants.VP_TOKEN
import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.io.Base64UrlStrict
import at.asitplus.signum.indispensable.josef.JsonWebKey
import at.asitplus.signum.indispensable.josef.JsonWebKeySet
import at.asitplus.signum.indispensable.josef.JwsSigned
Expand All @@ -26,7 +27,6 @@ import at.asitplus.wallet.lib.oidc.helper.PresentationFactory
import at.asitplus.wallet.lib.oidc.helpers.AuthorizationResponsePreparationState
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
import io.github.aakira.napier.Napier
import io.matthewnelson.encoding.base16.Base16
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.datetime.Clock
import kotlinx.serialization.json.JsonPrimitive
Expand Down Expand Up @@ -285,9 +285,8 @@ class OidcSiopWallet(
private fun Holder.CreatePresentationResult.toJsonPrimitive() = when (this) {
is Holder.CreatePresentationResult.Signed -> JsonPrimitive(jws)
is Holder.CreatePresentationResult.SdJwt -> JsonPrimitive(sdJwt)
is Holder.CreatePresentationResult.Document -> JsonPrimitive(
document.serialize().encodeToString(Base16(strict = true))
)
is Holder.CreatePresentationResult.DeviceResponse ->
JsonPrimitive(deviceResponse.serialize().encodeToString(Base64UrlStrict))
}

private fun List<JsonPrimitive>.singleOrArray() = if (size == 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ class OidcSiopIsoProtocolTest : FreeSpec({
val result = verifierSiop.validateAuthnResponseFromPost(authnResponse.params.formUrlEncode())
result.shouldBeInstanceOf<OidcSiopVerifier.AuthnResponseResult.SuccessIso>()

val document = result.document
val document = result.documents.first()

document.validItems.shouldNotBeEmpty()
document.validItems.shouldBeSingleton()
Expand Down Expand Up @@ -217,5 +217,5 @@ private suspend fun runProcess(

val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf<OidcSiopVerifier.AuthnResponseResult.SuccessIso>()
return result.document
return result.documents.first()
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({

val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf<OidcSiopVerifier.AuthnResponseResult.SuccessIso>()
result.document.validItems.shouldNotBeEmpty()
result.documents.first().validItems.shouldNotBeEmpty()
}
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ package at.asitplus.wallet.lib.agent

import at.asitplus.KmmResult
import at.asitplus.KmmResult.Companion.wrap
import at.asitplus.signum.indispensable.getJcaPublicKey
import at.asitplus.signum.indispensable.josef.*
import at.asitplus.signum.supreme.HazardousMaterials
import at.asitplus.signum.supreme.hazmat.jcaPrivateKey
import java.security.Security
import javax.crypto.Cipher
import javax.crypto.KeyAgreement
import javax.crypto.Mac
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec

actual open class PlatformCryptoShim actual constructor(actual val keyMaterial: KeyMaterial) {
Expand All @@ -18,18 +23,13 @@ actual open class PlatformCryptoShim actual constructor(actual val keyMaterial:
algorithm: JweEncryption
): KmmResult<AuthenticatedCiphertext> = runCatching {
val jcaCiphertext = Cipher.getInstance(algorithm.jcaName).also {
it.init(
Cipher.ENCRYPT_MODE,
SecretKeySpec(key, algorithm.jcaKeySpecName),
IvParameterSpec(iv)
)
if (algorithm.isAuthenticatedEncryption) {
it.init(
Cipher.ENCRYPT_MODE,
SecretKeySpec(key, algorithm.jcaKeySpecName),
GCMParameterSpec(algorithm.ivLengthBits, iv)
)
it.updateAAD(aad)
} else {
it.init(
Cipher.ENCRYPT_MODE,
SecretKeySpec(key, algorithm.jcaKeySpecName),
)
}
}.doFinal(input)
if (algorithm.isAuthenticatedEncryption) {
Expand All @@ -41,7 +41,6 @@ actual open class PlatformCryptoShim actual constructor(actual val keyMaterial:
}
}.wrap()


actual open suspend fun decrypt(
key: ByteArray,
iv: ByteArray,
Expand All @@ -50,34 +49,43 @@ actual open class PlatformCryptoShim actual constructor(actual val keyMaterial:
authTag: ByteArray,
algorithm: JweEncryption
): KmmResult<ByteArray> = runCatching {
val wholeInput = input + if (algorithm.isAuthenticatedEncryption) authTag else byteArrayOf()
Cipher.getInstance(algorithm.jcaName).also {
it.init(
Cipher.DECRYPT_MODE,
SecretKeySpec(key, algorithm.jcaKeySpecName),
IvParameterSpec(iv)
)
if (algorithm.isAuthenticatedEncryption) {
it.init(
Cipher.DECRYPT_MODE,
SecretKeySpec(key, algorithm.jcaKeySpecName),
GCMParameterSpec(algorithm.ivLengthBits, iv)
)
it.updateAAD(aad)
} else {
it.init(
Cipher.DECRYPT_MODE,
SecretKeySpec(key, algorithm.jcaKeySpecName),
)
}
}.doFinal(input + authTag)
}.doFinal(wholeInput)
}.wrap()

actual open fun performKeyAgreement(
ephemeralKey: EphemeralKeyHolder,
recipientKey: JsonWebKey,
algorithm: JweAlgorithm
): KmmResult<ByteArray> {
return KmmResult.success("sharedSecret-${algorithm.identifier}".encodeToByteArray())
}
): KmmResult<ByteArray> = runCatching {
val jvmKey = recipientKey.toCryptoPublicKey().getOrThrow().getJcaPublicKey().getOrThrow()
KeyAgreement.getInstance(algorithm.jcaName).also {
@OptIn(HazardousMaterials::class)
it.init(ephemeralKey.key.jcaPrivateKey)
it.doPhase(jvmKey, true)
}.generateSecret()
}.wrap()

actual open fun performKeyAgreement(ephemeralKey: JsonWebKey, algorithm: JweAlgorithm): KmmResult<ByteArray> {
return KmmResult.success("sharedSecret-${algorithm.identifier}".encodeToByteArray())
}
actual open fun performKeyAgreement(
ephemeralKey: JsonWebKey,
algorithm: JweAlgorithm
): KmmResult<ByteArray> = runCatching {
val publicKey = ephemeralKey.toCryptoPublicKey().getOrThrow().getJcaPublicKey().getOrThrow()
KeyAgreement.getInstance(algorithm.jcaName).also {
@OptIn(HazardousMaterials::class)
it.init(keyMaterial.getUnderLyingSigner().jcaPrivateKey)
it.doPhase(publicKey, true)
}.generateSecret()
}.wrap()

actual open fun hmac(
key: ByteArray,
Expand Down
10 changes: 3 additions & 7 deletions vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Holder.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package at.asitplus.wallet.lib.agent

import at.asitplus.KmmResult
import at.asitplus.dif.*
import at.asitplus.jsonpath.core.NodeList
import at.asitplus.jsonpath.core.NormalizedJsonPath
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.VerifiablePresentation
import at.asitplus.dif.ConstraintField
import at.asitplus.dif.FormatHolder
import at.asitplus.dif.InputDescriptor
import at.asitplus.dif.PresentationDefinition
import at.asitplus.dif.PresentationSubmission
import at.asitplus.wallet.lib.iso.IssuerSigned
import kotlinx.serialization.Serializable

Expand Down Expand Up @@ -189,9 +185,9 @@ interface Holder {
data class SdJwt(val sdJwt: String) : CreatePresentationResult()

/**
* [document] contains a valid ISO 18013 [Document] with [IssuerSigned] and [DeviceSigned] structures
* [deviceResponse] contains a valid ISO 18013 [DeviceResponse] with [Document] and [DeviceSigned] structures
*/
data class Document(val document: at.asitplus.wallet.lib.iso.Document) :
data class DeviceResponse(val deviceResponse: at.asitplus.wallet.lib.iso.DeviceResponse) :
CreatePresentationResult()
}

Expand Down
43 changes: 30 additions & 13 deletions vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -240,50 +240,69 @@ class Validator(
)
}

/**
* Validates an ISO device response, equivalent of a Verifiable Presentation
*/
@Throws(IllegalArgumentException::class)
fun verifyDeviceResponse(deviceResponse: DeviceResponse, challenge: String): Verifier.VerifyPresentationResult {
if (deviceResponse.status != 0U) {
throw IllegalArgumentException("status")
.also { Napier.w("Status invalid: ${deviceResponse.status}") }
}
if (deviceResponse.documents == null) {
throw IllegalArgumentException("documents")
.also { Napier.w("No documents: $deviceResponse") }
}
return Verifier.VerifyPresentationResult.SuccessIso(
documents = deviceResponse.documents.map { verifyDocument(it, challenge) }
)
}

/**
* Validates an ISO document, equivalent of a Verifiable Presentation
*/
fun verifyDocument(doc: Document, challenge: String): Verifier.VerifyPresentationResult {
@Throws(IllegalArgumentException::class)
fun verifyDocument(doc: Document, challenge: String): IsoDocumentParsed {
val docSerialized = doc.serialize().encodeToString(Base16(strict = true))
if (doc.errors != null) {
return Verifier.VerifyPresentationResult.InvalidStructure(docSerialized)
throw IllegalArgumentException("errors")
.also { Napier.w("Document has errors: ${doc.errors}") }
}
val issuerSigned = doc.issuerSigned
val issuerAuth = issuerSigned.issuerAuth

val issuerKey = issuerAuth.unprotectedHeader?.certificateChain?.let {
X509Certificate.decodeFromDerOrNull(it)?.publicKey?.toCoseKey()?.getOrNull()
} ?: return Verifier.VerifyPresentationResult.InvalidStructure(docSerialized)
} ?: throw IllegalArgumentException("issuerKey")
.also { Napier.w("Got no issuer key in $issuerAuth") }

if (verifierCoseService.verifyCose(issuerAuth, issuerKey).isFailure) {
return Verifier.VerifyPresentationResult.InvalidStructure(docSerialized)
throw IllegalArgumentException("issuerAuth")
.also { Napier.w("IssuerAuth not verified: $issuerAuth") }
}

val mso = issuerSigned.getIssuerAuthPayloadAsMso().getOrNull()
?: return Verifier.VerifyPresentationResult.InvalidStructure(docSerialized)
?: throw IllegalArgumentException("mso")
.also { Napier.w("MSO is null: ${issuerAuth.payload?.encodeToString(Base16(strict = true))}") }
if (mso.docType != doc.docType) {
return Verifier.VerifyPresentationResult.InvalidStructure(docSerialized)
throw IllegalArgumentException("mso.docType")
.also { Napier.w("Invalid MSO docType '${mso.docType}' does not match Doc docType '${doc.docType}") }
}
val walletKey = mso.deviceKeyInfo.deviceKey
val deviceSignature = doc.deviceSigned.deviceAuth.deviceSignature
?: return Verifier.VerifyPresentationResult.InvalidStructure(docSerialized)
?: throw IllegalArgumentException("deviceSignature")
.also { Napier.w("DeviceSignature is null: ${doc.deviceSigned.deviceAuth}") }

if (verifierCoseService.verifyCose(deviceSignature, walletKey).isFailure) {
return Verifier.VerifyPresentationResult.InvalidStructure(docSerialized)
throw IllegalArgumentException("deviceSignature")
.also { Napier.w("DeviceSignature not verified") }
}

val deviceSignaturePayload = deviceSignature.payload
?: return Verifier.VerifyPresentationResult.InvalidStructure(docSerialized)
?: throw IllegalArgumentException("challenge")
.also { Napier.w("DeviceSignature does not contain challenge") }
if (!deviceSignaturePayload.contentEquals(challenge.encodeToByteArray())) {
return Verifier.VerifyPresentationResult.InvalidStructure(docSerialized)
throw IllegalArgumentException("challenge")
.also { Napier.w("DeviceSignature does not contain correct challenge") }
}

Expand All @@ -298,9 +317,7 @@ class Validator(
}
}
}
return Verifier.VerifyPresentationResult.SuccessIso(
IsoDocumentParsed(mso = mso, validItems = validItems, invalidItems = invalidItems)
)
return IsoDocumentParsed(mso = mso, validItems = validItems, invalidItems = invalidItems)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class VerifiablePresentationFactory(
challenge: String,
credential: SubjectCredentialStore.StoreEntry.Iso,
requestedClaims: Collection<NormalizedJsonPath>
): Holder.CreatePresentationResult.Document {
): Holder.CreatePresentationResult.DeviceResponse {
val deviceSignature = coseService.createSignedCose(
payload = challenge.encodeToByteArray(), addKeyId = false
).getOrElse {
Expand Down Expand Up @@ -99,20 +99,25 @@ class VerifiablePresentationFactory(
?: throw PresentationException("Attribute not available in credential: $['$namespace']['$attributeName']")
}
}

return Holder.CreatePresentationResult.Document(
Document(
docType = credential.scheme.isoDocType!!,
issuerSigned = IssuerSigned.fromIssuerSignedItems(
namespacedItems = disclosedItems,
issuerAuth = credential.issuerSigned.issuerAuth
),
deviceSigned = DeviceSigned(
namespaces = ByteStringWrapper(DeviceNameSpaces(mapOf())),
deviceAuth = DeviceAuth(
deviceSignature = deviceSignature
return Holder.CreatePresentationResult.DeviceResponse(
DeviceResponse(
version = "1.0",
documents = arrayOf(
Document(
docType = credential.scheme.isoDocType!!,
issuerSigned = IssuerSigned.fromIssuerSignedItems(
namespacedItems = disclosedItems,
issuerAuth = credential.issuerSigned.issuerAuth
),
deviceSigned = DeviceSigned(
namespaces = ByteStringWrapper(DeviceNameSpaces(mapOf())),
deviceAuth = DeviceAuth(
deviceSignature = deviceSignature
)
)
)
)
),
status = 0U,
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.josef.JwsSigned
import at.asitplus.signum.indispensable.josef.jwkId
import at.asitplus.signum.indispensable.josef.toJsonWebKey
import at.asitplus.wallet.lib.data.IsoDocumentParsed
import at.asitplus.wallet.lib.data.SelectiveDisclosureItem
import at.asitplus.wallet.lib.data.VerifiableCredentialJws
import at.asitplus.wallet.lib.data.VerifiableCredentialSdJwt
import at.asitplus.wallet.lib.data.VerifiablePresentationParsed
import at.asitplus.wallet.lib.data.*
import at.asitplus.wallet.lib.iso.IssuerSigned


Expand Down Expand Up @@ -53,7 +49,7 @@ interface Verifier {
val isRevoked: Boolean
) : VerifyPresentationResult()

data class SuccessIso(val document: IsoDocumentParsed) : VerifyPresentationResult()
data class SuccessIso(val documents: Collection<IsoDocumentParsed>) : VerifyPresentationResult()
data class InvalidStructure(val input: String) : VerifyPresentationResult()
data class NotVerified(val input: String, val challenge: String) : VerifyPresentationResult()
}
Expand Down
Loading

0 comments on commit ddb6023

Please sign in to comment.