Skip to content

Commit

Permalink
Log when SAML realm public keys change for diagnosing `Signature veri…
Browse files Browse the repository at this point in the history
…fication failed` (#91066)
  • Loading branch information
justincr-elastic authored Oct 25, 2022
1 parent 3231835 commit e5cdf69
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
import java.security.GeneralSecurityException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.PublicKey;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
Expand All @@ -93,6 +94,8 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -292,7 +295,7 @@ static String require(RealmConfig config, Setting.AffixSetting<String> setting)
return value;
}

private static IdpConfiguration getIdpConfiguration(
static IdpConfiguration getIdpConfiguration(
RealmConfig config,
MetadataResolver metadataResolver,
Supplier<EntityDescriptor> idpDescriptor
Expand All @@ -312,6 +315,7 @@ private static IdpConfiguration getIdpConfiguration(
throw new IllegalStateException("Cannot initialise SAML IDP resolvers for realm " + config.name(), e);
}

final Consumer<List<Credential>> diffLogger = SamlRealm.diffLogger();
final String entityID = idpDescriptor.get().getEntityID();
return new IdpConfiguration(entityID, () -> {
try {
Expand All @@ -322,13 +326,40 @@ private static IdpConfiguration getIdpConfiguration(
new UsageCriterion(UsageType.SIGNING)
)
);
return CollectionUtils.iterableAsArrayList(credentials);
final List<Credential> list = CollectionUtils.iterableAsArrayList(credentials);
diffLogger.accept(list);
return list;
} catch (ResolverException e) {
throw new IllegalStateException("Cannot resolve SAML IDP credentials resolver for realm " + config.name(), e);
}
});
}

private static Consumer<List<Credential>> diffLogger() {
final AtomicReference<Set<PublicKey>> previousCredentialsRef = new AtomicReference<>(null);
return new Consumer<>() {
private static final Logger LOGGER = LogManager.getLogger(IdpConfiguration.class);

@Override
public void accept(final List<Credential> newCredentials) {
final Set<PublicKey> newPublicKeys = newCredentials.stream().map(cert -> cert.getPublicKey()).collect(Collectors.toSet());
final Set<PublicKey> previousPublicKeys = previousCredentialsRef.get();
if (previousPublicKeys == null) {
LOGGER.trace("Signing credentials initialized, added: [{}]", newCredentials.size());
} else {
final Set<PublicKey> added = Sets.difference(newPublicKeys, previousPublicKeys);
final Set<PublicKey> removed = Sets.difference(previousPublicKeys, newPublicKeys);
if (added.isEmpty() && removed.isEmpty()) {
LOGGER.debug("Signing credentials did not change, current: [{}]", newCredentials.size());
} else {
LOGGER.info("Signing credentials changed, added: [{}], removed: [{}]", added.size(), removed.size());
}
}
previousCredentialsRef.set(newPublicKeys);
}
};
}

