diff --git a/.gitignore b/.gitignore
index fc4ed6e86..365f5b205 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,5 +24,4 @@ _site
credentials.yml
.flattened-pom.xml
-.mvn/.gradle-enterprise
.mvn/.develocity
diff --git a/.mvn/develocity.xml b/.mvn/develocity.xml
deleted file mode 100644
index b54815e75..000000000
--- a/.mvn/develocity.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
- https://ge.spring.io
-
-
- false
- true
- true
-
- #{{'0.0.0.0'}}
-
-
-
-
- true
-
-
-
-
- ${env.GRADLE_ENTERPRISE_CACHE_USERNAME}
- ${env.GRADLE_ENTERPRISE_CACHE_PASSWORD}
-
-
- true
- #{env['GRADLE_ENTERPRISE_CACHE_USERNAME'] != null and env['GRADLE_ENTERPRISE_CACHE_PASSWORD'] != null}
-
-
-
\ No newline at end of file
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
index 854714cee..a36ba0531 100644
--- a/.mvn/extensions.xml
+++ b/.mvn/extensions.xml
@@ -1,13 +1,8 @@
- com.gradle
- develocity-maven-extension
- 1.21.3
-
-
- com.gradle
- common-custom-user-data-maven-extension
- 2.0
+ io.spring.develocity.conventions
+ develocity-conventions-maven-extension
+ 0.0.19
\ No newline at end of file
diff --git a/Jenkinsfile b/Jenkinsfile
index f479045e6..bc34f8b63 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -37,8 +37,7 @@ pipeline {
options { timeout(time: 30, unit: 'MINUTES')}
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
- GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}")
- GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}")
+ DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
script {
@@ -63,8 +62,7 @@ pipeline {
options { timeout(time: 30, unit: 'MINUTES')}
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
- GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}")
- GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}")
+ DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
script {
@@ -82,8 +80,7 @@ pipeline {
options { timeout(time: 30, unit: 'MINUTES')}
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
- GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}")
- GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}")
+ DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
script {
@@ -101,8 +98,7 @@ pipeline {
options { timeout(time: 30, unit: 'MINUTES')}
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
- GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}")
- GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}")
+ DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
script {
@@ -120,8 +116,7 @@ pipeline {
options { timeout(time: 30, unit: 'MINUTES')}
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
- GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}")
- GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}")
+ DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
script {
@@ -137,8 +132,7 @@ pipeline {
options { timeout(time: 30, unit: 'MINUTES')}
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
- GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}")
- GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}")
+ DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
script {
@@ -161,8 +155,7 @@ pipeline {
KEYRING = credentials('spring-signing-secring.gpg')
PASSPHRASE = credentials('spring-gpg-passphrase')
STAGING_PROFILE_ID = credentials('spring-data-release-deployment-maven-central-staging-profile-id')
- GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}")
- GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}")
+ DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
@@ -234,8 +227,7 @@ pipeline {
KEYRING = credentials('spring-signing-secring.gpg')
PASSPHRASE = credentials('spring-gpg-passphrase')
STAGING_PROFILE_ID = credentials('spring-data-release-deployment-maven-central-staging-profile-id')
- GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}")
- GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}")
+ DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
@@ -294,8 +286,7 @@ pipeline {
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
- GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}")
- GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}")
+ DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
}
steps {
@@ -303,8 +294,6 @@ pipeline {
docker.withRegistry('', "${p['dockerhub.credentials']}") {
docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.basic']) {
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ' +
- 'GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USR} ' +
- 'GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PSW} ' +
'./mvnw -s settings.xml -Pjakarta-ee-10,distribute,docs ' +
'-Dartifactory.server=https://repo.spring.io ' +
"-Dartifactory.username=${ARTIFACTORY_USR} " +
diff --git a/ci/build-and-deploy-to-artifactory.sh b/ci/build-and-deploy-to-artifactory.sh
index 5960f8b46..e8e5eb91b 100755
--- a/ci/build-and-deploy-to-artifactory.sh
+++ b/ci/build-and-deploy-to-artifactory.sh
@@ -4,9 +4,6 @@ set -euo pipefail
RELEASE_TYPE=$1
-export GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USR}
-export GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PSW}
-
echo 'Deploying to Artifactory...'
MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" ./mvnw \
diff --git a/ci/build-and-deploy-to-maven-central.sh b/ci/build-and-deploy-to-maven-central.sh
index 9eb2a5730..5a48d3762 100755
--- a/ci/build-and-deploy-to-maven-central.sh
+++ b/ci/build-and-deploy-to-maven-central.sh
@@ -5,9 +5,6 @@ set -euo pipefail
PROJECT_VERSION=$1
STAGING_REPOSITORY_ID=$2
-export GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USR}
-export GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PSW}
-
echo 'Staging on Maven Central...'
GNUPGHOME=/tmp/gpghome
diff --git a/ci/pipeline.properties b/ci/pipeline.properties
index 2aea08744..09915555f 100644
--- a/ci/pipeline.properties
+++ b/ci/pipeline.properties
@@ -18,5 +18,4 @@ docker.java.inside.basic=-v $HOME:/tmp/jenkins-home
# Credentials
artifactory.credentials=02bd1690-b54f-4c9f-819d-a77cb7a9822c
dockerhub.credentials=hub.docker.com-springbuildmaster
-gradle-enterprise-cache.credentials=gradle_enterprise_cache_user
-gradle-enterprise.access-key=gradle_enterprise_secret_access_key
+develocity.access-key=gradle_enterprise_secret_access_key
diff --git a/ci/test.sh b/ci/test.sh
index ba458fba6..5ec6309d8 100755
--- a/ci/test.sh
+++ b/ci/test.sh
@@ -2,9 +2,6 @@
set -euo pipefail
-export GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USR}
-export GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PSW}
-
MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" \
./mvnw -s settings.xml \
-P${PROFILE} clean dependency:list test -Dsort -B -U
diff --git a/settings.xml b/settings.xml
index 28f0d1339..b474fd4ff 100644
--- a/settings.xml
+++ b/settings.xml
@@ -24,11 +24,6 @@
${env.ARTIFACTORY_USR}
${env.ARTIFACTORY_PSW}
-
- ge.spring.io
- ${env.GRADLE_ENTERPRISE_CACHE_USERNAME}
- ${env.GRADLE_ENTERPRISE_CACHE_PASSWORD}
-
\ No newline at end of file
diff --git a/spring-ws-core/src/main/java/org/springframework/ws/soap/client/SoapFaultClientException.java b/spring-ws-core/src/main/java/org/springframework/ws/soap/client/SoapFaultClientException.java
index 0ba93c4c5..f2e2e7d8b 100644
--- a/spring-ws-core/src/main/java/org/springframework/ws/soap/client/SoapFaultClientException.java
+++ b/spring-ws-core/src/main/java/org/springframework/ws/soap/client/SoapFaultClientException.java
@@ -23,6 +23,8 @@
import org.springframework.ws.soap.SoapFault;
import org.springframework.ws.soap.SoapMessage;
+import java.util.Optional;
+
/**
* Thrown by {@code SoapFaultMessageResolver} when the response message has a fault.
*
@@ -56,13 +58,17 @@ public QName getFaultCode() {
}
/**
- * Returns the fault string or reason. For SOAP 1.1, this returns the fault string. For SOAP 1.2, this returns the
- * fault reason for the default locale.
+ * Returns the fault string or reason from the SOAP fault.
+ * For SOAP 1.1, this method returns the fault string. For SOAP 1.2, it returns the fault reason for the default locale.
+ *
+ * Note that this method returns the same value as {@link #getMessage()}.
*
- * Note that this message returns the same as {@link #getMessage()}.
+ * The returned value is wrapped in an {@link Optional}. If the SOAP fault is not present, an empty {@link Optional} is returned.
+ *
+ * @return an {@link Optional} containing the fault string or reason if present, or an empty {@link Optional} if the SOAP fault is not present.
*/
- public String getFaultStringOrReason() {
- return soapFault != null ? soapFault.getFaultStringOrReason() : null;
+ public Optional getFaultStringOrReason() {
+ return Optional.ofNullable(this.soapFault)
+ .map(SoapFault::getFaultStringOrReason);
}
-
}
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