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-its|
Note
|
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:
@KeyserverIntegrationTest
class MyIT {
@Test
void my_test(KeyserverAccess keyserver) {
// HKP endpoint
URI hkpBase = keyserver.pksBaseUri(); // http://host:port/pks
// REST/JSON endpoint
URI apiBase = keyserver.apiBaseUri(); // http://host:port/api
// Direct JDBC for seeding / verifying persistence
String jdbcUrl = keyserver.jdbcUrl();
String dbUser = keyserver.dbUser();
String dbPass = 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"})
class LookupIT { ... }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-bytes -
Environment 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_size 192k;
}example.com {
request_body {
max_size 192KB
}
}