Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
27acfab
feat(java-sdk): add comprehensive DPoP (RFC 9449) support (DSPX-3397)
dmihalcik-virtru Jun 8, 2026
227c685
fix(java-sdk): fix DPoP compile errors, nonce caching, and add 401-retry
dmihalcik-virtru Jun 9, 2026
f014f37
feat(java-sdk): add --dpop and --dpop-key CLI flags to encrypt/decrypt
dmihalcik-virtru Jun 9, 2026
88f8363
fix(java-sdk): propagate --dpop flags to subcommand help via ScopeTyp…
dmihalcik-virtru Jun 9, 2026
ba8df35
fix(cmdline): replace System.exit in Supports with Callable<Integer>
dmihalcik-virtru Jun 10, 2026
565e518
test(java-sdk): add TokenSource unit tests and remove unused altindag…
dmihalcik-virtru Jun 11, 2026
dea0db5
docs: DPoP nonce challenge design spec
dmihalcik-virtru Jun 16, 2026
6959e50
docs: DPoP nonce challenge implementation plan
dmihalcik-virtru Jun 16, 2026
e21f387
feat(sdk): retry token request with nonce on use_dpop_nonce (RFC 9449…
dmihalcik-virtru Jun 16, 2026
2a60cae
fix(sdk): address DPoP nonce retry quality issues
dmihalcik-virtru Jun 16, 2026
f832f40
feat(cmdline): declare dpop_nonce_challenge support
dmihalcik-virtru Jun 16, 2026
82c31cc
fix(sdk): harden DPoP retry interceptor
dmihalcik-virtru Jun 16, 2026
a4f8911
fix(sdk): tighten TokenSource error handling and JWK validation
dmihalcik-virtru Jun 16, 2026
61e8708
fix(cmdline): restore fast-fail validation for credential options
dmihalcik-virtru Jun 16, 2026
6955c61
fix(cmdline): print stack trace for failures that escape picocli
dmihalcik-virtru Jun 16, 2026
30f9ebf
Add verbose logging and DPoP retry exception logging
dmihalcik-virtru Jun 16, 2026
c854ca8
Fall back to Bearer scheme when AS returns non-DPoP-bound token
dmihalcik-virtru Jun 16, 2026
bd52761
fix(sdk): strip query and fragment from DPoP htu claim
dmihalcik-virtru Jun 16, 2026
69ba817
test(sdk): advertise DPoP token_type in SDKBuilder mock IdP
dmihalcik-virtru Jun 16, 2026
47343c5
docs(sdk): correct RFC 9449 section citations and remove stale plan/spec
dmihalcik-virtru Jun 16, 2026
3d2ba84
fix(sdk): fail loudly when DPoP is requested but well-known omits pla…
dmihalcik-virtru Jun 16, 2026
9a2d5fa
fix(cmdline): validate DPoP key options at parse time + add coverage
dmihalcik-virtru Jun 16, 2026
07692f4
debug(sdk): add DPoP path/method/claims logging to AuthInterceptor
dmihalcik-virtru Jun 17, 2026
4b99dcf
fix(sdk): disable Connect-GET on authenticated client (DPoP htm drift)
dmihalcik-virtru Jun 17, 2026
f4b1328
fix(sdk): log malformed DPoP nonce challenge instead of swallowing 401
dmihalcik-virtru Jul 1, 2026
eea6f27
fix(sdk): drop stale DPoP proof header on Bearer-downgrade retry
dmihalcik-virtru Jul 1, 2026
61a4b8c
fix(sdk): read token and scheme atomically to close a data race
dmihalcik-virtru Jul 1, 2026
475a42a
fix(sdk): reject public-only DPoP JWK in central validation
dmihalcik-virtru Jul 1, 2026
6fb0c6a
refactor(cmdline): validate DpopMaterial in its constructor
dmihalcik-virtru Jul 1, 2026
953d35a
docs(sdk): correct getAuthHeaders Javadoc for Bearer fallback
dmihalcik-virtru Jul 1, 2026
083f4e6
test(sdk): assert DPoP proof htm claim reflects request method
dmihalcik-virtru Jul 1, 2026
b3649b4
test(sdk): cover AuthInterceptor connect-kotlin request/response path
dmihalcik-virtru Jul 1, 2026
e7fc2cc
refactor(sdk): model token scheme as an enum, make AuthHeaders static
dmihalcik-virtru Jul 1, 2026
5eade00
docs(sdk): clarify retry-once semantics and trim redundant helper Jav…
dmihalcik-virtru Jul 1, 2026
ca4c1a5
fix(sdk): surface persistent DPoP nonce challenge after retry
dmihalcik-virtru Jul 2, 2026
723b17b
docs: note JAVA_HOME setup via brew for local builds
dmihalcik-virtru Jul 2, 2026
6e63f6d
Merge branch 'main' into DSPX-3397-java-sdk
dmihalcik-virtru Jul 6, 2026
15b009c
test(sdk): cover DPoP Bearer-downgrade retry and explicit proof algor…
dmihalcik-virtru Jul 7, 2026
6e13d27
feat(cmdline): remove DPoP algorithm/key CLI flags
dmihalcik-virtru Jul 13, 2026
2f1d2e4
chore(sdk): remove unused imports flagged by SonarCloud
dmihalcik-virtru Jul 14, 2026
131b82c
feat(cmdline): list features and add --json to `supports`
dmihalcik-virtru Jul 14, 2026
7c24672
refactor(sdk): address SonarCloud findings
dmihalcik-virtru Jul 14, 2026
a47fc9f
fix(sdk,cmdline): address PR review comments
dmihalcik-virtru Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

## Build, Test, and Dev Commands

- `java`/`mvn` are not on PATH and `JAVA_HOME` is unset here. Set it from the Homebrew prefix (compiles the release-11 target fine, no version lookup needed):
`export JAVA_HOME="$(brew --prefix openjdk@21)/libexec/openjdk.jdk/Contents/Home"`
Other brew JDKs available: `openjdk` (latest), `openjdk@21`, `openjdk@25`, `openjdk@26`.
- Build everything (runs codegen): `mvn clean install`
- Fast local build (skip tests): `mvn clean install -DskipTests`
- SDK unit tests: `mvn -pl sdk test`
Expand Down
12 changes: 12 additions & 0 deletions cmdline/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@
<artifactId>sdk</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
</dependencies>

<profiles>
Expand Down
137 changes: 111 additions & 26 deletions cmdline/src/main/java/io/opentdf/platform/Command.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
package io.opentdf.platform;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.nimbusds.jose.jwk.JWK;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

import java.text.ParseException;
import com.google.gson.JsonSyntaxException;
import io.opentdf.platform.sdk.AssertionConfig;
import io.opentdf.platform.sdk.AutoConfigureException;
import io.opentdf.platform.sdk.Config;
import io.opentdf.platform.sdk.KeyType;
import io.opentdf.platform.sdk.SDK;
import io.opentdf.platform.sdk.SDKBuilder;
import picocli.CommandLine;
import picocli.CommandLine.HelpCommand;
import picocli.CommandLine.Option;
import com.google.gson.reflect.TypeToken;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
Expand All @@ -39,13 +27,27 @@
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.function.Consumer;

import io.opentdf.platform.sdk.AssertionConfig;
import io.opentdf.platform.sdk.AutoConfigureException;
import io.opentdf.platform.sdk.Config;
import io.opentdf.platform.sdk.KeyType;
import io.opentdf.platform.sdk.SDK;
import io.opentdf.platform.sdk.SDKBuilder;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.config.Configurator;
import picocli.CommandLine;
import picocli.CommandLine.HelpCommand;
import picocli.CommandLine.Option;
/**
* Constants for the TDF command line tool.
* These must be compile-time constants to appear in annotations.
Expand All @@ -58,17 +60,67 @@ class Versions {
public static final String TDF_SPEC = "4.3.0";
}

@CommandLine.Command(name = "tdf", subcommands = { HelpCommand.class }, version = "{\"version\":\"" + Versions.SDK
+ "\",\"tdfSpecVersion\":\"" + Versions.TDF_SPEC + "\"}")
@CommandLine.Command(name = "tdf", subcommands = { HelpCommand.class,
Command.Supports.class }, version = "{\"version\":\"" + Versions.SDK
+ "\",\"tdfSpecVersion\":\"" + Versions.TDF_SPEC + "\"}")
class Command {
@Option(names = { "-V", "--version" }, versionHelp = true, description = "display version info")
boolean versionInfoRequested;

// Picocli injects the parsed command spec here so buildSDK() can raise
// ParameterException with the right help context when required options
// are missing for encrypt/decrypt/metadata (which all call buildSDK()).
@CommandLine.Spec
CommandLine.Model.CommandSpec spec;

@CommandLine.Command(name = "supports", description = "Check if a feature is supported, or list all supported features")
static class Supports implements Callable<Integer> {
// Static, always-on capabilities of this build. There is no "known but
// unsupported" feature here: anything not in this list is simply unrecognized.
private static final List<String> FEATURES = List.of("dpop", "dpop_nonce_challenge");

@CommandLine.Parameters(index = "0", arity = "0..1", description = "Feature to check (e.g., dpop). Omit to list all supported features.")
private String feature;

@Option(names = "--json", description = "Output as a JSON object mapping feature name to supported (boolean)")
private boolean json;

@Override
public Integer call() {
if (feature == null) {
printFeatures(FEATURES);
return 0;
}

Optional<String> canonical = FEATURES.stream().filter(f -> f.equalsIgnoreCase(feature)).findFirst();
if (json) {
Map<String, Boolean> result = new LinkedHashMap<>();
// Emit the canonical feature name for recognized features so casing is stable
// for automation; echo the raw input only for unknown features.
result.put(canonical.orElse(feature), canonical.isPresent());
System.out.println(new Gson().toJson(result));
}
return canonical.isPresent() ? 0 : 1;
}

private void printFeatures(List<String> features) {
if (json) {
Map<String, Boolean> result = new LinkedHashMap<>();
features.forEach(f -> result.put(f, true));
System.out.println(new Gson().toJson(result));
} else {
features.forEach(System.out::println);
}
}
}

private static class AssertionKeyDeserializer implements JsonDeserializer<AssertionConfig.AssertionKey> {
@Override
public AssertionConfig.AssertionKey deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
public AssertionConfig.AssertionKey deserialize(JsonElement json, java.lang.reflect.Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
AssertionConfig.AssertionKey assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.NotDefined, null);
AssertionConfig.AssertionKey assertionKey = new AssertionConfig.AssertionKey(
AssertionConfig.AssertionKeyAlg.NotDefined, null);

if (jsonObject.has("alg")) {
assertionKey.alg = context.deserialize(jsonObject.get("alg"), AssertionConfig.AssertionKeyAlg.class);
Expand All @@ -78,13 +130,15 @@ public AssertionConfig.AssertionKey deserialize(JsonElement json, java.lang.refl
}
if (jsonObject.has("jwk")) {
try {
assertionKey.jwk = JWK.parse(jsonObject.get("jwk").toString());
assertionKey.jwk = com.nimbusds.jose.jwk.JWK.parse(jsonObject.get("jwk").toString());
} catch (ParseException e) {
throw new JsonParseException("Failed to parse jwk", e);
}
}
if (jsonObject.has("x5c")) {
assertionKey.x5c = context.deserialize(jsonObject.get("x5c"), new TypeToken<List<com.nimbusds.jose.util.Base64>>() {}.getType());
assertionKey.x5c = context.deserialize(jsonObject.get("x5c"),
new TypeToken<List<com.nimbusds.jose.util.Base64>>() {
}.getType());
}

return assertionKey;
Expand All @@ -102,7 +156,19 @@ private Gson buildGson() {
private static final String PEM_HEADER = "-----BEGIN (.*)-----";
private static final String PEM_FOOTER = "-----END (.*)-----";

@Option(names = { "--client-secret" }, required = true)
@Option(names = { "-v", "--verbose" }, scope = CommandLine.ScopeType.INHERIT, defaultValue = "false", description = "Enable verbose output including stack traces on error")
void setVerbose(boolean verbose) {
this.verbose = verbose;
if (verbose) {
var root = org.apache.logging.log4j.LogManager.getRootLogger();
if (!root.getLevel().isLessSpecificThan(Level.DEBUG)) {
Configurator.setRootLevel(Level.DEBUG);
}
}
Comment thread
dmihalcik-virtru marked this conversation as resolved.
}
boolean verbose;

@Option(names = { "--client-secret" })
private String clientSecret;

@Option(names = { "-h", "--plaintext" }, defaultValue = "false")
Expand All @@ -111,10 +177,10 @@ private Gson buildGson() {
@Option(names = { "-i", "--insecure" }, defaultValue = "false")
private boolean insecure;

@Option(names = { "--client-id" }, required = true)
@Option(names = { "--client-id" })
private String clientId;

@Option(names = { "-p", "--platform-endpoint" }, required = true)
@Option(names = { "-p", "--platform-endpoint" })
private String platformEndpoint;

private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, boolean publicKey)
Expand Down Expand Up @@ -222,7 +288,7 @@ void encrypt(
} catch (JsonSyntaxException e) {
// try it as a file path
try {
String fileJson = new String(Files.readAllBytes(Paths.get(assertionConfig)));
String fileJson = Files.readString(Paths.get(assertionConfig));
assertionConfigs = gson.fromJson(fileJson, AssertionConfig[].class);
} catch (JsonSyntaxException e2) {
throw new RuntimeException("Failed to parse assertion from file, expects an list of assertions",
Expand Down Expand Up @@ -258,6 +324,24 @@ void encrypt(
}

private SDK buildSDK() {
// The picocli @Option annotations on platformEndpoint/clientId/clientSecret are

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is really useful

// intentionally NOT marked required = true so that `tdf supports <feature>` can
// run without credentials. Subcommands that actually build an SDK enforce them
// here so the failure surfaces as a normal picocli ParameterException (exit 2)
// rather than a deep SDK error.
if (platformEndpoint == null || platformEndpoint.isEmpty()) {
throw new CommandLine.ParameterException(spec.commandLine(),
"Missing required option: '--platform-endpoint=<platformEndpoint>'");
}
if (clientId == null || clientId.isEmpty()) {
throw new CommandLine.ParameterException(spec.commandLine(),
"Missing required option: '--client-id=<clientId>'");
}
if (clientSecret == null || clientSecret.isEmpty()) {
throw new CommandLine.ParameterException(spec.commandLine(),
"Missing required option: '--client-secret=<clientSecret>'");
}

SDKBuilder builder = new SDKBuilder();
if (insecure) {
builder.insecureSslFactory();
Expand Down Expand Up @@ -296,8 +380,9 @@ void decrypt(
} catch (JsonSyntaxException e) {
// try it as a file path
try {
String fileJson = new String(Files.readAllBytes(Paths.get(assertionVerificationInput)));
assertionVerificationKeys = gson.fromJson(fileJson, Config.AssertionVerificationKeys.class);
String fileJson = Files.readString(Paths.get(assertionVerificationInput));
assertionVerificationKeys = gson.fromJson(fileJson,
Config.AssertionVerificationKeys.class);
} catch (JsonSyntaxException e2) {
throw new RuntimeException("Failed to parse assertion verification keys from file", e2);
} catch (Exception e3) {
Expand Down
13 changes: 11 additions & 2 deletions cmdline/src/main/java/io/opentdf/platform/TDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@

public class TDF {
public static void main(String[] args) {
var result = new CommandLine(new Command()).execute(args);
System.exit(result);
var command = new Command();
var cmd = new CommandLine(command);
cmd.setExecutionExceptionHandler((ex, commandLine, parseResult) -> {
if (command.verbose) {
ex.printStackTrace(System.err);
} else {
System.err.println(ex.getMessage() != null ? ex.getMessage() : ex.toString());
}
return 1;
});
Comment thread
dmihalcik-virtru marked this conversation as resolved.
System.exit(cmd.execute(args));
}
}
6 changes: 3 additions & 3 deletions cmdline/src/main/resources/log4j2.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
</Console>
</appenders>
<loggers>
<Root level="trace">
<AppenderRef ref="Console"/>
</Root>
<root level="info">
<appender-ref ref="Console"/>
</root>
<Logger name="io.grpc.netty" level="error" additivity="false">
<AppenderRef ref="Console" />
</Logger>
Expand Down
Loading
Loading