Author: ieb Date: Thu Aug 15 18:02:06 2013 New Revision: 1514417 URL: http://svn.apache.org/r1514417 Log: Experimenting with sealing requests and responses with a hmac
Added: sling/whiteboard/ieb/sealed/ sling/whiteboard/ieb/sealed/pom.xml (with props) sling/whiteboard/ieb/sealed/src/ sling/whiteboard/ieb/sealed/src/main/ sling/whiteboard/ieb/sealed/src/main/java/ sling/whiteboard/ieb/sealed/src/main/java/org/ sling/whiteboard/ieb/sealed/src/main/java/org/apache/ sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/ sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/ sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealedResponse.java (with props) sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealingFilter.java (with props) Added: sling/whiteboard/ieb/sealed/pom.xml URL: http://svn.apache.org/viewvc/sling/whiteboard/ieb/sealed/pom.xml?rev=1514417&view=auto ============================================================================== --- sling/whiteboard/ieb/sealed/pom.xml (added) +++ sling/whiteboard/ieb/sealed/pom.xml Thu Aug 15 18:02:06 2013 @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.sling</groupId> + <artifactId>sling</artifactId> + <version>16</version> + </parent> + + <artifactId>org.apache.sling.sealed</artifactId> + <version>0.0.1-SNAPSHOT</version> + <packaging>bundle</packaging> + + <name>Apache Sling Sealer</name> + + <scm> + <connection>scm:svn:http://svn.apache.org/repos/asf/sling/whiteboard/ieb/sealed</connection> + <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/whiteboard/ieb/sealed</developerConnection> + <url>http://svn.apache.org/viewvc/sling/whiteboard/ieb/sealed</url> + </scm> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-scr-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Export-Package> + </Export-Package> + <Private-Package> + org.apache.sling.sealed.*, + </Private-Package> + </instructions> + </configuration> + </plugin> + </plugins> + </build> + <dependencies> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.api</artifactId> + <version>2.0.8</version> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.core</artifactId> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.compendium</artifactId> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>2.4</version> + </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + <version>1.5</version> + </dependency> + <!-- Testing --> + <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.commons.testing</artifactId> + <version>2.0.4-incubator</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <version>1.9.5</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.scr.annotations</artifactId> + </dependency> + </dependencies> +</project> Propchange: sling/whiteboard/ieb/sealed/pom.xml ------------------------------------------------------------------------------ svn:eol-style = native Propchange: sling/whiteboard/ieb/sealed/pom.xml ------------------------------------------------------------------------------ svn:mime-type = text/xml Added: sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealedResponse.java URL: http://svn.apache.org/viewvc/sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealedResponse.java?rev=1514417&view=auto ============================================================================== --- sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealedResponse.java (added) +++ sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealedResponse.java Thu Aug 15 18:02:06 2013 @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The SF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.apache.sling.sealed; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper; + +/** + * Captures a response, and makes part of it available for signing when the response is sent. + */ +public class SealedResponse extends SlingHttpServletResponseWrapper { + + private ServletOutputStream outputStream; + private ByteArrayOutputStream byteOutputStream; + private PrintWriter writer; + private Map<String, String> headers = new HashMap<String, String>(); + + public SealedResponse(ServletRequest request, ServletResponse response) { + super(getResponseToWrap(response)); + } + + private static SlingHttpServletResponse getResponseToWrap(ServletResponse response) { + try { + return (SlingHttpServletResponse) response; + } catch ( ClassCastException e ) { + throw new IllegalArgumentException(); + } + } + + @Override + public void addHeader(String name, String value) { + headers.put(name, value); + super.addHeader(name, value); + } + + @Override + public void setHeader(String name, String value) { + headers.put(name, value); + super.setHeader(name, value); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + if ( writer != null ) { + throw new IllegalStateException("Writer has been retrieved already"); + } + if ( outputStream == null ) { + byteOutputStream = new ByteArrayOutputStream(); + outputStream = new ServletOutputStream() { + + @Override + public void write(int arg0) throws IOException { + byteOutputStream.write(arg0); + } + }; + } + return outputStream; + } + + @Override + public PrintWriter getWriter() throws IOException { + if ( outputStream != null ) { + throw new IllegalStateException("OutputStream has been retrieved already"); + } + if ( writer == null ) { + byteOutputStream = new ByteArrayOutputStream(); + writer = new PrintWriter(byteOutputStream); + } + return writer; + } + + /** + * Seal and commit the response. + * @param sealingFilter + * @throws IOException + */ + public void seal(SealingFilter sealingFilter) throws IOException { + super.setHeader(SealingFilter.X_SIG_HEADER, sealingFilter.getHmac(byteOutputStream, headers)); + super.getOutputStream().write(byteOutputStream.toByteArray()); + } + + +} Propchange: sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealedResponse.java ------------------------------------------------------------------------------ svn:eol-style = native Added: sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealingFilter.java URL: http://svn.apache.org/viewvc/sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealingFilter.java?rev=1514417&view=auto ============================================================================== --- sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealingFilter.java (added) +++ sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealingFilter.java Thu Aug 15 18:02:06 2013 @@ -0,0 +1,252 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The SF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.apache.sling.sealed; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Properties; +import org.apache.felix.scr.annotations.Property; +import org.apache.felix.scr.annotations.Service; + +@Service(value = Filter.class) +@Component(immediate = true, metatype = true) +@Properties(@Property(name = "path", value = "/sealed")) +public class SealingFilter implements Filter { + + public static final String X_SIG_HEADER = "x-sig"; + + private static final String HMAC_ALG = "HmacSHA256"; + + @Property(cardinality = Integer.MAX_VALUE) + private static final String PROP_PATTERNS = "sealed-paths"; + + @Property(cardinality= Integer.MAX_VALUE) + private static final String PROP_HEADERNAMES = "sealed-headers"; + + @Property + private static final String PROP_SHARED_KEY = "shared-key"; + + + + private Pattern[] pathPatterns; + + private String[] headerNames; + + private Key key; + + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Activate + public void activate(Map<String, Object> properties) { + pathPatterns = buildPatterns((String[]) properties.get(PROP_PATTERNS)); + headerNames = buildHeaderNames((String[]) properties.get(PROP_HEADERNAMES)); + key = buildKey((String) properties.get(PROP_SHARED_KEY)); + } + + private Key buildKey(String sharedKey) { + if ( sharedKey != null ) { + return new SecretKeySpec(sharedKey.getBytes(), HMAC_ALG); + } + return null; + + } + + private String[] buildHeaderNames(String[] names) { + if ( names != null ) { + return names; + } + return new String[0]; + } + + private Pattern[] buildPatterns(String[] patterns) { + if (patterns != null) { + Pattern[] p = new Pattern[patterns.length]; + for ( int i =0;i< p.length; i++ ) { + p[i] = Pattern.compile(patterns[i]); + } + return p; + } + return new Pattern[0]; + } + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (!verify(request) && response instanceof HttpServletResponse) { + ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN, + "Untrusted request"); + } else { + if (secure(request)) { + SealedResponse sealedResponse = new SealedResponse(request, response); + chain.doFilter(request, sealedResponse); + sealedResponse.seal(this); + } else { + chain.doFilter(request, response); + } + } + } + + /** + * Should the request and response be secure and trusted ? + * + * @param request + * @return + */ + private boolean secure(ServletRequest request) { + if (key != null && request instanceof HttpServletRequest) { + return match(((HttpServletRequest) request).getRequestURI(), pathPatterns); + } + return false; + } + + /** + * Verify that the trusted parts of the servlet request have not been + * tampered with. + * + * @param request + * @return true if the request has not been tampered with. + * @throws IOException + */ + private boolean verify(ServletRequest request) throws IOException { + if (request instanceof HttpServletRequest) { + HttpServletRequest hrequest = (HttpServletRequest) request; + if (secure(hrequest)) { + return verifyHmac(buildMessage(hrequest), hrequest.getHeader(X_SIG_HEADER)); + } + } + return true; + } + + /** + * verify a message against a hmac. + * + * @param message the message + * @param hmac the hmac + * @return true if the message hasn't been tampered with and the hmac was + * created with the same key. + */ + private boolean verifyHmac(String message, String hmac) { + String newMac = doMac(message); + return (hmac != null && newMac != null && !hmac.equals(newMac)); + } + + /** + * Perform a hmac on a message. + * + * @param message the message + * @return the hmac of the message or null if the hmac cant be computed. + */ + private String doMac(String message) { + try { + Mac mac = Mac.getInstance(HMAC_ALG); + mac.init(key); + return new String(Base64.encodeBase64(mac.doFinal(message.getBytes("UTF-8"))), "UTF-8"); + } catch (UnsupportedEncodingException e) { + } catch (IllegalStateException e) { + } catch (NoSuchAlgorithmException e) { + } catch (InvalidKeyException e) { + } + return null; + } + + /** + * Build a message from the request + * + * @param request + * @return + * @throws IOException + */ + private String buildMessage(HttpServletRequest request) throws IOException { + StringBuilder message = new StringBuilder(); + for (String headerName : headerNames) { + message.append(request.getHeader(headerName)); + } + ServletInputStream in = request.getInputStream(); + in.mark(1024 * 1024); + message.append(new String(Base64.encodeBase64(IOUtils.toByteArray(in)), "UTF-8")); + in.reset(); + // not certain what will happen if I close in + in.close(); + return message.toString(); + } + + private String buildMesssage(ByteArrayOutputStream byteOutputStream, Map<String, String> headers) + throws UnsupportedEncodingException { + StringBuilder message = new StringBuilder(); + for (String headerName : headerNames) { + message.append(headers.get(headerName)); + } + message.append(new String(Base64.encodeBase64(byteOutputStream.toByteArray()), "UTF-8")); + return message.toString(); + } + + /** + * return true if the test matches any of the patterns. + * + * @param test + * @param patterns + * @return + */ + private boolean match(String test, Pattern[] patterns) { + for (Pattern urls : patterns) { + if (urls.matcher(test).find()) { + return true; + } + } + return false; + } + + public void destroy() { + } + + /** + * Get a hmac from a captured response. + * @param byteOutputStream + * @param headers + * @return + * @throws UnsupportedEncodingException + */ + public String getHmac(ByteArrayOutputStream byteOutputStream, Map<String, String> headers) + throws UnsupportedEncodingException { + return doMac(buildMesssage(byteOutputStream, headers)); + } + +} Propchange: sling/whiteboard/ieb/sealed/src/main/java/org/apache/sling/sealed/SealingFilter.java ------------------------------------------------------------------------------ svn:eol-style = native