diff --git a/test-server/server/src/main/java/aws/cryptography/esdk/testserver/server/config/EsdkClientFactory.java b/test-server/server/src/main/java/aws/cryptography/esdk/testserver/server/config/EsdkClientFactory.java index 1b25f249..17a792db 100644 --- a/test-server/server/src/main/java/aws/cryptography/esdk/testserver/server/config/EsdkClientFactory.java +++ b/test-server/server/src/main/java/aws/cryptography/esdk/testserver/server/config/EsdkClientFactory.java @@ -26,11 +26,25 @@ import aws.cryptography.esdk.testserver.server.registry.EsdkClient; import aws.cryptography.esdk.testserver.server.registry.RealEsdkClient; import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.amazonaws.encryptionsdk.MasterKeyProvider; +import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager; +import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCache; +import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.kmssdkv2.AwsKmsMrkAwareMasterKeyProvider; +import com.amazonaws.encryptionsdk.kmssdkv2.KmsMasterKeyProvider; +import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Base64; import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.crypto.spec.SecretKeySpec; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.kms.KmsClient; @@ -103,8 +117,15 @@ * that case the factory fetches it once via {@code kms:GetPublicKey} at * construction (a network call the design explicitly permits at * {@code CreateClient} time); supplying {@code publicKey} in the config keeps - * construction fully offline. The Caching CMM remains unwired and causes a - * construction failure (GenericServerError) if requested (Requirement 3.6). + * construction fully offline. + * + *

