Skip to content

Commit c513e8c

Browse files
authored
fix(sdk): deserialize object statement values correctly (#219)
Some SDKs don't set assertion values as objects instead of strings so we set them to string values when we deserialize Also: * encapsulate the `gson` instance that we use for `Manifest` deserialization so that we don't have to keep setting the options in different places * remove the `ManifestDeserializer` since its purpose is to make the `assertions` list non-null but since we have encapsulated the deserialization we can set it there * add a setting to ignore assertions in the `cmdline` app so that we can do so in xtests
1 parent 0f47702 commit c513e8c

File tree

6 files changed

+232
-84
lines changed

6 files changed

+232
-84
lines changed

cmdline/src/main/java/io/opentdf/platform/Command.java

+22-29
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
package io.opentdf.platform;
22

3+
import com.google.gson.Gson;
34
import com.google.gson.JsonSyntaxException;
45
import com.nimbusds.jose.JOSEException;
5-
import io.opentdf.platform.sdk.*;
6-
import io.opentdf.platform.sdk.TDF;
6+
import io.opentdf.platform.sdk.AssertionConfig;
7+
import io.opentdf.platform.sdk.AutoConfigureException;
8+
import io.opentdf.platform.sdk.Config;
79
import io.opentdf.platform.sdk.Config.AssertionVerificationKeys;
8-
9-
import com.google.gson.Gson;
10+
import io.opentdf.platform.sdk.NanoTDF;
11+
import io.opentdf.platform.sdk.SDK;
12+
import io.opentdf.platform.sdk.SDKBuilder;
13+
import io.opentdf.platform.sdk.TDF;
14+
import nl.altindag.ssl.SSLFactory;
1015
import org.apache.commons.codec.DecoderException;
11-
import org.bouncycastle.crypto.RuntimeCryptoException;
12-
1316
import picocli.CommandLine;
1417
import picocli.CommandLine.HelpCommand;
1518
import picocli.CommandLine.Option;
1619

17-
import javax.crypto.BadPaddingException;
18-
import javax.crypto.IllegalBlockSizeException;
19-
import javax.crypto.NoSuchPaddingException;
2020
import java.io.BufferedInputStream;
2121
import java.io.BufferedOutputStream;
2222
import java.io.File;
@@ -30,14 +30,11 @@
3030
import java.nio.file.Path;
3131
import java.nio.file.Paths;
3232
import java.nio.file.StandardOpenOption;
33-
import java.security.InvalidAlgorithmParameterException;
34-
import java.security.InvalidKeyException;
33+
import java.security.KeyFactory;
3534
import java.security.NoSuchAlgorithmException;
3635
import java.security.spec.InvalidKeySpecException;
3736
import java.security.spec.PKCS8EncodedKeySpec;
3837
import java.security.spec.X509EncodedKeySpec;
39-
import java.security.KeyFactory;
40-
import java.security.PrivateKey;
4138
import java.text.ParseException;
4239
import java.util.ArrayList;
4340
import java.util.Base64;
@@ -47,11 +44,6 @@
4744
import java.util.concurrent.ExecutionException;
4845
import java.util.function.Consumer;
4946

50-
import nl.altindag.ssl.SSLFactory;
51-
import nl.altindag.ssl.util.TrustManagerUtils;
52-
53-
import javax.net.ssl.TrustManager;
54-
5547
@CommandLine.Command(
5648
name = "tdf",
5749
subcommands = {HelpCommand.class},
@@ -234,12 +226,11 @@ private SDK buildSDK() {
234226

235227
@CommandLine.Command(name = "decrypt")
236228
void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath,
229+
@Option(names = { "--with-assertion-verification-disabled" }, defaultValue = "false") boolean disableAssertionVerification,
237230
@Option(names = { "--with-assertion-verification-keys" }, defaultValue = Option.NULL_VALUE) Optional<String> assertionVerification)
238-
throws IOException,
239-
InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException,
240-
BadPaddingException, InvalidKeyException, TDF.FailedToCreateGMAC,
241-
JOSEException, ParseException, NoSuchAlgorithmException, DecoderException {
231+
throws IOException, TDF.FailedToCreateGMAC, JOSEException, ParseException, NoSuchAlgorithmException, DecoderException {
242232
var sdk = buildSDK();
233+
var opts = new ArrayList<Consumer<Config.TDFReaderConfig>>();
243234
try (var in = FileChannel.open(tdfPath, StandardOpenOption.READ)) {
244235
try (var stdout = new BufferedOutputStream(System.out)) {
245236
if (assertionVerification.isPresent()) {
@@ -269,14 +260,16 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath,
269260
throw new RuntimeException("Error with assertion verification key: " + e.getMessage(), e);
270261
}
271262
}
272-
Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig(
273-
Config.withAssertionVerificationKeys(assertionVerificationKeys));
274-
var reader = new TDF().loadTDF(in, sdk.getServices().kas(), readerConfig);
275-
reader.readPayload(stdout);
276-
} else {
277-
var reader = new TDF().loadTDF(in, sdk.getServices().kas());
278-
reader.readPayload(stdout);
263+
opts.add(Config.withAssertionVerificationKeys(assertionVerificationKeys));
279264
}
265+
266+
if (disableAssertionVerification) {
267+
opts.add(Config.withDisableAssertionVerification(true));
268+
}
269+
270+
var readerConfig = Config.newTDFReaderConfig(opts.toArray(new Consumer[0]));
271+
var reader = new TDF().loadTDF(in, sdk.getServices().kas(), readerConfig);
272+
reader.readPayload(stdout);
280273
}
281274
}
282275
}

sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java

+36-18
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,15 @@ public class Manifest {
5252
private static final String kAssertionSignature = "assertionSig";
5353

5454
private static final Gson gson = new GsonBuilder()
55-
.registerTypeAdapter(Manifest.class, new ManifestDeserializer())
56-
.create();
55+
.registerTypeAdapter(AssertionConfig.Statement.class, new AssertionValueAdapter())
56+
.create();
5757
@SerializedName(value = "schemaVersion")
5858
String tdfVersion;
5959

60+
public static String toJson(Manifest manifest) {
61+
return gson.toJson(manifest);
62+
}
63+
6064
@Override
6165
public boolean equals(Object o) {
6266
if (this == o) return true;
@@ -318,7 +322,6 @@ static public class Assertion {
318322
public String appliesToState;
319323
public AssertionConfig.Statement statement;
320324
public Binding binding;
321-
322325
static public class HashValues {
323326
private final String assertionHash;
324327
private final String signature;
@@ -467,28 +470,43 @@ private JWSVerifier createVerifier(AssertionConfig.AssertionKey assertionKey) th
467470
}
468471
}
469472

470-
public EncryptionInformation encryptionInformation;
471-
public Payload payload;
472-
public List<Assertion> assertions = new ArrayList<>();
473-
474-
public static class ManifestDeserializer implements JsonDeserializer<Object> {
473+
public static class AssertionValueAdapter implements JsonDeserializer<AssertionConfig.Statement> {
475474
@Override
476-
public Manifest deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
477-
// Let Gson handle the default deserialization of the object first
478-
Manifest manifest = new Gson().fromJson(json, typeOfT);
479-
// Now check if the `assertions` field is null and replace it with an empty list if necessary
480-
if (manifest.assertions == null) {
481-
manifest.assertions = new ArrayList<>(); // Replace null with empty list
475+
public AssertionConfig.Statement deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
476+
if (!json.isJsonObject()) {
477+
throw new IllegalArgumentException(String.format("%s is not a JSON object", AssertionConfig.Statement.class.getName()));
478+
}
479+
var obj = json.getAsJsonObject();
480+
var statement = new AssertionConfig.Statement();
481+
if (obj.has("format")) {
482+
statement.format = obj.get("format").getAsString();
482483
}
483-
return manifest;
484+
if (obj.has("schema")) {
485+
statement.schema = obj.get("schema").getAsString();
486+
}
487+
if (obj.has("value")) {
488+
var value = obj.get("value");
489+
if (value.isJsonPrimitive()) {
490+
// it's already a primitive (hopefully string) so we don't need its escaped value here
491+
statement.value = value.getAsString();
492+
} else {
493+
statement.value = value.toString();
494+
}
495+
}
496+
return statement;
484497
}
485498
}
486499

500+
public EncryptionInformation encryptionInformation;
501+
public Payload payload;
502+
public List<Assertion> assertions = new ArrayList<>();
487503
protected static Manifest readManifest(Reader reader) {
488504
Manifest result = gson.fromJson(reader, Manifest.class);
489-
if (result == null) {
490-
throw new IllegalArgumentException("Manifest is null");
491-
} else if (result.payload == null) {
505+
if (result.assertions == null) {
506+
result.assertions = new ArrayList<>();
507+
}
508+
509+
if (result.payload == null) {
492510
throw new IllegalArgumentException("Manifest with null payload");
493511
} else if (result.encryptionInformation == null) {
494512
throw new IllegalArgumentException("Manifest with null encryptionInformation");

sdk/src/main/java/io/opentdf/platform/sdk/TDF.java

+2-6
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@
77
import io.opentdf.platform.policy.Value;
88
import io.opentdf.platform.policy.attributes.AttributesServiceGrpc.AttributesServiceFutureStub;
99
import io.opentdf.platform.sdk.Config.TDFConfig;
10-
import io.opentdf.platform.sdk.Manifest.ManifestDeserializer;
1110
import io.opentdf.platform.sdk.Autoconfigure.AttributeValueFQN;
1211
import io.opentdf.platform.sdk.Config.KASInfo;
1312

1413
import org.apache.commons.codec.DecoderException;
1514
import org.apache.commons.codec.binary.Hex;
16-
import org.erdtman.jcs.JsonCanonicalizer;
1715
import org.slf4j.Logger;
1816
import org.slf4j.LoggerFactory;
1917

@@ -79,9 +77,7 @@ public TDF() {
7977

8078
private static final SecureRandom sRandom = new SecureRandom();
8179

82-
private static final Gson gson = new GsonBuilder()
83-
.registerTypeAdapter(Manifest.class, new ManifestDeserializer())
84-
.create();
80+
private static final Gson gson = new GsonBuilder().create();
8581

8682
public class SplitKeyException extends IOException {
8783
public SplitKeyException(String errorMessage) {
@@ -561,7 +557,7 @@ public TDFObject createTDF(InputStream payload,
561557
}
562558

563559
tdfObject.manifest.assertions = signedAssertions;
564-
String manifestAsStr = gson.toJson(tdfObject.manifest);
560+
String manifestAsStr = Manifest.toJson(tdfObject.manifest);
565561

566562
tdfWriter.appendManifest(manifestAsStr);
567563
tdfObject.size = tdfWriter.finish();

sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java

+41-22
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
package io.opentdf.platform.sdk;
22

3-
import org.junit.jupiter.api.Test;
43
import com.google.gson.Gson;
5-
import com.google.gson.GsonBuilder;
6-
7-
import io.opentdf.platform.sdk.Manifest.ManifestDeserializer;
4+
import org.junit.jupiter.api.Test;
85

9-
import java.nio.charset.Charset;
10-
import java.nio.charset.StandardCharsets;
6+
import java.io.IOException;
7+
import java.io.InputStreamReader;
8+
import java.io.StringReader;
119
import java.util.List;
1210
import java.util.Map;
1311

14-
import static org.junit.Assert.assertNotNull;
12+
import static org.assertj.core.api.Assertions.assertThat;
1513
import static org.junit.jupiter.api.Assertions.assertEquals;
1614

1715
public class ManifestTest {
@@ -64,15 +62,11 @@ void testManifestMarshalAndUnMarshal() {
6462
" }\n" +
6563
"}";
6664

67-
GsonBuilder gsonBuilder = new GsonBuilder();
68-
Gson gson = gsonBuilder.setPrettyPrinting()
69-
.registerTypeAdapter(Manifest.class, new ManifestDeserializer())
70-
.create();
71-
Manifest manifest = gson.fromJson(kManifestJsonFromTDF, Manifest.class);
65+
Manifest manifest = Manifest.readManifest(new StringReader(kManifestJsonFromTDF));
7266

7367
// Test payload
7468
assertEquals(manifest.payload.url, "0.payload");
75-
assertEquals(manifest.payload.isEncrypted, true);
69+
assertThat(manifest.payload.isEncrypted).isTrue();
7670

7771
// Test encryptionInformation
7872
assertEquals(manifest.encryptionInformation.keyAccessType, "split");
@@ -90,8 +84,8 @@ void testManifestMarshalAndUnMarshal() {
9084
assertEquals(manifest.encryptionInformation.integrityInformation.segmentHashAlg, "GMAC");
9185
assertEquals(manifest.encryptionInformation.integrityInformation.segments.get(0).segmentSize, 1048576);
9286

93-
var serialized = gson.toJson(manifest);
94-
var deserializedAgain = gson.fromJson(serialized, Manifest.class);
87+
var serialized = Manifest.toJson(manifest);
88+
var deserializedAgain = Manifest.readManifest(new StringReader(serialized));
9589

9690
assertEquals(manifest, deserializedAgain, "something changed when we deserialized -> serialized -> deserialized");
9791
}
@@ -146,18 +140,43 @@ void testAssertionNull() {
146140
" \"assertions\": null\n"+
147141
"}";
148142

149-
GsonBuilder gsonBuilder = new GsonBuilder();
150-
Gson gson = gsonBuilder.setPrettyPrinting()
151-
.registerTypeAdapter(Manifest.class, new ManifestDeserializer())
152-
.create();
153-
Manifest manifest = gson.fromJson(kManifestJsonFromTDF, Manifest.class);
143+
Manifest manifest = Manifest.readManifest(new StringReader(kManifestJsonFromTDF));
154144

155145
// Test payload for sanity check
156146
assertEquals(manifest.payload.url, "0.payload");
157-
assertEquals(manifest.payload.isEncrypted, true);
147+
assertThat(manifest.payload.isEncrypted).isTrue();
158148
// Test assertion deserialization
159-
assertNotNull(manifest.assertions);
149+
assertThat(manifest.assertions).isNotNull();
160150
assertEquals(manifest.assertions.size(), 0);
151+
}
152+
153+
@Test
154+
void testReadingManifestWithObjectStatementValue() throws IOException {
155+
final Manifest manifest;
156+
try (var mStream = getClass().getResourceAsStream("/io.opentdf.platform.sdk.TestData/manifest-with-object-statement-value.json")) {
157+
assert mStream != null;
158+
manifest = Manifest.readManifest(new InputStreamReader(mStream)) ;
159+
}
160+
161+
assertThat(manifest.assertions).hasSize(2);
161162

163+
var statementValStr = manifest.assertions.get(0).statement.value;
164+
var statementVal = new Gson().fromJson(statementValStr, Map.class);
165+
assertThat(statementVal).isEqualTo(
166+
Map.of("ocl",
167+
Map.of("pol", "2ccf11cb-6c9a-4e49-9746-a7f0a295945d",
168+
"cls", "SECRET",
169+
"catl", List.of(
170+
Map.of(
171+
"type", "P",
172+
"name", "Releasable To",
173+
"vals", List.of("usa")
174+
)
175+
),
176+
"dcr", "2024-12-17T13:00:52Z"
177+
),
178+
"context", Map.of("@base", "urn:nato:stanag:5636:A:1:elements:json")
179+
)
180+
);
162181
}
163182
}

sdk/src/test/java/io/opentdf/platform/sdk/ZipReaderTest.java

+2-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package io.opentdf.platform.sdk;
2-
import com.google.gson.GsonBuilder;
3-
4-
import io.opentdf.platform.sdk.Manifest.ManifestDeserializer;
52

3+
import com.google.gson.Gson;
64
import org.apache.commons.compress.archivers.zip.Zip64Mode;
75
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
86
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
@@ -22,9 +20,6 @@
2220
import java.util.stream.IntStream;
2321

2422
import static org.assertj.core.api.Assertions.assertThat;
25-
import static org.junit.jupiter.api.Assertions.assertEquals;
26-
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
27-
import static org.junit.jupiter.api.Assertions.assertNotNull;
2823

2924
public class ZipReaderTest {
3025

@@ -47,9 +42,7 @@ protected static void testReadingZipChannel(SeekableByteChannel fileChannel, boo
4742
if (entry.getName().endsWith(".json")) {
4843
entry.getData().transferTo(stream);
4944
var data = stream.toString(StandardCharsets.UTF_8);
50-
var gson = new GsonBuilder()
51-
.registerTypeAdapter(Manifest.class, new ManifestDeserializer())
52-
.create();
45+
var gson = new Gson();
5346
var map = gson.fromJson(data, Map.class);
5447

5548
if (test) {

0 commit comments

Comments
 (0)