Skip to content

Commit c0cf30c

Browse files
authored
Fix cloudfront ecdsa integration tests (#6637)
* Add support for ECDSA signed CloudFront URLs * Fix checkstyle * Fix type + improve error message * Fix integration test - ensure key group has all required keys. * Use wiater until instead of Thread.sleep * Update waiter comments
1 parent 0732867 commit c0cf30c

File tree

6 files changed

+401
-132
lines changed

6 files changed

+401
-132
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Amazon CloudFront",
4+
"contributor": "",
5+
"description": "Add support for ECDSA signed URLs."
6+
}

services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilities.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.net.URI;
2121
import java.security.InvalidKeyException;
22+
import java.security.PrivateKey;
2223
import java.util.function.Consumer;
2324
import software.amazon.awssdk.annotations.Immutable;
2425
import software.amazon.awssdk.annotations.SdkPublicApi;
@@ -140,7 +141,7 @@ public SignedUrl getSignedUrlWithCannedPolicy(CannedSignerRequest request) {
140141
try {
141142
String resourceUrl = request.resourceUrl();
142143
String cannedPolicy = SigningUtils.buildCannedPolicy(resourceUrl, request.expirationDate());
143-
byte[] signatureBytes = SigningUtils.signWithSha1Rsa(cannedPolicy.getBytes(UTF_8), request.privateKey());
144+
byte[] signatureBytes = signPolicy(cannedPolicy.getBytes(UTF_8), request.privateKey());
144145
String urlSafeSignature = SigningUtils.makeBytesUrlSafe(signatureBytes);
145146
URI uri = URI.create(resourceUrl);
146147
String protocol = uri.getScheme();
@@ -266,7 +267,7 @@ public SignedUrl getSignedUrlWithCustomPolicy(CustomSignerRequest request) {
266267
request.expirationDate(),
267268
request.ipRange());
268269

269-
byte[] signatureBytes = SigningUtils.signWithSha1Rsa(policy.getBytes(UTF_8), request.privateKey());
270+
byte[] signatureBytes = signPolicy(policy.getBytes(UTF_8), request.privateKey());
270271
String urlSafePolicy = SigningUtils.makeStringUrlSafe(policy);
271272
String urlSafeSignature = SigningUtils.makeBytesUrlSafe(signatureBytes);
272273
URI uri = URI.create(resourceUrl);
@@ -368,7 +369,7 @@ public CookiesForCannedPolicy getCookiesForCannedPolicy(Consumer<CannedSignerReq
368369
public CookiesForCannedPolicy getCookiesForCannedPolicy(CannedSignerRequest request) {
369370
try {
370371
String cannedPolicy = SigningUtils.buildCannedPolicy(request.resourceUrl(), request.expirationDate());
371-
byte[] signatureBytes = SigningUtils.signWithSha1Rsa(cannedPolicy.getBytes(UTF_8), request.privateKey());
372+
byte[] signatureBytes = signPolicy(cannedPolicy.getBytes(UTF_8), request.privateKey());
372373
String urlSafeSignature = SigningUtils.makeBytesUrlSafe(signatureBytes);
373374
String expiry = String.valueOf(request.expirationDate().getEpochSecond());
374375
return DefaultCookiesForCannedPolicy.builder()
@@ -469,7 +470,7 @@ public CookiesForCustomPolicy getCookiesForCustomPolicy(CustomSignerRequest requ
469470
try {
470471
String policy = SigningUtils.buildCustomPolicy(request.resourceUrl(), request.activeDate(), request.expirationDate(),
471472
request.ipRange());
472-
byte[] signatureBytes = SigningUtils.signWithSha1Rsa(policy.getBytes(UTF_8), request.privateKey());
473+
byte[] signatureBytes = signPolicy(policy.getBytes(UTF_8), request.privateKey());
473474
String urlSafePolicy = SigningUtils.makeStringUrlSafe(policy);
474475
String urlSafeSignature = SigningUtils.makeBytesUrlSafe(signatureBytes);
475476
return DefaultCookiesForCustomPolicy.builder()
@@ -482,4 +483,20 @@ public CookiesForCustomPolicy getCookiesForCustomPolicy(CustomSignerRequest requ
482483
}
483484
}
484485

486+
private static byte[] signPolicy(byte[] policyToSign, PrivateKey privateKey) throws InvalidKeyException {
487+
// all CloudFront signed urls currently require the SHA1 and currently only support RSA and EC
488+
switch (privateKey.getAlgorithm()) {
489+
case "RSA":
490+
return SigningUtils.signWithSha1Rsa(policyToSign, privateKey);
491+
case "EC":
492+
case "ECDSA":
493+
return SigningUtils.signWithSha1ECDSA(policyToSign, privateKey);
494+
default:
495+
// do not attempt to use a generic Signer based on the privateKey algorithm:
496+
// future supported key types likely require different hash algorithms (eg, SHA256 or higher instead of SHA1)
497+
throw new IllegalArgumentException(
498+
"Unsupported key algorithm for CloudFront signed URL: " + privateKey.getAlgorithm());
499+
}
500+
}
501+
485502
}

services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/internal/auth/Pem.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.List;
2828
import java.util.regex.Pattern;
2929
import software.amazon.awssdk.annotations.SdkInternalApi;
30+
import software.amazon.awssdk.services.cloudfront.internal.utils.SigningUtils;
3031

3132
@SdkInternalApi
3233
public final class Pem {
@@ -51,16 +52,19 @@ public static PrivateKey readPrivateKey(InputStream is) throws InvalidKeySpecExc
5152
for (PemObject object : objects) {
5253
switch (object.getPemObjectType()) {
5354
case PRIVATE_KEY_PKCS1:
55+
// only supports RSA keys, so load it as RSA.
5456
return Rsa.privateKeyFromPkcs1(object.getDerBytes());
5557
case PRIVATE_KEY_PKCS8:
56-
return Rsa.privateKeyFromPkcs8(object.getDerBytes());
58+
return SigningUtils.privateKeyFromPkcs8(object.getDerBytes());
5759
default:
5860
break;
5961
}
6062
}
6163
throw new IllegalArgumentException("Found no private key");
6264
}
6365

66+
67+
6468
/**
6569
* Returns the first public key that is found from the input stream of a PEM
6670
* file.

services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/internal/utils/SigningUtils.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,20 @@
2121
import java.nio.file.Files;
2222
import java.nio.file.Path;
2323
import java.security.InvalidKeyException;
24+
import java.security.KeyFactory;
2425
import java.security.NoSuchAlgorithmException;
2526
import java.security.PrivateKey;
2627
import java.security.SecureRandom;
2728
import java.security.Signature;
2829
import java.security.SignatureException;
30+
import java.security.spec.EncodedKeySpec;
31+
import java.security.spec.InvalidKeySpecException;
32+
import java.security.spec.PKCS8EncodedKeySpec;
2933
import java.time.Instant;
3034
import java.util.Base64;
3135
import software.amazon.awssdk.annotations.SdkInternalApi;
3236
import software.amazon.awssdk.core.exception.SdkClientException;
3337
import software.amazon.awssdk.services.cloudfront.internal.auth.Pem;
34-
import software.amazon.awssdk.services.cloudfront.internal.auth.Rsa;
3538
import software.amazon.awssdk.utils.IoUtils;
3639
import software.amazon.awssdk.utils.StringUtils;
3740
import software.amazon.awssdk.utils.Validate;
@@ -139,6 +142,22 @@ public static byte[] signWithSha1Rsa(byte[] dataToSign, PrivateKey privateKey) t
139142
}
140143
}
141144

145+
/**
146+
* Signs the data given with the private key given, using the SHA1withECDSA
147+
* algorithm provided by bouncy castle.
148+
*/
149+
public static byte[] signWithSha1ECDSA(byte[] dataToSign, PrivateKey privateKey) throws InvalidKeyException {
150+
try {
151+
Signature signature = Signature.getInstance("SHA1withECDSA");
152+
SecureRandom random = new SecureRandom();
153+
signature.initSign(privateKey, random);
154+
signature.update(dataToSign);
155+
return signature.sign();
156+
} catch (NoSuchAlgorithmException | SignatureException e) {
157+
throw new IllegalStateException(e);
158+
}
159+
}
160+
142161
/**
143162
* Generate a policy document that describes custom access permissions to
144163
* apply via a private distribution's signed URL.
@@ -198,10 +217,35 @@ public static PrivateKey loadPrivateKey(Path keyFile) throws Exception {
198217
}
199218
if (StringUtils.lowerCase(keyFile.toString()).endsWith(".der")) {
200219
try (InputStream is = Files.newInputStream(keyFile)) {
201-
return Rsa.privateKeyFromPkcs8(IoUtils.toByteArray(is));
220+
return privateKeyFromPkcs8(IoUtils.toByteArray(is));
202221
}
203222
}
204223
throw SdkClientException.create("Unsupported file type for private key");
205224
}
206225

226+
/**
227+
* Attempt to load a private key from PKCS8 DER
228+
*/
229+
public static PrivateKey privateKeyFromPkcs8(byte[] derBytes) {
230+
EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(derBytes);
231+
try {
232+
return tryKeyLoadFromSpec(privateKeySpec);
233+
} catch (NoSuchAlgorithmException e) {
234+
throw new IllegalArgumentException(e);
235+
} catch (InvalidKeySpecException e) {
236+
throw new IllegalArgumentException("Invalid private key, unable to load as either RSA or ECDSA", e);
237+
}
238+
}
239+
240+
/**
241+
* We don't have a way to determine which algorithm to use, so we try to load as RSA and EC
242+
*/
243+
private static PrivateKey tryKeyLoadFromSpec(EncodedKeySpec privateKeySpec)
244+
throws NoSuchAlgorithmException, InvalidKeySpecException {
245+
try {
246+
return KeyFactory.getInstance("RSA").generatePrivate(privateKeySpec);
247+
} catch (InvalidKeySpecException rsaFail) {
248+
return KeyFactory.getInstance("EC").generatePrivate(privateKeySpec);
249+
}
250+
}
207251
}

0 commit comments

Comments
 (0)