Skip to content

Commit 7fb5eba

Browse files
committed
feat(sdk): add base key support for nanotdf
1 parent d235f13 commit 7fb5eba

File tree

3 files changed

+100
-28
lines changed

3 files changed

+100
-28
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,6 @@ public static ECPrivateKey privateKeyFromPem(String pemEncoding) {
215215
parser.close();
216216

217217
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER);
218-
;
219218
return (ECPrivateKey) converter.getPrivateKey(privateKeyInfo);
220219
} catch (IOException e) {
221220
throw new RuntimeException(e);

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

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.google.gson.Gson;
1717
import com.google.gson.GsonBuilder;
1818

19+
import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface;
1920
import org.bouncycastle.jce.interfaces.ECPublicKey;
2021
import org.slf4j.Logger;
2122
import org.slf4j.LoggerFactory;
@@ -65,6 +66,32 @@ public InvalidNanoTDFConfig(String errorMessage) {
6566
}
6667
}
6768

69+
private static Optional<Config.KASInfo> getBaseKey(WellKnownServiceClientInterface wellKnownService) {
70+
var key = Planner.fetchBaseKey(wellKnownService);
71+
key.ifPresent(k -> {
72+
if (!KeyType.fromAlgorithm(k.getPublicKey().getAlgorithm()).isEc()) {
73+
throw new SDKException(String.format("base key is not an EC key, cannot create NanoTDF using a key of type %s",
74+
k.getPublicKey().getAlgorithm()));
75+
}
76+
});
77+
78+
return key.map(Config.KASInfo::fromSimpleKasKey);
79+
}
80+
81+
private Optional<Config.KASInfo> getKasInfo(Config.NanoTDFConfig nanoTDFConfig) {
82+
if (nanoTDFConfig.kasInfoList.isEmpty()) {
83+
logger.debug("no kas info provided in NanoTDFConfig");
84+
return Optional.empty();
85+
}
86+
Config.KASInfo kasInfo = nanoTDFConfig.kasInfoList.get(0);
87+
String url = kasInfo.URL;
88+
if (kasInfo.PublicKey == null || kasInfo.PublicKey.isEmpty()) {
89+
logger.info("no public key provided for KAS at {}, retrieving", url);
90+
kasInfo = services.kas().getECPublicKey(kasInfo, nanoTDFConfig.eccMode.getCurve());
91+
}
92+
return Optional.of(kasInfo);
93+
}
94+
6895
private Config.HeaderInfo getHeaderInfo(Config.NanoTDFConfig nanoTDFConfig) throws InvalidNanoTDFConfig, UnsupportedNanoTDFFeature {
6996
if (nanoTDFConfig.collectionConfig.useCollection) {
7097
Config.HeaderInfo headerInfo = nanoTDFConfig.collectionConfig.getHeaderInfo();
@@ -74,19 +101,20 @@ private Config.HeaderInfo getHeaderInfo(Config.NanoTDFConfig nanoTDFConfig) thro
74101
}
75102

76103
Gson gson = new GsonBuilder().create();
77-
if (nanoTDFConfig.kasInfoList.isEmpty()) {
78-
throw new InvalidNanoTDFConfig("kas url is missing");
104+
Optional<Config.KASInfo> maybeKas = getKasInfo(nanoTDFConfig).or(() -> NanoTDF.getBaseKey(services.wellknown()));
105+
if (maybeKas.isEmpty()) {
106+
throw new SDKException("no KAS info provided and couldn't get base key, cannot create NanoTDF");
79107
}
80108

81-
Config.KASInfo kasInfo = nanoTDFConfig.kasInfoList.get(0);
109+
Config.KASInfo kasInfo = maybeKas.get();
82110
String url = kasInfo.URL;
83111
if (kasInfo.PublicKey == null || kasInfo.PublicKey.isEmpty()) {
84112
logger.info("no public key provided for KAS at {}, retrieving", url);
85113
kasInfo = services.kas().getECPublicKey(kasInfo, nanoTDFConfig.eccMode.getCurve());
86114
}
87115

88116
// Kas url resource locator
89-
ResourceLocator kasURL = new ResourceLocator(nanoTDFConfig.kasInfoList.get(0).URL, kasInfo.KID);
117+
ResourceLocator kasURL = new ResourceLocator(kasInfo.URL, kasInfo.KID);
90118
assert kasURL.getIdentifier() != null : "Identifier in ResourceLocator cannot be null";
91119

92120
NanoTDFType.ECCurve ecCurve = getEcCurve(nanoTDFConfig, kasInfo);
@@ -139,12 +167,10 @@ private Config.HeaderInfo getHeaderInfo(Config.NanoTDFConfig nanoTDFConfig) thro
139167
// Create header
140168
byte[] compressedPubKey = keyPair.compressECPublickey();
141169
Header header = new Header();
142-
ECCMode mode;
143-
if (nanoTDFConfig.eccMode.getCurve() != keyPair.getCurve()) {
144-
mode = new ECCMode(nanoTDFConfig.eccMode.getECCModeAsByte());
145-
mode.setEllipticCurve(keyPair.getCurve());
146-
} else {
147-
mode = nanoTDFConfig.eccMode;
170+
ECCMode mode = new ECCMode();
171+
mode.setEllipticCurve(keyPair.getCurve());
172+
if (logger.isWarnEnabled() && !nanoTDFConfig.eccMode.equals(mode)) {
173+
logger.warn("ECC mode provided in NanoTDFConfig: {}, ECC mode from key: {}", nanoTDFConfig.eccMode.getCurve(), mode.getCurve());
148174
}
149175
header.setECCMode(mode);
150176
header.setPayloadConfig(nanoTDFConfig.config);
@@ -169,10 +195,10 @@ private static NanoTDFType.ECCurve getEcCurve(Config.NanoTDFConfig nanoTDFConfig
169195
logger.info("no curve specified in KASInfo, using the curve from config [{}]", nanoTDFConfig.eccMode.getCurve());
170196
ecCurve = nanoTDFConfig.eccMode.getCurve();
171197
} else {
172-
if (specifiedCurve.get() != nanoTDFConfig.eccMode.getCurve()) {
173-
logger.warn("ECCurve in NanoTDFConfig [{}] does not match the curve in KASInfo, using KASInfo curve [{}]", nanoTDFConfig.eccMode.getCurve(), specifiedCurve);
174-
}
175198
ecCurve = specifiedCurve.get();
199+
if (ecCurve != nanoTDFConfig.eccMode.getCurve()) {
200+
logger.warn("ECCurve in NanoTDFConfig [{}] does not match the curve in KASInfo, using KASInfo curve [{}]", nanoTDFConfig.eccMode.getCurve(), ecCurve);
201+
}
176202
}
177203
return ecCurve;
178204
}

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

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.connectrpc.ResponseMessage;
44
import com.connectrpc.UnaryBlockingCall;
5+
import com.google.protobuf.Struct;
6+
import com.google.protobuf.Value;
57
import io.opentdf.platform.policy.KeyAccessServer;
68
import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClient;
79
import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest;
@@ -11,6 +13,8 @@
1113

1214
import java.nio.charset.StandardCharsets;
1315

16+
import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse;
17+
import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface;
1418
import org.apache.commons.io.output.ByteArrayOutputStream;
1519
import org.junit.jupiter.api.BeforeAll;
1620
import org.junit.jupiter.api.Test;
@@ -23,6 +27,7 @@
2327
import java.util.Base64;
2428
import java.util.Collections;
2529
import java.util.List;
30+
import java.util.Objects;
2631
import java.util.Random;
2732

2833
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@@ -45,14 +50,27 @@ public class NanoTDFTest {
4550
"oVP7Vpcx\n" +
4651
"-----END PRIVATE KEY-----";
4752

53+
private static final String BASE_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" +
54+
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/NawR/F7RJfX/odyOLPjl+5Ce1Br\n" +
55+
"QZ/MBCIerHe26HzlBSbpa7HQHZx9PYVamHTw9+iJCY3dm8Uwp4Ab2uehnA==\n" +
56+
"-----END PUBLIC KEY-----";
57+
58+
private static final String BASE_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" +
59+
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgB3YtAvS7lctHlPsq\n" +
60+
"bZI8OX1B9W1c4GAIxzwKzD6iPkqhRANCAAT81rBH8XtEl9f+h3I4s+OX7kJ7UGtB\n" +
61+
"n8wEIh6sd7bofOUFJulrsdAdnH09hVqYdPD36IkJjd2bxTCngBva56Gc\n" +
62+
"-----END PRIVATE KEY-----" ;
63+
4864
private static final String KID = "r1";
65+
private static final String BASE_KID = "basekid";
4966

5067
protected static KeyAccessServerRegistryServiceClient kasRegistryService;
5168
protected static List<String> registeredKases = List.of(
5269
"https://api.example.com/kas",
5370
"https://other.org/kas2",
5471
"http://localhost:8181/kas",
55-
"https://localhost:8383/kas"
72+
"https://localhost:8383/kas",
73+
"https://api.kaswithbasekey.example.com"
5674
);
5775
protected static String platformUrl = "http://localhost:8080";
5876

@@ -70,10 +88,16 @@ public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) {
7088

7189
@Override
7290
public KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) {
91+
var k2 = kasInfo.clone();
92+
if (Objects.equals(kasInfo.KID, BASE_KID)) {
93+
assertThat(kasInfo.URL).isEqualTo("https://api.kaswithbasekey.example.com");
94+
assertThat(kasInfo.Algorithm).isEqualTo("ec:secp384r1");
95+
k2.PublicKey = BASE_PUBLIC_KEY;
96+
return k2;
97+
}
7398
if (kasInfo.Algorithm != null && !"ec:secp256r1".equals(kasInfo.Algorithm)) {
7499
throw new IllegalArgumentException("Unexpected algorithm: " + kasInfo);
75100
}
76-
var k2 = kasInfo.clone();
77101
k2.KID = KID;
78102
k2.PublicKey = kasPublicKey;
79103
k2.Algorithm = "ec:secp256r1";
@@ -82,19 +106,14 @@ public KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve)
82106

83107
@Override
84108
public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) {
85-
int index = Integer.parseInt(keyAccess.url);
86-
var decryptor = new AsymDecryption(keypairs.get(index).getPrivate());
87-
var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey);
88-
try {
89-
return decryptor.decrypt(bytes);
90-
} catch (Exception e) {
91-
throw new RuntimeException(e);
92-
}
109+
throw new UnsupportedOperationException("no unwrapping ZTDFs here");
93110
}
94111

