Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add assertion verification #216

Merged
merged 8 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading