Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AmazonCloudFront-51e8b2d.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "Amazon CloudFront",
"contributor": "",
"description": "Add support for ECDSA signed URLs."
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -368,7 +369,7 @@ public CookiesForCannedPolicy getCookiesForCannedPolicy(Consumer<CannedSignerReq
public CookiesForCannedPolicy getCookiesForCannedPolicy(CannedSignerRequest request) {
try {
String cannedPolicy = SigningUtils.buildCannedPolicy(request.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);
String expiry = String.valueOf(request.expirationDate().getEpochSecond());
return DefaultCookiesForCannedPolicy.builder()
Expand Down Expand Up @@ -469,7 +470,7 @@ public CookiesForCustomPolicy getCookiesForCustomPolicy(CustomSignerRequest requ
try {
String policy = SigningUtils.buildCustomPolicy(request.resourceUrl(), request.activeDate(), 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);
return DefaultCookiesForCustomPolicy.builder()
Expand All @@ -482,4 +483,20 @@ public CookiesForCustomPolicy getCookiesForCustomPolicy(CustomSignerRequest requ
}
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.List;
import java.util.regex.Pattern;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.services.cloudfront.internal.utils.SigningUtils;

@SdkInternalApi
public final class Pem {
Expand All @@ -51,16 +52,19 @@ public static PrivateKey readPrivateKey(InputStream is) throws InvalidKeySpecExc
for (PemObject object : objects) {
switch (object.getPemObjectType()) {
case PRIVATE_KEY_PKCS1:
// only supports RSA keys, so load it as RSA.
return Rsa.privateKeyFromPkcs1(object.getDerBytes());
case PRIVATE_KEY_PKCS8:
return Rsa.privateKeyFromPkcs8(object.getDerBytes());
return SigningUtils.privateKeyFromPkcs8(object.getDerBytes());
default:
break;
}
}
throw new IllegalArgumentException("Found no private key");
}



/**
* Returns the first public key that is found from the input stream of a PEM
* file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,20 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.services.cloudfront.internal.auth.Pem;
import software.amazon.awssdk.services.cloudfront.internal.auth.Rsa;
import software.amazon.awssdk.utils.IoUtils;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.awssdk.utils.Validate;
Expand Down Expand Up @@ -139,6 +142,22 @@ public static byte[] signWithSha1Rsa(byte[] dataToSign, PrivateKey privateKey) t
}
}

/**
* Signs the data given with the private key given, using the SHA1withECDSA
* algorithm provided by bouncy castle.
*/
public static byte[] signWithSha1ECDSA(byte[] dataToSign, PrivateKey privateKey) throws InvalidKeyException {
try {
Signature signature = Signature.getInstance("SHA1withECDSA");
SecureRandom random = new SecureRandom();
signature.initSign(privateKey, random);
signature.update(dataToSign);
return signature.sign();
} catch (NoSuchAlgorithmException | SignatureException e) {
throw new IllegalStateException(e);
}
}

/**
* Generate a policy document that describes custom access permissions to
* apply via a private distribution's signed URL.
Expand Down Expand Up @@ -198,10 +217,35 @@ public static PrivateKey loadPrivateKey(Path keyFile) throws Exception {
}
if (StringUtils.lowerCase(keyFile.toString()).endsWith(".der")) {
try (InputStream is = Files.newInputStream(keyFile)) {
return Rsa.privateKeyFromPkcs8(IoUtils.toByteArray(is));
return privateKeyFromPkcs8(IoUtils.toByteArray(is));
}
}
throw SdkClientException.create("Unsupported file type for private key");
}

/**
* Attempt to load a private key from PKCS8 DER
*/
public static PrivateKey privateKeyFromPkcs8(byte[] derBytes) {
EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(derBytes);
try {
return tryKeyLoadFromSpec(privateKeySpec);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException("Invalid private key, unable to load as either RSA or ECDSA", e);
}
}

/**
* We don't have a way to determine which algorithm to use, so we try to load as RSA and EC
*/
private static PrivateKey tryKeyLoadFromSpec(EncodedKeySpec privateKeySpec)
throws NoSuchAlgorithmException, InvalidKeySpecException {
try {
return KeyFactory.getInstance("RSA").generatePrivate(privateKeySpec);
} catch (InvalidKeySpecException rsaFail) {
return KeyFactory.getInstance("EC").generatePrivate(privateKeySpec);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a debug statement mentioning falling back to EC?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good question - I'm a little on the fence about this code in general here. Ideally we would use the EncodedKeySpec#getAlgorithm method, but its not available until Java 9 :-(. So I'd say conceptually this is a little different than a fall back.

What I did do is add an additional catch(InvalidKeySpecException) in the privateKeyFromPkcs8 method that calls this that makes it more clear that the given private key file wasn't one of the supported formats.

What do you think? I'm happy to add logging, but I think it might be a little confusing to see "falling back to EC" when thats what you intended by passing an EC private key?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, makes sense. I see the new revision added improved error message, lgtm!

}
}
}
Loading
Loading