Просмотр исходного кода

Add support sign SAML metadata

Closes gh-14801
Max Batischev 1 год назад
Родитель
Сommit
801e808f67

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -74,6 +74,8 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
 
 	private boolean usePrettyPrint = true;
 
+	private boolean signMetadata = false;
+
 	public OpenSamlMetadataResolver() {
 		this.entityDescriptorMarshaller = (EntityDescriptorMarshaller) XMLObjectProviderRegistrySupport
 			.getMarshallerFactory()
@@ -111,6 +113,9 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
 		SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration);
 		entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor);
 		this.entityDescriptorCustomizer.accept(new EntityDescriptorParameters(entityDescriptor, registration));
+		if (this.signMetadata) {
+			return OpenSamlSigningUtils.sign(entityDescriptor, registration);
+		}
 		return entityDescriptor;
 	}
 
@@ -128,6 +133,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
 	/**
 	 * Configure whether to pretty-print the metadata XML. This can be helpful when
 	 * signing the metadata payload.
+	 *
 	 * @since 6.2
 	 **/
 	public void setUsePrettyPrint(boolean usePrettyPrint) {
@@ -238,6 +244,15 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
 		}
 	}
 
+	/**
+	 * Configure whether to sign the metadata, defaults to {@code false}.
+	 *
+	 * @since 6.4
+	 */
+	public void setSignMetadata(boolean signMetadata) {
+		this.signMetadata = signMetadata;
+	}
+
 	/**
 	 * A tuple containing an OpenSAML {@link EntityDescriptor} and its associated
 	 * {@link RelyingPartyRegistration}

+ 195 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlSigningUtils.java

@@ -0,0 +1,195 @@
+/*
+ * Copyright 2002-2024 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
+ *
+ *      https://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.saml2.provider.service.metadata;
+
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.xml.SerializeSupport;
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.core.xml.io.Marshaller;
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.saml.security.impl.SAMLMetadataSignatureSigningParametersResolver;
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.credential.BasicCredential;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialSupport;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.xmlsec.SignatureSigningParameters;
+import org.opensaml.xmlsec.SignatureSigningParametersResolver;
+import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion;
+import org.opensaml.xmlsec.crypto.XMLSigningUtil;
+import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration;
+import org.opensaml.xmlsec.keyinfo.KeyInfoGeneratorManager;
+import org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager;
+import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
+import org.opensaml.xmlsec.signature.SignableXMLObject;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
+import org.opensaml.xmlsec.signature.support.SignatureSupport;
+import org.w3c.dom.Element;
+
+import org.springframework.security.saml2.Saml2Exception;
+import org.springframework.security.saml2.core.Saml2ParameterNames;
+import org.springframework.security.saml2.core.Saml2X509Credential;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.util.Assert;
+import org.springframework.web.util.UriComponentsBuilder;
+import org.springframework.web.util.UriUtils;
+
+/**
+ * Utility methods for signing SAML components with OpenSAML
+ *
+ * For internal use only.
+ *
+ * @author Josh Cummings
+ * @since 6.3
+ */
+final class OpenSamlSigningUtils {
+
+	static String serialize(XMLObject object) {
+		try {
+			Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object);
+			Element element = marshaller.marshall(object);
+			return SerializeSupport.nodeToString(element);
+		}
+		catch (MarshallingException ex) {
+			throw new Saml2Exception(ex);
+		}
+	}
+
+	static <O extends SignableXMLObject> O sign(O object, RelyingPartyRegistration relyingPartyRegistration) {
+		SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration);
+		try {
+			SignatureSupport.signObject(object, parameters);
+			return object;
+		}
+		catch (Exception ex) {
+			throw new Saml2Exception(ex);
+		}
+	}
+
+	static QueryParametersPartial sign(RelyingPartyRegistration registration) {
+		return new QueryParametersPartial(registration);
+	}
+
+	private static SignatureSigningParameters resolveSigningParameters(
+			RelyingPartyRegistration relyingPartyRegistration) {
+		List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration);
+		List<String> algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms();
+		List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
+		String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
+		SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
+		CriteriaSet criteria = new CriteriaSet();
+		BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
+		signingConfiguration.setSigningCredentials(credentials);
+		signingConfiguration.setSignatureAlgorithms(algorithms);
+		signingConfiguration.setSignatureReferenceDigestMethods(digests);
+		signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
+		signingConfiguration.setKeyInfoGeneratorManager(buildSignatureKeyInfoGeneratorManager());
+		criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration));
+		try {
+			SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
+			Assert.notNull(parameters, "Failed to resolve any signing credential");
+			return parameters;
+		}
+		catch (Exception ex) {
+			throw new Saml2Exception(ex);
+		}
+	}
+
+	private static NamedKeyInfoGeneratorManager buildSignatureKeyInfoGeneratorManager() {
+		final NamedKeyInfoGeneratorManager namedManager = new NamedKeyInfoGeneratorManager();
+
+		namedManager.setUseDefaultManager(true);
+		final KeyInfoGeneratorManager defaultManager = namedManager.getDefaultManager();
+
+		// Generator for X509Credentials
+		final X509KeyInfoGeneratorFactory x509Factory = new X509KeyInfoGeneratorFactory();
+		x509Factory.setEmitEntityCertificate(true);
+		x509Factory.setEmitEntityCertificateChain(true);
+
+		defaultManager.registerFactory(x509Factory);
+
+		return namedManager;
+	}
+
+	private static List<Credential> resolveSigningCredentials(RelyingPartyRegistration relyingPartyRegistration) {
+		List<Credential> credentials = new ArrayList<>();
+		for (Saml2X509Credential x509Credential : relyingPartyRegistration.getSigningX509Credentials()) {
+			X509Certificate certificate = x509Credential.getCertificate();
+			PrivateKey privateKey = x509Credential.getPrivateKey();
+			BasicCredential credential = CredentialSupport.getSimpleCredential(certificate, privateKey);
+			credential.setEntityId(relyingPartyRegistration.getEntityId());
+			credential.setUsageType(UsageType.SIGNING);
+			credentials.add(credential);
+		}
+		return credentials;
+	}
+
+	private OpenSamlSigningUtils() {
+
+	}
+
+	static class QueryParametersPartial {
+
+		final RelyingPartyRegistration registration;
+
+		final Map<String, String> components = new LinkedHashMap<>();
+
+		QueryParametersPartial(RelyingPartyRegistration registration) {
+			this.registration = registration;
+		}
+
+		QueryParametersPartial param(String key, String value) {
+			this.components.put(key, value);
+			return this;
+		}
+
+		Map<String, String> parameters() {
+			SignatureSigningParameters parameters = resolveSigningParameters(this.registration);
+			Credential credential = parameters.getSigningCredential();
+			String algorithmUri = parameters.getSignatureAlgorithm();
+			this.components.put(Saml2ParameterNames.SIG_ALG, algorithmUri);
+			UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
+			for (Map.Entry<String, String> component : this.components.entrySet()) {
+				builder.queryParam(component.getKey(),
+						UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
+			}
+			String queryString = builder.build(true).toString().substring(1);
+			try {
+				byte[] rawSignature = XMLSigningUtil.signWithURI(credential, algorithmUri,
+						queryString.getBytes(StandardCharsets.UTF_8));
+				String b64Signature = Saml2Utils.samlEncode(rawSignature);
+				this.components.put(Saml2ParameterNames.SIGNATURE, b64Signature);
+			}
+			catch (SecurityException ex) {
+				throw new Saml2Exception(ex);
+			}
+			return this.components;
+		}
+
+	}
+
+}

