/************************************************************************* * Copyright 2016 Hewlett-Packard Enterprise, 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.login.AuthenticationException; import com.eucalyptus.auth.login.HmacLoginModuleSupport; import com.eucalyptus.auth.login.SecurityContext; import com.eucalyptus.crypto.Digest; import com.eucalyptus.crypto.util.SecurityHeader; import com.eucalyptus.crypto.util.SecurityParameter; import com.eucalyptus.crypto.util.Timestamps; import com.eucalyptus.http.MappingHttpRequest; import com.eucalyptus.objectstorage.exceptions.s3.*; import com.eucalyptus.objectstorage.util.ObjectStorageProperties; import com.eucalyptus.objectstorage.util.ObjectStorageProperties.SubResource; import com.eucalyptus.ws.StackConfiguration; import com.eucalyptus.ws.util.HmacUtils.SignatureCredential; import com.google.common.base.*; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.common.io.BaseEncoding; import org.apache.log4j.Logger; import org.joda.time.DateTime; import javax.security.auth.login.LoginException; import java.nio.ByteBuffer; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import static com.eucalyptus.auth.login.Hmacv4LoginModule.digestUTF8; /** * S3 V4 specific authentication utilities. */ public final class S3V4Authentication { private static final Logger LOG = Logger.getLogger(S3V4Authentication.class); private static final Splitter CSV_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings(); private static final Splitter NVP_SPLITTER = Splitter.on('=').limit(2).trimResults().omitEmptyStrings(); private static final String AWS_V4_TERMINATOR = "aws4_request"; public static final String AWS_V4_AUTH_TYPE = "AWS4-HMAC-SHA256"; public static final String AWS_CONTENT_SHA_HEADER = "x-amz-content-sha256"; public static final String AWS_EXPIRES_PARAM = "x-amz-expires"; public static final String AWS_DECODED_CONTENT_LEN = "x-amz-decoded-content-length"; public static final String STREAMING_PAYLOAD = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"; private static final String STREAMING_PAYLOAD_CHUNK_PREFIX = "AWS4-HMAC-SHA256-PAYLOAD"; public static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"; enum V4AuthComponent { Credential, SignedHeaders, Signature } private S3V4Authentication() { } static void login(MappingHttpRequest request, Date date, SignatureCredential credential, String signedHeaders, String signature, String securityToken, String payloadHash) throws S3Exception { String stringToSign = buildStringToSign(request, date, credential, signedHeaders, payloadHash); ObjectStorageWrappedCredentials creds = new ObjectStorageWrappedCredentials(request.getCorrelationId(), date==null?null:date.getTime( ), stringToSign, credential, signedHeaders, signature, securityToken, payloadHash); login(request, credential.getAccessKeyId(), creds); } static void loginChunk(MappingHttpRequest request, Date date, SignatureCredential credential, String signedHeaders, String signature, String securityToken, String previousSignature, ByteBuffer payload) throws S3Exception { String stringToSign = buildChunkStringToSign(date, credential, previousSignature, payload); ObjectStorageWrappedCredentials creds = new ObjectStorageWrappedCredentials(request.getCorrelationId(), date==null?null:date.getTime( ), stringToSign, credential, signedHeaders, signature, securityToken, null); login(request, credential.getAccessKeyId(), creds); } /** * Attempts a login and retries sign a signed string that does not contain a path if the initial attempt fails. */ static void login(MappingHttpRequest request, String accessKeyId, ObjectStorageWrappedCredentials creds) throws S3Exception { try { SecurityContext.getLoginContext(creds).login(); } catch (LoginException ex) { if (ex.getMessage().contains("The AWS Access Key Id you provided does not exist in our records")) throw new InvalidAccessKeyIdException(accessKeyId); LOG.debug("CorrelationId: " + request.getCorrelationId() + " Authentication failed due to signature mismatch:", ex); StringBuilder canonicalRequest = buildCanonicalRequest(request, creds.signedHeaders, creds.payloadHash); throw new SignatureDoesNotMatchException(accessKeyId, creds.getLoginData(), creds.signature, canonicalRequest.toString()); } catch (Exception e) { LOG.warn("CorrelationId: " + request.getCorrelationId() + " Unexpected failure trying to authenticate request", e); throw new InternalErrorException(e); } } /** * @see <a href="http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html">Creating an S3 v4 string to sign</a> */ private static String buildStringToSign(MappingHttpRequest request, Date date, SignatureCredential credential, String signedHeaders, String payloadHash) throws S3Exception { try { StringBuilder canonicalRequest = buildCanonicalRequest(request, signedHeaders, payloadHash); return buildStringToSign(date, credential, canonicalRequest); } catch (Exception e) { throw new InternalErrorException(e); } } private static String buildStringToSign(Date date, SignatureCredential credential, CharSequence canonicalRequest) throws Exception { StringBuilder sb = new StringBuilder(256); sb.append(SecurityHeader.Value.AWS4_HMAC_SHA256.value()).append('\n'); sb.append(Timestamps.formatShortIso8601Timestamp(date)).append('\n'); sb.append(credential.getCredentialScope()).append('\n'); sb.append(digestUTF8(canonicalRequest)); return sb.toString(); } private static String buildChunkStringToSign(Date date, SignatureCredential credential, String previousSignature, ByteBuffer payload) { StringBuilder sb = new StringBuilder(256); sb.append(STREAMING_PAYLOAD_CHUNK_PREFIX).append('\n'); sb.append(Timestamps.formatShortIso8601Timestamp(date)).append('\n'); sb.append(credential.getCredentialScope()).append('\n'); sb.append(previousSignature).append('\n'); sb.append(digestUTF8("")).append('\n'); sb.append(BaseEncoding.base16().lowerCase().encode(Digest.SHA256.digestBinary(payload))); return sb.toString(); } /** * @see <a href="http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html">Creating a canonical S3 v4 request</a> */ static StringBuilder buildCanonicalRequest(MappingHttpRequest request, String signedHeaders, String payloadHash) { StringBuilder sb = new StringBuilder(512); // Request method sb.append(request.getMethod().getName()); sb.append('\n'); // Resource path sb.append(buildCanonicalResourcePath(request.getServicePath())); sb.append('\n'); // Query parameters buildCanonicalQueryString(request.getParameters(), sb); sb.append('\n'); // Headers buildCanonicalHeaders(request, signedHeaders, sb); sb.append('\n'); // Signed headers sb.append(signedHeaders); sb.append('\n'); // Payload if (payloadHash != null) sb.append(payloadHash); return sb; } /** * Returns the canonicalized resource path for the service endpoint. */ static String buildCanonicalResourcePath(String path) { if (path == null || path.isEmpty()) return "/"; if (path.startsWith("/")) return path; else return "/".concat(path); } static void buildCanonicalQueryString(Map<String,String> parameters, StringBuilder sb) { boolean firstParam = true; for (String parameter : Ordering.natural().sortedCopy(parameters.keySet())) { // Ignore signature parameters if (SecurityParameter.X_Amz_Signature.parameter().equals(parameter)) continue; if (!firstParam) sb.append('&'); String value = parameters.get(parameter); sb.append(HmacLoginModuleSupport.urlencode(parameter)); sb.append('='); if (!Strings.isNullOrEmpty(value)) { Optional<SubResource> subResource = Enums.getIfPresent(SubResource.class, parameter); if (subResource.isPresent() && subResource.get().isObjectSubResource) sb.append(""); else sb.append(HmacLoginModuleSupport.urlencode(value)); } firstParam = false; } } static void buildCanonicalHeaders(MappingHttpRequest request, String signedHeaders, StringBuilder sb) { for (String header : signedHeaders.split(";")) { List<String> values = Lists.transform(request.getHeaders(header), text -> text != null ? text.trim() : null); sb.append(header.toLowerCase()); sb.append(':'); sb.append(Joiner.on(',').join(Ordering.<String>natural().sortedCopy(values))); sb.append('\n'); } } static String getUnverifiedPayloadHash( final MappingHttpRequest request) throws AccessDeniedException { final String contentShaHeader = request.getHeader(S3V4Authentication.AWS_CONTENT_SHA_HEADER); if ( !Strings.isNullOrEmpty(contentShaHeader) ) { if ( !STREAMING_PAYLOAD.equals(contentShaHeader) && !UNSIGNED_PAYLOAD.equals(contentShaHeader) ) { final byte[] binSha256 = BaseEncoding.base16( ).lowerCase( ).decode( contentShaHeader ); if ( binSha256.length != 32 ) { throw new AccessDeniedException(null, "x-amz-content-sha256 header is invalid."); } } } else { throw new AccessDeniedException( null, "x-amz-content-sha256 header is missing." ); } return contentShaHeader; } static String getDateFromParams(Map<String, String> parameters) throws AccessDeniedException { String result = parameters.get(SecurityHeader.X_Amz_Date.header().toLowerCase()); if (result == null) throw new AccessDeniedException(null, "X-Amz-Date parameter must be specified."); return result; } static void validateExpiresFromParams(Map<String, String> parameters, Date date) throws AccessDeniedException { String expires = parameters.get(AWS_EXPIRES_PARAM); if (expires == null) throw new AccessDeniedException(null, "X-Amz-Expires parameter must be specified."); Long expireTime; try { expireTime = Long.parseLong(expires); } catch (NumberFormatException e) { throw new AccessDeniedException(null, "Invalid X-Amz-Expires parameter."); } if (expireTime < 1 || expireTime > 604800) throw new AccessDeniedException(null, "Invalid Expires parameter."); DateTime currentTime = DateTime.now(); DateTime dt = new DateTime(date); if (currentTime.isBefore(dt.minusMillis((int) ObjectStorageProperties.EXPIRATION_LIMIT))) throw new AccessDeniedException(null, "Cannot process request. X-Amz-Date is not yet valid."); if (currentTime.isAfter(dt.plusSeconds(expireTime.intValue() + StackConfiguration.CLOCK_SKEW_SEC))) throw new AccessDeniedException(null, "Cannot process request. Expired."); } static Long getAndVerifyDecodedContentLength(MappingHttpRequest request, String contentSha) throws S3Exception { if (!STREAMING_PAYLOAD.equals(contentSha)) return null; String decodedContentLength = request.getHeader(AWS_DECODED_CONTENT_LEN); if (Strings.isNullOrEmpty(decodedContentLength)) throw new MissingContentLengthException(null, "Missing x-amz-decoded-content-length header"); try { return Long.valueOf(decodedContentLength); } catch (NumberFormatException e) { throw new MissingContentLengthException(null, "Invalid x-amz-decoded-content-length header"); } } static SignatureCredential getAndVerifyCredential(Date date, String credentialStr) throws AccessDeniedException { try { SignatureCredential credential = new SignatureCredential(credentialStr); credential.verify(date, null, null, AWS_V4_TERMINATOR); return credential; } catch (AuthenticationException e) { throw new AccessDeniedException(null, "Credential header is invalid."); } } /** * Returns the auth components for the sigv4 request's Authentication header. * * @see <a href="http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html">S3 Docs</a> */ static Map<V4AuthComponent, String> getV4AuthComponents(String authHeader) { authHeader = authHeader.replaceFirst(AWS_V4_AUTH_TYPE, "").trim(); Iterable<String> authParts = CSV_SPLITTER.split(authHeader); Map<V4AuthComponent, String> authParams = new HashMap<>(); for (String nvp : authParts) { Iterable<String> nameAndValue = NVP_SPLITTER.split(nvp); try { V4AuthComponent name = V4AuthComponent.valueOf(Iterables.get(nameAndValue, 0, "")); String value = Iterables.get(nameAndValue, 1, ""); if (value != null && !value.isEmpty()) authParams.put(name, value); } catch (IllegalArgumentException ignore) { } } return authParams; } }