Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/prism-integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: PRISM Integration Tests

concurrency:
group: ${{ github.head_ref }}${{ github.ref }}-prism-integration-tests
cancel-in-progress: true

on:
pull_request:
push:
branches:
- "main"
workflow_dispatch:

defaults:
run:
shell: bash

permissions:
contents: read

jobs:
prism-integration:
name: Run PRISM integration tests
runs-on: ubuntu-latest
permissions:
contents: read
env:
MEDIATOR_PRISM_E2E_ENABLED: "true"
NEOPRISM_VERSION: "0.14.1"
NEOPRISM_BASE_URL: "http://127.0.0.1:18081"
MEDIATOR_PRISM_MONGO_URI: "mongodb://127.0.0.1:27018/messages"
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Checkout mediator
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Java and Scala
uses: olafurpg/setup-scala@32ffa16635ff8f19cc21ea253a987f0fdf29844c # v14
with:
java-version: openjdk@1.17

- name: Start NeoPRISM dev node
run: |
docker run --rm -d \
--name neoprism \
-p 18081:8080 \
-e NPRISM_DB_URL=sqlite::memory: \
hyperledgeridentus/identus-neoprism:${NEOPRISM_VERSION} dev

- name: Start MongoDB
run: |
docker run --rm -d \
--name mediator-prism-mongo \
-p 27018:27017 \
mongo:7

- name: Wait for NeoPRISM
run: |
for i in {1..60}; do
if curl --fail --silent "${NEOPRISM_BASE_URL}/api/_system/health" > /dev/null; then
exit 0
fi
sleep 1
done
echo "NeoPRISM failed to become healthy" >&2
docker logs neoprism || true
exit 1

- name: Wait for MongoDB
run: |
for i in {1..60}; do
if docker exec mediator-prism-mongo mongosh --quiet --eval 'db.runCommand({ ping: 1 }).ok' | grep -qx '1'; then
exit 0
fi
sleep 1
done
echo "MongoDB failed to become healthy" >&2
docker logs mediator-prism-mongo || true
exit 1

- name: Run mediator PRISM integration test
run: |
sbt -mem 2048 -J-Xmx5120m "mediator/testOnly org.hyperledger.identus.mediator.prism.MediatorPrismE2ESpec"

- name: Show NeoPRISM logs on failure
if: failure()
run: docker logs neoprism || true