+ 72 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/Saml2Utils.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2002-2024 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
+ *
+ *      https://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.saml2.provider.service.metadata;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterOutputStream;
+
+import org.springframework.security.saml2.Saml2Exception;
+
+/**
+ * @since 6.3
+ */
+final class Saml2Utils {
+
+	private Saml2Utils() {
+	}
+
+	static String samlEncode(byte[] b) {
+		return Base64.getEncoder().encodeToString(b);
+	}
+
+	static byte[] samlDecode(String s) {
+		return Base64.getMimeDecoder().decode(s);
+	}
+
+	static byte[] samlDeflate(String s) {
+		try {
+			ByteArrayOutputStream b = new ByteArrayOutputStream();
+			DeflaterOutputStream deflater = new DeflaterOutputStream(b, new Deflater(Deflater.DEFLATED, true));
+			deflater.write(s.getBytes(StandardCharsets.UTF_8));
+			deflater.finish();
+			return b.toByteArray();
+		}
+		catch (IOException ex) {
+			throw new Saml2Exception("Unable to deflate string", ex);
+		}
+	}
+
+	static String samlInflate(byte[] b) {
+		try {
+			ByteArrayOutputStream out = new ByteArrayOutputStream();
+			InflaterOutputStream iout = new InflaterOutputStream(out, new Inflater(true));
+			iout.write(b);
+			iout.finish();
+			return new String(out.toByteArray(), StandardCharsets.UTF_8);
+		}
+		catch (IOException ex) {
+			throw new Saml2Exception("Unable to inflate string", ex);
+		}
+	}
+
+}

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -49,6 +49,33 @@ public class OpenSamlMetadataResolverTests {
 			.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
 	}
 
