/* * Copyright (C) 2014 Intel Corporation * All rights reserved. */ package com.intel.mtwilson.shiro.authc.x509; import java.io.InputStream; import java.security.NoSuchAlgorithmException; import java.util.Calendar; import java.util.Date; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.time.DurationFormatUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.util.WebUtils; import com.intel.mtwilson.security.http.RsaSignatureInput; import com.intel.mtwilson.shiro.HttpAuthenticationFilter; import java.io.IOException; import java.security.MessageDigest; import java.util.HashMap; import java.util.Map; import org.apache.commons.codec.binary.Hex; import org.bouncycastle.asn1.x509.DigestInfo; import org.bouncycastle.asn1.DERObjectIdentifier; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; /** * * @author jbuhacoff */ public class X509AuthenticationFilter extends HttpAuthenticationFilter { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(X509AuthenticationFilter.class); private int expiresAfter = 60 * 60 * 1000; // 1 hour, in milliseconds, max is Integer.MAX_VALUE private final String headerAttributeNameValuePair = "([a-zA-Z0-9_-]+)=\"([^\"]+)\""; private final Pattern headerAttributeNameValuePairPattern = Pattern.compile(headerAttributeNameValuePair); public X509AuthenticationFilter() { super(); setAuthenticationScheme("X509"); } /** * Override the expiration window. Default is 1 hour. * * @param expiresAfter */ public void setExpiresAfter(int expiresAfter) { this.expiresAfter = expiresAfter; } public int getExpiresAfter() { return expiresAfter; } @Override protected AuthenticationToken createToken(ServletRequest request) { log.debug("createToken"); try { HttpServletRequest httpRequest = WebUtils.toHttp(request); Authorization authorization = getAuthorization(httpRequest); // clients MUST include a Date header in the request covered by a signature (we compare it to the anti-replay protection window) if(!ArrayUtils.contains(authorization.headerNames, "Date")) { throw new IllegalArgumentException("Request must include Date header"); } RsaSignatureInput signatureInput = getSignatureInputFromHttpRequest(httpRequest, authorization); String content = signatureInput.toString(); // may throw IllegalArgumentException if any required field is null or invalid // log.debug("Document content (signature input):\n'{}'\n", content); byte[] document = content.getBytes("UTF-8"); byte[] signature = Base64.decodeBase64(authorization.signatureBase64); String signatureAlgorithm = signatureAlgorithm(authorization.signatureAlgorithm); byte[] fingerprint = Base64.decodeBase64(authorization.fingerprintBase64); log.trace("Signature being added to token is {}", Hex.encodeHexString(signature)); log.trace("Fingerprint being added to token is {}", Hex.encodeHexString(fingerprint)); log.trace("Document being added to token is {}", Hex.encodeHexString(document)); byte[] digest = getDigest(document, signatureAlgorithm); // example: 3031300d060960864801650304020105000420 8373ed7ae4a499534f3eb02fb898a0eafea48a334e2f0a5703e7dc474360786a the space between the two hex parts shows where the alg id ends and the sha256 digest of the document itself begins log.debug("Digest with alg id included is: {}", Hex.encodeHexString(digest)); X509AuthenticationToken token = new X509AuthenticationToken(new Fingerprint(fingerprint), new Credential(signature, digest), signatureInput, request.getRemoteAddr()); log.debug("createToken: returning X509AuthenticationToken"); return token; } catch (IOException | NoSuchAlgorithmException e) { throw new AuthenticationException("Cannot authenticate request: " + e.getMessage(), e); } } private String getRequestBody(HttpServletRequest httpRequest) throws IOException { log.debug("Reading request body"); // get the request body (even if empty) - the input stream must be repeatable // so the endpoint will be able to read it again for processing the request InputStream in = httpRequest.getInputStream(); if (!in.markSupported()) { throw new IOException("Request input stream is not repeatable; evaluating X509 authorization would prevent further processing of request"); } String requestBody = IOUtils.toString(in); in.reset(); // to allow other filters or servlets to process the request return requestBody; } private Map<String, String> getRequestHeaders(HttpServletRequest httpRequest, String[] headerNames) { HashMap<String, String> headerValues = new HashMap<>(); for (String headerName : headerNames) { headerValues.put(headerName, httpRequest.getHeader(headerName)); } return headerValues; } private Authorization getAuthorization(HttpServletRequest httpRequest) { String authorizationText = httpRequest.getHeader(getAuthorizationHeaderName()); log.debug("Parsing authorization header: {}", authorizationText); Authorization authorization = parseAuthorization(authorizationText); log.info("X509CertificateAuthorization: Request timestamp ok"); return authorization; } /** * Example signature document: Request: GET * https://10.1.71.56:8443/mtwilson/v2/WLMService/resources/oem Realm: From: * Ca0ES/b4gqW6aExUoCvSOxb68fOIqrN9dPhYUmZImFM= Signature-Algorithm: * SHA256withRSA X-Nonce: AAABRQ9M90Y56zLFR/0hc8B6LDB+qO3r Date: Sat, 29 Mar * 2014 12:24:33 PDT * * * * @param httpRequest * @param a * @return * @throws IOException */ private RsaSignatureInput getSignatureInputFromHttpRequest(HttpServletRequest httpRequest, Authorization a) throws IOException { RsaSignatureInput signatureInput = new RsaSignatureInput(); signatureInput.httpMethod = httpRequest.getMethod(); signatureInput.url = getURL(httpRequest); // protocol, host, port, path, and query string as sent by client signatureInput.realm = a.realm; signatureInput.fingerprintBase64 = a.fingerprintBase64; signatureInput.signatureAlgorithm = a.signatureAlgorithm; signatureInput.headerNames = a.headerNames; signatureInput.headers = getRequestHeaders(httpRequest, a.headerNames); signatureInput.body = getRequestBody(httpRequest); // throws IOException if error on read or if InputStream is not repeatable; log.debug("signature input body is {} bytes.", signatureInput.body.length()); return signatureInput; } private String getURL(HttpServletRequest httpRequest) { String url = httpRequest.getRequestURL().toString(); String query = httpRequest.getQueryString(); if (query == null) { query = ""; } log.debug("request URL is {} with query string {}", url, query); String queryDelimiter = query.isEmpty() ? "" : "?"; return url + queryDelimiter + query; } private byte[] getDigest(byte[] document, String signatureAlgorithm) throws NoSuchAlgorithmException, IOException { log.debug("Signature algorithm {}", signatureAlgorithm); String digestAlgorithm = getDigestAlgorithm(signatureAlgorithm); log.debug("Digest algorithm {}", digestAlgorithm); String oid = getOidForAlgorithm(digestAlgorithm); log.debug("OID for {} is {}", digestAlgorithm, oid); // compute the digest of the document using the digest algorithm MessageDigest md = MessageDigest.getInstance(digestAlgorithm); // like SHA1; throws NoSuchAlgorithmException byte[] digest = md.digest(document); log.debug("Document digest is {}", Hex.encodeHexString(digest)); // java format for the digest is algorithm oid followed by the hash AlgorithmIdentifier algId = new AlgorithmIdentifier(new DERObjectIdentifier(oid), null); DigestInfo digestInfo = new DigestInfo(algId, digest); return digestInfo.getEncoded(); // throws IOException } // reference: http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html private String getDigestAlgorithm(String signatureAlgorithm) { if ("SHA1withRSA".equals(signatureAlgorithm)) { return "SHA-1"; } if ("SHA256withRSA".equals(signatureAlgorithm)) { return "SHA-256"; } if ("SHA384withRSA".equals(signatureAlgorithm)) { return "SHA-384"; } if ("SHA512withRSA".equals(signatureAlgorithm)) { return "SHA-512"; } throw new IllegalArgumentException("Unknown signature algorithm " + signatureAlgorithm); } private String getOidForAlgorithm(String digestAlgorithm) { // if( "MD5".equalsIgnoreCase(algorithm) ) { // return "1.2.840.113549.2.5"; // } if ("SHA-1".equalsIgnoreCase(digestAlgorithm) || "SHA1".equalsIgnoreCase(digestAlgorithm)) { return "1.3.14.3.2.26"; } // if( "SHA1withRSA".equalsIgnoreCase(algorithm) ) { // return "1.3.14.3.2.29"; // } if ("SHA-256".equalsIgnoreCase(digestAlgorithm) || "SHA256".equalsIgnoreCase(digestAlgorithm)) { return "2.16.840.1.101.3.4.2.1"; } if ("SHA-384".equalsIgnoreCase(digestAlgorithm) || "SHA384".equalsIgnoreCase(digestAlgorithm)) { return "2.16.840.1.101.3.4.2.2"; } if ("SHA-512".equalsIgnoreCase(digestAlgorithm) || "SHA512".equalsIgnoreCase(digestAlgorithm)) { return "2.16.840.1.101.3.4.2.3"; } throw new IllegalArgumentException("Unknown OID for algorithm " + digestAlgorithm); } /** * Standardizes signature algorithm names to the Java name. * "SHA256withRSA".equals(signatureAlgorithm("RSA-SHA256")); // true * * @param name * @return */ private String signatureAlgorithm(String name) { if ("RSA-SHA256".equals(name)) { return "SHA256withRSA"; } return name; } /** * * 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; } /** * 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; } }