diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java index efc331acde56c..f7f2af023e9d8 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/IdentityProviderPlugin.java @@ -34,11 +34,14 @@ import org.elasticsearch.xpack.idp.action.SamlInitiateSingleSignOnAction; import org.elasticsearch.xpack.idp.action.TransportSamlInitiateSingleSignOnAction; import org.elasticsearch.xpack.idp.rest.action.RestSamlInitiateSingleSignOnAction; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestAction; +import org.elasticsearch.xpack.idp.action.TransportSamlValidateAuthnRequestAction; +import org.elasticsearch.xpack.idp.rest.RestSamlValidateAuthenticationRequestAction; import org.elasticsearch.xpack.idp.saml.idp.CloudIdp; -import org.elasticsearch.xpack.idp.saml.support.SamlUtils; +import org.elasticsearch.xpack.idp.saml.support.SamlInit; -import java.net.URI; -import java.net.URISyntaxException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.Collection; @@ -56,30 +59,30 @@ public class IdentityProviderPlugin extends Plugin implements ActionPlugin { public static final Setting IDP_ENTITY_ID = Setting.simpleString("xpack.idp.entity_id", Setting.Property.NodeScope); public static final Setting IDP_SSO_REDIRECT_ENDPOINT = Setting.simpleString("xpack.idp.sso_endpoint.redirect", value -> { try { - new URI(value); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.redirect]. Not a valid URI", e); + new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.redirect]. Not a valid URL", e); } }, Setting.Property.NodeScope); public static final Setting IDP_SSO_POST_ENDPOINT = Setting.simpleString("xpack.idp.sso_endpoint.post", value -> { try { - new URI(value); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.post]. Not a valid URI", e); + new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.sso_endpoint.post]. Not a valid URL", e); } }, Setting.Property.NodeScope); public static final Setting IDP_SLO_REDIRECT_ENDPOINT = Setting.simpleString("xpack.idp.slo_endpoint.redirect", value -> { try { - new URI(value); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.redirect]. Not a valid URI", e); + new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.redirect]. Not a valid URL", e); } }, Setting.Property.NodeScope); public static final Setting IDP_SLO_POST_ENDPOINT = Setting.simpleString("xpack.idp.slo_endpoint.post", value -> { try { - new URI(value); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.post]. Not a valid URI", e); + new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid value [" + value + "] for [xpack.idp.slo_endpoint.post]. Not a valid URL", e); } }, Setting.Property.NodeScope); public static final Setting IDP_SIGNING_KEY_ALIAS = Setting.simpleString("xpack.idp.signing.keystore.alias", @@ -101,7 +104,7 @@ public Collection createComponents(Client client, ClusterService cluster return List.of(); } - SamlUtils.initialize(); + SamlInit.initialize(); CloudIdp idp = new CloudIdp(environment, settings); return List.of(); } @@ -111,8 +114,9 @@ public Collection createComponents(Client client, ClusterService cluster if (enabled == false) { return Collections.emptyList(); } - return Collections.singletonList( - new ActionHandler<>(SamlInitiateSingleSignOnAction.INSTANCE, TransportSamlInitiateSingleSignOnAction.class) + return List.of( + new ActionHandler<>(SamlInitiateSingleSignOnAction.INSTANCE, TransportSamlInitiateSingleSignOnAction.class), + new ActionHandler<>(SamlValidateAuthnRequestAction.INSTANCE, TransportSamlValidateAuthnRequestAction.class) ); } @@ -124,7 +128,9 @@ public List getRestHandlers(Settings settings, RestController restC if (enabled == false) { return Collections.emptyList(); } - return Collections.singletonList(new RestSamlInitiateSingleSignOnAction()); + return List.of( + new RestSamlInitiateSingleSignOnAction(), + new RestSamlValidateAuthenticationRequestAction()); } @Override @@ -139,4 +145,12 @@ public List> getSettings() { settings.addAll(X509KeyPairSettings.withPrefix("xpack.idp.signing.", false).getAllSettings()); return Collections.unmodifiableList(settings); } + + private static URL parseURL(String key, String value) { + try { + return new URL(value); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid value [" + value + "] for [" + key + "]. Not a valid URL", e); + } + } } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestAction.java new file mode 100644 index 0000000000000..bd246d0f9ff7f --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestAction.java @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.action; + +import org.elasticsearch.action.ActionType; + +public class SamlValidateAuthnRequestAction extends ActionType { + + public static final String NAME = "cluster:admin/idp/saml/validate"; + public static final SamlValidateAuthnRequestAction INSTANCE = new SamlValidateAuthnRequestAction(); + + private SamlValidateAuthnRequestAction() { + super(NAME, SamlValidateAuthnRequestResponse::new); + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java new file mode 100644 index 0000000000000..1fffcf23871d0 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequest.java @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.action; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +public class SamlValidateAuthnRequestRequest extends ActionRequest { + + private String queryString; + + public SamlValidateAuthnRequestRequest(StreamInput in) throws IOException { + super(in); + queryString = in.readString(); + } + + public SamlValidateAuthnRequestRequest() { + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.isNullOrEmpty(queryString)) { + validationException = addValidationError("Authentication request query string must be provided", validationException); + } + return validationException; + } + + public String getQueryString() { + return queryString; + } + + public void setQueryString(String queryString) { + this.queryString = queryString; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(queryString); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{queryString='" + queryString + "'}"; + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java new file mode 100644 index 0000000000000..15f9d2f4a0c34 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestResponse.java @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.action; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +public class SamlValidateAuthnRequestResponse extends ActionResponse { + + private final String spEntityId; + private final boolean forceAuthn; + private final Map authnState; + + public SamlValidateAuthnRequestResponse(StreamInput in) throws IOException { + super(in); + this.spEntityId = in.readString(); + this.forceAuthn = in.readBoolean(); + this.authnState = in.readMap(); + } + + public SamlValidateAuthnRequestResponse(String spEntityId, boolean forceAuthn, Map authnState) { + this.spEntityId = Objects.requireNonNull(spEntityId); + this.forceAuthn = forceAuthn; + this.authnState = Map.copyOf(Objects.requireNonNull(authnState)); + } + + public String getSpEntityId() { + return spEntityId; + } + + public boolean isForceAuthn() { + return forceAuthn; + } + + public Map getAuthnState() { + return authnState; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(spEntityId); + out.writeBoolean(forceAuthn); + out.writeMap(authnState); + + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{ spEntityId='" + getSpEntityId() + "',\n" + + " forceAuthn='" + isForceAuthn() + "',\n" + + " additionalData='" + getAuthnState() + "' }"; + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlInitiateSingleSignOnAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlInitiateSingleSignOnAction.java index a9b87211967de..4bc9601498978 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlInitiateSingleSignOnAction.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlInitiateSingleSignOnAction.java @@ -25,7 +25,6 @@ import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; import org.elasticsearch.xpack.idp.saml.support.SamlFactory; -import org.elasticsearch.xpack.idp.saml.support.SamlUtils; import org.opensaml.saml.saml2.core.Response; import java.io.IOException; @@ -72,7 +71,7 @@ protected void doExecute(Task task, SamlInitiateSingleSignOnRequest request, Clock.systemUTC(), idp); final Response response = builder.build(user, null); listener.onResponse(new SamlInitiateSingleSignOnResponse(user.getServiceProvider().getAssertionConsumerService().toString(), - SamlUtils.getXmlContent(response), + samlFactory.getXmlContent(response), user.getServiceProvider().getEntityId())); } catch (IOException e) { listener.onFailure(new IllegalArgumentException(e.getMessage())); diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java new file mode 100644 index 0000000000000..3fb9a715ef2a0 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/action/TransportSamlValidateAuthnRequestAction.java @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.action; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.env.Environment; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.idp.saml.authn.SamlAuthnRequestValidator; +import org.elasticsearch.xpack.idp.saml.idp.CloudIdp; +import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; +import org.elasticsearch.xpack.idp.saml.support.SamlFactory; + +public class TransportSamlValidateAuthnRequestAction extends HandledTransportAction { + + private final Environment env; + + @Inject + public TransportSamlValidateAuthnRequestAction(TransportService transportService, ActionFilters actionFilters, + Environment environment) { + super(SamlValidateAuthnRequestAction.NAME, transportService, actionFilters, SamlValidateAuthnRequestRequest::new); + this.env = environment; + } + + @Override + protected void doExecute(Task task, SamlValidateAuthnRequestRequest request, + ActionListener listener) { + final SamlIdentityProvider idp = new CloudIdp(env, env.settings()); + final SamlFactory samlFactory = new SamlFactory(); + final SamlAuthnRequestValidator validator = new SamlAuthnRequestValidator(samlFactory, idp); + try { + validator.processQueryString(request.getQueryString(), listener); + } catch (Exception e) { + listener.onFailure(e); + } + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java new file mode 100644 index 0000000000000..654cf5493fa9d --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/rest/RestSamlValidateAuthenticationRequestAction.java @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.rest; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestAction; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestRequest; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestResponse; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +public class RestSamlValidateAuthenticationRequestAction extends BaseRestHandler { + + static final ObjectParser PARSER = + new ObjectParser<>("idp_validate_authn_request", SamlValidateAuthnRequestRequest::new); + + static { + PARSER.declareString(SamlValidateAuthnRequestRequest::setQueryString, new ParseField("authn_request_query")); + } + + @Override + public String getName() { + return "saml_idp_validate_authn_request_action"; + } + + @Override + public List routes() { + return Collections.singletonList(new Route(POST, "/_idp/saml/validate")); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final SamlValidateAuthnRequestRequest validateRequest = PARSER.parse(parser, null); + return channel -> client.execute(SamlValidateAuthnRequestAction.INSTANCE, validateRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(SamlValidateAuthnRequestResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + builder.startObject("service_provider"); + builder.field("entity_id", response.getSpEntityId()); + builder.endObject(); + builder.field("force_authn", response.isForceAuthn()); + builder.field("authn_state", response.getAuthnState()); + builder.endObject(); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + } +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java new file mode 100644 index 0000000000000..34d536d5453ec --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidator.java @@ -0,0 +1,281 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.saml.authn; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.core.internal.io.Streams; +import org.elasticsearch.rest.RestUtils; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestResponse; +import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; +import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; +import org.elasticsearch.xpack.idp.saml.support.SamlFactory; +import org.elasticsearch.xpack.idp.saml.support.SamlInit; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.NameIDPolicy; +import org.opensaml.security.x509.X509Credential; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.Signature; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_REDIRECT_BINDING_URI; +import static org.opensaml.saml.saml2.core.NameIDType.UNSPECIFIED; + +/* + * Processes a SAML AuthnRequest, validates it and extracts necessary information + */ +public class SamlAuthnRequestValidator { + + private final SamlFactory samlFactory; + private final SamlIdentityProvider idp; + private final Logger logger = LogManager.getLogger(SamlAuthnRequestValidator.class); + private static final String[] XSD_FILES = new String[]{"/org/elasticsearch/xpack/idp/saml/support/saml-schema-protocol-2.0.xsd", + "/org/elasticsearch/xpack/idp/saml/support/saml-schema-assertion-2.0.xsd", + "/org/elasticsearch/xpack/idp/saml/support/xenc-schema.xsd", + "/org/elasticsearch/xpack/idp/saml/support/xmldsig-core-schema.xsd"}; + + private static final ThreadLocal THREAD_LOCAL_DOCUMENT_BUILDER = ThreadLocal.withInitial(() -> { + try { + return SamlFactory.getHardenedBuilder(XSD_FILES); + } catch (Exception e) { + throw new ElasticsearchSecurityException("Could not load XSD schema file", e); + } + }); + + public SamlAuthnRequestValidator(SamlFactory samlFactory, SamlIdentityProvider idp) { + SamlInit.initialize(); + this.samlFactory = samlFactory; + this.idp = idp; + } + + public void processQueryString(String queryString, ActionListener listener) { + try { + final Map parameters = new HashMap<>(); + RestUtils.decodeQueryString(queryString, 0, parameters); + if (parameters.isEmpty()) { + logAndRespond("Invalid Authentication Request query string (zero parameters)", listener); + return; + } + logger.trace(new ParameterizedMessage("Parsed the following parameters from the query string: {}", parameters)); + final String samlRequest = parameters.get("SAMLRequest"); + final String relayState = parameters.get("RelayState"); + final String sigAlg = parameters.get("SigAlg"); + final String signature = parameters.get("Signature"); + if (null == samlRequest) { + logAndRespond(new ParameterizedMessage("Query string [{}] does not contain a SAMLRequest parameter", queryString), + listener); + return; + } + // We consciously parse the AuthnRequest before we validate its signature as we need to get the Issuer, in order to + // verify if we know of this SP and get its credentials for signature verification + final Element root = parseSamlMessage(inflate(decodeBase64(samlRequest))); + if (samlFactory.elementNameMatches(root, "urn:oasis:names:tc:SAML:2.0:protocol", "AuthnRequest") == false) { + logAndRespond(new ParameterizedMessage("SAML message [{}] is not an AuthnRequest", samlFactory.text(root, 128)), listener); + return; + } + final AuthnRequest authnRequest = samlFactory.buildXmlObject(root, AuthnRequest.class); + final SamlServiceProvider sp = getSpFromIssuer(authnRequest.getIssuer()); + // If the Service Provider should not sign requests, do not try to handle signatures even if they are added to the request + if (sp.shouldSignAuthnRequests()) { + if (Strings.hasText(signature)) { + if (Strings.hasText(sigAlg) == false) { + logAndRespond(new ParameterizedMessage("Query string [{}] contains a Signature but SigAlg parameter is missing", + queryString), listener); + return; + } + final X509Credential spSigningCredential = sp.getSigningCredential(); + if (spSigningCredential == null) { + logAndRespond( + "Unable to validate signature of authentication request, " + + "Service Provider hasn't registered signing credentials", + listener); + return; + } + if (validateSignature(samlRequest, sigAlg, signature, sp.getSigningCredential(), relayState) == false) { + logAndRespond( + new ParameterizedMessage("Unable to validate signature of authentication request [{}] using credentials [{}]", + queryString, samlFactory.describeCredentials(Collections.singletonList(sp.getSigningCredential()))), listener); + return; + } + } else if (Strings.hasText(sigAlg)) { + logAndRespond(new ParameterizedMessage("Query string [{}] contains a SigAlg parameter but Signature is missing", + queryString), listener); + return; + } else { + logAndRespond("The Service Provider must sign authentication requests but no signature was found", listener); + return; + } + } + validateAuthnRequest(authnRequest, sp); + Map authnState = buildAuthnState(authnRequest, sp); + final SamlValidateAuthnRequestResponse response = new SamlValidateAuthnRequestResponse(sp.getEntityId(), + authnRequest.isForceAuthn(), authnState); + logger.trace(new ParameterizedMessage("Validated AuthnResponse from queryString [{}] and extracted [{}]", + queryString, response)); + listener.onResponse(response); + } catch (ElasticsearchSecurityException e) { + logger.debug("Could not process and validate AuthnRequest", e); + listener.onFailure(e); + } catch (Exception e) { + logAndRespond("Could not process and validate AuthnRequest", e, listener); + } + } + + private Map buildAuthnState(AuthnRequest request, SamlServiceProvider sp) { + Map authnState = new HashMap<>(); + final NameIDPolicy nameIDPolicy = request.getNameIDPolicy(); + if (null != nameIDPolicy) { + final String requestedFormat = request.getNameIDPolicy().getFormat(); + if (Strings.hasText(requestedFormat)) { + authnState.put("nameid_format", requestedFormat); + // we should not throw an error. Pass this as additional data so that the /saml/init API can + // return a SAML response with the appropriate status (3.4.1.1 in the core spec) + if (requestedFormat.equals(UNSPECIFIED) == false && sp.getAllowedNameIdFormats().contains(requestedFormat) == false) { + logger.warn(() -> + new ParameterizedMessage("The requested NameID format [{}] doesn't match the allowed NameID formats" + + "for this Service Provider are {}", requestedFormat, sp.getAllowedNameIdFormats())); + authnState.put("error", "invalid_nameid_policy"); + } + } + } + return authnState; + } + + private void validateAuthnRequest(AuthnRequest authnRequest, SamlServiceProvider sp) { + checkDestination(authnRequest); + checkAcs(authnRequest, sp); + } + + private boolean validateSignature(String samlRequest, String sigAlg, String signature, X509Credential credential, + @Nullable String relayState) { + try { + final String queryParam = relayState == null ? + "SAMLRequest=" + urlEncode(samlRequest) + "&SigAlg=" + urlEncode(sigAlg) : + "SAMLRequest=" + urlEncode(samlRequest) + "&RelayState=" + urlEncode(relayState) + "&SigAlg=" + urlEncode(sigAlg); + Signature sig = Signature.getInstance(samlFactory.getJavaAlorithmNameFromUri(sigAlg)); + sig.initVerify(credential.getEntityCertificate().getPublicKey()); + sig.update(queryParam.getBytes(StandardCharsets.UTF_8)); + return sig.verify(Base64.getDecoder().decode(signature)); + } catch (Exception e) { + throw new ElasticsearchSecurityException("Unable to validate signature of authentication request using credentials [{}]", + samlFactory.describeCredentials(Collections.singletonList(credential)), e); + } + } + + private SamlServiceProvider getSpFromIssuer(Issuer issuer) { + if (issuer == null || issuer.getValue() == null) { + throw new ElasticsearchSecurityException("SAML authentication request has no issuer"); + } + final String issuerString = issuer.getValue(); + final SamlServiceProvider serviceProvider = idp.getRegisteredServiceProvider(issuerString); + if (null == serviceProvider) { + throw new ElasticsearchSecurityException("Service Provider with Entity ID [{}] is not registered with this Identity Provider", + issuerString); + } + return serviceProvider; + } + + private void checkDestination(AuthnRequest request) { + final String url = idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI).toString(); + if (url.equals(request.getDestination()) == false) { + throw new ElasticsearchSecurityException( + "SAML authentication request [{}] is for destination [{}] but the SSO endpoint of this Identity Provider is [{}]", + request.getID(), request.getDestination(), url); + } + } + + private void checkAcs(AuthnRequest request, SamlServiceProvider sp) { + final String acs = request.getAssertionConsumerServiceURL(); + if (Strings.hasText(acs) == false) { + final String message = request.getAssertionConsumerServiceIndex() == null ? + "SAML authentication does not contain an AssertionConsumerService URL" : + "SAML authentication does not contain an AssertionConsumerService URL. It contains an Assertion Consumer Service Index " + + "but this IDP doesn't support multiple AssertionConsumerService URLs."; + throw new ElasticsearchSecurityException(message); + } + if (acs.equals(sp.getAssertionConsumerService().toString()) == false) { + throw new ElasticsearchSecurityException("The registered ACS URL for this Service Provider is [{}] but the authentication " + + "request contained [{}]", sp.getAssertionConsumerService(), acs); + } + } + + protected Element parseSamlMessage(byte[] content) { + final Element root; + try (ByteArrayInputStream input = new ByteArrayInputStream(content)) { + // This will parse and validate the input against the schemas + final Document doc = THREAD_LOCAL_DOCUMENT_BUILDER.get().parse(input); + root = doc.getDocumentElement(); + if (logger.isTraceEnabled()) { + logger.trace("Received SAML Message: {} \n", samlFactory.toString(root, true)); + } + } catch (SAXException | IOException e) { + throw new ElasticsearchSecurityException("Failed to parse SAML message", e); + } + return root; + } + + private byte[] decodeBase64(String content) { + try { + return Base64.getDecoder().decode(content.replaceAll("\\s+", "")); + } catch (IllegalArgumentException e) { + logger.info("Failed to decode base64 string [{}] - {}", content, e); + throw new ElasticsearchSecurityException("SAML message cannot be Base64 decoded", e); + } + } + + private byte[] inflate(byte[] bytes) { + Inflater inflater = new Inflater(true); + try (ByteArrayInputStream in = new ByteArrayInputStream(bytes); + InflaterInputStream inflate = new InflaterInputStream(in, inflater); + ByteArrayOutputStream out = new ByteArrayOutputStream(bytes.length * 3 / 2)) { + Streams.copy(inflate, out); + return out.toByteArray(); + } catch (IOException e) { + throw new ElasticsearchSecurityException("SAML message cannot be inflated", e); + } + } + + private String urlEncode(String param) throws UnsupportedEncodingException { + return URLEncoder.encode(param, StandardCharsets.UTF_8.name()); + } + + private void logAndRespond(String message, ActionListener listener) { + logger.debug(message); + listener.onFailure(new ElasticsearchSecurityException(message)); + } + + private void logAndRespond(String message, Throwable e, ActionListener listener) { + logger.debug(message); + listener.onFailure(new ElasticsearchSecurityException(message, e)); + } + + private void logAndRespond(ParameterizedMessage message, ActionListener listener) { + logger.debug(message.getFormattedMessage()); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage())); + } + +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilder.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilder.java index 93c532be78404..6b2d0e8d2cc9f 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilder.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilder.java @@ -12,6 +12,7 @@ import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; import org.elasticsearch.xpack.idp.saml.support.SamlFactory; +import org.elasticsearch.xpack.idp.saml.support.SamlInit; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.opensaml.core.xml.schema.XSString; @@ -45,11 +46,12 @@ */ public class SuccessfulAuthenticationResponseMessageBuilder { - private final SamlFactory samlFactory; private final Clock clock; private final SamlIdentityProvider idp; + private final SamlFactory samlFactory; public SuccessfulAuthenticationResponseMessageBuilder(SamlFactory samlFactory, Clock clock, SamlIdentityProvider idp) { + SamlInit.initialize(); this.samlFactory = samlFactory; this.clock = clock; this.idp = idp; diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java index 9c9e9ac41bf72..0b17ccbd18213 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/idp/CloudIdp.java @@ -33,6 +33,9 @@ import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SLO_REDIRECT_ENDPOINT; import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SSO_POST_ENDPOINT; import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SSO_REDIRECT_ENDPOINT; +import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_POST_BINDING_URI; +import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_REDIRECT_BINDING_URI; +import static org.opensaml.saml.saml2.core.NameIDType.TRANSIENT; public class CloudIdp implements SamlIdentityProvider { @@ -44,15 +47,15 @@ public class CloudIdp implements SamlIdentityProvider { public CloudIdp(Environment env, Settings settings) { this.entityId = require(settings, IDP_ENTITY_ID); - this.ssoEndpoints.put("redirect", require(settings, IDP_SSO_REDIRECT_ENDPOINT)); + this.ssoEndpoints.put(SAML2_REDIRECT_BINDING_URI, require(settings, IDP_SSO_REDIRECT_ENDPOINT)); if (settings.hasValue(IDP_SSO_POST_ENDPOINT.getKey())) { - this.ssoEndpoints.put("post", settings.get(IDP_SSO_POST_ENDPOINT.getKey())); + this.ssoEndpoints.put(SAML2_POST_BINDING_URI, IDP_SSO_POST_ENDPOINT.get(settings)); } if (settings.hasValue(IDP_SLO_POST_ENDPOINT.getKey())) { - this.sloEndpoints.put("post", settings.get(IDP_SLO_POST_ENDPOINT.getKey())); + this.sloEndpoints.put(SAML2_POST_BINDING_URI, IDP_SLO_POST_ENDPOINT.get(settings)); } if (settings.hasValue(IDP_SLO_REDIRECT_ENDPOINT.getKey())) { - this.sloEndpoints.put("redirect", settings.get(IDP_SLO_REDIRECT_ENDPOINT.getKey())); + this.sloEndpoints.put(SAML2_REDIRECT_BINDING_URI, IDP_SLO_REDIRECT_ENDPOINT.get(settings)); } this.signingCredential = buildSigningCredential(env, settings); this.registeredServiceProviders = gatherRegisteredServiceProviders(); @@ -81,10 +84,8 @@ public X509Credential getSigningCredential() { @Override public SamlServiceProvider getRegisteredServiceProvider(String spEntityId) { return registeredServiceProviders.get(spEntityId); - } - private static String require(Settings settings, Setting setting) { if (settings.hasValue(setting.getKey())) { return setting.get(settings); @@ -139,13 +140,13 @@ static X509Credential buildSigningCredential(Environment env, Settings settings) return new X509KeyManagerX509CredentialAdapter(keyManager, selectedAlias); } - private Map gatherRegisteredServiceProviders() { // TODO Fetch all the registered service providers from the index (?) they are persisted. // For now hardcode something to use. Map registeredSps = new HashMap<>(); registeredSps.put("https://sp.some.org", - new CloudServiceProvider("https://sp.some.org", "https://sp.some.org/api/security/v1/saml")); + new CloudServiceProvider("https://sp.some.org", "https://sp.some.org/api/security/v1/saml", Set.of(TRANSIENT), null, false, + false, null)); return registeredSps; } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java index 7e2311362869e..c65924ecb515a 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/CloudServiceProvider.java @@ -6,34 +6,49 @@ package org.elasticsearch.xpack.idp.saml.sp; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.xpack.idp.privileges.ServiceProviderPrivileges; import org.joda.time.Duration; import org.joda.time.ReadableDuration; +import org.opensaml.security.x509.X509Credential; -import java.net.URI; -import java.net.URISyntaxException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.Map; +import java.util.Set; + public class CloudServiceProvider implements SamlServiceProvider { private final String entityid; - private final URI assertionConsumerService; + private final URL assertionConsumerService; private final ReadableDuration authnExpiry; private final ServiceProviderPrivileges privileges; + private final Set allowedNameIdFormats; + private final X509Credential signingCredential; + private final boolean signAuthnRequests; + private final boolean signLogoutRequests; - public CloudServiceProvider(String entityId, String assertionConsumerService) { + public CloudServiceProvider(String entityId, String assertionConsumerService, Set allowedNameIdFormats, + ServiceProviderPrivileges privileges, boolean signAuthnRequests, boolean signLogoutRequests, + @Nullable X509Credential signingCredential) { if (Strings.isNullOrEmpty(entityId)) { throw new IllegalArgumentException("Service Provider Entity ID cannot be null or empty"); } this.entityid = entityId; try { - this.assertionConsumerService = new URI(assertionConsumerService); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid URI for Assertion Consumer Service", e); + this.assertionConsumerService = new URL(assertionConsumerService); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Invalid URL for Assertion Consumer Service", e); } + this.allowedNameIdFormats = Set.copyOf(allowedNameIdFormats); this.authnExpiry = Duration.standardMinutes(5); this.privileges = new ServiceProviderPrivileges("cloud-idp", "service$" + entityId, "action:sso", Map.of()); + this.signingCredential = signingCredential; + this.signLogoutRequests = signLogoutRequests; + this.signAuthnRequests = signAuthnRequests; + } @Override @@ -42,7 +57,12 @@ public String getEntityId() { } @Override - public URI getAssertionConsumerService() { + public Set getAllowedNameIdFormats() { + return allowedNameIdFormats; + } + + @Override + public URL getAssertionConsumerService() { return assertionConsumerService; } @@ -56,6 +76,21 @@ public AttributeNames getAttributeNames() { return new SamlServiceProvider.AttributeNames(); } + @Override + public X509Credential getSigningCredential() { + return signingCredential; + } + + @Override + public boolean shouldSignAuthnRequests() { + return signAuthnRequests; + } + + @Override + public boolean shouldSignLogoutRequests() { + return signLogoutRequests; + } + @Override public ServiceProviderPrivileges getPrivileges() { return privileges; diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java index 2c5c9e317bed4..a17bdfcc3956f 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java @@ -6,10 +6,13 @@ package org.elasticsearch.xpack.idp.saml.sp; +import org.elasticsearch.common.Nullable; import org.elasticsearch.xpack.idp.privileges.ServiceProviderPrivileges; import org.joda.time.ReadableDuration; +import org.opensaml.security.x509.X509Credential; -import java.net.URI; +import java.net.URL; +import java.util.Set; /** * SAML 2.0 configuration information about a specific service provider @@ -17,7 +20,9 @@ public interface SamlServiceProvider { String getEntityId(); - URI getAssertionConsumerService(); + Set getAllowedNameIdFormats(); + + URL getAssertionConsumerService(); ReadableDuration getAuthnExpiry(); @@ -27,5 +32,12 @@ class AttributeNames { AttributeNames getAttributeNames(); + @Nullable + X509Credential getSigningCredential(); + + boolean shouldSignAuthnRequests(); + + boolean shouldSignLogoutRequests(); + ServiceProviderPrivileges getPrivileges(); } diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java index c4a3eb8d22e32..66cf4fa96de94 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlFactory.java @@ -3,20 +3,53 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - package org.elasticsearch.xpack.idp.saml.support; -import org.elasticsearch.ElasticsearchException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.hash.MessageDigests; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.XMLObjectBuilderFactory; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.core.xml.io.UnmarshallerFactory; +import org.opensaml.core.xml.io.UnmarshallingException; import org.opensaml.core.xml.util.XMLObjectSupport; +import org.opensaml.saml.common.SAMLObject; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.x509.X509Credential; import org.w3c.dom.Element; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import javax.xml.XMLConstants; import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URISyntaxException; import java.security.SecureRandom; +import java.security.cert.CertificateEncodingException; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getUnmarshallerFactory; /** * Utility object for constructing new objects and values in a SAML 2.0 / OpenSAML context @@ -25,9 +58,10 @@ public class SamlFactory { private final XMLObjectBuilderFactory builderFactory; private final SecureRandom random; + private static final Logger LOGGER = LogManager.getLogger(SamlFactory.class); public SamlFactory() { - SamlUtils.initialize(); + SamlInit.initialize(); builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory(); random = new SecureRandom(); } @@ -62,11 +96,264 @@ private String randomNCName(int numberBytes) { return "_".concat(MessageDigests.toHexString(randomBytes)); } + public T buildObject(Class type, QName elementName) { + final XMLObject obj = builderFactory.getBuilder(elementName).buildObject(elementName); + if (type.isInstance(obj)) { + return type.cast(obj); + } else { + throw new IllegalArgumentException("Object for element " + elementName.getLocalPart() + " is of type " + obj.getClass() + + " not " + type); + } + } + + public String toString(Element element, boolean pretty) { + try { + StringWriter writer = new StringWriter(); + print(element, writer, pretty); + return writer.toString(); + } catch (TransformerException e) { + return "[" + element.getNamespaceURI() + "]" + element.getLocalName(); + } + } + + public T buildXmlObject(Element element, Class type) { + try { + UnmarshallerFactory unmarshallerFactory = getUnmarshallerFactory(); + Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); + if (unmarshaller == null) { + throw new ElasticsearchSecurityException("XML element [{}] cannot be unmarshalled to SAML type [{}] (no unmarshaller)", + element.getTagName(), type); + } + final XMLObject object = unmarshaller.unmarshall(element); + if (type.isInstance(object)) { + return type.cast(object); + } + Object[] args = new Object[]{element.getTagName(), type.getName(), object.getClass().getName()}; + throw new ElasticsearchSecurityException("SAML object [{}] is incorrect type. Expected [{}] but was [{}]", args); + } catch (UnmarshallingException e) { + throw new ElasticsearchSecurityException("Failed to unmarshall SAML content [{}]", e, element.getTagName()); + } + } + + void print(Element element, Writer writer, boolean pretty) throws TransformerException { + final Transformer serializer = getHardenedXMLTransformer(); + if (pretty) { + serializer.setOutputProperty(OutputKeys.INDENT, "yes"); + } + serializer.transform(new DOMSource(element), new StreamResult(writer)); + } + + public String getXmlContent(SAMLObject object){ + return getXmlContent(object, false); + } + + public String getXmlContent(SAMLObject object, boolean prettyPrint) { + try { + return toString(XMLObjectSupport.marshall(object), prettyPrint); + } catch (MarshallingException e) { + LOGGER.info("Error marshalling SAMLObject ", e); + return "_unserializable_"; + } + } + + public boolean elementNameMatches(Element element, String namespace, String localName) { + return localName.equals(element.getLocalName()) && namespace.equals(element.getNamespaceURI()); + } + + public String text(Element dom, int length) { + return text(dom, length, 0); + } + + public String text(XMLObject xml, int prefixLength, int suffixLength) { + final Element dom = xml.getDOM(); + if (dom == null) { + return null; + } + return text(dom, prefixLength, suffixLength); + } + + public String text(XMLObject xml, int length) { + return text(xml, length, 0); + } + + protected static String text(Element dom, int prefixLength, int suffixLength) { + + final String text = dom.getTextContent().trim(); + final int totalLength = prefixLength + suffixLength; + if (text.length() > totalLength) { + final String prefix = Strings.cleanTruncate(text, prefixLength) + "..."; + if (suffixLength == 0) { + return prefix; + } + int suffixIndex = text.length() - suffixLength; + if (Character.isHighSurrogate(text.charAt(suffixIndex))) { + suffixIndex++; + } + return prefix + text.substring(suffixIndex); + } else { + return text; + } + } + + public String describeCredentials(List credentials) { + return credentials.stream() + .map(c -> { + if (c == null) { + return ""; + } + byte[] encoded; + if (c instanceof X509Credential) { + X509Credential x = (X509Credential) c; + try { + encoded = x.getEntityCertificate().getEncoded(); + } catch (CertificateEncodingException e) { + encoded = c.getPublicKey().getEncoded(); + } + } else { + encoded = c.getPublicKey().getEncoded(); + } + return Base64.getEncoder().encodeToString(encoded).substring(0, 64) + "..."; + }) + .collect(Collectors.joining(",")); + } + public Element toDomElement(XMLObject object) { try { return XMLObjectSupport.marshall(object); } catch (MarshallingException e) { - throw new ElasticsearchException("failed to marshall SAML object to DOM element", e); + throw new ElasticsearchSecurityException("failed to marshall SAML object to DOM element", e); } } + + + @SuppressForbidden(reason = "This is the only allowed way to construct a Transformer") + public Transformer getHardenedXMLTransformer() throws TransformerConfigurationException { + final TransformerFactory tfactory = TransformerFactory.newInstance(); + tfactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + tfactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + tfactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + tfactory.setAttribute("indent-number", 2); + Transformer transformer = tfactory.newTransformer(); + transformer.setErrorListener(new SamlFactory.TransformerErrorListener()); + return transformer; + } + + /** + * Constructs a DocumentBuilder with all the necessary features for it to be secure + * + * @throws ParserConfigurationException if one of the features can't be set on the DocumentBuilderFactory + */ + @SuppressForbidden(reason = "This is the only allowed way to construct a DocumentBuilder") + public static DocumentBuilder getHardenedBuilder(String[] schemaFiles) throws ParserConfigurationException { + final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + // Ensure that Schema Validation is enabled for the factory + dbf.setValidating(true); + // Disallow internal and external entity expansion + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + dbf.setFeature("http://xml.org/sax/features/validation", true); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); + dbf.setIgnoringComments(true); + // This is required, otherwise schema validation causes signature invalidation + dbf.setFeature("http://apache.org/xml/features/validation/schema/normalized-value", false); + // Make sure that URL schema namespaces are not resolved/downloaded from URLs we do not control + dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "file,jar"); + dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file,jar"); + dbf.setFeature("http://apache.org/xml/features/honour-all-schemaLocations", true); + // Ensure we do not resolve XIncludes. Defaults to false, but set it explicitly to be future-proof + dbf.setXIncludeAware(false); + // Ensure we do not expand entity reference nodes + dbf.setExpandEntityReferences(false); + // Further limit danger from denial of service attacks + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + dbf.setAttribute("http://apache.org/xml/features/validation/schema", true); + dbf.setAttribute("http://apache.org/xml/features/validation/schema-full-checking", true); + dbf.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaLanguage", + XMLConstants.W3C_XML_SCHEMA_NS_URI); + // We ship our own xsd files for schema validation since we do not trust anyone else. + dbf.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaSource", resolveSchemaFilePaths(schemaFiles)); + DocumentBuilder documentBuilder = dbf.newDocumentBuilder(); + documentBuilder.setErrorHandler(new SamlFactory.DocumentBuilderErrorHandler()); + return documentBuilder; + } + + public String getJavaAlorithmNameFromUri(String sigAlg) { + switch (sigAlg) { + case "http://www.w3.org/2000/09/xmldsig#dsa-sha1": + return "SHA1withDSA"; + case "http://www.w3.org/2000/09/xmldsig#dsa-sha256": + return "SHA256withDSA"; + case "http://www.w3.org/2000/09/xmldsig#rsa-sha1": + return "SHA1withRSA"; + case "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": + return "SHA256withRSA"; + case "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256": + return "SHA256withECDSA"; + default: + throw new IllegalArgumentException("Unsupported signing algorithm identifier: " + sigAlg); + } + } + + + private static String[] resolveSchemaFilePaths(String[] relativePaths) { + + return Arrays.stream(relativePaths). + map(file -> { + try { + return SamlFactory.class.getResource(file).toURI().toString(); + } catch (URISyntaxException e) { + LOGGER.warn("Error resolving schema file path", e); + return null; + } + }).filter(Objects::nonNull).toArray(String[]::new); + } + + private static class DocumentBuilderErrorHandler implements org.xml.sax.ErrorHandler { + /** + * Enabling schema validation with `setValidating(true)` in our + * DocumentBuilderFactory requires that we provide our own + * ErrorHandler implementation + * + * @throws SAXException If the document we attempt to parse is not valid according to the specified schema. + */ + @Override + public void warning(SAXParseException e) throws SAXException { + LOGGER.debug("XML Parser error ", e); + throw e; + } + + @Override + public void error(SAXParseException e) throws SAXException { + warning(e); + } + + @Override + public void fatalError(SAXParseException e) throws SAXException { + warning(e); + } + } + + private static class TransformerErrorListener implements javax.xml.transform.ErrorListener { + + @Override + public void warning(TransformerException e) throws TransformerException { + LOGGER.debug("XML transformation error", e); + throw e; + } + + @Override + public void error(TransformerException e) throws TransformerException { + warning(e); + } + + @Override + public void fatalError(TransformerException e) throws TransformerException { + warning(e); + } + } + } + diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java new file mode 100644 index 0000000000000..e6bfe1db8b219 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlInit.java @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.saml.support; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.SpecialPermission; +import org.elasticsearch.xpack.core.security.support.RestorableContextClassLoader; +import org.opensaml.core.config.InitializationService; +import org.slf4j.LoggerFactory; + +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class SamlInit { + + private static final AtomicBoolean INITIALISED = new AtomicBoolean(false); + private static final Logger LOGGER = LogManager.getLogger(); + + private SamlInit() { } + + /** + * This is needed in order to initialize the underlying OpenSAML library. + * It must be called before doing anything that potentially interacts with OpenSAML (whether in server code, or in tests). + * The initialization happens within do privileged block as the underlying Apache XML security library has a permission check. + * The initialization happens with a specific context classloader as OpenSAML loads resources from its jar file. + */ + public static void initialize() { + if (INITIALISED.compareAndSet(false, true)) { + // We want to force these classes to be loaded _before_ we fiddle with the context classloader + LoggerFactory.getLogger(InitializationService.class); + SpecialPermission.check(); + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + LOGGER.debug("Initializing OpenSAML"); + try (RestorableContextClassLoader ignore = new RestorableContextClassLoader(InitializationService.class)) { + InitializationService.initialize(); + } + LOGGER.debug("Initialized OpenSAML"); + return null; + }); + } catch (PrivilegedActionException e) { + throw new ElasticsearchSecurityException("failed to set context classloader for SAML IdP", e); + } + } + } + +} diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSigner.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSigner.java index fdb6c5fe046ec..c7dd06078885e 100644 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSigner.java +++ b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSigner.java @@ -20,16 +20,17 @@ */ public class SamlObjectSigner { - private final SamlFactory samlFactory; private final SamlIdentityProvider idp; + private final SamlFactory samlFactory; public SamlObjectSigner(SamlFactory samlFactory, SamlIdentityProvider idp) { this.samlFactory = samlFactory; this.idp = idp; + SamlInit.initialize(); } public Element sign(SignableXMLObject object) { - final Signature signature = samlFactory.object(Signature.class, Signature.DEFAULT_ELEMENT_NAME); + final Signature signature = samlFactory.buildObject(Signature.class, Signature.DEFAULT_ELEMENT_NAME); signature.setSigningCredential(idp.getSigningCredential()); signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); diff --git a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java b/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java deleted file mode 100644 index 7f73bac8fc8ed..0000000000000 --- a/x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/support/SamlUtils.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -package org.elasticsearch.xpack.idp.saml.support; - -import javax.xml.XMLConstants; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import java.io.StringWriter; -import java.io.Writer; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; -import org.elasticsearch.ElasticsearchSecurityException; -import org.elasticsearch.SpecialPermission; -import org.elasticsearch.common.SuppressForbidden; -import org.elasticsearch.xpack.core.security.support.RestorableContextClassLoader; -import org.opensaml.core.config.InitializationService; -import org.opensaml.core.xml.io.MarshallingException; -import org.opensaml.core.xml.util.XMLObjectSupport; -import org.opensaml.saml.common.SAMLObject; -import org.opensaml.saml.saml2.core.Assertion; -import org.opensaml.saml.saml2.core.Response; -import org.slf4j.LoggerFactory; -import org.w3c.dom.Element; - -public class SamlUtils { - - private static final String SAML_MARSHALLING_ERROR_STRING = "_unserializable_"; - - private static final AtomicBoolean INITIALISED = new AtomicBoolean(false); - - private static final Logger LOGGER = LogManager.getLogger(SamlUtils.class); - - /** - * This is needed in order to initialize the underlying OpenSAML library. - * It must be called before doing anything that potentially interacts with OpenSAML (whether in server code, or in tests). - * The initialization happens within do privileged block as the underlying Apache XML security library has a permission check. - * The initialization happens with a specific context classloader as OpenSAML loads resources from its jar file. - */ - public static void initialize() { - if (INITIALISED.compareAndSet(false, true)) { - // We want to force these classes to be loaded _before_ we fiddle with the context classloader - LoggerFactory.getLogger(InitializationService.class); - SpecialPermission.check(); - try { - AccessController.doPrivileged((PrivilegedExceptionAction) () -> { - LOGGER.debug("Initializing OpenSAML"); - try (RestorableContextClassLoader ignore = new RestorableContextClassLoader(InitializationService.class)) { - InitializationService.initialize(); - } - LOGGER.debug("Initialized OpenSAML"); - return null; - }); - } catch (PrivilegedActionException e) { - throw new ElasticsearchSecurityException("failed to set context classloader for SAML IdP", e); - } - } - } - - static String toString(Element element, boolean pretty) { - try { - StringWriter writer = new StringWriter(); - print(element, writer, pretty); - return writer.toString(); - } catch (TransformerException e) { - return "[" + element.getNamespaceURI() + "]" + element.getLocalName(); - } - } - - static void print(Element element, Writer writer, boolean pretty) throws TransformerException { - final Transformer serializer = getHardenedXMLTransformer(); - if (pretty) { - serializer.setOutputProperty(OutputKeys.INDENT, "yes"); - } - serializer.transform(new DOMSource(element), new StreamResult(writer)); - } - - public static String getXmlContent(SAMLObject object){ - return getXmlContent(object, false); - } - - public static String getXmlContent(SAMLObject object, boolean prettyPrint) { - try { - return toString(XMLObjectSupport.marshall(object), prettyPrint); - } catch (MarshallingException e) { - LOGGER.info("Error marshalling SAMLObject ", e); - return SAML_MARSHALLING_ERROR_STRING; - } - } - - static String describeSamlObject(SAMLObject object) { - if (Response.class.isInstance(object)) { - Response response = (Response) object; - StringBuilder sb = new StringBuilder(); - sb.append("SAML Response: [\n"); - sb.append(" Destination: ").append(response.getDestination()).append("\n"); - sb.append(" Response ID: ").append(response.getID()).append("\n"); - sb.append(" In response to: ").append(response.getInResponseTo()).append("\n"); - sb.append(" Response issued at:").append(response.getIssueInstant()).append("\n"); - if (response.getIssuer() != null) { - sb.append(" Issuer: ").append(response.getIssuer().getValue()).append("\n"); - } - sb.append(" Number of unencrypted Assertions: ").append(response.getAssertions().size()).append("\n"); - sb.append(" Number of encrypted Assertions: ").append(response.getEncryptedAssertions().size()).append("\n"); - sb.append("]"); - return sb.toString(); - - } else if (Assertion.class.isInstance(object)) { - Assertion assertion = (Assertion) object; - StringBuilder sb = new StringBuilder(); - sb.append("SAML Assertion: [\n"); - sb.append(" Response ID: ").append(assertion.getID()).append("\n"); - sb.append(" Response issued at: ").append(assertion.getIssueInstant()).append("\n"); - if (assertion.getIssuer() != null) { - sb.append(" Issuer: ").append(assertion.getIssuer().getValue()).append("\n"); - } - sb.append(" Number of attribute statements: ").append(assertion.getAttributeStatements().size()).append("\n"); - sb.append(" Number of authentication statements: ").append(assertion.getAuthnStatements().size()).append("\n"); - sb.append("]"); - return sb.toString(); - } - return getXmlContent(object); - } - - @SuppressForbidden(reason = "This is the only allowed way to construct a Transformer") - public static Transformer getHardenedXMLTransformer() throws TransformerConfigurationException { - final TransformerFactory tfactory = TransformerFactory.newInstance(); - tfactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - tfactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - tfactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); - tfactory.setAttribute("indent-number", 2); - Transformer transformer = tfactory.newTransformer(); - transformer.setErrorListener(new ErrorListener()); - return transformer; - } - - private static class ErrorListener implements javax.xml.transform.ErrorListener { - - @Override - public void warning(TransformerException e) throws TransformerException { - LOGGER.debug("XML transformation error", e); - throw e; - } - - @Override - public void error(TransformerException e) throws TransformerException { - LOGGER.debug("XML transformation error", e); - throw e; - } - - @Override - public void fatalError(TransformerException e) throws TransformerException { - LOGGER.debug("XML transformation error", e); - throw e; - } - } -} diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequestTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequestTests.java new file mode 100644 index 0000000000000..7e7522103bee3 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/SamlValidateAuthnRequestRequestTests.java @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.action; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; + +public class SamlValidateAuthnRequestRequestTests extends ESTestCase { + + public void testSerialization() throws Exception { + final SamlValidateAuthnRequestRequest request = new SamlValidateAuthnRequestRequest(); + request.setQueryString("?SAMLRequest=x&RelayState=y&SigAlg=z&Signature=sig"); + final BytesStreamOutput out = new BytesStreamOutput(); + request.writeTo(out); + + final SamlValidateAuthnRequestRequest request1 = new SamlValidateAuthnRequestRequest(out.bytes().streamInput()); + assertThat(request.getQueryString(), equalTo(request1.getQueryString())); + final ActionRequestValidationException exception = request1.validate(); + assertNull(exception); + } + + public void testValidation() { + final SamlValidateAuthnRequestRequest request = new SamlValidateAuthnRequestRequest(); + final ActionRequestValidationException exception = request.validate(); + assertNotNull(exception); + assertThat(exception.validationErrors().size(), equalTo(1)); + assertThat(exception.validationErrors().get(0), containsString("Authentication request query string must be provided")); + } +} diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java new file mode 100644 index 0000000000000..967fc4de760c3 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SamlAuthnRequestValidatorTests.java @@ -0,0 +1,312 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.idp.saml.authn; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.xpack.idp.action.SamlValidateAuthnRequestResponse; +import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; +import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; +import org.elasticsearch.xpack.idp.saml.support.SamlFactory; +import org.elasticsearch.xpack.idp.saml.support.SamlInit; +import org.elasticsearch.xpack.idp.saml.test.IdpSamlTestCase; +import org.junit.Before; +import org.mockito.Mockito; +import org.opensaml.core.xml.util.XMLObjectSupport; +import org.opensaml.saml.common.SAMLObject; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.NameIDPolicy; +import org.opensaml.security.SecurityException; +import org.opensaml.security.x509.X509Credential; +import org.opensaml.xmlsec.crypto.XMLSigningUtil; +import org.opensaml.xmlsec.signature.support.SignatureConstants; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Set; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.joda.time.DateTime.now; +import static org.mockito.Mockito.when; +import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_REDIRECT_BINDING_URI; +import static org.opensaml.saml.saml2.core.NameIDType.PERSISTENT; +import static org.opensaml.saml.saml2.core.NameIDType.TRANSIENT; + +public class SamlAuthnRequestValidatorTests extends IdpSamlTestCase { + + private SamlAuthnRequestValidator validator; + private SamlIdentityProvider idp; + private SamlFactory samlFactory = new SamlFactory(); + + @Before + public void setupValidator() throws Exception { + SamlInit.initialize(); + idp = Mockito.mock(SamlIdentityProvider.class); + when(idp.getEntityId()).thenReturn("https://cloud.elastic.co/saml/idp"); + when(idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI)).thenReturn("https://cloud.elastic.co/saml/init"); + final SamlServiceProvider sp1 = Mockito.mock(SamlServiceProvider.class); + when(sp1.getEntityId()).thenReturn("https://sp1.kibana.org"); + when(sp1.getAssertionConsumerService()).thenReturn(new URL("https://sp1.kibana.org/saml/acs")); + when(sp1.getAllowedNameIdFormats()).thenReturn(Set.of(TRANSIENT)); + when(sp1.shouldSignAuthnRequests()).thenReturn(false); + final SamlServiceProvider sp2 = Mockito.mock(SamlServiceProvider.class); + when(sp2.getEntityId()).thenReturn("https://sp2.kibana.org"); + when(sp2.getAssertionConsumerService()).thenReturn(new URL("https://sp2.kibana.org/saml/acs")); + when(sp2.getAllowedNameIdFormats()).thenReturn(Set.of(PERSISTENT)); + when(sp2.getSigningCredential()).thenReturn(readCredentials("RSA", 4096)); + when(sp2.shouldSignAuthnRequests()).thenReturn(true); + when(idp.getRegisteredServiceProvider("https://sp1.kibana.org")).thenReturn(sp1); + when(idp.getRegisteredServiceProvider("https://sp2.kibana.org")).thenReturn(sp2); + validator = new SamlAuthnRequestValidator(samlFactory, idp); + } + + public void testValidAuthnRequest() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + SamlValidateAuthnRequestResponse response = future.actionGet(); + assertThat(response.isForceAuthn(), equalTo(false)); + assertThat(response.getSpEntityId(), equalTo("https://sp1.kibana.org")); + assertThat(response.getAuthnState().size(), equalTo(1)); + assertThat(response.getAuthnState().get("nameid_format"), equalTo(TRANSIENT)); + } + + public void testValidSignedAuthnRequest() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), PERSISTENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState, true, + readCredentials("RSA", 4096)), future); + SamlValidateAuthnRequestResponse response = future.actionGet(); + assertThat(response.isForceAuthn(), equalTo(false)); + assertThat(response.getSpEntityId(), equalTo("https://sp2.kibana.org")); + assertThat(response.getAuthnState().size(), equalTo(1)); + assertThat(response.getAuthnState().get("nameid_format"), equalTo(PERSISTENT)); + } + + public void testValidSignedAuthnRequestWithoutRelayState() throws Exception { + final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), PERSISTENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, null, true, + readCredentials("RSA", 4096)), future); + SamlValidateAuthnRequestResponse response = future.actionGet(); + assertThat(response.isForceAuthn(), equalTo(false)); + assertThat(response.getSpEntityId(), equalTo("https://sp2.kibana.org")); + assertThat(response.getAuthnState().size(), equalTo(1)); + assertThat(response.getAuthnState().get("nameid_format"), equalTo(PERSISTENT)); + } + + public void testValidSignedAuthnRequestWhenServiceProviderShouldNotSign() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA", 4096)), future); + SamlValidateAuthnRequestResponse response = future.actionGet(); + assertThat(response.isForceAuthn(), equalTo(false)); + assertThat(response.getSpEntityId(), equalTo("https://sp1.kibana.org")); + assertThat(response.getAuthnState().size(), equalTo(1)); + assertThat(response.getAuthnState().get("nameid_format"), equalTo(TRANSIENT)); + } + + public void testValidUnSignedAuthnRequestWhenServiceProviderShouldSign() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("The Service Provider must sign authentication requests but no signature was found")); + } + + public void testSignedAuthnRequestWithWrongKey() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA2", 4096)), future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Unable to validate signature of authentication request")); + } + + public void testSignedAuthnRequestWithWrongSizeKey() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp2.kibana.org", "https://sp2.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState, true, readCredentials("RSA", 2048)), future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("Unable to validate signature of authentication request")); + } + + public void testWrongDestination() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + "wrong_destination", TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("but the SSO endpoint of this Identity Provider is")); + assertThat(e.getMessage(), containsString("wrong_destination")); + } + + public void testUnregisteredAcsForSp() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://malicious.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("The registered ACS URL for this Service Provider is")); + assertThat(e.getMessage(), containsString("https://malicious.kibana.org/saml/acs")); + } + + public void testUnregisteredSp() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://unknown.kibana.org", "https://unknown.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("is not registered with this Identity Provider")); + assertThat(e.getMessage(), containsString("https://unknown.kibana.org")); + } + + public void testAuthnRequestWithoutAcsUrl() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); + // remove ACS + authnRequest.setAssertionConsumerServiceURL(null); + final boolean containsIndex = randomBoolean(); + if (containsIndex) { + authnRequest.setAssertionConsumerServiceIndex(randomInt(10)); + } + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("SAML authentication does not contain an AssertionConsumerService URL")); + if (containsIndex) { + assertThat(e.getMessage(), containsString("It contains an Assertion Consumer Service Index ")); + } + } + + public void testAuthnRequestWithoutIssuer() { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), TRANSIENT); + // remove issuer + authnRequest.setIssuer(null); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, + future::actionGet); + assertThat(e.getMessage(), containsString("SAML authentication request has no issuer")); + } + + public void testInvalidNameIDPolicy() throws Exception { + final String relayState = randomAlphaOfLength(6); + final AuthnRequest authnRequest = buildAuthnRequest("https://sp1.kibana.org", "https://sp1.kibana.org/saml/acs", + idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI), PERSISTENT); + PlainActionFuture future = new PlainActionFuture<>(); + validator.processQueryString(getQueryString(authnRequest, relayState), future); + SamlValidateAuthnRequestResponse response = future.actionGet(); + assertThat(response.isForceAuthn(), equalTo(false)); + assertThat(response.getSpEntityId(), equalTo("https://sp1.kibana.org")); + assertThat(response.getAuthnState().size(), equalTo(2)); + assertThat(response.getAuthnState().get("nameid_format"), equalTo(PERSISTENT)); + assertThat(response.getAuthnState().get("error"), equalTo("invalid_nameid_policy")); + } + + private AuthnRequest buildAuthnRequest(String entityId, String acs, String destination, String nameIdFormat) { + final Issuer issuer = samlFactory.buildObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); + issuer.setValue(entityId); + final NameIDPolicy nameIDPolicy = samlFactory.buildObject(NameIDPolicy.class, NameIDPolicy.DEFAULT_ELEMENT_NAME); + nameIDPolicy.setFormat(nameIdFormat); + final AuthnRequest authnRequest = samlFactory.buildObject(AuthnRequest.class, AuthnRequest.DEFAULT_ELEMENT_NAME); + authnRequest.setID(samlFactory.secureIdentifier()); + authnRequest.setIssuer(issuer); + authnRequest.setIssueInstant(now()); + authnRequest.setAssertionConsumerServiceURL(acs); + authnRequest.setDestination(destination); + authnRequest.setNameIDPolicy(nameIDPolicy); + return authnRequest; + } + + private String getQueryString(AuthnRequest authnRequest, String relayState) { + return getQueryString(authnRequest, relayState, false, null); + } + + private String getQueryString(AuthnRequest authnRequest, String relayState, boolean sign, @Nullable X509Credential credential) { + try { + final String request = deflateAndBase64Encode(authnRequest); + String queryParam = "SAMLRequest=" + urlEncode(request); + if (relayState != null) { + queryParam += "&RelayState=" + urlEncode(relayState); + } + if (sign) { + final String algo = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256; + queryParam += "&SigAlg=" + urlEncode(algo); + final byte[] sig = sign(queryParam, algo, credential); + queryParam += "&Signature=" + urlEncode(base64Encode(sig)); + } + return queryParam; + } catch (Exception e) { + throw new ElasticsearchException("Cannot construct SAML redirect", e); + } + } + + private String base64Encode(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + private String urlEncode(String param) throws UnsupportedEncodingException { + return URLEncoder.encode(param, StandardCharsets.UTF_8.name()); + } + + private String deflateAndBase64Encode(SAMLObject message) + throws Exception { + Deflater deflater = new Deflater(Deflater.DEFLATED, true); + try (ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + DeflaterOutputStream deflaterStream = new DeflaterOutputStream(bytesOut, deflater)) { + String messageStr = samlFactory.toString(XMLObjectSupport.marshall(message), false); + deflaterStream.write(messageStr.getBytes(StandardCharsets.UTF_8)); + deflaterStream.finish(); + return base64Encode(bytesOut.toByteArray()); + } + } + + private byte[] sign(String text, String algo, X509Credential credential) throws SecurityException { + return sign(text.getBytes(StandardCharsets.UTF_8), algo, credential); + } + + private byte[] sign(byte[] content, String algo, X509Credential credential) throws SecurityException { + return XMLSigningUtil.signWithURI(credential, algo, content); + } + + +} diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java index 4ddd9a2561a1b..d4493b543cc18 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilderTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider; import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider; import org.elasticsearch.xpack.idp.saml.support.SamlFactory; +import org.elasticsearch.xpack.idp.saml.support.SamlInit; import org.elasticsearch.xpack.idp.saml.support.SamlObjectSigner; import org.elasticsearch.xpack.idp.saml.support.XmlValidator; import org.elasticsearch.xpack.idp.saml.test.IdpSamlTestCase; @@ -17,8 +18,7 @@ import org.opensaml.saml.saml2.core.Response; import org.w3c.dom.Element; -import java.net.URI; -import java.net.URISyntaxException; +import java.net.URL; import java.time.Clock; import java.util.Set; @@ -30,11 +30,12 @@ public class SuccessfulAuthenticationResponseMessageBuilderTests extends IdpSamlTestCase { private SamlIdentityProvider idp; - private SamlFactory samlFactory; private XmlValidator validator; + private SamlFactory samlFactory; @Before public void setupSaml() throws Exception { + SamlInit.initialize(); samlFactory = new SamlFactory(); validator = new XmlValidator("saml-schema-protocol-2.0.xsd"); @@ -61,14 +62,14 @@ public void testSignedResponseIsValidAgainstXmlSchema() throws Exception { validator.validate(xml); } - private Response buildResponse() throws URISyntaxException { + private Response buildResponse() throws Exception{ final Clock clock = Clock.systemUTC(); final SamlServiceProvider sp = mock(SamlServiceProvider.class); final String baseServiceUrl = "https://" + randomAlphaOfLength(32) + ".us-east-1.aws.found.io/"; - final URI acs = new URI(baseServiceUrl + "api/security/saml/callback"); + final String acs = baseServiceUrl + "api/security/saml/callback"; when(sp.getEntityId()).thenReturn(baseServiceUrl); - when(sp.getAssertionConsumerService()).thenReturn(acs); + when(sp.getAssertionConsumerService()).thenReturn(new URL(acs)); when(sp.getAuthnExpiry()).thenReturn(Duration.standardMinutes(10)); when(sp.getAttributeNames()).thenReturn(new SamlServiceProvider.AttributeNames()); @@ -77,8 +78,8 @@ private Response buildResponse() throws URISyntaxException { when(user.getGroups()).thenReturn(Set.of(randomArray(1, 5, String[]::new, () -> randomAlphaOfLengthBetween(4, 12)))); when(user.getServiceProvider()).thenReturn(sp); - final SuccessfulAuthenticationResponseMessageBuilder builder = new SuccessfulAuthenticationResponseMessageBuilder(samlFactory, - clock, idp); + final SuccessfulAuthenticationResponseMessageBuilder builder = + new SuccessfulAuthenticationResponseMessageBuilder(samlFactory, clock, idp); return builder.build(user, null); } diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java index 5ffbf92b204f3..dcfdeebb601b1 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/idp/IdentityProviderPluginConfigurationTests.java @@ -31,6 +31,8 @@ import static org.elasticsearch.xpack.idp.IdentityProviderPlugin.IDP_SSO_REDIRECT_ENDPOINT; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; +import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_POST_BINDING_URI; +import static org.opensaml.saml.common.xml.SAMLConstants.SAML2_REDIRECT_BINDING_URI; public class IdentityProviderPluginConfigurationTests extends IdpSamlTestCase { @@ -46,10 +48,10 @@ public void testAllSettings() { final Environment env = TestEnvironment.newEnvironment(settings); CloudIdp idp = new CloudIdp(env, settings); Assert.assertThat(idp.getEntityId(), equalTo("urn:elastic:cloud:idp")); - Assert.assertThat(idp.getSingleSignOnEndpoint("redirect"), equalTo("https://idp.org/sso/redirect")); - Assert.assertThat(idp.getSingleSignOnEndpoint("post"), equalTo("https://idp.org/sso/post")); - Assert.assertThat(idp.getSingleLogoutEndpoint("redirect"), equalTo("https://idp.org/slo/redirect")); - Assert.assertThat(idp.getSingleLogoutEndpoint("post"), equalTo("https://idp.org/slo/post")); + Assert.assertThat(idp.getSingleSignOnEndpoint(SAML2_REDIRECT_BINDING_URI).toString(), equalTo("https://idp.org/sso/redirect")); + Assert.assertThat(idp.getSingleSignOnEndpoint(SAML2_POST_BINDING_URI).toString(), equalTo("https://idp.org/sso/post")); + Assert.assertThat(idp.getSingleLogoutEndpoint(SAML2_REDIRECT_BINDING_URI).toString(), equalTo("https://idp.org/slo/redirect")); + Assert.assertThat(idp.getSingleLogoutEndpoint(SAML2_POST_BINDING_URI).toString(), equalTo("https://idp.org/slo/post")); } public void testInvalidSsoEndpoint() { @@ -61,7 +63,7 @@ public void testInvalidSsoEndpoint() { final Environment env = TestEnvironment.newEnvironment(settings); IllegalArgumentException e = LuceneTestCase.expectThrows(IllegalArgumentException.class, () -> new CloudIdp(env, settings)); Assert.assertThat(e.getMessage(), Matchers.containsString(IDP_SSO_REDIRECT_ENDPOINT.getKey())); - Assert.assertThat(e.getMessage(), Matchers.containsString("Not a valid URI")); + Assert.assertThat(e.getMessage(), Matchers.containsString("Not a valid URL")); } public void testCreateSigningCredentialFromPemFiles() throws Exception { diff --git a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSignerTests.java b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSignerTests.java index 1c3ef966e515e..66b508fa673b6 100644 --- a/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSignerTests.java +++ b/x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/saml/support/SamlObjectSignerTests.java @@ -38,11 +38,11 @@ public class SamlObjectSignerTests extends IdpSamlTestCase { - private SamlFactory factory; - + private SamlFactory samlFactory; @Before public void setupState() { - factory = new SamlFactory(); + SamlInit.initialize(); + samlFactory = new SamlFactory(); } public void testSignLogoutRequest() throws Exception { @@ -106,7 +106,7 @@ private void verifySignatureExists(Element signedElement) { private Element sign(SignableSAMLObject request, String entityId, X509Credential credential) { SamlIdentityProvider idp = buildIdP(entityId, credential); - SamlObjectSigner signer = new SamlObjectSigner(factory, idp); + SamlObjectSigner signer = new SamlObjectSigner(samlFactory, idp); return signer.sign(request); } @@ -119,34 +119,34 @@ private SamlIdentityProvider buildIdP(String entityId, X509Credential credential } private LogoutRequest createLogoutRequest(String entityId) { - final LogoutRequest request = factory.object(LogoutRequest.class, LogoutRequest.DEFAULT_ELEMENT_NAME); + final LogoutRequest request = samlFactory.buildObject(LogoutRequest.class, LogoutRequest.DEFAULT_ELEMENT_NAME); request.setNotOnOrAfter(DateTime.now().plusMinutes(15)); - final NameID nameID = factory.object(NameID.class, NameID.DEFAULT_ELEMENT_NAME); + final NameID nameID = samlFactory.buildObject(NameID.class, NameID.DEFAULT_ELEMENT_NAME); nameID.setFormat(NameIDType.TRANSIENT); nameID.setValue(randomAlphaOfLengthBetween(24, 64)); request.setNameID(nameID); - final Issuer issuer = factory.object(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); + final Issuer issuer = samlFactory.buildObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); issuer.setValue(entityId); request.setIssuer(issuer); return request; } private Response createAuthnResponse(String entityId) { - final Response response = factory.object(Response.class, Response.DEFAULT_ELEMENT_NAME); + final Response response = samlFactory.buildObject(Response.class, Response.DEFAULT_ELEMENT_NAME); final DateTime now = DateTime.now(); response.setIssueInstant(now); response.setID(randomAlphaOfLength(24)); - final StatusCode code = factory.object(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME); + final StatusCode code = samlFactory.buildObject(StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME); code.setValue(StatusCode.AUTHN_FAILED); - Status status = factory.object(Status.class, Status.DEFAULT_ELEMENT_NAME); + Status status = samlFactory.buildObject(Status.class, Status.DEFAULT_ELEMENT_NAME); status.setStatusCode(code); response.setStatus(status); - final Issuer issuer = factory.object(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); + final Issuer issuer = samlFactory.buildObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); issuer.setValue(entityId); response.setIssuer(issuer); return response; diff --git a/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.crt b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.crt new file mode 100644 index 0000000000000..77bef513f58e7 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUC6eGyW4ccdmz4bbQHVtbogQ5Ip0wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJc2FtbC10ZXN0MB4XDTIwMDIxMzA2MjEzNVoXDTIwMDMx +NDA2MjEzNVowFDESMBAGA1UEAwwJc2FtbC10ZXN0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAmy/NbPNOgv4YCA+RvTyWwS/pneWiVeqGEbCxTWetVK8k +83hypxjSWZleQk8bVcfRVKwswYnIK1XwR1W0bnJyjXwo4dVT5nSOrzyLa3Z+BEIf +KgRAk+rQ/o5u46HxXDifoiY45hP+AOA/0btvRXmQYkMI7R+/BjpntD3hNnh+8Z4V +O+5+nTvlpVbOqEsanMG9VdeA8hL6rzve0mBU8zZkOkTlDOiWzIJST5n84phowt8a +cNucrKyHxa90PJBC9vyva5o460QMiwRWe5SByP+h5j5n5mHMb3e8km22fctzMOct +fCcIEAJMbw347euE7/ZjaueXtic5QCxcaoyDvf1v+Y/DJFb3xDnqXHwS+xzD4lM/ +3dScqHI5atpkghnhWSaFhEuH9BtQWwsTCojcckUbTFS/yQM5PJqiLhWk6QpazIOj +S8kP9T9ta0zJDsfPKUNgrLWUn0cj8L6eZ87ET1E4fkJiD1WfeuvDmsPhImYCBWKY +Iv3SXncs7uwe3EyJCuiJmncFXTJ2FruR6OkIOpWi6qh7V/ocTvSnP2MHJ08M8NwJ +vv+ZIdjsAcEhn0kosfyI3ZkDsAwxait3ipG/asMp97T4qvDrnhHQXVYLsILq/+SV +bZx42g29vzIFjGflqw0RxtxQvfb3xM9V71mnbJd5WHmN1IBp02Ct9dHi58Bh1z8C +AwEAAaNTMFEwHQYDVR0OBBYEFFTTlGzPZb0FlDrRKRp1jB7e1J5HMB8GA1UdIwQY +MBaAFFTTlGzPZb0FlDrRKRp1jB7e1J5HMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAAM3Uz89I/7tfxtWW4lsPdE1jVJEzhufgu5Kj3JX7BfUpR1O +6lz3RNHr55hjffAoiUd2//ZprmRgDnOcGx7P82fR7DXVsIBR6Jj19hO4w1Mwz2Ae +kmdQ0Uiz1UYzul+U5o0+2WwH8sdgoNi+0U/LbgMFC3OT9XPAYvWfmJh07OKCJct1 ++Eq0bVEZKbyQUzpwN1+fuX6DM7e21uOsZZgELi+pMx3UM8siw1veU6umbK6o1pML +9px9j3rYGs8jP+s7bkrdfMam4tWmkcA9rYNHcjaTODuemaN5MH/q4HtvGUFyXTHH +Z9cEc0LWg+83LmCIzCevmHmPpUHo285DFbs9gjC7effnXait7RH/SZ5ygR2c9bfX +pOp5laVCnmgSg/EMkA47XnJI11iowlzqKZ/Bv6sydmB5V15ZJmyr61p/i7u0J6xj +cCg0hZHZHBf2Wfihu2Cf1oK7NB7iwfJNHlKCcwc2RxIWAdCarF0e7yeLQdzbCy94 +orTQGIGgFu/ct8ip44yg5/HcJMvruDende9NKb7rjnSTxfqhBjjT6gnrxroKksWH +eiMTg6g+ICTsAj4LHQ8LCoqdCBvGLOqmQeeDKZrYu1LRXbMD0PJchopGnCg0DAxP +ndxNt7jpXjkrKH/C31LLRBD94ZSjeRCPqkoxeGTBTu0Uru3HNdlSnaxmr6gn +-----END CERTIFICATE----- diff --git a/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.key b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.key new file mode 100644 index 0000000000000..8d9454d33df4b --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA2_4096.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCbL81s806C/hgI +D5G9PJbBL+md5aJV6oYRsLFNZ61UryTzeHKnGNJZmV5CTxtVx9FUrCzBicgrVfBH +VbRucnKNfCjh1VPmdI6vPItrdn4EQh8qBECT6tD+jm7jofFcOJ+iJjjmE/4A4D/R +u29FeZBiQwjtH78GOme0PeE2eH7xnhU77n6dO+WlVs6oSxqcwb1V14DyEvqvO97S +YFTzNmQ6ROUM6JbMglJPmfzimGjC3xpw25ysrIfFr3Q8kEL2/K9rmjjrRAyLBFZ7 +lIHI/6HmPmfmYcxvd7ySbbZ9y3Mw5y18JwgQAkxvDfjt64Tv9mNq55e2JzlALFxq +jIO9/W/5j8MkVvfEOepcfBL7HMPiUz/d1Jyocjlq2mSCGeFZJoWES4f0G1BbCxMK +iNxyRRtMVL/JAzk8mqIuFaTpClrMg6NLyQ/1P21rTMkOx88pQ2CstZSfRyPwvp5n +zsRPUTh+QmIPVZ9668Oaw+EiZgIFYpgi/dJedyzu7B7cTIkK6ImadwVdMnYWu5Ho +6Qg6laLqqHtX+hxO9Kc/YwcnTwzw3Am+/5kh2OwBwSGfSSix/IjdmQOwDDFqK3eK +kb9qwyn3tPiq8OueEdBdVguwgur/5JVtnHjaDb2/MgWMZ+WrDRHG3FC99vfEz1Xv +Wadsl3lYeY3UgGnTYK310eLnwGHXPwIDAQABAoICAH5j8GtLviXxzKDEDW6SajXt +T9fJru1KlObTgZQJXKIcA3xIHRj6nldbEenvg9PZaiQMFmeKT/z7gTaYFtvdWm7h +MGq91Bdd1tfh96sOVpQhRkByNiZCmPS6DJZYzrrNYzYs6yp4HeeYvGGUxotO9skQ +z7RQpsPrzYp+224BtWIT3jdxhq2ImwCOMsHeaU5CfHHtkpeV3ZXarR+qVYXARuEu +O17IEAmAGom6YLwsZSZRjrdcJb58xhe7TnAFDoUaR8TbVoBGa/DNF6KO0SVjDfj3 +2qdH/7jJTEv2DytlB+xMXD9Bis9/D8FjH9qmdx75DP+En4s7ZuVdO+eLicCzKP5T +2Na8bJ+QRo20k4P0hQLsT2oMag7jaTi9n3Qv8/Ia3V6108uGWtsT9xH9pbVxptMt +4Y2TpGqe2ljs6nGoO4wVE55I++exutUpXyGC/NUS9TBhqwsjMtDd+43JI9hhZvFY +0r6f4MJgr23WYPyVSNFi1c6p4wun3oJNd8HLxMLwtYCB3eS4+SgdFKfYRXXAZqIP +dc0X4TtE90VzyCtOSmUSQVRJ3mDz9ey80ZH7bsGg6ZZ4JefdGNpCD6/QUqR4eHLy +GDyr0jAkLWAG5q0TbnbhsihSaCQGgcPYSagTZUUDMAyQ7BkxoX+T12xMmgcXqGme +ATu2I+sMT1mmMIW1HvABAoIBAQDLr1jhBRWObR6JFQr0frotimKsdlt1ENKzIbYj +pP6lf2eFr7x0qie5gkpcWJizEyFs2mWmNtsdgHSpj8L0+PHnnRczSnYk/eo0AS+I +vtci9/9dTJb7X+nwQ49LsijQt3UVqNfdSiFpxphqixB1C0R0ISxF4CfxpmGH4YYN +TIBmhZwFS7/Dutrw/MtzTg7kpa/IK72CTn9b+Re9vWhFghgCUW12jnsUXKriYCRr +73kUCHk6h8L7ZA4HseIDzKOzAwr/pyD1wl+wh2YPchy/FFIm4tMFYVZZnMFw7jIX +cR3q1G0vI4pIzuze2x33Sad6VGL3eymatSJZUQ0ZQLBKctCRAoIBAQDDC5wWeBLA +fHv6MxO5uToFZAfqWvdItQthxe8wDsLMJ0tWfB/j2ySqzVh6Nt3BeWkeR/E6ZAAr +JqzNXvpoCwq4udWZVE3FjPBY/YlxQj0tjwfJaoDcWeCpxIj9AC+veKh8NH/5t7e8 +YLMgeoG4JsKQWUI7FZAZlRV36cWnkc3ICZcKv+A9Kxz9PtZWge5Fudd4cib8zMxX ++jrG/NvIhHYuQVee5jm2BflQB7OWntabjj8Q141IDPDiMIvarNiBliQQH5aL+Orv +Q2uq8R3sOXfyTAh1P3blD3H3DOoi9kalqyoYCCpUP1J3H1hFe3M5xS7gt2D2b8ZI +xWYCQQItURLPAoIBAGOVpZTO0Y1DGIzZNvP3Cbu+TyQW8fw4b2uZbd3bi40Kz2hz +VWaM0sNGSmkAABh9n/gCh3ROma7A4UkNurnfBRbCnVc8bqsDYgBvG/h1peW47qeF +DMR2ToBj56mQv5fcOmiOlovg9JksudRNjYxMJ/nHRkgboivptwGiZ9IUtypo5LFK +KcXek7EDR81mOq3bvCfYCt2s12P2U383HcXuJyLrAOPYIaEA9aOccfI7sSz0P6E4 +Q0J51so4VFKbOOYK9NOtuNWvZO2AJYwjtJ5PpyLP/3A/+OHzXDiEnQUNDx2DIqbL +b47NbM5Av1PqNPIAVCq/ksXLDbIxiH/yOr8Lh6ECggEBAKzYtwR6eDO4na3GYe6u +pSpt0U3wO0BloKaO4D74dcyx7ePAsAofBEmRHJ2BzddNHsjE/JPAVx3mcjC1wLc/ +QcsedJoiyaEOG1jhplGGX+zl2gK2rWeZBq2sC7IZ3ihkhvs7E0Zbdyorj9Jyfk6R +ms6NK1Js83yYT6QquQyc+34QcZgHHlNWx6PtccjL8Do+TSLiFoblfxlgGYKKRsNW +D91B7sJUSER02tH/YTlTnd/QmTb1rrBxN29bkjTCBEQip+bZ93InrxtF5j5g2yGi +dBd3Je+xxE2N+HL1MPC2yzi5jN4rLkfmrc//KRy6IAzMH5TpLbZ2q4OF27aXobRD +KqUCggEATNxcmKNCEKBtpRHtcXa5qi4dEDIPYb2eeIrJMc0O/fD/SCiDzZL8Ewp3 +C4A7h1fTLxVCeAM2A+LYTLZXdZ04M4keOiBmagOMkTfl5UYcSJfP3UjHYf71HSFY +pbJmow5iNlLizGExH+ZgV+3bKz2mJmBQVmVU46/1i5CeoJECllS/NBB9Qc571eG4 +ENJ1EcIMVcYsdAgB82L9mQtUYdxZJNy1QRMNbinuISqew6+NEzZyJlyyOtI0kEGm +gOo6hf27PjiHHGvERgHVLLy4ZPz6JOVITWaYRVs5Oxemk/Q+v1Lv60MEYxzh9rni +eJirRnJDCpyAo0tJ/FyjbG07Mu+dcg== +-----END PRIVATE KEY----- diff --git a/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.crt b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.crt new file mode 100644 index 0000000000000..191f5ceb8db13 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUei1EtkLvWStQusThWwgO14R+gFowDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJc2FtbC10ZXN0MB4XDTIwMDIwNjIwMzA0OFoXDTIwMDMw +NzIwMzA0OFowFDESMBAGA1UEAwwJc2FtbC10ZXN0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAuylnpy9h6cKUz6uCBi6eHBNW5GN6fQb1rU3BLjj5BMbX +Oa4Ebj84OOsNVtS5/vskMtBH5R+Y1IT49Evd0pKgkJtvbso7avq/jfmi18uDI2Q7 +BjTzWC7aOkfaYXr5aBPEQpapjxV6QxirgMqz5x4WTgfBDfRr+gw2oXWsaVaQ7xkE +CeIIra7j0vgOfTfSR4BQlbEiZktVvaE882kKGZPdDvklGs1/3DMHM09kRpxR/CUc +3s8XtjH3DjUhK2MxX77gXxRXMMrWNWpk1/oXF/MnTkIiJhYqD7lnudbGXaLnaP38 +M6/OtHPluqomS5j2tVGjhh8vIHA/kXTh6lPH3rmGtHe3PgkARohojPsf62RXk22y +cbXeMrvaw/V1cQmDUrmm8ha8qAi48uu6oRK3pk81j3LI+Q4SCbP1gllaRnwP7b8x +gxbKEzNJp83+a8eM+6zLlLchxjP2MhU41VkzKtomQ+tJlT9mSFLD48b06EKTRUQ9 +rK5bpv1j+TOBCxfKAB9NSGwxcjlVhokxKKGrJlPb7aPJzqY4lcRzZJylhqz7gM+Q +vSwOvGbw7m45hbBNGV2VJi00VP6d+O/2p2bg/fniV4rjh5pWgjKh0kLUzp9kqFDn +bpZhTcBP1YrfR1M2YUP7BBA887AQ5nOD5PRaV58Sz4F+K4pbwG4c5Dld1WNWJTsC +AwEAAaNTMFEwHQYDVR0OBBYEFFlOCjw+wyUxFg2kpSzLYROZ1WLjMB8GA1UdIwQY +MBaAFFlOCjw+wyUxFg2kpSzLYROZ1WLjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAIxs61pAUU0R7j8guc9vOziNCm48KwrrqzSQ7JvgrPdrZ0mv +3JN+luAFcA+4itVU5LqO92V5PVuxhbNLOvAx7+qR32zRF863Crpq/jAux+3azBsG +yGyjqmA+bL1wXGLrn+Tmndwc90iEcRPcOZIDreSF0mRXesBVcXEg1E5gdKg4Hm7O +dbMZEHITzQ1/uSAQ0WxONsABS0SYgplk20IyWm2nOjtt5zw54mD6TfSgeirQa6Ja +oht6n7EfIIMo5wWxOXnAXOMdBzN4xf3S6u57fOXDkbHcS0zDYiEFP+FpXB+eabsH +fUUKou7YHrzno2vaoAASLpfRXy3WlHqulg0ZaDspY/0mAZxwwUL294yO+/tBsh8y +3a9qep/mEl0h0ghj25PeWR+XeNPBth1Y54XDKSXGjWXRekn0dLMzazksvJgDN6Ip +0hX5StqHbiJX10WpWdSOT06ynh+N6OI+VBj8ndcnLGfSE6gtL9JH3IKNtUgodr6Z ++CcyZswWKutHyyZE5vteNQFKeTidCQAw9kRW6gtGUVRU0+PrMvD/8WhSd6WkFS4X +jN+BXrmruCSGugdL9fgpg21qKcZwkR9rYQXqRPK+nTiVCRrOzUyTFnPmusz8fg7e +g6ONaf2xMUeWfI+F8kK4NH5GkGggGqQDtes3Y+bWQ28lV7ny44TkMBARz6zH +-----END CERTIFICATE----- diff --git a/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.key b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.key new file mode 100644 index 0000000000000..1d83738b7a2b9 --- /dev/null +++ b/x-pack/plugin/identity-provider/src/test/resources/keypair/keypair_RSA_4096.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC7KWenL2HpwpTP +q4IGLp4cE1bkY3p9BvWtTcEuOPkExtc5rgRuPzg46w1W1Ln++yQy0EflH5jUhPj0 +S93SkqCQm29uyjtq+r+N+aLXy4MjZDsGNPNYLto6R9phevloE8RClqmPFXpDGKuA +yrPnHhZOB8EN9Gv6DDahdaxpVpDvGQQJ4gitruPS+A59N9JHgFCVsSJmS1W9oTzz +aQoZk90O+SUazX/cMwczT2RGnFH8JRzezxe2MfcONSErYzFfvuBfFFcwytY1amTX ++hcX8ydOQiImFioPuWe51sZdoudo/fwzr860c+W6qiZLmPa1UaOGHy8gcD+RdOHq +U8feuYa0d7c+CQBGiGiM+x/rZFeTbbJxtd4yu9rD9XVxCYNSuabyFryoCLjy67qh +EremTzWPcsj5DhIJs/WCWVpGfA/tvzGDFsoTM0mnzf5rx4z7rMuUtyHGM/YyFTjV +WTMq2iZD60mVP2ZIUsPjxvToQpNFRD2srlum/WP5M4ELF8oAH01IbDFyOVWGiTEo +oasmU9vto8nOpjiVxHNknKWGrPuAz5C9LA68ZvDubjmFsE0ZXZUmLTRU/p347/an +ZuD9+eJXiuOHmlaCMqHSQtTOn2SoUOdulmFNwE/Vit9HUzZhQ/sEEDzzsBDmc4Pk +9FpXnxLPgX4rilvAbhzkOV3VY1YlOwIDAQABAoICAFLRl5RbWzBdcgwTEI47wqsZ +w7F8c48vrTbq2Tji7Q44DrTvU/aU8wP8vwJVT5iM+Q+jKq0wtigUTzWK/LVZPMPA +hCa6RmCoZGsms/BZlcXrbFLqy2OSF+8CLJhGGmb7mDT/BjjSgC+AkyOCjukOX0BY +Hg1WwxD6ppH7yDc0nx2uLCNTahOw+A86xO6T7PDGzuHuaBJr85zd5GKxcE6xJ3ig +ttKNbK67xcvmrbCxK3Yv1f6iFRQCOBiJWgwg8JA1noN0NMDagL9SPR11BRML/bCb +gxDnbeVXXZbWeyCkHVJQUXFtskIXpuhasmdxYHoLjhijY+8uvHJZDj7qo97iqcAf +jzbvnDq+59hDrcYiLAeYY9VPCyhzMTF1dybdqVRLslwGsryzIGAqVGZi7Suf6Ufw +8HuLd2uJmkRf/g/hFxVcvkavylwpNKYniaw8O0lHqqh1jTVZFufF/E6MvH63y+wB +2lj1PyXgg27Lh2ehm0btu/9/iuLYyNUSEuKJ9lTSHLdzHlWxmk7Wg3jOus/9B++X +NQpCtpE8IZA4KoTkvnmsepPwzzGXgmSwnEraveV4c/59JOyVa7V8hEYGQMdeQcsa +av7PZkgdSlYygfjyK0gu/d6tpTdbVWk6Wo2suTWvTHVosisqUdODiaK5rqwIeAyD +wHWsTSlDiqZyuIqoyB3BAoIBAQDsUqYM8izDFLXhcstgdLfaU1drMDZze0FLqWyw +ItcCeQNpAbev1w5FgaJn41NdYDSKmfH3yJ1rDlUwz7qI4L0PZO7mHe3AgVDvNbJO +9w1cooQrv17zG2wPGbFosmiEzJdTl0Wbtd44AxSwSjCMmO5v0SJWbxnz/hkM47pG +98TB2QMY30hh9fJTguVDzf8uo0Ba4IdK59k59JyBai3hIuVhwEpA+hzRMmX0IMHN +QphmKcsM13B8mxJQtrw5WOeXiJypaEQ3g5z+fIxAP4ZPHFKLu6CR5+dYnbvRn7Qs +v54JanujCLaQoY5lvMlAtc/ahQohuxHdiPY22JqeshdKfg4FAoIBAQDKvtw27L81 +GOEY6rC3qtcEVZbXWheytp2CzxYITXgRzDIcr64ftJijN107/n9R+bnfSCEKZXfb +h+1MHM2ieVsGfT3WjRviEtmNXCWuMOUJJt/QeNXKLJ2D3G82P9QFdHn6KALOxE1L +VCE6xRoTmGIw1zO7MhdZqURD7CiE5XOTQOpwJ1wxxJAJK1agbV8mI9+lGdpSDoex +Ye4VxRVKFg+lAImy6NE+I7ILx08Wuo8tAe25lGKdnxaST4DfFSNQ97JMawJ259+7 +/4f/Z1k528UwiCBmmkPd32YRES1AQBS4dlyYo7b5d7JCfGgnluu0Ij4IAd6YG0Sq +fDOX/wcJWoo/AoIBAQCScdT8iMY8TYU4JA7rwuh7ntG5VrMndOXNoTv9Glnq6lq2 +o53UlYv+forJnhF297sXeLZ9OhBy2vrnuJJ7Z+lAtOdmKn2+oWQqGDhavrXFBeCp +y3lnZULdSKqrmoMyaCYOYtt71oPSafZUGGVlew+msw54SXjnvjp/xk8U001WYsiM +jTEpo2N7ZM316p2LpRaoOiMcw/fU9kRV3hp4PUuv+50IDvX0nSqY0gPG0tHl3Vg6 +qPbIAMCky9hwISvCt6F9SWOcky6idUPkPHlaZM6yUJ78S50NSRGuoSzh3gxtAVOE +gE3B6pt3aDm+o+weGfthU6+KfOk2q5uFZA8S1nKRAoIBAAiLZqiB7cUFGWd9bJkx +AALUwJiu2VTU0WqT46/R4/+MBYij9sirScuHJ8zBewgvaB/64DmZ8SU1Y1DT3e69 +ApMRcCu8orIybSzB92yR7vUlyET28rukibbm8WDt67eXIxTh3sxsBX3pO8VITUMK +Jwk76Jg5EjbbJV9fco23Rdms4LR9VCtcx107kFkOgvYOiPxiTIssxZc6M1iDh9Qp +fdtMXIFHwpnF01kfuzuP/Ty+WhB6KnXOIJtl8l50e5qL9d22nNH6D7nSmR6wclg3 +5OWYtxTb54RQR/LsMGYGzVjA1EIylCGLY93ddF5GxARBdS/NNF5gt9IfjeUYAGnq +gO0CggEAMUDRZs1YYiuwCMQRuvi3a9JtoJxQ05YdN04Njn5bC/0E4CFDT2eTFozQ +JBLgxhwZ+UxFVMRLCGLzL7zaCYvwnnpzIHkTnfdPQJF5/LEEbYTlvKDdk6jqrwAW +BhIuLxJk1r/opNWKOyAEsCADXnpPgbb+0jYuJgEHCIFeyExx8nZcCRVhZoZeFsJ3 +fO2fqc+mV+UN4r1wHejlKp10OrNEeuPE5tJuwO1/QtKVTFIxOUn0PqftoCUUwzI7 +R/2dJC5AFueMbVQlu7Rx2rI0S816sapYniPZrnv66QsDAYRic3/m84c3mVj3YCrp +vkIxxqLwG5fIEleJmcvzZGyM1zAPew== +-----END PRIVATE KEY-----