diff --git a/.github/release-please/release-please-config.main.json b/.github/release-please/release-please-config.main.json index 55a6077b..152a2b2b 100644 --- a/.github/release-please/release-please-config.main.json +++ b/.github/release-please/release-please-config.main.json @@ -12,6 +12,10 @@ { "type": "generic", "path": "cmdline/src/main/java/io/opentdf/platform/Command.java" + }, + { + "type": "generic", + "path": "sdk/src/main/java/io/opentdf/platform/sdk/Version.java" } ] } diff --git a/.github/release-please/release-please-config.release_branches.json b/.github/release-please/release-please-config.release_branches.json index 6177d966..f36a077a 100644 --- a/.github/release-please/release-please-config.release_branches.json +++ b/.github/release-please/release-please-config.release_branches.json @@ -12,6 +12,10 @@ { "type": "generic", "path": "cmdline/src/main/java/io/opentdf/platform/Command.java" + }, + { + "type": "generic", + "path": "sdk/src/main/java/io/opentdf/platform/sdk/Version.java" } ] } diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index 415f8ce5..4879ac9e 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -54,15 +54,11 @@ 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 }, version = "{\"version\":\"" + Versions.SDK + + "\",\"tdfSpecVersion\":\"" + Versions.TDF_SPEC + "\"}") class Command { - @Option(names = {"-V", "--version"}, versionHelp = true, description = "display version info") + @Option(names = { "-V", "--version" }, versionHelp = true, description = "display version info") boolean versionInfoRequested; private static final String PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----"; @@ -85,7 +81,8 @@ class Command { @Option(names = { "-p", "--platform-endpoint" }, required = true) private String platformEndpoint; - private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, boolean publicKey) throws RuntimeException{ + private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, boolean publicKey) + throws RuntimeException { if (alg == AssertionConfig.AssertionKeyAlg.HS256) { if (key instanceof String) { key = ((String) key).getBytes(StandardCharsets.UTF_8); @@ -101,14 +98,14 @@ private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, b } String pem = (String) key; String pemWithNewlines = pem.replace("\\n", "\n"); - if (publicKey){ - String base64EncodedPem= pemWithNewlines - .replaceAll(PEM_HEADER, "") - .replaceAll(PEM_FOOTER, "") - .replaceAll("\\s", "") - .replaceAll("\r\n", "") - .replaceAll("\n", "") - .trim(); + if (publicKey) { + String base64EncodedPem = pemWithNewlines + .replaceAll(PEM_HEADER, "") + .replaceAll(PEM_FOOTER, "") + .replaceAll("\\s", "") + .replaceAll("\r\n", "") + .replaceAll("\n", "") + .trim(); byte[] decoded = Base64.getDecoder().decode(base64EncodedPem); X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded); KeyFactory kf = null; @@ -122,7 +119,7 @@ private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, b } catch (InvalidKeySpecException e) { throw new RuntimeException(e); } - }else { + } else { String privateKeyPEM = pemWithNewlines .replace(PRIVATE_KEY_HEADER, "") .replace(PRIVATE_KEY_FOOTER, "") @@ -174,6 +171,7 @@ void encrypt( List> configs = new ArrayList<>(); configs.add(Config.withKasInformation(kasInfos)); metadata.map(Config::withMetaData).ifPresent(configs::add); + configs.add(Config.withSystemMetadataAssertion()); autoconfigure.map(Config::withAutoconfigure).ifPresent(configs::add); encapKeyType.map(Config::WithWrappingKeyAlg).ifPresent(configs::add); mimeType.map(Config::withMimeType).ifPresent(configs::add); @@ -191,8 +189,9 @@ void encrypt( String fileJson = new String(Files.readAllBytes(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", e2); - } catch(Exception e3) { + throw new RuntimeException("Failed to parse assertion from file, expects an list of assertions", + e2); + } catch (Exception e3) { throw new RuntimeException("Could not parse assertion as json string or path to file", e3); } } @@ -238,11 +237,15 @@ private SDK buildSDK() { @CommandLine.Command(name = "decrypt") void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, - @Option(names = { "--rewrap-key-type" }, defaultValue = Option.NULL_VALUE, description = "Preferred rewrap algorithm, one of ${COMPLETION-CANDIDATES}") Optional rewrapKeyType, - @Option(names = { "--with-assertion-verification-disabled" }, defaultValue = "false") boolean disableAssertionVerification, - @Option(names = { "--with-assertion-verification-keys" }, defaultValue = Option.NULL_VALUE) Optional assertionVerification, + @Option(names = { + "--rewrap-key-type" }, defaultValue = Option.NULL_VALUE, description = "Preferred rewrap algorithm, one of ${COMPLETION-CANDIDATES}") Optional rewrapKeyType, + @Option(names = { + "--with-assertion-verification-disabled" }, defaultValue = "false") boolean disableAssertionVerification, + @Option(names = { + "--with-assertion-verification-keys" }, defaultValue = Option.NULL_VALUE) Optional assertionVerification, @Option(names = { "--kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional kasAllowlistStr, - @Option(names = { "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) + @Option(names = { + "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) throws Exception { try (var sdk = buildSDK()) { var opts = new ArrayList>(); @@ -254,7 +257,8 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, AssertionVerificationKeys assertionVerificationKeys; try { - assertionVerificationKeys = gson.fromJson(assertionVerificationInput, AssertionVerificationKeys.class); + assertionVerificationKeys = gson.fromJson(assertionVerificationInput, + AssertionVerificationKeys.class); } catch (JsonSyntaxException e) { // try it as a file path try { @@ -263,16 +267,20 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, } catch (JsonSyntaxException e2) { throw new RuntimeException("Failed to parse assertion verification keys from file", e2); } catch (Exception e3) { - throw new RuntimeException("Could not parse assertion verification keys as json string or path to file", e3); + throw new RuntimeException( + "Could not parse assertion verification keys as json string or path to file", + e3); } } - for (Map.Entry entry : assertionVerificationKeys.keys.entrySet()) { + for (Map.Entry entry : assertionVerificationKeys.keys + .entrySet()) { try { Object correctedKey = correctKeyType(entry.getValue().alg, entry.getValue().key, true); entry.setValue(new AssertionConfig.AssertionKey(entry.getValue().alg, correctedKey)); } catch (Exception e) { - throw new RuntimeException("Error with assertion verification key: " + e.getMessage(), e); + throw new RuntimeException("Error with assertion verification key: " + e.getMessage(), + e); } } opts.add(Config.withAssertionVerificationKeys(assertionVerificationKeys)); @@ -296,8 +304,10 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, @CommandLine.Command(name = "metadata") void readMetadata(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, - @Option(names = { "--kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional kasAllowlistStr, - @Option(names = { "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) throws IOException { + @Option(names = { "--kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional kasAllowlistStr, + @Option(names = { + "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) + throws IOException { var sdk = buildSDK(); var opts = new ArrayList>(); try (var in = FileChannel.open(tdfPath, StandardOpenOption.READ)) { @@ -344,8 +354,10 @@ void createNanoTDF( @CommandLine.Command(name = "decryptnano") void readNanoTDF(@Option(names = { "-f", "--file" }, required = true) Path nanoTDFPath, - @Option(names = { "--kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional kasAllowlistStr, - @Option(names = { "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) throws Exception { + @Option(names = { "--kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional kasAllowlistStr, + @Option(names = { + "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) + throws Exception { var sdk = buildSDK(); try (var in = FileChannel.open(nanoTDFPath, StandardOpenOption.READ)) { try (var stdout = new BufferedOutputStream(System.out)) { diff --git a/sdk/pom.xml b/sdk/pom.xml index 03c2ef0f..ef2ed2f3 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 io.opentdf.platform:sdk sdk @@ -11,7 +13,8 @@ jar 0.22.1 - https://github.com/CodeIntelligenceTesting/jazzer/releases/download/v${jazzer.version} + + https://github.com/CodeIntelligenceTesting/jazzer/releases/download/v${jazzer.version} 2.1.0 0.7.2 4.12.0 @@ -287,6 +290,12 @@ + + + src/main/resources + true + + @@ -362,17 +371,18 @@ - - - - + + + + - - - - + + + + @@ -475,7 +485,8 @@ - + fuzz @@ -499,18 +510,21 @@ - + - + - + - - - - + + + + @@ -552,22 +566,28 @@ - - - - + + + + - - + + - + - + - - + + @@ -581,4 +601,4 @@ - + \ No newline at end of file diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java index 9d9b6084..e36c304c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java @@ -1,17 +1,24 @@ package io.opentdf.platform.sdk; +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; import java.util.Objects; /** - * Represents the configuration for assertions, encapsulating various types, scopes, states, keys, + * Represents the configuration for assertions, encapsulating various types, + * scopes, states, keys, * and statements involved in assertion handling. */ public class AssertionConfig { public enum Type { HandlingAssertion("handling"), - BaseAssertion("base"); + BaseAssertion("other"); private final String type; @@ -120,4 +127,82 @@ public int hashCode() { public AppliesToState appliesToState; public Statement statement; public AssertionKey signingKey; + + /** + * Inner class to hold system metadata for assertion. + * Fields are named to match the JSON output of the original Go function. + */ + static private class SystemMetadata { + @SerializedName("tdf_spec_version") + final String tdfSpecVersion; + + @SerializedName("creation_date") + final String creationDate; + + @SerializedName("operating_system") + final String operatingSystem; + + @SerializedName("sdk_version") + final String sdkVersion; + + @SerializedName("java_version") // Corresponds to "go_version" in the Go example + final String javaVersion; + + @SerializedName("architecture") + final String architecture; + + SystemMetadata(String tdfSpecVersion, String creationDate, String operatingSystem, + String sdkVersion, String javaVersion, String architecture) { + this.tdfSpecVersion = tdfSpecVersion; + this.creationDate = creationDate; + this.operatingSystem = operatingSystem; + this.sdkVersion = sdkVersion; + this.javaVersion = javaVersion; + this.architecture = architecture; + } + } + + /** + * Returns a default assertion configuration with predefined system metadata. + * This method mimics the behavior of the Go function + * GetSystemMetadataAssertionConfig. + * + * @param tdfSpecVersionFromSDK The TDF specification version (e.g., "4.3.0"). + * @param sdkInternalVersion The internal version of this SDK (e.g., + * "1.0.0"), which will be prefixed with "Java-". + * @return An {@link AssertionConfig} populated with system metadata. + * @throws SDKException if there's an error marshalling the metadata to JSON. + */ + public static AssertionConfig getSystemMetadataAssertionConfig(String tdfSpecVersionFromSDK) { + String creationDate = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + String operatingSystem = System.getProperty("os.name"); + String sdkVersion = "Java-" + Version.SDK; + String javaVersion = System.getProperty("java.version"); + String architecture = System.getProperty("os.arch"); + + SystemMetadata metadata = new SystemMetadata(tdfSpecVersionFromSDK, creationDate, operatingSystem, + sdkVersion, javaVersion, architecture); + + Gson gson = new Gson(); // A new Gson instance is used for simplicity here. + String metadataJSON; + try { + metadataJSON = gson.toJson(metadata); + } catch (com.google.gson.JsonIOException | com.google.gson.JsonSyntaxException e) { + throw new SDKException("Failed to marshal system metadata to JSON", e); + } + + AssertionConfig config = new AssertionConfig(); + config.id = "system-metadata"; + config.type = Type.BaseAssertion; + config.scope = Scope.Payload; // Maps from Go's PayloadScope + config.appliesToState = AppliesToState.Unencrypted; + + Statement statement = new Statement(); + statement.format = "json"; + statement.schema = "system-metadata-v1"; + statement.value = metadataJSON; + config.statement = statement; + + return config; + } } \ No newline at end of file diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java index 88757be0..ea49d074 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -194,6 +194,7 @@ public static class TDFConfig { public KeyType wrappingKeyType; public boolean hexEncodeRootAndSegmentHashes; public boolean renderVersionInfoInManifest; + public boolean systemMetadataAssertion; public TDFConfig() { this.autoconfigure = true; @@ -210,6 +211,7 @@ public TDFConfig() { this.wrappingKeyType = KeyType.RSA2048Key; this.hexEncodeRootAndSegmentHashes = false; this.renderVersionInfoInManifest = true; + this.systemMetadataAssertion = false; } } @@ -336,6 +338,10 @@ public static Consumer withMimeType(String mimeType) { return (TDFConfig config) -> config.mimeType = mimeType; } + public static Consumer withSystemMetadataAssertion() { + return (TDFConfig config) -> config.systemMetadataAssertion = true; + } + public static class NanoTDFConfig { public ECCMode eccMode; public NanoTDFType.Cipher cipher; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index 80522d38..2ae08f5b 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -46,19 +46,27 @@ private static byte[] tdfECKeySaltCompute() { } public static final byte[] GLOBAL_KEY_SALT = tdfECKeySaltCompute(); - private static final String EMPTY_SPLIT_ID = ""; - private static final String TDF_VERSION = "4.3.0"; + static final String EMPTY_SPLIT_ID = ""; // Made package-private for TDFTest usage if needed, or could be private if + // not used by TDFTest + /** + * The TDF specification version this SDK implements. + */ + public static final String TDF_SPEC_VERSION = "4.3.0"; private static final String KEY_ACCESS_SCHEMA_VERSION = "1.0"; private final long maximumSize; private final SDK.Services services; /** - * Constructs a new TDF instance using the default maximum input size defined by MAX_TDF_INPUT_SIZE. + * Constructs a new TDF instance using the default maximum input size defined by + * MAX_TDF_INPUT_SIZE. *

- * This constructor is primarily used to initialize the TDF object with the standard maximum - * input size, which controls the maximum size of the input data that can be processed. - * For test purposes, an alternative constructor allows for setting a custom maximum input size. + * This constructor is primarily used to initialize the TDF object with the + * standard maximum + * input size, which controls the maximum size of the input data that can be + * processed. + * For test purposes, an alternative constructor allows for setting a custom + * maximum input size. */ TDF(SDK.Services services) { this(MAX_TDF_INPUT_SIZE, services); @@ -139,7 +147,7 @@ private PolicyObject createPolicyObject(List at private static final Base64.Encoder encoder = Base64.getEncoder(); private void prepareManifest(Config.TDFConfig tdfConfig, Map> splits) { - manifest.tdfVersion = tdfConfig.renderVersionInfoInManifest ? TDF_VERSION : null; + manifest.tdfVersion = tdfConfig.renderVersionInfoInManifest ? TDF_SPEC_VERSION : null; manifest.encryptionInformation.keyAccessType = kSplitKeyType; manifest.encryptionInformation.keyAccessObj = new ArrayList<>(); @@ -184,7 +192,8 @@ private void prepareManifest(Config.TDFConfig tdfConfig, Map Config.MAX_SEGMENT_SIZE) { - throw new IllegalStateException("Segment size " + segment.encryptedSegmentSize + " exceeded limit " + Config.MAX_SEGMENT_SIZE); - } // MIN_SEGMENT_SIZE NOT validated out due to tests needing small segment sizes with existing payloads + throw new IllegalStateException("Segment size " + segment.encryptedSegmentSize + " exceeded limit " + + Config.MAX_SEGMENT_SIZE); + } // MIN_SEGMENT_SIZE NOT validated out due to tests needing small segment sizes + // with existing payloads byte[] readBuf = new byte[(int) segment.encryptedSegmentSize]; int bytesRead = tdfReader.readPayloadBytes(readBuf); if (readBuf.length != bytesRead) { - throw new IllegalStateException("unable to read bytes for segment (wanted " + segment.encryptedSegmentSize + " but got " + bytesRead + ")"); + throw new IllegalStateException("unable to read bytes for segment (wanted " + + segment.encryptedSegmentSize + " but got " + bytesRead + ")"); } var isLegacyTdf = manifest.tdfVersion == null || manifest.tdfVersion.isEmpty(); @@ -344,7 +358,8 @@ private static byte[] calculateSignature(byte[] data, byte[] secret, Config.Inte } if (kGMACPayloadLength > data.length) { - throw new IllegalArgumentException("tried to calculate GMAC on too small a payload. payload is "+ data.length + "bytes while GMAC is " + kGMACPayloadLength + " bytes"); + throw new IllegalArgumentException("tried to calculate GMAC on too small a payload. payload is " + + data.length + "bytes while GMAC is " + kGMACPayloadLength + " bytes"); } return Arrays.copyOfRange(data, data.length - kGMACPayloadLength, data.length); @@ -354,6 +369,12 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo Planner planner = new Planner(tdfConfig, services, Autoconfigure::createGranter); Map> splits = planner.getSplits(); + // Add System Metadata Assertion if configured + if (tdfConfig.systemMetadataAssertion) { + AssertionConfig systemAssertion = AssertionConfig.getSystemMetadataAssertionConfig(TDF_SPEC_VERSION); + tdfConfig.assertionConfigList.add(systemAssertion); + } + TDFObject tdfObject = new TDFObject(); tdfObject.prepareManifest(tdfConfig, splits); @@ -405,7 +426,8 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo Manifest.RootSignature rootSignature = new Manifest.RootSignature(); - byte[] rootSig = calculateSignature(aggregateHash.toByteArray(), tdfObject.payloadKey, tdfConfig.integrityAlgorithm); + byte[] rootSig = calculateSignature(aggregateHash.toByteArray(), tdfObject.payloadKey, + tdfConfig.integrityAlgorithm); byte[] encodedRootSig = tdfConfig.hexEncodeRootAndSegmentHashes ? Hex.encodeHexString(rootSig).getBytes(StandardCharsets.UTF_8) : rootSig; @@ -437,6 +459,7 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo tdfObject.manifest.payload.isEncrypted = true; List signedAssertions = new ArrayList<>(tdfConfig.assertionConfigList.size()); + for (var assertionConfig : tdfConfig.assertionConfigList) { var assertion = new Manifest.Assertion(); assertion.id = assertionConfig.id; @@ -469,8 +492,7 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo } var hashValues = new Manifest.Assertion.HashValues( assertionHashAsHex, - encodedHash - ); + encodedHash); try { assertion.sign(hashValues, assertionSigningKey); } catch (KeyLengthException e) { @@ -493,13 +515,16 @@ Reader loadTDF(SeekableByteChannel tdf, String platformUrl) throws SDKException, return loadTDF(tdf, Config.newTDFReaderConfig(), platformUrl); } - Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig, String platformUrl) throws SDKException, IOException { - if (!tdfReaderConfig.ignoreKasAllowlist && (tdfReaderConfig.kasAllowlist == null || tdfReaderConfig.kasAllowlist.isEmpty())) { + Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig, String platformUrl) + throws SDKException, IOException { + if (!tdfReaderConfig.ignoreKasAllowlist + && (tdfReaderConfig.kasAllowlist == null || tdfReaderConfig.kasAllowlist.isEmpty())) { ListKeyAccessServersRequest request = ListKeyAccessServersRequest.newBuilder() .build(); ListKeyAccessServersResponse response; try { - response = RequestHelper.getOrThrow(services.kasRegistry().listKeyAccessServersBlocking(request, Collections.emptyMap()).execute()); + response = RequestHelper.getOrThrow( + services.kasRegistry().listKeyAccessServersBlocking(request, Collections.emptyMap()).execute()); } catch (ConnectException e) { throw new SDKException("error getting kas servers", e); } @@ -519,6 +544,7 @@ Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig) String manifestJson = tdfReader.manifest(); // use Manifest.readManifest in order to validate the Manifest input Manifest manifest = Manifest.readManifest(manifestJson); + byte[] payloadKey = new byte[GCM_KEY_SIZE]; String unencryptedMetadata = null; @@ -541,13 +567,17 @@ Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig) if (tdfReaderConfig.ignoreKasAllowlist) { logger.warn("Ignoring KasAllowlist for url {}", realAddress); } else if (tdfReaderConfig.kasAllowlist == null || tdfReaderConfig.kasAllowlist.isEmpty()) { - logger.error("KasAllowlist: No KAS allowlist provided and no KeyAccessServerRegistry available, {} is not allowed", realAddress); - throw new SDK.KasAllowlistException("No KAS allowlist provided and no KeyAccessServerRegistry available"); + logger.error( + "KasAllowlist: No KAS allowlist provided and no KeyAccessServerRegistry available, {} is not allowed", + realAddress); + throw new SDK.KasAllowlistException( + "No KAS allowlist provided and no KeyAccessServerRegistry available"); } else if (!tdfReaderConfig.kasAllowlist.contains(realAddress)) { logger.error("KasAllowlist: kas url {} is not allowed", realAddress); - throw new SDK.KasAllowlistException("KasAllowlist: kas url "+realAddress+" is not allowed"); + throw new SDK.KasAllowlistException("KasAllowlist: kas url " + realAddress + " is not allowed"); } - unwrappedKey = services.kas().unwrap(keyAccess, manifest.encryptionInformation.policy, tdfReaderConfig.sessionKeyType); + unwrappedKey = services.kas().unwrap(keyAccess, manifest.encryptionInformation.policy, + tdfReaderConfig.sessionKeyType); } catch (Exception e) { skippedSplits.put(ss, e); continue; @@ -640,7 +670,8 @@ Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig) int encryptedSegSize = manifest.encryptionInformation.integrityInformation.encryptedSegmentSizeDefault; if (segmentSize != encryptedSegSize - (kGcmIvSize + kAesBlockSize)) { - throw new IllegalStateException("segment size mismatch. encrypted segment size differs from plaintext segment size. the TDF is invalid"); + throw new IllegalStateException( + "segment size mismatch. encrypted segment size differs from plaintext segment size. the TDF is invalid"); } var aggregateHashByteArrayBytes = aggregateHash.toByteArray(); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Version.java b/sdk/src/main/java/io/opentdf/platform/sdk/Version.java index f0d5f809..ef0ee8fd 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Version.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Version.java @@ -10,13 +10,18 @@ import java.util.regex.Pattern; class Version implements Comparable { + + // Version of the SDK, managed by release-please. + public static final String SDK = "0.9.1-SNAPSHOT"; // x-release-please-version + private final int major; private final int minor; private final int patch; private final String prereleaseAndMetadata; private static final Logger log = LoggerFactory.getLogger(Version.class); - Pattern SEMVER_PATTERN = Pattern.compile("^(?0|[1-9]\\d*)\\.(?0|[1-9]\\d*)\\.(?0|[1-9]\\d*)(?\\D.*)?$"); + Pattern SEMVER_PATTERN = Pattern.compile( + "^(?0|[1-9]\\d*)\\.(?0|[1-9]\\d*)\\.(?0|[1-9]\\d*)(?\\D.*)?$"); @Override public String toString() { @@ -63,7 +68,8 @@ public int compareTo(@Nonnull Version o) { @Override public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; + if (o == null || getClass() != o.getClass()) + return false; Version version = (Version) o; return major == version.major && minor == version.minor && patch == version.patch; } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java index 517ca8f9..c28bd2bd 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -2,7 +2,9 @@ import com.connectrpc.ResponseMessage; import com.connectrpc.UnaryBlockingCall; +import com.google.gson.reflect.TypeToken; import com.nimbusds.jose.JOSEException; +import com.google.gson.Gson; import io.opentdf.platform.policy.KeyAccessServer; import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClient; import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; @@ -25,6 +27,7 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collections; +import java.util.Map; import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; @@ -33,700 +36,806 @@ import java.util.stream.Collectors; import static io.opentdf.platform.sdk.TDF.GLOBAL_KEY_SALT; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThat; public class TDFTest { - protected static KeyAccessServerRegistryServiceClient kasRegistryService; - protected static String platformUrl = "http://localhost:8080"; + protected static KeyAccessServerRegistryServiceClient kasRegistryService; + protected static String platformUrl = "http://localhost:8080"; - protected static SDK.KAS kas = new SDK.KAS() { - @Override - public void close() { - } + protected static SDK.KAS kas = new SDK.KAS() { + @Override + public void close() { + } + + @Override + public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { + // handle platform url + int index; + // if the kasinfo url contains the platform url, remove it + if (kasInfo.URL.startsWith(platformUrl)) { + index = Integer.parseInt(kasInfo.URL + .replaceFirst("^" + Pattern.quote(platformUrl) + "/kas", "")); + } else { + index = Integer.parseInt(kasInfo.URL.replaceFirst("^https://example.com/kas", "")); + } + var kiCopy = new Config.KASInfo(); + kiCopy.KID = "r1"; + kiCopy.PublicKey = CryptoUtils.getPublicKeyPEM(keypairs.get(index).getPublic()); + kiCopy.URL = kasInfo.URL; + return kiCopy; + } + + @Override + public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) { + + try { + int index; + // if the keyAccess.url contains the platform url, remove it + if (keyAccess.url.startsWith(platformUrl)) { + index = Integer.parseInt(keyAccess.url + .replaceFirst("^" + Pattern.quote(platformUrl) + "/kas", "")); + } else { + index = Integer.parseInt( + keyAccess.url.replaceFirst("^https://example.com/kas", "")); + } + var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey); + if (sessionKeyType.isEc()) { + var kasPrivateKey = CryptoUtils + .getPrivateKeyPEM(keypairs.get(index).getPrivate()); + var privateKey = ECKeyPair.privateKeyFromPem(kasPrivateKey); + var clientEphemeralPublicKey = keyAccess.ephemeralPublicKey; + var publicKey = ECKeyPair.publicKeyFromPem(clientEphemeralPublicKey); + byte[] symKey = ECKeyPair.computeECDHKey(publicKey, privateKey); + + var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symKey); + + AesGcm gcm = new AesGcm(sessionKey); + AesGcm.Encrypted encrypted = new AesGcm.Encrypted(bytes); + return gcm.decrypt(encrypted); + } else { + var decryptor = new AsymDecryption(keypairs.get(index).getPrivate()); + return decryptor.decrypt(bytes); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) { + return null; + } + + @Override + public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) { + return null; + } + + @Override + public KASKeyCache getKeyCache() { + return new KASKeyCache(); + } + }; - @Override - public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { - // handle platform url - int index; - // if the kasinfo url contains the platform url, remove it - if (kasInfo.URL.startsWith(platformUrl)) { - index = Integer.parseInt(kasInfo.URL.replaceFirst("^" + Pattern.quote(platformUrl) + "/kas", "")); - } else { - index = Integer.parseInt(kasInfo.URL.replaceFirst("^https://example.com/kas", "")); - } - var kiCopy = new Config.KASInfo(); - kiCopy.KID = "r1"; - kiCopy.PublicKey = CryptoUtils.getPublicKeyPEM(keypairs.get(index).getPublic()); - kiCopy.URL = kasInfo.URL; - return kiCopy; + private static ArrayList keypairs = new ArrayList<>(); + + @BeforeAll + static void setupKeyPairsAndMocks() { + for (int i = 0; i < 2 + new Random().nextInt(5); i++) { + if (i % 2 == 0) { + keypairs.add(CryptoUtils.generateRSAKeypair()); + } else { + keypairs.add(CryptoUtils.generateECKeypair(KeyType.EC256Key.getECCurve().getCurveName())); + } + } + + kasRegistryService = mock(KeyAccessServerRegistryServiceClient.class); + List kasRegEntries = new ArrayList<>(); + for (Config.KASInfo kasInfo : getRSAKASInfos()) { + kasRegEntries.add(KeyAccessServer.newBuilder() + .setUri(kasInfo.URL).build()); + } + for (Config.KASInfo kasInfo : getECKASInfos()) { + kasRegEntries.add(KeyAccessServer.newBuilder() + .setUri(kasInfo.URL).build()); + } + ListKeyAccessServersResponse mockResponse = ListKeyAccessServersResponse.newBuilder() + .addAllKeyAccessServers(kasRegEntries) + .build(); + + // Stub the listKeyAccessServers method + when(kasRegistryService.listKeyAccessServersBlocking(any(ListKeyAccessServersRequest.class), any())) + .thenReturn(new UnaryBlockingCall<>() { + @Override + public ResponseMessage execute() { + return new ResponseMessage.Success<>(mockResponse, + Collections.emptyMap(), + Collections.emptyMap()); + } + + @Override + public void cancel() { + // this never happens in tests + } + }); } - @Override - public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) { + @Test + void testSimpleTDFEncryptAndDecrypt() throws Exception { - try { - int index; - // if the keyAccess.url contains the platform url, remove it - if (keyAccess.url.startsWith(platformUrl)) { - index = Integer.parseInt(keyAccess.url.replaceFirst("^" + Pattern.quote(platformUrl) + "/kas", "")); - } else { - index = Integer.parseInt(keyAccess.url.replaceFirst("^https://example.com/kas", "")); + class TDFConfigPair { + public final Config.TDFConfig tdfConfig; + public final Config.TDFReaderConfig tdfReaderConfig; + + public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReaderConfig) { + this.tdfConfig = tdfConfig; + this.tdfReaderConfig = tdfReaderConfig; + } } - var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey); - if (sessionKeyType.isEc()) { - var kasPrivateKey = CryptoUtils.getPrivateKeyPEM(keypairs.get(index).getPrivate()); - var privateKey = ECKeyPair.privateKeyFromPem(kasPrivateKey); - var clientEphemeralPublicKey = keyAccess.ephemeralPublicKey; - var publicKey = ECKeyPair.publicKeyFromPem(clientEphemeralPublicKey); - byte[] symKey = ECKeyPair.computeECDHKey(publicKey, privateKey); - - var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symKey); - - AesGcm gcm = new AesGcm(sessionKey); - AesGcm.Encrypted encrypted = new AesGcm.Encrypted(bytes); - return gcm.decrypt(encrypted); - } else { - var decryptor = new AsymDecryption(keypairs.get(index).getPrivate()); - return decryptor.decrypt(bytes); + + SecureRandom secureRandom = new SecureRandom(); + byte[] key = new byte[32]; + secureRandom.nextBytes(key); + + var assertion1 = new AssertionConfig(); + assertion1.id = "assertion1"; + assertion1.type = AssertionConfig.Type.BaseAssertion; + assertion1.scope = AssertionConfig.Scope.TrustedDataObj; + assertion1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertion1.statement = new AssertionConfig.Statement(); + assertion1.statement.format = "base64binary"; + assertion1.statement.schema = "text"; + assertion1.statement.value = "ICAgIDxlZGoOkVkaD4="; + assertion1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); + + var assertionVerificationKeys = new Config.AssertionVerificationKeys(); + assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey( + AssertionConfig.AssertionKeyAlg.HS256, + key); + + List tdfConfigPairs = List.of( + new TDFConfigPair( + Config.newTDFConfig(Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withMetaData("here is some metadata"), + Config.withDataAttributes( + "https://example.org/attr/a/value/b", + "https://example.org/attr/c/value/d"), + Config.withAssertionConfig(assertion1)), + Config.newTDFReaderConfig(Config.withAssertionVerificationKeys( + assertionVerificationKeys))), + new TDFConfigPair( + Config.newTDFConfig(Config.withAutoconfigure(false), + Config.withKasInformation(getECKASInfos()), + Config.withMetaData("here is some metadata"), + Config.WithWrappingKeyAlg(KeyType.EC256Key), + Config.withDataAttributes( + "https://example.org/attr/a/value/b", + "https://example.org/attr/c/value/d"), + Config.withAssertionConfig(assertion1)), + Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys( + assertionVerificationKeys), + Config.WithSessionKeyType(KeyType.EC256Key)))); + + for (TDFConfigPair configPair : tdfConfigPairs) { + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + var manifest = tdf.createTDF(plainTextInputStream, tdfOutputStream, configPair.tdfConfig) + .getManifest(); + + assertThat(manifest.assertions).asList().hasSize(1); + var assertion = manifest.assertions.get(0); + assertThat(assertion.appliesToState).isEqualTo("unencrypted"); + assertThat(assertion.type).isEqualTo("other"); + assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); + assertThat(assertion.statement.schema).isEqualTo("text"); + assertThat(assertion.statement.format).isEqualTo("base64binary"); + + assertThat(manifest.payload.isEncrypted).isTrue(); + var size = manifest.encryptionInformation.integrityInformation.segments.stream() + .map(s -> s.segmentSize) + .reduce(0L, Long::sum); + assertThat(size).isEqualTo(plainText.getBytes().length); + + var unwrappedData = new ByteArrayOutputStream(); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + configPair.tdfReaderConfig, platformUrl); + assertThat(reader.getManifest().payload.mimeType).isEqualTo("application/octet-stream"); + + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + assertThat(reader.getMetadata()).isEqualTo("here is some metadata"); + + var policyObject = reader.readPolicyObject(); + assertThat(policyObject).isNotNull(); + assertThat(policyObject.body.dataAttributes.stream().map(a -> a.attribute) + .collect(Collectors.toList())) + .asList() + .containsExactlyInAnyOrder("https://example.org/attr/a/value/b", + "https://example.org/attr/c/value/d"); } - } catch (Exception e) { - throw new RuntimeException(e); - } } - @Override - public KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) { - return null; + @Test + void testSimpleTDFWithAssertionWithRS256() throws Exception { + String assertion1Id = "assertion1"; + var keypair = CryptoUtils.generateRSAKeypair(); + var assertionConfig = new AssertionConfig(); + assertionConfig.id = assertion1Id; + assertionConfig.type = AssertionConfig.Type.BaseAssertion; + assertionConfig.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig.statement = new AssertionConfig.Statement(); + assertionConfig.statement.format = "base64binary"; + assertionConfig.statement.schema = "text"; + assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4="; + assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, + keypair.getPrivate()); + + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = "https://example.com/kas" + 0; + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(rsaKasInfo), + Config.withSystemMetadataAssertion(), + Config.withAssertionConfig(assertionConfig)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var assertionVerificationKeys = new Config.AssertionVerificationKeys(); + assertionVerificationKeys.keys.put(assertion1Id, + new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, + keypair.getPublic())); + + var unwrappedData = new ByteArrayOutputStream(); + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys(assertionVerificationKeys)); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, + platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); } - @Override - public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) { - return null; + @Test + void testWithAssertionVerificationDisabled() throws Exception { + String assertion1Id = "assertion1"; + var keypair = CryptoUtils.generateRSAKeypair(); + var assertionConfig = new AssertionConfig(); + assertionConfig.id = assertion1Id; + assertionConfig.type = AssertionConfig.Type.BaseAssertion; + assertionConfig.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig.statement = new AssertionConfig.Statement(); + assertionConfig.statement.format = "base64binary"; + assertionConfig.statement.schema = "text"; + assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4="; + assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, + keypair.getPrivate()); + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withAssertionConfig(assertionConfig)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var assertionVerificationKeys = new Config.AssertionVerificationKeys(); + assertionVerificationKeys.keys.put(assertion1Id, + new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, + keypair.getPublic())); + + var unwrappedData = new ByteArrayOutputStream(); + var dataToUnwrap = new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()); + var emptyConfig = Config.newTDFReaderConfig(); + var thrown = assertThrows(SDKException.class, () -> { + tdf.loadTDF(dataToUnwrap, emptyConfig, platformUrl); + }); + assertThat(thrown.getCause()).isInstanceOf(JOSEException.class); + + // try with assertion verification disabled and not passing the assertion + // verification keys + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withDisableAssertionVerification(true)); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, + platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); } - @Override - public KASKeyCache getKeyCache() { - return new KASKeyCache(); + @Test + void testSimpleTDFWithAssertionWithHS256() throws Exception { + String assertion1Id = "assertion1"; + var assertionConfig1 = new AssertionConfig(); + assertionConfig1.id = assertion1Id; + assertionConfig1.type = AssertionConfig.Type.BaseAssertion; + assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig1.statement = new AssertionConfig.Statement(); + assertionConfig1.statement.format = "base64binary"; + assertionConfig1.statement.schema = "text"; + assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; + + String assertion2Id = "assertion2"; + var assertionConfig2 = new AssertionConfig(); + assertionConfig2.id = assertion2Id; + assertionConfig2.type = AssertionConfig.Type.HandlingAssertion; + assertionConfig2.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig2.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig2.statement = new AssertionConfig.Statement(); + assertionConfig2.statement.format = "json"; + assertionConfig2.statement.schema = "urn:nato:stanag:5636:A:1:elements:json"; + assertionConfig2.statement.value = "{\"uuid\":\"f74efb60-4a9a-11ef-a6f1-8ee1a61c148a\",\"body\":{\"dataAttributes\":null,\"dissem\":null}}"; + + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = "https://example.com/kas" + 0; + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(rsaKasInfo), + Config.withAssertionConfig(assertionConfig1, assertionConfig2)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var unwrappedData = new ByteArrayOutputStream(); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + Config.newTDFReaderConfig(), platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + + var manifest = reader.getManifest(); + var assertions = manifest.assertions; + assertThat(assertions.size()).isEqualTo(2); + for (var assertion : assertions) { + if (assertion.id.equals(assertion1Id)) { + assertThat(assertion.statement.format).isEqualTo("base64binary"); + assertThat(assertion.statement.schema).isEqualTo("text"); + assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); + assertThat(assertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); + } else if (assertion.id.equals(assertion2Id)) { + assertThat(assertion.statement.format).isEqualTo("json"); + assertThat(assertion.statement.schema) + .isEqualTo("urn:nato:stanag:5636:A:1:elements:json"); + assertThat(assertion.statement.value).isEqualTo( + "{\"uuid\":\"f74efb60-4a9a-11ef-a6f1-8ee1a61c148a\",\"body\":{\"dataAttributes\":null,\"dissem\":null}}"); + assertThat(assertion.type).isEqualTo(AssertionConfig.Type.HandlingAssertion.toString()); + } else { + throw new RuntimeException("unexpected assertion id: " + assertion.id); + } + } } - }; - - private static ArrayList keypairs = new ArrayList<>(); - - @BeforeAll - static void setupKeyPairsAndMocks() { - for (int i = 0; i < 2 + new Random().nextInt(5); i++) { - if (i % 2 == 0) { - keypairs.add(CryptoUtils.generateRSAKeypair()); - } else { - keypairs.add(CryptoUtils.generateECKeypair(NanoTDFType.ECCurve.SECP256R1.getCurveName())); - } + + @Test + void testSimpleTDFWithAssertionWithHS256Failure() throws Exception { + // var keypair = CryptoUtils.generateRSAKeypair(); + SecureRandom secureRandom = new SecureRandom(); + byte[] key = new byte[32]; + secureRandom.nextBytes(key); + + String assertion1Id = "assertion1"; + var assertionConfig1 = new AssertionConfig(); + assertionConfig1.id = assertion1Id; + assertionConfig1.type = AssertionConfig.Type.BaseAssertion; + assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig1.statement = new AssertionConfig.Statement(); + assertionConfig1.statement.format = "base64binary"; + assertionConfig1.statement.schema = "text"; + assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; + assertionConfig1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, + key); + + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = "https://example.com/kas" + 0; + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(rsaKasInfo), + Config.withAssertionConfig(assertionConfig1)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + byte[] notkey = new byte[32]; + secureRandom.nextBytes(notkey); + var assertionVerificationKeys = new Config.AssertionVerificationKeys(); + assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey( + AssertionConfig.AssertionKeyAlg.HS256, + notkey); + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys(assertionVerificationKeys)); + + try { + tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, + platformUrl); + throw new RuntimeException("assertion verify key error thrown"); + + } catch (SDKException e) { + assertThat(e).hasMessageContaining("verify"); + } } - kasRegistryService = mock(KeyAccessServerRegistryServiceClient.class); - List kasRegEntries = new ArrayList<>(); - for (Config.KASInfo kasInfo : getRSAKASInfos()) { - kasRegEntries.add(KeyAccessServer.newBuilder() - .setUri(kasInfo.URL).build()); + @Test + public void testCreatingTDFWithMultipleSegments() throws Exception { + var random = new Random(); + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); + + // data should be large enough to have multiple complete and a partial segment + var data = new byte[(int) (Config.MIN_SEGMENT_SIZE * 2.8)]; + random.nextBytes(data); + var plainTextInputStream = new ByteArrayInputStream(data); + var tdfOutputStream = new ByteArrayOutputStream(); + var tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + var unwrappedData = new ByteArrayOutputStream(); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toByteArray()) + .withFailMessage("extracted data does not match") + .containsExactly(data); + } - for (Config.KASInfo kasInfo : getECKASInfos()) { - kasRegEntries.add(KeyAccessServer.newBuilder() - .setUri(kasInfo.URL).build()); + + @Test + public void testCreatingTooLargeTDF() { + var random = new Random(); + var maxSize = random.nextInt(1024); + var numReturned = new AtomicInteger(0); + + // return 1 more byte than the maximum size + var is = new InputStream() { + @Override + public int read() { + if (numReturned.get() > maxSize) { + return -1; + } + numReturned.incrementAndGet(); + return 1; + } + + @Override + public int read(byte[] b, int off, int len) { + var numToReturn = Math.min(len, maxSize - numReturned.get() + 1); + numReturned.addAndGet(numToReturn); + return numToReturn; + } + }; + + var os = new OutputStream() { + @Override + public void write(int b) { + } + + @Override + public void write(byte[] b, int off, int len) { + } + }; + + var tdf = new TDF(maxSize, new FakeServicesBuilder().setKas(kas).build()); + var tdfConfig = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); + assertThrows(SDK.DataSizeNotSupported.class, + () -> tdf.createTDF(is, os, tdfConfig), + "didn't throw an exception when we created TDF that was too large"); + assertThat(numReturned.get()) + .withFailMessage("test returned the wrong number of bytes") + .isEqualTo(maxSize + 1); } - ListKeyAccessServersResponse mockResponse = ListKeyAccessServersResponse.newBuilder() - .addAllKeyAccessServers(kasRegEntries) - .build(); - - // Stub the listKeyAccessServers method - when(kasRegistryService.listKeyAccessServersBlocking(any(ListKeyAccessServersRequest.class), any())) - .thenReturn(new UnaryBlockingCall<>() { - @Override - public ResponseMessage execute() { - return new ResponseMessage.Success<>(mockResponse, Collections.emptyMap(), Collections.emptyMap()); - } - - @Override - public void cancel() { - // this never happens in tests - } - }); - } - @Test - void testSimpleTDFEncryptAndDecrypt() throws Exception { + @Test + public void testCreateTDFWithMimeType() throws Exception { + final String mimeType = "application/pdf"; - class TDFConfigPair { - public final Config.TDFConfig tdfConfig; - public final Config.TDFReaderConfig tdfReaderConfig; + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withMimeType(mimeType)); - public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReaderConfig) { - this.tdfConfig = tdfConfig; - this.tdfReaderConfig = tdfReaderConfig; - } - } + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - SecureRandom secureRandom = new SecureRandom(); - byte[] key = new byte[32]; - secureRandom.nextBytes(key); - - var assertion1 = new AssertionConfig(); - assertion1.id = "assertion1"; - assertion1.type = AssertionConfig.Type.BaseAssertion; - assertion1.scope = AssertionConfig.Scope.TrustedDataObj; - assertion1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertion1.statement = new AssertionConfig.Statement(); - assertion1.statement.format = "base64binary"; - assertion1.statement.schema = "text"; - assertion1.statement.value = "ICAgIDxlZGoOkVkaD4="; - assertion1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); - - var assertionVerificationKeys = new Config.AssertionVerificationKeys(); - assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, - key); - - List tdfConfigPairs = List.of( - new TDFConfigPair( - Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(getRSAKASInfos()), - Config.withMetaData("here is some metadata"), - Config.withDataAttributes("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"), - Config.withAssertionConfig(assertion1)), - Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(assertionVerificationKeys)) - ), - new TDFConfigPair( - Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(getECKASInfos()), - Config.withMetaData("here is some metadata"), - Config.WithWrappingKeyAlg(KeyType.EC256Key), - Config.withDataAttributes("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"), - Config.withAssertionConfig(assertion1)), - Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(assertionVerificationKeys), - Config.WithSessionKeyType(KeyType.EC256Key)) - ) - ); - - for (TDFConfigPair configPair : tdfConfigPairs) { - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - var manifest = tdf.createTDF(plainTextInputStream, tdfOutputStream, configPair.tdfConfig).getManifest(); - - assertThat(manifest.assertions).asList().hasSize(1); - var assertion = manifest.assertions.get(0); - assertThat(assertion.appliesToState).isEqualTo("unencrypted"); - assertThat(assertion.type).isEqualTo("base"); - assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); - assertThat(assertion.statement.schema).isEqualTo("text"); - assertThat(assertion.statement.format).isEqualTo("base64binary"); - - assertThat(manifest.payload.isEncrypted).isTrue(); - var size = manifest.encryptionInformation.integrityInformation.segments.stream().map(s -> s.segmentSize).reduce(0L, Long::sum); - assertThat(size).isEqualTo(plainText.getBytes().length); - - var unwrappedData = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), configPair.tdfReaderConfig, platformUrl); - assertThat(reader.getManifest().payload.mimeType).isEqualTo("application/octet-stream"); - - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - assertThat(reader.getMetadata()).isEqualTo("here is some metadata"); - - var policyObject = reader.readPolicyObject(); - assertThat(policyObject).isNotNull(); - assertThat(policyObject.body.dataAttributes.stream().map(a -> a.attribute).collect(Collectors.toList())).asList() - .containsExactlyInAnyOrder("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); + assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType); } - } - - @Test - void testSimpleTDFWithAssertionWithRS256() throws Exception { - String assertion1Id = "assertion1"; - var keypair = CryptoUtils.generateRSAKeypair(); - var assertionConfig = new AssertionConfig(); - assertionConfig.id = assertion1Id; - assertionConfig.type = AssertionConfig.Type.BaseAssertion; - assertionConfig.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig.statement = new AssertionConfig.Statement(); - assertionConfig.statement.format = "base64binary"; - assertionConfig.statement.schema = "text"; - assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4="; - assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, - keypair.getPrivate()); - - var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = "https://example.com/kas"+ 0; - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(rsaKasInfo), - Config.withAssertionConfig(assertionConfig)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - var assertionVerificationKeys = new Config.AssertionVerificationKeys(); - assertionVerificationKeys.keys.put(assertion1Id, - new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, keypair.getPublic())); - - var unwrappedData = new ByteArrayOutputStream(); - Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.withAssertionVerificationKeys(assertionVerificationKeys)); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - } - - @Test - void testWithAssertionVerificationDisabled() throws Exception { - String assertion1Id = "assertion1"; - var keypair = CryptoUtils.generateRSAKeypair(); - var assertionConfig = new AssertionConfig(); - assertionConfig.id = assertion1Id; - assertionConfig.type = AssertionConfig.Type.BaseAssertion; - assertionConfig.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig.statement = new AssertionConfig.Statement(); - assertionConfig.statement.format = "base64binary"; - assertionConfig.statement.schema = "text"; - assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4="; - assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, - keypair.getPrivate()); - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withAssertionConfig(assertionConfig)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - var assertionVerificationKeys = new Config.AssertionVerificationKeys(); - assertionVerificationKeys.keys.put(assertion1Id, - new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, keypair.getPublic())); - - var unwrappedData = new ByteArrayOutputStream(); - var dataToUnwrap = new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()); - var emptyConfig= Config.newTDFReaderConfig(); - var thrown = assertThrows(SDKException.class, () -> { - tdf.loadTDF(dataToUnwrap, emptyConfig, platformUrl); - }); - assertThat(thrown.getCause()).isInstanceOf(JOSEException.class); - - // try with assertion verification disabled and not passing the assertion verification keys - Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.withDisableAssertionVerification(true)); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - } - @Test - void testSimpleTDFWithAssertionWithHS256() throws Exception { - String assertion1Id = "assertion1"; - var assertionConfig1 = new AssertionConfig(); - assertionConfig1.id = assertion1Id; - assertionConfig1.type = AssertionConfig.Type.BaseAssertion; - assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig1.statement = new AssertionConfig.Statement(); - assertionConfig1.statement.format = "base64binary"; - assertionConfig1.statement.schema = "text"; - assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; - - String assertion2Id = "assertion2"; - var assertionConfig2 = new AssertionConfig(); - assertionConfig2.id = assertion2Id; - assertionConfig2.type = AssertionConfig.Type.HandlingAssertion; - assertionConfig2.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig2.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig2.statement = new AssertionConfig.Statement(); - assertionConfig2.statement.format = "json"; - assertionConfig2.statement.schema = "urn:nato:stanag:5636:A:1:elements:json"; - assertionConfig2.statement.value = "{\"uuid\":\"f74efb60-4a9a-11ef-a6f1-8ee1a61c148a\",\"body\":{\"dataAttributes\":null,\"dissem\":null}}"; - - var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = "https://example.com/kas"+ 0; - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(rsaKasInfo), - Config.withAssertionConfig(assertionConfig1, assertionConfig2)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - var unwrappedData = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), Config.newTDFReaderConfig(), platformUrl); - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - - var manifest = reader.getManifest(); - var assertions = manifest.assertions; - assertThat(assertions.size()).isEqualTo(2); - for (var assertion : assertions) { - if (assertion.id.equals(assertion1Id)) { + + @Test + void legacyTDFRoundTrips() throws IOException { + final String mimeType = "application/pdf"; + var assertionConfig1 = new AssertionConfig(); + assertionConfig1.id = "assertion1"; + assertionConfig1.type = AssertionConfig.Type.BaseAssertion; + assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig1.statement = new AssertionConfig.Statement(); + assertionConfig1.statement.format = "base64binary"; + assertionConfig1.statement.schema = "text"; + assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withTargetMode("4.2.1"), + Config.withAssertionConfig(assertionConfig1), + Config.withMimeType(mimeType)); + + byte[] data = new byte[129]; + new Random().nextBytes(data); + InputStream plainTextInputStream = new ByteArrayInputStream(data); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var dataOutputStream = new ByteArrayOutputStream(); + + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); + var integrityInformation = reader.getManifest().encryptionInformation.integrityInformation; + assertThat(reader.getManifest().tdfVersion).isNull(); + var decodedSignature = Base64.getDecoder().decode(integrityInformation.rootSignature.signature); + for (var b : decodedSignature) { + assertThat(isHexChar(b)) + .withFailMessage("non-hex byte in signature: " + b) + .isTrue(); + } + for (var s : integrityInformation.segments) { + var decodedSegmentSignature = Base64.getDecoder().decode(s.hash); + for (var b : decodedSegmentSignature) { + assertThat(isHexChar(b)) + .withFailMessage("non-hex byte in segment signature: " + b) + .isTrue(); + } + } + reader.readPayload(dataOutputStream); + assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType); + assertArrayEquals(data, dataOutputStream.toByteArray(), "extracted data does not match"); + var manifest = reader.getManifest(); + var assertions = manifest.assertions; + assertThat(assertions.size()).isEqualTo(1); + var assertion = assertions.get(0); + assertThat(assertion.id).isEqualTo("assertion1"); assertThat(assertion.statement.format).isEqualTo("base64binary"); assertThat(assertion.statement.schema).isEqualTo("text"); assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); assertThat(assertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); - } else if (assertion.id.equals(assertion2Id)) { - assertThat(assertion.statement.format).isEqualTo("json"); - assertThat(assertion.statement.schema).isEqualTo("urn:nato:stanag:5636:A:1:elements:json"); - assertThat(assertion.statement.value).isEqualTo( - "{\"uuid\":\"f74efb60-4a9a-11ef-a6f1-8ee1a61c148a\",\"body\":{\"dataAttributes\":null,\"dissem\":null}}"); - assertThat(assertion.type).isEqualTo(AssertionConfig.Type.HandlingAssertion.toString()); - } else { - throw new RuntimeException("unexpected assertion id: " + assertion.id); - } - } - } - - @Test - void testSimpleTDFWithAssertionWithHS256Failure() throws Exception { - // var keypair = CryptoUtils.generateRSAKeypair(); - SecureRandom secureRandom = new SecureRandom(); - byte[] key = new byte[32]; - secureRandom.nextBytes(key); - - String assertion1Id = "assertion1"; - var assertionConfig1 = new AssertionConfig(); - assertionConfig1.id = assertion1Id; - assertionConfig1.type = AssertionConfig.Type.BaseAssertion; - assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig1.statement = new AssertionConfig.Statement(); - assertionConfig1.statement.format = "base64binary"; - assertionConfig1.statement.schema = "text"; - assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; - assertionConfig1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); - - var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = "https://example.com/kas"+ 0; - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(rsaKasInfo), - Config.withAssertionConfig(assertionConfig1)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - byte[] notkey = new byte[32]; - secureRandom.nextBytes(notkey); - var assertionVerificationKeys = new Config.AssertionVerificationKeys(); - assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, - notkey); - Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.withAssertionVerificationKeys(assertionVerificationKeys)); - - try { - tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); - throw new RuntimeException("assertion verify key error thrown"); - - } catch (SDKException e) { - assertThat(e).hasMessageContaining("verify"); - } - } - - @Test - public void testCreatingTDFWithMultipleSegments() throws Exception { - var random = new Random(); - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); - - // data should be large enough to have multiple complete and a partial segment - var data = new byte[(int)(Config.MIN_SEGMENT_SIZE * 2.8)]; - random.nextBytes(data); - var plainTextInputStream = new ByteArrayInputStream(data); - var tdfOutputStream = new ByteArrayOutputStream(); - var tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - var unwrappedData = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toByteArray()) - .withFailMessage("extracted data does not match") - .containsExactly(data); - - } - - @Test - public void testCreatingTooLargeTDF() { - var random = new Random(); - var maxSize = random.nextInt(1024); - var numReturned = new AtomicInteger(0); - - // return 1 more byte than the maximum size - var is = new InputStream() { - @Override - public int read() { - if (numReturned.get() > maxSize) { - return -1; - } - numReturned.incrementAndGet(); - return 1; - } - - @Override - public int read(byte[] b, int off, int len) { - var numToReturn = Math.min(len, maxSize - numReturned.get() + 1); - numReturned.addAndGet(numToReturn); - return numToReturn; - } - }; - - var os = new OutputStream() { - @Override - public void write(int b) { - } - - @Override - public void write(byte[] b, int off, int len) { - } - }; - - var tdf = new TDF(maxSize, new FakeServicesBuilder().setKas(kas).build()); - var tdfConfig = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); - assertThrows(SDK.DataSizeNotSupported.class, - () -> tdf.createTDF(is, os, tdfConfig), - "didn't throw an exception when we created TDF that was too large"); - assertThat(numReturned.get()) - .withFailMessage("test returned the wrong number of bytes") - .isEqualTo(maxSize + 1); - } - - @Test - public void testCreateTDFWithMimeType() throws Exception { - final String mimeType = "application/pdf"; - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withMimeType(mimeType)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); - assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType); - } - - @Test - void legacyTDFRoundTrips() throws IOException { - final String mimeType = "application/pdf"; - var assertionConfig1 = new AssertionConfig(); - assertionConfig1.id = "assertion1"; - assertionConfig1.type = AssertionConfig.Type.BaseAssertion; - assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig1.statement = new AssertionConfig.Statement(); - assertionConfig1.statement.format = "base64binary"; - assertionConfig1.statement.schema = "text"; - assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withTargetMode("4.2.1"), - Config.withAssertionConfig(assertionConfig1), - Config.withMimeType(mimeType)); - - byte[] data = new byte[129]; - new Random().nextBytes(data); - InputStream plainTextInputStream = new ByteArrayInputStream(data); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - var dataOutputStream = new ByteArrayOutputStream(); - - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); - var integrityInformation = reader.getManifest().encryptionInformation.integrityInformation; - assertThat(reader.getManifest().tdfVersion).isNull(); - var decodedSignature = Base64.getDecoder().decode(integrityInformation.rootSignature.signature); - for (var b: decodedSignature) { - assertThat(isHexChar(b)) - .withFailMessage("non-hex byte in signature: " + b) - .isTrue(); } - for (var s: integrityInformation.segments) { - var decodedSegmentSignature = Base64.getDecoder().decode(s.hash); - for (var b: decodedSegmentSignature) { - assertThat(isHexChar(b)) - .withFailMessage("non-hex byte in segment signature: " + b) - .isTrue(); - } - } - reader.readPayload(dataOutputStream); - assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType); - assertArrayEquals(data, dataOutputStream.toByteArray(), "extracted data does not match"); - var manifest = reader.getManifest(); - var assertions = manifest.assertions; - assertThat(assertions.size()).isEqualTo(1); - var assertion = assertions.get(0); - assertThat(assertion.id).isEqualTo("assertion1"); - assertThat(assertion.statement.format).isEqualTo("base64binary"); - assertThat(assertion.statement.schema).isEqualTo("text"); - assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); - assertThat(assertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); - } - - @Test - void testKasAllowlist() throws Exception { - - KeyAccessServerRegistryServiceClient kasRegistryServiceNoUrl = mock(KeyAccessServerRegistryServiceClient.class); - List kasRegEntries = new ArrayList<>(); - kasRegEntries.add(KeyAccessServer.newBuilder() - .setUri("http://example.com/kas0").build()); - - ListKeyAccessServersResponse mockResponse = ListKeyAccessServersResponse.newBuilder() - .addAllKeyAccessServers(kasRegEntries) - .build(); - - // Stub the listKeyAccessServers method - when(kasRegistryServiceNoUrl.listKeyAccessServersBlocking(any(ListKeyAccessServersRequest.class), any())) - .thenReturn(new UnaryBlockingCall<>() { - @Override - public ResponseMessage execute() { - return new ResponseMessage.Success<>(mockResponse, Collections.emptyMap(), Collections.emptyMap()); - } - @Override - public void cancel() { - // we never do this during tests - } - } + @Test + void testSystemMetadataAssertion() throws Exception { + Config.TDFConfig tdfConfig = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withSystemMetadataAssertion() // Enable system metadata assertion ); - var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = "https://example.com/kas"+Integer.toString(0); + String plainText = "Test data for system metadata assertion."; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + var createdManifest = tdf.createTDF(plainTextInputStream, tdfOutputStream, tdfConfig).getManifest(); + + // Verify the created manifest directly + assertThat(createdManifest.assertions).isNotNull(); + assertThat(createdManifest.assertions.size()).isEqualTo(1); + Manifest.Assertion sysAssertion = createdManifest.assertions.get(0); + assertThat(sysAssertion.id).isEqualTo("system-metadata"); + assertThat(sysAssertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); + assertThat(sysAssertion.scope).isEqualTo(AssertionConfig.Scope.Payload.toString()); + assertThat(sysAssertion.appliesToState) + .isEqualTo(AssertionConfig.AppliesToState.Unencrypted.toString()); + assertThat(sysAssertion.statement.format).isEqualTo("json"); + assertThat(sysAssertion.statement.schema).isEqualTo("system-metadata-v1"); + + // Deserialize and check the metadata JSON + Gson gson = new Gson(); + java.lang.reflect.Type mapType = new TypeToken>() { + }.getType(); + Map metadataMap = gson.fromJson(sysAssertion.statement.value, mapType); + + assertThat(metadataMap).containsKey("tdf_spec_version"); + assertThat(metadataMap.get("tdf_spec_version")).isEqualTo(TDF.TDF_SPEC_VERSION); + assertThat(metadataMap).containsKey("creation_date"); + assertThat(metadataMap.get("creation_date")).isNotBlank(); + assertThat(metadataMap).containsKey("operating_system"); + assertThat(metadataMap.get("operating_system")).isEqualTo(System.getProperty("os.name")); + assertThat(metadataMap).containsKey("sdk_version"); + assertThat(metadataMap.get("sdk_version")).isEqualTo("Java-" + Version.SDK); + assertThat(metadataMap).containsKey("java_version"); + assertThat(metadataMap.get("java_version")).isEqualTo(System.getProperty("java.version")); + assertThat(metadataMap).containsKey("architecture"); + assertThat(metadataMap.get("architecture")).isEqualTo(System.getProperty("os.arch")); + } - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(rsaKasInfo)); + @Test + void testKasAllowlist() throws Exception { + + KeyAccessServerRegistryServiceClient kasRegistryServiceNoUrl = mock( + KeyAccessServerRegistryServiceClient.class); + List kasRegEntries = new ArrayList<>(); + kasRegEntries.add(KeyAccessServer.newBuilder() + .setUri("http://example.com/kas0").build()); + + ListKeyAccessServersResponse mockResponse = ListKeyAccessServersResponse.newBuilder() + .addAllKeyAccessServers(kasRegEntries) + .build(); + + // Stub the listKeyAccessServers method + when(kasRegistryServiceNoUrl.listKeyAccessServersBlocking(any(ListKeyAccessServersRequest.class), + any())) + .thenReturn(new UnaryBlockingCall<>() { + @Override + public ResponseMessage execute() { + return new ResponseMessage.Success<>(mockResponse, + Collections.emptyMap(), + Collections.emptyMap()); + } + + @Override + public void cancel() { + // we never do this during tests + } + }); + + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = "https://example.com/kas" + Integer.toString(0); + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(rsaKasInfo)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryServiceNoUrl).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var unwrappedData = new ByteArrayOutputStream(); + + // should throw error because the kas url is not in the allowlist + try { + tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + Config.newTDFReaderConfig(), + platformUrl); + throw new RuntimeException("expected allowlist error to be thrown"); + } catch (Exception e) { + assertThat(e).hasMessageContaining("KasAllowlist"); + } - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + // with custom allowlist should succeed + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.WithKasAllowlist("https://example.com")); + tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); + + // with ignore allowlist should succeed + readerConfig = Config.newTDFReaderConfig( + Config.WithIgnoreKasAllowlist(true)); + Reader reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + readerConfig, + platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + + // use the platform url as kas url, should succeed + var platformKasInfo = new Config.KASInfo(); + platformKasInfo.URL = platformUrl + "/kas" + Integer.toString(0); + config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(platformKasInfo)); + plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + tdfOutputStream = new ByteArrayOutputStream(); + tdf = new TDF(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryServiceNoUrl) + .build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + unwrappedData = new ByteArrayOutputStream(); + reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + Config.newTDFReaderConfig(), platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + } - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryServiceNoUrl).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + @Nonnull + private static Config.KASInfo[] getKASInfos(Predicate filter) { + var kasInfos = new ArrayList(); + for (int i = 0; i < keypairs.size(); i++) { + if (filter.test(i)) { + var kasInfo = new Config.KASInfo(); + kasInfo.URL = "https://example.com/kas" + Integer.toString(i); + kasInfo.PublicKey = null; + kasInfos.add(kasInfo); + } + } + return kasInfos.toArray(Config.KASInfo[]::new); + } - var unwrappedData = new ByteArrayOutputStream(); + @Nonnull + private static Config.KASInfo[] getRSAKASInfos() { + return getKASInfos(i -> i % 2 == 0); + } - // should throw error because the kas url is not in the allowlist - try { - tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), Config.newTDFReaderConfig(), platformUrl); - throw new RuntimeException("expected allowlist error to be thrown"); - } catch (Exception e) { - assertThat(e).hasMessageContaining("KasAllowlist"); + @Nonnull + private static Config.KASInfo[] getECKASInfos() { + return getKASInfos(i -> i % 2 != 0); } - // with custom allowlist should succeed - Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.WithKasAllowlist("https://example.com")); - tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); - - // with ignore allowlist should succeed - readerConfig = Config.newTDFReaderConfig( - Config.WithIgnoreKasAllowlist(true)); - Reader reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - - - // use the platform url as kas url, should succeed - var platformKasInfo = new Config.KASInfo(); - platformKasInfo.URL = platformUrl+"/kas"+Integer.toString(0); - config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(platformKasInfo)); - plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - tdfOutputStream = new ByteArrayOutputStream(); - tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryServiceNoUrl).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - unwrappedData = new ByteArrayOutputStream(); - reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), Config.newTDFReaderConfig(), platformUrl); - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - } - - @Nonnull - private static Config.KASInfo[] getKASInfos(Predicate filter) { - var kasInfos = new ArrayList(); - for (int i = 0; i < keypairs.size(); i++) { - if (filter.test(i)) { - var kasInfo = new Config.KASInfo(); - kasInfo.URL = "https://example.com/kas"+Integer.toString(i); - kasInfo.PublicKey = null; - kasInfos.add(kasInfo); - } + private static boolean isHexChar(byte b) { + return (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9'); } - return kasInfos.toArray(Config.KASInfo[]::new); - } - - @Nonnull - private static Config.KASInfo[] getRSAKASInfos() { - return getKASInfos(i -> i % 2 == 0); - } - - @Nonnull - private static Config.KASInfo[] getECKASInfos() { - return getKASInfos(i -> i % 2 != 0); - } - - private static boolean isHexChar(byte b) { - return (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9'); - } }