Skip to content

Repository files navigation

Java Keyserver

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.

Maven CI Integration Tests License codecov.io Renovate AppServer

State of this project

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=index HKP compliance.

  • ❏ Multiple operation modes (local, syncing, mirror, grouping).

  • ❏ Outbound email sending wired to production SMTP.

License

This project is dual-licensed under EUPL-1.2 OR Apache-2.0. See LICENSE and the texts in LICENSES/.

Planned features

Modern Rest API

  • ❏ Implemented

Multiple operation modes

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.

Privacy features

  • ❏ 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.

Configuration options

  • ❏ Mode of operation.

  • ❏ Proxy support.

  • ❏ Exclusion rules for mirroring.

  • ❏ Exclusion rules for syncing.

Prerequisites

  • 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).

Building

# Compile + run unit tests
./mvnw verify

# Build without running tests (faster for packaging)
./mvnw package -DskipTests

Running locally

Start a PostgreSQL database
# 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 postgres
Start the HKP endpoint in dev mode (hot-reload)
KEYSERVER_DB_PASSWORD=keyserver ./mvnw -pl web/openpgp-keyserver-protocol -am liberty:dev

The HKP endpoint is then available at http://localhost:9080/pks/.

Integration tests

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.

Prerequisites

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.sock

For 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.

Running integration tests

# 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.

Writing new integration tests

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.

CI

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.

Upload size limits (POST /pks/add)

The server enforces a maximum keytext payload size in the application layer before OpenPGP parsing to reduce oversized-upload DoS risk.

  • Default limit: 131072 bytes (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:

nginx
server {
    client_max_body_size 192k;
}
Caddy
example.com {
    request_body {
        max_size 192KB
    }
}

About

A java keyserver with multiple operation modes (local, mirror, sync)

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages