Skip to content

feat(core): support trusted Bedrock identity sessions#57

Merged
robinbraemer merged 24 commits into
mainfrom
fm/connect-java-bedrock-claims-u6
Jul 15, 2026
Merged

feat(core): support trusted Bedrock identity sessions#57
robinbraemer merged 24 commits into
mainfrom
fm/connect-java-bedrock-claims-u6

Conversation

@robinbraemer

@robinbraemer robinbraemer commented Jul 15, 2026

Copy link
Copy Markdown
Member

Intent

Recover Connect Java PR #57's two approved Bedrock admission boundary blockers: use one Guice/platform-owned coordinator for raw Watch/libp2p admissions behind opaque consume-once generation tokens; keep raw sessions out of SessionProposal, LocalSession.Context, players and reflection/Gson-reachable graphs; remove static ADMISSION_OWNERS and compatibility-created registries/executors; preserve constructor descriptors as threadless direct-raw compatibility paths; keep the existing Moxy GeyserMC#464 protocol, verifier, API, fixtures and platform behavior unchanged; prove both transports, generation-safe slow verification replacement/expiry/close behavior, lifecycle cleanup and legacy compatibility. Do not alter Moxy, docs, deployments, dependencies, releases, or default behavior.

What Changed

  • Consume Moxy-signed Bedrock identity sessions over Watch and libp2p, validate their envelope and transport scope, expose session-bound claims through ConnectApi, and apply trusted profiles on Spigot and Velocity.
  • Route private admission data through a platform-owned coordinator with opaque, consume-once generation tokens, sanitizing public session/player graphs and cleaning up superseded, expired, rejected, disconnected, or closed admissions.
  • Harden identity readiness and Ed25519 key metadata/configuration validation, add protocol and capability propagation, and cover transport, lifecycle, profile mapping, and legacy constructor compatibility with fixtures and tests.

Risk Assessment

✅ Low: The focused remediation correctly waits for coordinator cleanup, and the full branch review found no remaining material intent, lifecycle, isolation, or compatibility risks.

Testing

After base-to-target mapping and replacing an auxiliary JShell evidence attempt that hit a cached compiler-plugin conflict, focused tests, complete core/Velocity suites, the multi-module build, ABI/scope audits, and direct non-UI transport/lifecycle transcripts all succeeded; generated output was removed and only intentional evidence-test edits remain.

Evidence: Transport admission transcript

LIBP2P: ACCEPTED, same-stream; WATCH: admitted. Both used opaque tokens with no private envelope exposed through proposals or staged players.

    LIBP2P admission: session=session-1, result=ACCEPTED, sameStream=true, token=opaque, proposalPrivateEnvelope=false, stagedPlayerPrivateEnvelope=false
    WATCH admission: session=session-1, token=opaque, proposalPrivateEnvelope=false, stagedPlayerPrivateEnvelope=false
BUILD SUCCESSFUL in 1s
Evidence: Generation and lifecycle transcript

Demonstrates opaque monotonic tokens, consume-once verification, stale-verification rejection after replacement/expiry/close, and platform cleanup.

    TOKEN shape: fields=1, fieldType=long, newerGeneration=true
    TOKEN consumption: firstVerification=allowed, secondVerification=rejected
    SLOW verification + replacement: replacementCompletedWhileBlocked=true, staleVerificationAllowed=false, staleClaimsPublished=false, replacementPrivateEnvelope=false
    SLOW verification + expiry: cleanupCompletedWhileBlocked=true, staleVerificationAllowed=false, staleClaimsPublished=false
    SLOW verification + close: closeCompletedWhileBlocked=true, staleVerificationAllowed=false, staleClaimsPublished=false
    PLATFORM disable: coordinatorExecutorShutdown=true, pendingCleanupTasks=0
BUILD SUCCESSFUL in 1s
Evidence: Constructor compatibility descriptors

Compiled descriptors retain legacy and coordinator-aware constructors for LocalSession, WatchClient, BedrockIdentityEnforcer, ConnectPlatform, and SpigotPlatform.

Compiled from "LocalSession.java"
public final class com.minekube.connect.network.netty.LocalSession {
  public com.minekube.connect.network.netty.LocalSession(com.minekube.connect.api.logger.ConnectLogger, com.minekube.connect.api.SimpleConnectApi, com.minekube.connect.tunnel.Tunneler, java.net.SocketAddress, com.minekube.connect.watch.SessionProposal);
    descriptor: (Lcom/minekube/connect/api/logger/ConnectLogger;Lcom/minekube/connect/api/SimpleConnectApi;Lcom/minekube/connect/tunnel/Tunneler;Ljava/net/SocketAddress;Lcom/minekube/connect/watch/SessionProposal;)V

  public com.minekube.connect.network.netty.LocalSession(com.minekube.connect.api.logger.ConnectLogger, com.minekube.connect.api.SimpleConnectApi, com.minekube.connect.tunnel.Tunneler, java.net.SocketAddress, com.minekube.connect.watch.SessionProposal, com.minekube.connect.bedrock.BedrockAdmissionCoordinator);
    descriptor: (Lcom/minekube/connect/api/logger/ConnectLogger;Lcom/minekube/connect/api/SimpleConnectApi;Lcom/minekube/connect/tunnel/Tunneler;Ljava/net/SocketAddress;Lcom/minekube/connect/watch/SessionProposal;Lcom/minekube/connect/bedrock/BedrockAdmissionCoordinator;)V

  public static java.util.Optional<com.minekube.connect.network.netty.LocalSession$Context> context(io.netty.channel.Channel);
    descriptor: (Lio/netty/channel/Channel;)Ljava/util/Optional;

  public static void context(io.netty.channel.Channel, java.util.function.Consumer<com.minekube.connect.network.netty.LocalSession$Context>);
    descriptor: (Lio/netty/channel/Channel;Ljava/util/function/Consumer;)V

  public void connect();
    descriptor: ()V

  public static void setPlatformEventLoopGroup(io.netty.channel.EventLoopGroup);
    descriptor: (Lio/netty/channel/EventLoopGroup;)V

  public static void createDirectByteBufAllocator();
    descriptor: ()V
}
Compiled from "WatchClient.java"
public class com.minekube.connect.watch.WatchClient {
  public com.minekube.connect.watch.WatchClient(okhttp3.OkHttpClient, com.minekube.connect.config.ConnectConfig, com.minekube.connect.bedrock.BedrockIdentityReadiness, com.minekube.connect.bedrock.BedrockAdmissionCoordinator);
    descriptor: (Lokhttp3/OkHttpClient;Lcom/minekube/connect/config/ConnectConfig;Lcom/minekube/connect/bedrock/BedrockIdentityReadiness;Lcom/minekube/connect/bedrock/BedrockAdmissionCoordinator;)V

  public com.minekube.connect.watch.WatchClient(okhttp3.OkHttpClient, com.minekube.connect.config.ConnectConfig);
    descriptor: (Lokhttp3/OkHttpClient;Lcom/minekube/connect/config/ConnectConfig;)V

  public okhttp3.WebSocket watch(com.minekube.connect.watch.Watcher);
    descriptor: (Lcom/minekube/connect/watch/Watcher;)Lokhttp3/WebSocket;
}
Compiled from "BedrockIdentityEnforcer.java"
public final class com.minekube.connect.bedrock.BedrockIdentityEnforcer {
  public com.minekube.connect.bedrock.BedrockIdentityEnforcer(com.minekube.connect.config.ConnectConfig, com.minekube.connect.api.logger.ConnectLogger, com.minekube.connect.bedrock.BedrockIdentityKeyProvider, com.minekube.connect.bedrock.BedrockAdmissionCoordinator);
    descriptor: (Lcom/minekube/connect/config/ConnectConfig;Lcom/minekube/connect/api/logger/ConnectLogger;Lcom/minekube/connect/bedrock/BedrockIdentityKeyProvider;Lcom/minekube/connect/bedrock/BedrockAdmissionCoordinator;)V

  public com.minekube.connect.bedrock.BedrockIdentityEnforcer(com.minekube.connect.config.ConnectConfig, com.minekube.connect.api.logger.ConnectLogger, okhttp3.OkHttpClient);
    descriptor: (Lcom/minekube/connect/config/ConnectConfig;Lcom/minekube/connect/api/logger/ConnectLogger;Lokhttp3/OkHttpClient;)V

  public com.minekube.connect.bedrock.BedrockIdentityEnforcer$Decision verify(com.minekube.connect.network.netty.LocalSession$Context);
    descriptor: (Lcom/minekube/connect/network/netty/LocalSession$Context;)Lcom/minekube/connect/bedrock/BedrockIdentityEnforcer$Decision;

  public void reject(com.minekube.connect.network.netty.LocalSession$Context, com.minekube.connect.bedrock.BedrockIdentityEnforcer$Decision);
    descriptor: (Lcom/minekube/connect/network/netty/LocalSession$Context;Lcom/minekube/connect/bedrock/BedrockIdentityEnforcer$Decision;)V

  public com.minekube.connect.bedrock.BedrockIdentityEnforcer$Decision verify(com.minekube.connect.api.player.ConnectPlayer);
    descriptor: (Lcom/minekube/connect/api/player/ConnectPlayer;)Lcom/minekube/connect/bedrock/BedrockIdentityEnforcer$Decision;
}
Compiled from "ConnectPlatform.java"
public class com.minekube.connect.ConnectPlatform {
  public com.minekube.connect.ConnectPlatform(com.minekube.connect.api.ConnectApi, com.minekube.connect.api.inject.PlatformInjector, com.minekube.connect.api.logger.ConnectLogger, com.google.inject.Injector);
    descriptor: (Lcom/minekube/connect/api/ConnectApi;Lcom/minekube/connect/api/inject/PlatformInjector;Lcom/minekube/connect/api/logger/ConnectLogger;Lcom/google/inject/Injector;)V

  public com.minekube.connect.ConnectPlatform(com.minekube.connect.api.ConnectApi, com.minekube.connect.api.inject.PlatformInjector, com.minekube.connect.api.logger.ConnectLogger, com.google.inject.Injector, com.minekube.connect.bedrock.BedrockAdmissionCoordinator);
    descriptor: (Lcom/minekube/connect/api/ConnectApi;Lcom/minekube/connect/api/inject/PlatformInjector;Lcom/minekube/connect/api/logger/ConnectLogger;Lcom/google/inject/Injector;Lcom/minekube/connect/bedrock/BedrockAdmissionCoordinator;)V

  public void init(java.nio.file.Path, com.minekube.connect.config.ConfigLoader, com.minekube.connect.config.ConfigHolder, com.minekube.connect.api.packet.PacketHandlers);
    descriptor: (Ljava/nio/file/Path;Lcom/minekube/connect/config/ConfigLoader;Lcom/minekube/connect/config/ConfigHolder;Lcom/minekube/connect/api/packet/PacketHandlers;)V

  public boolean enable(com.google.inject.Module...);
    descriptor: ([Lcom/google/inject/Module;)Z

  public boolean disable();
    descriptor: ()Z

  public boolean isProxy();
    descriptor: ()Z
}
Compiled from "SpigotPlatform.java"
public final class com.minekube.connect.SpigotPlatform extends com.minekube.connect.ConnectPlatform {
  public com.minekube.connect.SpigotPlatform(com.minekube.connect.api.ConnectApi, com.minekube.connect.api.inject.PlatformInjector, com.minekube.connect.api.logger.ConnectLogger, com.google.inject.Injector);
    descriptor: (Lcom/minekube/connect/api/ConnectApi;Lcom/minekube/connect/api/inject/PlatformInjector;Lcom/minekube/connect/api/logger/ConnectLogger;Lcom/google/inject/Injector;)V

  public com.minekube.connect.SpigotPlatform(com.minekube.connect.api.ConnectApi, com.minekube.connect.api.inject.PlatformInjector, com.minekube.connect.api.logger.ConnectLogger, com.google.inject.Injector, com.minekube.connect.bedrock.BedrockAdmissionCoordinator);
    descriptor: (Lcom/minekube/connect/api/ConnectApi;Lcom/minekube/connect/api/inject/PlatformInjector;Lcom/minekube/connect/api/logger/ConnectLogger;Lcom/google/inject/Injector;Lcom/minekube/connect/bedrock/BedrockAdmissionCoordinator;)V

  public boolean enable(com.google.inject.Module...);
    descriptor: ([Lcom/google/inject/Module;)Z
}
Evidence: Forbidden-scope audit

No docs, deployments, dependency manifests, release automation, or production ADMISSION_OWNERS changed/present.

SCOPE audit: no docs, deployments, dependency manifests, release automation, or production ADMISSION_OWNERS changed/present

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 5 issues found → auto-fixed (3) ✅
  • 🚨 core/src/main/java/com/minekube/connect/api/SimpleConnectApi.java:51 - The required criterion says raw sessions must stay out of “reflection/Gson-reachable graphs,” but the globally exposed SimpleConnectApi directly references the coordinator, whose admissions map contains raw Session objects. Reflection or Gson traversal of the API implementation can therefore reach every pending envelope. Move admission staging/discarding out of the public API object and retain only a claims-only lookup capability there.
  • 🚨 core/src/main/java/com/minekube/connect/bedrock/BedrockAdmissionCoordinator.java:80 - Generation replacement is ordered by stage time, not proposal generation: this filter ignores older admissions whose player is still null. An older Watch proposal delayed in prepare() can therefore stage after a newer libp2p proposal, cancel the newer generation, and become authoritative. This contradicts the required generation-safe replacement behavior; register and supersede generations when proposals are created.
  • 🚨 core/src/main/java/com/minekube/connect/bedrock/VerifiedBedrockIdentityRegistry.java:34 - The required single-coordinator boundary is incomplete: VerifiedBedrockIdentityRegistry still independently accepts raw Sessions, retains private admission profiles, and creates a second cleanup executor. Production no longer calls this path, but tests preserve it. Make the registry claims-only and remove its obsolete admission/TTL machinery, subject to confirming this public class is not an intended compatibility surface.
  • 🚨 core/src/main/java/com/minekube/connect/bedrock/VerifiedBedrockIdentityRegistry.java:29 - Verified claims are keyed only by the Edge-provided session ID, while SimpleConnectApi uses value equality to decide whether a player is current. Reused session IDs can therefore make a different player—or a stale equal ConnectPlayer instance—retrieve another generation’s claims. Bind claims to the exact public ConnectPlayer identity and check players.get(uuid) == player.
  • ⚠️ core/src/main/java/com/minekube/connect/bedrock/BedrockAdmissionCoordinator.java:61 - Rejecting a proposal before stage() does not cancel its coordinator admission; the raw signed envelope and one scheduled task remain until the 30-second TTL. Invalid Watch proposals and libp2p starter failures can accumulate sensitive raw state under load. Add a rejection hook that consumes/cancels the token immediately.

🔧 Fix: Harden Bedrock admission generation and claims isolation
2 issues (1 error, 1 warning) still open:

