/************************************************************************* * Copyright 2009-2015 Eucalyptus Systems, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. ************************************************************************/ package com.eucalyptus.objectstorage.pipeline.auth; import com.eucalyptus.auth.euare.DelegatingUserPrincipal; import com.eucalyptus.auth.principal.PolicyVersion; import com.eucalyptus.auth.principal.PolicyVersions; import com.eucalyptus.auth.principal.Principals; import com.eucalyptus.auth.principal.UserPrincipal; import com.eucalyptus.context.Context; import com.eucalyptus.context.Contexts; import com.eucalyptus.context.NoSuchContextException; import com.eucalyptus.crypto.util.SecurityHeader; import com.eucalyptus.crypto.util.SecurityParameter; import com.eucalyptus.crypto.util.Timestamps; import com.eucalyptus.crypto.util.Timestamps.Type; import com.eucalyptus.http.MappingHttpRequest; import com.eucalyptus.objectstorage.exceptions.s3.*; import com.eucalyptus.objectstorage.pipeline.auth.S3V4Authentication.V4AuthComponent; import com.eucalyptus.objectstorage.pipeline.handlers.AwsChunkStream.AwsChunk; import com.eucalyptus.objectstorage.util.ObjectStorageProperties; import com.eucalyptus.ws.util.HmacUtils.SignatureCredential; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import javaslang.control.Try.CheckedFunction; import org.apache.log4j.Logger; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.joda.time.DateTime; import javax.annotation.Nonnull; import javax.security.auth.Subject; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.*; import static com.eucalyptus.objectstorage.pipeline.auth.S3V2Authentication.AWS_V2_AUTH_TYPE; /** * REST and query string based V2 and V4 authentication for S3. * * @see <a href="http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html">AWS S3 Sigv4 docs</a> * @see <a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html">AWS S3 Sigv2 docs</a> */ public final class S3Authentication { private static final Logger LOG = Logger.getLogger(S3Authentication.class); private S3Authentication() { } public enum S3Authenticator { V2_HEADER { public void authenticate(MappingHttpRequest request, Map<String, String> lowercaseParams) throws S3Exception { String authHeader = request.getHeader(HttpHeaders.Names.AUTHORIZATION).replaceFirst(AWS_V2_AUTH_TYPE, "").trim(); String[] signatureElements = authHeader.split(":"); String accessKeyId = signatureElements[0]; String signature = signatureElements[1]; String dateStr = getDateFromHeaders(request); Date date = parseDate(dateStr); assertDateNotSkewed(date); if (request.getHeader(SecurityHeader.X_Amz_Date.header()) != null) dateStr = ""; String canonicalizedAmzHeaders = S3V2Authentication.buildCanonicalHeaders(request, false); String securityToken = request.getHeader(SecurityParameter.X_Amz_Security_Token.parameter()); S3V2Authentication.login(request, date, dateStr, canonicalizedAmzHeaders, accessKeyId, signature, securityToken); } }, V2_PARAMS { public void authenticate(MappingHttpRequest request, Map<String, String> lowercaseParams) throws S3Exception { String expiresStr = S3V2Authentication.getAndValidateExpiresFromParameters(lowercaseParams); String canonicalizedAmzHeaders = S3V2Authentication.buildCanonicalHeaders(request, true); String accessKeyId = request.getParameters().remove(SecurityParameter.AWSAccessKeyId.toString()); String signature = getSignatureFromParameters(lowercaseParams); String securityToken = lowercaseParams.get(SecurityParameter.X_Amz_Security_Token.parameter().toLowerCase()); S3V2Authentication.login(request, null, expiresStr, canonicalizedAmzHeaders, accessKeyId, signature, securityToken); } }, V4_HEADER { public void authenticate(MappingHttpRequest request, Map<String, String> lowercaseParams) throws S3Exception { Map<V4AuthComponent, String> authComponents = S3V4Authentication.getV4AuthComponents(request.getHeader(HttpHeaders.Names .AUTHORIZATION)); String dateStr = getDateFromHeaders(request); Date date = parseDate(dateStr); assertDateNotSkewed(date); SignatureCredential credential = S3V4Authentication.getAndVerifyCredential(date, authComponents.get(V4AuthComponent.Credential)); String signedHeaders = authComponents.get(V4AuthComponent.SignedHeaders); String signature = authComponents.get(V4AuthComponent.Signature); String securityToken = request.getHeader(SecurityParameter.X_Amz_Security_Token.parameter()); String payloadHash = S3V4Authentication.getUnverifiedPayloadHash(request); Long decodedContentLength = S3V4Authentication.getAndVerifyDecodedContentLength(request, payloadHash); S3V4Authentication.login(request, date, credential, signedHeaders, signature, securityToken, payloadHash); // Convert content length from V4 to V2 if (decodedContentLength != null) HttpHeaders.setContentLength(request, decodedContentLength); } }, V4_PARAMS { public void authenticate(MappingHttpRequest request, Map<String, String> lowercaseParams) throws S3Exception { String dateStr = S3V4Authentication.getDateFromParams(lowercaseParams); Date date = parseDate(dateStr); S3V4Authentication.validateExpiresFromParams(lowercaseParams, date); String credentialStr = lowercaseParams.get(SecurityParameter.X_Amz_Credential.parameter().toLowerCase()); SignatureCredential credential = S3V4Authentication.getAndVerifyCredential(date, credentialStr); String signedHeaders = lowercaseParams.get(SecurityParameter.X_Amz_SignedHeaders.parameter().toLowerCase()); String signature = lowercaseParams.get(SecurityParameter.X_Amz_Signature.parameter().toLowerCase()); String securityToken = lowercaseParams.get(SecurityParameter.X_Amz_Security_Token.parameter().toLowerCase()); S3V4Authentication.login(request, date, credential, signedHeaders, signature, securityToken, S3V4Authentication.UNSIGNED_PAYLOAD); } }, ANONYMOUS { public void authenticate(MappingHttpRequest request, Map<String, String> lowercaseParams) throws S3Exception { try { final Context context = Contexts.lookup( request.getCorrelationId( ) ); final Subject subject = new Subject( ); final UserPrincipal principal = new DelegatingUserPrincipal( Principals.nobodyUser( ) ) { @Nonnull @Override public List<PolicyVersion> getPrincipalPolicies( ) { return ImmutableList.of( PolicyVersions.getAdministratorPolicy( ) ); } }; subject.getPrincipals( ).add( principal ); context.setUser( principal ); context.setSubject( subject ); } catch (NoSuchContextException e) { LOG.error(e, e); throw new AccessDeniedException(); } } }; /** * Authenticates the request. * * @throws AccessDeniedException if the auth info is invalid * @throws SignatureDoesNotMatchException if the signature is invalid * @throws InvalidAccessKeyIdException if the contextual AWS key is is invalid * @throws InternalErrorException if something unexpected occurs */ public abstract void authenticate(MappingHttpRequest request, Map<String, String> lowercaseParams) throws S3Exception; /** * Returns the S3Authenticator for the request. * * @throws MissingSecurityHeaderException if an Authorization header is present, but is invalid */ public static S3Authenticator of(MappingHttpRequest request, Map<String, String> lowercaseParams) throws MissingSecurityHeaderException { // Handle headers request String authHeader = request.getHeader(SecurityParameter.Authorization.toString()); if (!Strings.isNullOrEmpty(authHeader)) { if (authHeader.startsWith(S3V4Authentication.AWS_V4_AUTH_TYPE)) return S3Authenticator.V4_HEADER; else if (authHeader.startsWith(S3V2Authentication.AWS_V2_AUTH_TYPE)) return S3Authenticator.V2_HEADER; else throw new MissingSecurityHeaderException(null, "Malformed or unexpected format for Authentication header"); } // Handle param request if (lowercaseParams.containsKey(SecurityParameter.X_Amz_Algorithm.parameter().toLowerCase()) || lowercaseParams.containsKey (SecurityParameter.X_Amz_Date.parameter().toLowerCase())) return S3Authenticator.V4_PARAMS; else if (request.getParameters().containsKey(SecurityParameter.AWSAccessKeyId.parameter())) return S3Authenticator.V2_PARAMS; // Handle anonymous request return S3Authenticator.ANONYMOUS; } } public static void authenticateV4Streaming(MappingHttpRequest request, List<AwsChunk> chunks) throws S3Exception { Map<V4AuthComponent, String> authComponents = S3V4Authentication.getV4AuthComponents(request.getHeader(HttpHeaders.Names .AUTHORIZATION)); String dateStr = getDateFromHeaders(request); Date date = parseDate(dateStr); SignatureCredential credential = S3V4Authentication.getAndVerifyCredential(date, authComponents.get(V4AuthComponent.Credential)); String signedHeaders = authComponents.get(V4AuthComponent.SignedHeaders); String securityToken = request.getHeader(SecurityParameter.X_Amz_Security_Token.parameter()); String seedSignature = authComponents.get(V4AuthComponent.Signature); for (AwsChunk chunk : chunks) { String previousSignature = chunk.previousSignature == null ? seedSignature : chunk.previousSignature; S3V4Authentication.loginChunk(request, date, credential, signedHeaders, chunk.chunkSignature, securityToken, previousSignature, chunk.getPayload()); } } private static String getDateFromHeaders(MappingHttpRequest request) throws AccessDeniedException { String result = request.getHeader(SecurityHeader.X_Amz_Date.header()); if (result == null) result = request.getHeader(SecurityHeader.Date.header()); if (result == null) throw new AccessDeniedException(null, "X-Amz-Date header must be specified."); return result; } static Date parseDate(String dateStr) throws AccessDeniedException { Date date = null; try { date = Timestamps.parseTimestamp(dateStr, Type.ISO_8601); } catch (Exception ignore) { } try { if (date == null) date = Timestamps.parseTimestamp(dateStr, Type.RFC_2616); } catch (Exception ex) { LOG.error("Cannot parse date: " + dateStr); throw new AccessDeniedException(null, "Unable to parse date."); } return date; } static void assertDateNotSkewed(final Date date) throws RequestTimeTooSkewedException { DateTime currentTime = DateTime.now(); DateTime dt = new DateTime(date); if (dt.isBefore(currentTime.minusMillis((int) ObjectStorageProperties.EXPIRATION_LIMIT))) throw new RequestTimeTooSkewedException(); if (dt.isAfter(currentTime.plusMillis((int) ObjectStorageProperties.EXPIRATION_LIMIT))) throw new RequestTimeTooSkewedException(); } private static String getSignatureFromParameters(Map<String, String> parameters) throws InvalidSecurityException { String signature = parameters.remove(SecurityParameter.Signature.toString().toLowerCase()); if (signature == null) throw new InvalidSecurityException("No signature found"); return signature; } }