-
Notifications
You must be signed in to change notification settings - Fork 2
feat: introduce the base key for nano flows #273
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -16,6 +16,7 @@ | |||||||||||||||||||||
import com.google.gson.Gson; | ||||||||||||||||||||||
import com.google.gson.GsonBuilder; | ||||||||||||||||||||||
|
||||||||||||||||||||||
import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface; | ||||||||||||||||||||||
import org.bouncycastle.jce.interfaces.ECPublicKey; | ||||||||||||||||||||||
import org.slf4j.Logger; | ||||||||||||||||||||||
import org.slf4j.LoggerFactory; | ||||||||||||||||||||||
|
@@ -65,6 +66,32 @@ public InvalidNanoTDFConfig(String errorMessage) { | |||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
private static Optional<Config.KASInfo> getBaseKey(WellKnownServiceClientInterface wellKnownService) { | ||||||||||||||||||||||
var key = Planner.fetchBaseKey(wellKnownService); | ||||||||||||||||||||||
key.ifPresent(k -> { | ||||||||||||||||||||||
if (!KeyType.fromAlgorithm(k.getPublicKey().getAlgorithm()).isEc()) { | ||||||||||||||||||||||
throw new SDKException(String.format("base key is not an EC key, cannot create NanoTDF using a key of type %s", | ||||||||||||||||||||||
k.getPublicKey().getAlgorithm())); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
}); | ||||||||||||||||||||||
|
||||||||||||||||||||||
return key.map(Config.KASInfo::fromSimpleKasKey); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
private Optional<Config.KASInfo> getKasInfo(Config.NanoTDFConfig nanoTDFConfig) { | ||||||||||||||||||||||
if (nanoTDFConfig.kasInfoList.isEmpty()) { | ||||||||||||||||||||||
logger.debug("no kas info provided in NanoTDFConfig"); | ||||||||||||||||||||||
return Optional.empty(); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
Config.KASInfo kasInfo = nanoTDFConfig.kasInfoList.get(0); | ||||||||||||||||||||||
String url = kasInfo.URL; | ||||||||||||||||||||||
if (kasInfo.PublicKey == null || kasInfo.PublicKey.isEmpty()) { | ||||||||||||||||||||||
logger.info("no public key provided for KAS at {}, retrieving", url); | ||||||||||||||||||||||
kasInfo = services.kas().getECPublicKey(kasInfo, nanoTDFConfig.eccMode.getCurve()); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
return Optional.of(kasInfo); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
private Config.HeaderInfo getHeaderInfo(Config.NanoTDFConfig nanoTDFConfig) throws InvalidNanoTDFConfig, UnsupportedNanoTDFFeature { | ||||||||||||||||||||||
if (nanoTDFConfig.collectionConfig.useCollection) { | ||||||||||||||||||||||
Config.HeaderInfo headerInfo = nanoTDFConfig.collectionConfig.getHeaderInfo(); | ||||||||||||||||||||||
|
@@ -74,19 +101,20 @@ private Config.HeaderInfo getHeaderInfo(Config.NanoTDFConfig nanoTDFConfig) thro | |||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
Gson gson = new GsonBuilder().create(); | ||||||||||||||||||||||
if (nanoTDFConfig.kasInfoList.isEmpty()) { | ||||||||||||||||||||||
throw new InvalidNanoTDFConfig("kas url is missing"); | ||||||||||||||||||||||
Optional<Config.KASInfo> maybeKas = getKasInfo(nanoTDFConfig).or(() -> NanoTDF.getBaseKey(services.wellknown())); | ||||||||||||||||||||||
if (maybeKas.isEmpty()) { | ||||||||||||||||||||||
throw new SDKException("no KAS info provided and couldn't get base key, cannot create NanoTDF"); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
Config.KASInfo kasInfo = nanoTDFConfig.kasInfoList.get(0); | ||||||||||||||||||||||
Config.KASInfo kasInfo = maybeKas.get(); | ||||||||||||||||||||||
String url = kasInfo.URL; | ||||||||||||||||||||||
if (kasInfo.PublicKey == null || kasInfo.PublicKey.isEmpty()) { | ||||||||||||||||||||||
logger.info("no public key provided for KAS at {}, retrieving", url); | ||||||||||||||||||||||
kasInfo = services.kas().getECPublicKey(kasInfo, nanoTDFConfig.eccMode.getCurve()); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Kas url resource locator | ||||||||||||||||||||||
ResourceLocator kasURL = new ResourceLocator(nanoTDFConfig.kasInfoList.get(0).URL, kasInfo.KID); | ||||||||||||||||||||||
ResourceLocator kasURL = new ResourceLocator(kasInfo.URL, kasInfo.KID); | ||||||||||||||||||||||
assert kasURL.getIdentifier() != null : "Identifier in ResourceLocator cannot be null"; | ||||||||||||||||||||||
|
||||||||||||||||||||||
NanoTDFType.ECCurve ecCurve = getEcCurve(nanoTDFConfig, kasInfo); | ||||||||||||||||||||||
|
@@ -139,12 +167,10 @@ private Config.HeaderInfo getHeaderInfo(Config.NanoTDFConfig nanoTDFConfig) thro | |||||||||||||||||||||
// Create header | ||||||||||||||||||||||
byte[] compressedPubKey = keyPair.compressECPublickey(); | ||||||||||||||||||||||
Header header = new Header(); | ||||||||||||||||||||||
ECCMode mode; | ||||||||||||||||||||||
if (nanoTDFConfig.eccMode.getCurve() != keyPair.getCurve()) { | ||||||||||||||||||||||
mode = new ECCMode(nanoTDFConfig.eccMode.getECCModeAsByte()); | ||||||||||||||||||||||
mode.setEllipticCurve(keyPair.getCurve()); | ||||||||||||||||||||||
} else { | ||||||||||||||||||||||
mode = nanoTDFConfig.eccMode; | ||||||||||||||||||||||
ECCMode mode = new ECCMode(); | ||||||||||||||||||||||
mode.setEllipticCurve(keyPair.getCurve()); | ||||||||||||||||||||||
if (logger.isWarnEnabled() && !nanoTDFConfig.eccMode.equals(mode)) { | ||||||||||||||||||||||
logger.warn("ECC mode provided in NanoTDFConfig: {}, ECC mode from key: {}", nanoTDFConfig.eccMode.getCurve(), mode.getCurve()); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
Comment on lines
+170
to
174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a couple of issues with the new logic for determining the
The previous logic correctly preserved the binding setting. Here is a suggested fix that addresses both issues by preserving the binding and correctly comparing the curves.
Suggested change
|
||||||||||||||||||||||
header.setECCMode(mode); | ||||||||||||||||||||||
header.setPayloadConfig(nanoTDFConfig.config); | ||||||||||||||||||||||
|
@@ -169,10 +195,10 @@ private static NanoTDFType.ECCurve getEcCurve(Config.NanoTDFConfig nanoTDFConfig | |||||||||||||||||||||
logger.info("no curve specified in KASInfo, using the curve from config [{}]", nanoTDFConfig.eccMode.getCurve()); | ||||||||||||||||||||||
ecCurve = nanoTDFConfig.eccMode.getCurve(); | ||||||||||||||||||||||
} else { | ||||||||||||||||||||||
if (specifiedCurve.get() != nanoTDFConfig.eccMode.getCurve()) { | ||||||||||||||||||||||
logger.warn("ECCurve in NanoTDFConfig [{}] does not match the curve in KASInfo, using KASInfo curve [{}]", nanoTDFConfig.eccMode.getCurve(), specifiedCurve); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
ecCurve = specifiedCurve.get(); | ||||||||||||||||||||||
if (ecCurve != nanoTDFConfig.eccMode.getCurve()) { | ||||||||||||||||||||||
logger.warn("ECCurve in NanoTDFConfig [{}] does not match the curve in KASInfo, using KASInfo curve [{}]", nanoTDFConfig.eccMode.getCurve(), ecCurve); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
return ecCurve; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -2,6 +2,8 @@ | |||||
|
||||||
import com.connectrpc.ResponseMessage; | ||||||
import com.connectrpc.UnaryBlockingCall; | ||||||
import com.google.protobuf.Struct; | ||||||
import com.google.protobuf.Value; | ||||||
import io.opentdf.platform.policy.KeyAccessServer; | ||||||
import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClient; | ||||||
import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; | ||||||
|
@@ -11,6 +13,8 @@ | |||||
|
||||||
import java.nio.charset.StandardCharsets; | ||||||
|
||||||
import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse; | ||||||
import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface; | ||||||
import org.apache.commons.io.output.ByteArrayOutputStream; | ||||||
import org.junit.jupiter.api.BeforeAll; | ||||||
import org.junit.jupiter.api.Test; | ||||||
|
@@ -23,6 +27,7 @@ | |||||
import java.util.Base64; | ||||||
import java.util.Collections; | ||||||
import java.util.List; | ||||||
import java.util.Objects; | ||||||
import java.util.Random; | ||||||
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; | ||||||
|
@@ -45,14 +50,27 @@ public class NanoTDFTest { | |||||
"oVP7Vpcx\n" + | ||||||
"-----END PRIVATE KEY-----"; | ||||||
|
||||||
private static final String BASE_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" + | ||||||
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/NawR/F7RJfX/odyOLPjl+5Ce1Br\n" + | ||||||
"QZ/MBCIerHe26HzlBSbpa7HQHZx9PYVamHTw9+iJCY3dm8Uwp4Ab2uehnA==\n" + | ||||||
"-----END PUBLIC KEY-----"; | ||||||
|
||||||
private static final String BASE_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" + | ||||||
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgB3YtAvS7lctHlPsq\n" + | ||||||
"bZI8OX1B9W1c4GAIxzwKzD6iPkqhRANCAAT81rBH8XtEl9f+h3I4s+OX7kJ7UGtB\n" + | ||||||
"n8wEIh6sd7bofOUFJulrsdAdnH09hVqYdPD36IkJjd2bxTCngBva56Gc\n" + | ||||||
"-----END PRIVATE KEY-----"; | ||||||
|
||||||
private static final String KID = "r1"; | ||||||
private static final String BASE_KID = "basekid"; | ||||||
|
||||||
protected static KeyAccessServerRegistryServiceClient kasRegistryService; | ||||||
protected static List<String> registeredKases = List.of( | ||||||
"https://api.example.com/kas", | ||||||
"https://other.org/kas2", | ||||||
"http://localhost:8181/kas", | ||||||
"https://localhost:8383/kas" | ||||||
"https://localhost:8383/kas", | ||||||
"https://api.kaswithbasekey.example.com" | ||||||
); | ||||||
protected static String platformUrl = "http://localhost:8080"; | ||||||
|
||||||
|
@@ -70,10 +88,16 @@ public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { | |||||
|
||||||
@Override | ||||||
public KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) { | ||||||
var k2 = kasInfo.clone(); | ||||||
if (Objects.equals(kasInfo.KID, BASE_KID)) { | ||||||
assertThat(kasInfo.URL).isEqualTo("https://api.kaswithbasekey.example.com"); | ||||||
assertThat(kasInfo.Algorithm).isEqualTo("ec:secp384r1"); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The assertion for the algorithm appears to be incorrect. The
Suggested change
|
||||||
k2.PublicKey = BASE_PUBLIC_KEY; | ||||||
return k2; | ||||||
} | ||||||
if (kasInfo.Algorithm != null && !"ec:secp256r1".equals(kasInfo.Algorithm)) { | ||||||
throw new IllegalArgumentException("Unexpected algorithm: " + kasInfo); | ||||||
} | ||||||
var k2 = kasInfo.clone(); | ||||||
k2.KID = KID; | ||||||
k2.PublicKey = kasPublicKey; | ||||||
k2.Algorithm = "ec:secp256r1"; | ||||||
|
@@ -82,19 +106,14 @@ public KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) | |||||
|
||||||
@Override | ||||||
public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) { | ||||||
int index = Integer.parseInt(keyAccess.url); | ||||||
var decryptor = new AsymDecryption(keypairs.get(index).getPrivate()); | ||||||
var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey); | ||||||
try { | ||||||
return decryptor.decrypt(bytes); | ||||||
} catch (Exception e) { | ||||||
throw new RuntimeException(e); | ||||||
} | ||||||
throw new UnsupportedOperationException("no unwrapping ZTDFs here"); | ||||||
} | ||||||
|
||||||
@Override | ||||||
public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) { | ||||||
|
||||||
String key = Objects.equals(kasURL, "https://api.kaswithbasekey.example.com") | ||||||
? BASE_PRIVATE_KEY | ||||||
: kasPrivateKey; | ||||||
byte[] headerAsBytes = Base64.getDecoder().decode(header); | ||||||
Header nTDFHeader = new Header(ByteBuffer.wrap(headerAsBytes)); | ||||||
byte[] ephemeralKey = nTDFHeader.getEphemeralKey(); | ||||||
|
@@ -103,7 +122,7 @@ public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kas | |||||
|
||||||
// Generate symmetric key | ||||||
byte[] symmetricKey = ECKeyPair.computeECDHKey(ECKeyPair.publicKeyFromPem(publicKeyAsPem), | ||||||
ECKeyPair.privateKeyFromPem(kasPrivateKey)); | ||||||
ECKeyPair.privateKeyFromPem(key)); | ||||||
|
||||||
// Generate HKDF key | ||||||
MessageDigest digest; | ||||||
|
@@ -113,8 +132,7 @@ public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kas | |||||
throw new SDKException("error creating SHA-256 message digest", e); | ||||||
} | ||||||
byte[] hashOfSalt = digest.digest(NanoTDF.MAGIC_NUMBER_AND_VERSION); | ||||||
byte[] key = ECKeyPair.calculateHKDF(hashOfSalt, symmetricKey); | ||||||
return key; | ||||||
return ECKeyPair.calculateHKDF(hashOfSalt, symmetricKey); | ||||||
} | ||||||
|
||||||
@Override | ||||||
|
@@ -203,6 +221,35 @@ void encryptionAndDecryptionWithValidKey() throws Exception { | |||||
} | ||||||
} | ||||||
|
||||||
@Test | ||||||
void encryptionAndDecryptWithBaseKey() throws Exception { | ||||||
var baseKeyJson = "{\"kas_url\":\"https://api.kaswithbasekey.example.com\",\"public_key\":{\"algorithm\":\"ALGORITHM_EC_P256\",\"kid\":\"" + BASE_KID + "\",\"pem\": \"" + BASE_PUBLIC_KEY + "\"}}"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Using a JSON library to build the string is safer, but a direct fix is provided below.
Suggested change
|
||||||
var val = Value.newBuilder().setStringValue(baseKeyJson).build(); | ||||||
var config = Struct.newBuilder().putFields("base_key", val).build(); | ||||||
WellKnownServiceClientInterface wellknown = mock(WellKnownServiceClientInterface.class); | ||||||
GetWellKnownConfigurationResponse response = GetWellKnownConfigurationResponse.newBuilder().setConfiguration(config).build(); | ||||||
when(wellknown.getWellKnownConfigurationBlocking(any(), any())).thenReturn(TestUtil.successfulUnaryCall(response)); | ||||||
Config.NanoTDFConfig nanoConfig = Config.newNanoTDFConfig( | ||||||
Config.witDataAttributes("https://example.com/attr/Classification/value/S", | ||||||
"https://example.com/attr/Classification/value/X") | ||||||
); | ||||||
|
||||||
String plainText = "Virtru!!"; | ||||||
ByteBuffer byteBuffer = ByteBuffer.wrap(plainText.getBytes()); | ||||||
ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); | ||||||
NanoTDF nanoTDF = new NanoTDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).setWellknownService(wellknown).build()); | ||||||
nanoTDF.createNanoTDF(byteBuffer, tdfOutputStream, nanoConfig); | ||||||
|
||||||
byte[] nanoTDFBytes = tdfOutputStream.toByteArray(); | ||||||
ByteArrayOutputStream plainTextStream = new ByteArrayOutputStream(); | ||||||
nanoTDF = new NanoTDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); | ||||||
nanoTDF.readNanoTDF(ByteBuffer.wrap(nanoTDFBytes), plainTextStream, platformUrl); | ||||||
String out = new String(plainTextStream.toByteArray(), StandardCharsets.UTF_8); | ||||||
assertThat(out).isEqualTo(plainText); | ||||||
// KAS KID | ||||||
assertThat(new String(nanoTDFBytes, StandardCharsets.UTF_8)).contains(BASE_KID); | ||||||
} | ||||||
|
||||||
@Test | ||||||
void testWithDifferentConfigAndKeyValues() throws Exception { | ||||||
var kasInfos = new ArrayList<>(); | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block of code to check for and retrieve a missing public key is redundant. The preceding call to
getKasInfo(nanoTDFConfig).or(() -> NanoTDF.getBaseKey(services.wellknown()))
already ensures that theKASInfo
object inmaybeKas
will have its public key populated.Specifically:
getKasInfo
fetches the public key if it's missing.getBaseKey
returns aKASInfo
that is created with a public key.This block can be removed for better code clarity and to avoid unnecessary checks. The
url
variable also becomes unused and can be removed.