diff --git a/poetry.lock b/poetry.lock
index a3197efc6..fe4b90619 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -780,6 +780,113 @@ MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
+[[package]]
+name = "lxml"
+version = "4.9.3"
+description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
+files = [
+ {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"},
+ {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"},
+ {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"},
+ {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"},
+ {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"},
+ {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"},
+ {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"},
+ {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"},
+ {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"},
+ {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"},
+ {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"},
+ {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"},
+ {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"},
+ {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"},
+ {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"},
+ {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"},
+ {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"},
+ {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"},
+ {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"},
+ {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"},
+ {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"},
+ {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"},
+ {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"},
+ {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"},
+ {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"},
+ {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"},
+ {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"},
+ {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"},
+ {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"},
+ {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"},
+ {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"},
+ {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"},
+ {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"},
+ {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"},
+ {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"},
+ {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"},
+ {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"},
+ {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"},
+ {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"},
+ {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"},
+ {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"},
+ {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"},
+ {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"},
+ {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"},
+ {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"},
+ {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"},
+ {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"},
+ {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"},
+ {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"},
+ {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"},
+ {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"},
+ {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"},
+ {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"},
+ {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"},
+ {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"},
+ {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"},
+ {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"},
+ {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"},
+ {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"},
+ {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"},
+ {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"},
+ {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"},
+ {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"},
+ {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"},
+ {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"},
+ {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"},
+ {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"},
+ {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"},
+ {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"},
+ {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"},
+ {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"},
+ {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"},
+]
+
+[package.extras]
+cssselect = ["cssselect (>=0.7)"]
+html5 = ["html5lib"]
+htmlsoup = ["BeautifulSoup4"]
+source = ["Cython (>=0.29.35)"]
+
[[package]]
name = "markupsafe"
version = "2.1.3"
@@ -1935,4 +2042,4 @@ s2repoze = ["paste", "repoze.who", "zope.interface"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
-content-hash = "a13573a313282527140c0f524bf455edcffa750e7c3da6a0e9c601a9b1e351df"
+content-hash = "65192018417a3dbe3df5854a55b82d3e26213a24e3ae91bdb68e32433f26f899"
diff --git a/pyproject.toml b/pyproject.toml
index 059128d17..030d2ac8f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,6 +49,7 @@ pytz = "*"
requests = "^2"
xmlschema = ">=1.2.1"
"zope.interface" = {optional = true, version = "*"}
+lxml = "^4.9.3"
[tool.poetry.extras]
s2repoze = ["paste", "repoze-who", "zope-interface"]
diff --git a/src/saml2/__init__.py b/src/saml2/__init__.py
index 01067b4be..67ec291b6 100644
--- a/src/saml2/__init__.py
+++ b/src/saml2/__init__.py
@@ -20,9 +20,22 @@
from typing import Any
from typing import Optional
from typing import Union
+
from xml.etree import ElementTree
+from xml.etree.ElementTree import Element
+from xml.etree.ElementTree import iselement
import defusedxml.ElementTree
+from defusedxml.ElementTree import tostring
+from defusedxml.ElementTree import fromstring
+
+import lxml.etree
+from lxml.etree import XMLParser
+from lxml.etree import tostring
+from lxml.etree import fromstring
+from lxml.etree import ElementTree
+from lxml.etree import Element
+from lxml.etree import iselement
from saml2.validate import valid_instance
from saml2.version import version as __version__
@@ -68,6 +81,30 @@ def class_name(instance):
return f"{instance.c_namespace}:{instance.c_tag}"
+def xml_from_string(xmlstr, *args, **kwargs):
+ if isinstance(xmlstr, str):
+ xmlstr = xmlstr.encode("utf-8")
+
+ parser = XMLParser(
+ no_network=True,
+ ns_clean=False,
+ remove_comments=True,
+ remove_pis=True,
+ strip_cdata=True,
+ resolve_entities=False,
+ huge_tree=False,
+ )
+ data = fromstring(xmlstr, parser=parser, *args, **kwargs)
+ #data = fromstring(xmlstr)
+
+ return data
+
+
+def xml_to_string(xmldata, *args, **kwargs):
+ xmlstr = tostring(xmldata, *args, **kwargs)
+ return xmlstr
+
+
def create_class_from_xml_string(target_class, xml_string):
"""Creates an instance of the target class from a string.
@@ -84,7 +121,7 @@ def create_class_from_xml_string(target_class, xml_string):
"""
if not isinstance(xml_string, bytes):
xml_string = xml_string.encode("utf-8")
- tree = defusedxml.ElementTree.fromstring(xml_string)
+ tree = xml_from_string(xml_string)
return create_class_from_element_tree(target_class, tree)
@@ -155,24 +192,31 @@ def __init__(self, tag, namespace=None, attributes=None, children=None, text=Non
self.attributes = attributes or {}
self.children = children or []
self.text = text
+ self.c_ns_prefix = {}
def to_string(self):
"""Serialize the object into a XML string"""
element_tree = self.transfer_to_element_tree()
- return ElementTree.tostring(element_tree, encoding="UTF-8")
+ return xml_to_string(element_tree, encoding="UTF-8")
def transfer_to_element_tree(self):
if self.tag is None:
return None
- element_tree = ElementTree.Element("")
-
- if self.namespace is not None:
- element_tree.tag = f"{{{self.namespace}}}{self.tag}"
- else:
- element_tree.tag = self.tag
+ _tag = (
+ f"{{{self.namespace}}}{self.tag}"
+ if self.namespace is not None
+ else self.tag
+ )
+ element_tree = Element(_tag)
for key, value in iter(self.attributes.items()):
+ if key.startswith("xmlns:"):
+ prefix = key.split(":")[1]
+ if prefix == "ns":
+ prefix = None
+ self.c_ns_prefix[prefix] = value
+ continue
element_tree.attrib[key] = value
for child in self.children:
@@ -263,12 +307,16 @@ def loadd(self, ava):
def extension_element_from_string(xml_string):
- element_tree = defusedxml.ElementTree.fromstring(xml_string)
+ element_tree = xml_from_string(xml_string)
return _extension_element_from_element_tree(element_tree)
def _extension_element_from_element_tree(element_tree):
elementc_tag = element_tree.tag
+ if not isinstance(elementc_tag, str):
+ # XXX node is of type Entity (&e) or comment
+ # XXX enities and comments should be removed
+ return None
if "}" in elementc_tag:
namespace = elementc_tag[1 : elementc_tag.index("}")]
tag = elementc_tag[elementc_tag.index("}") + 1 :]
@@ -287,13 +335,14 @@ def _extension_element_from_element_tree(element_tree):
class ExtensionContainer:
c_tag = ""
c_namespace = ""
+ c_ns_prefix = {}
- def __init__(self, text=None, extension_elements=None, extension_attributes=None):
-
+ def __init__(self, text=None, extension_elements=None, extension_attributes=None, nsmap=None):
self.text = text
self.extension_elements = extension_elements or []
self.extension_attributes = extension_attributes or {}
self.encrypted_assertion = None
+ self.c_ns_prefix = nsmap or {}
# Three methods to create an object from an ElementTree
def harvest_element_tree(self, tree):
@@ -315,6 +364,12 @@ def _add_members_to_element_tree(self, tree):
for child in self.extension_elements:
child.become_child_element_of(tree)
for attribute, value in iter(self.extension_attributes.items()):
+ if attribute.startswith("xmlns:"):
+ prefix = attribute.split(":")[1]
+ if prefix == "ns":
+ prefix = None
+ self.c_ns_prefix[prefix] = value
+ continue
tree.attrib[attribute] = value
tree.text = self.text
@@ -514,10 +569,10 @@ def become_child_element_of(self, node):
:param node: The node to which this instance should be a child
"""
- new_child = self._to_element_tree()
+ new_child = self._to_element_tree(self.c_ns_prefix)
node.append(new_child)
- def _to_element_tree(self):
+ def _to_element_tree(self, nsmap={}):
"""
Note, this method is designed to be used only with classes that have a
@@ -525,7 +580,7 @@ def _to_element_tree(self):
should not be called on in this class.
"""
- new_tree = ElementTree.Element(f"{{{self.__class__.c_namespace}}}{self.__class__.c_tag}")
+ new_tree = Element(f"{{{self.__class__.c_namespace}}}{self.__class__.c_tag}", nsmap=nsmap)
self._add_members_to_element_tree(new_tree)
return new_tree
@@ -588,7 +643,7 @@ def get_xml_string_with_self_contained_assertion_within_advice_encrypted_asserti
if assertion is not None:
self.set_prefixes(assertion, prefix_map)
- return ElementTree.tostring(tree, encoding="UTF-8").decode("utf-8")
+ return xml_to_string(tree, encoding="UTF-8").decode("utf-8")
def get_xml_string_with_self_contained_assertion_within_encrypted_assertion(self, assertion_tag):
"""Makes a encrypted assertion only containing self contained
@@ -603,18 +658,19 @@ def get_xml_string_with_self_contained_assertion_within_encrypted_assertion(self
self.set_prefixes(tree.find(self.encrypted_assertion._to_element_tree().tag).find(assertion_tag), prefix_map)
- return ElementTree.tostring(tree, encoding="UTF-8").decode("utf-8")
+ return xml_to_string(tree, encoding="UTF-8").decode("utf-8")
def set_prefixes(self, elem, prefix_map):
# check if this is a tree wrapper
- if not ElementTree.iselement(elem):
+ if not iselement(elem):
elem = elem.getroot()
# build uri map and add to root element
uri_map = {}
for prefix, uri in prefix_map.items():
uri_map[uri] = prefix
+ # XXX issue
elem.set(f"xmlns:{prefix}", uri)
# fixup all elements in the tree
@@ -652,7 +708,7 @@ def to_string_force_namespace(self, nspair):
self.set_prefixes(elem, nspair)
- return ElementTree.tostring(elem, encoding="UTF-8")
+ return xml_to_string(elem, encoding="UTF-8")
def to_string(self, nspair=None):
"""Converts the Saml object to a string containing XML.
@@ -661,13 +717,12 @@ def to_string(self, nspair=None):
constructing the text representation.
:return: String representation of the object
"""
- if not nspair and self.c_ns_prefix:
- nspair = self.c_ns_prefix
+ nspair = nspair or self.c_ns_prefix or None
- if nspair:
- self.register_prefix(nspair)
+ #if nspair:
+ # self.register_prefix(nspair)
- return ElementTree.tostring(self._to_element_tree(), encoding="UTF-8")
+ return xml_to_string(self._to_element_tree(nsmap=nspair), encoding="UTF-8")
def __str__(self):
# Yes this is confusing. http://bugs.python.org/issue10942
diff --git a/src/saml2/pack.py b/src/saml2/pack.py
index 99c32476b..ac49ac854 100644
--- a/src/saml2/pack.py
+++ b/src/saml2/pack.py
@@ -18,9 +18,12 @@
import logging
from urllib.parse import urlencode
from urllib.parse import urlparse
-from xml.etree import ElementTree as ElementTree
-import defusedxml.ElementTree
+from xml.etree.ElementTree import Element
+from lxml.etree import Element
+
+from saml2 import xml_from_string
+from saml2 import xml_to_string
import saml2
from saml2.s_utils import deflate_and_base64_encode
@@ -201,19 +204,19 @@ def make_soap_enveloped_saml_thingy(thingy, header_parts=None):
:param thingy: The SAML thingy
:return: The SOAP envelope as a string
"""
- envelope = ElementTree.Element("")
- envelope.tag = "{%s}Envelope" % NAMESPACE
+ envelope_tag = "{%s}Envelope" % NAMESPACE
+ envelope = Element(envelope_tag)
if header_parts:
- header = ElementTree.Element("")
- header.tag = "{%s}Header" % NAMESPACE
+ header_tag = "{%s}Header" % NAMESPACE
+ header = Element(header_tag)
envelope.append(header)
for part in header_parts:
# This doesn't work if the headers are signed
part.become_child_element_of(header)
- body = ElementTree.Element("")
- body.tag = "{%s}Body" % NAMESPACE
+ body_tag = "{%s}Body" % NAMESPACE
+ body = Element(body_tag)
envelope.append(body)
if isinstance(thingy, str):
@@ -224,10 +227,10 @@ def make_soap_enveloped_saml_thingy(thingy, header_parts=None):
thingy = "\n".join(_part[1:])
thingy = thingy.replace(PREFIX, "")
logger.debug("thingy: %s", thingy)
- _child = ElementTree.Element("")
- _child.tag = "{%s}FuddleMuddle" % DUMMY_NAMESPACE
+ _child_tag = "{%s}FuddleMuddle" % DUMMY_NAMESPACE
+ _child = Element(_child_tag)
body.append(_child)
- _str = ElementTree.tostring(envelope, encoding="UTF-8")
+ _str = xml_to_string(envelope, encoding="UTF-8")
if isinstance(_str, bytes):
_str = _str.decode("utf-8")
logger.debug("SOAP precursor: %s", _str)
@@ -242,7 +245,7 @@ def make_soap_enveloped_saml_thingy(thingy, header_parts=None):
return _str.replace(cut2, thingy)
else:
thingy.become_child_element_of(body)
- return ElementTree.tostring(envelope, encoding="UTF-8")
+ return xml_to_string(envelope, encoding="UTF-8")
def http_soap_message(message):
@@ -267,7 +270,7 @@ def parse_soap_enveloped_saml(text, body_class, header_class=None):
:param text: The SOAP object as XML
:return: header parts and body as saml.samlbase instances
"""
- envelope = defusedxml.ElementTree.fromstring(text)
+ envelope = xml_to_string(text)
envelope_tag = "{%s}Envelope" % NAMESPACE
if envelope.tag != envelope_tag:
diff --git a/src/saml2/s_utils.py b/src/saml2/s_utils.py
index 04a19c9ec..87a5ab466 100644
--- a/src/saml2/s_utils.py
+++ b/src/saml2/s_utils.py
@@ -332,6 +332,8 @@ def do_attribute(val, typ, key):
attrval = do_ava(val, typ)
if attrval:
attr.attribute_value = attrval
+ for attrval_v in attrval:
+ attr.c_ns_prefix.update(attrval_v.c_ns_prefix)
if isinstance(key, str):
attr.name = key
diff --git a/src/saml2/saml.py b/src/saml2/saml.py
index 1c01dc16c..dd934f11b 100644
--- a/src/saml2/saml.py
+++ b/src/saml2/saml.py
@@ -154,6 +154,7 @@ def __init__(self, text=None, extension_elements=None, extension_attributes=None
SamlBase.__init__(
self, text=None, extension_elements=extension_elements, extension_attributes=extension_attributes
)
+ self.c_ns_prefix["xs"] = XS_NAMESPACE
if self._extatt:
self.extension_attributes = self._extatt
@@ -192,12 +193,16 @@ def set_type(self, typ):
self._extatt[XSI_TYPE] = typ
if typ.startswith("xs:"):
+ self.c_ns_prefix["xs"] = XS_NAMESPACE
+ if not self.extension_attributes:
+ self.extension_attributes = {}
try:
self.extension_attributes["xmlns:xs"] = XS_NAMESPACE
except AttributeError:
self._extatt["xmlns:xs"] = XS_NAMESPACE
if typ.startswith("xsd:"):
+ self.c_ns_prefix["xsd"] = XS_NAMESPACE
try:
self.extension_attributes["xmlns:xsd"] = XS_NAMESPACE
except AttributeError:
@@ -351,6 +356,9 @@ def harvest_element_tree(self, tree):
self._convert_element_tree_to_member(child)
for attribute, value in iter(tree.attrib.items()):
self._convert_element_attribute_to_member(attribute, value)
+ if hasattr(tree, 'nsmap') and tree.nsmap:
+ self.c_ns_prefix = {**tree.nsmap, **self.c_ns_prefix}
+ #import ipdb; ipdb.set_trace() # noqa XXX
# if we have added children to this node
# we consider whitespace insignificant
@@ -1119,7 +1127,8 @@ def __init__(
# when consuming such elements, default to NAME_FORMAT_UNSPECIFIED as NameFormat
def harvest_element_tree(self, tree):
- tree.attrib.setdefault("NameFormat", NAME_FORMAT_UNSPECIFIED)
+ name_format = tree.attrib.get("NameFormat", NAME_FORMAT_UNSPECIFIED)
+ tree.attrib["NameFormat"] = name_format
SamlBase.harvest_element_tree(self, tree)
diff --git a/tests/test_00_xmldsig.py b/tests/test_00_xmldsig.py
index 059e8a049..cfc1a8481 100644
--- a/tests/test_00_xmldsig.py
+++ b/tests/test_00_xmldsig.py
@@ -12,6 +12,26 @@
import saml2.xmldsig as ds
+def test_eidas_ns():
+ from saml2.xml.schema import validate as validate_doc_with_schema
+ xmlstr = """
+ CZ/CZ/f93fab3a-b132-4c21-ba05-f00a9988441e
+ """
+ assert validate_doc_with_schema(xmlstr) is None
+
+ import saml2
+ from saml2.saml import AttributeValue
+ item = saml2.create_class_from_xml_string(AttributeValue, xmlstr)
+ assert item is not None
+
+ assert validate_doc_with_schema(str(item)) is None
+
+
class TestObject:
def setup_class(self):
self.object = ds.Object()
diff --git a/tests/test_03_saml2.py b/tests/test_03_saml2.py
index b96afd9c1..54060614b 100644
--- a/tests/test_03_saml2.py
+++ b/tests/test_03_saml2.py
@@ -163,8 +163,7 @@ def test_create_class_from_xml_string_xxe():
]>
&lol1;
"""
- with raises(EntitiesForbidden):
- create_class_from_xml_string(NameID, xml)
+ assert create_class_from_xml_string(NameID, xml) is None
def test_ee_1():
@@ -458,8 +457,9 @@ def test_ee_xxe():
]>
&lol1;
"""
- with raises(EntitiesForbidden):
- saml2.extension_element_from_string(xml)
+ x = saml2.extension_element_from_string(xml)
+ assert x.tag == "lolz"
+ assert x.text is None
def test_extension_element_loadd():
diff --git a/tests/test_12_s_utils.py b/tests/test_12_s_utils.py
index 454cd0bb1..62098be4d 100644
--- a/tests/test_12_s_utils.py
+++ b/tests/test_12_s_utils.py
@@ -16,30 +16,10 @@
XML_HEADER = "\n"
-SUCCESS_STATUS_NO_HEADER = (
- ''
-)
+SUCCESS_STATUS_NO_HEADER = ''
SUCCESS_STATUS = f"{XML_HEADER}{SUCCESS_STATUS_NO_HEADER}"
-
-ERROR_STATUS_NO_HEADER = (
- 'Error resolving "
- "principal"
-)
-
-ERROR_STATUS_NO_HEADER_EMPTY = (
- '"
-)
-
+ERROR_STATUS_NO_HEADER = 'Error resolving principal'
+ERROR_STATUS_NO_HEADER_EMPTY = ''
ERROR_STATUS = f"{XML_HEADER}{ERROR_STATUS_NO_HEADER}"
ERROR_STATUS_EMPTY = f"{XML_HEADER}{ERROR_STATUS_NO_HEADER_EMPTY}"
@@ -172,10 +152,10 @@ def test_attribute_statement():
assert statement.keyswv() == ["attribute"]
assert len(statement.attribute) == 2
attr0 = statement.attribute[0]
- assert _eq(attr0.keyswv(), ["name", "attribute_value", "name_format"])
+ assert _eq(attr0.keyswv(), ["c_ns_prefix", "name", "attribute_value", "name_format"])
assert len(attr0.attribute_value) == 1
attr1 = statement.attribute[1]
- assert _eq(attr1.keyswv(), ["name", "attribute_value", "name_format"])
+ assert _eq(attr1.keyswv(), ["c_ns_prefix", "name", "attribute_value", "name_format"])
assert len(attr1.attribute_value) == 1
if attr0.name == "givenName":
assert attr0.attribute_value[0].text == "Derek"
@@ -264,7 +244,7 @@ def test_do_attribute_statement_0():
assert statement.keyswv() == ["attribute"]
assert len(statement.attribute) == 1
attr0 = statement.attribute[0]
- assert _eq(attr0.keyswv(), ["name", "attribute_value", "name_format"])
+ assert _eq(attr0.keyswv(), ["c_ns_prefix", "name", "attribute_value", "name_format"])
assert attr0.name == "vo_attr"
assert len(attr0.attribute_value) == 1
assert attr0.attribute_value[0].text == "foobar"
@@ -276,9 +256,9 @@ def test_do_attribute_statement():
assert statement.keyswv() == ["attribute"]
assert len(statement.attribute) == 2
attr0 = statement.attribute[0]
- assert _eq(attr0.keyswv(), ["name", "attribute_value", "name_format"])
+ assert _eq(attr0.keyswv(), ["c_ns_prefix", "name", "attribute_value", "name_format"])
attr1 = statement.attribute[1]
- assert _eq(attr1.keyswv(), ["name", "attribute_value", "name_format"])
+ assert _eq(attr1.keyswv(), ["c_ns_prefix", "name", "attribute_value", "name_format"])
if attr0.name == "givenName":
assert len(attr0.attribute_value) == 2
assert _eq([av.text for av in attr0.attribute_value], ["Derek", "Sanderson"])
@@ -307,7 +287,7 @@ def test_do_attribute_statement_multi():
assert statement.keyswv() == ["attribute"]
assert len(statement.attribute)
- assert _eq(statement.attribute[0].keyswv(), ["name", "name_format", "friendly_name", "attribute_value"])
+ assert _eq(statement.attribute[0].keyswv(), ["c_ns_prefix", "name", "name_format", "friendly_name", "attribute_value"])
attribute = statement.attribute[0]
assert attribute.name == "urn:oid:1.3.6.1.4.1.5923.1.1.1.7"
assert attribute.name_format == ("urn:oasis:names:tc:SAML:2.0:attrname-format:uri")
diff --git a/tests/test_40_sigver.py b/tests/test_40_sigver.py
index 0049e1711..4cb593a3d 100644
--- a/tests/test_40_sigver.py
+++ b/tests/test_40_sigver.py
@@ -161,8 +161,8 @@ def setup_class(self):
signature=sigver.pre_signature_part("id-11111", self.sec.my_cert, 1),
attribute_statement=do_attribute_statement(
{
- ("name:surName", "nameformat", "surName"): ("Foo", ""),
- ("name:givenName", "nameformat", "givenName"): ("Bar", ""),
+ ("name:surName", "nameformat", "surName"): ("Foo", "xs:string"),
+ ("name:givenName", "nameformat", "givenName"): ("Bar", "xs:string"),
}
),
)
diff --git a/tests/test_42_enc.py b/tests/test_42_enc.py
index 8f15fe249..c051c781f 100644
--- a/tests/test_42_enc.py
+++ b/tests/test_42_enc.py
@@ -14,7 +14,7 @@
__author__ = "roland"
-TMPL_NO_HEADER = """{key_info}"""
+TMPL_NO_HEADER = """{key_info}"""
TMPL = f"\n{TMPL_NO_HEADER}"
IDENTITY = {
diff --git a/tests/test_43_soap.py b/tests/test_43_soap.py
index 91a45fda6..623e4c292 100644
--- a/tests/test_43_soap.py
+++ b/tests/test_43_soap.py
@@ -1,6 +1,8 @@
#!/usr/bin/env python
-from xml.etree import ElementTree as ElementTree
+from saml2 import Element
+from saml2 import ElementTree
+from saml2 import xml_from_string
from defusedxml.common import EntitiesForbidden
from pytest import raises
@@ -14,8 +16,8 @@
example = """
-
https://www.example.com/SAML
@@ -32,7 +34,7 @@
def test_parse_soap_envelope():
- envelope = ElementTree.fromstring(example)
+ envelope = xml_from_string(example)
assert envelope.tag == "{%s}Envelope" % NAMESPACE
# How to check that it's the right type ?
assert len(envelope) == 1
@@ -45,10 +47,10 @@ def test_parse_soap_envelope():
def test_make_soap_envelope():
- envelope = ElementTree.Element("")
- envelope.tag = "{%s}Envelope" % NAMESPACE
- body = ElementTree.Element("")
- body.tag = "{%s}Body" % NAMESPACE
+ envelope_tag = "{%s}Envelope" % NAMESPACE
+ envelope = Element(envelope_tag)
+ body_tag = "{%s}Body" % NAMESPACE
+ body = Element(body_tag)
envelope.append(body)
request = samlp.AuthnRequest()
request.become_child_element_of(body)