Skip to content

Commit

Permalink
feat: Add assertion verification (#216)
Browse files Browse the repository at this point in the history
Adding wither-assertion-verification-keys to the cli, support for pasing
in signing key pems in the assertions json string
  • Loading branch information
elizabethhealy authored Dec 16, 2024
1 parent cf6f932 commit e0f8caf
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 12 deletions.
79 changes: 79 additions & 0 deletions .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,85 @@ jobs:
fi
working-directory: cmdline

- name: Encrypt/Decrypt Assertions
run: |
echo "basic assertions"
echo 'here is some data to encrypt' > data
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\"}"}}]'
java -jar target/cmdline.jar \
--client-id=opentdf-sdk \
--client-secret=secret \
--platform-endpoint=localhost:8080 \
-h\
encrypt --kas-url=localhost:8080 --mime-type=text/plain --with-assertions=$ASSERTIONS --autoconfigure=false -f data -m 'here is some metadata' > test.tdf
java -jar target/cmdline.jar \
--client-id=opentdf-sdk \
--client-secret=secret \
--platform-endpoint=localhost:8080 \
-h\
decrypt -f test.tdf > decrypted
if ! diff -q data decrypted; then
printf 'decrypted data is incorrect [%s]' "$(< decrypted)"
exit 1
fi
HS256_KEY=$(openssl rand -base64 32)
openssl genpkey -algorithm RSA -out rs_private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in rs_private_key.pem -out rs_public_key.pem
RS256_PRIVATE_KEY=$(awk '{printf "%s\\n", $0}' rs_private_key.pem)
RS256_PUBLIC_KEY=$(awk '{printf "%s\\n", $0}' rs_public_key.pem)
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'"}}]'
SIGNED_ASSERTION_VERIFICATON_HS256='{"keys":{"assertion1":{"alg":"HS256","key":"'$HS256_KEY'"}}}'
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'"}}]'
SIGNED_ASSERTION_VERIFICATON_RS256='{"keys":{"assertion1":{"alg":"RS256","key":"'$RS256_PUBLIC_KEY'"}}}'
echo "hs256 assertions"
java -jar target/cmdline.jar \
--client-id=opentdf-sdk \
--client-secret=secret \
--platform-endpoint=localhost:8080 \
-h\
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
java -jar target/cmdline.jar \
--client-id=opentdf-sdk \
--client-secret=secret \
--platform-endpoint=localhost:8080 \
-h\
decrypt --with-assertion-verification-keys="$SIGNED_ASSERTION_VERIFICATON_HS256" -f test.tdf > decrypted
if ! diff -q data decrypted; then
printf 'decrypted data is incorrect [%s]' "$(< decrypted)"
exit 1
fi
echo "rs256 assertions"
java -jar target/cmdline.jar \
--client-id=opentdf-sdk \
--client-secret=secret \
--platform-endpoint=localhost:8080 \
-h\
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
java -jar target/cmdline.jar \
--client-id=opentdf-sdk \
--client-secret=secret \
--platform-endpoint=localhost:8080 \
-h\
decrypt --with-assertion-verification-keys "$SIGNED_ASSERTION_VERIFICATON_RS256" -f test.tdf > decrypted
if ! diff -q data decrypted; then
printf 'decrypted data is incorrect [%s]' "$(< decrypted)"
exit 1
fi
working-directory: cmdline

- name: Start additional kas
uses: opentdf/platform/test/start-additional-kas@main
with:
Expand Down
146 changes: 141 additions & 5 deletions cmdline/src/main/java/io/opentdf/platform/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
import com.nimbusds.jose.JOSEException;
import io.opentdf.platform.sdk.*;
import io.opentdf.platform.sdk.TDF;
import io.opentdf.platform.sdk.Config.AssertionVerificationKeys;

import com.google.gson.Gson;
import org.apache.commons.codec.DecoderException;
import org.bouncycastle.crypto.RuntimeCryptoException;

import picocli.CommandLine;
import picocli.CommandLine.HelpCommand;
import picocli.CommandLine.Option;
Expand All @@ -22,14 +25,24 @@
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
Expand All @@ -39,9 +52,15 @@

import javax.net.ssl.TrustManager;


