diff --git a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java index 3b457cf46..c04cfc58a 100644 --- a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java +++ b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java @@ -20,8 +20,11 @@ import java.security.Principal; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -59,6 +62,9 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; + /** * A WS-Security endpoint interceptor based on Apache's WSS4J. This interceptor supports messages created by the * {@link org.springframework.ws.soap.axiom.AxiomSoapMessageFactory} and the @@ -138,6 +144,7 @@ * @author Jamin Hitchcock * @author Rob Leland * @author Lars Uffmann + * @author Andreas Winter * @see Apache WSS4J 2.0 * @since 2.3.0 */ @@ -194,6 +201,8 @@ public class Wss4jSecurityInterceptor extends AbstractWsSecurityInterceptor impl // To maintain same behavior as default, this flag is set to true private boolean removeSecurityHeader = true; + private List signatureSubjectDnPatterns = emptyList(); + /** * Create a {@link WSSecurityEngine} by default. */ @@ -225,6 +234,15 @@ public void setSecurementActor(String securementActor) { handler.setOption(WSHandlerConstants.ACTOR, securementActor); } + /** + * Defines whether to use a single certificate or a whole certificate chain when constructing + * a BinarySecurityToken used for direct reference in signature. + * The default is "true", meaning that only a single certificate is used. + */ + public void setSecurementSignatureSingleCertificate(boolean useSingleCertificate) { + handler.setOption(WSHandlerConstants.USE_SINGLE_CERTIFICATE, useSingleCertificate); + } + public void setSecurementEncryptionCrypto(Crypto securementEncryptionCrypto) { handler.setSecurementEncryptionCrypto(securementEncryptionCrypto); } @@ -485,6 +503,19 @@ public void setValidationSignatureCrypto(Crypto signatureCrypto) { this.validationSignatureCrypto = signatureCrypto; } + /** + * Certificate constraints which will be applied to the subject DN of the certificate used for + * signature validation, after trust verification of the certificate chain associated with the + * certificate. + * + * @param patterns A list of regex patterns which will be applied to the subject DN. + * + * @see WSS4J configuration: SIG_SUBJECT_CERT_CONSTRAINTS + */ + public void setValidationSubjectDnConstraints(List patterns) { + signatureSubjectDnPatterns = patterns; + } + /** Whether to enable signatureConfirmation or not. By default signatureConfirmation is enabled */ public void setEnableSignatureConfirmation(boolean enableSignatureConfirmation) { @@ -670,6 +701,7 @@ protected RequestData initializeRequestData(MessageContext messageContext) { // allow for qualified password types for .Net interoperability requestData.setAllowNamespaceQualifiedPasswordTypes(true); + requestData.setSubjectCertConstraints(signatureSubjectDnPatterns); return requestData; } @@ -710,6 +742,8 @@ protected RequestData initializeValidationRequestData(MessageContext messageCont // allow for qualified password types for .Net interoperability requestData.setAllowNamespaceQualifiedPasswordTypes(true); + requestData.setSubjectCertConstraints(signatureSubjectDnPatterns); + return requestData; } diff --git a/spring-ws-security/src/test/java/org/springframework/ws/soap/security/wss4j2/Wss4jMessageInterceptorSignTestCase.java b/spring-ws-security/src/test/java/org/springframework/ws/soap/security/wss4j2/Wss4jMessageInterceptorSignTestCase.java index 0e8490af9..729221b73 100644 --- a/spring-ws-security/src/test/java/org/springframework/ws/soap/security/wss4j2/Wss4jMessageInterceptorSignTestCase.java +++ b/spring-ws-security/src/test/java/org/springframework/ws/soap/security/wss4j2/Wss4jMessageInterceptorSignTestCase.java @@ -18,7 +18,9 @@ import static org.assertj.core.api.Assertions.*; +import java.util.List; import java.util.Properties; +import java.util.regex.Pattern; import org.junit.jupiter.api.Test; import org.springframework.ws.WebServiceMessage; @@ -121,4 +123,36 @@ public void testSignResponseWithSignatureUser() throws Exception { assertXpathExists("Absent SignatureConfirmation element", "/SOAP-ENV:Envelope/SOAP-ENV:Header/wsse:Security/ds:Signature", document); } + + @Test + public void testValidateCertificateSubjectDnConstraintsShouldMatchSubject() throws Exception { + SoapMessage message = createSignedTestSoapMessage(); + MessageContext messageContext = getSoap11MessageContext(createSignedTestSoapMessage()); + interceptor.secureMessage(message, messageContext); + + interceptor.setValidationActions("Signature"); + interceptor.setValidationSubjectDnConstraints(List.of(Pattern.compile(".*"))); + assertThatCode(() ->interceptor.validateMessage(message, messageContext)).doesNotThrowAnyException(); + } + + @Test + public void testValidateCertificateSubjectDnConstraintsShouldFailForNotMatchingSubject() throws Exception { + SoapMessage message = createSignedTestSoapMessage(); + MessageContext messageContext = getSoap11MessageContext(createSignedTestSoapMessage()); + interceptor.secureMessage(message, messageContext); + + interceptor.setValidationActions("Signature"); + interceptor.setValidationSubjectDnConstraints(List.of(Pattern.compile("O=Some Other Company"))); + Throwable catched = catchThrowable(() -> interceptor.validateMessage(message, messageContext)); + assertThat(catched).isInstanceOf(Wss4jSecurityValidationException.class); + } + + private SoapMessage createSignedTestSoapMessage() throws Exception { + interceptor.setSecurementActions("Signature"); + interceptor.setSecurementSignatureKeyIdentifier("DirectReference"); + interceptor.setSecurementSignatureSingleCertificate(false); + interceptor.setSecurementPassword("123456"); + interceptor.setSecurementUsername("testkey"); + return loadSoap11Message("empty-soap.xml"); + } } diff --git a/spring-ws-security/src/test/resources/private.jks b/spring-ws-security/src/test/resources/private.jks index b3b10e36f..15a4ebafe 100644 Binary files a/spring-ws-security/src/test/resources/private.jks and b/spring-ws-security/src/test/resources/private.jks differ