From 5eb6d4071e8be8f74bb2c078162fadef81283061 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Mon, 17 Aug 2026 02:11:13 +0900 Subject: [PATCH] feat: validate responses encrypted with XML Encryption 1.1 An IdP that encrypts with XML Encryption 1.1 puts an element inside . That position is a wildcard whose processContents is strict, so the element has to be declared somewhere in the schema set; the set here carried XML Encryption 1.0 only. The result was that the entire response failed schema validation -- before decryption was even attempted -- with: cvc-complex-type.2.4.c: ... no declaration found for element 'xenc11:MGF' Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd Nothing is wrong with such a message: the keys, the certificate and the ciphertext are all fine, and the same response is accepted and decrypted once schema validation is switched off. Current Keycloak encrypts this way by default (xmlenc11#rsa-oaep with mgf1sha256, xmlenc11#aes256-gcm), so a service provider asking for encrypted assertions could not accept its assertions at all. Adds the W3C XML Encryption 1.1 schema next to the ones already bundled and compiles it into every schema set built here, rather than editing a published schema to import it. The resolver also maps the 1.1 namespace to the local copy, so a document that imports it is never fetched over the network. The added test would pass just as well if validation had been loosened, so it also asserts that an element that does not exist in the 1.1 namespace is still refused. Without the schema the test fails on the response it is named for. --- .../saml2/core/util/SchemaFactory.java | 22 +++- .../main/resources/schemas/xenc-schema-11.xsd | 107 ++++++++++++++++++ .../saml2/core/test/util/UtilsTest.java | 25 ++++ ...id_encrypted_assertion_xmlenc11.xml.base64 | 1 + 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 core/src/main/resources/schemas/xenc-schema-11.xsd create mode 100644 core/src/test/resources/data/responses/valid_encrypted_assertion_xmlenc11.xml.base64 diff --git a/core/src/main/java/org/codelibs/saml2/core/util/SchemaFactory.java b/core/src/main/java/org/codelibs/saml2/core/util/SchemaFactory.java index b95b755..8497f83 100644 --- a/core/src/main/java/org/codelibs/saml2/core/util/SchemaFactory.java +++ b/core/src/main/java/org/codelibs/saml2/core/util/SchemaFactory.java @@ -5,6 +5,8 @@ import java.util.Locale; import javax.xml.XMLConstants; +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import org.slf4j.Logger; @@ -36,6 +38,17 @@ private SchemaFactory() { /** URL of the SAML 2.0 protocol schema. */ public static final URL SAML_SCHEMA_PROTOCOL_2_0 = SchemaFactory.class.getResource("/schemas/saml-schema-protocol-2.0.xsd"); + /** + * URL of the XML Encryption 1.1 schema, which is added to every schema set built here. + * + *

Nothing in the SAML schemas imports this namespace, but {@code xenc:EncryptionMethod} + * ends in a wildcard whose {@code processContents} is {@code strict}, so any element carried + * there has to be declared somewhere in the schema set or the document is invalid. An IdP + * encrypting with XML Encryption 1.1 puts {@code } in exactly that position, and + * without this schema the whole response is refused before it is decrypted.

+ */ + private static final URL XML_ENCRYPTION_SCHEMA_1_1 = SchemaFactory.class.getResource("/schemas/xenc-schema-11.xsd"); + /** * Loads a {@link Schema} from the given URL, resolving referenced schemas and DTDs locally. * @@ -66,6 +79,7 @@ public LSInput resolveResource(final String type, final String namespaceURI, fin case "urn:oasis:names:tc:SAML:metadata:algsupport" -> getLocalResource("sstc-saml-metadata-algsupport-v1.0.xsd"); case "urn:oasis:names:tc:SAML:metadata:ui" -> getLocalResource("sstc-saml-metadata-ui-v1.0.xsd"); case "http://www.w3.org/2001/04/xmlenc#" -> getLocalResource("xenc-schema.xsd"); + case "http://www.w3.org/2009/xmlenc11#" -> getLocalResource("xenc-schema-11.xsd"); case "http://www.w3.org/XML/1998/namespace" -> getLocalResource("xml.xsd"); case "http://www.w3.org/2000/09/xmldsig#" -> getLocalResource("xmldsig-core-schema.xsd"); default -> null; @@ -108,6 +122,12 @@ public LSInput getLocalResource(final String name) throws Exception { return lsInput; } }); - return factory.newSchema(schemaUrl); + if (XML_ENCRYPTION_SCHEMA_1_1 == null) { + // the schema is packaged next to the others; carry on without it rather than fail + LOGGER.warn("xenc-schema-11.xsd is missing, so XML Encryption 1.1 elements will not validate"); + return factory.newSchema(schemaUrl); + } + return factory.newSchema(new Source[] { new StreamSource(schemaUrl.toExternalForm()), + new StreamSource(XML_ENCRYPTION_SCHEMA_1_1.toExternalForm()) }); } } diff --git a/core/src/main/resources/schemas/xenc-schema-11.xsd b/core/src/main/resources/schemas/xenc-schema-11.xsd new file mode 100644 index 0000000..0550c3b --- /dev/null +++ b/core/src/main/resources/schemas/xenc-schema-11.xsd @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/test/java/org/codelibs/saml2/core/test/util/UtilsTest.java b/core/src/test/java/org/codelibs/saml2/core/test/util/UtilsTest.java index ac7d2b4..967d23d 100644 --- a/core/src/test/java/org/codelibs/saml2/core/test/util/UtilsTest.java +++ b/core/src/test/java/org/codelibs/saml2/core/test/util/UtilsTest.java @@ -156,6 +156,31 @@ public void testLoadXML() throws URISyntaxException, IOException { * * @see org.codelibs.saml2.core.core.util.Util#validateXML */ + /** + * Tests the ValidateXML method + * Case: Validates a response encrypted with XML Encryption 1.1 + * + * @throws Exception + * + * @see org.codelibs.saml2.core.core.util.Util#validateXML + */ + @Test + public void testValidateXMLEncryptionMethodFromXmlEnc11() throws Exception { + // An IdP encrypting with XML Encryption 1.1 puts inside + // , whose wildcard is strict: unless the 1.1 schema is part of the + // schema set, the whole response is invalid and is refused before it is ever decrypted. + String response = + new String(Base64.decodeBase64(Util.getFileAsString("data/responses/valid_encrypted_assertion_xmlenc11.xml.base64"))); + assertThat(response, containsString("xenc11:MGF")); + + assertTrue(Util.validateXML(Util.loadXML(response), SchemaFactory.SAML_SCHEMA_PROTOCOL_2_0)); + + // and the namespace is known rather than waved through: an element that does not exist in + // it is still refused, which is what tells this apart from switching validation off + String bogus = response.replace("xenc11:MGF", "xenc11:NoSuchElement"); + assertFalse(Util.validateXML(Util.loadXML(bogus), SchemaFactory.SAML_SCHEMA_PROTOCOL_2_0)); + } + @Test public void testValidateXMLBadFormat() throws Exception { String metadataUnloaded = ""; diff --git a/core/src/test/resources/data/responses/valid_encrypted_assertion_xmlenc11.xml.base64 b/core/src/test/resources/data/responses/valid_encrypted_assertion_xmlenc11.xml.base64 new file mode 100644 index 0000000..fb3cd82 --- /dev/null +++ b/core/src/test/resources/data/responses/valid_encrypted_assertion_xmlenc11.xml.base64 @@ -0,0 +1 @@ +PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNWY0NjgyNDk2MDkwNDBjNmEzNTFhYzFiZTBlOWZjNjA1MzNmZjA5ZDNkIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMy0yOVQxMjowMTo1N1oiIERlc3RpbmF0aW9uPSJodHRwOi8vbG9jYWxob3N0OjgwODAvamF2YS1zYW1sLWpzcHNhbXBsZS9hY3MuanNwIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JlNjBiOGNhZjhlOWQxOWI3YTM1NTFiMjQ0ZjExNmM5NDdmZjI0N2QiPg0KICAgIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+DQogICAgPHNhbWxwOlN0YXR1cz4NCiAgICAgICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPg0KICAgIDwvc2FtbHA6U3RhdHVzPg0KICAgIA0KPHNhbWw6RW5jcnlwdGVkQXNzZXJ0aW9uPjx4ZW5jOkVuY3J5cHRlZERhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIiB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBUeXBlPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNFbGVtZW50Ij48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwOS94bWxlbmMxMSNhZXMyNTYtZ2NtIi8+PGRzaWc6S2V5SW5mbyB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48eGVuYzpFbmNyeXB0ZWRLZXk+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDkveG1sZW5jMTEjcnNhLW9hZXAiPjxkc2lnOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIvPjx4ZW5jMTE6TUdGIHhtbG5zOnhlbmMxMT0iaHR0cDovL3d3dy53My5vcmcvMjAwOS94bWxlbmMxMSMiIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwOS94bWxlbmMxMSNtZ2Yxc2hhMjU2Ii8+PC94ZW5jOkVuY3J5cHRpb25NZXRob2Q+PHhlbmM6Q2lwaGVyRGF0YT48eGVuYzpDaXBoZXJWYWx1ZT5lWURRZ1cwK2IrNUp3Z3lwUmZRSWVKa0JxcXpZSDBvSjQ5eDBhQ0ttem91UlEzK1IrelRTMEZxdDM1N0tqRkUrcmNjM0ViMWJ6Y3NEM3doTlVXQmVrMzJlbDVLWit0eklxMUl1RWtQNG0wOTNjOUhlWE1GSVY1VzZkQjl2Z1JTbXVXeUZRemp3d0k2VXY3ME1PQ2lnUGVUZmp4Qk1MVlBLYjRLSzFLS0hkOUE9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kc2lnOktleUluZm8+DQogICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+KytvZDArZzhDV2tCOEE3R3NTb004aVQ2a0xkK2JBTE1adnNhWnRVTFM4cXdKdGNCV29ycTF1VjFvR093dzVnb2s3NEdnQzFpbUhXd2ZYRTdDUklBZkxFU1F3N2VrZkZBZ09ESGtVcTVaM3d0OFIzSUgxMHVDaUg0MHVDQjJiT2hoeFpqSUtUR0oxMlEreFZYRUZXYXZEdlp4alhRSkM1a2RPMTFDVUhicnNiUTE5MG0xdkROUUpjTDRBV3FtWFloa1JLeGhKS3k3V3Rpc2dCR2NWZWdyWlY4dmlvZGN2SHRXVXBrZ0JGa2ZVSlVwZ0hIVlJhK0xMMnJLRGNNTFNGSmVtdjhGc1A4ME8waEZ1NlRWa2t5S3dTL3lja3dTQ2ZpTGFnRUsvdHFtd0M0cU0xd2l4OEdTQWRmaWZQODNuT1JMbEdZNkowd2RZdGVuYi9pWStnd213UDdib3Q1b0JpZm10ZTYySjcvNk9CbHlua3c3QkR4dU1rMFVyZ1h5Nk5JVnFaUHFNTzY1RzZtV2haS0E1ZTF3K3c0MFprZGkyQ2E1cTg4Tm9FOEJCVVBnNysycjZxczM0VlVpQ0Q0QWRvNEJqOWNaOTd4QVVzYTlPMHVEbmR5UWV1RFVNM2dkaGk5RzRXNmIwL3VNOUJKZm10VmN5VUZBb0xRMU10QmtZamNic2JwS0FGbUlQVXNta1JsNlkvSWl6eGNXQmhYOUQrR3U3bmFvTWdEOVcrL09yNU5Fczlob0Fnb3N5MGR0K1kxZCtLUHhFd0ppS0VJcTdqT211cDZWbVMxektqZ25PUmwvRDRJNWkwQjFJY1VRbWdTS3JrL25EWG1panZNVUpZOFN5MEhhMk96NVNkMFhDRFdUamN4cnAzRkQwVmxNTnV1UTBOQXprMmI4QVhXejY5VllESUV6SmZ4OVZFaVlOd3NOdWNlVGtzUVk5K1c3dThGbk5HQjdkL05sZ0NPQkxid3JJRHNXZ2tkN083NW4rdmJKWmsza2FHd1dpbWcrNHpCckt2TFh1UkIrdjBGUGl2bUk0c1Z0WENOZFhQekVXb1RpTE5ic29ZWkkvNE12SnlxdnA5cTZVanF1R0hIcWVCVUtqa0lHM3NWWmpiVCtNckZWOFArRXFiSVMyZkJOOEF3dDI5Ri9paHFQdUVtL1REWTRvcmFIMm5vajlPWnlQRktWNzNGM0VQV3JxaXFzZkdmcEVLRjQrZFJRUXhXK01ERkFZQlZMZmlPSDFEYzZYblY4SVlUNHhsbjY3SmV4SGZEdUVtK3hUcHVGTi9ENGpQZDFWWTQydlovVGZCQURkQmU0QTQyeHdWMTF5YnZDcVBJbHJuNWlhenNiVFN5OFJUaUkvSi9KbVFmNVg1eVlJMVpnUWZoQnB0UzlIUk9JWnFZVm5KWjlsL2xxTUFGQzhuWkMyV3hiM0pyT1Y3NjZINE1aUkFGWTdlaDBqSkJrMFRsemdaMncxdXE4TkVhUVlsNnZjYVAwUVc1TEEzTDdvR0tSTHB6clQzbEZiOHdEUmxxeTNnL3hDdFpYQ1hQWm5qTTJ4di8zRy8zV2R0NlFkZ0xPQmRQSU5uRlljb1VRQlJjOE9Lc3NGWEl3eTdBTmVkZ3NTbGhtWnljZks2WUNRaXBGRDRRcmRYSklXTmwzRVRXR3BJdnlaN1hmcXFlcUxINnZ3OWRjWDEvRi9KVUJjWWpHelpMRldvbVhXeDBLZDBUTTNIMlVubWhJdW0zU3kwY2ZXcSszTVBwamJUQUQ5Y3FSMnQ2OGhOcU9sazYxelJRS3J2N1Y0WXF1S1dVdUVBYVVXS1cydVFwTlBKcjJqWE5mNWdvSGhXTzZvK014ZUFNVkoyQ0J6T0VSd2tqVmI4NkNMallUUVQ0bU1uaW52eUhER3BhSkxvbk04YmpueHBjOFRRTXczaXVXT0UrZEkwemZrbDRRRzFieWFHMDQ0ampBaFFWVEpUOTRhZXF1VCtVYkZXNkZxRDRYckd1Nkp1SGZkK2xBZHdOSjdBeHROaUk1ZmtWUlVtQ0lEWWhXVHVxQTRMOHF6QVlZdmVEUk5rRCtsL0xGem9ZZm5nYnplVU9vVGVWRE5EbDVUYi8wdXlVM1BPWVlzMVFVN3B2VnlKUnZ3OEUweHNtaEx2R3NYalJER2E1NzZmektnMXlha25CMlhONVFJUk5yZldmOWt5WmdJcFp2cnFwQXRjNU4reEZHS1g2M2pYV2pzU0Q1WDduNE0zaEhvekpXbjU4OEluOVp2OUl6akNuTWN2c0FDKzhoYWFIVko5Si8wWXIzTk9JTGY2VHhpcUVLNDNSS2JHWGRtY3cyUUpwUGlUa2RPQXRHWTA1bTlOSi9lQ1hyQ3dpVTNONDRCa2sxRFV6WDdrQStaSjF4RnlrbzhGa1hJcFhkWEphK05aM1VsTEdYUlRIdUY3dG8wSlR0VE4xRGVIWDE3MlhUbGhjSlJCbHMrUXUxTytVUjV0WTZmNDY4aEJ5V1hQUE83bkJXVVBxT2dDWWhZVlhaU3JEUXYzVmpDcnIyU2h6dEZkMlQwN0ltSm5UNGRWMlhrREk0VUE5bitzcDN4UmIrOWZEMHhzd0VFZWI0M0tBdnpjeG9xNWRTN1h1dVBxeVVpVXE3TC95VWFSSlY0MmFXOTBMN3NHcUM2dFI0ZS9yR3VsSm9GbDJpRTA1dnlPai9hKzB1QnlMOTNZTkErT3lEaE5QQnVIVjFkcngvK3RVLzVtQnFCdCs4VHVsMmVTZUpZVWVSTjNNTnI2b2wvM1BBakUrT0h3Vk96ZHU4OGd5MFkrMTM1YzJXTWIyZmN3dVdYZXVMVzZIZTVlaUNDbUNoWWpkUkhlZ3R5Wi80S3Q3dHNUV2dveWJNc2kvR3h3bjIxSjlLOVFVTExaMmlmV3kzaFNJbGpUZjVOZXF4NlNQWE1UWmNQenZxT2xvMFBZUUJIdExObmRRQStKbzJCVzVvdENtQmt2WWRRQWUwMDc4SlhpNW9saHhRVHlkQkI0WGlFbjl5YWJnZmRMa1FpY2pydUpTZTBISzZhUGpjS1RTWXVNcHVCZzJmbHlDTXV6YzJBY1p0UjhxTHZzUEl0WXFFWG00dXFRaTZxbjExVzZlcjUyYm9HM0liODJOZzV0bVZUUjZxak5hdEM3Zk9tK0RrQ3BlcWVWT01uUmYvbmtTSGxmQW9mK2hEWDV0a2ZvRUlHdngvRjRheVJmc0tJWnR0MHBLd3VicGFSUHZFOXZIS3JxeElLQVFDL3BTaXQ3QTJtcEtXcUN6YmJyY1UyeUVHcXYzbSs5dzdoSlhGNlcrZElmMU92Q2xjaXBtMTJDY2Jua1ZkQUlJdkcvTTF2aWFFZE9ZeGEzdVpVK0JYNHBZRjNZbUNsaUE1VUJlVHpabmNsdTJqQ2lzMHBTM0J5K3lHd0Jtc2pYSGhyMTh6SXlJZVBwTGZiNlgxVHB4MTRhUWxoa2NocEhiMHUreUQxa3pFeXY2U29Mdm03TUFZcHcxcXNZbmd4bnExbzdDN0hCUTNobEw4UEQ4M3dxMnV4SlVJU2IrS292Y0JQbmhWRHhLNTZ4N1BzUFl3YWU4RmkyVDcvenZTVTlFUy9wNHhTdTM2d2dLSGF1TE9MWERKeGlPNG13QmJmTlh2cGFkOXJrZnNLWUNqbmgwZFFXNjNubG1RMDhvWkFVT3NUWXRBZHc5MUhObHROMG1tMEdaODdSSC9xSTJPMFFMdk8yWWRIV1R2cG1QbTNnM1FxK29TSnF2czI4WGRLaWFicGtXVyt5c28rZ3JSYWNmb1VPU2xYSTBUdjFVSnpYekhEUjJxbkp4cmtSM25rV1FFZ3RWK28zbDlYOVladUo4aTRwMWZGTVA2TTU0SHRodWV3bFBBSDNiVDJaeGV1cUM3bmFyRndnZnVtZ0hOL1hob2J4ZHZlRjJGTW9jVklWcUt5ZmN3dnM5Y1JlRWp6clR5VzQ3RVpoaUZEL1V0Qy9wOFlLdTdkb1FPSENCeFd3b2VzcUJNbWQ1NHByZDJicklnOWxhRzFuNzZJbkdSQ05wL2VaZENPQ05nZ0w0RkxHRjAxMXlVbVBLejJOQTNVMTNRTFRyQXhLVzZkeXF1V0s4bk5RVW9BOG9WKy9WVjFja1BhTGZ2V0FnNnMzekJUUGdJZzB4RE01SHNNSzVlcFY1WGltUDwveGVuYzpDaXBoZXJWYWx1ZT4NCiAgIDwveGVuYzpDaXBoZXJEYXRhPg0KPC94ZW5jOkVuY3J5cHRlZERhdGE+PC9zYW1sOkVuY3J5cHRlZEFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg== \ No newline at end of file