static SpConfiguration getSpConfiguration(RealmConfig config) throws IOException, GeneralSecurityException {
final String serviceProviderId = require(config, SP_ENTITY_ID);
final String assertionConsumerServiceURL = require(config, SP_ACS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@
import org.elasticsearch.common.settings.SettingsException;
import org.elasticsearch.common.ssl.PemUtils;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.env.Environment;
import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.license.MockLicenseState;
import org.elasticsearch.test.http.MockResponse;
import org.elasticsearch.test.http.MockWebServer;
import org.elasticsearch.threadpool.TestThreadPool;
import org.elasticsearch.watcher.FileWatcher;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
Expand All @@ -35,6 +39,7 @@
import org.elasticsearch.xpack.security.Security;
import org.elasticsearch.xpack.security.authc.Realms;
import org.elasticsearch.xpack.security.authc.support.MockLookupRealm;
import org.elasticsearch.xpack.security.support.FileReloadListener;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.mockito.Mockito;
Expand Down Expand Up @@ -65,6 +70,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Stream;
Expand All @@ -82,7 +88,9 @@
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.ArgumentMatchers.any;
Expand Down Expand Up @@ -811,6 +819,82 @@ public void testCorrectRealmSelected() throws Exception {
assertThat(SamlRealm.findSamlRealms(realms, "incorrect", "https://idp.test:443/saml/login"), empty());
}

public void testReadDifferentIdpMetadataSameKeyFromFiles() throws Exception {
// Confirm these files are located in /x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/saml/
final Path originalMetadataPath = getDataPath("idp1.xml");
final Path updatedMetadataPath = getDataPath("idp1-same-certs-updated-id-cacheDuration.xml");
assertThat(Files.exists(originalMetadataPath), is(true));
assertThat(Files.exists(updatedMetadataPath), is(true));
// Confirm the file contents are different
assertThat(Files.readString(originalMetadataPath), is(not(equalTo(Files.readString(updatedMetadataPath)))));

// Use a temp file to trigger load and reload by ResourceWatcherService
final Path realmMetadataPath = Files.createTempFile(PathUtils.get(createTempDir().toString()), "idp1-metadata", "xml");

final RealmConfig.RealmIdentifier realmIdentifier = new RealmConfig.RealmIdentifier(SamlRealmSettings.TYPE, "saml-idp1");
final RealmConfig realmConfig = new RealmConfig(
realmIdentifier,
Settings.builder().put(RealmSettings.getFullSettingKey(realmIdentifier, RealmSettings.ORDER_SETTING), 1).build(),
this.env,
this.threadContext
);

final TestThreadPool testThreadPool = new TestThreadPool("Async Reload");
try {
// Put original metadata contents into realm metadata file
Files.writeString(realmMetadataPath, Files.readString(originalMetadataPath));

final TimeValue timeValue = TimeValue.timeValueMillis(10);
final Settings resourceWatcherSettings = Settings.builder()
.put(ResourceWatcherService.RELOAD_INTERVAL_HIGH.getKey(), timeValue)
.put(ResourceWatcherService.RELOAD_INTERVAL_MEDIUM.getKey(), timeValue)
.put(ResourceWatcherService.RELOAD_INTERVAL_LOW.getKey(), timeValue)
.build();
try (ResourceWatcherService watcherService = new ResourceWatcherService(resourceWatcherSettings, testThreadPool)) {
Tuple<RealmConfig, SSLService> config = buildConfig(realmMetadataPath.toString());
Tuple<AbstractReloadingMetadataResolver, Supplier<EntityDescriptor>> tuple = SamlRealm.initializeResolver(
logger,
config.v1(),
config.v2(),
watcherService
);
try {
assertIdp1MetadataParsedCorrectly(tuple.v2().get());
final IdpConfiguration idpConf = SamlRealm.getIdpConfiguration(realmConfig, tuple.v1(), tuple.v2());

// Trigger initialized log message
final List<PublicKey> keys1 = idpConf.getSigningCredentials().stream().map(Credential::getPublicKey).toList();

// Add metadata update listener
final CountDownLatch metadataUpdateLatch = new CountDownLatch(1);
FileReloadListener metadataUpdateListener = new FileReloadListener(realmMetadataPath, metadataUpdateLatch::countDown);
FileWatcher metadataUpdateWatcher = new FileWatcher(realmMetadataPath);
metadataUpdateWatcher.addListener(metadataUpdateListener);
watcherService.add(metadataUpdateWatcher, ResourceWatcherService.Frequency.MEDIUM);
// Put updated metadata contents into realm metadata file
Files.writeString(realmMetadataPath, Files.readString(updatedMetadataPath));
// Remove metadata update listener
metadataUpdateLatch.await();
metadataUpdateWatcher.remove(metadataUpdateListener);

assertThat(Files.readString(realmMetadataPath), is(equalTo(Files.readString(updatedMetadataPath))));
// Trigger changed log message
final List<PublicKey> keys2 = idpConf.getSigningCredentials().stream().map(Credential::getPublicKey).toList();
assertThat(keys1, is(equalTo(keys2)));

// Trigger not changed log message
assertThat(Files.readString(realmMetadataPath), is(equalTo(Files.readString(updatedMetadataPath))));
final List<PublicKey> keys3 = idpConf.getSigningCredentials().stream().map(Credential::getPublicKey).toList();
assertThat(keys1, is(equalTo(keys3)));
} finally {
tuple.v1().destroy();
}
}
} finally {
testThreadPool.shutdown();
}
}

private EntityDescriptor mockIdp() {
final EntityDescriptor descriptor = mock(EntityDescriptor.class);
when(descriptor.getEntityID()).thenReturn("https://idp.saml/");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<EntityDescriptor ID="id2222222BB222B222" entityID="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/MD" xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ns2="http://www.w3.org/2000/09/xmldsig#" xmlns:ns4="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ns3="http://www.w3.org/2001/04/xmlenc#" cacheDuration="PT1439M">
<IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" ID="id6C633B4BB250B980idp">
<KeyDescriptor use="signing">
<ns2:KeyInfo>
<ns2:X509Data>
<ns2:X509Certificate>MIIDojCCAooCCQCVTd3p5WnWmjANBgkqhkiG9w0BAQsFADCBkjELMAkGA1UEBhMC
VVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMREwDwYDVQQK
DAhhdHJpY29yZTENMAsGA1UECwwEZGVtbzEXMBUGA1UEAwwOam9zc28tcHJvdmlk
ZXIxIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAYXRyaWNvcmUuY29tMB4XDTE2MDIw
MjE3MDIwM1oXDTI2MDEzMDE3MDIwM1owgZIxCzAJBgNVBAYTAlVTMQswCQYDVQQI
DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzERMA8GA1UECgwIYXRyaWNvcmUx
DTALBgNVBAsMBGRlbW8xFzAVBgNVBAMMDmpvc3NvLXByb3ZpZGVyMSMwIQYJKoZI
hvcNAQkBFhRzdXBwb3J0QGF0cmljb3JlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAKCBJiMEjYh2Id50qMGGuZzivqFy7t3IwsJgjbS+xV3Jf5Mm
PyXh1AsYpk8eKSYDb+H8+hROeqxbSneXjAi5msrD+oCJnMwz0/uMUPsmntjlrbWS
e2P2vGfLWLp708YLh2RyAA3Iz2Vx5fdbN+14zPfdMF/uNuD4e8XTU7PJcX4cIPna
58P1ko3mCMVoPFI2KLess/EafBvc5OBBmTo3KeQ59hGRdNtCe5oeuLHapfLWnl36
MHHkV/sdV+xVV/NsO5lVJ4al/n7snOsqBvUm++Zbey1OI3CWp9+q1CnnqFxzRiJy
SahYF5FoSiWJKpw7tXHkyU93FCVeBV5c5zxqVykCAwEAATANBgkqhkiG9w0BAQsF
AAOCAQEAU27Ag+jrg+xVbRZc3Dqk40PitlvLiT619U8eyt0LHAhX+ZGy/Ao+pJAx
SWHLP6YofG+EO3Fl4sgJ5S9py+PZwDgRQR1xfUsZ5a8tk6c0NPHpcHBU2pMuYQA+
OoE7g5EIeAhPsmMeM2IH4Yz6qmzhvYBAvbDvGJYHi+Udxp8JHlKYjkieVw+9kI58
0YKeUIKXng4XXSuFHspYRLS2iDRfmeJsveOUYr9y7L4XrbLJIG/kVcpFiLkzsWJp
1j6hwqPe748wekASae/+96l3NjT1AyNnD7rzyskUiNI6wb28OZeJoPczgzIedKXY
dmFqLRuLeSLDJK2EiUATRUqE3ys7Fw==</ns2:X509Certificate>
</ns2:X509Data>
</ns2:KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="encryption">
<ns2:KeyInfo>
<ns2:X509Data>
<ns2:X509Certificate>MIIDojCCAooCCQCVTd3p5WnWmjANBgkqhkiG9w0BAQsFADCBkjELMAkGA1UEBhMC
VVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMREwDwYDVQQK
DAhhdHJpY29yZTENMAsGA1UECwwEZGVtbzEXMBUGA1UEAwwOam9zc28tcHJvdmlk
ZXIxIzAhBgkqhkiG9w0BCQEWFHN1cHBvcnRAYXRyaWNvcmUuY29tMB4XDTE2MDIw
MjE3MDIwM1oXDTI2MDEzMDE3MDIwM1owgZIxCzAJBgNVBAYTAlVTMQswCQYDVQQI
DAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzERMA8GA1UECgwIYXRyaWNvcmUx
DTALBgNVBAsMBGRlbW8xFzAVBgNVBAMMDmpvc3NvLXByb3ZpZGVyMSMwIQYJKoZI
hvcNAQkBFhRzdXBwb3J0QGF0cmljb3JlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAKCBJiMEjYh2Id50qMGGuZzivqFy7t3IwsJgjbS+xV3Jf5Mm
PyXh1AsYpk8eKSYDb+H8+hROeqxbSneXjAi5msrD+oCJnMwz0/uMUPsmntjlrbWS
e2P2vGfLWLp708YLh2RyAA3Iz2Vx5fdbN+14zPfdMF/uNuD4e8XTU7PJcX4cIPna
58P1ko3mCMVoPFI2KLess/EafBvc5OBBmTo3KeQ59hGRdNtCe5oeuLHapfLWnl36
MHHkV/sdV+xVV/NsO5lVJ4al/n7snOsqBvUm++Zbey1OI3CWp9+q1CnnqFxzRiJy
SahYF5FoSiWJKpw7tXHkyU93FCVeBV5c5zxqVykCAwEAATANBgkqhkiG9w0BAQsF
AAOCAQEAU27Ag+jrg+xVbRZc3Dqk40PitlvLiT619U8eyt0LHAhX+ZGy/Ao+pJAx
SWHLP6YofG+EO3Fl4sgJ5S9py+PZwDgRQR1xfUsZ5a8tk6c0NPHpcHBU2pMuYQA+
OoE7g5EIeAhPsmMeM2IH4Yz6qmzhvYBAvbDvGJYHi+Udxp8JHlKYjkieVw+9kI58
0YKeUIKXng4XXSuFHspYRLS2iDRfmeJsveOUYr9y7L4XrbLJIG/kVcpFiLkzsWJp
1j6hwqPe748wekASae/+96l3NjT1AyNnD7rzyskUiNI6wb28OZeJoPczgzIedKXY
dmFqLRuLeSLDJK2EiUATRUqE3ys7Fw==</ns2:X509Certificate>
</ns2:X509Data>
</ns2:KeyInfo>
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc">
<ns3:KeySize>128</ns3:KeySize>
</EncryptionMethod>
</KeyDescriptor>
<ArtifactResolutionService isDefault="true" index="0" Location="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/ARTIFACT/SOAP" Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"/>
<ArtifactResolutionService isDefault="true" index="1" Location="local://JOSSO-TUTORIAL/IDP1/SAML2/ARTIFACT/LOCAL" Binding="urn:oasis:names:tc:SAML:2.0:bindings:LOCAL"/>
<ArtifactResolutionService isDefault="true" index="0" Location="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML11/ARTIFACT/SOAP" Binding="urn:oasis:names:tc:SAML:1.1:bindings:SOAP"/>
<SingleLogoutService ResponseLocation="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/SLO_RESPONSE/POST" Location="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/SLO/POST" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"/>
<SingleLogoutService ResponseLocation="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/SLO_RESPONSE/REDIR" Location="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/SLO/REDIR" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"/>
<SingleLogoutService Location="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/SLO/SOAP" Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"/>
<SingleLogoutService Location="local://JOSSO-TUTORIAL/IDP1/SAML2/SLO/LOCAL" Binding="urn:oasis:names:tc:SAML:2.0:bindings:LOCAL"/>
<ManageNameIDService Location="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/MNI/SOAP" Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"/>
<ManageNameIDService ResponseLocation="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/MNI_RESPONSE/SOAP" Location="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/RNI" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"/>
<ManageNameIDService ResponseLocation="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/MNI_RESPONSE/REDIR" Location="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/RNI/REDIR" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"/>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<SingleSignOnService Location="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/SSO/POST" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"/>
<SingleSignOnService Location="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/SSO/REDIR" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"/>
</IDPSSODescriptor>
<Organization>
<OrganizationName xml:lang="en">Atricore JOSSO 2 IDP</OrganizationName>
<OrganizationDisplayName xml:lang="en">Atricore, Inc.</OrganizationDisplayName>
<OrganizationURL xml:lang="en">http://www.atricore.org</OrganizationURL>
</Organization>
<ContactPerson contactType="other"/>
</EntityDescriptor>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<EntityDescriptor ID="id6C633B4BB250B980" entityID="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/MD" xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ns2="http://www.w3.org/2000/09/xmldsig#" xmlns:ns4="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ns3="http://www.w3.org/2001/04/xmlenc#">
<EntityDescriptor ID="id6C633B4BB250B980" entityID="http://demo_josso_1.josso.dev.docker:8081/IDBUS/JOSSO-TUTORIAL/IDP1/SAML2/MD" xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ns2="http://www.w3.org/2000/09/xmldsig#" xmlns:ns4="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ns3="http://www.w3.org/2001/04/xmlenc#" cacheDuration="PT1440M">
<IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" ID="id6C633B4BB250B980idp">
<KeyDescriptor use="signing">
<ns2:KeyInfo>
Expand Down

0 comments on commit e5cdf69

Please sign in to comment.