1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package software.amazon.awssdk.services.cloudfront.internal.utils; 17 18 import static java.nio.charset.StandardCharsets.UTF_8; 19 20 import java.io.InputStream; 21 import java.nio.file.Files; 22 import java.nio.file.Path; 23 import java.security.InvalidKeyException; 24 import java.security.NoSuchAlgorithmException; 25 import java.security.PrivateKey; 26 import java.security.SecureRandom; 27 import java.security.Signature; 28 import java.security.SignatureException; 29 import java.time.Instant; 30 import java.util.Base64; 31 import software.amazon.awssdk.annotations.SdkInternalApi; 32 import software.amazon.awssdk.core.exception.SdkClientException; 33 import software.amazon.awssdk.services.cloudfront.internal.auth.Pem; 34 import software.amazon.awssdk.services.cloudfront.internal.auth.Rsa; 35 import software.amazon.awssdk.utils.IoUtils; 36 import software.amazon.awssdk.utils.StringUtils; 37 38 @SdkInternalApi 39 public final class SigningUtils { 40 SigningUtils()41 private SigningUtils() { 42 } 43 44 /** 45 * Returns a "canned" policy for the given parameters. 46 * For more information, see 47 * <a href = 48 * "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-canned-policy.html" 49 * >Creating a signed URL using a canned policy</a> 50 * or 51 * <a href= 52 * "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-canned-policy.html" 53 * >Setting signed cookies using a canned policy</a>. 54 */ buildCannedPolicy(String resourceUrl, Instant expirationDate)55 public static String buildCannedPolicy(String resourceUrl, Instant expirationDate) { 56 return "{\"Statement\":[{\"Resource\":\"" 57 + resourceUrl 58 + "\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":" 59 + expirationDate.getEpochSecond() 60 + "}}}]}"; 61 } 62 63 /** 64 * Returns a custom policy for the given parameters. 65 * For more information, see <a href= 66 * "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html" 67 * >Creating a signed URL using a custom policy</a> 68 * or 69 * <a href= 70 * "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html" 71 * >Setting signed cookies using a custom policy</a>. 72 */ buildCustomPolicy(String resourceUrl, Instant activeDate, Instant expirationDate, String ipAddress)73 public static String buildCustomPolicy(String resourceUrl, Instant activeDate, Instant expirationDate, 74 String ipAddress) { 75 return "{\"Statement\": [{" 76 + "\"Resource\":\"" 77 + resourceUrl 78 + "\"" 79 + ",\"Condition\":{" 80 + "\"DateLessThan\":{\"AWS:EpochTime\":" 81 + expirationDate.getEpochSecond() 82 + "}" 83 + (ipAddress == null 84 ? "" 85 : ",\"IpAddress\":{\"AWS:SourceIp\":\"" + ipAddress + "\"}" 86 ) 87 + (activeDate == null 88 ? "" 89 : ",\"DateGreaterThan\":{\"AWS:EpochTime\":" + activeDate.getEpochSecond() + "}" 90 ) 91 + "}}]}"; 92 } 93 94 /** 95 * Converts the given data to be safe for use in signed URLs for a private 96 * distribution by using specialized Base64 encoding. 97 */ makeBytesUrlSafe(byte[] bytes)98 public static String makeBytesUrlSafe(byte[] bytes) { 99 byte[] encoded = Base64.getEncoder().encode(bytes); 100 for (int i = 0; i < encoded.length; i++) { 101 switch (encoded[i]) { 102 case '+': 103 encoded[i] = '-'; 104 continue; 105 case '=': 106 encoded[i] = '_'; 107 continue; 108 case '/': 109 encoded[i] = '~'; 110 continue; 111 default: 112 } 113 } 114 return new String(encoded, UTF_8); 115 } 116 117 /** 118 * Converts the given string to be safe for use in signed URLs for a private 119 * distribution. 120 */ makeStringUrlSafe(String str)121 public static String makeStringUrlSafe(String str) { 122 return makeBytesUrlSafe(str.getBytes(UTF_8)); 123 } 124 125 /** 126 * Signs the data given with the private key given, using the SHA1withRSA 127 * algorithm provided by bouncy castle. 128 */ signWithSha1Rsa(byte[] dataToSign, PrivateKey privateKey)129 public static byte[] signWithSha1Rsa(byte[] dataToSign, PrivateKey privateKey) throws InvalidKeyException { 130 try { 131 Signature signature = Signature.getInstance("SHA1withRSA"); 132 SecureRandom random = new SecureRandom(); 133 signature.initSign(privateKey, random); 134 signature.update(dataToSign); 135 return signature.sign(); 136 } catch (NoSuchAlgorithmException | SignatureException e) { 137 throw new IllegalStateException(e); 138 } 139 } 140 141 /** 142 * Generate a policy document that describes custom access permissions to 143 * apply via a private distribution's signed URL. 144 * 145 * @param resourceUrl 146 * The HTTP/S resource path that restricts which distribution and 147 * S3 objects will be accessible in a signed URL, i.e., 148 * <tt>"https://" + distributionName + "/" + objectKey</tt> (may 149 * also include URL parameters). The '*' and '?' characters can 150 * be used as a wildcards to allow multi-character or single-character 151 * matches respectively: 152 * <ul> 153 * <li><tt>*</tt> : All distributions/objects will be accessible</li> 154 * <li><tt>a1b2c3d4e5f6g7.cloudfront.net/*</tt> : All objects 155 * within the distribution a1b2c3d4e5f6g7 will be accessible</li> 156 * <li><tt>a1b2c3d4e5f6g7.cloudfront.net/path/to/object.txt</tt> 157 * : Only the S3 object named <tt>path/to/object.txt</tt> in the 158 * distribution a1b2c3d4e5f6g7 will be accessible.</li> 159 * </ul> 160 * @param activeDate 161 * An optional UTC time and date when the signed URL will become 162 * active. If null, the signed URL will be active as soon as it 163 * is created. 164 * @param expirationDate 165 * The UTC time and date when the signed URL will expire. REQUIRED. 166 * @param limitToIpAddressCidr 167 * An optional range of client IP addresses that will be allowed 168 * to access the distribution, specified as an IPv4 CIDR range 169 * (IPv6 format is not supported). If null, the CIDR will be omitted 170 * and any client will be permitted. 171 * @return A policy document describing the access permission to apply when 172 * generating a signed URL. 173 */ buildCustomPolicyForSignedUrl(String resourceUrl, Instant activeDate, Instant expirationDate, String limitToIpAddressCidr)174 public static String buildCustomPolicyForSignedUrl(String resourceUrl, 175 Instant activeDate, 176 Instant expirationDate, 177 String limitToIpAddressCidr) { 178 if (expirationDate == null) { 179 throw SdkClientException.create("Expiration date must be provided to sign CloudFront URLs"); 180 } 181 if (resourceUrl == null) { 182 resourceUrl = "*"; 183 } 184 return buildCustomPolicy(resourceUrl, activeDate, expirationDate, limitToIpAddressCidr); 185 } 186 187 /** 188 * Creates a private key from the file given, either in pem or der format. 189 * Other formats will cause an exception to be thrown. 190 */ loadPrivateKey(Path keyFile)191 public static PrivateKey loadPrivateKey(Path keyFile) throws Exception { 192 if (StringUtils.lowerCase(keyFile.toString()).endsWith(".pem")) { 193 try (InputStream is = Files.newInputStream(keyFile)) { 194 return Pem.readPrivateKey(is); 195 } 196 } 197 if (StringUtils.lowerCase(keyFile.toString()).endsWith(".der")) { 198 try (InputStream is = Files.newInputStream(keyFile)) { 199 return Rsa.privateKeyFromPkcs8(IoUtils.toByteArray(is)); 200 } 201 } 202 throw SdkClientException.create("Unsupported file type for private key"); 203 } 204 205 } 206