From febb46b7020e1ad6f9eeb8ad39505c7426ff3035 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 28 Mar 2018 13:43:29 +0300 Subject: [PATCH] [SAML] Saml metadata signing (elastic/x-pack-elasticsearch#4184) Adds option to sign generated Service Provider SAML metadata - Using a (possibly password protected) PEM encoded keypair - Using a keypair stored in a (possibly password protected) PKCSelastic/x-pack-elasticsearch#12 keystore Resolves elastic/x-pack-elasticsearch#3982 Original commit: elastic/x-pack-elasticsearch@7b806d76f82d55ce69854989551f27ac2b30e120 --- .../xpack/core/ssl/CertUtils.java | 3 +- .../authc/saml/SamlMetadataCommand.java | 144 ++++++++- .../authc/saml/SamlMetadataCommandTests.java | 276 ++++++++++++++++++ .../xpack/security/authc/saml/saml.p12 | Bin 0 -> 2461 bytes .../authc/saml/saml_with_password.key | 30 ++ .../authc/saml/saml_with_password.p12 | Bin 0 -> 2461 bytes 6 files changed, 448 insertions(+), 5 deletions(-) create mode 100644 plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml.p12 create mode 100644 plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml_with_password.key create mode 100644 plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml_with_password.p12 diff --git a/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertUtils.java b/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertUtils.java index 4c42aa927cc..e1203a09ebb 100644 --- a/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertUtils.java +++ b/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/CertUtils.java @@ -396,7 +396,8 @@ public class CertUtils { * @param keyPassword A supplier for the password for each key. The key alias is supplied as an argument to the function, and it should * return the password for that key. If it returns {@code null}, then the key-pair for that alias is not read. */ - static Map readPkcs12KeyPairs(Path path, char[] password, Function keyPassword, Environment env) + public static Map readPkcs12KeyPairs(Path path, char[] password, Function keyPassword, Environment + env) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException { final KeyStore store = readKeyStore(path, "PKCS12", password); final Enumeration enumeration = store.aliases(); diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java index 8b3b13860ce..a123a0ab500 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommand.java @@ -6,9 +6,17 @@ package org.elasticsearch.xpack.security.authc.saml; import java.io.InputStream; +import java.io.Reader; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.security.Key; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -16,6 +24,7 @@ import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import joptsimple.OptionParser; @@ -28,6 +37,7 @@ import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.SuppressForbidden; import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.UserException; +import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.logging.Loggers; @@ -38,10 +48,18 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; +import org.elasticsearch.xpack.core.ssl.CertUtils; import org.elasticsearch.xpack.security.authc.saml.SamlSpMetadataBuilder.ContactInfo; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.x509.BasicX509Credential; +import org.opensaml.xmlsec.signature.Signature; +import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.opensaml.xmlsec.signature.support.Signer; import org.w3c.dom.Element; import org.xml.sax.SAXException; @@ -65,6 +83,10 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand { private final OptionSpec orgDisplayNameSpec; private final OptionSpec orgUrlSpec; private final OptionSpec contactsSpec; + private final OptionSpec signingPkcs12PathSpec; + private final OptionSpec signingCertPathSpec; + private final OptionSpec signingKeyPathSpec; + private final OptionSpec keyPasswordSpec; public static void main(String[] args) throws Exception { new SamlMetadataCommand().main(args, Terminal.DEFAULT); @@ -84,6 +106,18 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand { orgUrlSpec = parser.accepts("organisation-url", "the URL of the organisation operating this service") .requiredIf(orgNameSpec).withRequiredArg(); contactsSpec = parser.accepts("contacts", "Include contact information in metadata").availableUnless(batchSpec); + signingPkcs12PathSpec = parser.accepts("signing-bundle", "path to an existing key pair (in PKCS#12 format) to be used for " + + "signing ") + .withRequiredArg(); + signingCertPathSpec = parser.accepts("signing-cert", "path to an existing signing certificate") + .availableUnless(signingPkcs12PathSpec) + .withRequiredArg(); + signingKeyPathSpec = parser.accepts("signing-key", "path to an existing signing private key") + .availableIf(signingCertPathSpec) + .requiredIf(signingCertPathSpec) + .withRequiredArg(); + keyPasswordSpec = parser.accepts("signing-key-password", "password for an existing signing private key or keypair") + .withOptionalArg(); } @Override @@ -95,7 +129,9 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand { SamlUtils.initialize(logger); final EntityDescriptor descriptor = buildEntityDescriptor(terminal, options, env); - final Path xml = writeOutput(terminal, options, descriptor); + Element element = possiblySignDescriptor(terminal, options, descriptor, env); + + final Path xml = writeOutput(terminal, options, element); validateXml(terminal, xml); } @@ -181,9 +217,42 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand { return builder.build(); } - private Path writeOutput(Terminal terminal, OptionSet options, EntityDescriptor descriptor) throws Exception { - final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller(); - final Element element = marshaller.marshall(descriptor); + // package-protected for testing + Element possiblySignDescriptor(Terminal terminal, OptionSet options, EntityDescriptor descriptor, Environment env) + throws UserException { + try { + final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller(); + if (options.has(signingPkcs12PathSpec) || (options.has(signingCertPathSpec) && options.has(signingKeyPathSpec))) { + Signature signature = (Signature) XMLObjectProviderRegistrySupport.getBuilderFactory() + .getBuilder(Signature.DEFAULT_ELEMENT_NAME) + .buildObject(Signature.DEFAULT_ELEMENT_NAME); + signature.setSigningCredential(buildSigningCredential(terminal, options, env)); + signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); + signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + descriptor.setSignature(signature); + Element element = marshaller.marshall(descriptor); + Signer.signObject(signature); + return element; + } else { + return marshaller.marshall(descriptor); + } + } catch (Exception e) { + String errorMessage; + if (e instanceof MarshallingException) { + errorMessage = "Error serializing Metadata to file"; + } else if (e instanceof org.opensaml.xmlsec.signature.support.SignatureException) { + errorMessage = "Error attempting to sign Metadata"; + } else { + errorMessage = "Error building signing credentials from provided keyPair"; + } + terminal.println(Terminal.Verbosity.SILENT, errorMessage); + terminal.println("The following errors were found:"); + printExceptions(terminal, e); + throw new UserException(ExitCodes.CANT_CREATE, "Unable to create metadata document"); + } + } + + private Path writeOutput(Terminal terminal, OptionSet options, Element element) throws Exception { final Path outputFile = resolvePath(option(outputPathSpec, options, "saml-elasticsearch-metadata.xml")); final Writer writer = Files.newBufferedWriter(outputFile); SamlUtils.print(element, writer, true); @@ -191,7 +260,74 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand { return outputFile; } + private Credential buildSigningCredential(Terminal terminal, OptionSet options, Environment env) throws + Exception { + X509Certificate signingCertificate; + PrivateKey signingKey; + char[] password = getChars(keyPasswordSpec.value(options)); + if (options.has(signingPkcs12PathSpec)) { + Path p12Path = resolvePath(signingPkcs12PathSpec.value(options)); + Map keys = withPassword("certificate bundle (" + p12Path + ")", password, + terminal, keyPassword -> CertUtils.readPkcs12KeyPairs(p12Path, keyPassword, a -> keyPassword, env)); + if (keys.size() != 1) { + throw new IllegalArgumentException("expected a single key in file [" + p12Path.toAbsolutePath() + "] but found [" + + keys.size() + "]"); + } + final Map.Entry pair = keys.entrySet().iterator().next(); + signingCertificate = (X509Certificate) pair.getKey(); + signingKey = (PrivateKey) pair.getValue(); + } else { + Path cert = resolvePath(signingCertPathSpec.value(options)); + Path key = resolvePath(signingKeyPathSpec.value(options)); + final String resolvedSigningCertPath = cert.toAbsolutePath().toString(); + Certificate[] certificates = CertUtils.readCertificates(Collections.singletonList(resolvedSigningCertPath), env); + if (certificates.length != 1) { + throw new IllegalArgumentException("expected a single certificate in file [" + resolvedSigningCertPath + "] but found [" + + certificates.length + "]"); + } + signingCertificate = (X509Certificate) certificates[0]; + signingKey = readSigningKey(key, password, terminal); + } + return new BasicX509Credential(signingCertificate, signingKey); + } + + private static T withPassword(String description, char[] password, Terminal terminal, + CheckedFunction body) throws E { + if (password == null) { + char[] promptedValue = terminal.readSecret("Enter password for " + description + " : "); + try { + return body.apply(promptedValue); + } finally { + Arrays.fill(promptedValue, (char) 0); + } + } else { + return body.apply(password); + } + } + + private static char[] getChars(String password) { + return password == null ? null : password.toCharArray(); + } + + private static PrivateKey readSigningKey(Path path, char[] password, Terminal terminal) + throws Exception { + AtomicReference passwordReference = new AtomicReference<>(password); + try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + return CertUtils.readPrivateKey(reader, () -> { + if (password != null) { + return password; + } + char[] promptedValue = terminal.readSecret("Enter password for the signing key (" + path.getFileName() + ") : "); + passwordReference.set(promptedValue); + return promptedValue; + }); + } finally { + if (passwordReference.get() != null) { + Arrays.fill(passwordReference.get(), (char) 0); + } + } + } private void validateXml(Terminal terminal, Path xml) throws Exception { try (InputStream xmlInput = Files.newInputStream(xml)) { SamlUtils.validate(xmlInput, METADATA_SCHEMA); diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommandTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommandTests.java index b8343062144..c03b095bd2d 100644 --- a/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommandTests.java +++ b/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlMetadataCommandTests.java @@ -18,12 +18,22 @@ import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.RequestedAttribute; import org.opensaml.saml.saml2.metadata.SPSSODescriptor; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.security.credential.Credential; import org.opensaml.security.credential.UsageType; +import org.opensaml.security.x509.BasicX509Credential; import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; +import org.opensaml.xmlsec.signature.Signature; import org.opensaml.xmlsec.signature.X509Certificate; import org.opensaml.xmlsec.signature.X509Data; +import org.opensaml.xmlsec.signature.support.SignatureValidator; +import org.w3c.dom.Element; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.cert.Certificate; import java.util.Collections; import java.util.List; @@ -267,4 +277,270 @@ public class SamlMetadataCommandTests extends SamlTestCase { assertThat(attributes.get(1).getFriendlyName(), equalTo("principal")); assertThat(attributes.get(1).getName(), equalTo("urn:oid:0.9.2342.19200300.100.1.1")); } + + public void testSigningMetadataWithPfx() throws Exception { + final Path certPath = getDataPath("saml.crt"); + final Path keyPath = getDataPath("saml.key"); + final Path p12Path = getDataPath("saml.p12"); + final SamlMetadataCommand command = new SamlMetadataCommand(); + final OptionSet options = command.getParser().parse(new String[]{ + "-signing-bundle", p12Path.toString() + }); + + final boolean useSigningCredentials = randomBoolean(); + final Settings.Builder settingsBuilder = Settings.builder() + .put("path.home", createTempDir()) + .put(RealmSettings.PREFIX + "my_saml.type", "saml") + .put(RealmSettings.PREFIX + "my_saml.order", 1) + .put(RealmSettings.PREFIX + "my_saml.idp.entity_id", "https://okta.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.entity_id", "https://kibana.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.acs", "https://kibana.my.corp/saml/login") + .put(RealmSettings.PREFIX + "my_saml.sp.logout", "https://kibana.my.corp/saml/logout") + .put(RealmSettings.PREFIX + "my_saml.attributes.principal", "urn:oid:0.9.2342.19200300.100.1.1"); + if (useSigningCredentials) { + settingsBuilder.put(RealmSettings.PREFIX + "my_saml.signing.certificate", certPath.toString()) + .put(RealmSettings.PREFIX + "my_saml.signing.key", keyPath.toString()); + } + final Settings settings = settingsBuilder.build(); + final Environment env = TestEnvironment.newEnvironment(settings); + + final MockTerminal terminal = new MockTerminal(); + + // What is the friendly name for "principal" attribute "urn:oid:0.9.2342.19200300.100.1.1" [default: principal] + terminal.addTextInput(""); + terminal.addSecretInput(""); + + final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env); + command.possiblySignDescriptor(terminal, options, descriptor, env); + assertThat(descriptor, notNullValue()); + // Verify generated signature + assertThat(descriptor.getSignature(), notNullValue()); + assertThat(validateSignature(descriptor.getSignature()), equalTo(true)); + // Make sure that Signing didn't mangle the XML at all and we can still read metadata + assertThat(descriptor.getEntityID(), equalTo("https://kibana.my.corp/")); + + assertThat(descriptor.getRoleDescriptors(), iterableWithSize(1)); + assertThat(descriptor.getRoleDescriptors().get(0), instanceOf(SPSSODescriptor.class)); + final SPSSODescriptor spDescriptor = (SPSSODescriptor) descriptor.getRoleDescriptors().get(0); + + assertThat(spDescriptor.getAssertionConsumerServices(), iterableWithSize(1)); + assertThat(spDescriptor.getAssertionConsumerServices().get(0).getLocation(), equalTo("https://kibana.my.corp/saml/login")); + assertThat(spDescriptor.getAssertionConsumerServices().get(0).isDefault(), equalTo(true)); + assertThat(spDescriptor.getAssertionConsumerServices().get(0).getIndex(), equalTo(1)); + assertThat(spDescriptor.getAssertionConsumerServices().get(0).getBinding(), equalTo(SAMLConstants.SAML2_POST_BINDING_URI)); + + final RequestedAttribute uidAttribute = spDescriptor.getAttributeConsumingServices().get(0).getRequestAttributes().get(0); + assertThat(uidAttribute.getName(), equalTo("urn:oid:0.9.2342.19200300.100.1.1")); + assertThat(uidAttribute.getFriendlyName(), equalTo("principal")); + + assertThat(spDescriptor.isAuthnRequestsSigned(), equalTo(useSigningCredentials)); + assertThat(spDescriptor.getWantAssertionsSigned(), equalTo(true)); + } + + public void testSigningMetadataWithPasswordProtectedPfx() throws Exception { + final Path certPath = getDataPath("saml.crt"); + final Path keyPath = getDataPath("saml.key"); + final Path p12Path = getDataPath("saml_with_password.p12"); + final SamlMetadataCommand command = new SamlMetadataCommand(); + final OptionSet options = command.getParser().parse(new String[]{ + "-signing-bundle", p12Path.toString(), + "-signing-key-password", "saml" + }); + + final boolean useSigningCredentials = randomBoolean(); + final Settings.Builder settingsBuilder = Settings.builder() + .put("path.home", createTempDir()) + .put(RealmSettings.PREFIX + "my_saml.type", "saml") + .put(RealmSettings.PREFIX + "my_saml.order", 1) + .put(RealmSettings.PREFIX + "my_saml.idp.entity_id", "https://okta.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.entity_id", "https://kibana.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.acs", "https://kibana.my.corp/saml/login") + .put(RealmSettings.PREFIX + "my_saml.sp.logout", "https://kibana.my.corp/saml/logout"); + if (useSigningCredentials) { + settingsBuilder.put(RealmSettings.PREFIX + "my_saml.signing.certificate", certPath.toString()) + .put(RealmSettings.PREFIX + "my_saml.signing.key", keyPath.toString()); + } + final Settings settings = settingsBuilder.build(); + final Environment env = TestEnvironment.newEnvironment(settings); + + final MockTerminal terminal = new MockTerminal(); + + final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env); + Element e = command.possiblySignDescriptor(terminal, options, descriptor, env); + String a = SamlUtils.toString(e); + assertThat(descriptor, notNullValue()); + // Verify generated signature + assertThat(descriptor.getSignature(), notNullValue()); + assertThat(validateSignature(descriptor.getSignature()), equalTo(true)); + } + + public void testErrorSigningMetadataWithWrongPassword() throws Exception { + final Path certPath = getDataPath("saml.crt"); + final Path keyPath = getDataPath("saml.key"); + final Path p12Path = getDataPath("saml_with_password.p12"); + final SamlMetadataCommand command = new SamlMetadataCommand(); + final OptionSet options = command.getParser().parse(new String[]{ + "-signing-bundle", p12Path.toString(), + "-signing-key-password", "wrong_password" + }); + + final boolean useSigningCredentials = randomBoolean(); + final Settings.Builder settingsBuilder = Settings.builder() + .put("path.home", createTempDir()) + .put(RealmSettings.PREFIX + "my_saml.type", "saml") + .put(RealmSettings.PREFIX + "my_saml.order", 1) + .put(RealmSettings.PREFIX + "my_saml.idp.entity_id", "https://okta.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.entity_id", "https://kibana.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.acs", "https://kibana.my.corp/saml/login") + .put(RealmSettings.PREFIX + "my_saml.sp.logout", "https://kibana.my.corp/saml/logout"); + if (useSigningCredentials) { + settingsBuilder.put(RealmSettings.PREFIX + "my_saml.signing.certificate", certPath.toString()) + .put(RealmSettings.PREFIX + "my_saml.signing.key", keyPath.toString()); + } + final Settings settings = settingsBuilder.build(); + final Environment env = TestEnvironment.newEnvironment(settings); + + final MockTerminal terminal = new MockTerminal(); + + final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env); + final UserException userException = expectThrows(UserException.class, () -> command.possiblySignDescriptor(terminal, options, + descriptor, env)); + assertThat(userException.getMessage(), containsString("Unable to create metadata document")); + assertThat(terminal.getOutput(), containsString("keystore password was incorrect")); + } + + public void testSigningMetadataWithPem() throws Exception { + //Use this keypair for signing the metadata also + final Path certPath = getDataPath("saml.crt"); + final Path keyPath = getDataPath("saml.key"); + + final SamlMetadataCommand command = new SamlMetadataCommand(); + final OptionSet options = command.getParser().parse(new String[]{ + "-signing-cert", certPath.toString(), + "-signing-key", keyPath.toString() + }); + + final boolean useSigningCredentials = randomBoolean(); + final Settings.Builder settingsBuilder = Settings.builder() + .put("path.home", createTempDir()) + .put(RealmSettings.PREFIX + "my_saml.type", "saml") + .put(RealmSettings.PREFIX + "my_saml.order", 1) + .put(RealmSettings.PREFIX + "my_saml.idp.entity_id", "https://okta.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.entity_id", "https://kibana.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.acs", "https://kibana.my.corp/saml/login") + .put(RealmSettings.PREFIX + "my_saml.sp.logout", "https://kibana.my.corp/saml/logout"); + if (useSigningCredentials) { + settingsBuilder.put(RealmSettings.PREFIX + "my_saml.signing.certificate", certPath.toString()) + .put(RealmSettings.PREFIX + "my_saml.signing.key", keyPath.toString()); + } + final Settings settings = settingsBuilder.build(); + final Environment env = TestEnvironment.newEnvironment(settings); + + final MockTerminal terminal = new MockTerminal(); + + final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env); + command.possiblySignDescriptor(terminal, options, descriptor, env); + assertThat(descriptor, notNullValue()); + // Verify generated signature + assertThat(descriptor.getSignature(), notNullValue()); + assertThat(validateSignature(descriptor.getSignature()), equalTo(true)); + } + + public void testSigningMetadataWithPasswordProtectedPem() throws Exception { + //Use same keypair for signing the metadata + final Path signingKeyPath = getDataPath("saml_with_password.key"); + + final Path certPath = getDataPath("saml.crt"); + final Path keyPath = getDataPath("saml.key"); + + final SamlMetadataCommand command = new SamlMetadataCommand(); + final OptionSet options = command.getParser().parse(new String[]{ + "-signing-cert", certPath.toString(), + "-signing-key", signingKeyPath.toString(), + "-signing-key-password", "saml" + + }); + + final boolean useSigningCredentials = randomBoolean(); + final Settings.Builder settingsBuilder = Settings.builder() + .put("path.home", createTempDir()) + .put(RealmSettings.PREFIX + "my_saml.type", "saml") + .put(RealmSettings.PREFIX + "my_saml.order", 1) + .put(RealmSettings.PREFIX + "my_saml.idp.entity_id", "https://okta.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.entity_id", "https://kibana.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.acs", "https://kibana.my.corp/saml/login") + .put(RealmSettings.PREFIX + "my_saml.sp.logout", "https://kibana.my.corp/saml/logout"); + if (useSigningCredentials) { + settingsBuilder.put(RealmSettings.PREFIX + "my_saml.signing.certificate", certPath.toString()) + .put(RealmSettings.PREFIX + "my_saml.signing.key", keyPath.toString()); + } + final Settings settings = settingsBuilder.build(); + final Environment env = TestEnvironment.newEnvironment(settings); + + final MockTerminal terminal = new MockTerminal(); + + final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env); + command.possiblySignDescriptor(terminal, options, descriptor, env); + assertThat(descriptor, notNullValue()); + // Verify generated signature + assertThat(descriptor.getSignature(), notNullValue()); + assertThat(validateSignature(descriptor.getSignature()), equalTo(true)); + } + + public void testSigningMetadataWithPasswordProtectedPemInTerminal() throws Exception { + //Use same keypair for signing the metadata + final Path signingKeyPath = getDataPath("saml_with_password.key"); + + final Path certPath = getDataPath("saml.crt"); + final Path keyPath = getDataPath("saml.key"); + + final SamlMetadataCommand command = new SamlMetadataCommand(); + final OptionSet options = command.getParser().parse(new String[]{ + "-signing-cert", certPath.toString(), + "-signing-key", signingKeyPath.toString() + + }); + + final boolean useSigningCredentials = randomBoolean(); + final Settings.Builder settingsBuilder = Settings.builder() + .put("path.home", createTempDir()) + .put(RealmSettings.PREFIX + "my_saml.type", "saml") + .put(RealmSettings.PREFIX + "my_saml.order", 1) + .put(RealmSettings.PREFIX + "my_saml.idp.entity_id", "https://okta.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.entity_id", "https://kibana.my.corp/") + .put(RealmSettings.PREFIX + "my_saml.sp.acs", "https://kibana.my.corp/saml/login") + .put(RealmSettings.PREFIX + "my_saml.sp.logout", "https://kibana.my.corp/saml/logout"); + if (useSigningCredentials) { + settingsBuilder.put(RealmSettings.PREFIX + "my_saml.signing.certificate", certPath.toString()) + .put(RealmSettings.PREFIX + "my_saml.signing.key", keyPath.toString()); + } + final Settings settings = settingsBuilder.build(); + final Environment env = TestEnvironment.newEnvironment(settings); + + final MockTerminal terminal = new MockTerminal(); + + terminal.addSecretInput("saml"); + + final EntityDescriptor descriptor = command.buildEntityDescriptor(terminal, options, env); + command.possiblySignDescriptor(terminal, options, descriptor, env); + assertThat(descriptor, notNullValue()); + // Verify generated signature + assertThat(descriptor.getSignature(), notNullValue()); + assertThat(validateSignature(descriptor.getSignature()), equalTo(true)); + } + + private boolean validateSignature(Signature signature) { + try { + Certificate[] certificates = CertUtils.readCertificates(Collections.singletonList(getDataPath("saml.crt").toString()), null); + PrivateKey key = CertUtils.readPrivateKey(Files.newBufferedReader(getDataPath("saml.key"), StandardCharsets.UTF_8), + ""::toCharArray); + Credential verificationCredential = new BasicX509Credential((java.security.cert.X509Certificate) certificates[0], key); + SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); + profileValidator.validate(signature); + SignatureValidator.validate(signature, verificationCredential); + return true; + } catch (Exception e) { + return false; + } + } } \ No newline at end of file diff --git a/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml.p12 b/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml.p12 new file mode 100644 index 0000000000000000000000000000000000000000..be2e45d1074aa19f681c701091031e76a8e69ef8 GIT binary patch literal 2461 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 b%ofuCQ+y>0N~;72$MpJ@5m