Skip to content

Commit 4868af6

Browse files
Add Issuer to failed SAML Signature validation logs when available (#126310) (#127219)
* Add Issuer to failed SAML Signature validation logs when available * [CI] Auto commit changes from spotless * Fix tests * Update docs/changelog/126310.yaml * address review comments * replace String.format call * update formatIssuer to describeIssuer * [CI] Auto commit changes from spotless * truncate long issuers in log messages * [CI] Auto commit changes from spotless * handle null issuer value * address review comments --------- Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co> (cherry picked from commit ceaa01a)
1 parent 8e4aef0 commit 4868af6

File tree

6 files changed

+146
-46
lines changed

6 files changed

+146
-46
lines changed

docs/changelog/126310.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 126310
2+
summary: Add Issuer to failed SAML Signature validation logs when available
3+
area: Security
4+
type: enhancement
5+
issues:
6+
- 111022

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticator.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ private SamlAttributes authenticateResponse(Element element, Collection<String>
9393
}
9494
final boolean requireSignedAssertions;
9595
if (response.isSigned()) {
96-
validateSignature(response.getSignature());
96+
validateSignature(response.getSignature(), response.getIssuer());
9797
requireSignedAssertions = false;
9898
} else {
9999
requireSignedAssertions = true;
@@ -199,7 +199,7 @@ private List<Attribute> processAssertion(Assertion assertion, boolean requireSig
199199
}
200200
// Do not further process unsigned Assertions
201201
if (assertion.isSigned()) {
202-
validateSignature(assertion.getSignature());
202+
validateSignature(assertion.getSignature(), assertion.getIssuer());
203203
} else if (requireSignature) {
204204
throw samlException("Assertion [{}] is not signed, but a signature is required", assertion.getElementQName());
205205
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlLogoutRequestHandler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private Result parseLogout(LogoutRequest logoutRequest, boolean requireSignature
7373
throw samlException("Logout request is not signed");
7474
}
7575
} else {
76-
validateSignature(signature);
76+
validateSignature(signature, logoutRequest.getIssuer());
7777
}
7878

7979
checkIssuer(logoutRequest.getIssuer(), logoutRequest);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlLogoutResponseHandler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public void handle(boolean httpRedirect, String payload, Collection<String> allo
4545
if (logoutResponse.getSignature() == null) {
4646
throw samlException("LogoutResponse is not signed, but is required for HTTP-Post binding");
4747
}
48-
validateSignature(logoutResponse.getSignature());
48+
validateSignature(logoutResponse.getSignature(), logoutResponse.getIssuer());
4949
}
5050
checkInResponseTo(logoutResponse, allowedSamlRequestIds);
5151
checkStatus(logoutResponse.getStatus());

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlObjectHandler.java

+39-20
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ public class SamlObjectHandler {
9595
}
9696
});
9797

98+
private static final int ISSUER_VALUE_MAX_LENGTH = 512;
99+
98100
protected final Logger logger = LogManager.getLogger(getClass());
99101

100102
@Nullable
@@ -161,13 +163,13 @@ protected static String describe(Collection<X509Credential> credentials) {
161163
return credentials.stream().map(credential -> describe(credential.getEntityCertificate())).collect(Collectors.joining(","));
162164
}
163165

164-
void validateSignature(Signature signature) {
166+
void validateSignature(Signature signature, @Nullable Issuer issuer) {
165167
final String signatureText = text(signature, 32);
166168
SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
167169
try {
168170
profileValidator.validate(signature);
169171
} catch (SignatureException e) {
170-
throw samlSignatureException(idp.getSigningCredentials(), signatureText, e);
172+
throw samlSignatureException(issuer, idp.getSigningCredentials(), signatureText, e);
171173
}
172174

173175
checkIdpSignature(credential -> {
@@ -200,21 +202,21 @@ void validateSignature(Signature signature) {
200202
);
201203
return true;
202204
} catch (PrivilegedActionException e) {
203-
logger.warn("SecurityException while attempting to validate SAML signature", e);
205+
logger.warn("SecurityException while attempting to validate SAML signature." + describeIssuer(issuer), e);
204206
return false;
205207
}
206208
});
207209
} catch (PrivilegedActionException e) {
208210
throw new SecurityException("SecurityException while attempting to validate SAML signature", e);
209211
}
210-
}, signatureText);
212+
}, signatureText, issuer);
211213
}
212214