+	@Test
+	public void resolveWhenRelyingPartyAndSignMetadataSetThenMetadataMatches() {
+		RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full()
+			.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
+			.build();
+		OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
+		openSamlMetadataResolver.setSignMetadata(true);
+		String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration);
+		assertThat(metadata).contains("<md:EntityDescriptor")
+			.contains("entityID=\"rp-entity-id\"")
+			.contains("<md:KeyDescriptor use=\"signing\">")
+			.contains("<md:KeyDescriptor use=\"encryption\">")
+			.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
+			.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
+			.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
+			.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"")
+			.contains("Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"")
+			.contains("CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#")
+			.contains("SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
+			.contains("Reference URI=\"\"")
+			.contains("Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature")
+			.contains("Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"")
+			.contains("DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"")
+			.contains("DigestValue")
+			.contains("SignatureValue");
+	}
+
 	@Test
 	public void resolveWhenRelyingPartyNoCredentialsThenMetadataMatches() {
 		RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials()
@@ -122,4 +149,37 @@ public class OpenSamlMetadataResolverTests {
 			.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"");
 	}
 
+	@Test
+	public void resolveIterableWhenRelyingPartiesAndSignMetadataSetThenMetadataMatches() {
+		RelyingPartyRegistration one = TestRelyingPartyRegistrations.full()
+			.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
+			.build();
+		RelyingPartyRegistration two = TestRelyingPartyRegistrations.full()
+			.entityId("two")
+			.assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT)
+			.build();
+		OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver();
+		openSamlMetadataResolver.setSignMetadata(true);
+		String metadata = openSamlMetadataResolver.resolve(List.of(one, two));
+		assertThat(metadata).contains("<md:EntitiesDescriptor")
+			.contains("<md:EntityDescriptor")
+			.contains("entityID=\"rp-entity-id\"")
+			.contains("entityID=\"two\"")
+			.contains("<md:KeyDescriptor use=\"signing\">")
+			.contains("<md:KeyDescriptor use=\"encryption\">")
+			.contains("<ds:X509Certificate>MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBh")
+			.contains("Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\"")
+			.contains("Location=\"https://rp.example.org/acs\" index=\"1\"")
+			.contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\"")
+			.contains("Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"")
+			.contains("CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#")
+			.contains("SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
+			.contains("Reference URI=\"\"")
+			.contains("Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature")
+			.contains("Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"")
+			.contains("DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"")
+			.contains("DigestValue")
+			.contains("SignatureValue");
+	}
+
 }