Skip to content

Commit

Permalink
Add SAML AuthN request signing tests (elastic#48444) (elastic#61585)
Browse files Browse the repository at this point in the history
- Add a unit test for our signing code
- Change SAML IT to use signed authentication requests for Shibboleth to consume

Backport of elastic#48444
  • Loading branch information
jkakavas authored Aug 27, 2020
1 parent 7ccf9f5 commit a89b165
Show file tree
Hide file tree
Showing 14 changed files with 306 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SingleSignOnService;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
Expand All @@ -38,17 +36,7 @@ public class SamlAuthnRequestBuilderTests extends SamlTestCase {
@Before
public void init() throws Exception {
SamlUtils.initialize(logger);

final SingleSignOnService sso = SamlUtils.buildObject(SingleSignOnService.class, SingleSignOnService.DEFAULT_ELEMENT_NAME);
sso.setLocation(IDP_URL);
sso.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);

final IDPSSODescriptor idpRole = SamlUtils.buildObject(IDPSSODescriptor.class, IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
idpRole.getSingleSignOnServices().add(sso);

idpDescriptor = SamlUtils.buildObject(EntityDescriptor.class, EntityDescriptor.DEFAULT_ELEMENT_NAME);
idpDescriptor.setEntityID(IDP_ENTITY_ID);
idpDescriptor.getRoleDescriptors().add(idpRole);
idpDescriptor = buildIdPDescriptor(IDP_URL, IDP_ENTITY_ID);
}

public void testBuildRequestWithPersistentNameAndNoForceAuth() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.security.x509.X509Credential;

import java.io.UnsupportedEncodingException;
Expand All @@ -19,7 +21,9 @@
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.time.Clock;
import java.util.Base64;
import java.util.Collections;

import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
Expand All @@ -29,6 +33,9 @@
public class SamlRedirectTests extends SamlTestCase {

private static final String IDP_ENTITY_ID = "https://idp.test/";
private static final String SP_ENTITY_ID = "https://sp.example.com/";
private static final String ACS_URL = "https://sp.example.com/saml/acs";
private static final String IDP_URL = "https://idp.test/saml/sso/redirect";
private static final String LOGOUT_URL = "https://idp.test/saml/logout";

private static final SigningConfiguration NO_SIGNING = new SigningConfiguration(emptySet(), null);
Expand Down Expand Up @@ -87,33 +94,58 @@ public void testLogoutRequestSigning() throws Exception {
final SamlRedirect redirect = new SamlRedirect(buildLogoutRequest(LOGOUT_URL + "?"), spConfig);
final String url = redirect.getRedirectUrl();
final String queryParam = url.split("\\?")[1].split("&Signature")[0];
final String params[] = url.split("\\?")[1].split("&");
assert (params.length == 3);
String sigAlg = parseAndUrlDecodeParameter(params[1]);
// We currently only support signing with SHA256withRSA, this test should be updated if we add support for more
assertThat(sigAlg, equalTo("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"));
sigAlg = "SHA256withRSA";
final String signature = parseAndUrlDecodeParameter(params[2]);
assertThat(validateSignature(queryParam, sigAlg, signature, credential), equalTo(true));
assertThat(validateSignature(queryParam, sigAlg, signature, invalidCredential), equalTo(false));
assertThat(validateSignature(queryParam.substring(0, queryParam.length() - 5), sigAlg, signature, credential), equalTo(false));
final String signature = validateUrlAndGetSignature(redirect.getRedirectUrl());
assertThat(validateSignature(queryParam, signature, credential), equalTo(true));
assertThat(validateSignature(queryParam, signature, invalidCredential), equalTo(false));
assertThat(validateSignature(queryParam.substring(0, queryParam.length() - 5), signature, credential), equalTo(false));
}

public void testAuthnRequestSigning() throws Exception {
final X509Credential credential = (X509Credential) buildOpenSamlCredential(readRandomKeyPair()).get(0);
X509Credential invalidCredential = (X509Credential) buildOpenSamlCredential(readRandomKeyPair()).get(0);
while (invalidCredential.getEntityCertificate().getSerialNumber().equals(credential.getEntityCertificate().getSerialNumber())) {
invalidCredential = (X509Credential) buildOpenSamlCredential(readRandomKeyPair()).get(0);
}
final SigningConfiguration signingConfig = new SigningConfiguration(singleton("*"), credential);
SpConfiguration sp = new SpConfiguration(SP_ENTITY_ID, ACS_URL, LOGOUT_URL, signingConfig, null, Collections.emptyList());

EntityDescriptor idpDescriptor = buildIdPDescriptor(IDP_URL, IDP_ENTITY_ID);

final SamlRedirect redirect = new SamlRedirect(new SamlAuthnRequestBuilder(sp, SAMLConstants.SAML2_POST_BINDING_URI,
idpDescriptor, SAMLConstants.SAML2_REDIRECT_BINDING_URI, Clock.systemUTC()).build(), signingConfig);
final String url = redirect.getRedirectUrl();
final String queryParam = url.split("\\?")[1].split("&Signature")[0];
final String signature = validateUrlAndGetSignature(redirect.getRedirectUrl());
assertThat(validateSignature(queryParam, signature, credential), equalTo(true));
assertThat(validateSignature(queryParam, signature, invalidCredential), equalTo(false));
assertThat(validateSignature(queryParam.substring(0, queryParam.length() - 5), signature, credential),
equalTo(false));
}

private String parseAndUrlDecodeParameter(String parameter) throws UnsupportedEncodingException {
final String value = parameter.split("=", 2)[1];
return URLDecoder.decode(value, "UTF-8");
}

private boolean validateSignature(String queryParam, String sigAlg, String signature, X509Credential credential) {
private String validateUrlAndGetSignature(String url) throws UnsupportedEncodingException {
final String params[] = url.split("\\?")[1].split("&");
assert (params.length == 3);
String sigAlg = parseAndUrlDecodeParameter(params[1]);
// We currently only support signing with SHA256withRSA, this test should be updated if we add support for more
assertThat(sigAlg, equalTo("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"));
return parseAndUrlDecodeParameter(params[2]);
}

private boolean validateSignature(String queryParam, String signature, X509Credential credential) {
try {
Signature sig = Signature.getInstance(sigAlg);
// We currently only support signing with SHA256withRSA, this test should be updated if we add support for more
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(credential.getPublicKey());
sig.update(queryParam.getBytes(StandardCharsets.UTF_8));
return sig.verify(Base64.getDecoder().decode(signature));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
return false;
}

}

private LogoutRequest buildLogoutRequest(String logoutUrl) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
import org.elasticsearch.xpack.core.ssl.PemUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.x509.impl.X509KeyManagerX509CredentialAdapter;

Expand Down Expand Up @@ -135,4 +139,18 @@ protected ElasticsearchSecurityException expectSamlException(ThrowingRunnable ru
assertThat("Exception " + exception + " should be a SAML exception", SamlUtils.isSamlException(exception), is(true));
return exception;
}

protected EntityDescriptor buildIdPDescriptor(String idpUrl, String idpEntityId) {
final SingleSignOnService sso = SamlUtils.buildObject(SingleSignOnService.class, SingleSignOnService.DEFAULT_ELEMENT_NAME);
sso.setLocation(idpUrl);
sso.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);

final IDPSSODescriptor idpRole = SamlUtils.buildObject(IDPSSODescriptor.class, IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
idpRole.getSingleSignOnServices().add(sso);

EntityDescriptor idpDescriptor = SamlUtils.buildObject(EntityDescriptor.class, EntityDescriptor.DEFAULT_ELEMENT_NAME);
idpDescriptor.setEntityID(idpEntityId);
idpDescriptor.getRoleDescriptors().add(idpRole);
return idpDescriptor;
}
}
13 changes: 11 additions & 2 deletions x-pack/qa/saml-idp-tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ testFixtures.useFixture ":x-pack:test:idp-fixture"


String outputDir = "${project.buildDir}/generated-resources/${project.name}"
task copyIdpFiles(type: Copy) {
from idpFixtureProject.files('idp/shibboleth-idp/credentials/idp-browser.pem', 'idp/shibboleth-idp/metadata/idp-metadata.xml');
def copyIdpFiles = tasks.register("copyIdpFiles", Copy) {
from idpFixtureProject.files('idp/shibboleth-idp/credentials/idp-browser.pem', 'idp/shibboleth-idp/metadata/idp-metadata.xml',
'idp/shibboleth-idp/credentials/sp-signing.key', 'idp/shibboleth-idp/credentials/sp-signing.crt');
into outputDir
}
project.sourceSets.test.output.dir(outputDir, builtBy: copyIdpFiles)
Expand Down Expand Up @@ -59,6 +60,8 @@ testClusters.integTest {
setting 'xpack.security.authc.realms.saml.shibboleth.sp.acs', 'http://localhost:54321/saml/acs1'
setting 'xpack.security.authc.realms.saml.shibboleth.attributes.principal', 'uid'
setting 'xpack.security.authc.realms.saml.shibboleth.attributes.name', 'urn:oid:2.5.4.3'
setting 'xpack.security.authc.realms.saml.shibboleth.signing.key', 'sp-signing.key'
setting 'xpack.security.authc.realms.saml.shibboleth.signing.certificate', 'sp-signing.crt'
// SAML realm 2 (uses authorization_realms)
setting 'xpack.security.authc.realms.saml.shibboleth_native.order', '2'
setting 'xpack.security.authc.realms.saml.shibboleth_native.idp.entity_id', 'https://test.shibboleth.elastic.local/'
Expand All @@ -67,6 +70,8 @@ testClusters.integTest {
setting 'xpack.security.authc.realms.saml.shibboleth_native.sp.acs', 'http://localhost:54321/saml/acs2'
setting 'xpack.security.authc.realms.saml.shibboleth_native.attributes.principal', 'uid'
setting 'xpack.security.authc.realms.saml.shibboleth_native.authorization_realms', 'native'
setting 'xpack.security.authc.realms.saml.shibboleth_native.signing.key', 'sp-signing.key'
setting 'xpack.security.authc.realms.saml.shibboleth_native.signing.certificate', 'sp-signing.crt'
// SAML realm 3 (used for negative tests with multiple realms)
setting 'xpack.security.authc.realms.saml.shibboleth_negative.order', '3'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.idp.entity_id', 'https://test.shibboleth.elastic.local/'
Expand All @@ -75,12 +80,16 @@ testClusters.integTest {
setting 'xpack.security.authc.realms.saml.shibboleth_negative.sp.acs', 'http://localhost:54321/saml/acs3'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.attributes.principal', 'uid'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.authorization_realms', 'native'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.signing.key', 'sp-signing.key'
setting 'xpack.security.authc.realms.saml.shibboleth_negative.signing.certificate', 'sp-signing.crt'
setting 'xpack.security.authc.realms.native.native.order', '4'

setting 'xpack.ml.enabled', 'false'
setting 'logger.org.elasticsearch.xpack.security', 'TRACE'

extraConfigFile 'idp-metadata.xml', file(outputDir + "/idp-metadata.xml")
extraConfigFile 'sp-signing.key', file(outputDir + "/sp-signing.key")
extraConfigFile 'sp-signing.crt', file(outputDir + "/sp-signing.crt")

user username: "test_admin", password: 'x-pack-test-password'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ public void setupNativeUser() throws IOException {
* <li>Uses that token to verify the user details</li>
* </ol>
*/

public void testLoginUserWithSamlRoleMapping() throws Exception {
// this realm name comes from the config in build.gradle
final Tuple<String, String> authTokens = loginViaSaml("shibboleth");
Expand Down
1 change: 1 addition & 0 deletions x-pack/test/idp-fixture/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ services:
volumes:
- ./idp/shibboleth-idp/conf:/opt/shibboleth-idp/conf
- ./idp/shibboleth-idp/credentials:/opt/shibboleth-idp/credentials
- ./idp/shibboleth-idp/metadata:/opt/shibboleth-idp/metadata
- ./idp/shib-jetty-base/start.d/ssl.ini:/opt/shib-jetty-base/start.d/ssl.ini

oidc-provider:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ idp.encryption.cert=%{idp.home}/credentials/idp-encryption.crt
#idp.encryption.key.2 = %{idp.home}/credentials/idp-encryption-old.key
#idp.encryption.cert.2 = %{idp.home}/credentials/idp-encryption-old.crt


# Sets the bean ID to use as a default security configuration set
#idp.security.config = shibboleth.DefaultSecurityConfiguration

Expand All @@ -72,13 +73,13 @@ idp.encryption.cert=%{idp.home}/credentials/idp-encryption.crt
#idp.trust.signatures = shibboleth.ChainingSignatureTrustEngine
# To pick only one set to one of:
# shibboleth.ExplicitKeySignatureTrustEngine, shibboleth.PKIXSignatureTrustEngine
#idp.trust.certificates = shibboleth.ChainingX509TrustEngine
#idp.trust.certificates = shibboleth.ExplicitKeyX509TrustEngine
# To pick only one set to one of:
# shibboleth.ExplicitKeyX509TrustEngine, shibboleth.PKIXX509TrustEngine

# If true, encryption will happen whenever a key to use can be located, but
# failure to encrypt won't result in request failure.
#idp.encryption.optional = false
idp.encryption.optional = true

# Configuration of client- and server-side storage plugins
#idp.storage.cleanupInterval = PT10M
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@
responsibility to ensure that the contents on disk are trustworthy.
-->

<!--
<MetadataProvider id="LocalMetadata" xsi:type="FilesystemMetadataProvider" metadataFile="PATH_TO_YOUR_METADATA"/>
-->
<!-- One metadada file per realm that we use in our elasticsearch/x-pack/qa/saml-idp-tests/build.gradle -->
<MetadataProvider id="LocalMetadata" xsi:type="FilesystemMetadataProvider" metadataFile="%{idp.home}/metadata/sp-metadata.xml"/>
<MetadataProvider id="LocalMetadata2" xsi:type="FilesystemMetadataProvider" metadataFile="%{idp.home}/metadata/sp-metadata2.xml"/>
<MetadataProvider id="LocalMetadata3" xsi:type="FilesystemMetadataProvider" metadataFile="%{idp.home}/metadata/sp-metadata3.xml"/>



<!--
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIULAHFP2QmBv0Nwq85ggfJa3p1EjMwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTEwMTUxMzI0MzBaFw0xOTEx
MTQxMzI0MzBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCeNmv5oV+QsORglGd62oSAklrevH8sbiMW6wym42c5
E6Fctq/jLJrY1oLdwOUMynSkaPmh9fAcZHtLUsjx6x4QcMEq/ODqBGvoJNZwomt+
uyVbpKDGjtrXZowGnUXPz7hsM2yofRiDUIjTsujrvFp/HqCRw/9K4Uw2x8EYL5FU
vaxxd36mjwbsZCXIjkOsi+jW/bM9RWQd3KF38Ar0vIP/wYK/5s9U+KSuOP3KsSOU
HSH/pnBFiXciU6bVHcZA3IRYnxzaTT2EQZEXyryfQuk3TWe9YvVusXs6kd9uW4x+
Lx/wovx4zy2iUpXhO/MVB1IG1SNM4w5ZE94mrLDtG+yOkGXqReesodPZYBC4OOaG
rZi4rH05FouU6p7QA+iqBCFNNLTrg0f62M/UcAVsANJqQUfFqe7pr1gewF6jlhBw
HEds19XDPmUHredOjDeGJMeJaH3GS8/M6s+MQpckffdmvF4WHwGAJvRf4AXW+G3a
9CTjq+E5I29+dWSvuMIIY/howKxQ9Q+IQhZYGhTH/Y9vpdfOdwVZYBB18zAI2VqJ
18iOCpmd0n/E6ZPyugCSlVZzSjSP+i/xz3c9rJnLkZ5dsOVwloNlw9jaVc/InCUN
KYyRhoxwkPRa9dzuogdDo41lz95xwikxDunTSUEU9yNjfofmWpuQMz5kYHWPsFbS
PQIDAQABo1MwUTAdBgNVHQ4EFgQUgowa3NlK8Dtl5KW1vL44+m0LnMAwHwYDVR0j
BBgwFoAUgowa3NlK8Dtl5KW1vL44+m0LnMAwDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAgEAZLIRn3HnTkiP6wp0gSbKqWdThM47atKqeU5l0iBcyBte
rNmkxnFD6z+6x6YkKtA1ttFTY4kzQ/lr0m+jRlWpMzNkBXU7bNvcbTA5LfrMq99l
QJpPuJ8eFM/B2PbYm5o0SEiTKVyYyjNXHze/alkflUTtNYQMJtSKYjFPvhx8/mVx
CHBC66K+oYydbuUgzR+WGrEhIkTOREeGmtVZ1M8zru8lbD1+I8T0eqcpBqPcRdmM
zSJjJopr+fM0v9LIpi22c50yZtHCLj9iAv8bQRaYnm6aV3kCb1Qv/Wadqk/7oeie
J16bv6VBSFLKyvez26HlYNNmsauz9cWN/h/LosURqmS6g+28bM+uH/UOp6CTgODp
OYqRpKW6/rmVPVejx7RBp63nNa/NMhbEngsR3QsBT5qcaMv7NZbK8v/tZDik3z7T
M4ld5hyUQw8WawopqhNAmTdu0ShBMQhWItQpZ3n12VNDFXXTbhdWxU/VY1hbesVu
C9pRcZXpVgC+tgxjfiRrruETbQyRM73evfnaBCgIaWXcTcyBuHHKneL4dhyOuBkz
4wgyJjVe88Cld/4VhPj2JqJasunfhgmknB6PmRdOQbFiIcYRIrdlk5ZblJzB+mnK
BOe4cVwR3qN0Hi7WqK8loakPVNnnpu7kGUvBKOeiKrXCBMgslffMbTw2ViTtEJo=
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
RrUiXgUEGJ6fmJUv9GjuMOY0CRq07GA=
-----END PRIVATE KEY-----
Loading

0 comments on commit a89b165

Please sign in to comment.