213215
/**
214216
* Tests whether the provided function returns {@code true} for any of the IdP's signing credentials.
215-
* @throws ElasticsearchSecurityException - A SAML exception if not matching credential is found.
217+
* @throws ElasticsearchSecurityException - A SAML exception if no matching credential is found.
216218
*/
217-
protected void checkIdpSignature(CheckedFunction<Credential, Boolean, Exception> check, String signatureText) {
219+
protected void checkIdpSignature(CheckedFunction<Credential, Boolean, Exception> check, String signatureText, @Nullable Issuer issuer) {
218220
final Predicate<Credential> predicate = credential -> {
219221
try {
220222
return check.apply(credential);
@@ -231,35 +233,52 @@ protected void checkIdpSignature(CheckedFunction<Credential, Boolean, Exception>
231233
logger.trace("SAML Signature failure caused by", e);
232234
return false;
233235
} catch (Exception e) {
234-
logger.warn("Exception while attempting to validate SAML Signature", e);
236+
logger.warn("Exception while attempting to validate SAML Signature." + describeIssuer(issuer), e);
235237
return false;
236238
}
237239
};
238240
final List<Credential> credentials = idp.getSigningCredentials();
239241
if (credentials.stream().anyMatch(predicate) == false) {
240-
throw samlSignatureException(credentials, signatureText);
242+
throw samlSignatureException(issuer, credentials, signatureText);
241243
}
242244
}
243245

244246
/**
245247
* Constructs a SAML specific exception with a consistent message regarding SAML Signature validation failures
246248
*/
247-
private ElasticsearchSecurityException samlSignatureException(List<Credential> credentials, String signature, Exception cause) {
249+
private ElasticsearchSecurityException samlSignatureException(
250+
@Nullable Issuer issuer,
251+
List<Credential> credentials,
252+
String signature,
253+
Exception cause
254+
) {
248255
logger.warn(
249-
"The XML Signature of this SAML message cannot be validated. Please verify that the saml realm uses the correct SAML"
250-
+ "metadata file/URL for this Identity Provider"
256+
"The XML Signature of this SAML message cannot be validated. Please verify that the saml realm uses the correct SAML "
257+
+ "metadata file/URL for this Identity Provider.{}",
258+
describeIssuer(issuer)
251259
);
252260
final String msg = "SAML Signature [{}] could not be validated against [{}]";
253-
return samlException(msg, cause, signature, describeCredentials(credentials));
261+
if (cause != null) {
262+
return samlException(msg, cause, signature, describeCredentials(credentials));
263+
} else {
264+
return samlException(msg, signature, describeCredentials(credentials));
265+
}
254266
}
255267

256-
private ElasticsearchSecurityException samlSignatureException(List<Credential> credentials, String signature) {
257-
logger.warn(
258-
"The XML Signature of this SAML message cannot be validated. Please verify that the saml realm uses the correct SAML"
259-
+ "metadata file/URL for this Identity Provider"
260-
);
261-
final String msg = "SAML Signature [{}] could not be validated against [{}]";
262-
return samlException(msg, signature, describeCredentials(credentials));
268+
private ElasticsearchSecurityException samlSignatureException(Issuer issuer, List<Credential> credentials, String signature) {
269+
return samlSignatureException(issuer, credentials, signature, null);
270+
}
271+
272+
// package private for testing
273+
static String describeIssuer(@Nullable Issuer issuer) {
274+
if (issuer == null || issuer.getValue() == null) {
275+
return "";
276+
}
277+
final String msg = " The issuer included in the SAML message was [%s]";
278+
if (issuer.getValue().length() > ISSUER_VALUE_MAX_LENGTH) {
279+
return Strings.format(msg + "...", Strings.cleanTruncate(issuer.getValue(), ISSUER_VALUE_MAX_LENGTH));
280+
}
281+
return Strings.format(msg, issuer.getValue());
263282
}
264283

265284
private static String describeCredentials(List<Credential> credentials) {
@@ -423,7 +442,7 @@ private void validateSignature(String inputString, String signatureAlgorithm, St
423442
);
424443
return false;
425444
}
426-
}, signatureText);
445+
}, signatureText, null);
427446
}
428447

