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 00000000000..be2e45d1074 Binary files /dev/null and b/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml.p12 differ diff --git a/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml_with_password.key b/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml_with_password.key new file mode 100644 index 00000000000..cc9a4fba426 --- /dev/null +++ b/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml_with_password.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,E5CF40A599E7BBB7 + +TnxQWgs1sKEW5OIFMWQS/Iyqz25nIf7321MiEa/Z8lakv2bh8MBJCIp7r3pN4viJ +K5bMJMIsjqUdolcvbuPXgtA6uuhnf/X4o7sU/fZhsgOiBkoj3r2pWOpHHx+am3G7 +uzrVlDgc36X5lVRUZLdnIeT3aLAk8+ObXF624TxISCUPZluGC0NwJlNZ5yxIeSTv +47hd1OVfdKH+IYoa8Illt10njcl398z5AUWiX+j6ozlJnnufi0kRdQJ3Da0q5rUg +IlUpqNmPwG2tl8Ys1tNaU8fpf22rLDo7S2P4S+SxzMvDYXFr/VDy4nFtWDuP6hTQ +VA51txLKOR0+FhZNOUOZH6YjFv4LOUS3/doonPrp/7z4C6cnf3MECtnhOG2zVcBX +FKCg7iKBmml92tOsCIAXCjUlpTHQniNqxNtOiWySlt/83XPLPQSpeNuSismWSHlN +lVUGkcysjEaZu86DVc6NN91s7oG0x/R0UKU17NUZFtCaYCXYtdtRlpgpmMOD8KFp +3NdZUFPQ0zetXqS/skdg1tKd49amKO7Qj+V9nWMzFnwPTM+LD3hV4Ehb2D41nTxz +b0UFNb/vcYUZzP6+OvgrSyhH9f7pYmyt+Ky80wJ1eOB1ZRReo0iXYPZZo4G8spJy +SHc18HswxU+ICMB77tHHDIJXnGQr9yTDYph3ZEs4m/NIP91f0XjeexsRcSDrKImD ++UY6cY8a76HOT70Wl+Na0ZCKE9BkpWLgbkZbH9arhIbW12wSdvO6oPGFU9FOVL2V +L0RMGSYQPowXyktBer+b1ZPrijOYWLqkn/S5prOjCjD/qxnWts9DGeNixrw3F7HY +yEUbl3amc1/Zxi60CsNHmV6wvQIzoz3Pz+U6foIeJxLj5glFQiA1Yhivd1YGWeFk +t3uqNGRe6C6sNzpF4LCKpxnEbZdAM6QzcmHIAcfINuc00zNjV6B3o1L91wXO/bd/ +VJ4zCcO0UxipLhaiXvMpjMOW3uJri5nlm1cWnGWH3M20l7hLQ4sUw0IUh9wA412K +6muTqlvwYQtYwwG0nz99bnKwHIO01vbAQACYk13j37wUsG57DFygxs+fcITV0AfB +PKwAKISmlr45PztXybEXg92mzkBsdXFMhpHeDhYDeC3g7DGXI5cEqbiATu8Gvsy2 +F4ylZMl2gHG4/2GSjEdHLv2uXPmZYFFeoJY+9GdNS6yz0ncn9yahG57OawbXR/fS +NPfLyDg8C+fRC2O0TI4/a51sXy2bEE0NBRdrY1VZp9sc4nsRvUdvxvdd++R+PeNt +oAqRHqTTksRJqBwy3+KMerGk0z9RLPzzLQkBVeH5bQtqhZq43bI6Zf8jDqKlb1H+ +YhkgU4DsO0iPKvtIMEa31U9Qc2nQiIWjSk4v8gr5tcqoekynV4rKosTV1+GSRlkn +l245GuOOE2PKVYn0jrUIn/IGzcMfORRH4/Sl/gy9ikYS70tykyJVoMplIeb1awMa ++FNAD17iNhaLOvuEfL5zCQtjXHyCRzReGxxmO9F2lN26Lr0MzM8k3bIrFTBVL/+n +5pg6I6i8CXFWfpi08fP5KDU447AaBvdozm9L2JWIKaxjHev+NIy2Og2qtR34nBPH +-----END RSA PRIVATE KEY----- diff --git a/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml_with_password.p12 b/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml_with_password.p12 new file mode 100644 index 00000000000..a764c67fc2a Binary files /dev/null and b/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/saml_with_password.p12 differ