org.springframework.security.web.header.writers.HpkpHeaderWriter Maven / Gradle / Ivy
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed 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.springframework.security.web.header.writers;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.header.HeaderWriter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Provides support for HTTP Public Key Pinning (HPKP).
*
*
* Since Section 4.1 states
* that a value on the order of 60 days (5,184,000 seconds) may be considered a good balance,
* we use this value as the default. This can be customized using {@link #setMaxAgeInSeconds(long)}.
*
*
*
* Because Appendix B recommends
* that operators should first deploy public key pinning by using the report-only mode,
* we opted to use this mode as default. This can be customized using {@link #setReportOnly(boolean)}.
*
*
*
* Since we need to validate a certificate chain, the "Public-Key-Pins" or "Public-Key-Pins-Report-Only" header
* will only be added when {@link HttpServletRequest#isSecure()} returns {@code true}.
*
*
*
* To set the pins you first need to extract the public key information from your certificate or key file
* and encode them using Base64. The following commands will help you extract the Base64 encoded information
* from a key file, a certificate signing request, or a certificate.
*
*
* openssl rsa -in my-key-file.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64
*
* openssl req -in my-signing-request.csr -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
*
* openssl x509 -in my-certificate.crt -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
*
*
*
* The following command will extract the Base64 encoded information for a website.
*
*
* openssl s_client -servername www.example.com -connect www.example.com:443 | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
*
*
*
*
* Some examples:
*
*
* Public-Key-Pins: max-age=3000;
* pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=";
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="
*
* Public-Key-Pins: max-age=5184000;
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
* pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="
*
* Public-Key-Pins: max-age=5184000;
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
* pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=";
* report-uri="http://example.com/pkp-report"
*
* Public-Key-Pins-Report-Only: max-age=5184000;
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
* pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=";
* report-uri="https://other.example.net/pkp-report"
*
* Public-Key-Pins: max-age=5184000;
* pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=";
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
* pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=";
* includeSubDomains
*
*
*
* @author Tim Ysewyn
* @since 4.1
*/
public final class HpkpHeaderWriter implements HeaderWriter {
private static final long DEFAULT_MAX_AGE_SECONDS = 5184000;
private static final String HPKP_HEADER_NAME = "Public-Key-Pins";
private static final String HPKP_RO_HEADER_NAME = "Public-Key-Pins-Report-Only";
private final Log logger = LogFactory.getLog(getClass());
private final RequestMatcher requestMatcher = new SecureRequestMatcher();
private Map pins = new LinkedHashMap<>();
private long maxAgeInSeconds;
private boolean includeSubDomains;
private boolean reportOnly;
private URI reportUri;
private String hpkpHeaderValue;
/**
* Creates a new instance
*
* @param maxAgeInSeconds maps to {@link #setMaxAgeInSeconds(long)}
* @param includeSubDomains maps to {@link #setIncludeSubDomains(boolean)}
* @param reportOnly maps to {@link #setReportOnly(boolean)}
*/
public HpkpHeaderWriter(long maxAgeInSeconds, boolean includeSubDomains, boolean reportOnly) {
this.maxAgeInSeconds = maxAgeInSeconds;
this.includeSubDomains = includeSubDomains;
this.reportOnly = reportOnly;
updateHpkpHeaderValue();
}
/**
* Creates a new instance
*
* @param maxAgeInSeconds maps to {@link #setMaxAgeInSeconds(long)}
* @param includeSubDomains maps to {@link #setIncludeSubDomains(boolean)}
*/
public HpkpHeaderWriter(long maxAgeInSeconds, boolean includeSubDomains) {
this(maxAgeInSeconds, includeSubDomains, true);
}
/**
* Creates a new instance
*
* @param maxAgeInSeconds maps to {@link #setMaxAgeInSeconds(long)}
*/
public HpkpHeaderWriter(long maxAgeInSeconds) {
this(maxAgeInSeconds, false);
}
/**
* Creates a new instance
*/
public HpkpHeaderWriter() {
this(DEFAULT_MAX_AGE_SECONDS);
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.web.headers.HeaderWriter#writeHeaders(javax
* .servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (requestMatcher.matches(request)) {
if (!pins.isEmpty()) {
response.setHeader(reportOnly ? HPKP_RO_HEADER_NAME : HPKP_HEADER_NAME, hpkpHeaderValue);
} if (logger.isDebugEnabled()) {
logger.debug("Not injecting HPKP header since there aren't any pins");
}
}
else if (logger.isDebugEnabled()) {
logger.debug("Not injecting HPKP header since it wasn't a secure connection");
}
}
/**
*
* Sets the value for the pin- directive of the Public-Key-Pins header.
*
*
*
* The pin directive specifies a way for web host operators to indicate
* a cryptographic identity that should be bound to a given web host.
* See Section 2.1.1 for additional details.
*
*
*
* To get a pin of
*
* Public-Key-Pins:
* pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=";
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="
*
* Use
*
*
* Map<String, String> pins = new HashMap<String, String>();
* pins.put("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "sha256");
* pins.put("E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=", "sha256");
*
*
*
* @param pins the map of base64-encoded SPKI fingerprint & cryptographic hash algorithm pairs.
* @throws IllegalArgumentException if pins is null
*/
public void setPins(Map pins) {
Assert.notNull(pins, "pins cannot be null");
this.pins = pins;
updateHpkpHeaderValue();
}
/**
*
* Adds a list of SHA256 hashed pins for the pin- directive of the Public-Key-Pins header.
*
*
*
* The pin directive specifies a way for web host operators to indicate
* a cryptographic identity that should be bound to a given web host.
* See Section 2.1.1 for additional details.
*
*
*
* To get a pin of
*
* Public-Key-Pins-Report-Only:
* pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=";
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="
*
* Use
*
* HpkpHeaderWriter hpkpHeaderWriter = new HpkpHeaderWriter();
* hpkpHeaderWriter.addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=");
*
*
* @param pins a list of base64-encoded SPKI fingerprints.
* @throws IllegalArgumentException if a pin is null
*/
public void addSha256Pins(String ... pins) {
for (String pin : pins) {
Assert.notNull(pin, "pin cannot be null");
this.pins.put(pin, "sha256");
}
updateHpkpHeaderValue();
}
/**
*
* Sets the value (in seconds) for the max-age directive of the Public-Key-Pins header. The default is 60 days.
*
*
*
* This instructs browsers how long they should regard the host (from whom the message was received)
* as a known pinned host. See Section
* 2.1.2 for additional details.
*
*
*
* To get a header like
*
* Public-Key-Pins-Report-Only: max-age=2592000;
* pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=";
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="
*
* Use
*
* HpkpHeaderWriter hpkpHeaderWriter = new HpkpHeaderWriter();
* hpkpHeaderWriter.setMaxAgeInSeconds(TimeUnit.DAYS.toSeconds(30));
*
*
* @param maxAgeInSeconds the maximum amount of time (in seconds) to regard the host
* as a known pinned host. (i.e. TimeUnit.DAYS.toSeconds(30) would set this to 30 days)
* @throws IllegalArgumentException if maxAgeInSeconds is negative
*/
public void setMaxAgeInSeconds(long maxAgeInSeconds) {
if (maxAgeInSeconds < 0) {
throw new IllegalArgumentException(
"maxAgeInSeconds must be non-negative. Got " + maxAgeInSeconds);
}
this.maxAgeInSeconds = maxAgeInSeconds;
updateHpkpHeaderValue();
}
/**
*
* If true, the pinning policy applies to this pinned host as well as any subdomains
* of the host's domain name. The default is false.
*
*
*
* See Section 2.1.3
* for additional details.
*
*
*
* To get a header like
*
* Public-Key-Pins-Report-Only: max-age=5184000;
* pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=";
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
* includeSubDomains
*
* you should set this to true.
*
*
* @param includeSubDomains true to include subdomains, else false
*/
public void setIncludeSubDomains(boolean includeSubDomains) {
this.includeSubDomains = includeSubDomains;
updateHpkpHeaderValue();
}
/**
*
* To get a Public-Key-Pins header you should set this to false,
* otherwise the header will be Public-Key-Pins-Report-Only. When in report-only mode,
* the browser should not terminate the connection with the server. By default this is true.
*
*
*
* See Section 2.1
* for additional details.
*
*
*
* To get a header like
*
* Public-Key-Pins: max-age=5184000;
* pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=";
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="
*
* you should the this to false.
*
*
* @param reportOnly true to report only, else false
*/
public void setReportOnly(boolean reportOnly) {
this.reportOnly = reportOnly;
}
/**
*
* Sets the URI to which the browser should report pin validation failures.
*
*
*
* See Section 2.1.4
* for additional details.
*
*
*
* To get a header like
*
* Public-Key-Pins-Report-Only: max-age=5184000;
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
* pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=";
* report-uri="https://other.example.net/pkp-report"
*
* Use
*
* HpkpHeaderWriter hpkpHeaderWriter = new HpkpHeaderWriter();
* hpkpHeaderWriter.setReportUri(new URI("https://other.example.net/pkp-report"));
*
*
* @param reportUri the URI where the browser should send the report to.
*/
public void setReportUri(URI reportUri) {
this.reportUri = reportUri;
updateHpkpHeaderValue();
}
/**
*
* Sets the URI to which the browser should report pin validation failures.
*
*
*
* See Section 2.1.4
* for additional details.
*
*
*
* To get a header like
*
* Public-Key-Pins-Report-Only: max-age=5184000;
* pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
* pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=";
* report-uri="https://other.example.net/pkp-report"
*
* Use
*
* HpkpHeaderWriter hpkpHeaderWriter = new HpkpHeaderWriter();
* hpkpHeaderWriter.setReportUri("https://other.example.net/pkp-report");
*
*
* @param reportUri the URI where the browser should send the report to.
* @throws IllegalArgumentException if the reportUri is not a valid URI
*/
public void setReportUri(String reportUri) {
try {
this.reportUri = new URI(reportUri);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
updateHpkpHeaderValue();
}
private void updateHpkpHeaderValue() {
String headerValue = "max-age=" + maxAgeInSeconds;
for (Map.Entry pin : pins.entrySet()) {
headerValue += " ; pin-" + pin.getValue() + "=\"" + pin.getKey() + "\"";
}
if (reportUri != null) {
headerValue += " ; report-uri=\"" + reportUri.toString() + "\"";
}
if (includeSubDomains) {
headerValue += " ; includeSubDomains";
}
this.hpkpHeaderValue = headerValue;
}
private static final class SecureRequestMatcher implements RequestMatcher {
public boolean matches(HttpServletRequest request) {
return request.isSecure();
}
}
}