The Caching CMM is the ESDK's {@code CachingCryptoMaterialsManager}: the + * Material Providers Library has no caching CMM, and the ESDK's caching CMM wraps + * master key providers rather than MPL keyrings. So a top-level Caching config is + * built over master key providers equivalent to its wrapped Default CMM's keyring + * (Raw AES and Raw RSA as {@code JceMasterKey}, the AWS KMS keyrings as the KMS + * master key providers, and Multi as a multi-provider); any other keyring, or a + * Caching CMM nested under another CMM, is a construction failure. */ public final class EsdkClientFactory { @@ -127,6 +148,10 @@ public EsdkClient create(ESDKClientConfig config) { Integer maxEdk = config.getMaxEncryptedDataKeys() == null ? null : Math.toIntExact(config.getMaxEncryptedDataKeys()); + CachingCmmConfig caching = config.getCmm().getCaching(); + if (caching != null) { + return new RealEsdkClient(commitmentPolicy, maxEdk, buildCachingCmm(caching)); + } ICryptographicMaterialsManager cmm = buildCmm(config.getCmm()); return new RealEsdkClient(commitmentPolicy, maxEdk, cmm); } @@ -154,12 +179,154 @@ private ICryptographicMaterialsManager buildCmm(CryptographicMaterialsManager cm } if (caching != null) { throw new UnsupportedOperationException( - "Caching CMM is not wired in this pass of the ESDK TestServer"); + "a Caching CMM is supported only as the client's top-level CMM"); } throw new IllegalArgumentException( "CryptographicMaterialsManager had no variant member set"); } + /** Entries the Caching CMM's local cache holds. */ + private static final int CACHE_CAPACITY = 100; + + private static CachingCryptoMaterialsManager buildCachingCmm(CachingCmmConfig config) { + DefaultCmmConfig underlying = config.getUnderlyingCMM().getDefault(); + if (underlying == null) { + throw new UnsupportedOperationException( + "the Caching CMM must wrap a Default CMM: the ESDK's caching CMM wraps master key providers"); + } + CachingCryptoMaterialsManager.Builder builder = CachingCryptoMaterialsManager.newBuilder() + .withMasterKeyProvider(toMasterKeyProvider(underlying.getKeyring())) + .withCache(new LocalCryptoMaterialsCache(CACHE_CAPACITY)) + .withMaxAge(config.getCacheLimitTtlSeconds(), TimeUnit.SECONDS); + if (config.getPartitionId() != null) { + builder.withPartitionId(config.getPartitionId()); + } + if (config.getLimitMessages() != null) { + builder.withMessageUseLimit(config.getLimitMessages()); + } + if (config.getLimitBytes() != null) { + builder.withByteUseLimit(config.getLimitBytes()); + } + return builder.build(); + } + + /** + * The master key provider equivalent to {@code keyring}: it writes and reads + * the same encrypted data keys as the MPL keyring of the same configuration. + */ + private static MasterKeyProvider toMasterKeyProvider(Keyring keyring) { + if (keyring.getRawAes() != null) { + RawAesKeyringConfig rawAes = keyring.getRawAes(); + return JceMasterKey.getInstance( + new SecretKeySpec(bytes(rawAes.getWrappingKey()), "AES"), + rawAes.getKeyNamespace(), rawAes.getKeyName(), "AES/GCM/NoPadding"); + } + if (keyring.getRawRsa() != null) { + RawRsaKeyringConfig rawRsa = keyring.getRawRsa(); + return JceMasterKey.getInstance( + rawRsa.getPublicKey() == null ? null : rsaPublicKey(rawRsa.getPublicKey()), + rawRsa.getPrivateKey() == null ? null : rsaPrivateKey(rawRsa.getPrivateKey()), + rawRsa.getKeyNamespace(), rawRsa.getKeyName(), + rsaTransformation(rawRsa.getPaddingScheme().getValue())); + } + if (keyring.getAwsKms() != null) { + requireNoGrantTokens(keyring.getAwsKms().hasGrantTokens() + && !keyring.getAwsKms().getGrantTokens().isEmpty()); + return KmsMasterKeyProvider.builder().buildStrict(keyring.getAwsKms().getKmsKeyId()); + } + if (keyring.getAwsKmsMrk() != null) { + requireNoGrantTokens(keyring.getAwsKmsMrk().hasGrantTokens() + && !keyring.getAwsKmsMrk().getGrantTokens().isEmpty()); + return AwsKmsMrkAwareMasterKeyProvider.builder() + .buildStrict(keyring.getAwsKmsMrk().getKmsKeyId()); + } + if (keyring.getAwsKmsMultiKeyring() != null) { + AwsKmsMultiKeyringConfig multi = keyring.getAwsKmsMultiKeyring(); + requireNoGrantTokens(multi.hasGrantTokens() && !multi.getGrantTokens().isEmpty()); + return KmsMasterKeyProvider.builder().buildStrict(kmsKeyIds(multi.getGenerator(), + multi.hasKmsKeyIds() ? multi.getKmsKeyIds() : List.of())); + } + if (keyring.getAwsKmsMrkMultiKeyring() != null) { + AwsKmsMrkMultiKeyringConfig multi = keyring.getAwsKmsMrkMultiKeyring(); + requireNoGrantTokens(multi.hasGrantTokens() && !multi.getGrantTokens().isEmpty()); + return AwsKmsMrkAwareMasterKeyProvider.builder().buildStrict(kmsKeyIds(multi.getGenerator(), + multi.hasKmsKeyIds() ? multi.getKmsKeyIds() : List.of())); + } + if (keyring.getMulti() != null) { + MultiKeyringConfig multi = keyring.getMulti(); + List> providers = new ArrayList<>(); + if (multi.getGenerator() != null) { + providers.add(toMasterKeyProvider(multi.getGenerator())); + } + for (Keyring child : multi.getChildKeyrings()) { + providers.add(toMasterKeyProvider(child)); + } + return MultipleProviderFactory.buildMultiProvider(providers); + } + throw new UnsupportedOperationException( + "this keyring is not supported under the Caching CMM: the ESDK's caching CMM wraps" + + " master key providers, and only the Raw AES, Raw RSA, AWS KMS, AWS KMS MRK, AWS KMS" + + " (MRK) multi, and Multi keyrings have master key provider equivalents"); + } + + private static List kmsKeyIds(String generator, List keyIds) { + List ids = new ArrayList<>(); + if (generator != null) { + ids.add(generator); + } + ids.addAll(keyIds); + return ids; + } + + private static void requireNoGrantTokens(boolean present) { + if (present) { + throw new UnsupportedOperationException( + "grant tokens are not supported under the Caching CMM"); + } + } + + /** The {@code JceMasterKey} RSA transformation for a modeled padding scheme. */ + private static String rsaTransformation(String paddingScheme) { + return switch (paddingScheme) { + case "PKCS1" -> "RSA/ECB/PKCS1Padding"; + case "OAEP_SHA1_MGF1" -> "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + case "OAEP_SHA256_MGF1" -> "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; + case "OAEP_SHA384_MGF1" -> "RSA/ECB/OAEPWithSHA-384AndMGF1Padding"; + case "OAEP_SHA512_MGF1" -> "RSA/ECB/OAEPWithSHA-512AndMGF1Padding"; + default -> throw new IllegalArgumentException("Unknown RSA padding scheme: " + paddingScheme); + }; + } + + private static PublicKey rsaPublicKey(ByteBuffer pem) { + try { + return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(pemBody(pem))); + } catch (Exception e) { + throw new IllegalArgumentException("invalid PEM RSA public key", e); + } + } + + private static PrivateKey rsaPrivateKey(ByteBuffer pem) { + try { + return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(pemBody(pem))); + } catch (Exception e) { + throw new IllegalArgumentException("invalid PEM RSA private key", e); + } + } + + /** The DER bytes inside a PEM block. */ + private static byte[] pemBody(ByteBuffer pem) { + String text = new String(bytes(pem), StandardCharsets.US_ASCII) + .replaceAll("-----[A-Z ]+-----", "") + .replaceAll("\\s", ""); + return Base64.getDecoder().decode(text); + } + + private static byte[] bytes(ByteBuffer buffer) { + byte[] copy = new byte[buffer.remaining()]; + buffer.duplicate().get(copy); + return copy; + } + private IKeyring buildKeyring(Keyring keyring) { RawAesKeyringConfig rawAes = keyring.getRawAes(); RawRsaKeyringConfig rawRsa = keyring.getRawRsa(); diff --git a/test-server/server/src/main/java/aws/cryptography/esdk/testserver/server/registry/RealEsdkClient.java b/test-server/server/src/main/java/aws/cryptography/esdk/testserver/server/registry/RealEsdkClient.java index 030ae7b7..fd78c429 100644 --- a/test-server/server/src/main/java/aws/cryptography/esdk/testserver/server/registry/RealEsdkClient.java +++ b/test-server/server/src/main/java/aws/cryptography/esdk/testserver/server/registry/RealEsdkClient.java @@ -5,6 +5,7 @@ import com.amazonaws.encryptionsdk.CommitmentPolicy; import com.amazonaws.encryptionsdk.CryptoAlgorithm; import com.amazonaws.encryptionsdk.CryptoInputStream; +import com.amazonaws.encryptionsdk.CryptoMaterialsManager; import com.amazonaws.encryptionsdk.CryptoResult; import java.io.InputStream; import java.io.OutputStream; @@ -16,7 +17,9 @@ * A configured {@link EsdkClient} backed by the REAL AWS Encryption SDK for Java. * It pairs an {@link AwsCrypto} configuration (commitment policy, optional max * encrypted data keys) with a real cryptographic materials manager built from the - * modeled config by {@code EsdkClientFactory}. + * modeled config by {@code EsdkClientFactory}: a Material Providers Library CMM, or, + * for the Caching CMM, the ESDK's own {@link CryptoMaterialsManager} + * ({@code CachingCryptoMaterialsManager}), which wraps master key providers. * *