- name: Show MongoDB logs on failure
if: failure()
run: docker logs mediator-prism-mongo || true
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ The mediator serves on port 8080 by default (`mediator.server.http.port` in `app
| `MONGODB_DB_NAME` | Database name | `mediator` |
| `PORT` | HTTP server port override | `8080` |
| `ESCALATE_TO` | Problem report escalation email | `atala@iohk.io` |
| `DID_PRISM_RESOLVER` | Base URL for resolving `did:prism` DID documents | Default in `application.conf` |

## MongoDB Dependency

Expand Down Expand Up @@ -124,7 +125,7 @@ The mediator identity requires two OKP key pairs in JOSE (JWK) format:
3. Generate Ed25519 key: `openssl genpkey -algorithm Ed25519 -out private_key_ed25519.pem`
4. Format to JWK similarly for `d` and `x` fields

The keys are set via environment variables (`KEY_AGREEMENT_D`, `KEY_AGREEMENT_X`, `KEY_AUTHENTICATION_D`, `KEY_AUTHENTICATION_X`). The mediator builds a `did:peer:2` DID from these keys and the service endpoints at startup (see `MediatorStandalone.scala` → `MediatorConfig.did`).
The keys are set via environment variables (`KEY_AGREEMENT_D`, `KEY_AGREEMENT_X`, `KEY_AUTHENTICATION_D`, `KEY_AUTHENTICATION_X`). By default, the mediator builds a `did:peer:2` DID from these keys and the service endpoints at startup. Operators can also provide an explicit DID plus `keyStore` directly in `application.conf`, which enables mediator identities such as `did:prism`.

**⚠️ Never use the demo keys from `build.sbt` in production.**

Expand All @@ -135,7 +136,7 @@ The keys are set via environment variables (`KEY_AGREEMENT_D`, `KEY_AGREEMENT_X`
`MediatorStandalone` (`mediator/src/main/scala/.../MediatorStandalone.scala`) — ZIO application that:

1. Loads HOCON config via `zio-config-typesafe` + `zio-config-magnolia`
2. Constructs a `did:peer:2` DID from the configured keys + endpoints
2. Constructs a `did:peer:2` DID from the configured keys + endpoints, or uses an explicitly configured DID + `keyStore`
3. Wires ZIO layers: `ReactiveMongoApi` → repos (`UserAccountRepo`, `MessageItemRepo`, `OutboxMessageRepo`) → `OperatorImp` → protocol handlers
4. Starts ZIO HTTP server on configured port (default 8080)

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,17 @@ To set up the mediator identity:

[How to generate mediator identity](./mediator-identity-key-generation.md)

By default, the mediator still builds a `did:peer:2` DID from the configured keys and service endpoints.
Optionally, operators can provide an explicit DID and `keyStore` directly in `application.conf`, which enables using
other DID methods such as `did:prism`.

- `KEY_AGREEMENT_D` - is the key agreement private key (MUST be a X25519 OKP key type).
- `KEY_AGREEMENT_X` - is the key agreement public key (MUST be a X25519 OKP key type).
- `KEY_AUTHENTICATION_D` - is the key authentication private key (MUST be an Ed25519 OKP key type).
- `KEY_AUTHENTICATION_X` - is the key authentication public key (MUST be an Ed25519 OKP key type).
- `SERVICE_ENDPOINTS` - is the list of endpoints of the mediator split by ';' where the mediator will listen to incoming
DIDComm messages.
- `DID_PRISM_RESOLVER` - overrides the base URL used to resolve `did:prism` DID documents.

#### mediator-storage

Expand Down
20 changes: 20 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ lazy val V = new {
val zioTest = "2.1.26"
val zioTestSbt = "2.1.26"
val zioTestMagnolia = "2.1.26"
val apollo = "1.8.4-kt2.1.20"

// For WEBAPP
val laminar = "17.2.1"
Expand All @@ -50,6 +51,7 @@ lazy val D = new {
val scalaDID = Def.setting("app.fmgp" %%% "did" % V.scalaDID)
val scalaDID_imp = Def.setting("app.fmgp" %%% "did-imp" % V.scalaDID)
val scalaDID_peer = Def.setting("app.fmgp" %%% "did-method-peer" % V.scalaDID)
val scalaDID_prism = Def.setting("app.fmgp" %%% "did-method-prism" % V.scalaDID)
val scalaDID_framework = Def.setting("app.fmgp" %%% "did-framework" % V.scalaDID)
val scalaDID_protocols = Def.setting("app.fmgp" %%% "did-comm-protocols" % V.scalaDID)

Expand Down Expand Up @@ -85,6 +87,15 @@ lazy val D = new {
val zioTest = Def.setting("dev.zio" %% "zio-test" % V.zioTest % Test)
val zioTestSbt = Def.setting("dev.zio" %% "zio-test-sbt" % V.zioTestSbt % Test)
val zioTestMagnolia = Def.setting("dev.zio" %% "zio-test-magnolia" % V.zioTestMagnolia % Test)
val scalaPbRuntime =
Def.setting("com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % Test)
val apolloJvm = Def.setting(
("org.hyperledger.identus" % "apollo-jvm" % V.apollo exclude (
"net.jcip",
"jcip-annotations"
)) % Test
)
val jcip = Def.setting("com.github.stephenc.jcip" % "jcip-annotations" % "1.0-1" % Test)

// For WEBAPP
val laminar = Def.setting("com.raquo" %%% "laminar" % V.laminar)
Expand Down Expand Up @@ -206,8 +217,14 @@ lazy val mediator = project
)
.settings((setupTestConfig): _*)
.settings(
Test / scalacOptions ~= (_.filterNot(_ == "-Xfatal-warnings")),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve fatal warnings for handwritten tests

This removes -Xfatal-warnings from the entire mediator test configuration merely to accommodate generated protobuf sources, so warnings in all handwritten tests will now pass locally and in CI. That bypasses the repository's documented no-warning quality gate; scope warning suppression to the managed ScalaPB output (or suppress its specific diagnostics) rather than disabling the gate for every test source.

AGENTS.md reference: AGENTS.md:L265-L270

Useful? React with 👍 / 👎.

Test / PB.protoSources += baseDirectory.value / "src" / "test" / "protobuf",
Test / PB.targets := Seq(
scalapb.gen() -> (Test / sourceManaged).value / "scalapb"
),
libraryDependencies += D.scalaDID_imp.value,
libraryDependencies += D.scalaDID_peer.value,
libraryDependencies += D.scalaDID_prism.value,
libraryDependencies += D.scalaDID_framework.value,
libraryDependencies += D.scalaDID_protocols.value,
// libraryDependencies += D.zioHttp.value, // also import from scala DID
Expand All @@ -228,6 +245,9 @@ lazy val mediator = project
D.zioTest.value,
D.zioTestSbt.value,
D.zioTestMagnolia.value,
D.scalaPbRuntime.value,
D.apolloJvm.value,
D.jcip.value,
),
testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")
)
Expand Down
2 changes: 2 additions & 0 deletions mediator/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ mediator = {
}
problem.report.escalateTo = "atala@iohk.io"
problem.report.escalateTo = ${?ESCALATE_TO}
didPrismResolver = "https://raw.githubusercontent.com/FabioPinheiro/prism-vdr/refs/heads/main/mainnet/diddoc"
didPrismResolver = ${?DID_PRISM_RESOLVER}
}
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ case class AgentExecutorMediator(
}
.tapError(ex => ZIO.logError(s"Error when execute Protocol: $ex"))
} yield goodAction
ret <- action match
ret <- (action match
case NoReply => ZIO.unit // TODO Maybe infor transport of immediately reply/close
case reply: AnyReply =>
import fmgp.did.comm.Operations._
Expand Down Expand Up @@ -252,7 +252,42 @@ case class AgentExecutorMediator(
case Some(ReturnRoute.all) | Some(ReturnRoute.thread) => transport.send(message)
}
} yield ()
).catchSome { case MediatorDidError(didFail) =>
handleResolverFailure(pMsgOrProblemReport, transport, didFail)
}
} yield ()

private def handleResolverFailure(
pMsgOrProblemReport: Either[ProblemReport, PlaintextMessage],
transport: TransportDIDComm[Any],
didFail: DidFail
): ZIO[Agent & Operations & Resolver, Nothing, Unit] =
import fmgp.did.comm.Operations._
pMsgOrProblemReport match
case Left(problemReport) =>
ZIO.logWarning(s"Resolver failure while replying with problem-report: $didFail") *>
sign(problemReport.toPlaintextMessage)
.flatMap(transport.send)
.tapError(error => ZIO.logError(s"Unable to sign fallback problem-report: $error"))
.ignore
case Right(plaintextMessage) =>
for {
agent <- ZIO.service[Agent]
fallbackProblem = Problems.resolutionError(
to = plaintextMessage.from.toSet.map(_.asTO),
from = agent.id.asFROM,
pthid = plaintextMessage.id,
piuri = plaintextMessage.`type`,
comment = s"Unable to resolve DID while handling message: $didFail"
)
_ <- ZIO.logWarning(
s"Resolver failure while handling '${plaintextMessage.`type`}' for thid '${plaintextMessage.id}': $didFail"
)
_ <- sign(fallbackProblem.toPlaintextMessage)
.flatMap(transport.send)
.tapError(error => ZIO.logError(s"Unable to sign fallback resolver problem-report: $error"))
.ignore
} yield ()
}

object AgentExecutorMediator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import fmgp.did.comm.*
import fmgp.did.comm.protocol.*
import fmgp.did.framework.TransportFactoryImp
import fmgp.did.method.peer.*
import fmgp.did.method.prism.*
import org.hyperledger.identus.mediator.db.*
import org.hyperledger.identus.mediator.protocols.*
import zio.*
Expand Down Expand Up @@ -49,20 +50,60 @@ object CurveConfig:
import CurveConfig.given

case class MediatorConfig(
endpoints: String,
keyAgreement: OKPPrivateKeyWithoutKid,
keyAuthentication: OKPPrivateKeyWithoutKid
did: DID,
keyStore: KeyStore
) {
val did = DIDPeer2.makeAgent(
Seq(keyAgreement, keyAuthentication),
endpoints
.split(";")
.toSeq
.map { endpoint => fmgp.util.Base64.encode(s"""{"t":"dm","s":{"uri":"$endpoint","a":["didcomm/v2"]}}""") }
.map(DIDPeerServiceEncodedNew(_))
)
val agentLayer: ZLayer[Any, Nothing, MediatorAgent] =
ZLayer(MediatorAgent.make(id = did.id, keyStore = did.keyStore))
ZLayer(MediatorAgent.make(id = did, keyStore = keyStore))
}

object MediatorConfig {

def legacy(
endpoints: String,
keyAgreement: OKPPrivateKeyWithoutKid,
keyAuthentication: OKPPrivateKeyWithoutKid
): MediatorConfig = {
val agent = DIDPeer2.makeAgent(
Seq(keyAgreement, keyAuthentication),
endpoints
.split(";")
.toSeq
.map { endpoint => fmgp.util.Base64.encode(s"""{"t":"dm","s":{"uri":"$endpoint","a":["didcomm/v2"]}}""") }
.map(DIDPeerServiceEncodedNew(_))
)
MediatorConfig(did = agent.id, keyStore = agent.keyStore)
}

private val didConfig =
Config
.string("did")
.mapOrFail(str =>
DIDSubject.either(str) match
case Left(value) => Left(Config.Error.InvalidData(Chunk("did"), "Fail to parse the DID: " + value.error))
case Right(value) => Right(value.toDID: DID)
)

private val keyStoreConfig =
Config
.Sequence(
Config.Fallback[PrivateKeyWithKid](Config.derived[OKPPrivateKeyWithKid], Config.derived[ECPrivateKeyWithKid])
)
.map(keys => KeyStore(keys.toSet))
.nested("keyStore")

private val explicitConfig =
(didConfig ++ keyStoreConfig).map((did, keyStore) => MediatorConfig(did = did, keyStore = keyStore))

private val legacyConfig =
(
Config.string("endpoints") ++
Config.derived[OKPPrivateKeyWithoutKid].nested("keyAgreement") ++
Config.derived[OKPPrivateKeyWithoutKid].nested("keyAuthentication")
).map((endpoints, keyAgreement, keyAuthentication) => legacy(endpoints, keyAgreement, keyAuthentication))

val config: Config[MediatorConfig] =
Config.Fallback(explicitConfig, legacyConfig)
Comment on lines +105 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject invalid explicit identity configuration

When an operator supplies an explicit did/keyStore but any field is missing or malformed, Config.Fallback retries the complete legacy configuration instead of reporting the error. In environments that still provide the legacy key variables—most notably the checked-in Docker Compose configuration—the mediator can therefore start under the legacy peer DID and keys rather than the configured PRISM identity, leaving clients addressing the configured DID unable to communicate and potentially activating the demo identity. Select the branch based on whether explicit identity fields are present, and propagate validation errors once that branch is selected.

AGENTS.md reference: AGENTS.md:L128-L130

Useful? React with 👍 / 👎.

}

case class DataBaseConfig(
Expand Down Expand Up @@ -99,6 +140,18 @@ object MediatorStandalone extends ZIOAppDefault {
override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
Runtime.removeDefaultLoggers >>> SLF4J.slf4j(mediatorColorFormat)

private def resolverLayer(didPrismResolverBaseUrl: String): ZLayer[Client & Scope, Nothing, Resolver] =
(
DidPeerResolver.layerDidPeerResolver ++
(HttpUtils.layer >>> DIDPrismResolver.layerDIDPrismResolver(didPrismResolverBaseUrl))
) >>>
ZLayer.fromZIO(
for {
peer <- ZIO.service[DidPeerResolver]
prism <- ZIO.service[DIDPrismResolver]
} yield MultiFallbackResolver(peer, prism): Resolver
)

def mainProgram = for {
_ <- Console.printLine( // https://patorjk.com/software/taag/#p=display&f=ANSI%20Shadow&t=Mediator
"""███╗ ███╗███████╗██████╗ ██╗ █████╗ ████████╗ ██████╗ ██████╗
Expand All @@ -111,11 +164,11 @@ object MediatorStandalone extends ZIOAppDefault {
|Visit: https://github.com/hyperledger-identus/mediator""".stripMargin
)
configs = ConfigProvider.fromResourcePath()
mediatorConfig <- configs.nested("identity").nested("mediator").load(deriveConfig[MediatorConfig])
mediatorConfig <- configs.nested("identity").nested("mediator").load(MediatorConfig.config)
agentLayer = mediatorConfig.agentLayer
_ <- ZIO.log(s"Identus Mediator APP. See https://github.com/hyperledger-identus/mediator")
_ <- ZIO.log(s"MediatorConfig: $mediatorConfig")
_ <- ZIO.log(s"DID: ${mediatorConfig.did.id.string}")
_ <- ZIO.log(s"DID: ${mediatorConfig.did.string}")
mediatorDbConfig <- configs.nested("database").nested("mediator").load(deriveConfig[DataBaseConfig])
_ <- ZIO.log(s"MediatorDb Connection String: ${mediatorDbConfig.displayConnectionString}")
port <- configs
Expand All @@ -130,12 +183,18 @@ object MediatorStandalone extends ZIOAppDefault {
.nested("mediator")
.load(Config.string("escalateTo"))
_ <- ZIO.log(s"Problem reports escalated to: $escalateTo")
transportFactory = Scope.default >>> (Client.default >>> TransportFactoryImp.layer)
didPrismResolverBaseUrl <- configs
.nested("mediator")
.load(Config.string("didPrismResolver"))
_ <- ZIO.log(s"DID PRISM resolver: $didPrismResolverBaseUrl")
httpClient = Scope.default ++ Client.default
transportFactory = httpClient >>> TransportFactoryImp.layer
resolver = httpClient >>> resolverLayer(didPrismResolverBaseUrl)
mongo = AsyncDriverResource.layer >>> ReactiveMongoApi.layer(mediatorDbConfig.finalConnectionString)
repos = mongo >>> (MessageItemRepo.layer ++ UserAccountRepo.layer ++ OutboxMessageRepo.layer)
myServer <- Server
.serve((MediatorAgent.didCommApp ++ DIDCommRoutes.app) @@ (Middleware.cors))
.provideSomeLayer(DidPeerResolver.layerDidPeerResolver)
.provideSomeLayer(resolver)
.provideSomeLayer(agentLayer)
.provideSomeLayer(repos)
.provideSomeLayer(Scope.default >>> ((agentLayer ++ transportFactory ++ repos) >>> OperatorImp.layer))
Expand Down
Loading
Loading