429448
protected byte[] decodeBase64(String content) {

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticatorTests.java

+97-22
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.opensaml.saml.saml2.core.SubjectConfirmation;
4040
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
4141
import org.opensaml.saml.saml2.core.impl.AuthnStatementBuilder;
42+
import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
4243
import org.opensaml.saml.saml2.encryption.Encrypter;
4344
import org.opensaml.security.credential.BasicCredential;
4445
import org.opensaml.security.credential.Credential;
@@ -83,7 +84,9 @@
8384
import static org.hamcrest.Matchers.contains;
8485
import static org.hamcrest.Matchers.containsInAnyOrder;
8586
import static org.hamcrest.Matchers.containsString;
87+
import static org.hamcrest.Matchers.endsWith;
8688
import static org.hamcrest.Matchers.equalTo;
89+
import static org.hamcrest.Matchers.hasLength;
8790
import static org.hamcrest.Matchers.instanceOf;
8891
import static org.hamcrest.Matchers.is;
8992
import static org.hamcrest.Matchers.iterableWithSize;
@@ -106,6 +109,9 @@ public class SamlAuthenticatorTests extends SamlResponseHandlerTests {
106109
+ "Attributes with a name clash may prevent authentication or interfere will role mapping. "
107110
+ "Change your IdP configuration to use a different attribute *"
108111
+ " that will not clash with any of [*]";
112+
private static final String SIGNATURE_VALIDATION_FAILED_LOG_MESSAGE = "The XML Signature of this SAML message cannot be validated. "
113+
+ "Please verify that the saml realm uses the correct SAML metadata file/URL for this Identity Provider. "
114+
+ "The issuer included in the SAML message was [https://idp.saml.elastic.test/]";
109115

110116
private SamlAuthenticator authenticator;
111117

@@ -741,16 +747,29 @@ public void testIncorrectSigningKeyIsRejected() throws Exception {
741747
// check that the content is valid when signed by the correct key-pair
742748
assertThat(authenticator.authenticate(token(signer.transform(xml, idpSigningCertificatePair))), notNullValue());
743749

744-
// check is rejected when signed by a different key-pair
745-
final Tuple<X509Certificate, PrivateKey> wrongKey = readKeyPair("RSA_4096_updated");
746-
final ElasticsearchSecurityException exception = expectThrows(
747-
ElasticsearchSecurityException.class,
748-
() -> authenticator.authenticate(token(signer.transform(xml, wrongKey)))
749-
);
750-
assertThat(exception.getMessage(), containsString("SAML Signature"));
751-
assertThat(exception.getMessage(), containsString("could not be validated"));
752-
assertThat(exception.getCause(), nullValue());
753-
assertThat(SamlUtils.isSamlException(exception), is(true));
750+
try (var mockLog = MockLog.capture(authenticator.getClass())) {
751+
mockLog.addExpectation(
752+
new MockLog.SeenEventExpectation(
753+
"Invalid Signature",
754+
authenticator.getClass().getName(),
755+
Level.WARN,
756+
SIGNATURE_VALIDATION_FAILED_LOG_MESSAGE
757+
)
758+
);
759+
760+
// check is rejected when signed by a different key-pair
761+
final Tuple<X509Certificate, PrivateKey> wrongKey = readKeyPair("RSA_4096_updated");
762+
final ElasticsearchSecurityException exception = expectThrows(
763+
ElasticsearchSecurityException.class,
764+
() -> authenticator.authenticate(token(signer.transform(xml, wrongKey)))
765+
);
766+
assertThat(exception.getMessage(), containsString("SAML Signature"));
767+
assertThat(exception.getMessage(), containsString("could not be validated"));
768+
assertThat(exception.getCause(), nullValue());
769+
assertThat(SamlUtils.isSamlException(exception), is(true));
770+
771+
mockLog.assertAllExpectationsMatched();
772+
}
754773
}
755774

756775
public void testSigningKeyIsReloadedForEachRequest() throws Exception {
@@ -1301,24 +1320,80 @@ public void testFailureWhenIdPCredentialsAreEmpty() throws Exception {
13011320
authenticator = buildAuthenticator(() -> emptyList(), emptyList());
13021321
final String xml = getSimpleResponseAsString(clock.instant());
13031322
final SamlToken token = token(signResponse(xml));
1304-
final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token));
1305-
assertThat(exception.getCause(), nullValue());
1306-
assertThat(exception.getMessage(), containsString("SAML Signature"));
1307-
assertThat(exception.getMessage(), containsString("could not be validated"));
1308-
// Restore the authenticator with credentials for the rest of the test cases
1309-
authenticator = buildAuthenticator(() -> buildOpenSamlCredential(idpSigningCertificatePair), emptyList());
1323+
1324+
try (var mockLog = MockLog.capture(authenticator.getClass())) {
1325+
mockLog.addExpectation(
1326+
new MockLog.SeenEventExpectation(
1327+
"Invalid signature",
1328+
authenticator.getClass().getName(),
1329+
Level.WARN,
1330+
SIGNATURE_VALIDATION_FAILED_LOG_MESSAGE
1331+
)
1332+
);
1333+
1334+
final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token));
1335+
assertThat(exception.getCause(), nullValue());
1336+
assertThat(exception.getMessage(), containsString("SAML Signature"));
1337+
assertThat(exception.getMessage(), containsString("could not be validated"));
1338+
1339+
mockLog.awaitAllExpectationsMatched();
1340+
}
13101341
}
13111342

