diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 7d52239a..fdbd9711 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -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: diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index 513c226b..62cec9aa 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -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; @@ -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; @@ -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; @@ -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, @@ -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)); } @@ -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 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 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); + } } } } 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 c31fc1b0..9d9b6084 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java @@ -119,5 +119,5 @@ public int hashCode() { public Scope scope; public AppliesToState appliesToState; public Statement statement; - public AssertionKey assertionKey; + public AssertionKey signingKey; } \ No newline at end of file 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 4bbd8008..d6e72aae 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -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); 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 326809f3..fdf3144d 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -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), @@ -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( @@ -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( @@ -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),