{
  "parent": {
    "cve": "CVE-2026-82329",
    "root_cause": "Default configuration registers a blank (empty-string) additional join key in JoinKeyAccess (Try.isEmpty() guards Try failure, not string emptiness; JoinKeyHashPair accepted blank keys). JoinKeyUtils.getSigningKey('') pkcs7-pads to the attacker-known constant 32 x 0x20, so any unauthenticated client can mint valid join JWTs and receive a never-expiring scp=admin service token.",
    "sink": "JoinServiceImpl.getValidatedJwtToken -> getJoinKey -> JoinKeyAccess.getTokenSignatureVerifiers -> jwt.verify(signingKey)",
    "entrypoint": "POST /access/api/v1/registry/join (RegistryNoAuthResource.join), no-kid try-all key selection"
  },
  "variants": [
    {
      "id": "router",
      "entrypoint": "POST /access/api/v1/registry/join/router (RegistryNoAuthResource.joinRouter -> JoinServiceImpl.joinRouter)",
      "same_sink": true,
      "same_root_cause": true,
      "difference": "Different no-auth HTTP endpoint and different post-verification flow (validateCheckUrl skipped by omitting check_url claim; validateNodeIdAndIP satisfied with fresh node claims; admin token returned wrapped in a join-key-signed JWT 'token' claim instead of a JSON body)",
      "validated_on_vulnerable": true,
      "validated_on_fixed": "blocked (HTTP 400)"
    },
    {
      "id": "router-override",
      "entrypoint": "POST /access/api/v1/registry/join/router?override=true",
      "same_sink": true,
      "same_root_cause": true,
      "difference": "Exercises the override branch that skips router node-id/IP topology validation",
      "validated_on_vulnerable": true,
      "validated_on_fixed": "blocked (HTTP 400)"
    },
    {
      "id": "join-kid",
      "entrypoint": "POST /access/api/v1/registry/join with explicit kid=sha256('') claim",
      "same_sink": true,
      "same_root_cause": true,
      "difference": "Exercises the kid-selected key branch of JoinKeyAccess.getRelevantJoinKeys (direct blank-key map hit) instead of the no-kid try-all branch",
      "validated_on_vulnerable": true,
      "validated_on_fixed": "blocked (HTTP 400)"
    }
  ],
  "sink_coverage_proof": {
    "method": "constant-pool scan + CFR decompilation of access-server-{api,core,rest,grpc} 7.176.15 (vuln) and 7.176.28 (fixed)",
    "findings": [
      "JoinKeyUtils.getSigningKey referenced only by JoinServiceImpl",
      "JoinKeyAccess referenced only by JoinServiceImpl, SecuritySubResource (authenticated admin API returning the main join key), and JoinKeyAccess itself",
      "JoinService referenced only by RegistryNoAuthResource (network, no-auth) and TopologyServiceImpl (internal)",
      "RegistryNoAuthResource exposes exactly two endpoints: /v1/registry/join and /v1/registry/join/router - both tested",
      "JoinServiceImpl and RegistryNoAuthResource are byte-identical between vulnerable and fixed builds; fix confined to JoinKeyAccess + JoinKeyHashPair"
    ],
    "conclusion": "All network-reachable paths to the blank-join-key sink were enumerated and tested. The fix patches the single shared choke point, so it covers every variant. Equivalence confidence: high."
  }
}
