package com.intel.mtwilson.security.jersey;
import com.intel.mtwilson.security.core.PublicKeyUserFinder;
import com.intel.mtwilson.security.core.PublicKeyUserInfo;
import com.intel.mtwilson.security.http.RsaSignatureInput;
import com.intel.dcsg.cpg.crypto.CryptographyException;
import com.intel.mtwilson.model.Md5Digest;
//import com.sun.jersey.core.header.HttpDateFormat;
import org.glassfish.jersey.message.internal.HttpDateFormat;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.cert.Certificate;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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 "PublicKey"
* which is an asymmetric-key scheme using RSA to sign requests.
*
* See also PublicKeyRequestVerifier/MtWilsonAuthorization
*
* @since 0.5.2
* @author jbuhacoff
*/
public class PublicKeyRequestVerifier {
private static Logger log = LoggerFactory.getLogger(PublicKeyRequestVerifier.class);
private PublicKeyUserFinder finder;
private int requestsExpireAfterMs = 60 * 60 * 1000; // 60 minutes
private String headerAttributeNameValuePair = "([a-zA-Z0-9_-]+)=\"([^\"]+)\"";
private Pattern headerAttributeNameValuePairPattern = Pattern.compile(headerAttributeNameValuePair);
public PublicKeyRequestVerifier(PublicKeyUserFinder finder) {
this.finder = finder;
}
/**
* 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 headers all the request headers, including 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 UnsupportedEncodingException, CryptographyException {
String authorizationHeader = headers.getFirst("Authorization");
// try {
Authorization a = parseAuthorization(authorizationHeader);
log.info("PublicKeyAuthorization: Request timestamp ok");
RsaSignatureInput signatureBlock = new RsaSignatureInput();
signatureBlock.httpMethod = a.httpMethod;
/*
* Bug #383 disabling support for this because it creates a security vulnerability
if( headers.containsKey("X-Original-URL") ) {
signatureBlock.url = headers.getFirst("X-Original-URL");
}
else if( headers.containsKey("X-Original-Request") ) {
signatureBlock.url = headers.getFirst("X-Original-Request");
}
else {
signatureBlock.url = a.url;
}
*/
signatureBlock.url = a.url;
signatureBlock.realm = a.realm;
signatureBlock.fingerprintBase64 = a.fingerprintBase64;
signatureBlock.signatureAlgorithm = a.signatureAlgorithm;
signatureBlock.headerNames = a.headerNames;
HashMap<String,String> headerValues = new HashMap<String,String>();
if( a.headerNames != null ) {
for(String headerName : a.headerNames) {
headerValues.put(headerName, headers.getFirst(headerName));
}
}
signatureBlock.headers = headerValues;
signatureBlock.body = requestBody;
String content = signatureBlock.toString(); // may throw IllegalArgumentException if any required field is null or invalid
//log.debug("PublicKeyAuthorization: Signed content ("+content.length()+") follows:\n"+content);
// locate the public key or x509 certificate that can verify the signature
byte[] document = content.getBytes("UTF-8");
byte[] signature = Base64.decodeBase64(a.signatureBase64);
String signatureAlgorithm = signatureAlgorithm(a.signatureAlgorithm);
byte[] fingerprint = Base64.decodeBase64(a.fingerprintBase64);
PublicKeyUserInfo userInfo = finder.getUserForIdentity(fingerprint);
log.debug("PublicKeyAuthorization Fingerprint: "+a.fingerprintBase64);
log.debug("PublicKeyAuthorization: Signature: "+a.signatureBase64);
log.debug("PublicKeyAuthorization: Algorithm: "+a.signatureAlgorithm);
if( userInfo == null ) {
log.error("PublicKeyAuthorization cannot find user with fingerprint "+a.fingerprintBase64);
return null;
}
boolean isValid = false;
if( userInfo.publicKey != null ) {
try {
isValid = verifySignature(document, userInfo.publicKey, signatureAlgorithm, signature);
log.debug("PublicKeyAuthorization verified signature using public key; result= "+isValid);
} catch (NoSuchAlgorithmException ex) {
throw new CryptographyException("Signature algorithm not supported: "+signatureAlgorithm, ex);
} catch (InvalidKeyException ex) {
throw new CryptographyException("Invalid key in certificate: "+ex.getMessage(), ex);
} catch (SignatureException ex) {
throw new CryptographyException("Unable to verify signature: "+ex.getMessage(), ex);
}
}
// show a warning if actual URL doesn't match signed URL, because it means we need to be careful when
// routing and authorizing the requested actions... must be done according to signed URL, not actual URL
if( a.url == null || !a.url.equals(requestUrl) ) {
log.warn("PublicKeyAuthorization: Actual URL did not match Signed URL");
log.debug(" Actual URL: "+requestUrl);
log.debug(" Signed URL: "+a.url);
}
if( isValid ) {
log.info("Request is authenticated");
// check if the request has expired by looking at the HTTP Date header, but only if it was signed.
if( signatureBlock.headers.containsKey("Date") ) {
try {
Date requestDate = HttpDateFormat.readDate(signatureBlock.headers.get("Date")); // http date must be in RFC 1123 date format (as specified by email RFC 822 and http RFC 2616)
if( isRequestExpired(requestDate) ) {
log.error("PublicKeyAuthorization: Request expired; date="+requestDate);
return null;
}
}
catch(ParseException e) {
throw new IllegalArgumentException("Authorization timestamp must conform to ISO 8601 format", e);
}
}
return new User(a.fingerprintBase64, userInfo.roles, "", Md5Digest.valueOf(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.getMessage(), 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.error("Request is NOT AUTHENTICATED");
return null;
}
private boolean isRequestExpired(Date timestamp) {
// request expiration policy
Calendar expirationTime = Calendar.getInstance();
expirationTime.setTime(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 headers all the request headers, including 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 UnsupportedEncodingException, 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( !"PublicKey".equals(terms[0]) ) { throw new IllegalArgumentException("Authorization type is not PublicKey"); }
for(int i=1; i<terms.length; i++) {
// each term after "PublicKey" 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("fingerprint") ) { a.fingerprintBase64 = value; }
if( name.equals("headers") ) { a.headerNames = value.split(","); }
if( name.equals("algorithm") || name.equals("digest") ) { a.signatureAlgorithm = value; }
if( name.equals("signature") ) { a.signatureBase64 = value; }
}
}
return a;
}
private boolean verifySignature(byte[] document, PublicKey publicKey, String signatureAlgorithm, byte[] signature) throws NoSuchAlgorithmException,InvalidKeyException, SignatureException {
Signature rsa = Signature.getInstance(signatureAlgorithm);
rsa.initVerify(publicKey);
rsa.update(document);
return rsa.verify(signature);
}
// commenting out unused function (6/11 1.2)
/*
private boolean verifySignature(byte[] document, Certificate certificate, String signatureAlgorithm, byte[] signature) throws NoSuchAlgorithmException,InvalidKeyException, SignatureException {
Signature rsa = Signature.getInstance(signatureAlgorithm);
rsa.initVerify(certificate);
rsa.update(document);
return rsa.verify(signature);
}
*/
/**
* Standardizes signature algorithm names to the Java name.
* "SHA256withRSA".equals(signatureAlgorithm("RSA-SHA256a")); // true
* @param name
* @return
*/
private String signatureAlgorithm(String name) {
if( "RSA-SHA256".equals(name) ) { return "SHA256withRSA"; }
return name;
}
/**
* This class represents the content of the HTTP Authorization header.
* It is very closely related to the RsaSignatureInput class but not
* the same because this class includes the base64-encoded signature
* from the Authorization header. Also, the selected HTTP header names
* to include in the signature are identified in the Authorization header
* so they are included here, but the values for those headers are not
* included here.
*/
public static class Authorization {
public String httpMethod;
public String url;
public String realm;
public String fingerprintBase64;
public String[] headerNames;
public String signatureAlgorithm;
public String signatureBase64;
}
}