Skip to content
Open
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
45 changes: 13 additions & 32 deletions sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.util.X509CertUtils;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import io.opentdf.platform.sdk.SDK.AssertionException;
Expand All @@ -52,7 +51,6 @@
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.AbstractList;
import java.util.ArrayList;
Expand Down Expand Up @@ -1008,6 +1006,11 @@ public void sign(final HashValues hashValues, final AssertionConfig.AssertionKey
// Checks the binding signature of the assertion and
// returns the hash and the signature. It returns an error if the verification
// fails.
//
// assertionKey is the key the reader selected for this assertion: a configured
// key, or the payload key with HS256. It is the only key used, and only with its
// own algorithm. A jwk or x5c in the JWS header never supplies the verification
// key, and the header's alg must equal the key's algorithm.
public Assertion.HashValues verify(AssertionConfig.AssertionKey assertionKey)
throws ParseException, JOSEException, java.security.cert.CertificateException {
if (binding == null) {
Expand All @@ -1018,38 +1021,16 @@ public Assertion.HashValues verify(AssertionConfig.AssertionKey assertionKey)
binding = null; // Clear the binding after use

SignedJWT signedJWT = SignedJWT.parse(signatureString);
JWSHeader header = signedJWT.getHeader();
JWSVerifier verifier = null;

// Check for JWK in header
if (header.getJWK() != null) {
try {
verifier = createVerifier(header.getJWK());
} catch (JOSEException e) {
throw new SDKException("Invalid JWK in JWT header", e);
}
}

// Check for X.509 certificate chain in header
if (verifier == null && header.getX509CertChain() != null && !header.getX509CertChain().isEmpty()) {
try {
X509Certificate cert = X509CertUtils.parse(header.getX509CertChain().get(0).decode());
if (cert.getPublicKey() instanceof RSAPublicKey) {
verifier = createVerifier((RSAPublicKey) cert.getPublicKey());
} else {
throw new SDKException("Unsupported public key type in X.509 certificate");
}
} catch (IllegalArgumentException e) {
throw new SDKException("Invalid Base64 in X.509 certificate in JWT header", e);
}
JWSVerifier verifier = createVerifier(assertionKey);

// RSASSAVerifier and MACVerifier accept every RS* or HS* algorithm, so pin it here.
JWSAlgorithm keyAlg = JWSAlgorithm.parse(assertionKey.alg.name());
JWSAlgorithm headerAlg = signedJWT.getHeader().getAlgorithm();
if (!keyAlg.equals(headerAlg)) {
throw new AssertionException("binding alg " + headerAlg
+ " does not match the verification key's alg " + keyAlg, this.id);
}


if (verifier == null) {
verifier = createVerifier(assertionKey);
}


if (!signedJWT.verify(verifier)) {
throw new SDKException("Unable to verify assertion signature");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package io.opentdf.platform.sdk;

import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.junit.jupiter.api.Test;

import java.security.KeyPair;
import java.security.SecureRandom;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* An assertion binding is verified only with the key the reader selected for it, and only with
* that key's algorithm. A jwk or x5c in the binding's JWS header is never used as the
* verification key.
*/
class AssertionConfiguredKeyTest {

private static final String ID = "assertion1";

private static Manifest.Assertion newAssertion() {
var assertion = new Manifest.Assertion();
assertion.id = ID;
assertion.type = AssertionConfig.Type.BaseAssertion.toString();
assertion.scope = AssertionConfig.Scope.TrustedDataObj.toString();
assertion.appliesToState = AssertionConfig.AppliesToState.Unencrypted.toString();
assertion.statement = new AssertionConfig.Statement();
assertion.statement.format = "string";
assertion.statement.schema = "text";
assertion.statement.value = "value";
return assertion;
}

private static Manifest.Assertion signedWith(AssertionConfig.AssertionKey signingKey) throws Exception {
var assertion = newAssertion();
assertion.sign(new Manifest.Assertion.HashValues("hash", "sig"), signingKey);
return assertion;
}

/** An assertion whose binding is a JWS with the given header alg, signed by {@code signer}. */
private static Manifest.Assertion boundWith(JWSAlgorithm alg, com.nimbusds.jose.JWSSigner signer)
throws Exception {
var claims = new JWTClaimsSet.Builder()
.claim("assertionHash", "hash")
.claim("assertionSig", "sig")
.build();
var jwt = new SignedJWT(new JWSHeader(alg), claims);
jwt.sign(signer);
var assertion = newAssertion();
assertion.binding = new Manifest.Binding();
assertion.binding.method = "jws";
assertion.binding.signature = jwt.serialize();
return assertion;
}

private static AssertionConfig.AssertionKey rs256(Object key) {
return new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, key);
}

private static AssertionConfig.AssertionKey hs256(byte[] key) {
return new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key);
}

private static byte[] secret(int length) {
var secret = new byte[length];
new SecureRandom().nextBytes(secret);
return secret;
}

private static JWK publicJwk(KeyPair keypair) throws Exception {
return JWK.parse(CryptoUtils.getPublicKeyJWK(keypair.getPublic())).toPublicJWK();
}

@Test
void embeddedJwkOfAnotherKeyDoesNotVerify() throws Exception {
var configured = CryptoUtils.generateRSAKeypair();
var other = CryptoUtils.generateRSAKeypair();
var assertion = signedWith(rs256(other.getPrivate()).withJwk(publicJwk(other)));
var verificationKey = rs256(configured.getPublic());

assertThatThrownBy(() -> assertion.verify(verificationKey))
.isInstanceOf(SDKException.class);
}

@Test
void embeddedX5cOfAnotherKeyDoesNotVerify() throws Exception {
var configured = CryptoUtils.generateRSAKeypair();
var other = CryptoUtils.generateRSAKeypair();
var cert = TestUtil.createTestCertificate(other.getPublic(), other.getPrivate());
var x5c = List.of(com.nimbusds.jose.util.Base64.encode(cert.getEncoded()));
var assertion = signedWith(rs256(other.getPrivate()).withX5c(x5c));
var verificationKey = rs256(configured.getPublic());

assertThatThrownBy(() -> assertion.verify(verificationKey))
.isInstanceOf(SDKException.class);
}

@Test
void embeddedJwkDoesNotReplaceThePayloadKey() throws Exception {
// With no key configured, the reader selects the payload key with HS256.
var signer = CryptoUtils.generateRSAKeypair();
var assertion = signedWith(rs256(signer.getPrivate()).withJwk(publicJwk(signer)));
var verificationKey = hs256(secret(32));

assertThatThrownBy(() -> assertion.verify(verificationKey))
.isInstanceOf(SDK.AssertionException.class)
.hasMessageContaining(ID);
}

@Test
void embeddedJwkOfTheConfiguredKeyVerifies() throws Exception {
var signer = CryptoUtils.generateRSAKeypair();
var assertion = signedWith(rs256(signer.getPrivate()).withJwk(publicJwk(signer)));

var hashValues = assertion.verify(rs256(signer.getPublic()));

assertThat(hashValues.getAssertionHash()).isEqualTo("hash");
assertThat(hashValues.getSignature()).isEqualTo("sig");
}

@Test
void headerAlgMustEqualTheRsaKeyAlg() throws Exception {
var signer = CryptoUtils.generateRSAKeypair();
var verificationKey = rs256(signer.getPublic());
for (var alg : List.of(JWSAlgorithm.RS384, JWSAlgorithm.RS512, JWSAlgorithm.PS256)) {
var assertion = boundWith(alg, new RSASSASigner(signer.getPrivate()));
assertThatThrownBy(() -> assertion.verify(verificationKey))
.as(alg.getName())
.isInstanceOf(SDK.AssertionException.class);
}
var control = boundWith(JWSAlgorithm.RS256, new RSASSASigner(signer.getPrivate()));
assertThat(control.verify(rs256(signer.getPublic())).getAssertionHash()).isEqualTo("hash");
}

@Test
void headerAlgMustEqualTheHmacKeyAlg() throws Exception {
var key = secret(64);
var verificationKey = hs256(key);
for (var alg : List.of(JWSAlgorithm.HS384, JWSAlgorithm.HS512)) {
var assertion = boundWith(alg, new MACSigner(key));
assertThatThrownBy(() -> assertion.verify(verificationKey))
.as(alg.getName())
.isInstanceOf(SDK.AssertionException.class);
}
var control = boundWith(JWSAlgorithm.HS256, new MACSigner(key));
assertThat(control.verify(hs256(key)).getAssertionHash()).isEqualTo("hash");
}
}
46 changes: 32 additions & 14 deletions sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.connectrpc.ResponseMessage;
import com.connectrpc.UnaryBlockingCall;
import com.google.gson.reflect.TypeToken;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWK;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
Expand Down Expand Up @@ -455,11 +454,37 @@ void testVerifyAssertionWithJwkHeaderInJWT() throws Exception {
.setKeyAccessServerRegistryService(kasRegistryService).build());
tdf.createTDF(plainTextInputStream, tdfOutputStream, config);

assertEmbeddedKeyAddsNoTrust(tdf, tdfOutputStream.toByteArray(), keypair, plainText);
}

/**
* A TDF whose assertion was signed by {@code signer} and carries the signer's public key in
* its JWS header. The embedded key grants nothing by itself: with no key configured the
* payload key with HS256 is selected and the RS256 binding fails, and an unrelated configured
* key fails too. Configuring the signer's key is what makes it verify.
*/
private void assertEmbeddedKeyAddsNoTrust(TDF tdf, byte[] tdfBytes, KeyPair signer, String plainText)
throws Exception {
var noKeyChannel = new SeekableInMemoryByteChannel(tdfBytes);
var noKeyConfig = Config.newTDFReaderConfig();
assertThatThrownBy(() -> tdf.loadTDF(noKeyChannel, noKeyConfig, platformUrl))
.isInstanceOf(SDK.AssertionException.class);

var unrelated = new Config.AssertionVerificationKeys();
unrelated.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256,
CryptoUtils.generateRSAKeypair().getPublic());
var unrelatedChannel = new SeekableInMemoryByteChannel(tdfBytes);
var unrelatedConfig = Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(unrelated));
assertThatThrownBy(() -> tdf.loadTDF(unrelatedChannel, unrelatedConfig, platformUrl))
.isInstanceOf(SDKException.class);

var configured = new Config.AssertionVerificationKeys();
configured.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256,
signer.getPublic());
var unwrappedData = new ByteArrayOutputStream();
var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()),
Config.newTDFReaderConfig(), platformUrl);
var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfBytes),
Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(configured)), platformUrl);
reader.readPayload(unwrappedData);

