Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add claims validation when creating oid4vci credential offer #1178

Merged
merged 13 commits into from
Jun 13, 2024
3 changes: 0 additions & 3 deletions .mega-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ DISABLE_LINTERS:
- PYTHON_MYPY
- PYTHON_PYRIGHT
- PYTHON_RUFF
# TODO: revert before merging to `main`. Disabled to ease the development of keycloak extension
- JAVA_CHECKSTYLE
- JAVA_PMD

DISABLE_ERRORS_LINTERS:
- KOTLIN_KTLINT
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package org.hyperledger.identus.oid4vci.domain

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

import java.util.UUID
import scala.jdk.CollectionConverters.*
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,22 @@ import org.hyperledger.identus.castor.core.model.did.{DID, PrismDID, Verificatio
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.{Issuer, JWT, JWTVerification, JwtCredential, W3cCredentialPayload}
import org.hyperledger.identus.pollux.vc.jwt.DID as PolluxDID
import org.hyperledger.identus.pollux.vc.jwt.DidResolver
import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema
import org.hyperledger.identus.pollux.core.service.{
CredentialService,
OID4VCIIssuerMetadataService,
OID4VCIIssuerMetadataServiceError,
URIDereferencer
}
import org.hyperledger.identus.pollux.vc.jwt.{
DID as PolluxDID,
DidResolver,
Issuer,
JWT,
JWTVerification,
JwtCredential,
W3cCredentialPayload
}
import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId}
import zio.*

Expand Down Expand Up @@ -67,6 +79,16 @@ object OIDCCredentialIssuerService {

case class DIDResolutionError(message: String) extends Error

case class CredentialConfigurationNotFound(issuerId: UUID, credentialConfigurationId: String) extends Error {
override def message: String =
s"Credential configuration with id $credentialConfigurationId not found for issuer $issuerId"
}

case class CredentialSchemaError(cause: org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError)
extends Error {
override def message: String = cause.message
}

case class ServiceError(message: String) extends Error

case class UnexpectedError(cause: Throwable) extends Error {
Expand All @@ -78,8 +100,10 @@ object OIDCCredentialIssuerService {
case class OIDCCredentialIssuerServiceImpl(
didNonSecretStorage: DIDNonSecretStorage,
credentialService: CredentialService,
issuerMetadataService: OID4VCIIssuerMetadataService,
issuanceSessionStorage: IssuanceSessionStorage,
didResolver: DidResolver
didResolver: DidResolver,
uriDereferencer: URIDereferencer,
) extends OIDCCredentialIssuerService {

import OIDCCredentialIssuerService.Error
Expand All @@ -95,7 +119,6 @@ case class OIDCCredentialIssuerServiceImpl(
)
.mapError(InvalidProof.apply)
_ <- verifiedJwtSignature.toZIO.mapError(InvalidProof.apply)
_ <- ZIO.succeed(println(s"JWT proof is verified: ${jwt.value}"))
} yield true
}

Expand All @@ -111,7 +134,7 @@ case class OIDCCredentialIssuerServiceImpl(
claims: zio.json.ast.Json,
credentialIdentifier: Option[String],
credentialDefinition: CredentialDefinition
): IO[OIDCCredentialIssuerService.Error, JWT] = {
): IO[Error, JWT] = {
for {
wac <- didNonSecretStorage
.getPrismDidWalletId(issuingDID)
Expand Down Expand Up @@ -181,7 +204,7 @@ case class OIDCCredentialIssuerServiceImpl(

override def getIssuanceSessionByIssuerState(
issuerState: String
): IO[OIDCCredentialIssuerService.Error, IssuanceSession] =
): IO[Error, IssuanceSession] =
issuanceSessionStorage
.getByIssuerState(issuerState)
.mapError(e => ServiceError(s"Failed to get issuance session: ${e.message}"))
Expand All @@ -193,9 +216,17 @@ case class OIDCCredentialIssuerServiceImpl(
credentialConfigurationId: String,
issuingDID: PrismDID,
claims: zio.json.ast.Json
): ZIO[WalletAccessContext, OIDCCredentialIssuerService.Error, CredentialOffer] =
// TODO: validate claims with credential schema
): ZIO[WalletAccessContext, Error, CredentialOffer] =
for {
schemaId <- issuerMetadataService
.getCredentialConfigurationById(issuerId, credentialConfigurationId)
.mapError { case _: OID4VCIIssuerMetadataServiceError.CredentialConfigurationNotFound =>
CredentialConfigurationNotFound(issuerId, credentialConfigurationId)
}
.map(_.schemaId)
_ <- CredentialSchema
.validateJWTCredentialSubject(schemaId.toString(), simpleZioToCirce(claims).noSpaces, uriDereferencer)
.mapError(e => CredentialSchemaError(e))
session <- buildNewIssuanceSession(issuerId, issuingDID, claims)
_ <- issuanceSessionStorage
.start(session)
Expand Down Expand Up @@ -247,8 +278,9 @@ case class OIDCCredentialIssuerServiceImpl(

object OIDCCredentialIssuerServiceImpl {
val layer: URLayer[
DIDNonSecretStorage & CredentialService & IssuanceSessionStorage & DidResolver,
DIDNonSecretStorage & CredentialService & IssuanceSessionStorage & DidResolver & URIDereferencer &
OID4VCIIssuerMetadataService,
OIDCCredentialIssuerService
] =
ZLayer.fromFunction(OIDCCredentialIssuerServiceImpl(_, _, _, _))
ZLayer.fromFunction(OIDCCredentialIssuerServiceImpl(_, _, _, _, _, _))
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.hyperledger.identus.oid4vci.domain

import com.nimbusds.jose.*
import com.nimbusds.jose.jwk.*
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}
Expand All @@ -10,21 +9,25 @@ 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}
import org.hyperledger.identus.oid4vci.storage.InMemoryIssuanceSessionService
import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialConfiguration
import org.hyperledger.identus.pollux.core.model.CredentialFormat
import org.hyperledger.identus.pollux.core.repository.{
CredentialRepository,
CredentialRepositoryInMemory,
CredentialStatusListRepositoryInMemory
}
import org.hyperledger.identus.pollux.core.service.*
import org.hyperledger.identus.pollux.vc.jwt.PrismDidResolver
import org.hyperledger.identus.shared.models.WalletId
import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId}
import zio.{Clock, Random, URLayer, ZIO, ZLayer}
import zio.json.*
import zio.json.ast.Json
import zio.mock.MockSpecDefault
import zio.test.*
import zio.test.Assertion.*

import java.net.URI
import java.time.Instant
import java.util.UUID
import scala.util.Try

Expand All @@ -34,11 +37,11 @@ object OIDCCredentialIssuerServiceSpec
with Openid4VCIProofJwtOps {

val layers: URLayer[
DIDService & ManagedDIDService & DIDNonSecretStorage,
DIDService & ManagedDIDService & DIDNonSecretStorage & OID4VCIIssuerMetadataService,
CredentialService & CredentialDefinitionService & OIDCCredentialIssuerService
] =
ZLayer.makeSome[
DIDService & ManagedDIDService & DIDNonSecretStorage,
DIDService & ManagedDIDService & DIDNonSecretStorage & OID4VCIIssuerMetadataService,
CredentialService & CredentialDefinitionService & OIDCCredentialIssuerService
](
InMemoryIssuanceSessionService.layer,
Expand All @@ -54,11 +57,11 @@ object OIDCCredentialIssuerServiceSpec
)

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

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

private val (holderOp, holderKp, holderDidMetadata, holderDidData) =
Expand All @@ -67,27 +70,27 @@ object OIDCCredentialIssuerServiceSpec
private val holderDidServiceExpectations =
MockDIDService.resolveDIDExpectation(holderDidMetadata, holderDidData)

private val holderManagedDIDServiceExpectations =
MockManagedDIDService.javaKeyPairWithDIDExpectation(holderKp)

private val issuerDidServiceExpectations =
MockDIDService.resolveDIDExpectation(issuerDidMetadata, issuerDidData)

private val issuerManagedDIDServiceExpectations =
MockManagedDIDService.javaKeyPairWithDIDExpectation(issuerKp)

private val getIssuerPrismDidWalletIdExpectation =
private val getIssuerPrismDidWalletIdExpectations =
MockDIDNonSecretStorage.getPrismDidWalletIdExpectation(issuerDidData.id, WalletId.default)

private def buildJwtProof(nonce: String, aud: UUID, iat: Int) = {
import org.bouncycastle.util.encoders.Hex
private val getCredentialConfigurationExpectations =
MockOID4VCIIssuerMetadataService.getCredentialConfigurationByIdExpectations(
CredentialConfiguration(
configurationId = "DrivingLicense",
format = CredentialFormat.JWT,
schemaId = URI("resource:///vc-schema-example.json"),
createdAt = Instant.EPOCH
)
)

private def buildJwtProof(nonce: String, aud: UUID, iat: Int) = {
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)
}

Expand All @@ -101,15 +104,16 @@ object OIDCCredentialIssuerServiceSpec
jwt = buildJwtProof(nonce, aud, iat)
result <- credentialIssuer.verifyJwtProof(jwt)
} yield assert(result)(equalTo(true))
}.provideSomeLayer(
holderDidServiceExpectations.toLayer ++
MockManagedDIDService.empty ++
// holderManagedDIDServiceExpectations.toLayer ++
MockDIDNonSecretStorage.empty >+> layers
}.provide(
holderDidServiceExpectations.toLayer,
MockManagedDIDService.empty,
MockDIDNonSecretStorage.empty,
MockOID4VCIIssuerMetadataService.empty,
layers
)
)

private val OIDCCredentialIssuerServiceSpec =
private val oid4vciCredentialIssuerServiceSpec =
suite("Simple JWT credential issuance")(
test("should issue a JWT credential") {
for {
Expand Down Expand Up @@ -143,10 +147,68 @@ object OIDCCredentialIssuerServiceSpec
// assert(jwtObject.getHeader.getKeyID)(equalTo(issuerDidData.id.toString)) && //TODO: add key ID to the header
assert(jwtObject.getHeader.getAlgorithm)(equalTo(JWSAlgorithm.ES256K)) &&
assert(name)(equalTo("Alice"))
}.provideSomeLayer(
issuerDidServiceExpectations.toLayer ++
issuerManagedDIDServiceExpectations.toLayer ++
getIssuerPrismDidWalletIdExpectation.toLayer >+> layers
}.provide(
issuerDidServiceExpectations.toLayer,
issuerManagedDIDServiceExpectations.toLayer,
getIssuerPrismDidWalletIdExpectations.toLayer,
MockOID4VCIIssuerMetadataService.empty,
layers
),
test("create credential-offer with valid claims") {
val wac = ZLayer.succeed(WalletAccessContext(WalletId.random))
val claims = Json(
"credentialSubject" -> Json.Obj(
"emailAddress" -> Json.Str("[email protected]"),
"givenName" -> Json.Str("Alice"),
"familyName" -> Json.Str("Wonderland"),
"dateOfIssuance" -> Json.Str("2000-01-01T10:00:00Z"),
"drivingLicenseID" -> Json.Str("12345"),
"drivingClass" -> Json.Num(5),
)
)
for {
oidcCredentialIssuerService <- ZIO.service[OIDCCredentialIssuerService]
offer <- oidcCredentialIssuerService
.createCredentialOffer(
URI("http://example.com").toURL(),
UUID.randomUUID(),
"DrivingLicense",
issuerDidData.id,
claims,
)
.provide(wac)
issuerState = offer.grants.get.authorization_code.issuer_state.get
session <- oidcCredentialIssuerService.getIssuanceSessionByIssuerState(issuerState)
} yield assert(session.claims)(equalTo(claims))
}.provide(
MockDIDService.empty,
MockManagedDIDService.empty,
MockDIDNonSecretStorage.empty,
getCredentialConfigurationExpectations.toLayer,
layers
),
test("reject credential-offer when created with invalid claims") {
val wac = ZLayer.succeed(WalletAccessContext(WalletId.random))
val claims = Json("credentialSubject" -> Json.Obj("foo" -> Json.Str("bar")))
for {
oidcCredentialIssuerService <- ZIO.service[OIDCCredentialIssuerService]
exit <- oidcCredentialIssuerService
.createCredentialOffer(
URI("http://example.com").toURL(),
UUID.randomUUID(),
"DrivingLicense",
issuerDidData.id,
claims,
)
.provide(wac)
.exit
} yield assert(exit)(failsWithA[OIDCCredentialIssuerService.Errors.CredentialSchemaError])
}.provide(
MockDIDService.empty,
MockManagedDIDService.empty,
MockDIDNonSecretStorage.empty,
getCredentialConfigurationExpectations.toLayer,
layers
)
)
}
Loading
Loading