A PGP public-key server written in Java, built on Jakarta EE 11 and Open Liberty. It accepts keys via the HKP protocol, verifies UIDs by email, and exposes lookup via a standards-compatible HKP endpoint. A modern REST/JSON API and additional operation modes are in development.
Active development. The core add → verify → lookup flow is functional end-to-end:
✓
POST /pks/add— accepts an ASCII-armored key, enforces size/subkey limits, enqueues UIDs for verification.✓
GET /pks/lookup?op=get&search=<email>— returns the armored public key for any verified UID.✓ UID email-verification flow (confirmation link sent by email, verified on callback).
❏ REST JSON API (
/api/…) — endpoints scaffolded, not yet wired to application logic.❏
op=indexHKP compliance.❏ Multiple operation modes (local, syncing, mirror, grouping).
❏ Outbound email sending wired to production SMTP.
This project is dual-licensed under EUPL-1.2 OR Apache-2.0.
See LICENSE and the texts in LICENSES/.
Unlike typical key servers, this keyserver supports multiple modes of operation:
- Local
❏ Implemented
A local keyserver which does not sync, e.g. for company use.
- Syncing
❏ Implemented
An outbound-syncing repository (similar to local, but only syncs outbound).
- Mirror
❏ Implemented
An inbound-syncing repository (only syncs incoming keys).Type a: sync everything (like most implementations).
Type b: sync only requested keys, like most Maven repository mirrors do.
Supports exclusion rules to not query specific email domains upstream.
- Grouping
❏ Implemented
Similar to nexus/artifactory groups, can group other upstream key servers for reading.
❏ Will remove keys after a while (configurable).
✓ Sends mails to UIDs for verification (local repositories only) before they go public.
❏ Removes signatures which are invalid for longer than one year.
❏ Removes revoked/invalid keys after one year unless re-uploaded.
Java 25 (required — the project targets
--release 25)A recent PostgreSQL database (see below)
Docker or Podman (required only for integration tests)
Apache Maven is included via the Maven wrapper (
./mvnw).
# Compile + run unit tests
./mvnw verify
# Build without running tests (faster for packaging)
./mvnw package -DskipTests# Podman (rootless)
podman run --name keyserver-db --rm \
-e POSTGRES_PASSWORD=keyserver \
-e POSTGRES_USER=keyserver \
-e POSTGRES_DB=keyserver \
-p 5432:5432 postgres
# Or with Docker
docker run --name keyserver-db --rm \
-e POSTGRES_PASSWORD=keyserver \
-e POSTGRES_USER=keyserver \
-e POSTGRES_DB=keyserver \
-p 5432:5432 postgresKEYSERVER_DB_PASSWORD=keyserver ./mvnw -pl web/openpgp-keyserver-protocol -am liberty:devThe HKP endpoint is then available at http://localhost:9080/pks/.
Integration tests start a real Open Liberty container and a real PostgreSQL container
via Testcontainers.
They are in the integration-tests module and are intentionally not run during a
normal ./mvnw verify build.
Docker or Podman must be available.
For Podman (rootless) — which is the preferred local runtime — the socket must be reachable before running tests:
systemctl --user start podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sockFor Docker, the standard /var/run/docker.sock is used automatically; no extra setup
is needed.
Note |
Ryuk is disabled globally via integration-tests/src/test/resources/testcontainers.properties
(ryuk.disabled=true) because Ryuk requires root access that Podman rootless does not grant.
Containers are cleaned up by the JUnit 5 extension after the test session ends.
|
# Build all modules first (WARs must exist before failsafe resolves them)
./mvnw package -DskipTests
# Run integration tests
./mvnw verify -pl integration-tests -am -P run-itsNote |
The Liberty image is pulled from the IBM Container Registry on first run
(icr.io/appcafe/open-liberty:kernel-slim-java25-openj9-ubi-minimal).
The first run may take several minutes while the image is downloaded and
Liberty features are installed.
|
Annotate a test class with @KeyserverIntegrationTest (from the extension package).
The extension injects a KeyserverAccess parameter into test methods:
@KeyserverIntegrationTestclassMyIT {
@Testvoidmy_test(KeyserverAccesskeyserver) {
// HKP endpointURIhkpBase = keyserver.pksBaseUri(); // http://host:port/pks// REST/JSON endpointURIapiBase = keyserver.apiBaseUri(); // http://host:port/api// Direct JDBC for seeding / verifying persistenceStringjdbcUrl = keyserver.jdbcUrl();
StringdbUser = keyserver.dbUser();
StringdbPass = keyserver.dbPassword();
}
}If the test class needs a specific database state, annotate it with @DatabaseSeed.
The extension will execute the listed SQL files before the class and truncate the
declared tables afterwards:
@KeyserverIntegrationTest@DatabaseSeed(value = {"sql/some-keys.sql"}, truncateAfter = {"keys", "uids"})
classLookupIT { ... }Classes without@DatabaseSeed share the common PostgreSQL instance, which starts
empty at the beginning of each test session.
Integration tests run in a dedicated GitHub Actions workflow
(.github/workflows/integration-tests.yml), separate from the standard PR build.
They are triggered on workflow_dispatch or on pushes to main that touch
integration-test or application code.
The workflow uses Podman (available on ubuntu-latest runners) instead of Docker.
The server enforces a maximum keytext payload size in the application layer
before OpenPGP parsing to reduce oversized-upload DoS risk.
Default limit:
131072bytes (128 KiB, UTF-8 byte length)MicroProfile Config property:
keyserver.pks.max-key-bytesEnvironment variable equivalent:
KEYSERVER_PKS_MAX_KEY_BYTES
For defense in depth, also enforce request/body limits at your reverse proxy.
Because /pks/add uses application/x-www-form-urlencoded, the HTTP request
body is larger than the decoded keytext near the limit. Use a slightly higher
proxy limit than the application limit to allow for form-encoding overhead.
Example snippets:
server{client_max_body_size192k;}example.com {
request_body {
max_size 192KB
}
}