95112
@Override
96113
public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) {
97-
114+
String key = Objects.equals(kasURL, "https://api.kaswithbasekey.example.com")
115+
? BASE_PRIVATE_KEY
116+
: kasPrivateKey;
98117
byte[] headerAsBytes = Base64.getDecoder().decode(header);
99118
Header nTDFHeader = new Header(ByteBuffer.wrap(headerAsBytes));
100119
byte[] ephemeralKey = nTDFHeader.getEphemeralKey();
@@ -103,7 +122,7 @@ public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kas
103122

104123
// Generate symmetric key
105124
byte[] symmetricKey = ECKeyPair.computeECDHKey(ECKeyPair.publicKeyFromPem(publicKeyAsPem),
106-
ECKeyPair.privateKeyFromPem(kasPrivateKey));
125+
ECKeyPair.privateKeyFromPem(key));
107126

108127
// Generate HKDF key
109128
MessageDigest digest;
@@ -113,8 +132,7 @@ public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kas
113132
throw new SDKException("error creating SHA-256 message digest", e);
114133
}
115134
byte[] hashOfSalt = digest.digest(NanoTDF.MAGIC_NUMBER_AND_VERSION);
116-
byte[] key = ECKeyPair.calculateHKDF(hashOfSalt, symmetricKey);
117-
return key;
135+
return ECKeyPair.calculateHKDF(hashOfSalt, symmetricKey);
118136
}
119137

