diff --git a/.changes/next-release/feature-AmazonCloudFront-51e8b2d.json b/.changes/next-release/feature-AmazonCloudFront-51e8b2d.json new file mode 100644 index 000000000000..2f5333dbdb25 --- /dev/null +++ b/.changes/next-release/feature-AmazonCloudFront-51e8b2d.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Amazon CloudFront", + "contributor": "", + "description": "Add support for ECDSA signed URLs." +} diff --git a/services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilities.java b/services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilities.java index 99e9569f0cbf..e5b2bceb90ff 100644 --- a/services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilities.java +++ b/services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilities.java @@ -19,6 +19,7 @@ import java.net.URI; import java.security.InvalidKeyException; +import java.security.PrivateKey; import java.util.function.Consumer; import software.amazon.awssdk.annotations.Immutable; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -140,7 +141,7 @@ public SignedUrl getSignedUrlWithCannedPolicy(CannedSignerRequest request) { try { String resourceUrl = request.resourceUrl(); String cannedPolicy = SigningUtils.buildCannedPolicy(resourceUrl, request.expirationDate()); - byte[] signatureBytes = SigningUtils.signWithSha1Rsa(cannedPolicy.getBytes(UTF_8), request.privateKey()); + byte[] signatureBytes = signPolicy(cannedPolicy.getBytes(UTF_8), request.privateKey()); String urlSafeSignature = SigningUtils.makeBytesUrlSafe(signatureBytes); URI uri = URI.create(resourceUrl); String protocol = uri.getScheme(); @@ -266,7 +267,7 @@ public SignedUrl getSignedUrlWithCustomPolicy(CustomSignerRequest request) { request.expirationDate(), request.ipRange()); - byte[] signatureBytes = SigningUtils.signWithSha1Rsa(policy.getBytes(UTF_8), request.privateKey()); + byte[] signatureBytes = signPolicy(policy.getBytes(UTF_8), request.privateKey()); String urlSafePolicy = SigningUtils.makeStringUrlSafe(policy); String urlSafeSignature = SigningUtils.makeBytesUrlSafe(signatureBytes); URI uri = URI.create(resourceUrl); @@ -368,7 +369,7 @@ public CookiesForCannedPolicy getCookiesForCannedPolicy(Consumer keyCases() throws Exception { + return Stream.of( + new KeyTestCase("RSA", rsaKeyPairId, rsaPrivateKey, rsaKeyFilePath), + new KeyTestCase("ECDSA", ecKeyPairId, ecPrivateKey, ecKeyFilePath) + ); + } + + @Test void unsignedUrl_shouldReturn403Response() throws Exception { SdkHttpClient client = ApacheHttpClient.create(); @@ -115,14 +153,15 @@ void unsignedUrl_shouldReturn403Response() throws Exception { assertThat(response.httpResponse().statusCode()).isEqualTo(expectedStatus); } - @Test - void getSignedUrlWithCannedPolicy_producesValidUrl() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedUrlWithCannedPolicy_producesValidUrl(KeyTestCase testCase) throws Exception { InputStream originalBucketContent = s3Client.getObject(r -> r.bucket(bucket).key(S3_OBJECT_KEY)); Instant expirationDate = LocalDate.of(2050, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); CannedSignerRequest request = CannedSignerRequest.builder() .resourceUrl(resourceUrl) - .privateKey(keyFilePath) - .keyPairId(keyPairId) + .privateKey(testCase.keyFilePath) + .keyPairId(testCase.keyPairId) .expirationDate(expirationDate).build(); SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCannedPolicy(request); SdkHttpClient client = ApacheHttpClient.create(); @@ -136,12 +175,13 @@ void getSignedUrlWithCannedPolicy_producesValidUrl() throws Exception { assertThat(retrievedBucketContent).hasSameContentAs(originalBucketContent); } - @Test - void getSignedUrlWithCannedPolicy_withExpiredDate_shouldReturn403Response() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedUrlWithCannedPolicy_withExpiredDate_shouldReturn403Response(KeyTestCase testCase) throws Exception { Instant expirationDate = LocalDate.of(2020, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCannedPolicy(r -> r.resourceUrl(resourceUrl) - .privateKey(privateKey) - .keyPairId(keyPairId) + .privateKey(testCase.privateKey) + .keyPairId(testCase.keyPairId) .expirationDate(expirationDate)); SdkHttpClient client = ApacheHttpClient.create(); HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder() @@ -151,15 +191,16 @@ void getSignedUrlWithCannedPolicy_withExpiredDate_shouldReturn403Response() thro assertThat(response.httpResponse().statusCode()).isEqualTo(expectedStatus); } - @Test - void getSignedUrlWithCustomPolicy_producesValidUrl() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedUrlWithCustomPolicy_producesValidUrl(KeyTestCase testCase) throws Exception { InputStream originalBucketContent = s3Client.getObject(r -> r.bucket(bucket).key(S3_OBJECT_KEY)); Instant activeDate = LocalDate.of(2022, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); Instant expirationDate = LocalDate.of(2050, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(resourceUrl) - .privateKey(keyFilePath) - .keyPairId(keyPairId) + .privateKey(testCase.keyFilePath) + .keyPairId(testCase.keyPairId) .expirationDate(expirationDate) .activeDate(activeDate).build(); SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(request); @@ -179,8 +220,8 @@ void getSignedUrlWithCustomPolicy_withFutureActiveDate_shouldReturn403Response() Instant activeDate = LocalDate.of(2040, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); Instant expirationDate = LocalDate.of(2050, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(r -> r.resourceUrl(resourceUrl) - .privateKey(privateKey) - .keyPairId(keyPairId) + .privateKey(rsaPrivateKey) + .keyPairId(rsaKeyPairId) .expirationDate(expirationDate) .activeDate(activeDate)); SdkHttpClient client = ApacheHttpClient.create(); @@ -191,13 +232,14 @@ void getSignedUrlWithCustomPolicy_withFutureActiveDate_shouldReturn403Response() assertThat(response.httpResponse().statusCode()).isEqualTo(expectedStatus); } - @Test - void getCookiesForCannedPolicy_producesValidCookies() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getCookiesForCannedPolicy_producesValidCookies(KeyTestCase testCase) throws Exception { InputStream originalBucketContent = s3Client.getObject(r -> r.bucket(bucket).key(S3_OBJECT_KEY)); Instant expirationDate = LocalDate.of(2050, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); CookiesForCannedPolicy cookies = cloudFrontUtilities.getCookiesForCannedPolicy(r -> r.resourceUrl(resourceUrl) - .privateKey(privateKey) - .keyPairId(keyPairId) + .privateKey(testCase.privateKey) + .keyPairId(testCase.keyPairId) .expirationDate(expirationDate)); SdkHttpClient client = ApacheHttpClient.create(); @@ -216,8 +258,8 @@ void getCookiesForCannedPolicy_withExpiredDate_shouldReturn403Response() throws Instant expirationDate = LocalDate.of(2020, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); CannedSignerRequest request = CannedSignerRequest.builder() .resourceUrl(resourceUrl) - .privateKey(keyFilePath) - .keyPairId(keyPairId) + .privateKey(rsaKeyFilePath) + .keyPairId(rsaKeyPairId) .expirationDate(expirationDate).build(); CookiesForCannedPolicy cookies = cloudFrontUtilities.getCookiesForCannedPolicy(request); @@ -229,14 +271,15 @@ void getCookiesForCannedPolicy_withExpiredDate_shouldReturn403Response() throws assertThat(response.httpResponse().statusCode()).isEqualTo(expectedStatus); } - @Test - void getCookiesForCustomPolicy_producesValidCookies() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getCookiesForCustomPolicy_producesValidCookies(KeyTestCase testCase) throws Exception { InputStream originalBucketContent = s3Client.getObject(r -> r.bucket(bucket).key(S3_OBJECT_KEY)); Instant activeDate = LocalDate.of(2022, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); Instant expirationDate = LocalDate.of(2050, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); CookiesForCustomPolicy cookies = cloudFrontUtilities.getCookiesForCustomPolicy(r -> r.resourceUrl(resourceUrl) - .privateKey(privateKey) - .keyPairId(keyPairId) + .privateKey(testCase.privateKey) + .keyPairId(testCase.keyPairId) .expirationDate(expirationDate) .activeDate(activeDate)); @@ -257,8 +300,8 @@ void getCookiesForCustomPolicy_withFutureActiveDate_shouldReturn403Response() th Instant expirationDate = LocalDate.of(2050, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(resourceUrl) - .privateKey(keyFilePath) - .keyPairId(keyPairId) + .privateKey(rsaKeyFilePath) + .keyPairId(rsaKeyPairId) .expirationDate(expirationDate) .activeDate(activeDate).build(); CookiesForCustomPolicy cookies = cloudFrontUtilities.getCookiesForCustomPolicy(request); @@ -271,8 +314,9 @@ void getCookiesForCustomPolicy_withFutureActiveDate_shouldReturn403Response() th assertThat(response.httpResponse().statusCode()).isEqualTo(expectedStatus); } - @Test - void getSignedUrlWithCustomPolicy_shouldAllowQueryParametersWhenUsingWildcard() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedUrlWithCustomPolicy_shouldAllowQueryParametersWhenUsingWildcard(KeyTestCase testCase) throws Exception { Instant expirationDate = LocalDate.of(2050, 1, 1) .atStartOfDay() .toInstant(ZoneOffset.of("Z")); @@ -283,8 +327,8 @@ void getSignedUrlWithCustomPolicy_shouldAllowQueryParametersWhenUsingWildcard() CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(resourceUrl) - .privateKey(keyFilePath) - .keyPairId(keyPairId) + .privateKey(testCase.keyFilePath) + .keyPairId(testCase.keyPairId) .resourceUrlPattern(resourceUrl + "*") .activeDate(activeDate) .expirationDate(expirationDate) @@ -308,8 +352,9 @@ void getSignedUrlWithCustomPolicy_shouldAllowQueryParametersWhenUsingWildcard() assertThat(response.httpResponse().statusCode()).isEqualTo(200); } - @Test - void getSignedUrlWithCustomPolicy_wildCardPath() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedUrlWithCustomPolicy_wildCardPath(KeyTestCase testCase) throws Exception { String resourceUri = "https://" + domainName; Instant expirationDate = LocalDate.of(2050, 1, 1) .atStartOfDay() @@ -321,8 +366,8 @@ void getSignedUrlWithCustomPolicy_wildCardPath() throws Exception { CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(resourceUri + "/foo/specific-file") - .privateKey(keyFilePath) - .keyPairId(keyPairId) + .privateKey(testCase.keyFilePath) + .keyPairId(testCase.keyPairId) .resourceUrlPattern(resourceUri + "/foo/*") .activeDate(activeDate) .expirationDate(expirationDate) @@ -344,8 +389,9 @@ void getSignedUrlWithCustomPolicy_wildCardPath() throws Exception { assertThat(response.httpResponse().statusCode()).isEqualTo(200); } - @Test - void getSignedUrlWithCustomPolicy_wildCardPolicyResource_allowsAnyPath() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedUrlWithCustomPolicy_wildCardPolicyResource_allowsAnyPath(KeyTestCase testCase) throws Exception { Instant expirationDate = LocalDate.of(2050, 1, 1) .atStartOfDay() .toInstant(ZoneOffset.of("Z")); @@ -356,8 +402,8 @@ void getSignedUrlWithCustomPolicy_wildCardPolicyResource_allowsAnyPath() throws CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(resourceUrl) - .privateKey(keyFilePath) - .keyPairId(keyPairId) + .privateKey(testCase.keyFilePath) + .keyPairId(testCase.keyPairId) .resourceUrlPattern("*") .activeDate(activeDate) .expirationDate(expirationDate) @@ -380,7 +426,8 @@ void getSignedUrlWithCustomPolicy_wildCardPolicyResource_allowsAnyPath() throws } private static void initStaticFields() throws Exception { - initializeKeyFileAndPair(); + initializeRsaKeyFileAndPair(); + initializeEcKeyFileAndPair(); originAccessId = getOrCreateOriginAccessIdentity(); keyGroupId = getOrCreateKeyGroup(); bucket = getOrCreateBucket(); @@ -392,16 +439,16 @@ private static void initStaticFields() throws Exception { distributionId = distribution.id; } - private static void initializeKeyFileAndPair() throws Exception { - keyFile = new RandomTempFile(UUID.randomUUID() + "-key.pem", 0); - keyFile.deleteOnExit(); - keyFilePath = keyFile.toPath(); + private static void initializeRsaKeyFileAndPair() throws Exception { + rsaKeyFile = new RandomTempFile(UUID.randomUUID() + "-key.pem", 0); + rsaKeyFile.deleteOnExit(); + rsaKeyFilePath = rsaKeyFile.toPath(); String privateKeyName = RESOURCE_PREFIX + "private-key"; String publicKeyName = RESOURCE_PREFIX + "public-key"; try { GetSecretValueResponse getSecretResponse = secretsManagerClient.getSecretValue(r -> r.secretId(privateKeyName)); - Files.write(keyFile.toPath(), getSecretResponse.secretBinary().asByteArray(), StandardOpenOption.TRUNCATE_EXISTING); + Files.write(rsaKeyFile.toPath(), getSecretResponse.secretBinary().asByteArray(), StandardOpenOption.TRUNCATE_EXISTING); Optional key = cloudFrontClient.listPublicKeys() .publicKeyList() @@ -410,8 +457,8 @@ private static void initializeKeyFileAndPair() throws Exception { .filter(k -> publicKeyName.equals(k.name())) .findAny(); if (key.isPresent()) { - privateKey = SigningUtils.loadPrivateKey(keyFilePath); - keyPairId = key.get().id(); + rsaPrivateKey = SigningUtils.loadPrivateKey(rsaKeyFilePath); + rsaKeyPairId = key.get().id(); return; } } catch (ResourceNotFoundException e) { @@ -425,13 +472,76 @@ private static void initializeKeyFileAndPair() throws Exception { kpg.initialize(2048); KeyPair keyPair = kpg.generateKeyPair(); - FileWriter writer = new FileWriter(keyFile); + FileWriter writer = new FileWriter(rsaKeyFile); + writer.write("-----BEGIN PRIVATE KEY-----\n"); + writer.write(ENCODER.encodeToString(keyPair.getPrivate().getEncoded())); + writer.write("\n-----END PRIVATE KEY-----\n"); + writer.close(); + + SdkBytes keyFileBytes = SdkBytes.fromByteArray(Files.readAllBytes(rsaKeyFilePath)); + try { + secretsManagerClient.createSecret(r -> r.name(privateKeyName) + .secretBinary(keyFileBytes)); + } catch (ResourceExistsException e) { + secretsManagerClient.putSecretValue(r -> r.secretId(privateKeyName) + .secretBinary(keyFileBytes)); + } + + String encodedKey = "-----BEGIN PUBLIC KEY-----\n" + + ENCODER.encodeToString(keyPair.getPublic().getEncoded()) + + "\n-----END PUBLIC KEY-----\n"; + + + CreatePublicKeyResponse publicKeyResponse = + cloudFrontClient.createPublicKey(r -> r.publicKeyConfig(k -> k.callerReference(CALLER_REFERENCE) + .name(publicKeyName) + .encodedKey(encodedKey))); + rsaPrivateKey = keyPair.getPrivate(); + rsaKeyPairId = publicKeyResponse.publicKey().id(); + } + + private static void initializeEcKeyFileAndPair() throws Exception { + ecKeyFile = new RandomTempFile(UUID.randomUUID() + "-key.pem", 0); + ecKeyFile.deleteOnExit(); + ecKeyFilePath = ecKeyFile.toPath(); + + String privateKeyName = RESOURCE_PREFIX + "private-key-ecdsa"; + String publicKeyName = RESOURCE_PREFIX + "public-key-ecdsa"; + + try { + GetSecretValueResponse getSecretResponse = secretsManagerClient.getSecretValue(r -> r.secretId(privateKeyName)); + Files.write(ecKeyFile.toPath(), getSecretResponse.secretBinary().asByteArray(), + StandardOpenOption.TRUNCATE_EXISTING); + + Optional key = cloudFrontClient.listPublicKeys() + .publicKeyList() + .items() + .stream() + .filter(k -> publicKeyName.equals(k.name())) + .findAny(); + if (key.isPresent()) { + ecPrivateKey = SigningUtils.loadPrivateKey(ecKeyFilePath); + ecKeyPairId = key.get().id(); + return; + } + } catch (ResourceNotFoundException e) { + // No private key, don't bother checking for a public one. + } + + System.out.println("Creating keys."); + + // We were missing a private or public key. Initialize them both. + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(new ECGenParameterSpec("secp256r1")); + KeyPair keyPair = kpg.generateKeyPair(); + + FileWriter writer = new FileWriter(ecKeyFile); writer.write("-----BEGIN PRIVATE KEY-----\n"); writer.write(ENCODER.encodeToString(keyPair.getPrivate().getEncoded())); writer.write("\n-----END PRIVATE KEY-----\n"); writer.close(); - SdkBytes keyFileBytes = SdkBytes.fromByteArray(Files.readAllBytes(keyFilePath)); + SdkBytes keyFileBytes = SdkBytes.fromByteArray(Files.readAllBytes(ecKeyFilePath)); try { secretsManagerClient.createSecret(r -> r.name(privateKeyName) .secretBinary(keyFileBytes)); @@ -449,8 +559,8 @@ private static void initializeKeyFileAndPair() throws Exception { cloudFrontClient.createPublicKey(r -> r.publicKeyConfig(k -> k.callerReference(CALLER_REFERENCE) .name(publicKeyName) .encodedKey(encodedKey))); - privateKey = keyPair.getPrivate(); - keyPairId = publicKeyResponse.publicKey().id(); + ecPrivateKey = keyPair.getPrivate(); + ecKeyPairId = publicKeyResponse.publicKey().id(); } private static String getOrCreateOriginAccessIdentity() { @@ -496,7 +606,7 @@ private static String getOrCreateKeyGroup() { System.out.println("Creating key group."); return cloudFrontClient.createKeyGroup(r -> r.keyGroupConfig(c -> c.name(keyGroupName) - .items(keyPairId))) + .items(rsaKeyPairId, ecKeyPairId))) .keyGroup() .id(); } diff --git a/services/cloudfront/src/test/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilitiesTest.java b/services/cloudfront/src/test/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilitiesTest.java index ee67d2969f05..a07e26d1e0ce 100644 --- a/services/cloudfront/src/test/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilitiesTest.java +++ b/services/cloudfront/src/test/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilitiesTest.java @@ -22,24 +22,35 @@ import java.io.File; import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.spec.ECGenParameterSpec; import java.time.LocalDate; import java.time.Instant; import java.time.ZoneOffset; import java.util.Base64; import java.util.StringJoiner; import java.util.stream.Stream; +import junit.framework.TestCase; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.services.cloudfront.cookie.CookiesForCannedPolicy; import software.amazon.awssdk.services.cloudfront.cookie.CookiesForCustomPolicy; +import software.amazon.awssdk.services.cloudfront.internal.utils.SigningUtils; import software.amazon.awssdk.services.cloudfront.model.CannedSignerRequest; import software.amazon.awssdk.services.cloudfront.model.CustomSignerRequest; import software.amazon.awssdk.services.cloudfront.url.SignedUrl; @@ -48,45 +59,82 @@ class CloudFrontUtilitiesTest { private static final String RESOURCE_URL = "https://d1npcfkc2mojrf.cloudfront.net/s3ObjectKey"; private static final String RESOURCE_URL_WITH_PORT = "https://d1npcfkc2mojrf.cloudfront.net:65535/s3ObjectKey"; - private static KeyPairGenerator kpg; - private static KeyPair keyPair; - private static File keyFile; - private static Path keyFilePath; private static CloudFrontUtilities cloudFrontUtilities; - @BeforeAll - static void setUp() throws Exception { - initKeys(); - cloudFrontUtilities = CloudFrontUtilities.create(); + @TempDir + static Path tempDir; + + private static class KeyTestCase { + final String name; + final KeyPair keyPair; + final Path keyFilePath; + + KeyTestCase(String name, KeyPair keyPair, Path keyFilePath) { + this.name = name; + this.keyPair = keyPair; + this.keyFilePath = keyFilePath; + } + + @Override + public String toString() { + return name; + } + + static KeyTestCase createRsaTestCase() { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair keyPair = kpg.generateKeyPair(); + return new KeyTestCase("RSA", keyPair, writeKeyToFile("rsa", keyPair)); + } catch(NoSuchAlgorithmException | IOException e) { + throw new RuntimeException(e); + } + } + + static KeyTestCase createECDSATestCase() { + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(new ECGenParameterSpec("secp256r1")); + KeyPair keyPair = kpg.generateKeyPair(); + return new KeyTestCase("ECDSA", keyPair, writeKeyToFile("ec", keyPair)); + } catch(NoSuchAlgorithmException | IOException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + private static Path writeKeyToFile(String name, KeyPair keyPair) throws IOException { + Path keyFilePath = tempDir.resolve(name + "_key.pem"); + try (Writer writer = Files.newBufferedWriter(keyFilePath)) { + writer.write("-----BEGIN PRIVATE KEY-----\n"); + writer.write(Base64.getEncoder() + .encodeToString(keyPair.getPrivate().getEncoded())); + writer.write("\n-----END PRIVATE KEY-----\n"); + } + return keyFilePath; + } + } - @AfterAll - static void tearDown() { - keyFile.deleteOnExit(); + static Stream keyCases() throws Exception { + return Stream.of( + KeyTestCase.createRsaTestCase(), + KeyTestCase.createECDSATestCase() + ); } - static void initKeys() throws Exception { - kpg = KeyPairGenerator.getInstance("RSA"); - kpg.initialize(2048); - keyPair = kpg.generateKeyPair(); - - Base64.Encoder encoder = Base64.getEncoder(); - keyFile = new File("key.pem"); - FileWriter writer = new FileWriter(keyFile); - writer.write("-----BEGIN PRIVATE KEY-----\n"); - writer.write(encoder.encodeToString(keyPair.getPrivate().getEncoded())); - writer.write("\n-----END PRIVATE KEY-----\n"); - writer.close(); - keyFilePath = keyFile.toPath(); + @BeforeAll + static void setUp() throws Exception { + cloudFrontUtilities = CloudFrontUtilities.create(); } - @Test - void getSignedURLWithCannedPolicy_producesValidUrl() { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCannedPolicy_producesValidUrl(KeyTestCase testCase) { Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCannedPolicy(r -> r .resourceUrl(RESOURCE_URL) - .privateKey(keyPair.getPrivate()) + .privateKey(testCase.keyPair.getPrivate()) .keyPairId("keyPairId") .expirationDate(expirationDate)); String url = signedUrl.url(); @@ -97,14 +145,15 @@ void getSignedURLWithCannedPolicy_producesValidUrl() { assertThat(expected).isEqualTo(url); } - @Test - void getSignedURLWithCannedPolicy_withQueryParams_producesValidUrl() { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCannedPolicy_withQueryParams_producesValidUrl(KeyTestCase testCase) { Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); String resourceUrlWithQueryParams = "https://d1npcfkc2mojrf.cloudfront.net/s3ObjectKey?a=b&c=d"; SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCannedPolicy(r -> r .resourceUrl(resourceUrlWithQueryParams) - .privateKey(keyPair.getPrivate()) + .privateKey(testCase.keyPair.getPrivate()) .keyPairId("keyPairId") .expirationDate(expirationDate)); String url = signedUrl.url(); @@ -116,15 +165,16 @@ void getSignedURLWithCannedPolicy_withQueryParams_producesValidUrl() { assertThat(expected).isEqualTo(url); } - @Test - void getSignedURLWithCustomPolicy_producesValidUrl() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCustomPolicy_producesValidUrl(KeyTestCase testCase) throws Exception { Instant activeDate = LocalDate.of(2022, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); String ipRange = "1.2.3.4"; SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(r -> { try { r.resourceUrl(RESOURCE_URL) - .privateKey(keyFilePath) + .privateKey(testCase.keyFilePath) .keyPairId("keyPairId") .expirationDate(expirationDate) .activeDate(activeDate) @@ -143,8 +193,9 @@ void getSignedURLWithCustomPolicy_producesValidUrl() throws Exception { assertThat(expected).isEqualTo(url); } - @Test - void getSignedURLWithCustomPolicy_withQueryParams_producesValidUrl() { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCustomPolicy_withQueryParams_producesValidUrl(KeyTestCase testCase) throws Exception { Instant activeDate = LocalDate.of(2022, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); String ipRange = "1.2.3.4"; @@ -152,7 +203,7 @@ void getSignedURLWithCustomPolicy_withQueryParams_producesValidUrl() { SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(r -> r .resourceUrl(resourceUrlWithQueryParams) - .privateKey(keyPair.getPrivate()) + .privateKey(testCase.keyPair.getPrivate()) .keyPairId("keyPairId") .expirationDate(expirationDate) .activeDate(activeDate) @@ -167,13 +218,14 @@ void getSignedURLWithCustomPolicy_withQueryParams_producesValidUrl() { assertThat(expected).isEqualTo(url); } - @Test - void getSignedURLWithCustomPolicy_withIpRangeOmitted_producesValidUrl() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCustomPolicy_withIpRangeOmitted_producesValidUrl(KeyTestCase testCase) throws Exception { Instant activeDate = LocalDate.of(2022, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(RESOURCE_URL) - .privateKey(keyFilePath) + .privateKey(testCase.keyFilePath) .keyPairId("keyPairId") .expirationDate(expirationDate) .activeDate(activeDate) @@ -189,13 +241,14 @@ void getSignedURLWithCustomPolicy_withIpRangeOmitted_producesValidUrl() throws E assertThat(expected).isEqualTo(url); } - @Test - void getSignedURLWithCustomPolicy_withActiveDateOmitted_producesValidUrl() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCustomPolicy_withActiveDateOmitted_producesValidUrl(KeyTestCase testCase) throws Exception { Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); String ipRange = "1.2.3.4"; CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(RESOURCE_URL) - .privateKey(keyFilePath) + .privateKey(testCase.keyFilePath) .keyPairId("keyPairId") .expirationDate(expirationDate) .ipRange(ipRange) @@ -211,25 +264,27 @@ void getSignedURLWithCustomPolicy_withActiveDateOmitted_producesValidUrl() throw assertThat(expected).isEqualTo(url); } - @Test - void getSignedURLWithCustomPolicy_withMissingExpirationDate_shouldThrowException() { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCustomPolicy_withMissingExpirationDate_shouldThrowException(KeyTestCase testCase) throws Exception { NullPointerException exception = assertThrows(NullPointerException.class, () -> cloudFrontUtilities.getSignedUrlWithCustomPolicy(r -> r .resourceUrl(RESOURCE_URL) - .privateKey(keyPair.getPrivate()) + .privateKey(testCase.keyPair.getPrivate()) .keyPairId("keyPairId")) ); assertThat(exception.getMessage().contains("Expiration date must be provided to sign CloudFront URLs")); } - @Test - void getSignedURLWithCannedPolicy_withEncodedUrl_doesNotDecodeUrl() { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCannedPolicy_withEncodedUrl_doesNotDecodeUrl(KeyTestCase testCase) throws Exception { String encodedUrl = "https://distributionDomain/s3ObjectKey/%40blob?v=1n1dm%2F01n1dm0"; Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCannedPolicy(r -> r .resourceUrl(encodedUrl) - .privateKey(keyPair.getPrivate()) + .privateKey(testCase.keyPair.getPrivate()) .keyPairId("keyPairId") .expirationDate(expirationDate)); String url = signedUrl.url(); @@ -240,8 +295,9 @@ void getSignedURLWithCannedPolicy_withEncodedUrl_doesNotDecodeUrl() { assertThat(expected).isEqualTo(url); } - @Test - void getSignedURLWithCustomPolicy_withEncodedUrl_doesNotDecodeUrl() { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCustomPolicy_withEncodedUrl_doesNotDecodeUrl(KeyTestCase testCase) throws Exception { String encodedUrl = "https://distributionDomain/s3ObjectKey/%40blob?v=1n1dm%2F01n1dm0"; Instant activeDate = LocalDate.of(2022, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); @@ -249,7 +305,7 @@ void getSignedURLWithCustomPolicy_withEncodedUrl_doesNotDecodeUrl() { SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(r -> { try { r.resourceUrl(encodedUrl) - .privateKey(keyFilePath) + .privateKey(testCase.keyFilePath) .keyPairId("keyPairId") .expirationDate(expirationDate) .activeDate(activeDate) @@ -268,36 +324,39 @@ void getSignedURLWithCustomPolicy_withEncodedUrl_doesNotDecodeUrl() { assertThat(expected).isEqualTo(url); } - @Test - void getSignedURLWithCannedPolicy_withPortNumber_returnsPortNumber() { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCannedPolicy_withPortNumber_returnsPortNumber(KeyTestCase testCase) throws Exception { Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCannedPolicy(r -> r .resourceUrl(RESOURCE_URL_WITH_PORT) - .privateKey(keyPair.getPrivate()) + .privateKey(testCase.keyPair.getPrivate()) .keyPairId("keyPairId") .expirationDate(expirationDate)); assertThat(signedUrl.url()).contains("65535"); } - @Test - void getSignedURLWithCustomPolicy_withPortNumber_returnsPortNumber() { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getSignedURLWithCustomPolicy_withPortNumber_returnsPortNumber(KeyTestCase testCase) throws Exception { Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); SignedUrl signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(r -> r .resourceUrl(RESOURCE_URL_WITH_PORT) - .privateKey(keyPair.getPrivate()) + .privateKey(testCase.keyPair.getPrivate()) .keyPairId("keyPairId") .expirationDate(expirationDate)); assertThat(signedUrl.url()).contains("65535"); } - @Test - void getCookiesForCannedPolicy_producesValidCookies() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getCookiesForCannedPolicy_producesValidCookies(KeyTestCase testCase) throws Exception { Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); CannedSignerRequest request = CannedSignerRequest.builder() .resourceUrl(RESOURCE_URL) - .privateKey(keyFilePath) + .privateKey(testCase.keyFilePath) .keyPairId("keyPairId") .expirationDate(expirationDate) .build(); @@ -306,14 +365,15 @@ void getCookiesForCannedPolicy_producesValidCookies() throws Exception { assertThat(cookiesForCannedPolicy.keyPairIdHeaderValue()).isEqualTo("CloudFront-Key-Pair-Id=keyPairId"); } - @Test - void getCookiesForCustomPolicy_producesValidCookies() throws Exception { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getCookiesForCustomPolicy_producesValidCookies(KeyTestCase testCase) throws Exception { Instant activeDate = LocalDate.of(2022, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); String ipRange = "1.2.3.4"; CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(RESOURCE_URL) - .privateKey(keyFilePath) + .privateKey(testCase.keyFilePath) .keyPairId("keyPairId") .expirationDate(expirationDate) .activeDate(activeDate) @@ -324,12 +384,13 @@ void getCookiesForCustomPolicy_producesValidCookies() throws Exception { assertThat(cookiesForCustomPolicy.keyPairIdHeaderValue()).isEqualTo("CloudFront-Key-Pair-Id=keyPairId"); } - @Test - void getCookiesForCustomPolicy_withActiveDateAndIpRangeOmitted_producesValidCookies() { + @ParameterizedTest(name = "{0}") + @MethodSource("keyCases") + void getCookiesForCustomPolicy_withActiveDateAndIpRangeOmitted_producesValidCookies(KeyTestCase testCase) throws Exception { Instant expirationDate = LocalDate.of(2024, 1, 1).atStartOfDay().toInstant(ZoneOffset.of("Z")); CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(RESOURCE_URL) - .privateKey(keyPair.getPrivate()) + .privateKey(testCase.keyPair.getPrivate()) .keyPairId("keyPairId") .expirationDate(expirationDate) .build(); @@ -342,11 +403,12 @@ void getCookiesForCustomPolicy_withActiveDateAndIpRangeOmitted_producesValidCook @MethodSource("provideUrlPatternsAndExpectedResources") void getSignedURLWithCustomPolicy_policyResourceUrlShouldHandleVariousPatterns( String resourceUrlPattern, String expectedResource) { + KeyTestCase testCase = KeyTestCase.createRsaTestCase(); String baseUrl = "https://d1234.cloudfront.net/images/photo.jpg"; Instant expiration = Instant.now().plusSeconds(3600); CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(baseUrl) - .privateKey(keyPair.getPrivate()) + .privateKey(testCase.keyPair.getPrivate()) .keyPairId("keyPairId") .resourceUrlPattern(resourceUrlPattern) .expirationDate(expiration)