Every call reaches the real ESDK. Any exception the ESDK throws is caught and * rethrown as an {@link EsdkClientException} so the operation wrapper forwards it @@ -28,14 +31,28 @@ public final class RealEsdkClient implements EsdkClient { private final CommitmentPolicy commitmentPolicy; private final Integer maxEncryptedDataKeys; + /** Exactly one of {@code cmm} and {@code legacyCmm} is set. */ private final ICryptographicMaterialsManager cmm; + private final CryptoMaterialsManager legacyCmm; public RealEsdkClient(CommitmentPolicy commitmentPolicy, Integer maxEncryptedDataKeys, ICryptographicMaterialsManager cmm) { + this(commitmentPolicy, maxEncryptedDataKeys, cmm, null); + } + + public RealEsdkClient(CommitmentPolicy commitmentPolicy, + Integer maxEncryptedDataKeys, + CryptoMaterialsManager legacyCmm) { + this(commitmentPolicy, maxEncryptedDataKeys, null, legacyCmm); + } + + private RealEsdkClient(CommitmentPolicy commitmentPolicy, Integer maxEncryptedDataKeys, + ICryptographicMaterialsManager cmm, CryptoMaterialsManager legacyCmm) { this.commitmentPolicy = commitmentPolicy; this.maxEncryptedDataKeys = maxEncryptedDataKeys; this.cmm = cmm; + this.legacyCmm = legacyCmm; } @Override @@ -43,8 +60,9 @@ public byte[] encrypt(byte[] plaintext, Map encryptionContext, String algorithmSuiteId, Long frameLength) throws EsdkClientException { try { AwsCrypto crypto = buildCrypto(algorithmSuiteId, frameLength); - CryptoResult result = - crypto.encryptData(cmm, plaintext, nonNull(encryptionContext)); + CryptoResult result = legacyCmm != null + ? crypto.encryptData(legacyCmm, plaintext, nonNull(encryptionContext)) + : crypto.encryptData(cmm, plaintext, nonNull(encryptionContext)); return result.getResult(); } catch (Exception esdkFailure) { throw new EsdkClientException(esdkFailure); @@ -57,6 +75,11 @@ public byte[] decrypt(byte[] ciphertext, Map encryptionContext) try { AwsCrypto crypto = buildCrypto(null, null); Map ec = nonNull(encryptionContext); + if (legacyCmm != null) { + CryptoResult result = crypto.decryptData(legacyCmm, ciphertext); + requireReproducedContext(ec, result.getEncryptionContext()); + return result.getResult(); + } CryptoResult result = ec.isEmpty() ? crypto.decryptData(cmm, ciphertext) : crypto.decryptData(cmm, ciphertext, ec); @@ -79,8 +102,9 @@ public void encryptStream(InputStream plaintext, OutputStream ciphertext, // stream round trip (Requirement 4.9); the read-side form finalizes correctly // for all inputs including empty. Both drive the real ESDK streaming encrypt // API (Requirement 4.5). - try (CryptoInputStream encrypting = - crypto.createEncryptingStream(cmm, plaintext, nonNull(encryptionContext))) { + try (CryptoInputStream encrypting = legacyCmm != null + ? crypto.createEncryptingStream(legacyCmm, plaintext, nonNull(encryptionContext)) + : crypto.createEncryptingStream(cmm, plaintext, nonNull(encryptionContext))) { encrypting.transferTo(ciphertext); } } catch (Exception esdkFailure) { @@ -98,9 +122,15 @@ public void decryptStream(InputStream ciphertext, OutputStream plaintext, // Required-Encryption-Context CMM (which drops the required keys from the // message header) can reconstruct them — mirroring the blob decrypt path. // Both drive the real ESDK streaming decrypt API (Requirement 4.6). - try (CryptoInputStream decrypting = ec.isEmpty() - ? crypto.createDecryptingStream(cmm, ciphertext) - : crypto.createDecryptingStream(cmm, ciphertext, ec)) { + try (CryptoInputStream decrypting = legacyCmm != null + ? crypto.createDecryptingStream(legacyCmm, ciphertext) + : ec.isEmpty() + ? crypto.createDecryptingStream(cmm, ciphertext) + : crypto.createDecryptingStream(cmm, ciphertext, ec)) { + if (legacyCmm != null) { + // Reads only the header, so no plaintext is released on a mismatch. + requireReproducedContext(ec, decrypting.getCryptoResult().getEncryptionContext()); + } decrypting.transferTo(plaintext); } } catch (Exception esdkFailure) { @@ -134,6 +164,21 @@ private AwsCrypto buildCrypto(String algorithmSuiteId, Long frameLength) { return builder.build(); } + /** + * The ESDK's own CMMs take no reproduced encryption context on decrypt, so the + * caller checks it against the message's, as the ESDK for Java examples do: + * every reproduced pair must be present with the same value. + */ + private static void requireReproducedContext(Map reproduced, + Map message) { + for (Map.Entry entry : reproduced.entrySet()) { + if (!entry.getValue().equals(message.get(entry.getKey()))) { + throw new IllegalArgumentException( + "the reproduced encryption context does not match the message's encryption context"); + } + } + } + private static Map nonNull(Map encryptionContext) { return encryptionContext == null ? Collections.emptyMap() : encryptionContext; } diff --git a/test-server/server/src/test/java/aws/cryptography/esdk/testserver/server/handler/CreateClientFailurePropertyTest.java b/test-server/server/src/test/java/aws/cryptography/esdk/testserver/server/handler/CreateClientFailurePropertyTest.java index 23414045..81902bcf 100644 --- a/test-server/server/src/test/java/aws/cryptography/esdk/testserver/server/handler/CreateClientFailurePropertyTest.java +++ b/test-server/server/src/test/java/aws/cryptography/esdk/testserver/server/handler/CreateClientFailurePropertyTest.java @@ -18,6 +18,7 @@ import aws.cryptography.esdk.testserver.server.model.Keyring; import aws.cryptography.esdk.testserver.server.model.MultiKeyringConfig; import aws.cryptography.esdk.testserver.server.model.RawAesKeyringConfig; +import aws.cryptography.esdk.testserver.server.model.RequiredEncryptionContextCmmConfig; import aws.cryptography.esdk.testserver.server.registry.ClientRegistry; import java.nio.ByteBuffer; import java.util.List; @@ -31,8 +32,9 @@ * unchanged, using jqwik. Runs a minimum of 100 generated iterations. Uses the * real {@link ConfigValidator} and {@link EsdkClientFactory}; the generated * configs are valid per the exactly-one-variant rule but cause the real ESDK - * client construction to fail deterministically and offline (an unwired - * Caching CMM, an AWS KMS RSA keyring missing its encryption algorithm, or a + * client construction to fail deterministically and offline (a Caching CMM + * wrapping a non-Default CMM, which the ESDK's caching CMM cannot wrap, an AWS + * KMS RSA keyring missing its encryption algorithm, or a * Raw AES keyring whose wrapping key length does not match its wrapping * algorithm), nested to varying depth. */ @@ -76,7 +78,12 @@ private static ESDKClientConfig constructionFailingConfig( CryptographicMaterialsManager cmm = useCachingCmm ? CryptographicMaterialsManager.builder() .caching(CachingCmmConfig.builder() - .underlyingCMM(validDefaultCmm()) + .underlyingCMM(CryptographicMaterialsManager.builder() + .requiredEncryptionContext(RequiredEncryptionContextCmmConfig.builder() + .underlyingCMM(validDefaultCmm()) + .requiredEncryptionContextKeys(List.of("purpose")) + .build()) + .build()) .cacheLimitTtlSeconds(60) .build()) .build()