120138
@Override
@@ -203,6 +221,35 @@ void encryptionAndDecryptionWithValidKey() throws Exception {
203221
}
204222
}
205223

224+
@Test
225+
void encryptionAndDecryptWithBaseKey() throws Exception {
226+
var baseKeyJson = "{\"kas_url\":\"https://api.kaswithbasekey.example.com\",\"public_key\":{\"algorithm\":\"ALGORITHM_EC_P256\",\"kid\":\"" + BASE_KID + "\",\"pem\": \"" + BASE_PUBLIC_KEY + "\"}}";
227+
var val = Value.newBuilder().setStringValue(baseKeyJson).build();
228+
var config = Struct.newBuilder().putFields("base_key", val).build();
229+
WellKnownServiceClientInterface wellknown = mock(WellKnownServiceClientInterface.class);
230+
GetWellKnownConfigurationResponse response = GetWellKnownConfigurationResponse.newBuilder().setConfiguration(config).build();
231+
when(wellknown.getWellKnownConfigurationBlocking(any(), any())).thenReturn(TestUtil.successfulUnaryCall(response));
232+
Config.NanoTDFConfig nanoConfig = Config.newNanoTDFConfig(
233+
Config.witDataAttributes("https://example.com/attr/Classification/value/S",
234+
"https://example.com/attr/Classification/value/X")
235+
);
236+
237+
String plainText = "Virtru!!";
238+
ByteBuffer byteBuffer = ByteBuffer.wrap(plainText.getBytes());
239+
ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream();
240+
NanoTDF nanoTDF = new NanoTDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).setWellknownService(wellknown).build());
241+
nanoTDF.createNanoTDF(byteBuffer, tdfOutputStream, nanoConfig);
242+
243+
byte[] nanoTDFBytes = tdfOutputStream.toByteArray();
244+
ByteArrayOutputStream plainTextStream = new ByteArrayOutputStream();
245+
nanoTDF = new NanoTDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build());
246+
nanoTDF.readNanoTDF(ByteBuffer.wrap(nanoTDFBytes), plainTextStream, platformUrl);
247+
String out = new String(plainTextStream.toByteArray(), StandardCharsets.UTF_8);
248+
assertThat(out).isEqualTo(plainText);
249+
// KAS KID
250+
assertThat(new String(nanoTDFBytes, StandardCharsets.UTF_8)).contains(BASE_KID);
251+
}
252+
206253
@Test
207254
void testWithDifferentConfigAndKeyValues() throws Exception {
208255
var kasInfos = new ArrayList<>();

0 commit comments

Comments
 (0)