/*
* Copyright (C) 2012 Intel Corporation
* All rights reserved.
*/
package test.io;
import com.intel.mtwilson.ApacheHttpClient;
import com.intel.mtwilson.ApiClient;
import com.intel.mtwilson.api.*;
import com.intel.mtwilson.HtmlErrorParser;
import com.intel.mtwilson.MultivaluedMapImpl;
import com.intel.dcsg.cpg.crypto.CryptographyException;
import com.intel.dcsg.cpg.crypto.RsaCredential;
import com.intel.dcsg.cpg.crypto.RsaCredentialX509;
import com.intel.dcsg.cpg.crypto.RsaUtil;
import com.intel.dcsg.cpg.crypto.SimpleKeystore;
import com.intel.mtwilson.i18n.ErrorCode;
import com.intel.mtwilson.datatypes.ErrorResponse;
import com.intel.mtwilson.datatypes.HostTrustResponse;
import com.intel.mtwilson.datatypes.HostTrustStatus;
import com.intel.dcsg.cpg.io.ConfigurationUtil;
import com.intel.mtwilson.model.*;
import com.intel.dcsg.cpg.rfc822.Rfc822Date;
import com.intel.dcsg.cpg.tls.policy.TlsUtil;
import com.intel.mtwilson.security.http.apache.ApacheHttpAuthorization;
import com.intel.mtwilson.security.http.HttpRequestURL;
import com.intel.mtwilson.security.http.RsaSignatureInput;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import static javax.ws.rs.core.MediaType.*;
import javax.ws.rs.core.MultivaluedMap;
import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.net.URLCodec;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpStatus;
import com.fasterxml.jackson.databind.ObjectMapper;
//import org.codehaus.jackson.map.ObjectMapper;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author jbuhacoff
*/
public class SecurityTest {
private final Logger log = LoggerFactory.getLogger(getClass());
@Test
public void testSaveSslCertificate() throws IOException, KeyManagementException, CryptographyException {
Configuration config = ConfigurationUtil.fromResource("/localhost-0.5.2.properties");
// ApiClient api = new ApiClient(config);
SimpleKeystore keystore = new SimpleKeystore(new File(config.getString("mtwilson.api.keystore")), config.getString("mtwilson.api.keystore.password"));
TlsUtil.addSslCertificatesToKeystore(keystore, new URL(config.getString("mtwilson.api.baseurl")));
// keystore.save();
}
/*
@Test
public void testGetSamlCertificate() throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, KeyStoreException, IOException, CertificateException, UnrecoverableEntryException, ApiException, SignatureException {
Configuration config = ConfigurationFactory.fromResource("/localhost-0.5.2.properties");
ApiClient api = new ApiClient(config);
X509Certificate certificate = api.getSamlCertificate();
log.debug("SAML Certificate Subject: {}", certificate.getSubjectX500Principal().getName());
log.debug("SAML Certificate Issuer: {}", certificate.getIssuerX500Principal().getName());
// URL attestationService = new URL(config.getString("mtwilson.api.baseurl"));
// SimpleKeystore keystore = new SimpleKeystore(new File(config.getString("mtwilson.api.keystore")), config.getString("mtwilson.api.keystore.password"));
// keystore.addTrustedSamlCertificate(certificate, attestationService.getHost());
// keystore.save();
// log.debug("Saved SAML certificate in keystore");
}
*/
@Test
public void testUnregisteredClient() throws IOException, NoSuchAlgorithmException, CryptographyException, CertificateEncodingException, KeyManagementException, ClientException, ApiException, SignatureException {
Configuration config = ConfigurationUtil.fromResource("/localhost-0.5.2.properties");
// create a new keypair to ensure it's not registered
KeyPair keypair = RsaUtil.generateRsaKeyPair(RsaUtil.MINIMUM_RSA_KEY_SIZE);
X509Certificate certificate = RsaUtil.generateX509Certificate("CN=unregistered", keypair, 1);
RsaCredentialX509 credential = new RsaCredentialX509(keypair.getPrivate(), certificate);
// use the keystore just because it already has the server ssl cert
SimpleKeystore keystore = new SimpleKeystore(new File(config.getString("mtwilson.api.keystore")), config.getString("mtwilson.api.keystore.password"));
ApiClient api = new ApiClient(new URL(config.getString("mtwilson.api.baseurl")), credential, keystore, config);
api.getHostTrust(new Hostname("1.2.3.4")); // hostname doesn't matter since the request should be rejected by the security filter
}
@Test
public void testInvalidAuthorizationHeader() throws IOException, KeyManagementException, FileNotFoundException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableEntryException, CertificateEncodingException, ApiException, SignatureException, com.intel.dcsg.cpg.crypto.CryptographyException {
Configuration config = ConfigurationUtil.fromResource("/localhost-0.5.2.properties");
// use the keystore just because it already has the server ssl cert
SimpleKeystore keystore = new SimpleKeystore(new File(config.getString("mtwilson.api.keystore")), config.getString("mtwilson.api.keystore.password"));
RsaCredentialX509 rsaCredential = keystore.getRsaCredentialX509(config.getString("mtwilson.api.key.alias"), config.getString("mtwilson.api.key.password"));
URL baseURL = new URL(config.getString("mtwilson.api.baseurl"));
ApacheHttpAuthorization authority = new CustomizedApacheRsaHttpAuthorization(rsaCredential);
ApacheHttpClient client = new ApacheHttpClient(baseURL, authority, keystore, config);
// ApiClient api = new ApiClient(baseURL, rsaCredential, keystore, config);
CustomizedApiClient api = new CustomizedApiClient(baseURL, client);
api.getHostTrust(new Hostname("1.2.3.4")); // hostname doesn't matter since the request should be rejected by the security filter
}
/**
* THE FOLLOWING "CUSTOMIZED" CLASSES EXIST ONLY TO SIMULATE BADLY BEHAVED
* CLIENTS FOR THE PURPOSE OF TESTING THE ERROR MESSAGES FROM THE WEB SERVICES
* SECURITY FILTER.
*
*/
private static class CustomizedApiClient {
private Logger log = LoggerFactory.getLogger(getClass());
private URL baseURL;
private ApacheHttpClient httpClient;
protected static final ObjectMapper mapper = new ObjectMapper();
public CustomizedApiClient(URL baseURL, ApacheHttpClient client) {
this.baseURL = baseURL;
this.httpClient = client;
log.info("CUSTOMIZED API CLIENT CONSTRUCTOR");
}
private String querystring(MultivaluedMap<String,String> query) {
URLCodec urlsafe = new URLCodec("UTF-8");
String queryString = "";
ArrayList<String> params = new ArrayList<String>();
for( String key : query.keySet() ) {
if( query.get(key) == null ) {
params.add(key+"=");
}
else {
for( String value : query.get(key) ) {
try {
params.add(key+"="+urlsafe.encode(value)); // XXX assumes that the keys don't have any special characters
} catch (EncoderException ex) {
log.error("Cannot encode query parameter: {}", value, ex);
}
}
}
queryString = StringUtils.join(params, "&");
}
return queryString;
}
//comment out unused function for removal (6/10 1.2)
//private String asurl(String apiPath) {
// return baseURL.toExternalForm().concat("/AttestationService/resources").concat(apiPath);
//}
private String asurl(String apiPath, MultivaluedMap<String,String> query) {
return baseURL.toExternalForm().concat("/AttestationService/resources").concat(apiPath).concat("?").concat(querystring(query));
}
// only call this if the Http Status is NOT OK in order to convert the response to an ApiException
private ApiException error(ApiResponse response) throws IOException, ApiException {
if( response.contentType.isCompatible(APPLICATION_JSON_TYPE) ) {
// a json error response from the web application. we need to provide the error message to the user.
ErrorResponse errorResponse;
try {
log.debug("Parsing JSON error response: "+new String(response.content, "UTF-8"));
errorResponse = json(new String(response.content, "UTF-8"), ErrorResponse.class);
}
catch(Exception e) {
// cannot parse the json response, so include the entire response for the user. we ignore the exception "e" because it just means we couldn't parse the response.
return new ApiException(response.httpReasonPhrase+": "+response.content, ErrorCode.UNKNOWN_ERROR);
}
return new ApiException(response.httpReasonPhrase+": "+errorResponse.getErrorMessage(), ErrorCode.valueOf(errorResponse.getErrorCode()));
}
else if( response.contentType.isCompatible(TEXT_HTML_TYPE) ) {
// typically html error message generated by web application container; we can ignore the html content because its generic
String errorMessage = response.httpReasonPhrase;
HtmlErrorParser errorParser = new HtmlErrorParser(new String(response.content, "UTF-8"));
if( errorParser.getRootCause() != null ) {
errorMessage = errorMessage.concat(": "+errorParser.getRootCause());
}
return new ApiException(errorMessage, ErrorCode.UNKNOWN_ERROR);
}
else {
// a non-json, non-html error response from the web application: so we include the response in the exception message.
return new ApiException(response.httpReasonPhrase+": "+new String(response.content, "UTF-8"), ErrorCode.UNKNOWN_ERROR);
}
}
private byte[] content(ApiResponse response) throws IOException, ApiException {
log.trace("Status: {} {}", response.httpStatusCode, response.httpReasonPhrase);
log.trace("Content-Type: {}", response.contentType.toString());
log.trace("Content: {}", response.content);
if( response.httpStatusCode == HttpStatus.SC_OK ) {
return response.content;
}
else {
throw error(response);
}
}
private <T> T json(ApiResponse response, Class<T> valueType) throws IOException, ApiException {
if( response.httpStatusCode == HttpStatus.SC_OK && response.contentType.isCompatible(APPLICATION_JSON_TYPE) ) {
return json(new String(response.content, "UTF-8"), valueType);
}
else if( response.httpStatusCode == HttpStatus.SC_OK ) {
log.error("Unexpected content type {} in response", response.contentType.toString());
throw new ApiException("Unexpected content type in response: "+response.contentType.toString());
}
else {
throw error(response);
}
}
private <T> T json(String document, Class<T> valueType) throws IOException, ApiException {
if( document == null ) {
throw new ApiException("Response from server has no content");
}
try {
return mapper.readValue(document, valueType);
}
catch(com.fasterxml.jackson.core.JsonParseException e) {
log.error("Cannot parse response: "+document);
throw new ApiException("Cannot parse response: "+document, e);
}
}
private String text(ApiResponse response) throws IOException, ApiException {
return new String(content(response), "UTF-8");
}
private HostTrustStatus parseHostTrustStatusString(String trustStatusString) {
HostTrustStatus trustStatus = new HostTrustStatus();
String[] parts = trustStatusString.split(",");
for (String part : parts) {
String[] subParts = part.split(":");
if (subParts[0].equals("BIOS")) {
trustStatus.bios = subParts[1].equals("1");
}
else if(subParts[0].equals("VMM")) {
trustStatus.vmm = subParts[1].equals("1");
}
}
return trustStatus;
}
public HostTrustResponse getHostTrust(Hostname hostname) throws IOException, ApiException, SignatureException {
MultivaluedMap<String,String> query = new MultivaluedMapImpl();
query.add("hostName", hostname.toString());
// need to support both formats: "BIOS:1,VMM:1" from 0.5.1 and JSON from 0.5.2
ApiResponse response = httpClient.get(asurl("/hosts/trust", query));
HostTrustResponse trust;
if( response.httpStatusCode == HttpStatus.SC_OK) {
if( APPLICATION_JSON_TYPE.equals(response.contentType) ) {
trust = json(response, HostTrustResponse.class);
}
else if( TEXT_PLAIN_TYPE.equals(response.contentType) ) {
trust = new HostTrustResponse(hostname, parseHostTrustStatusString(text(response)));
}
else {
throw new ApiException("Unexpected content type in response: "+response.contentType, ErrorCode.UNKNOWN_ERROR.getErrorCode());
}
return trust;
}
else {
throw error(response);
}
}
}
// just like ApacheRsaHttpAuthorization but using the CustomizedRsaAuthorization instead of the regular RsaAuthorization
public static class CustomizedApacheRsaHttpAuthorization implements ApacheHttpAuthorization {
private Logger log = LoggerFactory.getLogger(getClass());
private CustomizedRsaAuthorization authority;
public CustomizedApacheRsaHttpAuthorization(RsaCredential credential) {
log.info("CUSTOMIZED APACHE RSA HTTP AUTHORIZATION CONSTRUCTOR");
authority = new CustomizedRsaAuthorization(credential);
}
@Override
public void addAuthorization(HttpRequest request) throws SignatureException {
HashMap<String,String> headers = new HashMap<String,String>();
request.addHeader("Authorization",
authority.getAuthorizationQuietly(request.getRequestLine().getMethod(), request.getRequestLine().getUri(), headers));
// the RsaAuthorization class may generate headers for the request such as nonce and date, so we look for those and add them.
for(String key : headers.keySet()) {
request.addHeader(key, headers.get(key));
}
}
@Override
public void addAuthorization(HttpEntityEnclosingRequest request) throws SignatureException, IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
}
public static class CustomizedRsaAuthorization {
private Logger log = LoggerFactory.getLogger(getClass());
private RsaCredential credential;
private String realm;
public CustomizedRsaAuthorization(RsaCredential credential) {
this.credential = credential;
// CUSTOMIZE: try null, empty, or arbitrary attestation. if missing should get "Unauthorized: Authorization is missing realm"
this.realm = "Attestation";
log.info("CUSTOMIZED RSA AUTHORIZATION CONSTRUCTOR");
}
public String getAuthorizationQuietly(String httpMethod, String requestUrl, Map<String,String> headers) throws SignatureException {
return getAuthorizationQuietly(httpMethod, requestUrl, null, headers, null);
}
public String getAuthorizationQuietly(String httpMethod, String requestUrl, Map<String,Object> urlParams, Map<String,String> headers, String requestBody) throws SignatureException {
try {
return getAuthorization(httpMethod, requestUrl, urlParams, headers, requestBody);
}
catch(NoSuchAlgorithmException e) {
log.error("Algorithm not available: "+e.getMessage());
}
catch(InvalidKeyException e) {
log.error("Password not suitable for signature: "+e.getMessage());
}
catch(IOException e) {
log.error("Error creating signature: "+e.getMessage());
}
return null;
}
/**
* SECURITY FILTER TESTING - THIS IS THE METHOD TO CUSTOMIZE IN ORDER TO
* BREAK THE AUTHORIZATION HEADER IN DIFFERENT WAYS TO TEST THE SERVER
*
* @param httpMethod
* @param requestUrl
* @param urlParams
* @param headers
* @param requestBody
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws IOException
* @throws SignatureException
*/
public String getAuthorization(String httpMethod, String requestUrl, Map<String,Object> urlParams, Map<String,String> headers, String requestBody) throws NoSuchAlgorithmException, InvalidKeyException, IOException, SignatureException {
String nonce = new String(Base64.encodeBase64(nonce()));
headers.put("X-Nonce", nonce);
String username = new String(Base64.encodeBase64(credential.identity()));
//String timestamp = ISO8601.DATETIME.format(System.currentTimeMillis());
String timestamp;
if( headers.containsKey("Date") ) {
timestamp = headers.get("Date");
}
else {
timestamp = Rfc822Date.format(new Date());
headers.put("Date", timestamp);
}
RsaSignatureInput signatureBlock = new RsaSignatureInput();
signatureBlock.httpMethod = httpMethod;
signatureBlock.url = new HttpRequestURL(requestUrl,urlParams).toString();
signatureBlock.fingerprintBase64 = username;
signatureBlock.body = requestBody;
signatureBlock.signatureAlgorithm = credential.algorithm();
headers.put("X-Nonce", nonce);
headers.put("Date", timestamp);
signatureBlock.headers = headers;
signatureBlock.headerNames = new String[] { "X-Nonce", "Date" };
String content = signatureBlock.toString();
//log.debug("CUSTOMIZED signed content follows... ("+content.length()+") \n"+content);
byte[] signature = credential.signature(content.getBytes("UTF-8"));
// CUSTOMIZE: try null signature, empty string, or prepend/append/modify the text in signature; be aware that the base64 decoder may ignore characters that are not in a-zA-Z0-9+/ ; also be aware that if the user is not registered (found in database) then signature is ignored
String signatureBase64 = new String(Base64.encodeBase64(signature)); // CUSTOMIZE: .replace("x", "p"); will produce "Unauthorized: Authorization signature is invalid"
// CUSTOMIZE: try ABC123 or X509A instaed of X509. Should get "Unauthorized: Unsupported authorization scheme: ABC123"
String authorization = String.format("X509 %s", headerParams( realm, username, signatureBlock.headerNames, signatureBlock.signatureAlgorithm, signatureBase64));
log.debug("CUSTOMIZED authorization: "+authorization);
return authorization;
}
/**
* Generates a 24-byte nonce comprised of 8 bytes current time (milliseconds) and 16 bytes random data.
* @return
* @throws IOException if there was a problem generating the nonce
*/
private byte[] nonce() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
long currentTime = System.currentTimeMillis();
dos.writeLong(currentTime);
SecureRandom r = new SecureRandom();
byte[] nonce = new byte[16];
r.nextBytes(nonce);
dos.write(nonce);
dos.flush();
//byte[] noncedata = bos.toByteArray(); // should be 8 bytes timestamp + 16 bytes random numbers
//System.out.println("nonce data length = "+noncedata.length);
//assert noncedata.length == 24;
dos.close();
return bos.toByteArray();
}
/**
* Generates the parameters of the Authorization header.
* Sample output (with newlines for clarity - actual output is one line)
*
username="0685bd9184jfhq22",
httpMethod="GET",
uri="/reports/trust?hostName=example",
timestamp="2012-02-14T08:15:00PST",
nonce="4572616e48616d6d65724c61686176",
signature_method="HMAC-SHA256",
signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D"
*
* If any parameter is null, then it is not included in the output. The
* sample output above was generated with realm=null so the realm attribute
* was not included.
*
* @param httpMethod
* @param absoluteUrl
* @param fromToken
* @param nonce
* @param signatureMethod
* @param timestamp
* @return
*/
// realm, username, signatureBlock.headerNames, signatureBlock.signatureAlgorithm, signature
private String headerParams(String realm, String username, String[] headerNames, String signatureAlgorithm, String signatureBase64) {
String headerNamesCSV = StringUtils.join(headerNames, ",");
String[] input = new String[] { realm, username, headerNamesCSV, signatureAlgorithm, signatureBase64 };
String[] label = new String[] {"realm", "fingerprint", "headers", "algorithm", "signature"};
ArrayList<String> errors = new ArrayList<String>();
ArrayList<String> params = new ArrayList<String>();
for(int i=0; i<input.length; i++) {
if( input[i] != null && input[i].contains("\"") ) { errors.add(String.format("%s contains quotes", label[i])); }
if( input[i] != null ) { params.add(String.format("%s=\"%s\"", label[i], encodeHeaderAttributeValue(input[i]))); }
}
if( !errors.isEmpty() ) { throw new IllegalArgumentException("Cannot create authorization header: "+StringUtils.join(errors, ", ")); }
return StringUtils.join(params, ", ");
}
/**
* Encodes a string for use as an attribute value in the Authorization header.
* None of the values should include quotes. URL should be URL-encoded, and
* none of the other values allow quotes in their formats with the exception
* of "realm" and "username" which are application dependent but which we
* define as not including quotes.
* So, instead of encoding here, we throw errors (see headerParams function)
* when a value contains a quote or newline character.
* @param value
* @return
*/
private String encodeHeaderAttributeValue(String value) {
return value;
}
}
}