Skip to content

Commit 8e50e3c

Browse files
Max BatischevMax Batischev
authored andcommitted
Add support sign SAML metadata
Closes gh-14801
1 parent 8dd28b7 commit 8e50e3c

File tree

4 files changed

+344
-2
lines changed

4 files changed

+344
-2
lines changed

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -74,6 +74,8 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
7474

7575
private boolean usePrettyPrint = true;
7676

77+
private boolean signMetadata = false;
78+
7779
public OpenSamlMetadataResolver() {
7880
this.entityDescriptorMarshaller = (EntityDescriptorMarshaller) XMLObjectProviderRegistrySupport
7981
.getMarshallerFactory()
@@ -111,6 +113,9 @@ private EntityDescriptor entityDescriptor(RelyingPartyRegistration registration)
111113
SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration);
112114
entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor);
113115
this.entityDescriptorCustomizer.accept(new EntityDescriptorParameters(entityDescriptor, registration));
116+
if (this.signMetadata) {
117+
return OpenSamlSigningUtils.sign(entityDescriptor, registration);
118+
}
114119
return entityDescriptor;
115120
}
116121

@@ -128,6 +133,7 @@ public void setEntityDescriptorCustomizer(Consumer<EntityDescriptorParameters> e
128133
/**
129134
* Configure whether to pretty-print the metadata XML. This can be helpful when
130135
* signing the metadata payload.
136+
*
131137
* @since 6.2
132138
**/
133139
public void setUsePrettyPrint(boolean usePrettyPrint) {
@@ -238,6 +244,15 @@ private String serialize(EntitiesDescriptor entities) {
238244
}
239245
}
240246

247+
/**
248+
* Configure whether to sign the metadata.
249+
*
250+
* @since 6.3
251+
*/
252+
public void setSignMetadata(boolean signMetadata) {
253+
this.signMetadata = signMetadata;
254+
}
255+
241256
/**
242257
* A tuple containing an OpenSAML {@link EntityDescriptor} and its associated
243258
* {@link RelyingPartyRegistration}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.saml2.provider.service.metadata;
18+
19+
import java.nio.charset.StandardCharsets;
20+
import java.security.PrivateKey;
21+
import java.security.cert.X509Certificate;
22+
import java.util.ArrayList;
23+
import java.util.Collections;
24+
import java.util.LinkedHashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
29+
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
30+
import org.opensaml.core.xml.XMLObject;
31+
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
32+
import org.opensaml.core.xml.io.Marshaller;
33+
import org.opensaml.core.xml.io.MarshallingException;
34+
import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
35+
import org.opensaml.security.SecurityException;
36+
import org.opensaml.security.credential.BasicCredential;
37+
import org.opensaml.security.credential.Credential;
38+
import org.opensaml.security.credential.CredentialSupport;
39+
import org.opensaml.security.credential.UsageType;
40+
import org.opensaml.xmlsec.SignatureSigningParameters;
41+
import org.opensaml.xmlsec.SignatureSigningParametersResolver;
42+
import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
43+
import org.opensaml.xmlsec.crypto.XMLSigningUtil;
44+
import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
45+
import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager;
46+
import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager;
47+
import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
48+
import org.opensaml.xmlsec.signature.SignableXMLObject;
49+
import org.opensaml.xmlsec.signature.support.SignatureConstants;
50+
import org.opensaml.xmlsec.signature.support.SignatureSupport;
51+
import org.w3c.dom.Element;
52+
53+
import org.springframework.security.saml2.Saml2Exception;
54+
import org.springframework.security.saml2.core.Saml2ParameterNames;
55+
import org.springframework.security.saml2.core.Saml2X509Credential;
56+
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
57+
import org.springframework.util.Assert;
58+
import org.springframework.web.util.UriComponentsBuilder;
59+
import org.springframework.web.util.UriUtils;
60+
61+
/**
62+
* Utility methods for signing SAML components with OpenSAML
63+
*
64+
* For internal use only.
65+
*
66+
* @author Josh Cummings
67+
* @since 6.3
68+
*/
69+
final class OpenSamlSigningUtils {
70+
71+
static String serialize(XMLObject object) {
72+
try {
73+
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
74+
Element element = marshaller.marshall(object);
75+
return SerializeSupport.nodeToString(element);
76+
}
77+
catch (MarshallingException ex) {
78+
throw new Saml2Exception(ex);
79+
}
80+
}
81+
82+
static <O extends SignableXMLObject> O sign(O object, RelyingPartyRegistration relyingPartyRegistration) {
83+
SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration);
84+
try {
85+
SignatureSupport.signObject(object, parameters);
86+
return object;
87+
}
88+
catch (Exception ex) {
89+
throw new Saml2Exception(ex);
90+
}
91+
}
92+
93+
static QueryParametersPartial sign(RelyingPartyRegistration registration) {
94+
return new QueryParametersPartial(registration);
95+
}
96+
97+
private static SignatureSigningParameters resolveSigningParameters(
98+
RelyingPartyRegistration relyingPartyRegistration) {
99+
List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration);
100+
List<String> algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms();
101+
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
102+
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
103+
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
104+
CriteriaSet criteria = new CriteriaSet();
105+
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
106+
signingConfiguration.setSigningCredentials(credentials);
107+
signingConfiguration.setSignatureAlgorithms(algorithms);
108+
signingConfiguration.setSignatureReferenceDigestMethods(digests);
109+
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
110+
signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager());
111+
criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration));
112+
try {
113+
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
114+
Assert.notNull(parameters, "Failed to resolve any signing credential");
115+
return parameters;
116+
}
117+
catch (Exception ex) {
118+
throw new Saml2Exception(ex);
119+
}
120+
}
121+
122+
private static NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() {
123+
final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager();
124+
125+
namedManager.setUseDefaultManager(true);
126+
final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager();
127+
128+
// Generator for X509Credentials
129+
final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory();
130+
x509Factory.setEmitEntityCertificate(true);
131+
x509Factory.setEmitEntityCertificateChain(true);
132+
133+
defaultManager.registerFactory(x509Factory);
134+
135+
return namedManager;
136+
}
137+
138+
private static List<Credential> resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) {
139+
List<Credential> credentials = new ArrayList<>();
140+
for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) {
141+
X509Certificate certificate = x509Credential.getCertificate();
142+
PrivateKey privateKey = x509Credential.getPrivateKey();
143+
BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
144+
credential.setEntityId(relyingPartyRegistration.getEntityId());
145+
credential.setUsageType(UsageType.SIGNING);
146+
credentials.add(credential);
147+
}
148+
return credentials;
149+
}
150+
151+
private OpenSamlSigningUtils() {
152+
153+
}
154+
155+
static class QueryParametersPartial {
156+
157+
final RelyingPartyRegistration registration;
158+
159+
final Map<String, String> components = new LinkedHashMap<>();
160+
161+
QueryParametersPartial(RelyingPartyRegistration registration) {
162+
this.registration = registration;
163+
}
164+
165+
QueryParametersPartial param(String key, String value) {
166+
this.components.put(key, value);
167+
return this;
168+
}
169+
170+
Map<String, String> parameters() {
171+
SignatureSigningParameters parameters = resolveSigningParameters(this.registration);
172+
Credential credential = parameters.getSigningCredential();
173+
String algorithmUri = parameters.getSignatureAlgorithm();
174+
this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri);
175+
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
176+
for (Map.Entry<String, String> component : this.components.entrySet()) {
177+
builder.queryParam(component.getKey(),
178+
UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
179+
}
180+
String queryString = builder.build(true).toString().substring(1);
181+
try {
182+
byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
183+
queryString.getBytes(StandardCharsets.UTF_8));
184+
String b64Signature = Saml2Utils.samlEncode(rawSignature);
185+
this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature);
186+
}
187+
catch (SecurityException ex) {
188+
throw new Saml2Exception(ex);
189+
}
190+
return this.components;
191+
}
192+
193+
}
194+
195+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.saml2.provider.service.metadata;
18+
19+
import java.io.ByteArrayOutputStream;
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.Base64;
23+
import java.util.zip.Deflater;
24+
import java.util.zip.DeflaterOutputStream;
25+
import java.util.zip.Inflater;
26+
import java.util.zip.InflaterOutputStream;
27+
28+
import org.springframework.security.saml2.Saml2Exception;
29+
30+
/**
31+
* @since 6.3
32+
*/
33+
final class Saml2Utils {
34+
35+
private Saml2Utils() {
36+
}
37+
38+
static String samlEncode(byte[] b) {
39+
return Base64.getEncoder().encodeToString(b);
40+
}
41+
42+
static byte[] samlDecode(String s) {
43+
return Base64.getMimeDecoder().decode(s);
44+
}
45+
46+
static byte[] samlDeflate(String s) {
47+
try {
48+
ByteArrayOutputStream b = new ByteArrayOutputStream();
49+
DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(Deflater.DEFLATED, true));
50+
deflater.write(s.getBytes(StandardCharsets.UTF_8));
51+
deflater.finish();
52+
return b.toByteArray();
53+
}
54+
catch (IOException ex) {
55+
throw new Saml2Exception("Unable to deflate string", ex);
56+
}
57+
}
58+
59+
static String samlInflate(byte[] b) {
60+
try {
61+
ByteArrayOutputStream out = new ByteArrayOutputStream();
62+
InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
63+
iout.write(b);
64+
iout.finish();
65+
return new String(out.toByteArray(), StandardCharsets.UTF_8);
66+
}
67+
catch (IOException ex) {
68+
throw new Saml2Exception("Unable to inflate string", ex);
69+
}
70+
}
71+
72+
}

saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -49,6 +49,33 @@ public void resolveWhenRelyingPartyThenMetadataMatches() {
4949
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
5050
}
5151

52+
@Test
53+
public void resolveWhenRelyingPartyAndSignMetadataSetThenMetadataMatches() {
54+
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
55+
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
56+
.build();
57+
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
58+
openSamlMetadataResolver.setSignMetadata(true);
59+
String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration);
60+
assertThat(metadata).contains("<md:EntityDescriptor")
61+
.contains("entityID=\"rp-entity-id\"")
62+
.contains("<md:KeyDescriptor use=\"signing\">")
63+
.contains("<md:KeyDescriptor use=\"encryption\">")
64+
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
65+
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
66+
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
67+
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"")
68+
.contains("Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"")
69+
.contains("CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#")
70+
.contains("SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
71+
.contains("Reference URI=\"\"")
72+
.contains("Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature")
73+
.contains("Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"")
74+
.contains("DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"")
75+
.contains("DigestValue")
76+
.contains("SignatureValue");
77+
}
78+
5279
@Test
5380
public void resolveWhenRelyingPartyNoCredentialsThenMetadataMatches() {
5481
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials()
@@ -122,4 +149,37 @@ public void resolveIterableWhenRelyingPartiesThenMetadataMatches() {
122149
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
123150
}
124151

152+
@Test
153+
public void resolveIterableWhenRelyingPartiesAndSignMetadataSetThenMetadataMatches() {
154+
RelyingPartyRegistration one = TestRelyingPartyRegistrations.full()
155+
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
156+
.build();
157+
RelyingPartyRegistration two = TestRelyingPartyRegistrations.full()
158+
.entityId("two")
159+
.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
160+
.build();
161+
OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
162+
openSamlMetadataResolver.setSignMetadata(true);
163+
String metadata = openSamlMetadataResolver.resolve(List.of(one, two));
164+
assertThat(metadata).contains("<md:EntitiesDescriptor")
165+
.contains("<md:EntityDescriptor")
166+
.contains("entityID=\"rp-entity-id\"")
167+
.contains("entityID=\"two\"")
168+
.contains("<md:KeyDescriptor use=\"signing\">")
169+
.contains("<md:KeyDescriptor use=\"encryption\">")
170+
.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
171+
.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
172+
.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
173+
.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"")
174+
.contains("Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"")
175+
.contains("CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#")
176+
.contains("SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
177+
.contains("Reference URI=\"\"")
178+
.contains("Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature")
179+
.contains("Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"")
180+
.contains("DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"")
181+
.contains("DigestValue")
182+
.contains("SignatureValue");
183+
}
184+
125185
}

0 commit comments

Comments
 (0)