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
baul 2017-11-05 02:51:07 +08:00 zatwierdzone przez Andrew Gaul
rodzic 94bf8ca88e
commit ddfba4e7a4
4 zmienionych plików z 146 dodań i 38 usunięć

Wyświetl plik

@ -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");

Wyświetl plik

@ -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;

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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) {