Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 32 additions & 1 deletion cmdline/src/main/java/io/opentdf/platform/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@ private void printFeatures(List<String> features) {
}
}

/**
* Loosely converts string representations of the integrity algorithms to the allowed enum values.
*/
static class IntegrityAlgorithmConverter implements CommandLine.ITypeConverter<Config.IntegrityAlgorithm> {
@Override
public Config.IntegrityAlgorithm convert(String value) {
for (Config.IntegrityAlgorithm algorithm : Config.IntegrityAlgorithm.values()) {
if (algorithm.name().equalsIgnoreCase(value.trim())) {
return algorithm;
}
}
throw new CommandLine.TypeConversionException(
"expected one of [HS256, GMAC] (case-insensitive) but was '" + value + "'");
}
}

private static class AssertionKeyDeserializer implements JsonDeserializer<AssertionConfig.AssertionKey> {
@Override
public AssertionConfig.AssertionKey deserialize(JsonElement json, java.lang.reflect.Type typeOfT,
Expand Down Expand Up @@ -258,19 +274,34 @@ void encrypt(
@Option(names = {
"--encap-key-type" }, defaultValue = Option.NULL_VALUE, description = "Preferred key access key wrap algorithm, one of ${COMPLETION-CANDIDATES}") Optional<KeyType> encapKeyType,
@Option(names = { "--mime-type" }, defaultValue = Option.NULL_VALUE) Optional<String> mimeType,
@Option(names = {
"--root-integrity-algorithm" }, defaultValue = Option.NULL_VALUE, converter = IntegrityAlgorithmConverter.class, description = "Algorithm for the TDF root signature. Only HS256 is supported.") Optional<Config.IntegrityAlgorithm> rootIntegrityAlgorithm,
@Option(names = {
"--segment-integrity-algorithm" }, defaultValue = Option.NULL_VALUE, converter = IntegrityAlgorithmConverter.class, description = "Algorithm for segment hashes, one of ${COMPLETION-CANDIDATES}") Optional<Config.IntegrityAlgorithm> segmentIntegrityAlgorithm,
@Option(names = { "--with-assertions" }, defaultValue = Option.NULL_VALUE) Optional<String> assertion,
@Option(names = { "--with-target-mode" }, defaultValue = Option.NULL_VALUE) Optional<String> targetMode)

throws IOException, AutoConfigureException {

// Additional command line argument validation
List<Consumer<Config.TDFConfig>> integrityConfigs = new ArrayList<>();
segmentIntegrityAlgorithm.map(Config::withSegmentIntegrityAlgorithm).ifPresent(integrityConfigs::add);
rootIntegrityAlgorithm.ifPresent(alg -> {
try {
integrityConfigs.add(Config.withRootIntegrityAlgorithm(alg));
} catch (IllegalArgumentException e) {
throw new CommandLine.ParameterException(spec.commandLine(), e.getMessage(), e);
}
});

var sdk = buildSDK();
var kasInfos = kas.stream().map(k -> {
var ki = new Config.KASInfo();
ki.URL = k;
return ki;
}).toArray(Config.KASInfo[]::new);

List<Consumer<Config.TDFConfig>> configs = new ArrayList<>();
List<Consumer<Config.TDFConfig>> configs = new ArrayList<>(integrityConfigs);
configs.add(Config.withKasInformation(kasInfos));
metadata.map(Config::withMetaData).ifPresent(configs::add);
configs.add(Config.withSystemMetadataAssertion());
Expand Down
53 changes: 53 additions & 0 deletions cmdline/src/test/java/io/opentdf/platform/CommandTest.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package io.opentdf.platform;

import io.opentdf.platform.sdk.Config;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import picocli.CommandLine;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down Expand Up @@ -137,4 +144,50 @@ void supports_unknownFeature_json_false() {
assertThat(out.trim()).isEqualTo("{\"unknown_feature\":false}");
}

/** Runs `encrypt` with the given extra options, asserts it exited USAGE, and returns stderr. */
private String encryptErr(String... opts) {
StringWriter err = new StringWriter();
CommandLine cli = new CommandLine(new Command());
cli.setErr(new PrintWriter(err));

List<String> args = new ArrayList<>(List.of("encrypt", "-k", "https://kas.example.com", "-f", "/dev/null"));
Collections.addAll(args, opts);
int code = cli.execute(args.toArray(new String[0]));

assertThat(code).isEqualTo(CommandLine.ExitCode.USAGE);
return err.toString();
}

@ParameterizedTest
@CsvSource({ "gmac,GMAC", "GMAC,GMAC", "GMac,GMAC", "hs256,HS256", "HS256,HS256", "'HS256 ',HS256" })
void integrityAlgorithmConverter_parsesAnyCasingAndTrims(String in, Config.IntegrityAlgorithm expected) {
assertThat(new Command.IntegrityAlgorithmConverter().convert(in)).isEqualTo(expected);
}

@ParameterizedTest
@ValueSource(strings = { "gmac", "GMAC", "GMac" })
void encrypt_rootIntegrityAlgorithmGmac_isRejected(String value) {
assertThat(encryptErr("--root-integrity-algorithm", value))
.contains("unsupported root integrity algorithm");
}

@ParameterizedTest
@ValueSource(strings = { "gmac", "GMAC", "hs256", "HS256" })
void encrypt_segmentIntegrityAlgorithm_acceptsBothValuesInAnyCasing(String value) {
assertThat(encryptErr("--segment-integrity-algorithm", value))
.contains("Missing required option: '--platform-endpoint=<platformEndpoint>'");
}

@Test
void encrypt_unknownIntegrityAlgorithm_isRejected() {
assertThat(encryptErr("--segment-integrity-algorithm", "md5")).contains("--segment-integrity-algorithm");
}

@Test
void encryptHelp_listsIntegrityFlags() {
String help = new CommandLine(new Command()).getSubcommands().get("encrypt")
.getUsageMessage(CommandLine.Help.Ansi.OFF);

assertThat(help).contains("--root-integrity-algorithm", "--segment-integrity-algorithm");
}
}
24 changes: 24 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ public enum TDFFormat {
}

public enum IntegrityAlgorithm {
/** Use the HMAC algorithm with the DEK to build the hash. */
HS256,
/** For blocks encrypted with AES-GCM, extract the auth tag and use that as the hash. */
GMAC
}

Expand Down Expand Up @@ -337,6 +339,28 @@ public static Consumer<TDFConfig> withMimeType(String mimeType) {
return (TDFConfig config) -> config.mimeType = mimeType;
}

/**
* Selects the algorithm recorded in each segment's {@code hash}.
*/
public static Consumer<TDFConfig> withSegmentIntegrityAlgorithm(IntegrityAlgorithm algorithm) {
Objects.requireNonNull(algorithm, "segment integrity algorithm");
return (TDFConfig config) -> config.segmentIntegrityAlgorithm = algorithm;
}

/**
* Selects the algorithm used for {@code rootSignature}. {@code HS256} only, which is
* also the default.
*
* @throws IllegalArgumentException if {@code algorithm} is not HS256
*/
public static Consumer<TDFConfig> withRootIntegrityAlgorithm(IntegrityAlgorithm algorithm) {
if (algorithm != IntegrityAlgorithm.HS256) {
throw new IllegalArgumentException("unsupported root integrity algorithm: " + algorithm
+ "; the root signature must be " + IntegrityAlgorithm.HS256);
}
return (TDFConfig config) -> config.integrityAlgorithm = algorithm;
}

public static Consumer<TDFConfig> withSystemMetadataAssertion() {
return (TDFConfig config) -> config.systemMetadataAssertion = true;
}
Expand Down
15 changes: 15 additions & 0 deletions sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

Expand All @@ -24,6 +25,20 @@ void newTDFConfig_shouldCreateDefaultConfig() {
assertFalse(config.hexEncodeRootAndSegmentHashes);
}

@Test
void withSegmentIntegrityAlgorithm_setsSegmentOnly() {
Config.TDFConfig config = Config.newTDFConfig(
Config.withSegmentIntegrityAlgorithm(Config.IntegrityAlgorithm.HS256));
assertEquals(Config.IntegrityAlgorithm.HS256, config.segmentIntegrityAlgorithm);
assertEquals(Config.IntegrityAlgorithm.HS256, config.integrityAlgorithm);
}

@Test
void withRootIntegrityAlgorithm_rejectsGmac() {
assertThrows(IllegalArgumentException.class,
() -> Config.withRootIntegrityAlgorithm(Config.IntegrityAlgorithm.GMAC));
}

@Test
void withDataAttributes_shouldAddAttributes() throws AutoConfigureException {
Config.TDFConfig config = Config.newTDFConfig(Config.withDataAttributes("https://example.com/attr/attr1/value/value1", "https://example.com/attr/attr2/value/value2"));
Expand Down
Loading