package com.intel.mtwilson.security.http; import com.intel.dcsg.cpg.crypto.RsaCredential; import com.intel.dcsg.cpg.rfc822.Rfc822Date; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.SignatureException; import java.util.ArrayList; import java.util.Date; import java.util.Map; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * RSA-based signatures for Mt Wilson 0.5.2 in Authorization header using custom "PublicKey" and "X509" scheme * This class requires the following libraries: * org.apache.commons.codec.binary.Base64 from commons-codec * org.apache.commons.lang.StringUtils from commons-lang * @since 0.5.2 * @author jbuhacoff */ public class RsaAuthorization { private static Logger log = LoggerFactory.getLogger(RsaAuthorization.class); private RsaCredential credential; private String realm = null; public RsaAuthorization(RsaCredential credential) { this.credential = credential; } public void setRealm(String realmName) { realm = realmName; } /** * Use this method to sign requests that do not have a message body, * such as GET and DELETE. * @param httpMethod the string "GET" or "DELETE" * @param requestUrl the URL to access * @return content of the Authorization header to add to the request */ public String getAuthorizationQuietly(String httpMethod, String requestUrl, Map<String,String> headers) throws SignatureException { return getAuthorizationQuietly(httpMethod, requestUrl, null, headers, null); } /** * Use this method to sign requests that have a message body, * such as PUT and POST. You can also use it to sign requests with * no message body by passing null for the requestBody parameter. * @param httpMethod the string "GET" or "DELETE" * @param requestUrl the URL to access * @param requestBody the body of the request * @return content of the Authorization header to add to the request */ public String getAuthorizationQuietly(String httpMethod, String requestUrl, Map<String,String> headers, String requestBody) throws SignatureException { return getAuthorizationQuietly(httpMethod, requestUrl, null, headers, requestBody); } /** * Use this method to sign requests that do have a message body and URL * parameters. * @param httpMethod the string "GET" or "DELETE" * @param requestUrl the URL to access * @return content of the Authorization header to add to the request */ 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; } /** * Generates the content for the Authorization header, using the * Signature format: client-token : nonce : signature * * @param httpMethod such as "GET" or "POST" * @param requestUrl complete URL such as https://example.com/some/path * @return * @throws NoSuchAlgorithmException if your environment is missing the HmacSHA256 algorithm * @throws InvalidKeyException if the secretKey value you provided to the constructor is not suitable for use with HmacSHA256 * @throws IOException if there was a problem generating the nonce */ public String getAuthorization(String httpMethod, String requestUrl, Map<String,String> headers) throws NoSuchAlgorithmException, InvalidKeyException, IOException, SignatureException { return getAuthorization(httpMethod, requestUrl, null, headers, null); } /** * Generates the content for the Authorization header, using the * Signature format: client-token : nonce : signature * * @param httpMethod such as "GET" or "POST" * @param requestUrl complete URL such as https://example.com/some/path * @param requestBody only required if you are sending POST or PUT message body; can be null (will be converted to empty string) * @return * @throws NoSuchAlgorithmException if your environment is missing the HmacSHA256 algorithm * @throws InvalidKeyException if the secretKey value you provided to the constructor is not suitable for use with HmacSHA256 * @throws IOException if there was a problem generating the nonce */ public String getAuthorization(String httpMethod, String requestUrl, Map<String,String> headers, String requestBody) throws NoSuchAlgorithmException, InvalidKeyException, IOException, SignatureException { return getAuthorization(httpMethod, requestUrl, null, headers, requestBody); } /** * Generates the content for the Authorization header, using the * Signature format: client-token : nonce : signature * * @param httpMethod such as "GET" or "POST" * @param requestUrl complete URL such as https://example.com/some/path * @param urlParams a key-value map representing url parameters; the value can be null, or a single string value, a string array, or a list of strings * @return * @throws NoSuchAlgorithmException if your environment is missing the HmacSHA256 algorithm * @throws InvalidKeyException if the secretKey value you provided to the constructor is not suitable for use with HmacSHA256 * @throws IOException if there was a problem generating the nonce */ public String getAuthorization(String httpMethod, String requestUrl, Map<String,Object> urlParams, Map<String,String> headers) throws NoSuchAlgorithmException, InvalidKeyException, IOException, SignatureException { return getAuthorization(httpMethod, requestUrl, urlParams, headers, null); } /** * Generates the content for the Authorization header, using the authentication * scheme "PublicKey" or "X509". * * The nonce and identity/fingerprint are base64-encoded with no wrapping/chunking. * Implementation note: must use new String(Base64.encodeBase64(...)) to * avoid the wrapping. The Base.encodeBase64String automatically wraps. * * If the urlParams are given, they will be added to the request URL in * ascending alphabetical order. If any parameter is multi-valued, its values * will be listed in ascending alphabetical order. * * @param httpMethod such as "GET" or "POST" * @param requestUrl complete URL such as https://example.com/some/path or /some/path * @param urlParams a Map containing keys of type String and values of either type String or String[] * @param headers a Map which is used for INPUT OR OUTPUT, it will contain the "Date" header to be added to the request; or if it already contains a "Date" header that date will be used. Also the nonce will be added to this map with the key "X-Nonce". Both "Date" and "X-Nonce" must be added to the http request and sent to the server! * @param requestBody only required if you are sending POST or PUT message body; can be null (will be converted to empty string) * @return * @throws NoSuchAlgorithmException if your environment is missing the HmacSHA256 algorithm * @throws InvalidKeyException if the secretKey value you provided to the constructor is not suitable for use with HmacSHA256 * @throws IOException if there was a problem generating the nonce */ 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"); log.debug("request already contains date header: {}", timestamp); } else { timestamp = Rfc822Date.format(new Date()); log.debug("creating new date header for request: {}", timestamp); headers.put("Date", timestamp); } RsaSignatureInput signatureBlock = new RsaSignatureInput(); signatureBlock.httpMethod = httpMethod; signatureBlock.url = new HttpRequestURL(requestUrl,urlParams).toString(); signatureBlock.fingerprintBase64 = username; signatureBlock.body = requestBody; log.debug("signature input body is {} bytes.", (signatureBlock.body==null?0:signatureBlock.body.length())); signatureBlock.signatureAlgorithm = credential.algorithm(); signatureBlock.headers = headers; signatureBlock.headerNames = new String[] { "X-Nonce", "Date" }; String content = signatureBlock.toString(); log.debug("signed content is {} bytes.", content.length()); byte[] signature = credential.signature(content.getBytes("UTF-8")); String signatureBase64 = new String(Base64.encodeBase64(signature)); // right now we're sending it as "X509" instead of "PublicKey" because // we're using a certificate generated by java keytool and the signature // includes the SHA256withRSA algorithm OID, which is a java specific format // for rsa signatures, and pure public key signatures don't have that. String authorization = String.format("X509 %s", headerParams( realm, username, signatureBlock.headerNames, signatureBlock.signatureAlgorithm, signatureBase64)); //log.debug("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<>(); ArrayList<String> params = new ArrayList<>(); 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; } }