diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Pollux.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Pollux.kt index 5d41da2fd..2d60e06e6 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Pollux.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Pollux.kt @@ -49,7 +49,7 @@ interface Pollux { * @param offerJson The JSON object representing the credential offer. * @return The string representation of the processed result. */ - fun processCredentialRequestJWT( + suspend fun processCredentialRequestJWT( subjectDID: DID, privateKey: PrivateKey, offerJson: JsonObject @@ -63,7 +63,7 @@ interface Pollux { * @param offerJson The JSON object representing the credential offer. * @return The string representation of the processed result. */ - fun processCredentialRequestSDJWT( + suspend fun processCredentialRequestSDJWT( subjectDID: DID, privateKey: PrivateKey, offerJson: JsonObject diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImpl.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImpl.kt index dbc7f9305..3f1c96a18 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImpl.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImpl.kt @@ -53,6 +53,8 @@ import org.hyperledger.identus.apollo.base64.base64UrlDecodedBytes import org.hyperledger.identus.apollo.utils.KMMECSecp256k1PublicKey import org.hyperledger.identus.walletsdk.apollo.helpers.gunzip import org.hyperledger.identus.walletsdk.apollo.utils.Secp256k1PrivateKey +import org.hyperledger.identus.walletsdk.castor.did.prismdid.PrismDIDPublicKey +import org.hyperledger.identus.walletsdk.castor.did.prismdid.defaultId import org.hyperledger.identus.walletsdk.domain.buildingblocks.Apollo import org.hyperledger.identus.walletsdk.domain.buildingblocks.Castor import org.hyperledger.identus.walletsdk.domain.buildingblocks.Pollux @@ -80,6 +82,7 @@ import org.hyperledger.identus.walletsdk.domain.models.httpClient import org.hyperledger.identus.walletsdk.domain.models.keyManagement.CurveKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.CurvePointXKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.CurvePointYKey +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.ExportableKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.KeyTypes import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PrivateKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PublicKey @@ -255,15 +258,14 @@ open class PolluxImpl( * @return The created verifiable presentation JWT. */ @Throws(PolluxError.NoDomainOrChallengeFound::class) - override fun processCredentialRequestJWT( + override suspend fun processCredentialRequestJWT( subjectDID: DID, privateKey: PrivateKey, offerJson: JsonObject ): String { - val parsedPrivateKey = parsePrivateKey(privateKey) val domain = getDomain(offerJson) ?: throw PolluxError.NoDomainOrChallengeFound() val challenge = getChallenge(offerJson) ?: throw PolluxError.NoDomainOrChallengeFound() - return signClaimsRequestCredentialJWT(subjectDID, parsedPrivateKey, domain, challenge) + return signClaimsRequestCredentialJWT(subjectDID, privateKey, domain, challenge) } /** @@ -276,15 +278,14 @@ open class PolluxImpl( * @return The created verifiable presentation JWT. */ @Throws(PolluxError.NoDomainOrChallengeFound::class) - override fun processCredentialRequestSDJWT( + override suspend fun processCredentialRequestSDJWT( subjectDID: DID, privateKey: PrivateKey, offerJson: JsonObject ): String { - val parsedPrivateKey = parsePrivateKey(privateKey) val domain = getDomain(offerJson) ?: throw PolluxError.NoDomainOrChallengeFound() val challenge = getChallenge(offerJson) ?: throw PolluxError.NoDomainOrChallengeFound() - return signClaimsRequestCredentialJWT(subjectDID, parsedPrivateKey, domain, challenge) + return signClaimsRequestCredentialJWT(subjectDID, privateKey, domain, challenge) } /** @@ -634,9 +635,9 @@ open class PolluxImpl( * @param challenge The challenge value for the JWT. * @return The signed JWT as a string. */ - private fun signClaimsRequestCredentialJWT( + private suspend fun signClaimsRequestCredentialJWT( subjectDID: DID, - privateKey: ECPrivateKey, + privateKey: PrivateKey, domain: String, challenge: String ): String { @@ -653,9 +654,9 @@ open class PolluxImpl( * @param challenge The challenge value for the JWT. * @return The signed JWT as a string. */ - internal fun signClaimsProofPresentationJWT( + private suspend fun signClaimsProofPresentationJWT( subjectDID: DID, - privateKey: ECPrivateKey, + privateKey: PrivateKey, credential: Credential, domain: String, challenge: String @@ -673,9 +674,9 @@ open class PolluxImpl( * @param challenge The challenge value for the JWT. * @return The signed JWT as a string. */ - internal fun signClaimsProofPresentationSDJWT( + internal suspend fun signClaimsProofPresentationSDJWT( subjectDID: DID, - privateKey: ECPrivateKey, + privateKey: PrivateKey, credential: Credential, domain: String, challenge: String @@ -693,13 +694,18 @@ open class PolluxImpl( * @param credential The optional credential to be included in the JWT. * @return The signed JWT as a string. */ - private fun signClaims( + internal suspend fun signClaims( subjectDID: DID, - privateKey: ECPrivateKey, + privateKey: PrivateKey, domain: String, challenge: String, credential: Credential? = null ): String { + if (privateKey !is ExportableKey) { + throw PolluxError.PrivateKeyTypeNotSupportedError("The private key should be ${ExportableKey::class.simpleName}") + } + val ecPrivateKey = parsePrivateKey(privateKey) + val presentation: MutableMap> = mutableMapOf( CONTEXT to setOf(CONTEXT_URL), TYPE to setOf(VERIFIABLE_PRESENTATION) @@ -715,14 +721,17 @@ open class PolluxImpl( .claim(VP, presentation) .build() + val kid = getSigningKid(subjectDID) + // Generate a JWS header with the ES256K algorithm val header = JWSHeader.Builder(JWSAlgorithm.ES256K) + .keyID(kid) .build() // Sign the JWT with the private key val jwsObject = SignedJWT(header, claims) val signer = ECDSASigner( - privateKey as java.security.PrivateKey, + ecPrivateKey as java.security.PrivateKey, com.nimbusds.jose.jwk.Curve.SECP256K1 ) val provider = BouncyCastleProviderSingleton.getInstance() @@ -919,10 +928,9 @@ open class PolluxImpl( val signedChallenge = privateKey.sign(jwtPresentationDefinitionRequest.options.challenge.encodeToByteArray()) - val ecPrivateKey = parsePrivateKey(privateKey) val presentationJwt = signClaimsProofPresentationJWT( subjectDID = DID(subject), - privateKey = ecPrivateKey, + privateKey = privateKey, credential = credential, domain = jwtPresentationDefinitionRequest.options.domain, challenge = jwtPresentationDefinitionRequest.options.challenge @@ -1294,4 +1302,23 @@ open class PolluxImpl( } return ecPublicKeys.toTypedArray() } + + /** + * Method to get the kId from the DID authentication property, Master key. + * + * @param did the DID to resolve + * @return The verification method id as a string or null. + */ + private suspend fun getSigningKid(did: DID): String? { + val didDocHolder = castor.resolveDID(did.toString()) + val authentication = didDocHolder.coreProperties.find { property -> + property::class == DIDDocument.Authentication::class + } + val verificationMethod = + (authentication as DIDDocument.Authentication).verificationMethods.find { verificationMethod -> + verificationMethod.id.did == did && verificationMethod.id.fragment == PrismDIDPublicKey.Usage.AUTHENTICATION_KEY.defaultId() + } + + return verificationMethod?.id?.string() + } } diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/PolluxMock.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/PolluxMock.kt index ea533e17e..223a5670f 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/PolluxMock.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/PolluxMock.kt @@ -35,11 +35,11 @@ class PolluxMock : Pollux { TODO("Not yet implemented") } - override fun processCredentialRequestJWT(subjectDID: DID, privateKey: PrivateKey, offerJson: JsonObject): String { + override suspend fun processCredentialRequestJWT(subjectDID: DID, privateKey: PrivateKey, offerJson: JsonObject): String { TODO("Not yet implemented") } - override fun processCredentialRequestSDJWT(subjectDID: DID, privateKey: PrivateKey, offerJson: JsonObject): String { + override suspend fun processCredentialRequestSDJWT(subjectDID: DID, privateKey: PrivateKey, offerJson: JsonObject): String { TODO("Not yet implemented") } diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImplTest.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImplTest.kt index c35f6f868..037aec172 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImplTest.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImplTest.kt @@ -20,7 +20,10 @@ import junit.framework.TestCase.assertTrue import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import org.didcommx.didcomm.common.Typ +import org.hyperledger.identus.apollo.base64.base64UrlDecoded import org.hyperledger.identus.apollo.derivation.MnemonicHelper import org.hyperledger.identus.walletsdk.apollo.ApolloImpl import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519KeyPair @@ -1154,6 +1157,34 @@ class PolluxImplTest { assertFalse(pollux.verifyStatusListIndexForEncodedList(encodedList, 3)) } + @Test + fun `Test signClaims for JWT including kid`() = runTest { + pollux = PolluxImpl(apollo, castor, api) + val keyPair = Secp256k1KeyPair.generateKeyPair() + + val did = + DID("did:prism:cd6cf9f94a43c53e286b0f2015c0083701350a694f52a22ee02e3bd29d93eba9:CrQBCrEBEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQKJIokEe_iKRGsr0f2EEa1JHGm59g0qP7QMtw6FcVxW9xJDCg9hdXRoZW50aWNhdGlvbjAQBEouCglzZWNwMjU2azESIQKJIokEe_iKRGsr0f2EEa1JHGm59g0qP7QMtw6FcVxW9xotCgojZGlkY29tbS0xEhBESURDb21tTWVzc2FnaW5nGg1kaWQ6cGVlcjp0ZXN0") + val domain = "domain" + val challenge = "challenge" + val credential = JWTCredential.fromJwtString( + "eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6cHJpc206ZTAyZTgwOTlkNTAzNTEzNDVjNWRkODMxYTllOTExMmIzOTRhODVkMDA2NGEyZWI1OTQyOTA4MDczNGExNTliNjpDcmtCQ3JZQkVqb0tCbUYxZEdndE1SQUVTaTRLQ1hObFkzQXlOVFpyTVJJaEF1Vlljb3JmV25MMGZZdEE1dmdKSzRfLW9iM2JVRGMtdzJVT0hkTzNRRXZxRWpzS0IybHpjM1ZsTFRFUUFrb3VDZ2x6WldOd01qVTJhekVTSVFMQ3U5Tm50cXVwQmotME5DZE1BNzV6UmVCZXlhQ0pPMWFHWWVQNEJNUUhWQkk3Q2dkdFlYTjBaWEl3RUFGS0xnb0pjMlZqY0RJMU5tc3hFaUVET1dndlF4NnZSdTZ3VWI0RlljSnVhRUNqOUJqUE1KdlJwOEx3TTYxaEVUNCIsInN1YiI6ImRpZDpwcmlzbTpiZDgxZmY1NDQzNDJjMTAwNDZkZmE0YmEyOTVkNWIzNmU0Y2ZlNWE3ZWIxMjBlMTBlZTVjMjQ4NzAwNjUxMDA5OkNvVUJDb0lCRWpzS0IyMWhjM1JsY2pBUUFVb3VDZ2x6WldOd01qVTJhekVTSVFQdjVQNXl5Z3Jad2FKbFl6bDU5bTJIQURLVFhVTFBzUmUwa2dlRUh2dExnQkpEQ2c5aGRYUm9aVzUwYVdOaGRHbHZiakFRQkVvdUNnbHpaV053TWpVMmF6RVNJUVB2NVA1eXlnclp3YUpsWXpsNTltMkhBREtUWFVMUHNSZTBrZ2VFSHZ0TGdBIiwibmJmIjoxNzE1MDAwNjc0LCJleHAiOjE3MTg2MDA2NzQsInZjIjp7ImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImVtYWlsQWRkcmVzcyI6ImNyaXN0aWFuLmNhc3Ryb0Bpb2hrLmlvIiwiaWQiOiJkaWQ6cHJpc206YmQ4MWZmNTQ0MzQyYzEwMDQ2ZGZhNGJhMjk1ZDViMzZlNGNmZTVhN2ViMTIwZTEwZWU1YzI0ODcwMDY1MTAwOTpDb1VCQ29JQkVqc0tCMjFoYzNSbGNqQVFBVW91Q2dselpXTndNalUyYXpFU0lRUHY1UDV5eWdyWndhSmxZemw1OW0ySEFES1RYVUxQc1JlMGtnZUVIdnRMZ0JKRENnOWhkWFJvWlc1MGFXTmhkR2x2YmpBUUJFb3VDZ2x6WldOd01qVTJhekVTSVFQdjVQNXl5Z3Jad2FKbFl6bDU5bTJIQURLVFhVTFBzUmUwa2dlRUh2dExnQSJ9LCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sIkBjb250ZXh0IjpbImh0dHBzOlwvXC93d3cudzMub3JnXC8yMDE4XC9jcmVkZW50aWFsc1wvdjEiXSwiY3JlZGVudGlhbFN0YXR1cyI6eyJzdGF0dXNQdXJwb3NlIjoiUmV2b2NhdGlvbiIsInN0YXR1c0xpc3RJbmRleCI6MjUsImlkIjoiaHR0cDpcL1wvMTAuOTEuMTAwLjEyNjo4MDAwXC9wcmlzbS1hZ2VudFwvY3JlZGVudGlhbC1zdGF0dXNcLzUxNGU4NTI4LTRiMzgtNDc3YS1iMGU0LTMyNGJiZTIyMDQ2NCMyNSIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwOlwvXC8xMC45MS4xMDAuMTI2OjgwMDBcL3ByaXNtLWFnZW50XC9jcmVkZW50aWFsLXN0YXR1c1wvNTE0ZTg1MjgtNGIzOC00NzdhLWIwZTQtMzI0YmJlMjIwNDY0In19fQ.5OmmL5tdcRKugiHVt01PJUhp9r22zgMPPALUOB41g_1_BPHE3ezqJ2QhSmScU_EOGYcKksHftdrvfoND65nSjw" + ) + val signedClaims = pollux.signClaims( + subjectDID = did, + privateKey = keyPair.privateKey, + domain = domain, + challenge = challenge, + credential = credential + ) + assertTrue(signedClaims.contains(".")) + val splits = signedClaims.split(".") + val header = splits[0].base64UrlDecoded + val json = Json.parseToJsonElement(header) + assertTrue(json.jsonObject.containsKey("kid")) + val kid = json.jsonObject["kid"]!!.jsonPrimitive.content + assertEquals("did:prism:cd6cf9f94a43c53e286b0f2015c0083701350a694f52a22ee02e3bd29d93eba9:CrQBCrEBEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQKJIokEe_iKRGsr0f2EEa1JHGm59g0qP7QMtw6FcVxW9xJDCg9hdXRoZW50aWNhdGlvbjAQBEouCglzZWNwMjU2azESIQKJIokEe_iKRGsr0f2EEa1JHGm59g0qP7QMtw6FcVxW9xotCgojZGlkY29tbS0xEhBESURDb21tTWVzc2FnaW5nGg1kaWQ6cGVlcjp0ZXN0#authentication0", kid) + } + private suspend fun createVerificationTestCase(testCaseOptions: VerificationTestCase): Triple { val currentDate = Calendar.getInstance() val nextMonthDate = currentDate.clone() as Calendar