Skip to content

Commit b4f95e6

Browse files
feat(sdk): Add and expose tamper error types (#187)
Exception type for tamper detection -- make it the parent to the exception types we want to be able to catch for tamper --------- Co-authored-by: Dave Mihalcik <[email protected]>
1 parent 94b161d commit b4f95e6

File tree

4 files changed

+106
-15
lines changed

4 files changed

+106
-15
lines changed

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99
import com.nimbusds.jwt.JWTClaimsSet;
1010
import com.nimbusds.jwt.SignedJWT;
1111
import io.grpc.ManagedChannel;
12+
import io.grpc.StatusRuntimeException;
13+
import io.grpc.Status;
1214
import io.opentdf.platform.kas.AccessServiceGrpc;
1315
import io.opentdf.platform.kas.PublicKeyRequest;
1416
import io.opentdf.platform.kas.PublicKeyResponse;
1517
import io.opentdf.platform.kas.RewrapRequest;
18+
import io.opentdf.platform.kas.RewrapResponse;
1619
import io.opentdf.platform.sdk.Config.KASInfo;
1720
import io.opentdf.platform.sdk.nanotdf.ECKeyPair;
1821
import io.opentdf.platform.sdk.nanotdf.NanoTDFType;
22+
import io.opentdf.platform.sdk.TDF.KasBadRequestException;
1923

2024
import java.security.MessageDigest;
2125
import java.security.NoSuchAlgorithmException;
@@ -182,9 +186,19 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy) {
182186
.newBuilder()
183187
.setSignedRequestToken(jwt.serialize())
184188
.build();
185-
var response = getStub(keyAccess.url).rewrap(request);
186-
var wrappedKey = response.getEntityWrappedKey().toByteArray();
187-
return decryptor.decrypt(wrappedKey);
189+
RewrapResponse response;
190+
try {
191+
response = getStub(keyAccess.url).rewrap(request);
192+
var wrappedKey = response.getEntityWrappedKey().toByteArray();
193+
return decryptor.decrypt(wrappedKey);
194+
} catch (StatusRuntimeException e) {
195+
if (e.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) {
196+
// 400 Bad Request
197+
throw new KasBadRequestException("rewrap request 400: " + e.toString());
198+
}
199+
throw e;
200+
}
201+
188202
}
189203

190204
public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import com.nimbusds.jose.crypto.RSASSAVerifier;
1111
import com.nimbusds.jwt.JWTClaimsSet;
1212
import com.nimbusds.jwt.SignedJWT;
13+
14+
import io.opentdf.platform.sdk.TDF.AssertionException;
15+
1316
import org.apache.commons.codec.binary.Hex;
1417
import org.erdtman.jcs.JsonCanonicalizer;
1518

