kopia lustrzana https://github.com/gaul/s3proxy
v2/v4 auth improve
* 15 minitues timeskew * Add x-amz-date header or query parameter check * Change the timeskew logic to first get client req auth type * When v2,x-amz-date header format is rfc2616,when v4,is iso8601 * If have both x-amz-date header and date header in v2 auth,date value in stringtosign is x-amz-date header value,CanonicalizedAmzHeaders have no x-amz-header. Ref Delete example in site: http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html * Fix v2 query auth: If expires is nil ,does not mean that the auth type is not query auth type. Ref http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth It says that 'Additionally, you can limit a pre-signed request by specifying an expiration time.'pull/252/head
rodzic
94bf8ca88e
commit
ddfba4e7a4
|
@ -83,7 +83,8 @@ final class AwsSignature {
|
|||
* http://docs.aws.amazon.com/general/latest/gr/signature-version-2.html
|
||||
*/
|
||||
static String createAuthorizationSignature(
|
||||
HttpServletRequest request, String uri, String credential) {
|
||||
HttpServletRequest request, String uri, String credential,
|
||||
boolean queryAuth, boolean bothDateHeader) {
|
||||
// sort Amazon headers
|
||||
SortedSetMultimap<String, String> canonicalizedHeaders =
|
||||
TreeMultimap.create();
|
||||
|
@ -91,7 +92,8 @@ final class AwsSignature {
|
|||
Collection<String> headerValues = Collections.list(
|
||||
request.getHeaders(headerName));
|
||||
headerName = headerName.toLowerCase();
|
||||
if (!headerName.startsWith("x-amz-")) {
|
||||
if (!headerName.startsWith("x-amz-") || (bothDateHeader &&
|
||||
headerName.equalsIgnoreCase("x-amz-date"))) {
|
||||
continue;
|
||||
}
|
||||
if (headerValues.isEmpty()) {
|
||||
|
@ -103,7 +105,7 @@ final class AwsSignature {
|
|||
}
|
||||
}
|
||||
|
||||
// build string to sign
|
||||
// Build string to sign
|
||||
StringBuilder builder = new StringBuilder()
|
||||
.append(request.getMethod())
|
||||
.append('\n')
|
||||
|
@ -114,11 +116,28 @@ final class AwsSignature {
|
|||
HttpHeaders.CONTENT_TYPE)))
|
||||
.append('\n');
|
||||
String expires = request.getParameter("Expires");
|
||||
if (expires != null) {
|
||||
builder.append(expires);
|
||||
} else if (!canonicalizedHeaders.containsKey("x-amz-date")) {
|
||||
builder.append(request.getHeader(HttpHeaders.DATE));
|
||||
if (queryAuth) {
|
||||
// If expires is not nil, then it is query string sign
|
||||
// If expires is nil,maybe alse query string sign
|
||||
// So should check other accessid para ,presign to judge.
|
||||
// not the expires
|
||||
builder.append(Strings.nullToEmpty(expires));
|
||||
} else {
|
||||
if (!bothDateHeader) {
|
||||
if (canonicalizedHeaders.containsKey("x-amz-date")) {
|
||||
builder.append("");
|
||||
} else {
|
||||
builder.append(request.getHeader(HttpHeaders.DATE));
|
||||
}
|
||||
} else {
|
||||
if (!canonicalizedHeaders.containsKey("x-amz-date")) {
|
||||
builder.append(request.getHeader("x-amz-date"));
|
||||
} else {
|
||||
// panic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.append('\n');
|
||||
for (Map.Entry<String, String> entry : canonicalizedHeaders.entries()) {
|
||||
builder.append(entry.getKey()).append(':')
|
||||
|
@ -145,7 +164,7 @@ final class AwsSignature {
|
|||
String stringToSign = builder.toString();
|
||||
logger.trace("stringToSign: {}", stringToSign);
|
||||
|
||||
// sign string
|
||||
// Sign string
|
||||
Mac mac;
|
||||
try {
|
||||
mac = Mac.getInstance("HmacSHA1");
|
||||
|
|
|
@ -79,7 +79,10 @@ enum S3ErrorCode {
|
|||
"At least one of the preconditions you specified did not hold."),
|
||||
REQUEST_TIME_TOO_SKEWED(HttpServletResponse.SC_FORBIDDEN, "Forbidden"),
|
||||
REQUEST_TIMEOUT(HttpServletResponse.SC_BAD_REQUEST, "Bad Request"),
|
||||
SIGNATURE_DOES_NOT_MATCH(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
|
||||
SIGNATURE_DOES_NOT_MATCH(HttpServletResponse.SC_FORBIDDEN, "Forbidden"),
|
||||
X_AMZ_CONTENT_S_H_A_256_MISMATCH(HttpServletResponse.SC_BAD_REQUEST,
|
||||
"The provided 'x-amz-content-sha256' header does not match what" +
|
||||
" was computed.");
|
||||
|
||||
private final String errorCode;
|
||||
private final int httpStatusCode;
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package org.gaul.s3proxy;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class S3ProxyConstants {
|
||||
public static final String PROPERTY_ENDPOINT =
|
||||
"s3proxy.endpoint";
|
||||
|
@ -73,6 +75,8 @@ public final class S3ProxyConstants {
|
|||
public static final String PROPERTY_READ_ONLY_BLOBSTORE =
|
||||
"s3proxy.read-only-blobstore";
|
||||
|
||||
public static final long PROPERTY_TIMESKEW = TimeUnit.MINUTES.toSeconds(15);
|
||||
|
||||
static final String PROPERTY_ALT_JCLOUDS_PREFIX = "alt.";
|
||||
|
||||
private S3ProxyConstants() {
|
||||
|
|
|
@ -29,6 +29,7 @@ import java.io.Writer;
|
|||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
@ -296,15 +297,26 @@ public class S3ProxyHandler {
|
|||
for (String headerName : Collections.list(request.getHeaderNames())) {
|
||||
for (String headerValue : Collections.list(request.getHeaders(
|
||||
headerName))) {
|
||||
logger.trace("header: {}: {}", headerName,
|
||||
logger.debug("header: {}: {}", headerName,
|
||||
Strings.nullToEmpty(headerValue));
|
||||
}
|
||||
if (headerName.equalsIgnoreCase(HttpHeaders.DATE)) {
|
||||
hasDateHeader = true;
|
||||
} else if (headerName.equalsIgnoreCase("x-amz-date")) {
|
||||
hasXAmzDateHeader = true;
|
||||
logger.debug("have the x-amz-date heaer {}", headerName);
|
||||
// why x-amz-date name exist,but value is null?
|
||||
if ("".equals(request.getHeader("x-amz-date")) ||
|
||||
request.getHeader("x-amz-date") == null) {
|
||||
logger.debug("have empty x-amz-date");
|
||||
} else {
|
||||
hasXAmzDateHeader = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean haveBothDateHeader = false;
|
||||
if (hasDateHeader && hasXAmzDateHeader) {
|
||||
haveBothDateHeader = true;
|
||||
}
|
||||
|
||||
// when access information is not provided in request header,
|
||||
// treat it as anonymous, return all public accessible information
|
||||
|
@ -312,13 +324,15 @@ public class S3ProxyHandler {
|
|||
(method.equals("GET") || method.equals("HEAD") ||
|
||||
method.equals("POST")) &&
|
||||
request.getHeader(HttpHeaders.AUTHORIZATION) == null &&
|
||||
request.getParameter("X-Amz-Algorithm") == null &&
|
||||
request.getParameter("AWSAccessKeyId") == null &&
|
||||
// v2 or /v4
|
||||
request.getParameter("X-Amz-Algorithm") == null && // v4 query
|
||||
request.getParameter("AWSAccessKeyId") == null && // v2 query
|
||||
defaultBlobStore != null) {
|
||||
doHandleAnonymous(request, response, is, uri, defaultBlobStore);
|
||||
return;
|
||||
}
|
||||
|
||||
// should according the AWSAccessKeyId= Signature or auth header nil
|
||||
if (!anonymousIdentity && !hasDateHeader && !hasXAmzDateHeader &&
|
||||
request.getParameter("X-Amz-Date") == null &&
|
||||
request.getParameter("Expires") == null) {
|
||||
|
@ -327,23 +341,6 @@ public class S3ProxyHandler {
|
|||
" x-amz-date header");
|
||||
}
|
||||
|
||||
// TODO: apply sanity checks to X-Amz-Date
|
||||
if (hasDateHeader) {
|
||||
long date;
|
||||
try {
|
||||
date = request.getDateHeader(HttpHeaders.DATE);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
throw new S3Exception(S3ErrorCode.ACCESS_DENIED, iae);
|
||||
}
|
||||
if (date < 0) {
|
||||
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (now + TimeUnit.DAYS.toMillis(1) < date ||
|
||||
now - TimeUnit.DAYS.toMillis(1) > date) {
|
||||
throw new S3Exception(S3ErrorCode.REQUEST_TIME_TOO_SKEWED);
|
||||
}
|
||||
}
|
||||
|
||||
BlobStore blobStore;
|
||||
String requestIdentity = null;
|
||||
|
@ -355,7 +352,7 @@ public class S3ProxyHandler {
|
|||
if (!anonymousIdentity) {
|
||||
if (headerAuthorization == null) {
|
||||
String algorithm = request.getParameter("X-Amz-Algorithm");
|
||||
if (algorithm == null) {
|
||||
if (algorithm == null) { //v2 query
|
||||
String identity = request.getParameter("AWSAccessKeyId");
|
||||
String signature = request.getParameter("Signature");
|
||||
if (identity == null || signature == null) {
|
||||
|
@ -363,7 +360,7 @@ public class S3ProxyHandler {
|
|||
}
|
||||
headerAuthorization = "AWS " + identity + ":" + signature;
|
||||
presignedUrl = true;
|
||||
} else if (algorithm.equals("AWS4-HMAC-SHA256")) {
|
||||
} else if (algorithm.equals("AWS4-HMAC-SHA256")) { //v4 query
|
||||
String credential = request.getParameter(
|
||||
"X-Amz-Credential");
|
||||
String signedHeaders = request.getParameter(
|
||||
|
@ -384,12 +381,67 @@ public class S3ProxyHandler {
|
|||
|
||||
try {
|
||||
authHeader = new S3AuthorizationHeader(headerAuthorization);
|
||||
//whether v2 or v4 (normal header and query)
|
||||
} catch (IllegalArgumentException iae) {
|
||||
throw new S3Exception(S3ErrorCode.INVALID_ARGUMENT, iae);
|
||||
}
|
||||
requestIdentity = authHeader.identity;
|
||||
}
|
||||
|
||||
long dateSkew = 0; //date for timeskew check
|
||||
|
||||
//v2 GET /s3proxy-1080747708/foo?AWSAccessKeyId=local-identity&Expires=
|
||||
//1510322602&Signature=UTyfHY1b1Wgr5BFEn9dpPlWdtFE%3D)
|
||||
//have no date
|
||||
|
||||
boolean haveDate = true;
|
||||
|
||||
AuthenticationType finalAuthType = null;
|
||||
if (authHeader.authenticationType == AuthenticationType.AWS_V2 &&
|
||||
(authenticationType == AuthenticationType.AWS_V2 ||
|
||||
authenticationType == AuthenticationType.AWS_V2_OR_V4)) {
|
||||
finalAuthType = AuthenticationType.AWS_V2;
|
||||
} else if (authHeader.authenticationType == AuthenticationType.AWS_V4 &&
|
||||
(authenticationType == AuthenticationType.AWS_V4 ||
|
||||
authenticationType == AuthenticationType.AWS_V2_OR_V4)) {
|
||||
finalAuthType = AuthenticationType.AWS_V4;
|
||||
} else if (authenticationType != AuthenticationType.NONE) {
|
||||
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
|
||||
}
|
||||
|
||||
if (hasXAmzDateHeader) { //format diff between v2 and v4
|
||||
if (finalAuthType == AuthenticationType.AWS_V2) {
|
||||
dateSkew = request.getDateHeader("x-amz-date");
|
||||
dateSkew /= 1000;
|
||||
//case sensetive?
|
||||
} else if (finalAuthType == AuthenticationType.AWS_V4) {
|
||||
logger.debug("into process v4 {}",
|
||||
request.getHeader("x-amz-date"));
|
||||
|
||||
dateSkew = parseIso8601(request.getHeader("x-amz-date"));
|
||||
}
|
||||
} else if (request.getParameter("X-Amz-Date") != null) { // v4 query
|
||||
String dateString = request.getParameter("X-Amz-Date");
|
||||
dateSkew = parseIso8601(dateString);
|
||||
} else if (hasDateHeader) {
|
||||
try {
|
||||
dateSkew = request.getDateHeader(HttpHeaders.DATE);
|
||||
logger.debug("dateheader {}", dateSkew);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
throw new S3Exception(S3ErrorCode.ACCESS_DENIED, iae);
|
||||
}
|
||||
dateSkew /= 1000;
|
||||
logger.debug("dateheader {}", dateSkew);
|
||||
|
||||
} else {
|
||||
haveDate = false;
|
||||
}
|
||||
logger.debug("dateSkew {}", dateSkew);
|
||||
if (haveDate) {
|
||||
isTimeSkewed(dateSkew);
|
||||
}
|
||||
|
||||
|
||||
String[] path = uri.split("/", 3);
|
||||
for (int i = 0; i < path.length; i++) {
|
||||
path[i] = URLDecoder.decode(path[i], "UTF-8");
|
||||
|
@ -416,7 +468,7 @@ public class S3ProxyHandler {
|
|||
blobStore = provider.getValue();
|
||||
|
||||
String expiresString = request.getParameter("Expires");
|
||||
if (expiresString != null) {
|
||||
if (expiresString != null) { // v2 query
|
||||
long expires = Long.parseLong(expiresString);
|
||||
long nowSeconds = System.currentTimeMillis() / 1000;
|
||||
if (nowSeconds >= expires) {
|
||||
|
@ -425,8 +477,9 @@ public class S3ProxyHandler {
|
|||
}
|
||||
|
||||
String dateString = request.getParameter("X-Amz-Date");
|
||||
//from para v4 query
|
||||
expiresString = request.getParameter("X-Amz-Expires");
|
||||
if (dateString != null && expiresString != null) {
|
||||
if (dateString != null && expiresString != null) { //v4 query
|
||||
long date = parseIso8601(dateString);
|
||||
long expires = Long.parseLong(expiresString);
|
||||
long nowSeconds = System.currentTimeMillis() / 1000;
|
||||
|
@ -435,7 +488,7 @@ public class S3ProxyHandler {
|
|||
"Request has expired");
|
||||
}
|
||||
}
|
||||
|
||||
// The aim ?
|
||||
switch (authHeader.authenticationType) {
|
||||
case AWS_V2:
|
||||
switch (authenticationType) {
|
||||
|
@ -468,9 +521,10 @@ public class S3ProxyHandler {
|
|||
|
||||
// When presigned url is generated, it doesn't consider service path
|
||||
String uriForSigning = presignedUrl ? uri : this.servicePath + uri;
|
||||
if (authHeader.hmacAlgorithm == null) {
|
||||
if (authHeader.hmacAlgorithm == null) { //v2
|
||||
expectedSignature = AwsSignature.createAuthorizationSignature(
|
||||
request, uriForSigning, credential);
|
||||
request, uriForSigning, credential, presignedUrl,
|
||||
haveBothDateHeader);
|
||||
} else {
|
||||
String contentSha256 = request.getHeader(
|
||||
"x-amz-content-sha256");
|
||||
|
@ -486,16 +540,31 @@ public class S3ProxyHandler {
|
|||
payload = new byte[0];
|
||||
} else {
|
||||
// buffer the entire stream to calculate digest
|
||||
// why input stream read contentlength of header?
|
||||
payload = ByteStreams.toByteArray(ByteStreams.limit(
|
||||
is, v4MaxNonChunkedRequestSize + 1));
|
||||
if (payload.length == v4MaxNonChunkedRequestSize + 1) {
|
||||
throw new S3Exception(
|
||||
S3ErrorCode.MAX_MESSAGE_LENGTH_EXCEEDED);
|
||||
}
|
||||
|
||||
// maybe we should check this when signing,
|
||||
// a lot of dup code with aws sign code.
|
||||
MessageDigest md = MessageDigest.getInstance(
|
||||
authHeader.hashAlgorithm);
|
||||
byte[] hash = md.digest(payload);
|
||||
if (!contentSha256.equals(
|
||||
BaseEncoding.base16().lowerCase()
|
||||
.encode(hash))) {
|
||||
throw new S3Exception(
|
||||
S3ErrorCode
|
||||
.X_AMZ_CONTENT_S_H_A_256_MISMATCH);
|
||||
}
|
||||
is = new ByteArrayInputStream(payload);
|
||||
}
|
||||
|
||||
expectedSignature = AwsSignature
|
||||
.createAuthorizationSignatureV4(
|
||||
.createAuthorizationSignatureV4(// v4 sign
|
||||
baseRequest, authHeader, payload, uriForSigning,
|
||||
credential);
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
|
@ -2507,6 +2576,7 @@ public class S3ProxyHandler {
|
|||
SimpleDateFormat formatter = new SimpleDateFormat(
|
||||
"yyyyMMdd'T'HHmmss'Z'");
|
||||
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
logger.debug("8601date {}", date);
|
||||
try {
|
||||
return formatter.parse(date).getTime() / 1000;
|
||||
} catch (ParseException pe) {
|
||||
|
@ -2514,6 +2584,18 @@ public class S3ProxyHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private static void isTimeSkewed(long date) throws S3Exception {
|
||||
if (date < 0) {
|
||||
throw new S3Exception(S3ErrorCode.ACCESS_DENIED);
|
||||
}
|
||||
long now = System.currentTimeMillis() / 1000;
|
||||
if (now + S3ProxyConstants.PROPERTY_TIMESKEW < date ||
|
||||
now - S3ProxyConstants.PROPERTY_TIMESKEW > date) {
|
||||
logger.debug("time skewed {} {}", date, now);
|
||||
throw new S3Exception(S3ErrorCode.REQUEST_TIME_TOO_SKEWED);
|
||||
}
|
||||
}
|
||||
|
||||
// cannot call BlobStore.getContext().utils().date().iso8601DateFormatsince
|
||||
// it has unwanted millisecond precision
|
||||
private static String formatDate(Date date) {
|
||||
|
|
Ładowanie…
Reference in New Issue