Skip to content

Commit e0f8caf

Browse files
feat: Add assertion verification (#216)
Adding wither-assertion-verification-keys to the cli, support for pasing in signing key pems in the assertions json string
1 parent cf6f932 commit e0f8caf

File tree

5 files changed

+227
-12
lines changed

5 files changed

+227
-12
lines changed

.github/workflows/checks.yaml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,85 @@ jobs:
178178
fi
179179
working-directory: cmdline
180180

181+
- name: Encrypt/Decrypt Assertions
182+
run: |
183+
echo "basic assertions"
184+
echo 'here is some data to encrypt' > data
185+
186+
ASSERTIONS='[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"}}]'
187+
188+
java -jar target/cmdline.jar \
189+
--client-id=opentdf-sdk \
190+
--client-secret=secret \
191+
--platform-endpoint=localhost:8080 \
192+
-h\
193+
encrypt --kas-url=localhost:8080 --mime-type=text/plain --with-assertions=$ASSERTIONS --autoconfigure=false -f data -m 'here is some metadata' > test.tdf
194+
195+
java -jar target/cmdline.jar \
196+
--client-id=opentdf-sdk \
197+
--client-secret=secret \
198+
--platform-endpoint=localhost:8080 \
199+
-h\
200+
decrypt -f test.tdf > decrypted
201+
202+
if ! diff -q data decrypted; then
203+
printf 'decrypted data is incorrect [%s]' "$(< decrypted)"
204+
exit 1
205+
fi
206+
207+
HS256_KEY=$(openssl rand -base64 32)
208+
openssl genpkey -algorithm RSA -out rs_private_key.pem -pkeyopt rsa_keygen_bits:2048
209+
openssl rsa -pubout -in rs_private_key.pem -out rs_public_key.pem
210+
RS256_PRIVATE_KEY=$(awk '{printf "%s\\n", $0}' rs_private_key.pem)
211+
RS256_PUBLIC_KEY=$(awk '{printf "%s\\n", $0}' rs_public_key.pem)
212+
SIGNED_ASSERTIONS_HS256='[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"},"signingKey":{"alg":"HS256","key":"'$HS256_KEY'"}}]'
213+
SIGNED_ASSERTION_VERIFICATON_HS256='{"keys":{"assertion1":{"alg":"HS256","key":"'$HS256_KEY'"}}}'
214+
SIGNED_ASSERTIONS_RS256='[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"},"signingKey":{"alg":"RS256","key":"'$RS256_PRIVATE_KEY'"}}]'
215+
SIGNED_ASSERTION_VERIFICATON_RS256='{"keys":{"assertion1":{"alg":"RS256","key":"'$RS256_PUBLIC_KEY'"}}}'
216+
217+
echo "hs256 assertions"
218+
219+
java -jar target/cmdline.jar \
220+
--client-id=opentdf-sdk \
221+
--client-secret=secret \
222+
--platform-endpoint=localhost:8080 \
223+
-h\
224+
encrypt --kas-url=localhost:8080 --mime-type=text/plain --with-assertions="$SIGNED_ASSERTIONS_HS256" --autoconfigure=false -f data -m 'here is some metadata' > test.tdf
225+
226+
java -jar target/cmdline.jar \
227+
--client-id=opentdf-sdk \
228+
--client-secret=secret \
229+
--platform-endpoint=localhost:8080 \
230+
-h\
231+
decrypt --with-assertion-verification-keys="$SIGNED_ASSERTION_VERIFICATON_HS256" -f test.tdf > decrypted
232+
233+
if ! diff -q data decrypted; then
234+
printf 'decrypted data is incorrect [%s]' "$(< decrypted)"
235+
exit 1
236+
fi
237+
238+
echo "rs256 assertions"
239+
240+
java -jar target/cmdline.jar \
241+
--client-id=opentdf-sdk \
242+
--client-secret=secret \
243+
--platform-endpoint=localhost:8080 \
244+
-h\
245+
encrypt --kas-url=localhost:8080 --mime-type=text/plain --with-assertions "$SIGNED_ASSERTIONS_RS256" --autoconfigure=false -f data -m 'here is some metadata' > test.tdf
246+
247+
java -jar target/cmdline.jar \
248+
--client-id=opentdf-sdk \
249+
--client-secret=secret \
250+
--platform-endpoint=localhost:8080 \
251+
-h\
252+
decrypt --with-assertion-verification-keys "$SIGNED_ASSERTION_VERIFICATON_RS256" -f test.tdf > decrypted
253+
254+
if ! diff -q data decrypted; then
255+
printf 'decrypted data is incorrect [%s]' "$(< decrypted)"
256+
exit 1
257+
fi
258+
working-directory: cmdline
259+
181260
- name: Start additional kas
182261
uses: opentdf/platform/test/start-additional-kas@main
183262
with:

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

Lines changed: 141 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
import com.nimbusds.jose.JOSEException;
55
import io.opentdf.platform.sdk.*;
66
import io.opentdf.platform.sdk.TDF;
7+
import io.opentdf.platform.sdk.Config.AssertionVerificationKeys;
78

89
import com.google.gson.Gson;
910
import org.apache.commons.codec.DecoderException;
11+
import org.bouncycastle.crypto.RuntimeCryptoException;
12+
1013
import picocli.CommandLine;
1114
import picocli.CommandLine.HelpCommand;
1215
import picocli.CommandLine.Option;
@@ -22,14 +25,24 @@
2225
import java.io.PrintWriter;
2326
import java.nio.ByteBuffer;
2427
import java.nio.channels.FileChannel;
28+
import java.nio.charset.StandardCharsets;
29+
import java.nio.file.Files;
2530
import java.nio.file.Path;
31+
import java.nio.file.Paths;
2632
import java.nio.file.StandardOpenOption;
2733
import java.security.InvalidAlgorithmParameterException;
2834
import java.security.InvalidKeyException;
2935
import java.security.NoSuchAlgorithmException;
36+
import java.security.spec.InvalidKeySpecException;
37+
import java.security.spec.PKCS8EncodedKeySpec;
38+
import java.security.spec.X509EncodedKeySpec;
39+
import java.security.KeyFactory;
40+
import java.security.PrivateKey;
3041
import java.text.ParseException;
3142
import java.util.ArrayList;
43+
import java.util.Base64;
3244
import java.util.List;
45+
import java.util.Map;
3346
import java.util.Optional;
3447
import java.util.concurrent.ExecutionException;
3548
import java.util.function.Consumer;
@@ -39,9 +52,15 @@
3952

4053
import javax.net.ssl.TrustManager;
4154

55+
4256
@CommandLine.Command(name = "tdf", subcommands = {HelpCommand.class})
4357
class Command {
4458

59+
private static final String PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----";
60+
private static final String PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----";
61+
private static final String PEM_HEADER = "-----BEGIN (.*)-----";
62+
private static final String PEM_FOOTER = "-----END (.*)-----";
63+
4564
@Option(names = { "--client-secret" }, required = true)
4665
private String clientSecret;
4766

@@ -57,6 +76,68 @@ class Command {
5776
@Option(names = { "-p", "--platform-endpoint" }, required = true)
5877
private String platformEndpoint;
5978

79+
private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, boolean publicKey) throws RuntimeException{
80+
if (alg == AssertionConfig.AssertionKeyAlg.HS256) {
81+
if (key instanceof String) {
82+
key = ((String) key).getBytes(StandardCharsets.UTF_8);
83+
return key;
84+
} else if (key instanceof byte[]) {
85+
return key;
86+
} else {
87+
throw new RuntimeException("Unexpected type for assertion key");
88+
}
89+
} else if (alg == AssertionConfig.AssertionKeyAlg.RS256) {
90+
if (!(key instanceof String)) {
91+
throw new RuntimeException("Unexpected type for assertion key");
92+
}
93+
String pem = (String) key;
94+
String pemWithNewlines = pem.replace("\\n", "\n");
95+
if (publicKey){
96+
String base64EncodedPem= pemWithNewlines
97+
.replaceAll(PEM_HEADER, "")
98+
.replaceAll(PEM_FOOTER, "")
99+
.replaceAll("\\s", "")
100+
.replaceAll("\r\n", "")
101+
.replaceAll("\n", "")
102+
.trim();
103+
byte[] decoded = Base64.getDecoder().decode(base64EncodedPem);
104+
X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
105+
KeyFactory kf = null;
106+
try {
107+
kf = KeyFactory.getInstance("RSA");
108+
} catch (NoSuchAlgorithmException e) {
109+
throw new RuntimeException(e);
110+
}
111+
try {
112+
return kf.generatePublic(spec);
113+
} catch (InvalidKeySpecException e) {
114+
throw new RuntimeException(e);
115+
}
116+
}else {
117+
String privateKeyPEM = pemWithNewlines
118+
.replace(PRIVATE_KEY_HEADER, "")
119+
.replace(PRIVATE_KEY_FOOTER, "")
120+
.replaceAll("\\s", ""); // remove whitespaces
121+
122+
byte[] decoded = Base64.getDecoder().decode(privateKeyPEM);
123+
124+
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
125+
KeyFactory kf = null;
126+
try {
127+
kf = KeyFactory.getInstance("RSA");
128+
} catch (NoSuchAlgorithmException e) {
129+
throw new RuntimeException(e);
130+
}
131+
try {
132+
return kf.generatePrivate(spec);
133+
} catch (InvalidKeySpecException e) {
134+
throw new RuntimeException(e);
135+
}
136+
}
137+
}
138+
return null;
139+
}
140+
60141
@CommandLine.Command(name = "encrypt")
61142
void encrypt(
62143
@Option(names = { "-f", "--file" }, defaultValue = Option.NULL_VALUE) Optional<File> file,
@@ -92,9 +173,29 @@ void encrypt(
92173
try {
93174
assertionConfigs = gson.fromJson(assertionConfig, AssertionConfig[].class);
94175
} catch (JsonSyntaxException e) {
95-
throw new RuntimeException("Failed to parse assertion, expects an list of assertions", e);
176+
// try it as a file path
177+
try {
178+
String fielJson = new String(Files.readAllBytes(Paths.get(assertionConfig)));
179+
assertionConfigs = gson.fromJson(fielJson, AssertionConfig[].class);
180+
} catch (JsonSyntaxException e2) {
181+
throw new RuntimeException("Failed to parse assertion from file, expects an list of assertions", e2);
182+
} catch(Exception e3) {
183+
throw new RuntimeException("Could not parse assertion as json string or path to file", e3);
184+
}
185+
}
186+
// iterate through the assertions and correct the key types
187+
for (int i = 0; i < assertionConfigs.length; i++) {
188+
AssertionConfig config = assertionConfigs[i];
189+
if (config.signingKey != null && config.signingKey.isDefined()) {
190+
try {
191+
Object correctedKey = correctKeyType(config.signingKey.alg, config.signingKey.key, false);
192+
config.signingKey.key = correctedKey;
193+
} catch (Exception e) {
194+
throw new RuntimeException("Error with assertion signing key: " + e.getMessage(), e);
195+
}
196+
}
197+
assertionConfigs[i] = config;
96198
}
97-
98199
configs.add(Config.withAssertionConfig(assertionConfigs));
99200
}
100201

@@ -126,15 +227,50 @@ private SDK buildSDK() {
126227
}
127228

128229
@CommandLine.Command(name = "decrypt")
129-
void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath) throws IOException,
230+
void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath,
231+
@Option(names = { "--with-assertion-verification-keys" }, defaultValue = Option.NULL_VALUE) Optional<String> assertionVerification)
232+
throws IOException,
130233
InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException,
131234
BadPaddingException, InvalidKeyException, TDF.FailedToCreateGMAC,
132235
JOSEException, ParseException, NoSuchAlgorithmException, DecoderException {
133236
var sdk = buildSDK();
134237
try (var in = FileChannel.open(tdfPath, StandardOpenOption.READ)) {
135238
try (var stdout = new BufferedOutputStream(System.out)) {
136-
var reader = new TDF().loadTDF(in, sdk.getServices().kas());
137-
reader.readPayload(stdout);
239+
if (assertionVerification.isPresent()) {
240+
var assertionVerificationInput = assertionVerification.get();
241+
Gson gson = new Gson();
242+
243+
AssertionVerificationKeys assertionVerificationKeys;
244+
try {
245+
assertionVerificationKeys = gson.fromJson(assertionVerificationInput, AssertionVerificationKeys.class);
246+
} catch (JsonSyntaxException e) {
247+
// try it as a file path
248+
try {
249+
String fileJson = new String(Files.readAllBytes(Paths.get(assertionVerificationInput)));
250+
assertionVerificationKeys = gson.fromJson(fileJson, AssertionVerificationKeys.class);
251+
} catch (JsonSyntaxException e2) {
252+
throw new RuntimeException("Failed to parse assertion verification keys from file", e2);
253+
} catch(Exception e3) {
254+
throw new RuntimeException("Could not parse assertion verification keys as json string or path to file", e3);
255+
}
256+
}
257+
258+
for (Map.Entry<String, AssertionConfig.AssertionKey> entry : assertionVerificationKeys.keys.entrySet()){
259+
try {
260+
Object correctedKey = correctKeyType(entry.getValue().alg, entry.getValue().key, true);
261+
entry.setValue(new AssertionConfig.AssertionKey(entry.getValue().alg, correctedKey));
262+
} catch (Exception e) {
263+
throw new RuntimeException("Error with assertion verification key: " + e.getMessage(), e);
264+
}
265+
}
266+
Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig(
267+
Config.withAssertionVerificationKeys(assertionVerificationKeys));
268+
var reader = new TDF().loadTDF(in, sdk.getServices().kas(), readerConfig);
269+
reader.readPayload(stdout);
270+
} else {
271+
var reader = new TDF().loadTDF(in, sdk.getServices().kas());
272+
reader.readPayload(stdout);
273+
}
138274
}
139275
}
140276
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,5 +119,5 @@ public int hashCode() {
119119
public Scope scope;
120120
public AppliesToState appliesToState;
121121
public Statement statement;
122-
public AssertionKey assertionKey;
122+
public AssertionKey signingKey;
123123
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,8 +542,8 @@ public TDFObject createTDF(InputStream payload,
542542

543543
var assertionSigningKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256,
544544
tdfObject.aesGcm.getKey());
545-
if (assertionConfig.assertionKey != null && assertionConfig.assertionKey.isDefined()) {
546-
assertionSigningKey = assertionConfig.assertionKey;
545+
if (assertionConfig.signingKey != null && assertionConfig.signingKey.isDefined()) {
546+
assertionSigningKey = assertionConfig.signingKey;
547547
}
548548

549549
assertion.sign(new Manifest.Assertion.HashValues(assertionHash, encodedHash), assertionSigningKey);

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ void testSimpleTDFEncryptAndDecrypt() throws Exception {
9797
assertion1.statement.format = "base64binary";
9898
assertion1.statement.schema = "text";
9999
assertion1.statement.value = "ICAgIDxlZGoOkVkaD4=";
100-
assertion1.assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key);
100+
assertion1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key);
101101

102102
Config.TDFConfig config = Config.newTDFConfig(
103103
Config.withAutoconfigure(false),
@@ -151,7 +151,7 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception {
151151
assertionConfig.statement.format = "base64binary";
152152
assertionConfig.statement.schema = "text";
153153
assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4=";
154-
assertionConfig.assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256,
154+
assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256,
155155
keypair.getPrivate());
156156

157157
Config.TDFConfig config = Config.newTDFConfig(
@@ -195,7 +195,7 @@ void testWithAssertionVerificationDisabled() throws Exception {
195195
assertionConfig.statement.format = "base64binary";
196196
assertionConfig.statement.schema = "text";
197197
assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4=";
198-
assertionConfig.assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256,
198+
assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256,
199199
keypair.getPrivate());
200200

201201
Config.TDFConfig config = Config.newTDFConfig(
@@ -314,7 +314,7 @@ void testSimpleTDFWithAssertionWithHS256Failure() throws Exception {
314314
assertionConfig1.statement.format = "base64binary";
315315
assertionConfig1.statement.schema = "text";
316316
assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4=";
317-
assertionConfig1.assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key);
317+
assertionConfig1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key);
318318

319319
Config.TDFConfig config = Config.newTDFConfig(
320320
Config.withAutoconfigure(false),

0 commit comments

Comments
 (0)