13121343
public void testFailureWhenIdPCredentialsAreNull() throws Exception {
13131344
authenticator = buildAuthenticator(() -> singletonList(null), emptyList());
13141345
final String xml = getSimpleResponseAsString(clock.instant());
13151346
final SamlToken token = token(signResponse(xml));
1316-
final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token));
1317-
assertThat(exception.getCause(), nullValue());
1318-
assertThat(exception.getMessage(), containsString("SAML Signature"));
1319-
assertThat(exception.getMessage(), containsString("could not be validated"));
1320-
// Restore the authenticator with credentials for the rest of the test cases
1321-
authenticator = buildAuthenticator(() -> buildOpenSamlCredential(idpSigningCertificatePair), emptyList());
1347+
1348+
try (var mockLog = MockLog.capture(authenticator.getClass())) {
1349+
mockLog.addExpectation(
1350+
new MockLog.SeenEventExpectation(
1351+
"Invalid signature",
1352+
authenticator.getClass().getName(),
1353+
Level.WARN,
1354+
SIGNATURE_VALIDATION_FAILED_LOG_MESSAGE
1355+
)
1356+
);
1357+
mockLog.addExpectation(
1358+
new MockLog.SeenEventExpectation(
1359+
"Null credentials",
1360+
authenticator.getClass().getName(),
1361+
Level.WARN,
1362+
"Exception while attempting to validate SAML Signature. "
1363+
+ "The issuer included in the SAML message was [https://idp.saml.elastic.test/]"
1364+
)
1365+
);
1366+
1367+
final ElasticsearchSecurityException exception = expectSamlException(() -> authenticator.authenticate(token));
1368+
assertThat(exception.getCause(), nullValue());
1369+
assertThat(exception.getMessage(), containsString("SAML Signature"));
1370+
assertThat(exception.getMessage(), containsString("could not be validated"));
1371+
1372+
mockLog.awaitAllExpectationsMatched();
1373+
}
1374+
}
1375+
1376+
public void testDescribeNullIssuer() {
1377+
final Issuer issuer = randomFrom(new IssuerBuilder().buildObject(), null);
1378+
assertThat(SamlAuthenticator.describeIssuer(issuer), equalTo(""));
1379+
}
1380+
1381+
public void testDescribeIssuer() {
1382+
final Issuer issuer = new IssuerBuilder().buildObject();
1383+
issuer.setValue("https://idp.saml.elastic.test/");
1384+
assertThat(
1385+
SamlAuthenticator.describeIssuer(issuer),
1386+
equalTo(" The issuer included in the SAML message was [https://idp.saml.elastic.test/]")
1387+
);
1388+
}
1389+
1390+
public void testDescribeVeryLongIssuer() {
1391+
final Issuer issuer = new IssuerBuilder().buildObject();
1392+
issuer.setValue("https://idp.saml.elastic.test/" + randomAlphaOfLength(512));
1393+
1394+
final String description = SamlAuthenticator.describeIssuer(issuer);
1395+
assertThat(description, hasLength(562));
1396+
assertThat(description, endsWith("..."));
13221397
}
13231398

13241399
private interface CryptoTransform {

0 commit comments

Comments
 (0)