package com.intel.mtwilson.security.jersey;
import com.intel.mtwilson.datatypes.Role;
import com.intel.mtwilson.security.core.SecretKeyFinder;
import com.intel.mtwilson.security.http.HmacSignatureInput;
import com.intel.dcsg.cpg.iso8601.Iso8601Date;
import com.intel.dcsg.cpg.crypto.CryptographyException;
import com.intel.mtwilson.model.Md5Digest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Calendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.core.MultivaluedMap;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class requires the following libaries:
* org.apache.commons.codec.binary.Base64 from commons-codec
*
* It implements the server side authorization of incoming API requests using
* the HTTP "Authorization" header. The authentication scheme supported is "MtWilson"
* which is a symmetric-key scheme using HMAC-SHA256 to sign requests.
*
* See also PublicKeyAuthorization
*
* @since 0.5.2
* @author jbuhacoff
*/
public class HmacRequestVerifier {
private static Logger log = LoggerFactory.getLogger(HmacRequestVerifier.class);
private SecretKeyFinder finder;
private int requestsExpireAfterMs = 60 * 60 * 1000; // 60 minutes
private String headerAttributeNameValuePair = "([a-zA-Z0-9_-]+)=\"([^\"]+)\"";
private Pattern headerAttributeNameValuePairPattern = Pattern.compile(headerAttributeNameValuePair);
private boolean enforceSameHttpMethod = true;
private boolean enforceSameQueryString = true;
private boolean enforceSameURL = false;
private final String SIGNATURE_ALGORITHM = "HmacSHA256";
public HmacRequestVerifier(SecretKeyFinder finder) {
this.finder = finder;
}
public void setEnforceSameHttpMethod(boolean enabled) {
enforceSameHttpMethod = enabled;
}
public void setEnforceSameQueryString(boolean enabled) {
enforceSameQueryString = enabled;
}
public void setEnforceSameURL(boolean enabled) {
enforceSameURL = enabled;
}
/**
* Verifies the signature for a given request method, url, Authorization header, and request body.
* Information from the request is collected and signed by the secret key
* provided by the SecretKeyFinder provided to the constructor.
* The SecretKeyFinder retrieves the appropriate secret key to validate the request
* by looking up the userId token included in the request.
*
* @param httpMethod such as "GET" or "POST"
* @param requestUrl a complete request URL such as http://example.com/some/path
* @param authorizationHeader the Authorization header from the request
* @param requestBody the body of the request, or null
* @return a User object if the signature on the request is valid for a known user, or null if it's invalid or user cannot be found
*
*/
public User getUserForRequest(String httpMethod, String requestUrl, MultivaluedMap<String,String> headers, String requestBody) throws CryptographyException {
String authorizationHeader = headers.getFirst("Authorization");
// try {
Authorization a = parseAuthorization(authorizationHeader);
log.info("VerifyAuthorization: Request timestamp ok");
HmacSignatureInput signatureBlock = new HmacSignatureInput();
signatureBlock.httpMethod = a.httpMethod;
signatureBlock.absoluteUrl = a.url;
signatureBlock.fromToken = a.username;
signatureBlock.nonce = a.nonce;
signatureBlock.body = requestBody;
signatureBlock.signatureMethod = a.signatureMethod;
signatureBlock.timestamp = a.timestamp;
String content = signatureBlock.toString(); // may throw IllegalArgumentException if any required field is null or invalid
//log.debug("VerifyAuthorization: Signed content ("+content.length()+") follows:\n"+content);
String username = new String(Base64.decodeBase64(a.username));
String secretKey = finder.getSecretKeyForUserId(username);
String signature;
try {
signature = sign(content, secretKey); // may throw NoSuchAlgorithmException, InvalidKeyException
} catch (NoSuchAlgorithmException ex) {
throw new CryptographyException("Signature algorithm not supported: "+SIGNATURE_ALGORITHM, ex);
} catch (InvalidKeyException ex) {
throw new CryptographyException("Invalid key in signature: "+ex.getMessage(), ex);
}
log.debug("VerifyAuthorization: Username: "+username);
log.debug("VerifyAuthorization: Signature: "+signature);
// REST protection: make sure the actual HTTP method matches the one that was used to create the signature.
// We don't verify the full URL right now because it's legal for a proxy to change it... we'd have to allow
// for a configurable "authorized rewriting" in order to verify it.
// Or, we can verify it and there will be errors if a proxy rewrites it, which might be ok ???
if( enforceSameHttpMethod ) {
if( a.httpMethod == null ) { return null; }
else if( !a.httpMethod.equals(httpMethod) ) {
log.info("Actual HTTP method did not match Signed HTTP method");
log.debug(" Actual method: "+httpMethod);
log.debug(" Signed method: "+a.httpMethod);
throw new CryptographyException("Request HTTP method did not match Signed HTTP method");
}
}
if( enforceSameURL ) {
if( a.url == null ) { return null; }
else if( !a.url.equals(requestUrl) ) {
log.warn("VerifyAuthorization: Actual URL did not match Signed URL");
log.debug(" Actual URL: "+requestUrl);
log.debug(" Signed URL: "+a.url);
throw new CryptographyException("Request URL method did not match Signed URL");
}
}
if( enforceSameQueryString ) {
// not implemented yet... need to parse both URL's and compare the query strings.
log.warn("VerifyAuthorization: enforceSameQueryString is enabled but not implemented yet");
}
if( signature.equals(a.signature) ) {
log.info("Request is authenticated");
try {
if( signatureBlock.timestamp == null || isRequestExpired(signatureBlock.timestamp) ) { // may throw ParseException
return null;
}
}
catch(ParseException e) {
throw new IllegalArgumentException("Date is not in ISO8601 format: "+signatureBlock.timestamp, e);
}
return new User(username, new Role[] { Role.Attestation, Role.Whitelist }, username, Md5Digest.valueOf(Base64.decodeBase64(signature)));
}
/*
}
catch (ParseException e) {
log.error("Request authorization timestamp must conform to ISO 8601 format", e);
}
catch (IllegalArgumentException e) {
log.error("Required parameters are missing or invalid", e);
}
catch (NoSuchAlgorithmException e) {
log.error("Unsupported signature algorithm", e);
}
catch (InvalidKeyException e) {
log.error("Password is not a valid key for signature algorithm", e);
}
catch (Exception e) {
log.error("Unknown error while verifying signature", e);
}*/
log.info("Request is NOT AUTHENTICATED");
return null;
}
private boolean isRequestExpired(String timestamp) throws ParseException {
// request expiration policy
Calendar expirationTime = Calendar.getInstance();
expirationTime.setTime(Iso8601Date.valueOf(timestamp));
expirationTime.add(Calendar.MILLISECOND, requestsExpireAfterMs);
Calendar currentTime = Calendar.getInstance();
if( currentTime.after(expirationTime)) {
long diff = currentTime.getTimeInMillis() - expirationTime.getTimeInMillis();
log.warn(String.format("Request expired (%s)", DurationFormatUtils.formatDurationHMS(diff)));
return true;
}
return false;
}
/**
* Verifies the signature for a given request method, url, Authorization header, and request body.
* Information from the request is collected and signed by the secret key
* provided by the SecretKeyFinder provided to the constructor.
* The SecretKeyFinder retrieves the appropriate secret key to validate the request
* by looking up the userId token included in the request.
*
* @param httpMethod such as "GET" or "POST"
* @param requestUrl a complete request URL such as http://example.com/some/path
* @param authorizationHeader the Authorization header from the request
* @param requestBody the body of the request, or null
* @return true if the signature on the request is valid
*/
public boolean isSignatureValid(String httpMethod, String requestUrl, MultivaluedMap<String,String> headers, String requestBody) throws CryptographyException {
User user = getUserForRequest(httpMethod, requestUrl, headers, requestBody);
return user != null;
}
/**
*
* Authorization header format is like this: "Signature" *<SP <attribute-name "=" quoted-attribute-value>>
*
* Sample Authorization header:
*
Authorization: MtWilson
realm="Example",
username="0685bd9184jfhq22",
http_method="GET",
uri="/reports/trust?hostName=example",
timestamp="2012-02-14T08:15:00PST",
nonce="4572616e48616d6d65724c61686176",
signature_method="HMAC-SHA256",
signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D"
*
* @param authorizationHeader
* @return
*/
private Authorization parseAuthorization(String authorizationHeader) {
Authorization a = new Authorization();
// splitting on spaces should yield "MtWilson" followed by attribute name-value pairs
String[] terms = authorizationHeader.split(" ");
if( !"MtWilson".equals(terms[0]) ) { throw new IllegalArgumentException("Authorization type is not MtWilson"); }
for(int i=1; i<terms.length; i++) {
// each term after "MtWilson" is an attribute name-value pair, like realm="Example"
Matcher attributeNameValue = headerAttributeNameValuePairPattern.matcher(terms[i]);
if( attributeNameValue.find() ) {
String name = attributeNameValue.group(1);
String value = attributeNameValue.group(2);
if( name.equals("realm") ) { a.realm = value; }
if( name.equals("username") ) { a.username = value; }
if( name.equals("nonce") ) { a.nonce = value; }
if( name.equals("http_method") ) { a.httpMethod = value; }
if( name.equals("uri") ) { a.url = value; }
if( name.equals("timestamp") ) { a.timestamp = value; }
if( name.equals("signature") ) { a.signature = value; }
if( name.equals("signature_method") ) { a.signatureMethod = value; }
}
}
return a;
}
/**
* Given arbitrary text, returns a base64-encoded version of the text's HMAC using the
* previously specified secret key.
*
* @param text
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
private String sign(String text, String secretKey) throws NoSuchAlgorithmException,InvalidKeyException {
SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(), SIGNATURE_ALGORITHM);
Mac mac = Mac.getInstance(SIGNATURE_ALGORITHM);
mac.init(key);
return new String(Base64.encodeBase64(mac.doFinal(text.getBytes())));
}
public static class Authorization {
public String realm;
public String username;
public String nonce;
public String signature;
public String signatureMethod;
public String httpMethod;
public String url;
public String timestamp;
}
}