package com.intel.mtwilson.security.jersey;
import com.intel.mtwilson.security.core.X509UserFinder;
import com.intel.mtwilson.security.core.X509UserInfo;
import com.intel.mtwilson.security.http.RsaSignatureInput;
import com.intel.dcsg.cpg.rfc822.Rfc822Date;
import com.intel.dcsg.cpg.crypto.CryptographyException;
import com.intel.mtwilson.model.Md5Digest;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.cert.Certificate;
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.ArrayUtils;
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 "X509Certificate"
* which is an asymmetric-key scheme using RSA to sign requests.
*
* See also X509RequestVerifier and HmacAuthorization
*
* @since 0.5.2
* @author jbuhacoff
*/
public class X509RequestVerifier {
private static Logger log = LoggerFactory.getLogger(X509RequestVerifier.class);
private X509UserFinder finder;
private int requestsExpireAfterMs = 60 * 60 * 1000; // 1 hour, in milliseconds
private String headerAttributeNameValuePair = "([a-zA-Z0-9_-]+)=\"([^\"]+)\"";
private Pattern headerAttributeNameValuePairPattern = Pattern.compile(headerAttributeNameValuePair);
public X509RequestVerifier(X509UserFinder 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 CryptographyException, UnsupportedEncodingException {
String authorizationHeader = headers.getFirst("Authorization");
// try {
log.debug("Parsing authorization header: {}", authorizationHeader);
Authorization a = parseAuthorization(authorizationHeader);
log.info("X509CertificateAuthorization: Request timestamp ok");
RsaSignatureInput signatureBlock = new RsaSignatureInput();
signatureBlock.httpMethod = 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");
log.debug("X509CertificateAuthorization: Using X-Original-URL");
}
else if( headers.containsKey("X-Original-Request") ) {
signatureBlock.url = headers.getFirst("X-Original-Request");
log.debug("X509CertificateAuthorization: Using X-Original-Request");
}
else {
signatureBlock.url = requestUrl;
}
*/
signatureBlock.url = requestUrl;
signatureBlock.realm = a.realm;
signatureBlock.fingerprintBase64 = a.fingerprintBase64;
signatureBlock.signatureAlgorithm = a.signatureAlgorithm;
signatureBlock.headerNames = a.headerNames;
HashMap<String,String> headerValues = new HashMap<String,String>();
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("X509CertificateAuthorization: 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);
X509UserInfo userInfo = finder.getUserForX509Identity(fingerprint);
log.debug("X509CertificateAuthorization Fingerprint: {}",a.fingerprintBase64);
log.debug("X509CertificateAuthorization: Signature: {}",a.signatureBase64);
log.debug("X509CertificateAuthorization: Algorithm: {}",a.signatureAlgorithm);
if( userInfo == null ) {
log.error("X509CertificateAuthorization cannot find user with fingerprint: {} ", a.fingerprintBase64);
return null;
}
boolean isValid = false;
if( userInfo.certificate != null ) {
try {
isValid = verifySignature(document, userInfo.certificate, signatureAlgorithm, signature);
} 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);
}
log.debug("X509CertificateAuthorization verified signature using certificate; result= {}", isValid);
if( !isValid ) {
throw new IllegalArgumentException("Authorization signature is invalid");
}
}
// 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( !signatureBlock.url.equals(requestUrl) ) {
log.warn("X509CertificateAuthorization: Actual URL did not match Signed URL");
log.debug(" Actual URL: "+requestUrl);
log.debug(" Signed URL: "+signatureBlock.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") ) {
Date requestDate = Rfc822Date.parse(signatureBlock.headers.get("Date"));
if( isRequestExpired(requestDate) ) {
log.error("X509CertificateAuthorization: Request expired; date="+requestDate);
throw new IllegalArgumentException("Request expired"); //; current time is "+Iso8601Date.format(new Date()));
}
}
else {
throw new IllegalArgumentException("Missing date header in request");
}
return new User(a.fingerprintBase64, userInfo.roles, userInfo.loginName, Md5Digest.valueOf(signature));
}
/*
}
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("Request expired: {}", 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 CryptographyException, UnsupportedEncodingException {
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: X509
realm="Example",
fingerprint="0685bd9184jfhq22",
headers="X-Nonce,Date",
algorithm="RSA-SHA256",
signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D"
*
* @param authorizationHeader
* @return
*/
private Authorization parseAuthorization(String authorizationHeader) {
Authorization a = new Authorization();
// splitting on spaces should yield "X509" followed by attribute name-value pairs
String[] terms = authorizationHeader.split(" ");
if( !"X509".equals(terms[0]) ) { throw new IllegalArgumentException("Authorization type is not X509"); }
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") || name.equals("id") ) { 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; }
}
}
if( a.realm == null || a.realm.isEmpty() ) {
log.warn("Authorization is missing realm"); // throw new IllegalArgumentException("Authorization is missing realm"); // currently we allow undefined realm because we only have one database of users. in the future we could require a realm if we have moer than one and we need to know where to look things up.
}
if( a.fingerprintBase64 == null || a.fingerprintBase64.isEmpty() ) {
throw new IllegalArgumentException("Authorization is missing id/fingerprint");
}
if( a.signatureAlgorithm == null || a.signatureAlgorithm.isEmpty() ) {
throw new IllegalArgumentException("Authorization is missing signature algorithm");
}
if( a.signatureBase64 == null || a.signatureBase64.isEmpty() ) {
throw new IllegalArgumentException("Authorization is missing signature");
}
return a;
}
// commenting out unused function (6/11 1.2)
/*
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);
}
*/
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 realm;
public String fingerprintBase64;
public String[] headerNames = ArrayUtils.EMPTY_STRING_ARRAY;
public String signatureAlgorithm;
public String signatureBase64;
}
}