@CommandLine.Command(name = "tdf", subcommands = {HelpCommand.class})
class Command {

private static final String PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----";
private static final String PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----";
private static final String PEM_HEADER = "-----BEGIN (.*)-----";
private static final String PEM_FOOTER = "-----END (.*)-----";

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

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

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);
return key;
} else if (key instanceof byte[]) {
return key;
} else {
throw new RuntimeException("Unexpected type for assertion key");
}
} else if (alg == AssertionConfig.AssertionKeyAlg.RS256) {
if (!(key instanceof String)) {
throw new RuntimeException("Unexpected type for assertion key");
}
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();
byte[] decoded = Base64.getDecoder().decode(base64EncodedPem);
X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
KeyFactory kf = null;
try {
kf = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
try {
return kf.generatePublic(spec);
} catch (InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}else {
String privateKeyPEM = pemWithNewlines
.replace(PRIVATE_KEY_HEADER, "")
.replace(PRIVATE_KEY_FOOTER, "")
.replaceAll("\\s", ""); // remove whitespaces

byte[] decoded = Base64.getDecoder().decode(privateKeyPEM);

PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
KeyFactory kf = null;
try {
kf = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
try {
return kf.generatePrivate(spec);
} catch (InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}
}
return null;
}

@CommandLine.Command(name = "encrypt")
void encrypt(
@Option(names = { "-f", "--file" }, defaultValue = Option.NULL_VALUE) Optional<File> file,
Expand Down Expand Up @@ -92,9 +173,29 @@ void encrypt(
try {
assertionConfigs = gson.fromJson(assertionConfig, AssertionConfig[].class);
} catch (JsonSyntaxException e) {
throw new RuntimeException("Failed to parse assertion, expects an list of assertions", e);
// try it as a file path
try {
String fielJson = new String(Files.readAllBytes(Paths.get(assertionConfig)));
assertionConfigs = gson.fromJson(fielJson, 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("Could not parse assertion as json string or path to file", e3);
}
}
// iterate through the assertions and correct the key types
for (int i = 0; i < assertionConfigs.length; i++) {
AssertionConfig config = assertionConfigs[i];
if (config.signingKey != null && config.signingKey.isDefined()) {
try {
Object correctedKey = correctKeyType(config.signingKey.alg, config.signingKey.key, false);
config.signingKey.key = correctedKey;
} catch (Exception e) {
throw new RuntimeException("Error with assertion signing key: " + e.getMessage(), e);
}
}
assertionConfigs[i] = config;
}

configs.add(Config.withAssertionConfig(assertionConfigs));
}

Expand Down Expand Up @@ -126,15 +227,50 @@ private SDK buildSDK() {
}

@CommandLine.Command(name = "decrypt")
void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath) throws IOException,
void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath,
@Option(names = { "--with-assertion-verification-keys" }, defaultValue = Option.NULL_VALUE) Optional<String> assertionVerification)
throws IOException,
InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException,
BadPaddingException, InvalidKeyException, TDF.FailedToCreateGMAC,
JOSEException, ParseException, NoSuchAlgorithmException, DecoderException {
var sdk = buildSDK();
try (var in = FileChannel.open(tdfPath, StandardOpenOption.READ)) {
try (var stdout = new BufferedOutputStream(System.out)) {
var reader = new TDF().loadTDF(in, sdk.getServices().kas());
reader.readPayload(stdout);
if (assertionVerification.isPresent()) {
var assertionVerificationInput = assertionVerification.get();
Gson gson = new Gson();

AssertionVerificationKeys assertionVerificationKeys;
try {
assertionVerificationKeys = gson.fromJson(assertionVerificationInput, AssertionVerificationKeys.class);
} catch (JsonSyntaxException e) {
// try it as a file path
try {
String fileJson = new String(Files.readAllBytes(Paths.get(assertionVerificationInput)));
assertionVerificationKeys = gson.fromJson(fileJson, AssertionVerificationKeys.class);
} 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);
}
}

for (Map.Entry<String, AssertionConfig.AssertionKey> 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);
}
}
Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig(
Config.withAssertionVerificationKeys(assertionVerificationKeys));
var reader = new TDF().loadTDF(in, sdk.getServices().kas(), readerConfig);
reader.readPayload(stdout);
} else {
var reader = new TDF().loadTDF(in, sdk.getServices().kas());
reader.readPayload(stdout);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,5 @@ public int hashCode() {
public Scope scope;
public AppliesToState appliesToState;
public Statement statement;
public AssertionKey assertionKey;
public AssertionKey signingKey;
}
4 changes: 2 additions & 2 deletions sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -542,8 +542,8 @@ public TDFObject createTDF(InputStream payload,

var assertionSigningKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256,
tdfObject.aesGcm.getKey());
if (assertionConfig.assertionKey != null && assertionConfig.assertionKey.isDefined()) {
assertionSigningKey = assertionConfig.assertionKey;
if (assertionConfig.signingKey != null && assertionConfig.signingKey.isDefined()) {
assertionSigningKey = assertionConfig.signingKey;
}

assertion.sign(new Manifest.Assertion.HashValues(assertionHash, encodedHash), assertionSigningKey);
Expand Down
8 changes: 4 additions & 4 deletions sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ void testSimpleTDFEncryptAndDecrypt() throws Exception {
assertion1.statement.format = "base64binary";
assertion1.statement.schema = "text";
assertion1.statement.value = "ICAgIDxlZGoOkVkaD4=";
assertion1.assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key);
assertion1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key);

Config.TDFConfig config = Config.newTDFConfig(
Config.withAutoconfigure(false),
Expand Down Expand Up @@ -151,7 +151,7 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception {
assertionConfig.statement.format = "base64binary";
assertionConfig.statement.schema = "text";
assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4=";
assertionConfig.assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256,
assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256,
keypair.getPrivate());

Config.TDFConfig config = Config.newTDFConfig(
Expand Down Expand Up @@ -195,7 +195,7 @@ void testWithAssertionVerificationDisabled() throws Exception {
assertionConfig.statement.format = "base64binary";
assertionConfig.statement.schema = "text";
assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4=";
assertionConfig.assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256,
assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256,
keypair.getPrivate());

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

Config.TDFConfig config = Config.newTDFConfig(
Config.withAutoconfigure(false),
Expand Down

0 comments on commit e0f8caf

Please sign in to comment.