com.eucalyptus.objectstorage.pipeline.auth.S3V4Authentication.java Source code

Java tutorial

Introduction

Here is the source code for com.eucalyptus.objectstorage.pipeline.auth.S3V4Authentication.java

Source

/*************************************************************************
 * 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;
    }
}