Skip to content

Commit

Permalink
feat(agent): validate JWT proof in the credential request, update dem… (
Browse files Browse the repository at this point in the history
#1165)

Signed-off-by: Yurii Shynbuiev <[email protected]>
Signed-off-by: Hyperledger Bot <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Hyperledger Bot <[email protected]>
  • Loading branch information
3 people authored and Pat Losoponkul committed Jun 13, 2024
1 parent dc94c10 commit 0a709e3
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package org.hyperledger.identus.oid4vci.controller

import org.hyperledger.identus.agent.server.config.AppConfig
import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext}
import org.hyperledger.identus.api.http.ErrorResponse.badRequest
import org.hyperledger.identus.api.http.ErrorResponse.internalServerError
import org.hyperledger.identus.api.http.{ErrorResponse, RequestContext}
import org.hyperledger.identus.api.util.PaginationUtils
import org.hyperledger.identus.castor.core.model.did.PrismDID
import org.hyperledger.identus.oid4vci.CredentialIssuerEndpoints.ExtendedErrorResponse
import org.hyperledger.identus.oid4vci.domain.Openid4VCIProofJwtOps
import org.hyperledger.identus.oid4vci.http.*
import org.hyperledger.identus.oid4vci.http.CredentialErrorCode.*
import org.hyperledger.identus.oid4vci.service.OIDCCredentialIssuerService
import org.hyperledger.identus.oid4vci.CredentialIssuerEndpoints.ExtendedErrorResponse
import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialIssuer as PolluxCredentialIssuer
import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataService
import org.hyperledger.identus.pollux.vc.jwt.JWT
import org.hyperledger.identus.shared.models.WalletAccessContext
import zio.{IO, URLayer, ZIO, ZLayer}

