diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt index 4a6297ce2..4c7d80d0a 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/CryptoService.kt @@ -40,8 +40,7 @@ interface CryptoService { fun messageDigest(input: ByteArray, digest: Digest): KmmResult - val identifier: String - get() = toJsonWebKey().getIdentifier() + val keyId: String val jwsAlgorithm: JwsAlgorithm @@ -58,10 +57,8 @@ interface VerifierCryptoService { publicKey: JsonWebKey ): KmmResult -} - -expect object CryptoUtils { fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? + } data class AuthenticatedCiphertext(val ciphertext: ByteArray, val authtag: ByteArray) { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/EmptyCredentialDataProvider.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/EmptyCredentialDataProvider.kt index fd86f0f6f..89af13c1f 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/EmptyCredentialDataProvider.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/EmptyCredentialDataProvider.kt @@ -5,14 +5,9 @@ import at.asitplus.KmmResult object EmptyCredentialDataProvider : IssuerCredentialDataProvider { override fun getClaim(subjectId: String, attributeName: String) - : KmmResult = - KmmResult.failure(NotImplementedError()) + : KmmResult = KmmResult.failure(NullPointerException()) override fun getCredential(subjectId: String, attributeType: String) - : KmmResult = - KmmResult.failure(NotImplementedError()) + : KmmResult = KmmResult.failure(NullPointerException()) - override fun getCredentialWithType(subjectId: String, attributeTypes: Collection) - : KmmResult> = - KmmResult.failure(NotImplementedError()) } \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Holder.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Holder.kt index a99f5d7b2..e68cc8c1c 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Holder.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Holder.kt @@ -10,12 +10,6 @@ import at.asitplus.wallet.lib.data.VerifiablePresentation */ interface Holder { - /** - * The identifier for this agent, typically the `keyId` from the cryptographic key, - * e.g. `did:key:mAB...` or `urn:ietf:params:oauth:jwk-thumbprint:sha256:...` - */ - val identifier: String - /** * Sets the revocation list ot use for further processing of Verifiable Credentials * @@ -119,4 +113,5 @@ interface Holder { data class Signed(val jws: String) : CreatePresentationResult() } + } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt index cd16d284b..00069a511 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt @@ -3,9 +3,10 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.data.VerifiableCredentialJws import at.asitplus.wallet.lib.data.VerifiablePresentation import at.asitplus.wallet.lib.jws.DefaultJwsService -import at.asitplus.wallet.lib.jws.JwsContentTypeConstants +import at.asitplus.wallet.lib.jws.JwsContentType import at.asitplus.wallet.lib.jws.JwsService import io.github.aakira.napier.Napier +import kotlinx.datetime.Clock /** @@ -16,7 +17,7 @@ class HolderAgent constructor( private val validator: Validator = Validator.newDefaultInstance(), private val subjectCredentialStore: SubjectCredentialStore = InMemorySubjectCredentialStore(), private val jwsService: JwsService, - override val identifier: String + private val keyId: String ) : Holder { companion object { @@ -24,11 +25,12 @@ class HolderAgent constructor( cryptoService: CryptoService = DefaultCryptoService(), verifierCryptoService: VerifierCryptoService = DefaultVerifierCryptoService(), subjectCredentialStore: SubjectCredentialStore = InMemorySubjectCredentialStore(), + clock: Clock = Clock.System, ) = HolderAgent( validator = Validator.newDefaultInstance(verifierCryptoService, Parser()), subjectCredentialStore = subjectCredentialStore, jwsService = DefaultJwsService(cryptoService), - identifier = cryptoService.identifier, + keyId = cryptoService.keyId ) /** @@ -41,7 +43,7 @@ class HolderAgent constructor( validator = Validator.newDefaultInstance(DefaultVerifierCryptoService(), Parser()), subjectCredentialStore = subjectCredentialStore, jwsService = DefaultJwsService(cryptoService), - identifier = cryptoService.identifier, + keyId = cryptoService.keyId ) } @@ -65,7 +67,7 @@ class HolderAgent constructor( val rejected = mutableListOf() val attachments = mutableListOf() credentialList.forEach { cred -> - when (val vc = validator.verifyVcJws(cred.vcJws, identifier)) { + when (val vc = validator.verifyVcJws(cred.vcJws, keyId)) { is Verifier.VerifyCredentialResult.InvalidStructure -> rejected += vc.input is Verifier.VerifyCredentialResult.Revoked -> rejected += vc.input is Verifier.VerifyCredentialResult.Success -> accepted += vc.jws @@ -147,9 +149,9 @@ class HolderAgent constructor( audienceId: String, ): Holder.CreatePresentationResult? { val vp = VerifiablePresentation(validCredentials.toTypedArray()) - val vpSerialized = vp.toJws(challenge, identifier, audienceId).serialize() + val vpSerialized = vp.toJws(challenge, keyId, audienceId).serialize() val jwsPayload = vpSerialized.encodeToByteArray() - val jws = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + val jws = jwsService.createSignedJwt(JwsContentType.JWT, jwsPayload) ?: return null return Holder.CreatePresentationResult.Signed(jws) } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InternalNextMessage.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InternalNextMessage.kt index 025ca7fc0..ca0b4ecad 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InternalNextMessage.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InternalNextMessage.kt @@ -1,6 +1,5 @@ package at.asitplus.wallet.lib.agent -import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.msg.JsonWebMessage @@ -12,7 +11,7 @@ sealed class InternalNextMessage { data class SendAndWrap( val message: JsonWebMessage, - val senderKey: JsonWebKey? = null, + val senderKeyId: String? = null, val endpoint: String? = null ) : InternalNextMessage() @@ -22,7 +21,7 @@ sealed class InternalNextMessage { data class SendProblemReport( val message: JsonWebMessage, - val senderKey: JsonWebKey? = null, + val senderKeyId: String? = null, val endpoint: String? = null ) : InternalNextMessage() } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessenger.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessenger.kt index 4d4d089aa..176fae853 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessenger.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessenger.kt @@ -6,6 +6,7 @@ import at.asitplus.wallet.lib.data.ConstantIndex class IssueCredentialMessenger private constructor( private val issuer: Issuer? = null, private val holder: Holder? = null, + private val keyId: String, messageWrapper: MessageWrapper, private val serviceEndpoint: String = "https://example.com/", createProtocolWhenNotActive: Boolean = true, @@ -21,6 +22,7 @@ class IssueCredentialMessenger private constructor( override fun createProtocolInstance() = IssueCredentialProtocol( issuer = issuer, holder = holder, + keyId = keyId, serviceEndpoint = serviceEndpoint, credentialScheme = credentialScheme, ) @@ -32,10 +34,12 @@ class IssueCredentialMessenger private constructor( */ fun newHolderInstance( holder: Holder, + keyId: String, messageWrapper: MessageWrapper, credentialScheme: ConstantIndex.CredentialScheme = ConstantIndex.Generic, ) = IssueCredentialMessenger( holder = holder, + keyId = keyId, messageWrapper = messageWrapper, credentialScheme = credentialScheme, ) @@ -46,11 +50,13 @@ class IssueCredentialMessenger private constructor( */ fun newIssuerInstance( issuer: Issuer, + keyId: String, messageWrapper: MessageWrapper, serviceEndpoint: String, credentialScheme: ConstantIndex.CredentialScheme = ConstantIndex.Generic, ) = IssueCredentialMessenger( issuer = issuer, + keyId = keyId, messageWrapper = messageWrapper, serviceEndpoint = serviceEndpoint, credentialScheme = credentialScheme, diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialProtocol.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialProtocol.kt index f8d6dd9c5..928a0eeaa 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialProtocol.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialProtocol.kt @@ -3,11 +3,12 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.DataSourceProblem import at.asitplus.wallet.lib.data.AttributeIndex import at.asitplus.wallet.lib.data.ConstantIndex +import at.asitplus.wallet.lib.msg.RequestCredentialAttachment import at.asitplus.wallet.lib.data.SchemaIndex +import at.asitplus.wallet.lib.msg.SchemaReference import at.asitplus.wallet.lib.data.dif.CredentialDefinition import at.asitplus.wallet.lib.data.dif.CredentialManifest import at.asitplus.wallet.lib.data.jsonSerializer -import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.msg.AttachmentFormatReference import at.asitplus.wallet.lib.msg.IssueCredential import at.asitplus.wallet.lib.msg.IssueCredentialBody @@ -17,9 +18,7 @@ import at.asitplus.wallet.lib.msg.OutOfBandInvitation import at.asitplus.wallet.lib.msg.OutOfBandInvitationBody import at.asitplus.wallet.lib.msg.OutOfBandService import at.asitplus.wallet.lib.msg.RequestCredential -import at.asitplus.wallet.lib.msg.RequestCredentialAttachment import at.asitplus.wallet.lib.msg.RequestCredentialBody -import at.asitplus.wallet.lib.msg.SchemaReference import io.github.aakira.napier.Napier import kotlinx.serialization.encodeToString @@ -40,6 +39,7 @@ typealias IssueCredentialProtocolResult = Holder.StoredCredentialsResult class IssueCredentialProtocol( private val issuer: Issuer? = null, private val holder: Holder? = null, + private val keyId: String, private val serviceEndpoint: String? = null, private val credentialScheme: ConstantIndex.CredentialScheme ) : ProtocolStateMachine { @@ -51,9 +51,11 @@ class IssueCredentialProtocol( */ fun newHolderInstance( holder: Holder, + keyId: String, credentialScheme: ConstantIndex.CredentialScheme = ConstantIndex.Generic, ) = IssueCredentialProtocol( holder = holder, + keyId = keyId, credentialScheme = credentialScheme, ) @@ -63,10 +65,12 @@ class IssueCredentialProtocol( */ fun newIssuerInstance( issuer: Issuer, + keyId: String, serviceEndpoint: String? = null, credentialScheme: ConstantIndex.CredentialScheme = ConstantIndex.Generic, ) = IssueCredentialProtocol( issuer = issuer, + keyId = keyId, serviceEndpoint = serviceEndpoint, credentialScheme = credentialScheme, ) @@ -101,13 +105,13 @@ class IssueCredentialProtocol( return createRequestCredential() } - override suspend fun parseMessage(body: JsonWebMessage, senderKey: JsonWebKey): InternalNextMessage { + override suspend fun parseMessage(body: JsonWebMessage, senderKeyId: String): InternalNextMessage { when (this.state) { State.START -> { if (body is OutOfBandInvitation) - return createRequestCredential(body, senderKey) + return createRequestCredential(body, senderKeyId) if (body is RequestCredential) - return issueCredential(body, senderKey) + return issueCredential(body, senderKeyId) return InternalNextMessage.IncorrectState("messageType") .also { Napier.w("Unexpected messageType: ${body.type}") } } @@ -119,7 +123,7 @@ class IssueCredentialProtocol( if (body.parentThreadId != invitationId) return InternalNextMessage.IncorrectState("parentThreadId") .also { Napier.w("Unexpected parentThreadId: ${body.parentThreadId}") } - return issueCredential(body, senderKey) + return issueCredential(body, senderKeyId) } State.REQUEST_CREDENTIAL_SENT -> { @@ -138,8 +142,6 @@ class IssueCredentialProtocol( } private fun createOobInvitation(): InternalNextMessage { - val recipientKey = issuer?.identifier - ?: return InternalNextMessage.IncorrectState("issuer") val message = OutOfBandInvitation( body = OutOfBandInvitationBody( handshakeProtocols = arrayOf(SchemaIndex.PROT_ISSUE_CRED), @@ -148,7 +150,7 @@ class IssueCredentialProtocol( services = arrayOf( OutOfBandService( type = "did-communication", - recipientKeys = arrayOf(recipientKey), + recipientKeys = arrayOf(keyId), serviceEndpoint = serviceEndpoint ?: "https://example.com", ) ) @@ -161,21 +163,19 @@ class IssueCredentialProtocol( private fun createRequestCredential(): InternalNextMessage { val message = buildRequestCredentialMessage(credentialScheme) - ?: return InternalNextMessage.IncorrectState("holder") return InternalNextMessage.SendAndWrap(message) .also { this.threadId = message.threadId } .also { this.state = State.REQUEST_CREDENTIAL_SENT } } - private fun createRequestCredential(invitation: OutOfBandInvitation, senderKey: JsonWebKey): InternalNextMessage { + private fun createRequestCredential(invitation: OutOfBandInvitation, senderKeyId: String): InternalNextMessage { val credentialScheme = ConstantIndex.Parser.parseGoalCode(invitation.body.goalCode) ?: return problemReporter.problemLastMessage(invitation.threadId, "goal-code-unknown") val message = buildRequestCredentialMessage(credentialScheme, invitation.id) - ?: return InternalNextMessage.IncorrectState("holder") val serviceEndpoint = invitation.body.services?.let { if (it.isNotEmpty()) it[0].serviceEndpoint else null } - return InternalNextMessage.SendAndWrap(message, senderKey, serviceEndpoint) + return InternalNextMessage.SendAndWrap(message, senderKeyId, serviceEndpoint) .also { this.threadId = message.threadId } .also { this.state = State.REQUEST_CREDENTIAL_SENT } } @@ -183,12 +183,10 @@ class IssueCredentialProtocol( private fun buildRequestCredentialMessage( credentialScheme: ConstantIndex.CredentialScheme, parentThreadId: String? = null, - ): RequestCredential? { - val subject = holder?.identifier - ?: return null + ): RequestCredential { val credentialManifest = CredentialManifest( issuer = "somebody", - subject = subject, + subject = keyId, credential = CredentialDefinition( name = credentialScheme.credentialDefinitionName, schema = SchemaReference(uri = credentialScheme.schemaUri), @@ -214,7 +212,7 @@ class IssueCredentialProtocol( ) } - private suspend fun issueCredential(lastMessage: RequestCredential, senderKey: JsonWebKey): InternalNextMessage { + private suspend fun issueCredential(lastMessage: RequestCredential, senderKeyId: String): InternalNextMessage { val lastJwmAttachment = lastMessage.attachments?.firstOrNull() ?: return problemReporter.problemLastMessage(lastMessage.threadId, "attachments-missing") val requestCredentialAttachment = lastJwmAttachment.decodeString()?.let { @@ -223,13 +221,16 @@ class IssueCredentialProtocol( val uri = requestCredentialAttachment.credentialManifest.credential.schema.uri //default pupilID use case: binding key outside JWM, no subject in JWM. set subject to override - val subjectIdentifier = requestCredentialAttachment.credentialManifest.subject ?: senderKey.getIdentifier() + val subjectKeyId = requestCredentialAttachment.credentialManifest.subject ?: senderKeyId val requestedAttributeType = AttributeIndex.getTypeOfAttributeForSchemaUri(uri) - ?: return problemReporter.problemLastMessage(lastMessage.threadId, "requested-attributes-empty") + val requestedAttributes = AttributeIndex.getListOfAttributesForSchemaUri(uri) - val issuedCredentials = issuer?.issueCredentialWithTypes(subjectIdentifier, listOf(requestedAttributeType)) - ?: return problemReporter.problemInternal(lastMessage.threadId, "credentials-empty") + val issuedCredentials = when { + requestedAttributeType != null -> issuer?.issueCredential(subjectKeyId, requestedAttributeType) + requestedAttributes.isNotEmpty() -> issuer?.issueCredentials(subjectKeyId, requestedAttributes) + else -> return problemReporter.problemLastMessage(lastMessage.threadId, "requested-attributes-empty") + } ?: return problemReporter.problemInternal(lastMessage.threadId, "credentials-empty") //TODO: Pack this info into `args` or `comment` if (issuedCredentials.failed.isNotEmpty()) { @@ -272,7 +273,7 @@ class IssueCredentialProtocol( threadId = lastMessage.threadId!!, //is allowed to fail horribly attachments = (fulfillmentAttachments + binaryAttachments).toTypedArray() ) - return InternalNextMessage.SendAndWrap(message, senderKey) + return InternalNextMessage.SendAndWrap(message, senderKeyId) .also { this.threadId = message.threadId } .also { this.state = State.FINISHED } } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Issuer.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Issuer.kt index d4d75f6a9..261939ec5 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Issuer.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Issuer.kt @@ -24,31 +24,16 @@ interface Issuer { fun toStoreCredentialInput() = successful.map { Holder.StoreCredentialInput(it.vcJws, it.attachments) } } - /** - * The identifier for this agent, typically the `keyId` from the cryptographic key, - * e.g. `did:key:mAB...` or `urn:ietf:params:oauth:jwk-thumbprint:sha256:...` - */ - val identifier: String - /** * Issues credentials for all [attributeNames] to [subjectId] */ - @Deprecated(message = "Use attribute types only and call `issueCredentialWithTypes`") suspend fun issueCredentials(subjectId: String, attributeNames: List): IssuedCredentialResult /** * Issues credential for [attributeType] to [subjectId] */ - @Deprecated(message = "Use attribute types only and call `issueCredentialWithTypes`") suspend fun issueCredential(subjectId: String, attributeType: String): IssuedCredentialResult - /** - * Issues credentials for some [attributeTypes] (i.e. some of - * [at.asitplus.wallet.lib.data.ConstantIndex.CredentialScheme.vcType]) to the subject specified with [subjectId] - * (which should be a URL of the cryptographic key of the holder) - */ - suspend fun issueCredentialWithTypes(subjectId: String, attributeTypes: Collection): IssuedCredentialResult - /** * Wraps [credential] in a single [VerifiableCredential], * returns a JWS representation of that VC. diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt index 622c8a291..d2bbfb7c5 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt @@ -9,7 +9,7 @@ import at.asitplus.wallet.lib.data.CredentialStatus import at.asitplus.wallet.lib.data.RevocationListSubject import at.asitplus.wallet.lib.data.VerifiableCredential import at.asitplus.wallet.lib.jws.DefaultJwsService -import at.asitplus.wallet.lib.jws.JwsContentTypeConstants +import at.asitplus.wallet.lib.jws.JwsContentType import at.asitplus.wallet.lib.jws.JwsService import com.benasher44.uuid.uuid4 import io.github.aakira.napier.Napier @@ -30,13 +30,13 @@ class IssuerAgent constructor( private val revocationListLifetime: Duration = 48.hours, private val jwsService: JwsService, private val clock: Clock = Clock.System, - override val identifier: String, + private val keyId: String, private val timePeriodProvider: TimePeriodProvider = FixedTimePeriodProvider, ) : Issuer { companion object { fun newDefaultInstance( - cryptoService: CryptoService = DefaultCryptoService(), + cryptoService: CryptoService, verifierCryptoService: VerifierCryptoService = DefaultVerifierCryptoService(), issuerCredentialStore: IssuerCredentialStore = InMemoryIssuerCredentialStore(), clock: Clock = Clock.System, @@ -50,7 +50,7 @@ class IssuerAgent constructor( issuerCredentialStore = issuerCredentialStore, jwsService = DefaultJwsService(cryptoService), dataProvider = dataProvider, - identifier = cryptoService.identifier, + keyId = cryptoService.keyId, timePeriodProvider = timePeriodProvider, clock = clock, ) @@ -94,25 +94,6 @@ class IssuerAgent constructor( } ) - /** - * Issues credentials for some [attributeTypes] (i.e. some of - * [at.asitplus.wallet.lib.data.ConstantIndex.CredentialScheme.vcType]) to the subject specified with [subjectId] - * (which should be a URL of the cryptographic key of the holder) - */ - override suspend fun issueCredentialWithTypes( - subjectId: String, - attributeTypes: Collection - ): Issuer.IssuedCredentialResult { - val result = dataProvider.getCredentialWithType(subjectId, attributeTypes) - result.exceptionOrNull()?.let { failure -> - return Issuer.IssuedCredentialResult(failed = attributeTypes.map { Issuer.FailedAttribute(it, failure) }) - } - val issuedCredentials = result.getOrThrow().map { issueCredential(it) } - return Issuer.IssuedCredentialResult( - successful = issuedCredentials.flatMap { it.successful }, - failed = issuedCredentials.flatMap { it.failed }) - } - /** * Wraps [credential] into a single [VerifiableCredential], * returns a JWS representation of that VC. @@ -140,7 +121,7 @@ class IssuerAgent constructor( CredentialStatus(getRevocationListUrlFor(timePeriod), statusListIndex) val vc = VerifiableCredential( id = vcId, - issuer = identifier, + issuer = keyId, issuanceDate = issuanceDate, expirationDate = expirationDate, credentialStatus = credentialStatus, @@ -168,7 +149,7 @@ class IssuerAgent constructor( val subject = RevocationListSubject("$revocationListUrl#list", revocationList) val credential = VerifiableCredential( id = revocationListUrl, - issuer = identifier, + issuer = keyId, issuanceDate = clock.now(), lifetime = revocationListLifetime, credentialSubject = subject @@ -216,7 +197,7 @@ class IssuerAgent constructor( private suspend fun wrapVcInJws(vc: VerifiableCredential): String? { val jwsPayload = vc.toJws().serialize().encodeToByteArray() - return jwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + return jwsService.createSignedJwt(JwsContentType.JWT, jwsPayload) } private fun getRevocationListUrlFor(timePeriod: Int) = diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerCredentialDataProvider.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerCredentialDataProvider.kt index ef2d25ff0..688664199 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerCredentialDataProvider.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerCredentialDataProvider.kt @@ -13,25 +13,14 @@ interface IssuerCredentialDataProvider { * Gets called with the attribute name for atomic credentials, and the prefix * [at.asitplus.wallet.lib.data.SchemaIndex.ATTR_GENERIC_PREFIX]. */ - @Deprecated(message = "Use attribute types only and call `getCredentialWithType`") fun getClaim(subjectId: String, attributeName: String): KmmResult /** * Gets called with the credential type, i.e. one of * [at.asitplus.wallet.lib.data.ConstantIndex.CredentialScheme.vcType] */ - @Deprecated(message = "Use attribute types only and call `getCredentialWithType`") fun getCredential(subjectId: String, attributeType: String): KmmResult - /** - * Gets called with a list of credential types, i.e. some of - * [at.asitplus.wallet.lib.data.ConstantIndex.CredentialScheme.vcType] - */ - fun getCredentialWithType( - subjectId: String, - attributeTypes: Collection - ): KmmResult> - data class CredentialToBeIssued( val subject: CredentialSubject, val expiration: Instant, diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/MessageWrapper.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/MessageWrapper.kt index 6959cfb39..92e8eea86 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/MessageWrapper.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/MessageWrapper.kt @@ -6,7 +6,7 @@ import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.jws.JweAlgorithm import at.asitplus.wallet.lib.jws.JweEncrypted import at.asitplus.wallet.lib.jws.JweEncryption -import at.asitplus.wallet.lib.jws.JwsContentTypeConstants +import at.asitplus.wallet.lib.jws.JwsContentType import at.asitplus.wallet.lib.jws.JwsService import at.asitplus.wallet.lib.jws.JwsSigned import at.asitplus.wallet.lib.jws.VerifierJwsService @@ -41,17 +41,17 @@ class MessageWrapper( ?: return ReceivedMessage.Error .also { Napier.w("Could not parse JWE") } val payloadString = joseObject.payload.decodeToString() - if (joseObject.header.contentType == JwsContentTypeConstants.DIDCOMM_SIGNED_JSON) { + if (joseObject.header.contentType == JwsContentType.DIDCOMM_SIGNED_JSON) { val parsed = JwsSigned.parse(payloadString) ?: return ReceivedMessage.Error .also { Napier.w("Could not parse inner JWS") } return parseJwsMessage(parsed, payloadString) } - if (joseObject.header.contentType == JwsContentTypeConstants.DIDCOMM_PLAIN_JSON) { + if (joseObject.header.contentType == JwsContentType.DIDCOMM_PLAIN_JSON) { val message = JsonWebMessage.deserialize(payloadString) ?: return ReceivedMessage.Error .also { Napier.w("Could not parse plain message") } - return ReceivedMessage.Success(message, joseObject.header.getKey()) + return ReceivedMessage.Success(message, joseObject.header.keyId) } return ReceivedMessage.Error .also { Napier.w("ContentType not matching") } @@ -62,38 +62,44 @@ class MessageWrapper( if (!verifierJwsService.verifyJwsObject(joseObject, serialized)) return ReceivedMessage.Error .also { Napier.w("Signature invalid") } - if (joseObject.header.contentType == JwsContentTypeConstants.DIDCOMM_PLAIN_JSON) { + if (joseObject.header.contentType == JwsContentType.DIDCOMM_PLAIN_JSON) { val payloadString = joseObject.payload.decodeToString() val message = JsonWebMessage.deserialize(payloadString) ?: return ReceivedMessage.Error .also { Napier.w("Could not parse plain message") } - return ReceivedMessage.Success(message, joseObject.header.getKey()) + return ReceivedMessage.Success(message, joseObject.header.keyId) } return ReceivedMessage.Error .also { Napier.w("ContentType not matching") } } - fun createEncryptedJwe(jwm: JsonWebMessage, recipientKey: JsonWebKey): String? { + fun createEncryptedJwe(jwm: JsonWebMessage, recipientKeyId: String): String? { val jwePayload = jwm.serialize().encodeToByteArray() + val recipientKey = JsonWebKey.fromKeyId(recipientKeyId) + ?: return null + .also { Napier.w("Can not calc JWK from recipientKeyId: $recipientKeyId") } return jwsService.encryptJweObject( - JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON, + JwsContentType.DIDCOMM_ENCRYPTED_JSON, jwePayload, recipientKey, - JwsContentTypeConstants.DIDCOMM_PLAIN_JSON, + JwsContentType.DIDCOMM_PLAIN_JSON, JweAlgorithm.ECDH_ES, JweEncryption.A256GCM, ) } - suspend fun createSignedAndEncryptedJwe(jwm: JsonWebMessage, recipientKey: JsonWebKey): String? { + suspend fun createSignedAndEncryptedJwe(jwm: JsonWebMessage, recipientKeyId: String): String? { val jwePayload = createSignedJwt(jwm)?.encodeToByteArray() ?: return null .also { Napier.w("Can not create signed JWT for encryption") } + val recipientKey = JsonWebKey.fromKeyId(recipientKeyId) + ?: return null + .also { Napier.w("Can not calc JWK from recipientKeyId: $recipientKeyId") } return jwsService.encryptJweObject( - JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON, + JwsContentType.DIDCOMM_ENCRYPTED_JSON, jwePayload, recipientKey, - JwsContentTypeConstants.DIDCOMM_SIGNED_JSON, + JwsContentType.DIDCOMM_SIGNED_JSON, JweAlgorithm.ECDH_ES, JweEncryption.A256GCM, ) @@ -101,9 +107,9 @@ class MessageWrapper( suspend fun createSignedJwt(jwm: JsonWebMessage): String? { return jwsService.createSignedJwt( - JwsContentTypeConstants.DIDCOMM_SIGNED_JSON, + JwsContentType.DIDCOMM_SIGNED_JSON, jwm.serialize().encodeToByteArray(), - JwsContentTypeConstants.DIDCOMM_PLAIN_JSON + JwsContentType.DIDCOMM_PLAIN_JSON ) } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/OidcSiopProtocol.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/OidcSiopProtocol.kt index 1c251e443..6e701a96a 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/OidcSiopProtocol.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/OidcSiopProtocol.kt @@ -30,6 +30,8 @@ import at.asitplus.wallet.lib.oidc.JsonWebKeySet import at.asitplus.wallet.lib.oidc.RelyingPartyMetadata import com.benasher44.uuid.uuid4 import io.github.aakira.napier.Napier +import io.ktor.http.URLBuilder +import io.ktor.http.Url import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit @@ -110,80 +112,84 @@ class OidcSiopProtocol( jwtVp = FormatContainerJwt(algorithms = arrayOf("ES256")), ), ) - return AuthenticationRequest( - url = walletUrl, - params = AuthenticationRequestParameters( - responseType = "id_token vp_token", - clientId = relyingPartyUrl, - redirectUri = relyingPartyUrl, - scope = "openid profile", - state = stateOfRelyingParty, - nonce = relyingPartyChallenge, - clientMetadata = metadata, - idTokenType = IdTokenType.ATTESTER_SIGNED, - presentationDefinition = PresentationDefinition( - id = uuid4().toString(), - formats = FormatHolder( - jwtVp = FormatContainerJwt(algorithms = arrayOf("ES256")) - ), - inputDescriptors = arrayOf( - InputDescriptor( - id = uuid4().toString(), - format = FormatHolder( - jwtVp = FormatContainerJwt(algorithms = arrayOf("ES256")) - ), - schema = arrayOf(SchemaReference("https://example.com")), - constraints = Constraint( - fields = arrayOf( - ConstraintField( - path = arrayOf("$.type"), - filter = ConstraintFilter( - type = "string", - pattern = "IDCardCredential", - ) + val authenticationRequestParameters = AuthenticationRequestParameters( + responseType = "id_token vp_token", + clientId = relyingPartyUrl, + redirectUri = relyingPartyUrl, + scope = "openid profile", + state = stateOfRelyingParty, + nonce = relyingPartyChallenge, + clientMetadata = metadata, + idTokenType = IdTokenType.ATTESTER_SIGNED, + presentationDefinition = PresentationDefinition( + id = uuid4().toString(), + formats = FormatHolder( + jwtVp = FormatContainerJwt(algorithms = arrayOf("ES256")) + ), + inputDescriptors = arrayOf( + InputDescriptor( + id = uuid4().toString(), + format = FormatHolder( + jwtVp = FormatContainerJwt(algorithms = arrayOf("ES256")) + ), + schema = arrayOf(SchemaReference("https://example.com")), + constraints = Constraint( + fields = arrayOf( + ConstraintField( + path = arrayOf("$.type"), + filter = ConstraintFilter( + type = "string", + pattern = "IDCardCredential", ) - ), + ) ), - ) - ), + ), + ) ), ), - ).toUrl() + ) + val urlBuilder = URLBuilder(walletUrl) + authenticationRequestParameters.encodeToParameters() + .forEach { urlBuilder.parameters.append(it.key, it.value) } + return urlBuilder.buildString() } /** * Pass in the serialized [AuthenticationRequest] to create an [AuthenticationResponse] */ suspend fun createAuthnResponse(it: String): String? { - val request = AuthenticationRequest.parseUrl(it) - ?: return null - .also { Napier.w("Could not parse authentication request") } // TODO could also contain "request_uri" // TODO could also contain "response_mode=post" - stateOfRelyingParty = request.params.state - val audience = request.params.clientMetadata?.jsonWebKeySet?.keys?.get(0)?.getIdentifier() + val params = kotlin.runCatching { + val parsedUrl = Url(it) + parsedUrl.encodedQuery.decodeFromUrlQuery() + }.getOrNull() + ?: return null + .also { Napier.w("Could not parse authentication request") } + stateOfRelyingParty = params.state + val audience = params.clientMetadata?.jsonWebKeySet?.keys?.get(0)?.keyId ?: return null .also { Napier.w("Could not parse audience") } - if ("urn:ietf:params:oauth:jwk-thumbprint" !in request.params.clientMetadata.subjectSyntaxTypesSupported) + if ("urn:ietf:params:oauth:jwk-thumbprint" !in params.clientMetadata.subjectSyntaxTypesSupported) return null .also { Napier.w("Incompatible subject syntax types algorithms") } - if (request.params.clientId != request.params.redirectUri) + if (params.clientId != params.redirectUri) return null .also { Napier.w("client_id does not match redirect_uri") } - if ("id_token" !in request.params.responseType) + if ("id_token" !in params.responseType) return null .also { Napier.w("response_type is not \"id_token\"") } // TODO "claims" may be set by the RP to tell OP which attributes to release - if ("vp_token" !in request.params.responseType && request.params.presentationDefinition == null) + if ("vp_token" !in params.responseType && params.presentationDefinition == null) return null .also { Napier.w("vp_token not requested") } - if (request.params.clientMetadata.vpFormats == null) + if (params.clientMetadata.vpFormats == null) return null .also { Napier.w("Incompatible subject syntax types algorithms") } - if (request.params.clientMetadata.vpFormats.jwtVp?.algorithms?.contains("ES256") != true) + if (params.clientMetadata.vpFormats.jwtVp?.algorithms?.contains("ES256") != true) return null .also { Napier.w("Incompatible JWT algorithms") } - val vp = holder?.createPresentation(request.params.nonce, audience) + val vp = holder?.createPresentation(params.nonce, audience) ?: return null .also { Napier.w("Could not create presentation") } if (vp !is Holder.CreatePresentationResult.Signed) @@ -195,10 +201,10 @@ class OidcSiopProtocol( issuer = agentPublicKey.toJwkThumbprint(), subject = agentPublicKey.toJwkThumbprint(), subjectJwk = agentPublicKey, - audience = request.params.redirectUri, + audience = params.redirectUri, issuedAt = now, expiration = now + 60.seconds, - nonce = request.params.nonce, + nonce = params.nonce, ) val jwsPayload = idToken.serialize().encodeToByteArray() val jwsHeader = JwsHeader(JwsAlgorithm.ES256) @@ -207,8 +213,8 @@ class OidcSiopProtocol( .also { Napier.w("Could not sign id_token") } val presentationSubmission = PresentationSubmission( id = uuid4().toString(), - definitionId = request.params.presentationDefinition?.id ?: uuid4().toString(), - descriptorMap = request.params.presentationDefinition?.inputDescriptors?.map { + definitionId = params.presentationDefinition?.id ?: uuid4().toString(), + descriptorMap = params.presentationDefinition?.inputDescriptors?.map { PresentationSubmissionDescriptor( id = it.id, format = ClaimFormatEnum.JWT_VP, @@ -221,15 +227,16 @@ class OidcSiopProtocol( ) }?.toTypedArray() ) - return AuthenticationResponse( - url = request.params.redirectUri, - params = AuthenticationResponseParameters( - idToken = signedIdToken, - state = request.params.state, - vpToken = vp.jws, - presentationSubmission = presentationSubmission, - ) - ).toUrl() + val authenticationResponseParameters = AuthenticationResponseParameters( + idToken = signedIdToken, + state = params.state, + vpToken = vp.jws, + presentationSubmission = presentationSubmission, + ) + val urlBuilder = URLBuilder(params.redirectUri) + authenticationResponseParameters.encodeToParameters() + .forEach { urlBuilder.parameters.append(it.key, it.value) } + return urlBuilder.buildString() } sealed class AuthnResponseResult { @@ -241,10 +248,13 @@ class OidcSiopProtocol( * Validates the [AuthenticationResponse] from the wallet */ fun validateAuthnResponse(it: String): AuthnResponseResult { - val response = AuthenticationResponse.parseUrl(it) + val params = kotlin.runCatching { + val parsedUrl = Url(it) + parsedUrl.encodedQuery.decodeFromUrlQuery() + }.getOrNull() ?: return AuthnResponseResult.Error("url") - .also { Napier.w("Could not parse authentication response: $it") } - val idTokenJws = response.params.idToken + .also { Napier.w("Could not parse authentication response") } + val idTokenJws = params.idToken val jwsSigned = JwsSigned.parse(idTokenJws) ?: return AuthnResponseResult.Error("idToken") .also { Napier.w("Could not parse JWS from idToken: $idTokenJws") } @@ -275,7 +285,7 @@ class OidcSiopProtocol( if (idToken.subject != idToken.subjectJwk.toJwkThumbprint()) return AuthnResponseResult.Error("sub") .also { Napier.d("subject does not equal thumbprint of sub_jwk: ${idToken.subject}") } - val vp = response.params.vpToken + val vp = params.vpToken ?: return AuthnResponseResult.Error("vpToken is null") .also { Napier.w("No VP in response") } val verificationResult = verifier?.verifyPresentation(vp, relyingPartyChallenge) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Parser.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Parser.kt index b9fe200f7..a9571a9e6 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Parser.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Parser.kt @@ -29,35 +29,36 @@ class Parser( * * @param it the JWS enclosing the VP, in compact representation * @param challenge the nonce sent from the verifier to the holder creating the VP - * @param localIdentifier the identifier (e.g. `keyId`) of the verifier that has requested the VP from the holder + * @param localKeyId the keyId of the verifier that has requested the VP from the holder */ - fun parseVpJws(it: String, challenge: String, localIdentifier: String): ParseVpResult { + fun parseVpJws(it: String, challenge: String, localKeyId: String): ParseVpResult { Napier.d("Parsing VP $it") val jws = JwsSigned.parse(it) ?: return ParseVpResult.InvalidStructure(it) .also { Napier.w("Could not parse JWS") } val payload = jws.payload.decodeToString() - val kid = jws.header.keyId + val kid = jws.header.keyId?: return ParseVpResult.InvalidStructure(it) + .also { Napier.d("no kid in header") } val vpJws = kotlin.runCatching { VerifiablePresentationJws.deserialize(payload) }.getOrNull() ?: return ParseVpResult.InvalidStructure(it) .also { Napier.w("Could not parse payload") } - return parseVpJws(it, vpJws, kid, challenge, localIdentifier) + return parseVpJws(it, vpJws, kid, challenge, localKeyId) } fun parseVpJws( it: String, vpJws: VerifiablePresentationJws, - kid: String? = null, + kid: String, challenge: String, - localIdentifier: String + localKeyId: String ): ParseVpResult { if (vpJws.challenge != challenge) return ParseVpResult.InvalidStructure(it) .also { Napier.d("nonce invalid") } - if (vpJws.audience != localIdentifier) + if (vpJws.audience != localKeyId) return ParseVpResult.InvalidStructure(it) .also { Napier.d("aud invalid") } - if (kid != null && vpJws.issuer != kid) + if (vpJws.issuer != kid) return ParseVpResult.InvalidStructure(it) .also { Napier.d("iss invalid") } if (vpJws.jwtId != vpJws.vp.id) @@ -75,8 +76,8 @@ class Parser( * * @param it the JWS enclosing the VC, in compact representation */ - fun parseVcJws(it: String, vcJws: VerifiableCredentialJws, kid: String? = null): ParseVcResult { - if (kid != null && vcJws.issuer != kid) + fun parseVcJws(it: String, vcJws: VerifiableCredentialJws, kid: String): ParseVcResult { + if (vcJws.issuer != kid) return ParseVcResult.InvalidStructure(it) .also { Napier.d("iss invalid") } if (vcJws.issuer != vcJws.vc.issuer) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/PresentProofMessenger.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/PresentProofMessenger.kt index f5e56051b..d436a5e12 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/PresentProofMessenger.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/PresentProofMessenger.kt @@ -7,6 +7,7 @@ import com.benasher44.uuid.uuid4 class PresentProofMessenger private constructor( private val holder: Holder? = null, private val verifier: Verifier? = null, + private val keyId: String, messageWrapper: MessageWrapper, private val serviceEndpoint: String? = null, private val challengeForPresentation: String = uuid4().toString(), @@ -26,6 +27,7 @@ class PresentProofMessenger private constructor( holder = holder, requestedAttributeNames = requestedAttributeNames, credentialScheme = credentialScheme, + keyId = keyId, serviceEndpoint = serviceEndpoint, challengeForPresentation = challengeForPresentation, ) @@ -37,11 +39,13 @@ class PresentProofMessenger private constructor( */ fun newHolderInstance( holder: Holder, + keyId: String, messageWrapper: MessageWrapper, serviceEndpoint: String, credentialScheme: ConstantIndex.CredentialScheme = ConstantIndex.Generic, ) = PresentProofMessenger( holder = holder, + keyId = keyId, messageWrapper = messageWrapper, serviceEndpoint = serviceEndpoint, credentialScheme = credentialScheme, @@ -53,12 +57,14 @@ class PresentProofMessenger private constructor( */ fun newVerifierInstance( verifier: Verifier, + keyId: String, messageWrapper: MessageWrapper, credentialScheme: ConstantIndex.CredentialScheme = ConstantIndex.Generic, requestedAttributeNames: List? = null, challengeForPresentation: String = uuid4().toString() ) = PresentProofMessenger( verifier = verifier, + keyId = keyId, messageWrapper = messageWrapper, requestedAttributeNames = requestedAttributeNames, credentialScheme = credentialScheme, diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/PresentProofProtocol.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/PresentProofProtocol.kt index 8af6073c0..d29c4072d 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/PresentProofProtocol.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/PresentProofProtocol.kt @@ -1,7 +1,10 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.data.ConstantIndex +import at.asitplus.wallet.lib.msg.RequestPresentationAttachment +import at.asitplus.wallet.lib.msg.RequestPresentationAttachmentOptions import at.asitplus.wallet.lib.data.SchemaIndex +import at.asitplus.wallet.lib.msg.SchemaReference import at.asitplus.wallet.lib.data.dif.Constraint import at.asitplus.wallet.lib.data.dif.ConstraintField import at.asitplus.wallet.lib.data.dif.ConstraintFilter @@ -10,7 +13,6 @@ import at.asitplus.wallet.lib.data.dif.FormatHolder import at.asitplus.wallet.lib.data.dif.InputDescriptor import at.asitplus.wallet.lib.data.dif.PresentationDefinition import at.asitplus.wallet.lib.data.jsonSerializer -import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.msg.AttachmentFormatReference import at.asitplus.wallet.lib.msg.JsonWebMessage import at.asitplus.wallet.lib.msg.JwmAttachment @@ -20,10 +22,7 @@ import at.asitplus.wallet.lib.msg.OutOfBandService import at.asitplus.wallet.lib.msg.Presentation import at.asitplus.wallet.lib.msg.PresentationBody import at.asitplus.wallet.lib.msg.RequestPresentation -import at.asitplus.wallet.lib.msg.RequestPresentationAttachment -import at.asitplus.wallet.lib.msg.RequestPresentationAttachmentOptions import at.asitplus.wallet.lib.msg.RequestPresentationBody -import at.asitplus.wallet.lib.msg.SchemaReference import com.benasher44.uuid.uuid4 import io.github.aakira.napier.Napier import kotlinx.serialization.encodeToString @@ -49,6 +48,7 @@ class PresentProofProtocol( private val verifier: Verifier? = null, private val requestedAttributeNames: List? = null, private val credentialScheme: ConstantIndex.CredentialScheme, + private val keyId: String, private val serviceEndpoint: String?, private val challengeForPresentation: String, ) : ProtocolStateMachine { @@ -60,11 +60,13 @@ class PresentProofProtocol( */ fun newHolderInstance( holder: Holder, + keyId: String, serviceEndpoint: String, credentialScheme: ConstantIndex.CredentialScheme = ConstantIndex.Generic, ) = PresentProofProtocol( holder = holder, credentialScheme = credentialScheme, + keyId = keyId, serviceEndpoint = serviceEndpoint, challengeForPresentation = uuid4().toString(), ) @@ -75,6 +77,7 @@ class PresentProofProtocol( */ fun newVerifierInstance( verifier: Verifier, + keyId: String, serviceEndpoint: String? = null, credentialScheme: ConstantIndex.CredentialScheme = ConstantIndex.Generic, requestedAttributeNames: List? = null, @@ -82,6 +85,7 @@ class PresentProofProtocol( verifier = verifier, requestedAttributeNames = requestedAttributeNames, credentialScheme = credentialScheme, + keyId = keyId, serviceEndpoint = serviceEndpoint, challengeForPresentation = uuid4().toString() ) @@ -116,17 +120,19 @@ class PresentProofProtocol( return createRequestPresentation() } - override suspend fun parseMessage(body: JsonWebMessage, senderKey: JsonWebKey): InternalNextMessage { + override suspend fun parseMessage( + body: JsonWebMessage, + senderKeyId: String + ): InternalNextMessage { when (this.state) { State.START -> { if (body is OutOfBandInvitation) - return createRequestPresentation(body, senderKey) + return createRequestPresentation(body, senderKeyId) if (body is RequestPresentation) - return createPresentation(body, senderKey) + return createPresentation(body, senderKeyId) return InternalNextMessage.IncorrectState("messageType") .also { Napier.w("Unexpected messageType: ${body.type}") } } - State.INVITATION_SENT -> { if (body !is RequestPresentation) return InternalNextMessage.IncorrectState("messageType") @@ -134,9 +140,8 @@ class PresentProofProtocol( if (body.parentThreadId != invitationId) return InternalNextMessage.IncorrectState("parentThreadId") .also { Napier.w("Unexpected parentThreadId: ${body.parentThreadId}") } - return createPresentation(body, senderKey) + return createPresentation(body, senderKeyId) } - State.REQUEST_PRESENTATION_SENT -> { if (body !is Presentation) return InternalNextMessage.IncorrectState("messageType") @@ -146,15 +151,12 @@ class PresentProofProtocol( .also { Napier.w("Unexpected threadId: ${body.threadId}") } return verifyPresentation(body) } - else -> return InternalNextMessage.IncorrectState("state") .also { Napier.w("Unexpected state: $state") } } } private fun createOobInvitation(): InternalNextMessage { - val recipientKey = holder?.identifier - ?: return InternalNextMessage.IncorrectState("holder") val message = OutOfBandInvitation( body = OutOfBandInvitationBody( handshakeProtocols = arrayOf(SchemaIndex.PROT_PRESENT_PROOF), @@ -163,7 +165,7 @@ class PresentProofProtocol( services = arrayOf( OutOfBandService( type = "did-communication", - recipientKeys = arrayOf(recipientKey), + recipientKeys = arrayOf(keyId), serviceEndpoint = serviceEndpoint ?: "https://example.com", ) ), @@ -176,21 +178,22 @@ class PresentProofProtocol( private fun createRequestPresentation(): InternalNextMessage { val message = buildRequestPresentationMessage(credentialScheme, null) - ?: return InternalNextMessage.IncorrectState("verifier") return InternalNextMessage.SendAndWrap(message) .also { this.threadId = message.threadId } .also { this.state = State.REQUEST_PRESENTATION_SENT } } - private fun createRequestPresentation(invitation: OutOfBandInvitation, senderKey: JsonWebKey): InternalNextMessage { + private fun createRequestPresentation( + invitation: OutOfBandInvitation, + senderKeyId: String + ): InternalNextMessage { val credentialScheme = ConstantIndex.Parser.parseGoalCode(invitation.body.goalCode) ?: return problemReporter.problemLastMessage(invitation.threadId, "goal-code-unknown") val message = buildRequestPresentationMessage(credentialScheme, invitation.id) - ?: return InternalNextMessage.IncorrectState("verifier") val serviceEndpoint = invitation.body.services?.let { if (it.isNotEmpty()) it[0].serviceEndpoint else null } - return InternalNextMessage.SendAndWrap(message, senderKey, serviceEndpoint) + return InternalNextMessage.SendAndWrap(message, senderKeyId, serviceEndpoint) .also { this.threadId = message.threadId } .also { this.state = State.REQUEST_PRESENTATION_SENT } } @@ -198,9 +201,7 @@ class PresentProofProtocol( private fun buildRequestPresentationMessage( credentialScheme: ConstantIndex.CredentialScheme, parentThreadId: String? = null, - ): RequestPresentation? { - val verifierIdentifier = verifier?.identifier - ?: return null + ): RequestPresentation { val constraintsNames = requestedAttributeNames?.map(this::buildConstraintFieldForName) ?: listOf() val constraintsTypes = buildConstraintFieldForType(credentialScheme.vcType) @@ -222,10 +223,11 @@ class PresentProofProtocol( presentationDefinition = presentationDefinition, options = RequestPresentationAttachmentOptions( challenge = challengeForPresentation, - verifier = verifierIdentifier, + verifier = keyId ) ) - val attachment = JwmAttachment.encodeBase64(jsonSerializer.encodeToString(requestPresentation)) + val attachment = + JwmAttachment.encodeBase64(jsonSerializer.encodeToString(requestPresentation)) return RequestPresentation( body = RequestPresentationBody( comment = "Please show your credentials", @@ -251,7 +253,10 @@ class PresentProofProtocol( filter = ConstraintFilter(type = "string", const = attributeType) ) - private suspend fun createPresentation(lastMessage: RequestPresentation, senderKey: JsonWebKey): InternalNextMessage { + private suspend fun createPresentation( + lastMessage: RequestPresentation, + senderKeyId: String + ): InternalNextMessage { val attachments = lastMessage.attachments ?: return problemReporter.problemLastMessage( lastMessage.threadId, @@ -277,7 +282,7 @@ class PresentProofProtocol( .mapNotNull { it.const } val vp = holder?.createPresentation( requestPresentationAttachment.options.challenge, - requestPresentationAttachment.options.verifier ?: senderKey.getIdentifier(), + requestPresentationAttachment.options.verifier ?: senderKeyId, attributeTypes = requestedTypes.ifEmpty { null }, attributeNames = requestedFields.ifEmpty { null } ) ?: return problemReporter.problemInternal(lastMessage.threadId, "vp-empty") @@ -298,7 +303,7 @@ class PresentProofProtocol( threadId = lastMessage.threadId!!, attachment = attachment ) - return InternalNextMessage.SendAndWrap(message, senderKey) + return InternalNextMessage.SendAndWrap(message, senderKeyId) .also { this.threadId = message.threadId } .also { this.state = State.FINISHED } } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ProtocolMessenger.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ProtocolMessenger.kt index df81f1f1c..bd7e5b26c 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ProtocolMessenger.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ProtocolMessenger.kt @@ -97,8 +97,8 @@ abstract class ProtocolMessenger, U>( protocolRunManager.getActiveRuns().forEach { protocol -> when (val next = protocol.parseMessage( parsedMessage.body, - parsedMessage.senderKey ?: return NextMessage.Error("No sender key present") - .also { Napier.w("No sender key present") })) { + parsedMessage.senderKeyId ?: return NextMessage.Error("No Kid present") + .also { Napier.w("No kid present") })) { is InternalNextMessage.Finished -> return NextMessage.Result(protocol.getResult()) is InternalNextMessage.SendAndWrap -> return wrapNextMessage(next) is InternalNextMessage.SendProblemReport -> return wrapProblemReportMessage(next) @@ -112,8 +112,8 @@ abstract class ProtocolMessenger, U>( } private suspend fun wrapNextMessage(next: InternalNextMessage.SendAndWrap): NextMessage { - if (signAndEncryptFollowingMessages && next.senderKey != null) { - val signedAndEncryptedJwe = messageWrapper.createSignedAndEncryptedJwe(next.message, next.senderKey) + if (signAndEncryptFollowingMessages && next.senderKeyId != null) { + val signedAndEncryptedJwe = messageWrapper.createSignedAndEncryptedJwe(next.message, next.senderKeyId) ?: return NextMessage.SendProblemReport("Could not sign message", next.endpoint) return NextMessage.Send(signedAndEncryptedJwe, next.endpoint) } @@ -126,8 +126,8 @@ abstract class ProtocolMessenger, U>( } private suspend fun wrapProblemReportMessage(next: InternalNextMessage.SendProblemReport): NextMessage { - if (signAndEncryptFollowingMessages && next.senderKey != null) { - val signedAndEncryptedJwe = messageWrapper.createSignedAndEncryptedJwe(next.message, next.senderKey) + if (signAndEncryptFollowingMessages && next.senderKeyId != null) { + val signedAndEncryptedJwe = messageWrapper.createSignedAndEncryptedJwe(next.message, next.senderKeyId) ?: return NextMessage.SendProblemReport("Could not sign message", next.endpoint) return NextMessage.SendProblemReport(signedAndEncryptedJwe, next.endpoint) } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ProtocolStateMachine.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ProtocolStateMachine.kt index 572f39575..315f6ea87 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ProtocolStateMachine.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ProtocolStateMachine.kt @@ -1,6 +1,5 @@ package at.asitplus.wallet.lib.agent -import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.msg.JsonWebMessage @@ -15,7 +14,7 @@ interface ProtocolStateMachine { fun startDirect(): InternalNextMessage - suspend fun parseMessage(body: JsonWebMessage, senderKey: JsonWebKey): InternalNextMessage + suspend fun parseMessage(body: JsonWebMessage, senderKeyId: String): InternalNextMessage fun getResult(): T? diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ReceivedMessage.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ReceivedMessage.kt index 24f12efaf..6897eef01 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ReceivedMessage.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ReceivedMessage.kt @@ -1,13 +1,8 @@ package at.asitplus.wallet.lib.agent -import at.asitplus.wallet.lib.jws.JsonWebKey import at.asitplus.wallet.lib.msg.JsonWebMessage sealed class ReceivedMessage { - data class Success( - val body: JsonWebMessage, - val senderKey: JsonWebKey? = null, - ) : ReceivedMessage() - + data class Success(val body: JsonWebMessage, val senderKeyId: String?) : ReceivedMessage() object Error : ReceivedMessage() } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/SerializerSketch.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/SerializerSketch.kt new file mode 100644 index 000000000..07429d24d --- /dev/null +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/SerializerSketch.kt @@ -0,0 +1,63 @@ +package at.asitplus.wallet.lib.agent + +import io.ktor.http.decodeURLQueryComponent +import io.ktor.http.formUrlEncode +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.JsonUnquotedLiteral +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement + +typealias Parameters = Map + +@OptIn(ExperimentalSerializationApi::class) +inline fun Parameters.decode(): T = + json.decodeFromJsonElement(JsonObject(entries.associate { (k, v) -> + k to when (v[0]) { + '{' -> json.decodeFromString(v) + '[' -> json.decodeFromString(v) + else -> JsonUnquotedLiteral(v) //no quoted → can be any type for deserializing. requires lenient parsing + } + })) + +inline fun Parameters.decodeFromUrlQuery(): T = + entries.associate { (k, v) -> k.decodeURLQueryComponent() to v.decodeURLQueryComponent() }.decode() + +inline fun String.decodeFromPostBody(): T = split("&") + .associate { + it.substringBefore("=").decodeURLQueryComponent() to + it.substringAfter("=", "").decodeURLQueryComponent() + } + .decode() + +inline fun String.decodeFromUrlQuery(): T = split("&") + .associate { + it.substringBefore("=").decodeURLQueryComponent(plusIsSpace = true) to + it.substringAfter("=", "").decodeURLQueryComponent(plusIsSpace = true) + } + .decode() + +fun Parameters.formUrlEncode() = map { (k, v) -> k to v }.formUrlEncode() +inline fun T.encodeToParameters(): Parameters = + when (val tmp = json.encodeToJsonElement(this)) { + is JsonArray -> tmp.mapIndexed { i, v -> i.toString() to v } + is JsonObject -> tmp.map { (k, v) -> k to v } + else -> throw SerializationException("Literals are not supported") + }.associate { (k, v) -> k to if (v is JsonPrimitive) v.content else json.encodeToString(v) } + +@OptIn(ExperimentalSerializationApi::class) +val json by lazy { + Json { + prettyPrint = false + encodeDefaults = true + explicitNulls = false + ignoreUnknownKeys = true + isLenient = true + } +} \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt index 737371e58..9df995c0b 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Validator.kt @@ -58,7 +58,7 @@ class Validator( return false .also { Napier.w("Revocation List: Signature invalid") } val payload = jws.payload.decodeToString() - val kid = jws.header.keyId + val kid = jws.header.keyId ?: return false.also { Napier.d("no kid in header") } val vcJws = VerifiableCredentialJws.deserialize(payload) ?: return false .also { Napier.w("Revocation List: Could not parse payload") } @@ -133,7 +133,8 @@ class Validator( return Verifier.VerifyPresentationResult.InvalidStructure(it) .also { Napier.w("VP: Signature invalid") } val payload = jws.payload.decodeToString() - val kid = jws.header.keyId + val kid = jws.header.keyId ?: return Verifier.VerifyPresentationResult.InvalidStructure(it) + .also { Napier.d("no kid in header") } val vpJws = kotlin.runCatching { VerifiablePresentationJws.deserialize(payload) }.getOrNull() ?: return Verifier.VerifyPresentationResult.InvalidStructure(it) @@ -191,7 +192,8 @@ class Validator( if (checkRevocationStatus(vcJws) == RevocationStatus.REVOKED) return Verifier.VerifyCredentialResult.Revoked(it, vcJws) .also { Napier.d("VC: revoked") } - val kid = jws.header.keyId + val kid = jws.header.keyId ?: return Verifier.VerifyCredentialResult.InvalidStructure(it) + .also { Napier.d("VC: No kid in header") } return when (parser.parseVcJws(it, vcJws, kid)) { is Parser.ParseVcResult.InvalidStructure -> Verifier.VerifyCredentialResult.InvalidStructure(it) .also { Napier.d("VC: Invalid structure from Parser") } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Verifier.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Verifier.kt index 29f7a3a29..128767517 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Verifier.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Verifier.kt @@ -11,12 +11,6 @@ import at.asitplus.wallet.lib.data.VerifiablePresentationParsed */ interface Verifier { - /** - * The identifier for this agent, typically the `keyId` from the cryptographic key, - * e.g. `did:key:mAB...` or `urn:ietf:params:oauth:jwk-thumbprint:sha256:...` - */ - val identifier: String - /** * Set the revocation list to use for validating VCs (from [Issuer.issueRevocationListCredential]) */ diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifierAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifierAgent.kt index ce062c229..12a7bf21d 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifierAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/VerifierAgent.kt @@ -9,33 +9,25 @@ import at.asitplus.wallet.lib.data.VerifiablePresentationParsed */ class VerifierAgent private constructor( private val validator: Validator, - override val identifier: String + private val keyId: String ) : Verifier { companion object { fun newDefaultInstance( - identifier: String, + keyId: String, cryptoService: VerifierCryptoService = DefaultVerifierCryptoService(), validator: Validator = Validator.newDefaultInstance(cryptoService), ): VerifierAgent = VerifierAgent( validator = validator, - identifier = identifier + keyId = keyId ) /** * Explicitly short argument list to use it from Swift */ - fun newDefaultInstance(identifier: String): VerifierAgent = VerifierAgent( + fun newDefaultInstance(keyId: String): VerifierAgent = VerifierAgent( validator = Validator.newDefaultInstance(), - identifier = identifier - ) - - /** - * Creates a new verifier for a random `identifier` - */ - fun newRandomInstance(): VerifierAgent = VerifierAgent( - validator = Validator.newDefaultInstance(), - identifier = DefaultCryptoService().identifier, + keyId = keyId ) } @@ -47,7 +39,7 @@ class VerifierAgent private constructor( * Verifies a presentation of some credentials that a holder issued with that [challenge] we sent before. */ override fun verifyPresentation(it: String, challenge: String): Verifier.VerifyPresentationResult { - return validator.verifyVpJws(it, challenge, identifier) + return validator.verifyVpJws(it, challenge, keyId) } /** @@ -65,7 +57,7 @@ class VerifierAgent private constructor( } override fun verifyVcJws(it: String): Verifier.VerifyCredentialResult { - return validator.verifyVcJws(it, identifier) + return validator.verifyVcJws(it, keyId) } } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/AttributeIndex.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/AttributeIndex.kt index 10974d1c0..6d5b806db 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/AttributeIndex.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/AttributeIndex.kt @@ -4,14 +4,19 @@ object AttributeIndex { private val schemeSet = mutableSetOf() - init { - schemeSet += ConstantIndex.Generic - } - internal fun registerAttributeType(scheme: ConstantIndex.CredentialScheme) { schemeSet += scheme } + /** + * May return an empty list, if the Schema is not known, + * or it does not issue atomic credentials (see [getTypeOfAttributeForSchemaUri]) + */ + fun getListOfAttributesForSchemaUri(uri: String) = when (uri) { + SchemaIndex.CRED_GENERIC -> genericAttributes + else -> listOf() + } + /** * May return an empty list, if the Schema is not known */ diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/ConstantIndex.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/ConstantIndex.kt index bc73491a3..b10b740bd 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/ConstantIndex.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/ConstantIndex.kt @@ -20,8 +20,7 @@ object ConstantIndex { /** * Schema URL of the credential, used in [at.asitplus.wallet.lib.agent.IssueCredentialProtocol] to map - * from the requested schema to the internal attribute type used in [at.asitplus.wallet.lib.agent.Issuer] - * when issuing credentials. + * from the requested schema to the internal attribute type used in [at.asitplus.wallet.lib.agent.Issuer] when issuing credentials. */ val schemaUri: String diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentation.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentation.kt index 95de1cb88..670e34897 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentation.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/data/VerifiablePresentation.kt @@ -23,10 +23,10 @@ data class VerifiablePresentation( verifiableCredential = verifiableCredential ) - fun toJws(challenge: String, issuerId: String, audienceId: String) = VerifiablePresentationJws( + fun toJws(challenge: String, subjectId: String, audienceId: String) = VerifiablePresentationJws( vp = this, challenge = challenge, - issuer = issuerId, + issuer = subjectId, audience = audienceId, jwtId = id ) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JsonWebKey.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JsonWebKey.kt index 65710a00f..2b7ebfd3c 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JsonWebKey.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JsonWebKey.kt @@ -4,6 +4,7 @@ import at.asitplus.KmmResult import io.matthewnelson.component.base64.encodeBase64 import at.asitplus.wallet.lib.data.jsonSerializer import io.github.aakira.napier.Napier +import io.ktor.util.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString @@ -128,11 +129,6 @@ data class JsonWebKey( fun toJwkThumbprint() = Json.encodeToString(this).encodeToByteArray().toByteString().sha256().base64Url() - fun getIdentifier(): String { - return keyId - ?: "urn:ietf:params:oauth:jwk-thumbprint:sha256:${toJwkThumbprint()}" - } - override fun toString(): String { return "JsonWebKey(type=$type, curve=$curve, keyId=$keyId, x=${x?.encodeBase64()}, y=${y?.encodeBase64()})" } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweHeader.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweHeader.kt index 64640ee36..7362268c7 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweHeader.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JweHeader.kt @@ -1,6 +1,5 @@ package at.asitplus.wallet.lib.jws -import at.asitplus.wallet.lib.agent.CryptoUtils import at.asitplus.wallet.lib.data.jsonSerializer import io.github.aakira.napier.Napier import kotlinx.serialization.SerialName @@ -18,13 +17,11 @@ data class JweHeader( @SerialName("enc") val encryption: JweEncryption?, @SerialName("kid") - val keyId: String? = null, + val keyId: String, @SerialName("typ") - val type: String?, + val type: JwsContentType?, @SerialName("cty") - val contentType: String? = null, - @SerialName("jwk") - val jsonWebKey: JsonWebKey? = null, + val contentType: JwsContentType? = null, @SerialName("epk") val ephemeralKeyPair: JsonWebKey? = null, @SerialName("apu") @@ -35,48 +32,6 @@ data class JweHeader( val agreementPartyVInfo: ByteArray? = null, ) { fun serialize() = jsonSerializer.encodeToString(this) - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as JweHeader - - if (algorithm != other.algorithm) return false - if (encryption != other.encryption) return false - if (keyId != other.keyId) return false - if (type != other.type) return false - if (contentType != other.contentType) return false - if (jsonWebKey != other.jsonWebKey) return false - if (ephemeralKeyPair != other.ephemeralKeyPair) return false - if (agreementPartyUInfo != null) { - if (other.agreementPartyUInfo == null) return false - if (!agreementPartyUInfo.contentEquals(other.agreementPartyUInfo)) return false - } else if (other.agreementPartyUInfo != null) return false - if (agreementPartyVInfo != null) { - if (other.agreementPartyVInfo == null) return false - if (!agreementPartyVInfo.contentEquals(other.agreementPartyVInfo)) return false - } else if (other.agreementPartyVInfo != null) return false - - return true - } - - override fun hashCode(): Int { - var result = algorithm?.hashCode() ?: 0 - result = 31 * result + (encryption?.hashCode() ?: 0) - result = 31 * result + (keyId?.hashCode() ?: 0) - result = 31 * result + (type?.hashCode() ?: 0) - result = 31 * result + (contentType?.hashCode() ?: 0) - result = 31 * result + (jsonWebKey?.hashCode() ?: 0) - result = 31 * result + (ephemeralKeyPair?.hashCode() ?: 0) - result = 31 * result + (agreementPartyUInfo?.contentHashCode() ?: 0) - result = 31 * result + (agreementPartyVInfo?.contentHashCode() ?: 0) - return result - } - - fun getKey(): JsonWebKey? { - return jsonWebKey - ?: keyId?.let { JsonWebKey.fromKeyId(it) } - } companion object { fun deserialize(it: String) = kotlin.runCatching { diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsContentType.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsContentType.kt new file mode 100644 index 000000000..8e73d8283 --- /dev/null +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsContentType.kt @@ -0,0 +1,13 @@ +package at.asitplus.wallet.lib.jws + +import kotlinx.serialization.Serializable + +@Serializable(with = JwsContentTypeSerializer::class) +enum class JwsContentType(val text: String) { + + DIDCOMM_PLAIN_JSON("didcomm-plain+json"), + DIDCOMM_SIGNED_JSON("didcomm-signed+json"), + DIDCOMM_ENCRYPTED_JSON("didcomm-encrypted+json"), + JWT("JWT"); + +} \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsContentTypeSerializer.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsContentTypeSerializer.kt new file mode 100644 index 000000000..c09330367 --- /dev/null +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsContentTypeSerializer.kt @@ -0,0 +1,24 @@ +package at.asitplus.wallet.lib.jws + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object JwsContentTypeSerializer : KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("JwsContentTypeSerializer", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: JwsContentType?) { + value?.let { encoder.encodeString(it.text) } + } + + override fun deserialize(decoder: Decoder): JwsContentType? { + val decoded = decoder.decodeString() + return JwsContentType.values().firstOrNull { it.text == decoded } + } + +} \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsHeader.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsHeader.kt index b1264b5e2..2b68390f0 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsHeader.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsHeader.kt @@ -2,11 +2,8 @@ package at.asitplus.wallet.lib.jws -import at.asitplus.wallet.lib.agent.CryptoUtils -import at.asitplus.wallet.lib.data.InstantLongSerializer import at.asitplus.wallet.lib.data.jsonSerializer import io.github.aakira.napier.Napier -import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers @@ -23,20 +20,15 @@ data class JwsHeader( @SerialName("kid") val keyId: String? = null, @SerialName("typ") - val type: String? = null, + val type: JwsContentType? = null, @SerialName("cty") - val contentType: String? = null, + val contentType: JwsContentType? = null, @SerialName("x5c") val certificateChain: Array? = null, @SerialName("nbf") - @Serializable(with = InstantLongSerializer::class) - val notBefore: Instant? = null, - @SerialName("iat") - @Serializable(with = InstantLongSerializer::class) - val issuedAt: Instant? = null, + val notBefore: Long? = null, @SerialName("exp") - @Serializable(with = InstantLongSerializer::class) - val expiration: Instant? = null, + val expires: Long? = null, @SerialName("jwk") val jsonWebKey: JsonWebKey? = null ) { @@ -58,9 +50,10 @@ data class JwsHeader( if (!certificateChain.contentDeepEquals(other.certificateChain)) return false } else if (other.certificateChain != null) return false if (notBefore != other.notBefore) return false - if (issuedAt != other.issuedAt) return false - if (expiration != other.expiration) return false - return jsonWebKey == other.jsonWebKey + if (expires != other.expires) return false + if (jsonWebKey != other.jsonWebKey) return false + + return true } override fun hashCode(): Int { @@ -70,18 +63,11 @@ data class JwsHeader( result = 31 * result + (contentType?.hashCode() ?: 0) result = 31 * result + (certificateChain?.contentDeepHashCode() ?: 0) result = 31 * result + (notBefore?.hashCode() ?: 0) - result = 31 * result + (issuedAt?.hashCode() ?: 0) - result = 31 * result + (expiration?.hashCode() ?: 0) + result = 31 * result + (expires?.hashCode() ?: 0) result = 31 * result + (jsonWebKey?.hashCode() ?: 0) return result } - fun getKey(): JsonWebKey? { - return jsonWebKey - ?: keyId?.let { JsonWebKey.fromKeyId(it) } - ?: certificateChain?.firstOrNull()?.let { CryptoUtils.extractPublicKeyFromX509Cert(it) } - } - companion object { fun deserialize(it: String) = kotlin.runCatching { jsonSerializer.decodeFromString(it) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt index 889a48f56..2bee2c418 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/jws/JwsService.kt @@ -19,29 +19,23 @@ import kotlin.random.Random interface JwsService { suspend fun createSignedJwt( - type: String, + type: JwsContentType, payload: ByteArray, - contentType: String? = null + contentType: JwsContentType? = null ): String? suspend fun createSignedJws(header: JwsHeader, payload: ByteArray): String? /** - * Appends correct values for [JweHeader.keyId], [JwsHeader.algorithm] and [JwsHeader.jsonWebKey], - * if the corresponding options are set + * Appends correct values for [JweHeader.keyId], [JwsHeader.algorithm] and [JwsHeader.jsonWebKey] */ - suspend fun createSignedJwsAddingParams( - header: JwsHeader, - payload: ByteArray, - addKeyId: Boolean = true, - addJsonWebKey: Boolean = true - ): String? + suspend fun createSignedJwsAddingParams(header: JwsHeader, payload: ByteArray): String? fun encryptJweObject( - type: String, + type: JwsContentType, payload: ByteArray, recipientKey: JsonWebKey, - contentType: String? = null, + contentType: JwsContentType? = null, jweAlgorithm: JweAlgorithm, jweEncryption: JweEncryption ): String? @@ -61,24 +55,17 @@ interface VerifierJwsService { class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { override suspend fun createSignedJwt( - type: String, + type: JwsContentType, payload: ByteArray, - contentType: String? + contentType: JwsContentType? ): String? { - val jwsHeader = JwsHeader( - algorithm = cryptoService.jwsAlgorithm, - keyId = cryptoService.toJsonWebKey().keyId, - type = type, - contentType = contentType - ) + val jwsHeader = + JwsHeader(cryptoService.jwsAlgorithm, cryptoService.keyId, type, contentType) return createSignedJws(jwsHeader, payload) } override suspend fun createSignedJws(header: JwsHeader, payload: ByteArray): String? { - if (header.algorithm != cryptoService.jwsAlgorithm - || header.keyId?.let { it != cryptoService.toJsonWebKey().keyId } == true - || header.jsonWebKey?.let { it != cryptoService.toJsonWebKey() } == true - ) { + if (header.algorithm != cryptoService.jwsAlgorithm || header.keyId?.let { it != cryptoService.keyId } == true) { return null.also { Napier.w("Algorithm or keyId not matching to cryptoService") } } val signatureInput = header.serialize().encodeToByteArray() @@ -92,17 +79,12 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { return JwsSigned(header, payload, rawSignature, signatureInput).serialize() } - override suspend fun createSignedJwsAddingParams( - header: JwsHeader, - payload: ByteArray, - addKeyId: Boolean, - addJsonWebKey: Boolean - ): String? { - var copy = header.copy(algorithm = cryptoService.jwsAlgorithm) - if (addKeyId) - copy = copy.copy(keyId = cryptoService.toJsonWebKey().keyId) - if (addJsonWebKey) - copy = copy.copy(jsonWebKey = cryptoService.toJsonWebKey()) + override suspend fun createSignedJwsAddingParams(header: JwsHeader, payload: ByteArray): String? { + val copy = header.copy( + algorithm = cryptoService.jwsAlgorithm, + keyId = cryptoService.keyId, + jsonWebKey = cryptoService.toJsonWebKey() + ) return createSignedJws(copy, payload) } @@ -119,12 +101,13 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { Napier.w("No Z value from native code", it) return null } - val kdfInput = prependWithAdditionalInfo( - z, - header.encryption, - header.agreementPartyUInfo, - header.agreementPartyVInfo - ) + val kdfInput = + prependWithAdditionalInfo( + z, + header.encryption, + header.agreementPartyUInfo, + header.agreementPartyVInfo + ) val key = cryptoService.messageDigest(kdfInput, Digest.SHA256).getOrElse { Napier.w("No digest from native code", it) return null @@ -142,10 +125,10 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { } override fun encryptJweObject( - type: String, + type: JwsContentType, payload: ByteArray, recipientKey: JsonWebKey, - contentType: String?, + contentType: JwsContentType?, jweAlgorithm: JweAlgorithm, jweEncryption: JweEncryption ): String? { @@ -159,7 +142,7 @@ class DefaultJwsService(private val cryptoService: CryptoService) : JwsService { val jweHeader = JweHeader( algorithm = jweAlgorithm, encryption = jweEncryption, - jsonWebKey = cryptoService.toJsonWebKey(), + keyId = cryptoService.keyId, type = type, contentType = contentType, ephemeralKeyPair = ephemeralKeyPair.toPublicJsonWebKey() @@ -213,7 +196,9 @@ class DefaultVerifierJwsService( */ override fun verifyJwsObject(jwsObject: JwsSigned, serialized: String?): Boolean { val header = jwsObject.header - val publicKey = header.getKey() + val publicKey = header.keyId?.let { JsonWebKey.fromKeyId(it) } + ?: header.jsonWebKey + ?: header.certificateChain?.first()?.let { cryptoService.extractPublicKeyFromX509Cert(it) } ?: return false .also { Napier.w("Could not extract PublicKey from header: $header") } val verified = cryptoService.verify( diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt index 0455654a7..a191bc52b 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/JwmAttachment.kt @@ -5,6 +5,7 @@ import io.matthewnelson.component.base64.decodeBase64ToArray import io.matthewnelson.component.base64.encodeBase64 import com.benasher44.uuid.uuid4 import io.github.aakira.napier.Napier +import io.ktor.http.content.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequest.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequest.kt index 86a4b28de..3239ba97a 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequest.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequest.kt @@ -1,8 +1,5 @@ package at.asitplus.wallet.lib.oidc -import io.ktor.http.* -import io.ktor.util.* - /** * Container for a OIDC Authentication Request */ @@ -11,25 +8,4 @@ data class AuthenticationRequest( val params: AuthenticationRequestParameters, ) { - fun toUrl(): String { - val urlBuilder = URLBuilder(url) - params.serialize() - .forEach { (k, v) -> urlBuilder.parameters[k] = v.toString() } - return urlBuilder.buildString() - } - - companion object { - fun parseUrl(it: String): AuthenticationRequest? { - val url = kotlin.runCatching { Url(it) }.getOrNull() - ?: return null - val params = AuthenticationRequestParameters.deserialize( - url.parameters.flattenEntries().toMap() - ) ?: return null - return AuthenticationRequest( - url = "${url.protocol}://${url.host}${url.encodedPath}", - params = params - ) - } - } - } \ No newline at end of file diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponse.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponse.kt index cfc317a11..547483d4e 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponse.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponse.kt @@ -1,7 +1,5 @@ package at.asitplus.wallet.lib.oidc -import io.ktor.http.* - /** * Container for an OIDC Authentication Response */ @@ -9,31 +7,4 @@ data class AuthenticationResponse( val url: String, val params: AuthenticationResponseParameters, ) { - - fun toUrl(): String { - val urlBuilder = URLBuilder(url) - val parameters = Parameters.build { - params.serialize().forEach { (k, v) -> this[k] = v.toString() } - } - urlBuilder.encodedFragment = parameters.formUrlEncode() - return urlBuilder.buildString() - } - - companion object { - fun parseUrl(it: String): AuthenticationResponse? { - val url = kotlin.runCatching { Url(it) }.getOrNull() - ?: return null - val params = AuthenticationResponseParameters.deserialize( - url.fragment.split("&") - .associate { param -> - val split = param.split("=") - split[0] to param.replace(split[0] + "=", "") - } - ) ?: return null - return AuthenticationResponse( - url = "${url.protocol}://${url.host}${url.encodedPath}", - params = params - ) - } - } } \ No newline at end of file diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/TestUtils.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/TestUtils.kt new file mode 100644 index 000000000..8bd0641fc --- /dev/null +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/TestUtils.kt @@ -0,0 +1,8 @@ +package at.asitplus.wallet.lib + +import io.kotest.common.Platform +import io.kotest.common.platform +import io.kotest.core.spec.style.scopes.ContainerScope + +fun ContainerScope.nameHack(it: T) = + if (platform == Platform.JVM) testCase.name.testName + " → " + it else it.toString() diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentRevocationTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentRevocationTest.kt index d8f71cc73..942867207 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentRevocationTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentRevocationTest.kt @@ -4,7 +4,7 @@ import at.asitplus.wallet.lib.DefaultZlibService import at.asitplus.wallet.lib.KmmBitSet import at.asitplus.wallet.lib.agent.Verifier.VerifyCredentialResult.Success import at.asitplus.wallet.lib.data.AtomicAttributeCredential -import at.asitplus.wallet.lib.data.ConstantIndex +import at.asitplus.wallet.lib.data.AttributeIndex import at.asitplus.wallet.lib.toBitSet import com.benasher44.uuid.uuid4 import io.kotest.assertions.fail @@ -21,6 +21,7 @@ import kotlin.time.Duration.Companion.seconds class AgentRevocationTest : FreeSpec({ lateinit var issuerCredentialStore: IssuerCredentialStore + lateinit var verifierCryptoService: CryptoService lateinit var verifier: Verifier lateinit var issuer: Issuer lateinit var expectedRevokedIndexes: List @@ -32,7 +33,10 @@ class AgentRevocationTest : FreeSpec({ issuerCredentialStore = issuerCredentialStore, dataProvider = DummyCredentialDataProvider() ) - verifier = VerifierAgent.newRandomInstance() + verifierCryptoService = DefaultCryptoService() + verifier = VerifierAgent.newDefaultInstance( + keyId = verifierCryptoService.keyId, + ) expectedRevokedIndexes = issuerCredentialStore.revokeRandomCredentials() } @@ -52,11 +56,8 @@ class AgentRevocationTest : FreeSpec({ } "credentials should contain status information" { - val result = issuer.issueCredentialWithTypes( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ) - if (result.failed.isNotEmpty()) fail("no issued credentials") + val result = issuer.issueCredentials(verifierCryptoService.keyId, AttributeIndex.genericAttributes) + if (!result.failed.isEmpty()) fail("no issued credentials") result.successful.map { it.vcJws }.forEach { val vcJws = verifier.verifyVcJws(it) @@ -73,6 +74,8 @@ class AgentRevocationTest : FreeSpec({ cryptoService = DefaultCryptoService(), issuerCredentialStore = issuerCredentialStore, ) + verifierCryptoService = DefaultCryptoService() + verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.keyId) expectedRevokedIndexes = listOf(1, 2, 4, 6, 7, 9, 10, 12, 13, 14) issuerCredentialStore.revokeCredentialsWithIndexes(expectedRevokedIndexes) @@ -92,6 +95,8 @@ class AgentRevocationTest : FreeSpec({ cryptoService = DefaultCryptoService(), issuerCredentialStore = issuerCredentialStore, ) + verifierCryptoService = DefaultCryptoService() + verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.keyId) expectedRevokedIndexes = listOf(1, 2, 4, 6, 7, 9, 10, 12, 13, 14) issuerCredentialStore.revokeCredentialsWithIndexes(expectedRevokedIndexes) @@ -128,8 +133,7 @@ private fun IssuerCredentialStore.revokeCredentialsWithIndexes(revokedIndexes: L val expirationDate = issuanceDate + 60.seconds for (i in 1..16) { val vcId = uuid4().toString() - val revListIndex = - storeGetNextIndex(vcId, cred, issuanceDate, expirationDate, FixedTimePeriodProvider.timePeriod)!! + val revListIndex = storeGetNextIndex(vcId, cred, issuanceDate, expirationDate, FixedTimePeriodProvider.timePeriod)!! if (revokedIndexes.contains(revListIndex)) { revoke(vcId, FixedTimePeriodProvider.timePeriod) } diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentTest.kt index ba5c935b9..a04d5ff62 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentTest.kt @@ -2,7 +2,7 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.agent.DummyCredentialDataProvider.Companion.ATTRIBUTE_WITH_ATTACHMENT import at.asitplus.wallet.lib.data.AttributeIndex -import at.asitplus.wallet.lib.data.ConstantIndex +import at.asitplus.wallet.lib.data.SchemaIndex import com.benasher44.uuid.uuid4 import io.kotest.assertions.fail import io.kotest.core.spec.style.FreeSpec @@ -15,6 +15,9 @@ import io.kotest.matchers.types.shouldBeInstanceOf class AgentTest : FreeSpec({ + lateinit var issuerCryptoService: CryptoService + lateinit var holderCryptoService: CryptoService + lateinit var verifierCryptoService: CryptoService lateinit var issuer: Issuer lateinit var holder: Holder lateinit var verifier: Verifier @@ -23,25 +26,31 @@ class AgentTest : FreeSpec({ lateinit var challenge: String beforeEach { + issuerCryptoService = DefaultCryptoService() + holderCryptoService = DefaultCryptoService() + verifierCryptoService = DefaultCryptoService() issuerCredentialStore = InMemoryIssuerCredentialStore() holderCredentialStore = InMemorySubjectCredentialStore() issuer = IssuerAgent.newDefaultInstance( + cryptoService = issuerCryptoService, issuerCredentialStore = issuerCredentialStore, dataProvider = DummyCredentialDataProvider(), ) holder = HolderAgent.newDefaultInstance( + holderCryptoService, subjectCredentialStore = holderCredentialStore ) - verifier = VerifierAgent.newRandomInstance() + verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.keyId) challenge = uuid4().toString() } "simple walk-through success" { - val vcList = issuer.issueCredentialWithTypes(holder.identifier, listOf(ConstantIndex.Generic.vcType)) + val vcList = + issuer.issueCredentials(holderCryptoService.keyId, AttributeIndex.genericAttributes) if (vcList.failed.isNotEmpty()) fail("no issued credentials") holder.storeCredentials(vcList.toStoreCredentialInput()) - val vp = holder.createPresentation(challenge, verifier.identifier) + val vp = holder.createPresentation(challenge, verifierCryptoService.keyId) vp.shouldNotBeNull() vp.shouldBeInstanceOf() val verified = verifier.verifyPresentation(vp.jws, challenge) @@ -49,13 +58,15 @@ class AgentTest : FreeSpec({ } "simple walk-through success with attachments" { - // DummyCredentialProvider issues an attachment for "picture" - val vcList = issuer.issueCredentialWithTypes(holder.identifier, listOf(ConstantIndex.Generic.vcType)) + val vcList = issuer.issueCredentials( + holderCryptoService.keyId, + listOf("${SchemaIndex.ATTR_GENERIC_PREFIX}/$ATTRIBUTE_WITH_ATTACHMENT") + ) vcList.successful.shouldNotBeEmpty() holder.storeCredentials(vcList.toStoreCredentialInput()) holderCredentialStore.getAttachment(ATTRIBUTE_WITH_ATTACHMENT).getOrThrow().shouldNotBeNull() - val vp = holder.createPresentation(challenge, verifier.identifier) + val vp = holder.createPresentation(challenge, verifierCryptoService.keyId) vp.shouldNotBeNull() vp.shouldBeInstanceOf() val verified = verifier.verifyPresentation(vp.jws, challenge) @@ -63,11 +74,12 @@ class AgentTest : FreeSpec({ } "wrong keyId in presentation leads to InvalidStructure" { - val credentials = issuer.issueCredentialWithTypes(holder.identifier, listOf(ConstantIndex.Generic.vcType)) + val credentials = + issuer.issueCredentials(holderCryptoService.keyId, AttributeIndex.genericAttributes) if (credentials.failed.isNotEmpty()) fail("no issued credentials") holder.storeCredentials(credentials.toStoreCredentialInput()) - val vp = holder.createPresentation(challenge, issuer.identifier) + val vp = holder.createPresentation(challenge, issuerCryptoService.keyId) vp.shouldNotBeNull() vp.shouldBeInstanceOf() val result = verifier.verifyPresentation(vp.jws, challenge) @@ -75,7 +87,8 @@ class AgentTest : FreeSpec({ } "revoked credentials must not be validated" { - val credentials = issuer.issueCredentialWithTypes(verifier.identifier, listOf(ConstantIndex.Generic.vcType)) + val credentials = + issuer.issueCredentials(verifierCryptoService.keyId, AttributeIndex.genericAttributes) if (credentials.failed.isNotEmpty()) fail("no issued credentials") issuer.revokeCredentials(credentials.successful.map { it.vcJws }) shouldBe true @@ -91,7 +104,8 @@ class AgentTest : FreeSpec({ "building presentation with revoked credentials should not work" - { "when setting a revocation list before storing credentials" { - val credentials = issuer.issueCredentialWithTypes(holder.identifier, listOf(ConstantIndex.Generic.vcType)) + val credentials = + issuer.issueCredentials(holderCryptoService.keyId, AttributeIndex.genericAttributes) if (credentials.failed.isNotEmpty()) fail("no issued credentials") issuer.revokeCredentials(credentials.successful.map { it.vcJws }) shouldBe true val revocationListCredential = issuer.issueRevocationListCredential(FixedTimePeriodProvider.timePeriod) @@ -103,11 +117,12 @@ class AgentTest : FreeSpec({ storedCredentials.rejected shouldHaveSize credentials.successful.size storedCredentials.notVerified.shouldBeEmpty() - holder.createPresentation(challenge, verifier.identifier) shouldBe null + holder.createPresentation(challenge, verifierCryptoService.keyId) shouldBe null } "and when setting a revocation list after storing credentials" { - val credentials = issuer.issueCredentialWithTypes(holder.identifier, listOf(ConstantIndex.Generic.vcType)) + val credentials = + issuer.issueCredentials(holderCryptoService.keyId, AttributeIndex.genericAttributes) if (credentials.failed.isNotEmpty()) fail("no issued credentials") val storedCredentials = holder.storeCredentials(credentials.toStoreCredentialInput()) storedCredentials.accepted shouldHaveSize credentials.successful.size @@ -119,7 +134,7 @@ class AgentTest : FreeSpec({ revocationListCredential.shouldNotBeNull() holder.setRevocationList(revocationListCredential) shouldBe true - holder.createPresentation(challenge, verifier.identifier) shouldBe null + holder.createPresentation(challenge, verifierCryptoService.keyId) shouldBe null } } @@ -132,7 +147,8 @@ class AgentTest : FreeSpec({ } "when they are valid" - { - val credentials = issuer.issueCredentialWithTypes(holder.identifier, listOf(ConstantIndex.Generic.vcType)) + val credentials = + issuer.issueCredentials(holderCryptoService.keyId, AttributeIndex.genericAttributes) if (credentials.failed.isNotEmpty()) fail("no issued credentials") val storedCredentials = holder.storeCredentials(credentials.toStoreCredentialInput()) storedCredentials.accepted shouldHaveSize credentials.successful.size @@ -158,7 +174,8 @@ class AgentTest : FreeSpec({ } "when the issuer has revoked them" { - val credentials = issuer.issueCredentialWithTypes(holder.identifier, listOf(ConstantIndex.Generic.vcType)) + val credentials = + issuer.issueCredentials(holderCryptoService.keyId, AttributeIndex.genericAttributes) if (credentials.failed.isNotEmpty()) fail("no issued credentials") val storedCredentials = holder.storeCredentials(credentials.toStoreCredentialInput()) storedCredentials.accepted shouldHaveSize credentials.successful.size @@ -179,14 +196,15 @@ class AgentTest : FreeSpec({ } "building presentation without necessary credentials" { - holder.createPresentation(challenge, verifier.identifier) shouldBe null + holder.createPresentation(challenge, verifierCryptoService.keyId) shouldBe null } "valid presentation is valid" { - val credentials = issuer.issueCredentialWithTypes(holder.identifier, listOf(ConstantIndex.Generic.vcType)) + val credentials = + issuer.issueCredentials(holderCryptoService.keyId, AttributeIndex.genericAttributes) if (credentials.failed.isNotEmpty()) fail("no issued credentials") holder.storeCredentials(credentials.toStoreCredentialInput()) - val vp = holder.createPresentation(challenge, verifier.identifier) + val vp = holder.createPresentation(challenge, verifierCryptoService.keyId) vp.shouldNotBeNull() vp.shouldBeInstanceOf() @@ -198,15 +216,16 @@ class AgentTest : FreeSpec({ } "valid presentation is valid -- some other attributes revoked" { - val credentials = issuer.issueCredentialWithTypes(holder.identifier, listOf(ConstantIndex.Generic.vcType)) + val credentials = + issuer.issueCredentials(holderCryptoService.keyId, AttributeIndex.genericAttributes) if (credentials.failed.isNotEmpty()) fail("no issued credentials") holder.storeCredentials(credentials.toStoreCredentialInput()) - val vp = holder.createPresentation(challenge, verifier.identifier) + val vp = holder.createPresentation(challenge, verifierCryptoService.keyId) vp.shouldNotBeNull() vp.shouldBeInstanceOf() val credentialsToRevoke = - issuer.issueCredentialWithTypes(holder.identifier, listOf(ConstantIndex.Generic.vcType)) + issuer.issueCredentials(issuerCryptoService.keyId, AttributeIndex.genericAttributes) if (credentialsToRevoke.failed.isNotEmpty()) fail("no issued credentials") issuer.revokeCredentials(credentialsToRevoke.successful.map { it.vcJws }) shouldBe true val revocationList = issuer.issueRevocationListCredential(FixedTimePeriodProvider.timePeriod) diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/DummyCredentialDataProvider.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/DummyCredentialDataProvider.kt index 1d3077446..488e057a8 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/DummyCredentialDataProvider.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/DummyCredentialDataProvider.kt @@ -2,7 +2,6 @@ package at.asitplus.wallet.lib.agent import at.asitplus.KmmResult import at.asitplus.wallet.lib.data.AtomicAttributeCredential -import at.asitplus.wallet.lib.data.AttributeIndex import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.data.SchemaIndex import io.matthewnelson.component.encoding.base16.encodeBase16 @@ -17,32 +16,25 @@ class DummyCredentialDataProvider( private val defaultLifetime = 1.minutes override fun getClaim(subjectId: String, attributeName: String) = - KmmResult.failure(UnsupportedOperationException("empty")) + getClaimInt(subjectId, attributeName)?.let { + KmmResult.success( + IssuerCredentialDataProvider.CredentialToBeIssued( + it, + clock.now() + defaultLifetime, + ConstantIndex.Generic.vcType, + attachmentList(attributeName) + ) + ) + } ?: KmmResult.failure(UnsupportedOperationException("no data")) - override fun getCredential(subjectId: String, attributeType: String) = - KmmResult.failure(UnsupportedOperationException("empty")) + private fun attachmentList(attributeName: String) = if (attributeName.endsWith(ATTRIBUTE_WITH_ATTACHMENT)) + listOf(Issuer.Attachment(ATTRIBUTE_WITH_ATTACHMENT, "image/webp", byteArrayOf(32))) + else null - override fun getCredentialWithType( - subjectId: String, - attributeTypes: Collection - ): KmmResult> { - if (attributeTypes.contains(ConstantIndex.Generic.vcType)) { - return KmmResult.success( - AttributeIndex.genericAttributes.mapNotNull { attributeName -> - getClaimInt(subjectId, attributeName)?.let { - IssuerCredentialDataProvider.CredentialToBeIssued( - it, - clock.now() + defaultLifetime, - ConstantIndex.Generic.vcType, - attachmentList(attributeName) - ) - } - } - ) - } else { - return KmmResult.failure(UnsupportedOperationException("no data")) - } - } + override fun getCredential(subjectId: String, attributeType: String) = + KmmResult.failure( + UnsupportedOperationException("empty") + ) private fun getClaimInt(subjectId: String, attributeName: String) = when { attributeName.startsWith(SchemaIndex.ATTR_GENERIC_PREFIX + "/") -> @@ -50,19 +42,15 @@ class DummyCredentialDataProvider( "given-name" -> AtomicAttributeCredential(subjectId, attributeName, "Susanne") "family-name" -> AtomicAttributeCredential(subjectId, attributeName, "Meier") "date-of-birth" -> AtomicAttributeCredential(subjectId, attributeName, "1990-01-01") - "identifier" -> AtomicAttributeCredential(subjectId, attributeName, randomValue()) - "picture" -> AtomicAttributeCredential(subjectId, attributeName, randomValue()) + "identifier" -> AtomicAttributeCredential(subjectId, attributeName, randomId()) + "picture" -> AtomicAttributeCredential(subjectId, attributeName, randomId()) else -> null } else -> null } - private fun attachmentList(attributeName: String) = if (attributeName.endsWith(ATTRIBUTE_WITH_ATTACHMENT)) - listOf(Issuer.Attachment(ATTRIBUTE_WITH_ATTACHMENT, "image/webp", byteArrayOf(32))) - else null - - private fun randomValue() = Random.nextBytes(32).encodeBase16() + private fun randomId() = Random.nextBytes(32).encodeBase16() companion object { const val ATTRIBUTE_WITH_ATTACHMENT = "picture" diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessengerConcurrentTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessengerConcurrentTest.kt index d2ff7f9c2..a3792f8fa 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessengerConcurrentTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessengerConcurrentTest.kt @@ -16,10 +16,10 @@ import kotlinx.coroutines.launch class IssueCredentialMessengerConcurrentTest : FreeSpec() { - private lateinit var issuerCryptoService: CryptoService - private lateinit var issuer: Issuer - private lateinit var issuerServiceEndpoint: String - private lateinit var issuerMessenger: IssueCredentialMessenger + lateinit var issuerCryptoService: CryptoService + lateinit var issuer: Issuer + lateinit var issuerServiceEndpoint: String + lateinit var issuerMessenger: IssueCredentialMessenger init { beforeEach { @@ -50,6 +50,7 @@ class IssueCredentialMessengerConcurrentTest : FreeSpec() { val cryptoService = DefaultCryptoService() return IssueCredentialMessenger.newHolderInstance( holder = HolderAgent.newDefaultInstance(cryptoService), + keyId = cryptoService.keyId, messageWrapper = MessageWrapper(cryptoService), ) } @@ -57,6 +58,7 @@ class IssueCredentialMessengerConcurrentTest : FreeSpec() { private fun initIssuerMessenger(scheme: ConstantIndex.CredentialScheme) = IssueCredentialMessenger.newIssuerInstance( issuer = issuer, + keyId = issuerCryptoService.keyId, messageWrapper = MessageWrapper(issuerCryptoService), serviceEndpoint = issuerServiceEndpoint, credentialScheme = scheme, diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessengerTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessengerTest.kt index f016e8995..0f145a26a 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessengerTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialMessengerTest.kt @@ -42,17 +42,30 @@ class IssueCredentialMessengerTest : FreeSpec() { assertAttachment(issuedCredential, "${SchemaIndex.ATTR_GENERIC_PREFIX}/$ATTRIBUTE_WITH_ATTACHMENT") } - // can't be created with a wrong keyId anymore, so that test was removed + "wrongKeyId" { + holderMessenger = IssueCredentialMessenger.newHolderInstance( + holder = holder, + keyId = issuerCryptoService.keyId, + messageWrapper = MessageWrapper(holderCryptoService), + ) + issuerMessenger = initIssuerMessenger(ConstantIndex.Generic) + + val issuedCredential = runProtocolFlow() + + assertEmptyVc(issuedCredential) + } } private fun initHolderMessenger() = IssueCredentialMessenger.newHolderInstance( holder = holder, + keyId = holderCryptoService.keyId, messageWrapper = MessageWrapper(holderCryptoService), ) private fun initIssuerMessenger(scheme: ConstantIndex.CredentialScheme) = IssueCredentialMessenger.newIssuerInstance( issuer = issuer, + keyId = issuerCryptoService.keyId, messageWrapper = MessageWrapper(issuerCryptoService), serviceEndpoint = issuerServiceEndpoint, credentialScheme = scheme, diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialProtocolTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialProtocolTest.kt index b25b37fff..a61b61f6a 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialProtocolTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/IssueCredentialProtocolTest.kt @@ -1,10 +1,10 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.data.ConstantIndex +import at.asitplus.wallet.lib.msg.JwmAttachmentData import at.asitplus.wallet.lib.msg.AttachmentFormatReference import at.asitplus.wallet.lib.msg.IssueCredential import at.asitplus.wallet.lib.msg.JwmAttachment -import at.asitplus.wallet.lib.msg.JwmAttachmentData import at.asitplus.wallet.lib.msg.Presentation import at.asitplus.wallet.lib.msg.PresentationBody import at.asitplus.wallet.lib.msg.RequestCredential @@ -30,11 +30,13 @@ class IssueCredentialProtocolTest : FreeSpec({ holder = HolderAgent.newDefaultInstance(holderCryptoService) issuerProtocol = IssueCredentialProtocol.newIssuerInstance( issuer = issuer, + keyId = issuerCryptoService.keyId, serviceEndpoint = "https://example.com/issue?${uuid4()}", credentialScheme = ConstantIndex.Generic, ) holderProtocol = IssueCredentialProtocol.newHolderInstance( holder = holder, + keyId = holderCryptoService.keyId, credentialScheme = ConstantIndex.Generic, ) } @@ -44,15 +46,18 @@ class IssueCredentialProtocolTest : FreeSpec({ oobInvitation.shouldBeInstanceOf() val invitationMessage = oobInvitation.message - val parsedInvitation = holderProtocol.parseMessage(invitationMessage, issuerCryptoService.toJsonWebKey()) + val parsedInvitation = + holderProtocol.parseMessage(invitationMessage, issuerCryptoService.keyId) parsedInvitation.shouldBeInstanceOf() val requestCredential = parsedInvitation.message - val parsedRequestCredential = issuerProtocol.parseMessage(requestCredential, holderCryptoService.toJsonWebKey()) + val parsedRequestCredential = + issuerProtocol.parseMessage(requestCredential, holderCryptoService.keyId) parsedRequestCredential.shouldBeInstanceOf() val issueCredential = parsedRequestCredential.message - val parsedIssueCredential = holderProtocol.parseMessage(issueCredential, issuerCryptoService.toJsonWebKey()) + val parsedIssueCredential = + holderProtocol.parseMessage(issueCredential, issuerCryptoService.keyId) parsedIssueCredential.shouldBeInstanceOf() val issuedCredential = parsedIssueCredential.lastMessage @@ -64,11 +69,12 @@ class IssueCredentialProtocolTest : FreeSpec({ requestCredential.shouldBeInstanceOf() val parsedRequestCredential = - issuerProtocol.parseMessage(requestCredential.message, holderCryptoService.toJsonWebKey()) + issuerProtocol.parseMessage(requestCredential.message, holderCryptoService.keyId) parsedRequestCredential.shouldBeInstanceOf() val issueCredential = parsedRequestCredential.message - val parsedIssueCredential = holderProtocol.parseMessage(issueCredential, issuerCryptoService.toJsonWebKey()) + val parsedIssueCredential = + holderProtocol.parseMessage(issueCredential, issuerCryptoService.keyId) parsedIssueCredential.shouldBeInstanceOf() val issuedCredential = parsedIssueCredential.lastMessage @@ -82,7 +88,7 @@ class IssueCredentialProtocolTest : FreeSpec({ threadId = uuid4().toString(), attachment = JwmAttachment(id = uuid4().toString(), "mimeType", JwmAttachmentData()) ), - issuerCryptoService.toJsonWebKey() + issuerCryptoService.keyId ) parsed.shouldBeInstanceOf() } @@ -92,7 +98,8 @@ class IssueCredentialProtocolTest : FreeSpec({ oobInvitation.shouldBeInstanceOf() val invitationMessage = oobInvitation.message - val parsedInvitation = holderProtocol.parseMessage(invitationMessage, issuerCryptoService.toJsonWebKey()) + val parsedInvitation = + holderProtocol.parseMessage(invitationMessage, issuerCryptoService.keyId) parsedInvitation.shouldBeInstanceOf() val requestCredential = parsedInvitation.message @@ -110,7 +117,7 @@ class IssueCredentialProtocolTest : FreeSpec({ ) ) val parsedRequestCredential = - issuerProtocol.parseMessage(wrongRequestCredential, holderCryptoService.toJsonWebKey()) + issuerProtocol.parseMessage(wrongRequestCredential, holderCryptoService.keyId) parsedRequestCredential.shouldBeInstanceOf() val problemReport = parsedRequestCredential.message diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/OidcSiopProtocolTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/OidcSiopProtocolTest.kt index b2ad8ac26..6be478efd 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/OidcSiopProtocolTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/OidcSiopProtocolTest.kt @@ -1,10 +1,13 @@ package at.asitplus.wallet.lib.agent -import at.asitplus.wallet.lib.data.ConstantIndex import at.asitplus.wallet.lib.data.SchemaIndex import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain import io.kotest.matchers.types.shouldBeInstanceOf +import io.ktor.http.parseUrlEncodedParameters import kotlinx.coroutines.runBlocking class OidcSiopProtocolTest : FreeSpec({ @@ -22,15 +25,15 @@ class OidcSiopProtocolTest : FreeSpec({ holderCryptoService = DefaultCryptoService() verifierCryptoService = DefaultCryptoService() holder = HolderAgent.newDefaultInstance(holderCryptoService) - verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.identifier) + verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.keyId) runBlocking { holder.storeCredentials( IssuerAgent.newDefaultInstance( DefaultCryptoService(), dataProvider = DummyCredentialDataProvider(), - ).issueCredentialWithTypes( - holder.identifier, - listOf(ConstantIndex.Generic.vcType) + ).issueCredentials( + holderCryptoService.keyId, + listOf("${SchemaIndex.ATTR_GENERIC_PREFIX}/given-name") ).toStoreCredentialInput() ) } @@ -50,9 +53,14 @@ class OidcSiopProtocolTest : FreeSpec({ "test" { val authnRequest = verifierOidcSiopProtocol.createAuthnRequest() println(authnRequest) // len: 1084 chars + authnRequest.shouldContain("redirect_uri=https%3A%2F%2Fwallet.a-sit.at%2Fverifier") + authnRequest.shouldNotContain("redirect_uri=%22https%3A%2F%2Fwallet.a-sit.at%2Fverifier%22") - val authnResponse = holderOidcSiopProtocol.createAuthnResponse(authnRequest)!! + val authnResponse = holderOidcSiopProtocol.createAuthnResponse(authnRequest) + authnResponse.shouldNotBeNull() println(authnResponse) // len: 3702 chars with one "atomic" credential (string-string) + authnResponse.shouldContain("id_token=") + authnResponse.shouldNotContain("id_token=%22") val result = verifierOidcSiopProtocol.validateAuthnResponse(authnResponse) println(result) diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/PresentProofMessengerTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/PresentProofMessengerTest.kt index 44773933f..8f07e90cc 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/PresentProofMessengerTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/PresentProofMessengerTest.kt @@ -34,11 +34,11 @@ class PresentProofMessengerTest : FreeSpec() { verifierCryptoService = DefaultCryptoService() issuerCryptoService = DefaultCryptoService() holder = HolderAgent.newDefaultInstance(holderCryptoService) - verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.identifier) + verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.keyId) issuer = IssuerAgent.newDefaultInstance(issuerCryptoService) verifierChallenge = uuid4().toString() holderServiceEndpoint = "https://example.com/present-proof?${uuid4()}" - val credentialSubject = randomCredential(holderCryptoService.identifier) + val credentialSubject = randomCredential(holderCryptoService.keyId) runBlocking { holder.storeCredentials(issuer.issueCredential(credentialSubject).toStoreCredentialInput()) } @@ -47,11 +47,13 @@ class PresentProofMessengerTest : FreeSpec() { "presentProof" { val holderMessenger = PresentProofMessenger.newHolderInstance( holder = holder, + keyId = holderCryptoService.keyId, messageWrapper = MessageWrapper(holderCryptoService), serviceEndpoint = holderServiceEndpoint ) val verifierMessenger = PresentProofMessenger.newVerifierInstance( verifier = verifier, + keyId = verifierCryptoService.keyId, messageWrapper = MessageWrapper(verifierCryptoService) ) @@ -77,7 +79,7 @@ class PresentProofMessengerTest : FreeSpec() { } "selectiveDisclosure" { - val expectedSubject = randomCredential(holder.identifier) + val expectedSubject = randomCredential(holderCryptoService.keyId) val attributeName = (expectedSubject.subject as AtomicAttributeCredential).name val attributeValue = (expectedSubject.subject as AtomicAttributeCredential).value val expectedVc = issuer.issueCredential(expectedSubject) @@ -85,11 +87,13 @@ class PresentProofMessengerTest : FreeSpec() { val holderMessenger = PresentProofMessenger.newHolderInstance( holder = holder, + keyId = holderCryptoService.keyId, messageWrapper = MessageWrapper(holderCryptoService), serviceEndpoint = "https://example.com" ) val verifierMessenger = PresentProofMessenger.newVerifierInstance( verifier = verifier, + keyId = verifierCryptoService.keyId, messageWrapper = MessageWrapper(verifierCryptoService), challengeForPresentation = verifierChallenge, requestedAttributeNames = listOf(attributeName) @@ -115,7 +119,7 @@ class PresentProofMessengerTest : FreeSpec() { } "selectiveDisclosure_notFulfilled" { - val expectedSubject = randomCredential(holder.identifier) + val expectedSubject = randomCredential(holderCryptoService.keyId) val attributeName = (expectedSubject.subject as AtomicAttributeCredential).name val attributeValue = (expectedSubject.subject as AtomicAttributeCredential).value val expectedVc = issuer.issueCredential(expectedSubject).toStoreCredentialInput() @@ -123,11 +127,13 @@ class PresentProofMessengerTest : FreeSpec() { val holderMessenger = PresentProofMessenger.newHolderInstance( holder = holder, + keyId = holderCryptoService.keyId, messageWrapper = MessageWrapper(holderCryptoService), serviceEndpoint = "https://example.com/" ) var verifierMessenger = PresentProofMessenger.newVerifierInstance( verifier = verifier, + keyId = verifierCryptoService.keyId, messageWrapper = MessageWrapper(verifierCryptoService), challengeForPresentation = verifierChallenge, // subject is not expected to provide an attribute with this name @@ -156,6 +162,7 @@ class PresentProofMessengerTest : FreeSpec() { // note that the subject messenger is not recreated, i.e. it expects another "requestPresentation" message verifierMessenger = PresentProofMessenger.newVerifierInstance( verifier = verifier, + keyId = verifierCryptoService.keyId, messageWrapper = MessageWrapper(verifierCryptoService), challengeForPresentation = verifierChallenge, requestedAttributeNames = listOf(attributeName) diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/PresentProofProtocolTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/PresentProofProtocolTest.kt index 08cf8945f..735348d22 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/PresentProofProtocolTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/PresentProofProtocolTest.kt @@ -1,8 +1,8 @@ package at.asitplus.wallet.lib.agent -import at.asitplus.wallet.lib.data.ConstantIndex -import at.asitplus.wallet.lib.msg.JwmAttachment +import at.asitplus.wallet.lib.data.AttributeIndex import at.asitplus.wallet.lib.msg.JwmAttachmentData +import at.asitplus.wallet.lib.msg.JwmAttachment import at.asitplus.wallet.lib.msg.Presentation import at.asitplus.wallet.lib.msg.RequestCredential import at.asitplus.wallet.lib.msg.RequestCredentialBody @@ -24,14 +24,14 @@ class PresentProofProtocolTest : FreeSpec({ holderCryptoService = DefaultCryptoService() verifierCryptoService = DefaultCryptoService() holder = HolderAgent.newDefaultInstance(holderCryptoService) - verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.identifier) + verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.keyId) holderProtocol = PresentProofProtocol.newHolderInstance( - holder = holder, - serviceEndpoint = "https://example.com/" - ) - verifierProtocol = PresentProofProtocol.newVerifierInstance( - verifier = verifier + holder, + holderCryptoService.keyId, + "https://example.com/" ) + verifierProtocol = + PresentProofProtocol.newVerifierInstance(verifier, verifierCryptoService.keyId) } "presentProofGenericWithInvitation" { @@ -39,9 +39,9 @@ class PresentProofProtocolTest : FreeSpec({ IssuerAgent.newDefaultInstance( DefaultCryptoService(), dataProvider = DummyCredentialDataProvider(), - ).issueCredentialWithTypes( - holder.identifier, - listOf(ConstantIndex.Generic.vcType) + ).issueCredentials( + holderCryptoService.keyId, + AttributeIndex.genericAttributes ).toStoreCredentialInput() ) @@ -49,16 +49,18 @@ class PresentProofProtocolTest : FreeSpec({ oobInvitation.shouldBeInstanceOf() val invitationMessage = oobInvitation.message - val parsedInvitation = verifierProtocol.parseMessage(invitationMessage, holderCryptoService.toJsonWebKey()) + val parsedInvitation = + verifierProtocol.parseMessage(invitationMessage, holderCryptoService.keyId) parsedInvitation.shouldBeInstanceOf() val requestPresentation = parsedInvitation.message val parsedRequestPresentation = - holderProtocol.parseMessage(requestPresentation, verifierCryptoService.toJsonWebKey()) + holderProtocol.parseMessage(requestPresentation, verifierCryptoService.keyId) parsedRequestPresentation.shouldBeInstanceOf() val presentation = parsedRequestPresentation.message - val parsedPresentation = verifierProtocol.parseMessage(presentation, holderCryptoService.toJsonWebKey()) + val parsedPresentation = + verifierProtocol.parseMessage(presentation, holderCryptoService.keyId) parsedPresentation.shouldBeInstanceOf() val receivedPresentation = parsedPresentation.lastMessage @@ -70,21 +72,24 @@ class PresentProofProtocolTest : FreeSpec({ IssuerAgent.newDefaultInstance( DefaultCryptoService(), dataProvider = DummyCredentialDataProvider(), - ).issueCredentialWithTypes( - holder.identifier, - listOf(ConstantIndex.Generic.vcType) + ).issueCredentials( + holderCryptoService.keyId, + AttributeIndex.genericAttributes ).toStoreCredentialInput() ) val requestPresentation = verifierProtocol.startDirect() requestPresentation.shouldBeInstanceOf() - val parsedRequestPresentation = - holderProtocol.parseMessage(requestPresentation.message, verifierCryptoService.toJsonWebKey()) + val parsedRequestPresentation = holderProtocol.parseMessage( + requestPresentation.message, + verifierCryptoService.keyId + ) parsedRequestPresentation.shouldBeInstanceOf() val presentation = parsedRequestPresentation.message - val parsedPresentation = verifierProtocol.parseMessage(presentation, holderCryptoService.toJsonWebKey()) + val parsedPresentation = + verifierProtocol.parseMessage(presentation, holderCryptoService.keyId) parsedPresentation.shouldBeInstanceOf() val receivedPresentation = parsedPresentation.lastMessage @@ -98,7 +103,7 @@ class PresentProofProtocolTest : FreeSpec({ parentThreadId = uuid4().toString(), attachment = JwmAttachment(id = uuid4().toString(), "mimeType", JwmAttachmentData()) ), - holderCryptoService.toJsonWebKey() + holderCryptoService.keyId ) parsed.shouldBeInstanceOf() } @@ -108,12 +113,13 @@ class PresentProofProtocolTest : FreeSpec({ oobInvitation.shouldBeInstanceOf() val invitationMessage = oobInvitation.message - val parsedInvitation = verifierProtocol.parseMessage(invitationMessage, holderCryptoService.toJsonWebKey()) + val parsedInvitation = + verifierProtocol.parseMessage(invitationMessage, holderCryptoService.keyId) parsedInvitation.shouldBeInstanceOf() val requestPresentation = parsedInvitation.message val parsedRequestPresentation = - holderProtocol.parseMessage(requestPresentation, verifierCryptoService.toJsonWebKey()) + holderProtocol.parseMessage(requestPresentation, verifierCryptoService.keyId) parsedRequestPresentation.shouldBeInstanceOf() val problemReport = parsedRequestPresentation.message diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ProblemReporterTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ProblemReporterTest.kt index 7d1edeff0..752ad0da7 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ProblemReporterTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ProblemReporterTest.kt @@ -1,6 +1,7 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.msg.* +import at.asitplus.wallet.lib.nameHack import com.benasher44.uuid.uuid4 import io.kotest.core.spec.style.FreeSpec import io.kotest.datatest.withData @@ -11,7 +12,7 @@ class ProblemReporterTest : FreeSpec({ val problemReporter = ProblemReporter() "sorter" - { - withData(ProblemReportSorter.values().asList()) { + withData(nameFn = ::nameHack, ProblemReportSorter.values().asList()) { val report = ProblemReport( body = ProblemReportBody( sorter = it, @@ -32,7 +33,7 @@ class ProblemReporterTest : FreeSpec({ } "scope" - { - withData(ProblemReportScope.values().asList()) { + withData(nameFn = ::nameHack, ProblemReportScope.values().asList()) { val report = ProblemReport( body = ProblemReportBody( sorter = ProblemReportSorter.WARNING, @@ -53,7 +54,7 @@ class ProblemReporterTest : FreeSpec({ } "descriptor" - { - withData(ProblemReportDescriptor.values().asList()) { + withData(nameFn = ::nameHack, ProblemReportDescriptor.values().asList()) { val report = ProblemReport( body = ProblemReportBody( sorter = ProblemReportSorter.WARNING, diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt index 5cfff6669..a39833234 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt @@ -1,18 +1,19 @@ package at.asitplus.wallet.lib.agent import at.asitplus.wallet.lib.data.AtomicAttributeCredential -import at.asitplus.wallet.lib.data.ConstantIndex +import at.asitplus.wallet.lib.data.AttributeIndex import at.asitplus.wallet.lib.data.CredentialStatus import at.asitplus.wallet.lib.data.VerifiableCredential import at.asitplus.wallet.lib.data.VerifiableCredentialJws import at.asitplus.wallet.lib.jws.DefaultJwsService -import at.asitplus.wallet.lib.jws.JwsAlgorithm -import at.asitplus.wallet.lib.jws.JwsContentTypeConstants +import at.asitplus.wallet.lib.jws.JwsContentType import at.asitplus.wallet.lib.jws.JwsHeader import at.asitplus.wallet.lib.jws.JwsService import at.asitplus.wallet.lib.jws.JwsSigned +import at.asitplus.wallet.lib.nameHack import com.benasher44.uuid.uuid4 import io.kotest.core.spec.style.FreeSpec +import io.kotest.core.spec.style.scopes.ContainerScope import io.kotest.datatest.withData import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe @@ -26,11 +27,12 @@ import kotlin.time.Duration.Companion.seconds class ValidatorVcTest : FreeSpec() { - private lateinit var issuer: Issuer - private lateinit var issuerCredentialStore: IssuerCredentialStore - private lateinit var issuerJwsService: JwsService - private lateinit var issuerCryptoService: CryptoService - private lateinit var verifier: Verifier + lateinit var issuer: Issuer + lateinit var issuerCredentialStore: IssuerCredentialStore + lateinit var issuerJwsService: JwsService + lateinit var issuerCryptoService: CryptoService + lateinit var verifier: Verifier + lateinit var verifierCryptoService: CryptoService private val dataProvider: IssuerCredentialDataProvider = DummyCredentialDataProvider() private val revocationListUrl: String = "https://wallet.a-sit.at/backend/credentials/status/1" @@ -44,24 +46,20 @@ class ValidatorVcTest : FreeSpec() { issuerCredentialStore = issuerCredentialStore, ) issuerJwsService = DefaultJwsService(issuerCryptoService) - verifier = VerifierAgent.newRandomInstance() + verifierCryptoService = DefaultCryptoService() + verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.keyId) } "credentials are valid for" { - issuer.issueCredentialWithTypes( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).successful.map { it.vcJws } + issuer.issueCredentials(verifierCryptoService.keyId, AttributeIndex.genericAttributes) + .successful.map { it.vcJws } .forEach { verifier.verifyVcJws(it).shouldBeInstanceOf() } } "revoked credentials are not valid" { - issuer.issueCredentialWithTypes( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ) + issuer.issueCredentials(verifierCryptoService.keyId, AttributeIndex.genericAttributes) .successful .map { it.vcJws } .map { it to verifier.verifyVcJws(it) }.forEach { @@ -82,7 +80,7 @@ class ValidatorVcTest : FreeSpec() { } "wrong subject keyId is not be valid" { - issuer.issueCredentialWithTypes(uuid4().toString(), listOf(ConstantIndex.Generic.vcType)) + issuer.issueCredentials(uuid4().toString(), AttributeIndex.genericAttributes) .successful.map { it.vcJws }.forEach { verifier.verifyVcJws(it) .shouldBeInstanceOf() @@ -90,10 +88,7 @@ class ValidatorVcTest : FreeSpec() { } "credential with invalid JWS format is not valid" { - issuer.issueCredentialWithTypes( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ) + issuer.issueCredentials(verifierCryptoService.keyId, AttributeIndex.genericAttributes) .successful.map { it.vcJws } .map { it.replaceFirstChar { "f" } }.forEach { verifier.verifyVcJws(it) @@ -102,13 +97,9 @@ class ValidatorVcTest : FreeSpec() { } "Manually created and valid credential is valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it) + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { issueCredential(it) } .let { wrapVcInJws(it) } .let { signJws(it) } ?.let { @@ -118,13 +109,9 @@ class ValidatorVcTest : FreeSpec() { } "Wrong key ends in wrong signature is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it) + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { issueCredential(it) } .let { wrapVcInJws(it) } .let { wrapVcInJwsWrongKey(it) } ?.let { @@ -135,13 +122,9 @@ class ValidatorVcTest : FreeSpec() { } "Invalid sub in credential is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it) + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { issueCredential(it) } .let { wrapVcInJws(it, subject = "vc.id") } .let { signJws(it) } ?.let { @@ -152,13 +135,9 @@ class ValidatorVcTest : FreeSpec() { } "Invalid issuer in credential is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it) + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { issueCredential(it) } .let { wrapVcInJws(it, issuer = "vc.issuer") } .let { signJws(it) }?.let { verifier.verifyVcJws(it) @@ -168,13 +147,9 @@ class ValidatorVcTest : FreeSpec() { } "Invalid jwtId in credential is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it) + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { issueCredential(it) } .let { wrapVcInJws(it, jwtId = "vc.jwtId") } .let { signJws(it) } ?.let { @@ -185,13 +160,9 @@ class ValidatorVcTest : FreeSpec() { } "Invalid type in credential is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it) + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { issueCredential(it) } .also { it.type[0] = "fakeCredential" } .let { wrapVcInJws(it) } .let { signJws(it) } @@ -203,17 +174,13 @@ class ValidatorVcTest : FreeSpec() { } "Invalid expiration in credential is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it, expirationDate = Clock.System.now() - 1.hours) + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { issueCredential(it, expirationDate = Clock.System.now() - 1.hours) } .let { VerifiableCredentialJws( vc = it, - subject = verifier.identifier, + subject = verifierCryptoService.keyId, notBefore = it.issuanceDate, issuer = it.issuer, expiration = Clock.System.now() + 1.hours, @@ -229,13 +196,9 @@ class ValidatorVcTest : FreeSpec() { } "No expiration date is valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it, expirationDate = null) + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { issueCredential(it, expirationDate = null) } .let { wrapVcInJws(it) } .let { signJws(it) } ?.let { @@ -245,14 +208,14 @@ class ValidatorVcTest : FreeSpec() { } "Invalid jws-expiration in credential is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it, expirationDate = Clock.System.now() + 1.hours) - .let { wrapVcInJws(it, expirationDate = Clock.System.now() - 1.hours) } + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { + issueCredential(it, expirationDate = Clock.System.now() + 1.hours) + } + .let { + wrapVcInJws(it, expirationDate = Clock.System.now() - 1.hours) + } .let { signJws(it) } ?.let { verifier.verifyVcJws(it) @@ -262,15 +225,11 @@ class ValidatorVcTest : FreeSpec() { } "Expiration not matching in credential is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - it.let { - issueCredential(it, expirationDate = Clock.System.now() + 1.hours) - } + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { + issueCredential(it, expirationDate = Clock.System.now() + 1.hours) + } .let { wrapVcInJws(it, expirationDate = Clock.System.now() + 2.hours) } @@ -283,13 +242,9 @@ class ValidatorVcTest : FreeSpec() { } "Invalid NotBefore in credential is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - it.let { issueCredential(it) } + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { issueCredential(it) } .let { wrapVcInJws(it, issuanceDate = Clock.System.now() + 2.hours) } @@ -302,13 +257,11 @@ class ValidatorVcTest : FreeSpec() { } "Invalid issuance date in credential is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it, issuanceDate = Clock.System.now() + 1.hours) + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { + issueCredential(it, issuanceDate = Clock.System.now() + 1.hours) + } .let { wrapVcInJws(it) } .let { signJws(it) } ?.let { @@ -319,13 +272,11 @@ class ValidatorVcTest : FreeSpec() { } "Issuance date and not before not matching is not valid" - { - withData( - dataProvider.getCredentialWithType( - verifier.identifier, - listOf(ConstantIndex.Generic.vcType) - ).getOrThrow() - ) { - issueCredential(it, issuanceDate = Clock.System.now() - 1.hours) + withData(nameFn = ::vcName, AttributeIndex.genericAttributes) { + it.let { dataProvider.getClaim(verifierCryptoService.keyId, it).getOrThrow() } + .let { + issueCredential(it, issuanceDate = Clock.System.now() - 1.hours) + } .let { wrapVcInJws(it, issuanceDate = Clock.System.now()) } .let { signJws(it) } ?.let { @@ -350,7 +301,7 @@ class ValidatorVcTest : FreeSpec() { val credentialStatus = CredentialStatus(revocationListUrl, statusListIndex) return VerifiableCredential( id = vcId, - issuer = issuer.identifier, + issuer = issuerCryptoService.keyId, credentialStatus = credentialStatus, credentialSubject = sub, issuanceDate = issuanceDate, @@ -360,7 +311,7 @@ class ValidatorVcTest : FreeSpec() { private fun wrapVcInJws( it: VerifiableCredential, - subject: String = verifier.identifier, + subject: String = verifierCryptoService.keyId, issuer: String = it.issuer, jwtId: String = it.id, issuanceDate: Instant = it.issuanceDate, @@ -377,18 +328,19 @@ class ValidatorVcTest : FreeSpec() { private suspend fun signJws(vcJws: VerifiableCredentialJws): String? { val vcSerialized = vcJws.serialize() val jwsPayload = vcSerialized.encodeToByteArray() - return issuerJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + return issuerJwsService.createSignedJwt(JwsContentType.JWT, jwsPayload) } private suspend fun wrapVcInJwsWrongKey(vcJws: VerifiableCredentialJws): String? { val jwsHeader = JwsHeader( - algorithm = JwsAlgorithm.ES256, - keyId = verifier.identifier, - type = JwsContentTypeConstants.JWT + verifierCryptoService.jwsAlgorithm, + verifierCryptoService.keyId, + JwsContentType.JWT ) val jwsPayload = vcJws.serialize().encodeToByteArray() - val signatureInput = jwsHeader.serialize().encodeToByteArray().encodeBase64(Base64.UrlSafe()) + - "." + jwsPayload.encodeBase64(Base64.UrlSafe()) + val signatureInput = + jwsHeader.serialize().encodeToByteArray().encodeBase64(Base64.UrlSafe(pad = false)) + + "." + jwsPayload.encodeBase64(Base64.UrlSafe(pad = false)) val signatureInputBytes = signatureInput.encodeToByteArray() val signature = issuerCryptoService.sign(signatureInputBytes) .getOrElse { return null } @@ -396,3 +348,6 @@ class ValidatorVcTest : FreeSpec() { } } + +private fun ContainerScope.vcName(it: String) = nameHack(it.substring(it.lastIndexOf('/') + 1)) + diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt index 8d239648c..483b8ef41 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt @@ -1,10 +1,10 @@ package at.asitplus.wallet.lib.agent -import at.asitplus.wallet.lib.data.ConstantIndex +import at.asitplus.wallet.lib.data.AttributeIndex import at.asitplus.wallet.lib.data.VerifiablePresentation import at.asitplus.wallet.lib.data.VerifiablePresentationJws import at.asitplus.wallet.lib.jws.DefaultJwsService -import at.asitplus.wallet.lib.jws.JwsContentTypeConstants +import at.asitplus.wallet.lib.jws.JwsContentType import at.asitplus.wallet.lib.jws.JwsService import com.benasher44.uuid.uuid4 import io.kotest.core.spec.style.FreeSpec @@ -20,17 +20,21 @@ class ValidatorVpTest : FreeSpec({ lateinit var validator: Validator lateinit var issuer: Issuer lateinit var issuerCredentialStore: IssuerCredentialStore + lateinit var issuerCryptoService: CryptoService lateinit var holder: Holder lateinit var holderCredentialStore: SubjectCredentialStore lateinit var holderJwsService: JwsService lateinit var holderCryptoService: CryptoService lateinit var verifier: Verifier + lateinit var verifierCryptoService: CryptoService lateinit var challenge: String beforeEach { validator = Validator.newDefaultInstance(DefaultVerifierCryptoService()) issuerCredentialStore = InMemoryIssuerCredentialStore() + issuerCryptoService = DefaultCryptoService() issuer = IssuerAgent.newDefaultInstance( + cryptoService = issuerCryptoService, issuerCredentialStore = issuerCredentialStore, dataProvider = DummyCredentialDataProvider(), ) @@ -41,20 +45,19 @@ class ValidatorVpTest : FreeSpec({ subjectCredentialStore = holderCredentialStore, ) holderJwsService = DefaultJwsService(holderCryptoService) - verifier = VerifierAgent.newRandomInstance() + verifierCryptoService = DefaultCryptoService() + verifier = VerifierAgent.newDefaultInstance(verifierCryptoService.keyId) challenge = uuid4().toString() runBlocking { holder.storeCredentials( - issuer.issueCredentialWithTypes( - holder.identifier, - listOf(ConstantIndex.Generic.vcType) - ).toStoreCredentialInput() + issuer.issueCredentials(holderCryptoService.keyId, AttributeIndex.genericAttributes) + .toStoreCredentialInput() ) } } "correct challenge in VP leads to Success" { - val vp = holder.createPresentation(challenge, verifier.identifier) + val vp = holder.createPresentation(challenge, verifierCryptoService.keyId) vp.shouldNotBeNull() vp.shouldBeInstanceOf() @@ -66,7 +69,7 @@ class ValidatorVpTest : FreeSpec({ val holderCredentials = holder.getCredentials() holderCredentials.shouldNotBeNull() val holderVcSerialized = holderCredentials.map { it.vcSerialized }.map { it.reversed() } - val vp = holder.createPresentation(holderVcSerialized, challenge, verifier.identifier) + val vp = holder.createPresentation(holderVcSerialized, challenge, verifierCryptoService.keyId) vp.shouldNotBeNull() vp.shouldBeInstanceOf() @@ -78,7 +81,7 @@ class ValidatorVpTest : FreeSpec({ } "wrong challenge in VP leads to InvalidStructure" { - val vp = holder.createPresentation("challenge", verifier.identifier) + val vp = holder.createPresentation("challenge", verifierCryptoService.keyId) vp.shouldNotBeNull() vp.shouldBeInstanceOf() @@ -96,7 +99,7 @@ class ValidatorVpTest : FreeSpec({ } "valid parsed presentation should separate revoked and valid credentials" { - val vp = holder.createPresentation(challenge, verifier.identifier) + val vp = holder.createPresentation(challenge, verifierCryptoService.keyId) vp.shouldNotBeNull() vp.shouldBeInstanceOf() @@ -124,13 +127,10 @@ class ValidatorVpTest : FreeSpec({ (validCredentials.isEmpty()) shouldBe false val vp = VerifiablePresentation(validCredentials.toTypedArray()) - val vpSerialized = vp.toJws( - challenge = challenge, - issuerId = holder.identifier, - audienceId = verifier.identifier, - ).serialize() + val vpSerialized = + vp.toJws(challenge, holderCryptoService.keyId, verifierCryptoService.keyId).serialize() val jwsPayload = vpSerialized.encodeToByteArray() - val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + val vpJws = holderJwsService.createSignedJwt(JwsContentType.JWT, jwsPayload) vpJws.shouldNotBeNull() verifier.verifyPresentation(vpJws, challenge) @@ -142,15 +142,16 @@ class ValidatorVpTest : FreeSpec({ holderCredentialStore.getCredentials().getOrThrow().map { it.vcSerialized } val vp = VerifiablePresentation(credentials.toTypedArray()) - val vpSerialized = VerifiablePresentationJws( - vp = vp, - challenge = challenge, - issuer = verifier.identifier, - audience = verifier.identifier, - jwtId = vp.id, - ).serialize() + val vpSerialized = + VerifiablePresentationJws( + vp, + challenge, + issuer = verifierCryptoService.keyId, + verifierCryptoService.keyId, + vp.id + ).serialize() val jwsPayload = vpSerialized.encodeToByteArray() - val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + val vpJws = holderJwsService.createSignedJwt(JwsContentType.JWT, jwsPayload) vpJws.shouldNotBeNull() verifier.verifyPresentation(vpJws, challenge) @@ -161,15 +162,16 @@ class ValidatorVpTest : FreeSpec({ val credentials = holderCredentialStore.getCredentials().getOrThrow().map { it.vcSerialized } val vp = VerifiablePresentation(credentials.toTypedArray()) - val vpSerialized = VerifiablePresentationJws( - vp = vp, - challenge = challenge, - issuer = holder.identifier, - audience = verifier.identifier, - jwtId = "wrong_jwtId", - ).serialize() + val vpSerialized = + VerifiablePresentationJws( + vp, + challenge, + holderCryptoService.keyId, + verifierCryptoService.keyId, + "wrong_jwtId" + ).serialize() val jwsPayload = vpSerialized.encodeToByteArray() - val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + val vpJws = holderJwsService.createSignedJwt(JwsContentType.JWT, jwsPayload) vpJws.shouldNotBeNull() verifier.verifyPresentation(vpJws, challenge) @@ -179,18 +181,12 @@ class ValidatorVpTest : FreeSpec({ "Wrong type in VP is not valid" { val credentials = holderCredentialStore.getCredentials().getOrThrow().map { it.vcSerialized } - val vp = VerifiablePresentation( - id = "urn:uuid:${uuid4()}", - type = "wrong_type", - verifiableCredential = credentials.toTypedArray() - ) - val vpSerialized = vp.toJws( - challenge = challenge, - issuerId = holder.identifier, - audienceId = verifier.identifier, - ).serialize() + val vp = + VerifiablePresentation("urn:uuid:${uuid4()}", "wrong_type", credentials.toTypedArray()) + val vpSerialized = + vp.toJws(challenge, holderCryptoService.keyId, verifierCryptoService.keyId).serialize() val jwsPayload = vpSerialized.encodeToByteArray() - val vpJws = holderJwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) + val vpJws = holderJwsService.createSignedJwt(JwsContentType.JWT, jwsPayload) vpJws.shouldNotBeNull() verifier.verifyPresentation(vpJws, challenge) diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JweSerializationTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JweSerializationTest.kt index 445432757..9bd399de8 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JweSerializationTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JweSerializationTest.kt @@ -12,7 +12,7 @@ class JweSerializationTest : FreeSpec({ val kid = uuid4().toString() val algorithm = JweAlgorithm.ECDH_ES val encryption = JweEncryption.A256GCM - val type = JwsContentTypeConstants.JWT + val type = JwsContentType.JWT val jweHeader = JweHeader( algorithm = algorithm, encryption = encryption, @@ -25,15 +25,15 @@ class JweSerializationTest : FreeSpec({ serialized shouldContain """"${algorithm.text}"""" serialized shouldContain """"${encryption.text}"""" serialized shouldContain """"$kid"""" - serialized shouldContain """"$type"""" + serialized shouldContain """"${type.text}"""" } "Deserialization is correct" { val kid = uuid4().toString() val algorithm = JweAlgorithm.ECDH_ES val encryption = JweEncryption.A256GCM - val type = JwsContentTypeConstants.JWT - val serialized = """{"alg": "${algorithm.text}", "enc": "${encryption.text}", "kid": "$kid", "typ": "$type"}""" + val type = JwsContentType.JWT + val serialized = """{"alg": "${algorithm.text}", "enc": "${encryption.text}", "kid": "$kid", "typ": "${type.text}"}""" val parsed = JweHeader.deserialize(serialized) @@ -47,8 +47,8 @@ class JweSerializationTest : FreeSpec({ "Deserialization with unknown algorithm is correct" { val kid = uuid4().toString() val encryption = JweEncryption.A256GCM - val type = JwsContentTypeConstants.JWT - val serialized = """{"alg": "foo", "enc": "${encryption.text}", "kid": "$kid", "typ": "$type"}""" + val type = JwsContentType.JWT + val serialized = """{"alg": "foo", "enc": "${encryption.text}", "kid": "$kid", "typ": "${type.text}"}""" val parsed = JweHeader.deserialize(serialized) @@ -62,8 +62,8 @@ class JweSerializationTest : FreeSpec({ "Deserialization with unknown encryption is correct" { val kid = uuid4().toString() val algorithm = JweAlgorithm.ECDH_ES - val type = JwsContentTypeConstants.JWT - val serialized = """{"alg": "${algorithm.text}", "enc": "foo", "kid": "$kid", "typ": "$type"}""" + val type = JwsContentType.JWT + val serialized = """{"alg": "${algorithm.text}", "enc": "foo", "kid": "$kid", "typ": "${type.text}"}""" val parsed = JweHeader.deserialize(serialized) @@ -78,8 +78,7 @@ class JweSerializationTest : FreeSpec({ val kid = uuid4().toString() val algorithm = JweAlgorithm.ECDH_ES val encryption = JweEncryption.A256GCM - val type = uuid4().toString() - val serialized = """{"alg": "${algorithm.text}", "enc": "${encryption.text}", "kid": "$kid", "typ": "$type"}""" + val serialized = """{"alg": "${algorithm.text}", "enc": "${encryption.text}", "kid": "$kid", "typ": "foo"}""" val parsed = JweHeader.deserialize(serialized) @@ -87,7 +86,7 @@ class JweSerializationTest : FreeSpec({ parsed.algorithm shouldBe algorithm parsed.encryption shouldBe encryption parsed.keyId shouldBe kid - parsed.type shouldBe type + parsed.type shouldBe null } }) diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt index 52b4e9a0b..9421c3823 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsHeaderSerializationTest.kt @@ -17,7 +17,7 @@ class JwsHeaderSerializationTest : FreeSpec({ val second = Random.nextBytes(32) val algorithm = JwsAlgorithm.ES256 val kid = uuid4().toString() - val type = JwsContentTypeConstants.JWT + val type = JwsContentType.JWT val header = JwsHeader( algorithm = algorithm, keyId = kid, @@ -37,10 +37,10 @@ class JwsHeaderSerializationTest : FreeSpec({ val second = Random.nextBytes(32) val algorithm = JwsAlgorithm.ES256 val kid = uuid4().toString() - val type = JwsContentTypeConstants.JWT + val type = JwsContentType.JWT val serialized = - """{"alg": "${algorithm.text}", "kid": "$kid", "typ": "$type", "x5c":["${first.encodeBase64()}","${second.encodeBase64()}"]}""" + """{"alg": "${algorithm.text}", "kid": "$kid", "typ": "${type.text}", "x5c":["${first.encodeBase64()}","${second.encodeBase64()}"]}""" val parsed = JwsHeader.deserialize(serialized) diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceTest.kt index f21dc2a70..cae823e9a 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceTest.kt @@ -25,7 +25,7 @@ class JwsServiceTest : FreeSpec({ "signed object with bytes can be verified" { val payload = randomPayload.encodeToByteArray() - val signed = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, payload) + val signed = jwsService.createSignedJwt(JwsContentType.JWT, payload) signed.shouldNotBeNull() val parsed = JwsSigned.parse(signed) @@ -38,7 +38,7 @@ class JwsServiceTest : FreeSpec({ "signed object can be verified" { val stringPayload = jsonSerializer.encodeToString(randomPayload) - val signed = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, stringPayload.encodeToByteArray()) + val signed = jwsService.createSignedJwt(JwsContentType.JWT, stringPayload.encodeToByteArray()) signed.shouldNotBeNull() val parsed = JwsSigned.parse(signed) @@ -79,10 +79,10 @@ class JwsServiceTest : FreeSpec({ "encrypted object can be decrypted" { val stringPayload = jsonSerializer.encodeToString(randomPayload) val encrypted = jwsService.encryptJweObject( - JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON, + JwsContentType.DIDCOMM_ENCRYPTED_JSON, stringPayload.encodeToByteArray(), - cryptoService.toJsonWebKey(), - JwsContentTypeConstants.DIDCOMM_PLAIN_JSON, + JsonWebKey.fromKeyId(cryptoService.keyId)!!, + JwsContentType.DIDCOMM_PLAIN_JSON, JweAlgorithm.ECDH_ES, JweEncryption.A256GCM, ) diff --git a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index d2cf5e5ca..b21aa5e76 100644 --- a/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/iosMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -51,6 +51,7 @@ import platform.CoreFoundation.CFDictionaryAddValue as CFDictionaryAddValue1 @Suppress("UNCHECKED_CAST") actual class DefaultCryptoService : CryptoService { + override val keyId: String override val jwsAlgorithm = JwsAlgorithm.ES256 private val privateKey: SecKeyRef private val publicKey: SecKeyRef @@ -66,6 +67,7 @@ actual class DefaultCryptoService : CryptoService { val publicKeyData = SecKeyCopyExternalRepresentation(publicKey, null) val data = CFBridgingRelease(publicKeyData) as NSData this.jsonWebKey = JsonWebKey.fromAnsiX963Bytes(JwkType.EC, EcCurve.SECP_256_R_1, data.toByteArray())!! + this.keyId = jsonWebKey.keyId!! } override suspend fun sign(input: ByteArray): KmmResult { @@ -173,12 +175,7 @@ actual class DefaultVerifierCryptoService : VerifierCryptoService { } } -} - -@Suppress("UNCHECKED_CAST") -actual object CryptoUtils { - - actual fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? { + override fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? { memScoped { val certData = CFBridgingRetain(toData(it)) as CFDataRef val certificate = SecCertificateCreateWithData(null, certData) @@ -188,7 +185,6 @@ actual object CryptoUtils { return JsonWebKey.fromAnsiX963Bytes(JwkType.EC, EcCurve.SECP_256_R_1, data.toByteArray()) } } - } data class DefaultEphemeralKeyHolder(val publicKey: SecKeyRef, val privateKey: SecKeyRef? = null) : EphemeralKeyHolder { diff --git a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt index 6832c8f3d..8d2db73f0 100644 --- a/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt +++ b/vclib/src/jvmMain/kotlin/at/asitplus/wallet/lib/agent/DefaultCryptoService.kt @@ -34,6 +34,7 @@ actual open class DefaultCryptoService : CryptoService { private val ecCurve: EcCurve = EcCurve.SECP_256_R_1 private val keyPair: KeyPair private val jsonWebKey: JsonWebKey + override val keyId: String actual constructor() { this.keyPair = KeyPairGenerator.getInstance("EC").also { it.initialize(ecCurve.keyLengthBits) }.genKeyPair() @@ -43,6 +44,7 @@ actual open class DefaultCryptoService : CryptoService { (keyPair.public as ECPublicKey).w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), (keyPair.public as ECPublicKey).w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) )!! + this.keyId = jsonWebKey.keyId!! } constructor(keyPair: KeyPair) { @@ -53,6 +55,7 @@ actual open class DefaultCryptoService : CryptoService { (keyPair.public as ECPublicKey).w.affineX.toByteArray().ensureSize(ecCurve.coordinateLengthBytes), (keyPair.public as ECPublicKey).w.affineY.toByteArray().ensureSize(ecCurve.coordinateLengthBytes) )!! + this.keyId = jsonWebKey.keyId!! } override val jwsAlgorithm = JwsAlgorithm.ES256 @@ -175,17 +178,12 @@ actual open class DefaultVerifierCryptoService : VerifierCryptoService { } } -} - -actual object CryptoUtils { - - actual fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? = try { + override fun extractPublicKeyFromX509Cert(it: ByteArray): JsonWebKey? = try { val pubKey = CertificateFactory.getInstance("X.509").generateCertificate(it.inputStream()).publicKey if (pubKey is ECPublicKey) JsonWebKey.fromJcaKey(pubKey, EcCurve.SECP_256_R_1) else null } catch (e: Throwable) { null } - } val JwsAlgorithm.jcaName diff --git a/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceJvmTest.kt b/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceJvmTest.kt index 4e92d1b5c..f1f8868a2 100644 --- a/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceJvmTest.kt +++ b/vclib/src/jvmTest/kotlin/at/asitplus/wallet/lib/jws/JwsServiceJvmTest.kt @@ -47,10 +47,8 @@ class JwsServiceJvmTest : FreeSpec({ "signed object from ext. library can be verified" { val stringPayload = jsonSerializer.encodeToString(randomPayload) - val libHeader = JWSHeader.Builder(JWSAlgorithm.ES256) - .type(JOSEObjectType("JWT")) - .keyID(cryptoService.toJsonWebKey().keyId!!) - .build() + val libHeader = + JWSHeader.Builder(JWSAlgorithm.ES256).type(JOSEObjectType("JWT")).keyID(cryptoService.keyId).build() val libObject = JWSObject(libHeader, Payload(stringPayload)).also { it.sign(ECDSASigner(keyPair.private as ECPrivateKey)) } @@ -66,7 +64,7 @@ class JwsServiceJvmTest : FreeSpec({ "signed object can be verified with ext. library" { val stringPayload = jsonSerializer.encodeToString(randomPayload) - val signed = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, stringPayload.encodeToByteArray()) + val signed = jwsService.createSignedJwt(JwsContentType.JWT, stringPayload.encodeToByteArray()) val parsed = JWSObject.parse(signed) parsed.shouldNotBeNull() @@ -79,9 +77,9 @@ class JwsServiceJvmTest : FreeSpec({ "encrypted object from ext. library can be decrypted" { val stringPayload = jsonSerializer.encodeToString(randomPayload) val libHeader = JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM) - .type(JOSEObjectType(JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON)) - .contentType(JwsContentTypeConstants.DIDCOMM_PLAIN_JSON) - .keyID(cryptoService.toJsonWebKey().keyId!!) + .type(JOSEObjectType(JwsContentType.DIDCOMM_ENCRYPTED_JSON.text)) + .contentType(JwsContentType.DIDCOMM_PLAIN_JSON.text) + .keyID(cryptoService.keyId) .build() val libObject = JWEObject(libHeader, Payload(stringPayload)).also { it.encrypt(ECDHEncrypter(keyPair.public as ECPublicKey)) @@ -98,10 +96,10 @@ class JwsServiceJvmTest : FreeSpec({ "encrypted object can be decrypted with ext. library" { val stringPayload = jsonSerializer.encodeToString(randomPayload) val encrypted = jwsService.encryptJweObject( - JwsContentTypeConstants.DIDCOMM_ENCRYPTED_JSON, + JwsContentType.DIDCOMM_ENCRYPTED_JSON, stringPayload.encodeToByteArray(), cryptoService.toJsonWebKey(), - JwsContentTypeConstants.DIDCOMM_PLAIN_JSON, + JwsContentType.DIDCOMM_PLAIN_JSON, JweAlgorithm.ECDH_ES, JweEncryption.A256GCM, )