diff --git a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java index 1294c36908..36844c6f2b 100644 --- a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -85,8 +85,8 @@ public final class SecurityNamespaceHandler implements NamespaceHandler { public BeanDefinition parse(Element element, ParserContext pc) { if (!namespaceMatchesVersion(element)) { pc.getReaderContext() - .fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or spring-security-3.1.xsd schema or spring-security-3.2.xsd schema " - + "with Spring Security 4.0. Please update your schema declarations to the 4.0 schema.", + .fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or spring-security-3.1.xsd schema or spring-security-3.2.xsd schema or spring-security-4.0.xsd schema " + + "with Spring Security 4.1. Please update your schema declarations to the 4.1 schema.", element); } String name = pc.getDelegate().getLocalName(element); @@ -222,7 +222,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler { private boolean matchesVersionInternal(Element element) { String schemaLocation = element.getAttributeNS( "http://www.w3.org/2001/XMLSchema-instance", "schemaLocation"); - return schemaLocation.matches("(?m).*spring-security-4\\.0.*.xsd.*") + return schemaLocation.matches("(?m).*spring-security-4\\.1.*.xsd.*") || schemaLocation.matches("(?m).*spring-security.xsd.*") || !schemaLocation.matches("(?m).*spring-security.*"); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index 9334b04259..621e90b94b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * 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. @@ -15,8 +15,10 @@ */ package org.springframework.security.config.annotation.web.configurers; +import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Map; import javax.servlet.http.HttpServletRequest; @@ -26,6 +28,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.web.header.HeaderWriter; import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.header.writers.CacheControlHeadersWriter; +import org.springframework.security.web.header.writers.HpkpHeaderWriter; import org.springframework.security.web.header.writers.HstsHeaderWriter; import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter; import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter; @@ -41,7 +44,7 @@ import org.springframework.util.Assert; *
* *- * The default headers are include are: + * The default headers include are: *
* *@@ -55,6 +58,7 @@ import org.springframework.util.Assert; ** * @author Rob Winch + * @author Tim Ysewyn * @since 3.2 */ public class HeadersConfigurer
+ * 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. + *
+ * + * @param pins the map of base64-encoded SPKI fingerprint & cryptographic hash algorithm pairs. + * @throws IllegalArgumentException if pins is null + */ + public HpkpConfig withPins(Map+ * 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. + *
+ * + * @param pins a list of base64-encoded SPKI fingerprints. + * @throws IllegalArgumentException if a pin is null + */ + public HpkpConfig addSha256Pins(String ... pins) { + writer.addSha256Pins(pins); + return this; + } + + /** + *+ * 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. + *
+ * + * @param maxAgeInSeconds the maximum amount of time (in seconds) to regard the host + * as a known pinned host. + * @throws IllegalArgumentException if maxAgeInSeconds is negative + */ + public HpkpConfig maxAgeInSeconds(long maxAgeInSeconds) { + writer.setMaxAgeInSeconds(maxAgeInSeconds); + return this; + } + + /** + *+ * 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. + *
+ * + * @param includeSubDomains true to include subdomains, else false + */ + public HpkpConfig includeSubDomains(boolean includeSubDomains) { + writer.setIncludeSubDomains(includeSubDomains); + return this; + } + + /** + *+ * If true, the browser should not terminate the connection with the server. The default is true. + *
+ * + *+ * See Section 2.1 + * for additional details. + *
+ * + * @param reportOnly true to report only, else false + */ + public HpkpConfig reportOnly(boolean reportOnly) { + writer.setReportOnly(reportOnly); + return this; + } + + /** + *+ * Sets the URI to which the browser should report pin validation failures. + *
+ * + *+ * See Section 2.1.4 + * for additional details. + *
+ * + * @param reportUri the URI where the browser should send the report to. + */ + public HpkpConfig reportUri(URI reportUri) { + writer.setReportUri(reportUri); + return this; + } + + /** + *+ * Sets the URI to which the browser should report pin validation failures. + *
+ * + *+ * See Section 2.1.4 + * for additional details. + *
+ * + * @param reportUri the URI where the browser should send the report to. + * @throws IllegalArgumentException if the reportUri is not a valid URI + */ + public HpkpConfig reportUri(String reportUri) { + writer.setReportUri(reportUri); + return this; + } + + /** + * Prevents the header from being added to the response. + * + * @return the {@link HeadersConfigurer} for additional configuration. + */ + public HeadersConfigurer+ * 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+ * 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
+ * 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