/*************************************************************************
* 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.auth.login;
import static com.eucalyptus.ws.util.HmacUtils.headerLookup;
import static com.eucalyptus.ws.util.HmacUtils.parameterLookup;
import static com.eucalyptus.ws.util.HmacUtils.SignatureCredential;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.crypto.spec.SecretKeySpec;
import org.apache.log4j.Logger;
import com.eucalyptus.auth.AccessKeys;
import com.eucalyptus.auth.InvalidSignatureAuthException;
import com.eucalyptus.auth.principal.AccessKey;
import com.eucalyptus.auth.principal.UserPrincipal;
import com.eucalyptus.crypto.Digest;
import com.eucalyptus.crypto.Hmac;
import com.eucalyptus.crypto.util.SecurityHeader;
import com.eucalyptus.crypto.util.SecurityParameter;
import com.eucalyptus.crypto.util.Timestamps;
import com.eucalyptus.ws.util.HmacUtils;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.io.BaseEncoding;
public class Hmacv4LoginModule extends HmacLoginModuleSupport {
private static final Logger LOG = Logger.getLogger( Hmacv4LoginModule.class );
private static final String V4_TERMINATOR = "aws4_request";
public Hmacv4LoginModule() {
super(4);
}
@Override
public boolean authenticate( final HmacCredentials credentials ) throws Exception {
if ( credentials.getSignatureMethod() != Hmac.HmacSHA256 ) {
throw new AuthenticationException( "Invalid signature method for v4: " + credentials.getSignatureMethod() );
}
final String sig = credentials.getSignature( );
final Function<String,List<String>> headerLookup = headerLookup( credentials.getHeaders() );
final Function<String,List<String>> parameterLookup = parameterLookup( credentials.getParameters() );
final Map<String,String> authorizationParameters = credentials.getVariant().getAuthorizationParameters( headerLookup, parameterLookup );
final SignatureCredential signatureCredential = new SignatureCredential( authorizationParameters.get("Credential") );
final AccessKey accessKey = lookupAccessKey( credentials );
final Date date = HmacUtils.getSignatureDate( EnumSet.of(HmacUtils.SignatureVersion.SignatureV4), headerLookup, parameterLookup );
signatureCredential.verify( date, null, null, V4_TERMINATOR ); //TODO Do we want to validate region and service name?
final UserPrincipal user = accessKey.getPrincipal( );
final String secretKey = accessKey.getSecretKey( );
final byte[] signatureKey = getSignatureKey( secretKey, signatureCredential );
final CharSequence canonicalString = this.makeSubjectString( credentials, signatureCredential, authorizationParameters, date, false );
final byte[] computedSig = this.getHmacSHA256( signatureKey, canonicalString );
final byte[] providedSig = BaseEncoding.base16( ).lowerCase( ).decode( sig );
if ( !MessageDigest.isEqual( computedSig, providedSig ) ) {
final CharSequence canonicalStringNoPath = this.makeSubjectString( credentials, signatureCredential, authorizationParameters, date, true );
final byte[] computedSigNoPath = this.getHmacSHA256( signatureKey, canonicalStringNoPath );
if( !MessageDigest.isEqual( computedSigNoPath, providedSig ) ) {
throw new InvalidSignatureAuthException( "Signature validation failed" );
}
}
super.setCredential( credentials.getCredential( AccessKeys.getKeyType( accessKey ) ) );
super.setPrincipal( user );
return true;
}
private CharSequence makeSubjectString( @Nonnull final HmacCredentials credentials,
@Nonnull final SignatureCredential signatureCredential,
@Nonnull final Map<String,String> authorizationParameters,
@Nonnull final Date date,
final boolean skipPath ) throws Exception {
final String timestamp = Timestamps.formatShortIso8601Timestamp( date );
final StringBuilder sb = new StringBuilder( 256 );
sb.append( SecurityHeader.Value.AWS4_HMAC_SHA256.value() ).append( '\n' );
sb.append( timestamp ).append( '\n' );
sb.append( signatureCredential.getCredentialScope() ).append( '\n' );
sb.append( digestUTF8( makeCanonicalRequest( credentials, authorizationParameters, skipPath ) ) );
if ( signatureLogger.isTraceEnabled( ) ) signatureLogger.trace( "VERSION4: " + sb.toString( ) );
return sb;
}
private CharSequence makeCanonicalRequest( @Nonnull final HmacCredentials credentials,
@Nonnull final Map<String,String> authorizationParameters,
final boolean skipPath ) throws Exception {
final StringBuilder sb = new StringBuilder( 512 );
sb.append( credentials.getVerb( ) );
sb.append( '\n' );
sb.append( skipPath ? "/" : canonicalizePath( credentials.getServicePath( ) ) ); // AWS Java SDK always uses "/"
sb.append( '\n' );
boolean addedParam = false;
for ( final String parameter : Ordering.natural( ).sortedCopy( credentials.getParameters().keySet() ) ) {
if ( credentials.getVariant() == HmacUtils.SignatureVariant.SignatureV4Query && SecurityParameter.X_Amz_Signature.parameter().equals( parameter ) ) {
continue;
}
for ( final String value : Ordering.natural().sortedCopy( credentials.getParameters().get( parameter ) ) ) {
sb.append( urlencode(parameter) );
sb.append( '=' );
sb.append( urlencode(value) );
sb.append( '&' );
addedParam = true;
}
}
if ( addedParam ) sb.setLength( sb.length()-1 );
sb.append( '\n' );
for ( final String header : authorizationParameters.get("SignedHeaders").split(";") ) {
final List<String> values = Lists.transform( credentials.getHeaders().get( header ), new Function<String, String>() {
@Override
public String apply( final String text ) {
return text.trim();
}
} );
sb.append( header );
sb.append( ':' );
sb.append( Joiner.on( ',' ).join( Ordering.<String>natural().sortedCopy( values ) ) );
sb.append( '\n' );
}
sb.append( '\n' );
sb.append( authorizationParameters.get("SignedHeaders") );
sb.append( '\n' );
sb.append( digestUTF8( credentials.getBody() ) );
if ( signatureLogger.isTraceEnabled( ) ) signatureLogger.trace( "VERSION4: " + sb.toString( ) );
return sb;
}
/**
* Returns a hex encoded SHA256 hash of the {@code text}.
*/
public static String digestUTF8( final CharSequence text ) {
final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode( CharBuffer.wrap( text ) );
return BaseEncoding.base16( ).lowerCase( ).encode( Digest.SHA256.digestBinary( byteBuffer ) );
}
public static String canonicalizePath( final String servicePath ) throws URISyntaxException {
return servicePath.isEmpty() ? "/" : new URI("http", "0.0.0.0", servicePath, null).normalize().getPath(); //TODO encode path here when it becomes necessary
}
public static byte[] getHmacSHA256( final byte[] signatureKey,
final CharSequence data ) throws AuthenticationException {
final SecretKeySpec signingKey = new SecretKeySpec( signatureKey, Hmac.HmacSHA256.toString( ) );
try {
final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode( CharBuffer.wrap( data ) );
return Hmac.HmacSHA256.digestBinary( signingKey, byteBuffer );
} catch ( Exception e ) {
LOG.error( e, e );
throw new AuthenticationException( "Failed to compute signature" );
}
}
public static byte[] getSignatureKey( final String key,
final SignatureCredential credential ) throws Exception {
return getHmacSHA256(
getHmacSHA256(
getHmacSHA256(
getHmacSHA256( ("AWS4" + key).getBytes( StandardCharsets.UTF_8 ), credential.getDate() ),
credential.getRegion() ),
credential.getServiceName() ),
credential.getTerminator() );
}
}