Expand Down Expand Up @@ -120,7 +122,8 @@ case class CredentialIssuerControllerImpl(
credentialIssuerService: OIDCCredentialIssuerService,
issuerMetadataService: OID4VCIIssuerMetadataService,
agentBaseUrl: URL
) extends CredentialIssuerController {
) extends CredentialIssuerController
with Openid4VCIProofJwtOps {

import CredentialIssuerController.Errors.*
import OIDCCredentialIssuerService.Errors.*
Expand Down Expand Up @@ -162,14 +165,15 @@ case class CredentialIssuerControllerImpl(
case Some(JwtProof(proofType, jwt)) =>
for {
_ <- ZIO
.ifZIO(credentialIssuerService.verifyJwtProof(jwt))(
.ifZIO(credentialIssuerService.verifyJwtProof(JWT(jwt)))(
ZIO.unit,
ZIO.fail(OIDCCredentialIssuerService.Errors.InvalidProof("Invalid proof"))
)
.mapError { case InvalidProof(message) =>
badRequestInvalidProof(jwt, message)
}
nonce = "todo - extract jwt nonce" // TODO: validate proof and extract nonce
nonce <- getNonceFromJwt(JWT(jwt))
.mapError(throwable => badRequestInvalidProof(jwt, throwable.getMessage))
session <- credentialIssuerService
.getIssuanceSessionByNonce(nonce)
.mapError(_ => badRequestInvalidProof(jwt, "nonce is not associated to the issuance session"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.hyperledger.identus.oid4vci.domain

import com.nimbusds.jose.{JOSEObjectType, JWSAlgorithm, JWSHeader, JWSObject, JWSSigner, Payload}
import org.hyperledger.identus.castor.core.model.did.LongFormPrismDID
import org.hyperledger.identus.pollux.vc.jwt.{DidResolver, JWT}
import org.hyperledger.identus.pollux.vc.jwt.JwtSignerImplicits.*
import org.hyperledger.identus.shared.crypto.Secp256k1PrivateKey
import zio.Task
import zio.ZIO

import java.util.UUID
import scala.jdk.CollectionConverters.*
import scala.util.Try

trait Openid4VCIProofJwtOps {

val jwtTypeName = "openid4vci-proof+jwt"

val supportedAlgorithms: Set[String] = Set("ES256K")

private def buildHeaderFromLongFormDid(longFormDID: LongFormPrismDID) = {
new JWSHeader.Builder(JWSAlgorithm.ES256K)
.keyID(longFormDID.toString)
.`type`(new JOSEObjectType(jwtTypeName))
.build()
}

private def buildPayload(nonce: String, aud: UUID, iat: Int) = {
new Payload(Map("nonce" -> nonce, "aud" -> aud.toString, "iat" -> iat).asJava)
}

private def makeJwtProof(header: JWSHeader, payload: Payload, signer: JWSSigner): String = {
val jwt = new JWSObject(header, payload)
jwt.sign(signer)
jwt.serialize()
}

def makeJwtProof(
longFormPrismDID: LongFormPrismDID,
nonce: String,
aud: UUID,
iat: Int,
privateKey: Secp256k1PrivateKey
): JWT = {
val header = buildHeaderFromLongFormDid(longFormPrismDID)
val payload = buildPayload(nonce, aud, iat)
JWT(makeJwtProof(header, payload, privateKey.asJwtSigner))
}

def getKeyIdFromJwt(jwt: JWT): Task[String] = {
for {
jwsObject <- ZIO.fromTry(Try(JWSObject.parse(jwt.value)))
keyID = jwsObject.getHeader.getKeyID
_ <- ZIO.fail(new Exception("Key ID not found in JWT header")) when (keyID == null || keyID.isEmpty)
} yield keyID
}

def getNonceFromJwt(jwt: JWT): Task[String] = {
for {
jwsObject <- ZIO.fromTry(Try(JWSObject.parse(jwt.value)))
payload = jwsObject.getPayload.toJSONObject
nonce = payload.get("nonce").asInstanceOf[String]
_ <- ZIO.fail(new Exception("Nonce not found in JWT payload")) when (nonce == null || nonce.isEmpty)
} yield nonce
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import org.hyperledger.identus.oid4vci.domain.IssuanceSession
import org.hyperledger.identus.oid4vci.http.*
import org.hyperledger.identus.oid4vci.storage.IssuanceSessionStorage
import org.hyperledger.identus.pollux.core.service.CredentialService
import org.hyperledger.identus.pollux.vc.jwt.{DID, Issuer, JWT, JwtCredential, W3cCredentialPayload}
import org.hyperledger.identus.pollux.vc.jwt.{DID, Issuer, JWT, JWTVerification, JwtCredential, W3cCredentialPayload}
import org.hyperledger.identus.pollux.vc.jwt.DidResolver
import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId}
import zio.*

Expand All @@ -24,7 +25,7 @@ trait OIDCCredentialIssuerService {
import OIDCCredentialIssuerService.Error
import OIDCCredentialIssuerService.Errors.*

def verifyJwtProof(jwt: String): IO[InvalidProof, Boolean]
def verifyJwtProof(jwt: JWT): IO[InvalidProof, Boolean]

def validateCredentialDefinition(
credentialDefinition: CredentialDefinition
Expand Down Expand Up @@ -71,13 +72,25 @@ object OIDCCredentialIssuerService {
case class OIDCCredentialIssuerServiceImpl(
didNonSecretStorage: DIDNonSecretStorage,
credentialService: CredentialService,
issuanceSessionStorage: IssuanceSessionStorage
issuanceSessionStorage: IssuanceSessionStorage,
didResolver: DidResolver
) extends OIDCCredentialIssuerService {

import OIDCCredentialIssuerService.Error
import OIDCCredentialIssuerService.Errors.*
override def verifyJwtProof(jwt: String): IO[InvalidProof, Boolean] = {
ZIO.succeed(true) // TODO: implement

override def verifyJwtProof(jwt: JWT): IO[InvalidProof, Boolean] = {
for {
verifiedJwtSignature <- JWTVerification
.validateEncodedJwtWithKeyId(
jwt,
proofPurpose = Some(VerificationRelationship.AssertionMethod),
didResolver = didResolver
)
.mapError(InvalidProof.apply)
_ <- verifiedJwtSignature.toZIO.mapError(InvalidProof.apply)
_ <- ZIO.succeed(println(s"JWT proof is verified: ${jwt.value}"))
} yield true
}

override def validateCredentialDefinition(
Expand Down Expand Up @@ -213,8 +226,8 @@ case class OIDCCredentialIssuerServiceImpl(

object OIDCCredentialIssuerServiceImpl {
val layer: URLayer[
DIDNonSecretStorage & CredentialService & IssuanceSessionStorage,
DIDNonSecretStorage & CredentialService & IssuanceSessionStorage & DidResolver,
OIDCCredentialIssuerService
] =
ZLayer.fromFunction(OIDCCredentialIssuerServiceImpl(_, _, _))
ZLayer.fromFunction(OIDCCredentialIssuerServiceImpl(_, _, _, _))
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package org.hyperledger.identus.oid4vci.domain

import com.nimbusds.jose.*
import com.nimbusds.jose.crypto.*
import com.nimbusds.jose.jwk.*
import com.nimbusds.jose.jwk.gen.*
import org.hyperledger.identus.agent.walletapi.memory.GenericSecretStorageInMemory
import org.hyperledger.identus.agent.walletapi.service.{ManagedDIDService, MockManagedDIDService}
import org.hyperledger.identus.agent.walletapi.storage.{DIDNonSecretStorage, MockDIDNonSecretStorage}
import org.hyperledger.identus.castor.core.model.did.VerificationRelationship
import org.hyperledger.identus.castor.core.model.did.{PrismDID, VerificationRelationship}
import org.hyperledger.identus.castor.core.service.{DIDService, MockDIDService}
import org.hyperledger.identus.oid4vci.http.{ClaimDescriptor, CredentialDefinition, Localization}
import org.hyperledger.identus.oid4vci.service.{OIDCCredentialIssuerService, OIDCCredentialIssuerServiceImpl}
Expand All @@ -14,14 +18,23 @@ import org.hyperledger.identus.pollux.core.repository.{
CredentialStatusListRepositoryInMemory
}
import org.hyperledger.identus.pollux.core.service.*
import org.hyperledger.identus.pollux.vc.jwt.PrismDidResolver
import org.hyperledger.identus.pollux.vc.jwt.{ES256KSigner, PrismDidResolver}
import org.hyperledger.identus.pollux.vc.jwt.JWT
import org.hyperledger.identus.shared.models.WalletId
import zio.{URLayer, ZIO, ZLayer}
import zio.mock.MockSpecDefault
import zio.test.*
import zio.test.Assertion.*
import zio.{URLayer, ZIO, ZLayer}
import zio.Clock
import zio.Random

object OIDCCredentialIssuerServiceSpec extends MockSpecDefault with CredentialServiceSpecHelper {
import java.util.UUID
import scala.jdk.CollectionConverters._

object OIDCCredentialIssuerServiceSpec
extends MockSpecDefault
with CredentialServiceSpecHelper
with Openid4VCIProofJwtOps {

val layers: URLayer[
DIDService & ManagedDIDService & DIDNonSecretStorage,
Expand All @@ -44,17 +57,21 @@ object OIDCCredentialIssuerServiceSpec extends MockSpecDefault with CredentialSe
)

override def spec = suite("CredentialServiceImpl")(
OIDCCredentialIssuerServiceSpec
OIDCCredentialIssuerServiceSpec,
validateProofSpec
)

private val (issuerOp, issuerKp, issuerDidMetadata, issuerDidData) =
MockDIDService.createDID(VerificationRelationship.AssertionMethod)

// private val (holderOp, holderKp, holderDidMetadata, holderDidData) =
// MockDIDService.createDID(VerificationRelationship.Authentication)
//
// private val holderDidServiceExpectations =
// MockDIDService.resolveDIDExpectation(holderDidMetadata, holderDidData)
private val (holderOp, holderKp, holderDidMetadata, holderDidData) =
MockDIDService.createDID(VerificationRelationship.AssertionMethod)

private val holderDidServiceExpectations =
MockDIDService.resolveDIDExpectation(holderDidMetadata, holderDidData)

private val holderManagedDIDServiceExpectations =
MockManagedDIDService.javaKeyPairWithDIDExpectation(holderKp)

private val issuerDidServiceExpectations =
MockDIDService.resolveDIDExpectation(issuerDidMetadata, issuerDidData)
Expand All @@ -65,6 +82,36 @@ object OIDCCredentialIssuerServiceSpec extends MockSpecDefault with CredentialSe
private val getIssuerPrismDidWalletIdExpectation =
MockDIDNonSecretStorage.getPrismDidWalletIdExpectation(issuerDidData.id, WalletId.default)

private def buildJwtProof(nonce: String, aud: UUID, iat: Int) = {
import org.bouncycastle.util.encoders.Hex

val longFormDid = PrismDID.buildLongFormFromOperation(holderOp)

val encodedKey = Hex.toHexString(holderKp.privateKey.getEncoded)
println(s"Private Key: $encodedKey")
println("Long Form DID: " + longFormDid.toString)

makeJwtProof(longFormDid, nonce, aud, iat, holderKp.privateKey)
}

private val validateProofSpec = suite("Validate holder's proof of possession using the LongFormPrismDID")(
test("should validate the holder's proof of possession using the LongFormPrismDID") {
for {
credentialIssuer <- ZIO.service[OIDCCredentialIssuerService]
nonce <- Random.nextString(10)
aud <- Random.nextUUID
iat <- Clock.instant.map(_.getEpochSecond.toInt)
jwt = buildJwtProof(nonce, aud, iat)
result <- credentialIssuer.verifyJwtProof(jwt)
} yield assert(result)(equalTo(true))
}.provideSomeLayer(
holderDidServiceExpectations.toLayer ++
MockManagedDIDService.empty ++
// holderManagedDIDServiceExpectations.toLayer ++
MockDIDNonSecretStorage.empty >+> layers
)
)

private val OIDCCredentialIssuerServiceSpec =
suite("Simple JWT credential issuance")(
test("should issue a JWT credential") {
Expand Down
4 changes: 2 additions & 2 deletions examples/st-oid4vci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

Example of the script to install the required packages in a virtual environment:
```shell
python -m venv {path-to-the-project-dir}/open-enterprise-agent/examples/st-oid4vci/python-env
source {path-to-the-project-dir}/open-enterprise-agent/examples/st-oid4vci/python-env/bin/activate
python -m venv {path-to-the-project-dir}/identus-cloud-agent/examples/st-oid4vci/python-env
source {path-to-the-project-dir}/identus-cloud-agent/examples/st-oid4vci/python-env/bin/activate
pip install requests pyjwt cryptography
```

Expand Down
15 changes: 13 additions & 2 deletions examples/st-oid4vci/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import urllib

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization


MOCKSERVER_URL = "http://localhost:7777"
Expand All @@ -18,6 +19,11 @@

ALICE_CLIENT_ID = "alice-wallet"

HOLDER_LONG_FORM_DID = "did:prism:73196107e806b084d44339c847a3ae8dd279562f23895583f62cc91a2ee5b8fe:CnsKeRI8CghtYXN0ZXItMBABSi4KCXNlY3AyNTZrMRIhArrplJNfQYxthryRU87XdODy-YWUh5mqrvIfAdoZFeJBEjkKBWtleS0wEAJKLgoJc2VjcDI1NmsxEiEC8rsFplfYvRLazdWWi3LNR1gaAQXb-adVhZacJT4ntwE"
HOLDER_ASSERTION_PRIVATE_KEY_HEX = (
"2902637d412190fb08f5d0e0b2efc1eefae8060ae151e7951b69afbecbdd452e"
)


def prepare_mock_server():
# reset mock server
Expand Down Expand Up @@ -152,6 +158,7 @@ def holder_get_issuer_as_metadata(authorization_server: str):
metadata_url = f"{authorization_server}/.well-known/openid-configuration"
response = requests.get(metadata_url)
metadata = response.json()
print(f"Metadata: {metadata}")
return metadata


Expand Down Expand Up @@ -221,11 +228,15 @@ def holder_get_credential(credential_endpoint: str, token_response):
c_nonce = token_response["c_nonce"]

# generate proof
private_key = ec.generate_private_key(ec.SECP256K1())
private_key_bytes = bytes.fromhex(HOLDER_ASSERTION_PRIVATE_KEY_HEX)
private_key = ec.derive_private_key(
int.from_bytes(private_key_bytes, "big"), ec.SECP256K1()
)
public_key = private_key.public_key()
jwt_proof = jwt.encode(
headers={
"typ": "openid4vci-proof+jwt",
"kid": "did:prism:0000000000000000000000000000000000000000000000000000000000000000#key-1", # TODO: use actual DID
"kid": HOLDER_LONG_FORM_DID,
},
payload={
"iss": ALICE_CLIENT_ID,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package org.hyperledger.identus.pollux.vc.jwt

import com.nimbusds.jose.{JOSEObjectType, JWSAlgorithm, JWSHeader}
import com.nimbusds.jose.crypto.{ECDSASigner, Ed25519Signer}
import com.nimbusds.jose.crypto.{ECDSASigner, ECDSAVerifier, Ed25519Signer}
import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton
import com.nimbusds.jose.jwk.{Curve, ECKey, OctetKeyPair}
import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT}
import io.circe.*
import org.hyperledger.identus.shared.crypto.Ed25519KeyPair
import org.hyperledger.identus.shared.crypto.{
Ed25519KeyPair,
Ed25519PrivateKey,
Ed25519PublicKey,
Secp256k1PrivateKey,
Secp256k1PublicKey
}
import zio.*

import java.security.*
Expand All @@ -22,6 +28,19 @@ object JWT {
}
}

object JwtSignerImplicits {
import com.nimbusds.jose.JWSSigner

implicit class JwtSignerProviderSecp256k1(secp256k1PrivateKey: Secp256k1PrivateKey) {
def asJwtSigner: JWSSigner = {
val ecdsaSigner = ECDSASigner(secp256k1PrivateKey.toJavaPrivateKey, Curve.SECP256K1)
val bouncyCastleProvider = BouncyCastleProviderSingleton.getInstance
ecdsaSigner.getJCAContext.setProvider(bouncyCastleProvider)
ecdsaSigner
}
}
}

trait Signer {
def encode(claim: Json): JWT

Expand Down
Loading

0 comments on commit 0a709e3

Please sign in to comment.