  • 🚨 core/src/main/java/com/minekube/connect/bedrock/BedrockAdmissionCoordinator.java:60 - The required intent says admissions must use “opaque consume-once generation tokens” with “generation-safe … replacement/expiry/close behavior,” but tokens are created only when private identity properties exist. A coordinator-created tokenless proposal bypasses generation, expiry, and closed-state checks in stage(), so an older legacy Watch proposal can stage after a newer libp2p proposal—or after coordinator shutdown. Track every coordinator-created transport proposal; reserve the tokenless direct-raw path for compatibility constructors.
  • ⚠️ core/src/main/java/com/minekube/connect/network/netty/LocalSession.java:214 - The displaced player returned by addPlayer() is ignored. For the same UUID with different session IDs, coordinator supersession does not remove the older admission, allowing its raw envelope or stale verification to remain until channel cleanup/TTL. Capture the previous player and discard it through the coordinator, relocating the cleanup removed from SimpleConnectApi.

🔧 Fix: Tokenize all admissions and clean displaced players
1 warning still open:

  • ⚠️ core/src/test/java/com/minekube/connect/network/netty/LocalSessionBedrockIdentityTest.java:177 - The latch is released inside addPlayer(), before LocalSession performs admissionCoordinator.discard(displaced). The test thread can therefore inspect the admissions map before cleanup completes, making this required race test flaky. Synchronize on observing the discard itself before asserting coordinator state.

🔧 Fix: Synchronize displaced-admission test on coordinator cleanup
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Base-to-target inspection with git diff --stat 9946252506ec5e27a57ab37ac728b29a584dbefc..a3411e1cb6b51f05a1a1f6dc38976d1d6634eeef and git diff --name-status ...
  • Focused admission suite via ./gradlew :core:test --tests &#39;com.minekube.connect.bedrock.BedrockAdmissionCoordinatorTest&#39; --tests &#39;com.minekube.connect.bedrock.BedrockIdentityEnforcerTest&#39; --tests &#39;com.minekube.connect.bedrock.ConnectPlatformBedrockIdentityLifecycleTest&#39; --tests &#39;com.minekube.connect.bedrock.SimpleConnectApiBedrockIdentityTest&#39; --tests &#39;com.minekube.connect.network.netty.LocalSessionBedrockIdentityTest&#39; --tests &#39;com.minekube.connect.watch.SessionProposalTest&#39; --tests &#39;com.minekube.connect.watch.WatchClientTest&#39; --tests &#39;com.minekube.connect.tunnel.p2p.Libp2pSessionMapperTest&#39; --tests &#39;com.minekube.connect.tunnel.p2p.Libp2pSessionResponderTest&#39; --tests &#39;com.minekube.connect.tunnel.p2p.Libp2pRuntimeTest&#39; --stacktrace
  • Transport evidence via ./gradlew :core:test --tests &#39;com.minekube.connect.watch.WatchClientTest.privateWatchSessionUsesCoordinatorTokenAndSanitizedProposal&#39; --tests &#39;com.minekube.connect.tunnel.p2p.Libp2pSessionResponderTest.privateOfferUsesCoordinatorTokenAndSanitizedProposal&#39; --info
  • Generation/lifecycle evidence via focused BedrockAdmissionCoordinatorTest, slow-verification BedrockIdentityEnforcerTest, and ConnectPlatformBedrockIdentityLifecycleTest selectors with --info
  • Complete module tests via ./gradlew :core:test :velocity:test --stacktrace
  • Broader multi-platform verification via ./gradlew build --stacktrace
  • Binary compatibility audit using javap -s -public on the five compatibility-sensitive classes
  • Forbidden-scope and static-owner audit using target-diff path filtering and rg -n &#39;ADMISSION_OWNERS&#39; across production sources
  • Cleanup via ./gradlew clean followed by removal of Gradle-generated worktree caches; git diff --check passed
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@robinbraemer robinbraemer merged commit dd34043 into main Jul 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant