Skip to content

Commit

Permalink
feat: pollus JWT to support KID parameter (#195)
Browse files Browse the repository at this point in the history
Signed-off-by: Cristian G <[email protected]>
  • Loading branch information
cristianIOHK authored Sep 16, 2024
1 parent 93d21c8 commit 8ad7a91
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

/**
Expand All @@ -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)
}

/**
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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<String, Collection<String>> = mutableMapOf(
CONTEXT to setOf(CONTEXT_URL),
TYPE to setOf(VERIFIABLE_PRESENTATION)
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String, String, String> {
val currentDate = Calendar.getInstance()
val nextMonthDate = currentDate.clone() as Calendar
Expand Down

0 comments on commit 8ad7a91

Please sign in to comment.