assertThat(unwrappedData.toString(StandardCharsets.UTF_8))
.withFailMessage("extracted data does not match")
.isEqualTo(plainText);
Expand Down Expand Up @@ -503,14 +528,7 @@ void testVerifyAssertionWithX5cHeaderInJWT() throws Exception {
.setKeyAccessServerRegistryService(kasRegistryService).build());
tdf.createTDF(plainTextInputStream, tdfOutputStream, config);

var unwrappedData = new ByteArrayOutputStream();
var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()),
Config.newTDFReaderConfig(), platformUrl);
reader.readPayload(unwrappedData);

assertThat(unwrappedData.toString(StandardCharsets.UTF_8))
.withFailMessage("extracted data does not match")
.isEqualTo(plainText);
assertEmbeddedKeyAddsNoTrust(tdf, tdfOutputStream.toByteArray(), keypair, plainText);
}

@Test
Expand Down Expand Up @@ -551,10 +569,10 @@ void testWithAssertionVerificationDisabled() throws Exception {
var unwrappedData = new ByteArrayOutputStream();
var dataToUnwrap = new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray());
var emptyConfig = Config.newTDFReaderConfig();
var thrown = assertThrows(SDKException.class, () -> {
// no key configured, so the payload key with HS256 is selected, and the RS256 binding fails
assertThrows(SDK.AssertionException.class, () -> {
tdf.loadTDF(dataToUnwrap, emptyConfig, platformUrl);
});
assertThat(thrown.getCause()).isInstanceOf(JOSEException.class);

// try with assertion verification disabled and not passing the assertion
// verification keys
Expand Down
Loading