[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@7b806d76f8
This commit is contained in:
parent
c63d32482f
commit
febb46b702
|
@ -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
|
* @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.
|
* return the password for that key. If it returns {@code null}, then the key-pair for that alias is not read.
|
||||||
*/
|
*/
|
||||||
static Map<Certificate, Key> readPkcs12KeyPairs(Path path, char[] password, Function<String, char[]> keyPassword, Environment env)
|
public static Map<Certificate, Key> readPkcs12KeyPairs(Path path, char[] password, Function<String, char[]> keyPassword, Environment
|
||||||
|
env)
|
||||||
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException {
|
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableKeyException {
|
||||||
final KeyStore store = readKeyStore(path, "PKCS12", password);
|
final KeyStore store = readKeyStore(path, "PKCS12", password);
|
||||||
final Enumeration<String> enumeration = store.aliases();
|
final Enumeration<String> enumeration = store.aliases();
|
||||||
|
|
|
@ -6,9 +6,17 @@
|
||||||
package org.elasticsearch.xpack.security.authc.saml;
|
package org.elasticsearch.xpack.security.authc.saml;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.Reader;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
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.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -16,6 +24,7 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import joptsimple.OptionParser;
|
import joptsimple.OptionParser;
|
||||||
|
@ -28,6 +37,7 @@ import org.elasticsearch.cli.ExitCodes;
|
||||||
import org.elasticsearch.cli.SuppressForbidden;
|
import org.elasticsearch.cli.SuppressForbidden;
|
||||||
import org.elasticsearch.cli.Terminal;
|
import org.elasticsearch.cli.Terminal;
|
||||||
import org.elasticsearch.cli.UserException;
|
import org.elasticsearch.cli.UserException;
|
||||||
|
import org.elasticsearch.common.CheckedFunction;
|
||||||
import org.elasticsearch.common.Strings;
|
import org.elasticsearch.common.Strings;
|
||||||
import org.elasticsearch.common.io.PathUtils;
|
import org.elasticsearch.common.io.PathUtils;
|
||||||
import org.elasticsearch.common.logging.Loggers;
|
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.RealmConfig;
|
||||||
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
|
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
|
||||||
import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings;
|
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.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.core.AuthnRequest;
|
||||||
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
|
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
|
||||||
import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller;
|
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.w3c.dom.Element;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
|
@ -65,6 +83,10 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand {
|
||||||
private final OptionSpec<String> orgDisplayNameSpec;
|
private final OptionSpec<String> orgDisplayNameSpec;
|
||||||
private final OptionSpec<String> orgUrlSpec;
|
private final OptionSpec<String> orgUrlSpec;
|
||||||
private final OptionSpec<Void> contactsSpec;
|
private final OptionSpec<Void> contactsSpec;
|
||||||
|
private final OptionSpec<String> signingPkcs12PathSpec;
|
||||||
|
private final OptionSpec<String> signingCertPathSpec;
|
||||||
|
private final OptionSpec<String> signingKeyPathSpec;
|
||||||
|
private final OptionSpec<String> keyPasswordSpec;
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
new SamlMetadataCommand().main(args, Terminal.DEFAULT);
|
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")
|
orgUrlSpec = parser.accepts("organisation-url", "the URL of the organisation operating this service")
|
||||||
.requiredIf(orgNameSpec).withRequiredArg();
|
.requiredIf(orgNameSpec).withRequiredArg();
|
||||||
contactsSpec = parser.accepts("contacts", "Include contact information in metadata").availableUnless(batchSpec);
|
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
|
@Override
|
||||||
|
@ -95,7 +129,9 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand {
|
||||||
SamlUtils.initialize(logger);
|
SamlUtils.initialize(logger);
|
||||||
|
|
||||||
final EntityDescriptor descriptor = buildEntityDescriptor(terminal, options, env);
|
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);
|
validateXml(terminal, xml);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,9 +217,42 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand {
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path writeOutput(Terminal terminal, OptionSet options, EntityDescriptor descriptor) throws Exception {
|
// package-protected for testing
|
||||||
final EntityDescriptorMarshaller marshaller = new EntityDescriptorMarshaller();
|
Element possiblySignDescriptor(Terminal terminal, OptionSet options, EntityDescriptor descriptor, Environment env)
|
||||||
final Element element = marshaller.marshall(descriptor);
|
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 Path outputFile = resolvePath(option(outputPathSpec, options, "saml-elasticsearch-metadata.xml"));
|
||||||
final Writer writer = Files.newBufferedWriter(outputFile);
|
final Writer writer = Files.newBufferedWriter(outputFile);
|
||||||
SamlUtils.print(element, writer, true);
|
SamlUtils.print(element, writer, true);
|
||||||
|
@ -191,7 +260,74 @@ public class SamlMetadataCommand extends EnvironmentAwareCommand {
|
||||||
return outputFile;
|
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<Certificate, Key> 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<Certificate, Key> 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, E extends Exception> T withPassword(String description, char[] password, Terminal terminal,
|
||||||
|
CheckedFunction<char[], T, E> 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<char[]> 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 {
|
private void validateXml(Terminal terminal, Path xml) throws Exception {
|
||||||
try (InputStream xmlInput = Files.newInputStream(xml)) {
|
try (InputStream xmlInput = Files.newInputStream(xml)) {
|
||||||
SamlUtils.validate(xmlInput, METADATA_SCHEMA);
|
SamlUtils.validate(xmlInput, METADATA_SCHEMA);
|
||||||
|
|
|
@ -18,12 +18,22 @@ import org.opensaml.saml.common.xml.SAMLConstants;
|
||||||
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
|
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
|
||||||
import org.opensaml.saml.saml2.metadata.RequestedAttribute;
|
import org.opensaml.saml.saml2.metadata.RequestedAttribute;
|
||||||
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
|
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.credential.UsageType;
|
||||||
|
import org.opensaml.security.x509.BasicX509Credential;
|
||||||
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
|
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
|
||||||
|
import org.opensaml.xmlsec.signature.Signature;
|
||||||
import org.opensaml.xmlsec.signature.X509Certificate;
|
import org.opensaml.xmlsec.signature.X509Certificate;
|
||||||
import org.opensaml.xmlsec.signature.X509Data;
|
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.nio.file.Path;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -267,4 +277,270 @@ public class SamlMetadataCommandTests extends SamlTestCase {
|
||||||
assertThat(attributes.get(1).getFriendlyName(), equalTo("principal"));
|
assertThat(attributes.get(1).getFriendlyName(), equalTo("principal"));
|
||||||
assertThat(attributes.get(1).getName(), equalTo("urn:oid:0.9.2342.19200300.100.1.1"));
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Binary file not shown.
|
@ -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-----
|
Binary file not shown.
Loading…
Reference in New Issue