@@ -381,7 +384,7 @@ public void sign(final HashValues hashValues, final AssertionConfig.AssertionKey
381384
public Assertion.HashValues verify(AssertionConfig.AssertionKey assertionKey)
382385
throws ParseException, JOSEException {
383386
if (binding == null) {
384-
throw new SDKException("Binding is null in assertion");
387+
throw new AssertionException("Binding is null in assertion", this.id);
385388
}
386389

387390
String signatureString = binding.signature;

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

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,30 +119,48 @@ public FailedToCreateGMAC(String errorMessage) {
119119
}
120120
}
121121

122-
public static class NotValidateRootSignature extends RuntimeException {
123-
public NotValidateRootSignature(String errorMessage) {
122+
public static class TDFReadFailed extends RuntimeException {
123+
public TDFReadFailed(String errorMessage) {
124+
super(errorMessage);
125+
}
126+
}
127+
128+
public static class TamperException extends SDKException {
129+
public TamperException(String errorMessage) {
130+
super("[tamper detected] "+errorMessage);
131+
}
132+
}
133+
134+
public static class RootSignatureValidationException extends TamperException {
135+
public RootSignatureValidationException(String errorMessage) {
124136
super(errorMessage);
125137
}
126138
}
127139

128-
public static class SegmentSizeMismatch extends RuntimeException {
140+
public static class SegmentSizeMismatch extends TamperException {
129141
public SegmentSizeMismatch(String errorMessage) {
130142
super(errorMessage);
131143
}
132144
}
133145

134-
public static class SegmentSignatureMismatch extends RuntimeException {
146+
public static class SegmentSignatureMismatch extends TamperException {
135147
public SegmentSignatureMismatch(String errorMessage) {
136148
super(errorMessage);
137149
}
138150
}
139151

140-
public static class TDFReadFailed extends RuntimeException {
141-
public TDFReadFailed(String errorMessage) {
152+
public static class KasBadRequestException extends TamperException {
153+
public KasBadRequestException(String errorMessage) {
142154
super(errorMessage);
143155
}
144156
}
145157

158+
public static class AssertionException extends TamperException {
159+
public AssertionException(String errorMessage, String id) {
160+
super("assertion id: "+ id + "; " + errorMessage);
161+
}
162+
}
163+
146164
public static class EncryptedMetadata {
147165
private String ciphertext;
148166
private String iv;
@@ -558,7 +576,7 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas)
558576

559577
public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas,
560578
Config.TDFReaderConfig tdfReaderConfig)
561-
throws NotValidateRootSignature, SegmentSizeMismatch,
579+
throws RootSignatureValidationException, SegmentSizeMismatch,
562580
IOException, FailedToCreateGMAC, JOSEException, ParseException, NoSuchAlgorithmException, DecoderException {
563581

564582
TDFReader tdfReader = new TDFReader(tdf);
@@ -666,7 +684,7 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas,
666684
}
667685

668686
if (rootSignature.compareTo(rootSigValue) != 0) {
669-
throw new NotValidateRootSignature("root signature validation failed");
687+
throw new RootSignatureValidationException("root signature validation failed");
670688
}
671689

672690
int segmentSize = manifest.encryptionInformation.integrityInformation.segmentSizeDefault;
@@ -701,11 +719,11 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas,
701719
var encodeSignature = Base64.getEncoder().encodeToString(signature.getBytes());
702720

703721
if (!Objects.equals(hashOfAssertion, hashValues.getAssertionHash())) {
704-
throw new SDKException("assertion hash mismatch");
722+
throw new AssertionException("assertion hash mismatch", assertion.id);
705723
}
706724

707725
if (!Objects.equals(encodeSignature, hashValues.getSignature())) {
708-
throw new SDKException("failed integrity check on assertion signature");
726+
throw new AssertionException("failed integrity check on assertion signature", assertion.id);
709727
}
710728
}
711729

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

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse;
88
import io.opentdf.platform.policy.attributes.AttributesServiceGrpc;
99
import io.opentdf.platform.sdk.Config.KASInfo;
10+
import io.opentdf.platform.sdk.TDF.Reader;
1011
import io.opentdf.platform.sdk.nanotdf.NanoTDFType;
1112
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
1213
import org.junit.jupiter.api.BeforeAll;
@@ -30,7 +31,6 @@
3031
import static org.mockito.ArgumentMatchers.any;
3132
import static org.mockito.Mockito.lenient;
3233
import static org.mockito.Mockito.mock;
33-
3434
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
3535
import static org.junit.jupiter.api.Assertions.assertThrows;
3636

@@ -333,6 +333,62 @@ void testSimpleTDFWithAssertionWithHS256() throws Exception {
333333
}
334334
}
335335

336+
@Test
337+
void testSimpleTDFWithAssertionWithHS256Failure() throws Exception {
338+
339+
ListenableFuture<GetAttributeValuesByFqnsResponse> resp1 = mock(ListenableFuture.class);
340+
lenient().when(resp1.get()).thenReturn(GetAttributeValuesByFqnsResponse.newBuilder().build());
341+
lenient().when(attributeGrpcStub.getAttributeValuesByFqns(any(GetAttributeValuesByFqnsRequest.class)))
342+
.thenReturn(resp1);
343+
344+
// var keypair = CryptoUtils.generateRSAKeypair();
345+
SecureRandom secureRandom = new SecureRandom();
346+
byte[] key = new byte[32];
347+
secureRandom.nextBytes(key);
348+
349+
String assertion1Id = "assertion1";
350+
var assertionConfig1 = new AssertionConfig();
351+
assertionConfig1.id = assertion1Id;
352+
assertionConfig1.type = AssertionConfig.Type.BaseAssertion;
353+
assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj;
354+
assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted;
355+
assertionConfig1.statement = new AssertionConfig.Statement();
356+
assertionConfig1.statement.format = "base64binary";
357+
assertionConfig1.statement.schema = "text";
358+
assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4=";
359+
assertionConfig1.assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key);
360+
361+
Config.TDFConfig config = Config.newTDFConfig(
362+
Config.withAutoconfigure(false),
363+
Config.withKasInformation(getKASInfos()),
364+
Config.withAssertionConfig(assertionConfig1));
365+
366+
String plainText = "this is extremely sensitive stuff!!!";
367+
InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes());
368+
ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream();
369+
370+
TDF tdf = new TDF();
371+
tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, attributeGrpcStub);
372+
373+
byte[] notkey = new byte[32];
374+
secureRandom.nextBytes(notkey);
375+
var assertionVerificationKeys = new Config.AssertionVerificationKeys();
376+
assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256,
377+
notkey);
378+
Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig(
379+
Config.withAssertionVerificationKeys(assertionVerificationKeys));
380+
381+
var unwrappedData = new ByteArrayOutputStream();
382+
Reader reader;
383+
try {
384+
reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, readerConfig);
385+
throw new RuntimeException("assertion verify key error thrown");
386+
387+
} catch (SDKException e) {
388+
assertThat(e).hasMessageContaining("verify");
389+
}
390+
}
391+
336392
@Test
337393
public void testCreatingTDFWithMultipleSegments() throws Exception {
338394

0 commit comments

Comments
 (0)