All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.security.web.header.writers.HpkpHeaderWriter Maven / Gradle / Ivy

There is a newer version: 6.2.4
Show newest version
/*
 * 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(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy