pyFF 3.0.0__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyff-3.0.0 → pyff-3.1.0}/PKG-INFO +5 -5
- {pyff-3.0.0 → pyff-3.1.0}/README.rst +2 -2
- {pyff-3.0.0 → pyff-3.1.0}/pyproject.toml +3 -3
- {pyff-3.0.0 → pyff-3.1.0}/pyproject.toml.orig +3 -3
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/builtins.py +5 -4
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/resource.py +9 -3
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/samlmd.py +7 -7
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/edugain-trustinfo-2.0.xml +1 -1
- pyff-3.1.0/src/pyff/test/data/metadata/test07-xsi-type-middleware.xml +86 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_pipeline.py +117 -0
- pyff-3.1.0/src/pyff/test/test_xslt_namespaces.py +45 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/utils.py +74 -23
- {pyff-3.0.0 → pyff-3.1.0}/LICENSE +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/__init__.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/api.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/constants.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/decorators.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/exceptions.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/fetch.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/locks.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/logs.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/md.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/mdq.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/merge_strategies.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/parse.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/pipes.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/repo.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/MetadataExchange.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/XMLSchema.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/atom.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/cs-sstc-schema-assertion-1.1.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/oasis-200401-wss-wssecurity-secext-1.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/oasis-200401-wss-wssecurity-utility-1.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/privacy.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/saml-metadata-rpi-v1.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/saml-metadata-trustinfo-v1.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/saml-schema-assertion-2.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/saml-schema-metadata-2.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/saml-schema-protocol-2.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/schema.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/shibboleth-metadata-1.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/shibboleth-trust-1.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/shibboleth.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/sstc-metadata-attr.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/sstc-saml-idp-discovery.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/sstc-saml-metadata-algsupport.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/sstc-saml-metadata-ui-v1.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/sstc-saml-schema-assertion-2.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/sstc-saml-schema-metadata-2.0.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/ws-addr.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/ws-authorization.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/ws-federation.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/ws-securitypolicy-1.2.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/xenc-schema.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/xml.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/xmldsig-core-schema.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/schema/xrd-1.0-os.xsd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/store.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/__init__.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/bad_metadata/stockholmstad-c1107b3ceb78c5e9.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/eidas/countries/FR.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/eidas/countries/GR.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/eidas/countries/fr-connector.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/eidas/countries/fr-proxy.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/eidas/countries/gr-connector.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/eidas/countries/gr-proxy.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/eidas/eidas.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/empty/placeholder +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/empty.txt +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/images/BFC-Logo-80x60.gif +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/images/FHP_logo.svg +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/images/New_GEANT_logo.jpg +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/images/favicon.ico +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/images/sunet256.png +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/mdx/mdx.fd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/not-metadata.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/sharav.abes.fr.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/swamid-2.0-test.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test-sp-trustinfo-in-attr-broken-base64.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test-sp-trustinfo-in-attr.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test-sp.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test01.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test02-invalid.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test02-sp.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test03-invalid.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test04-trailing-slash-sp.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test05-xsd-prefix-idp.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test06-xs-and-xsd-declared.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/wayf-edugain-metadata.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/simple-pipeline/bad.fd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/simple-pipeline/certreport-swamid.fd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/simple-pipeline/certreport.fd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/simple-pipeline/idp.aco.net.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/simple-pipeline/signer.fd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/simple-pipeline/skriptenforum.net.xml +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/simple-pipeline/validator.fd +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_decorators.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_log.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_md_api.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_mdsl.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_repo.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_rwlock.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_simple_pipeline.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_store.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_time.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/test_utils.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/tools.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/wsgi.py +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/atom.xsl +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/atom2html.xsl +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/discojson.xsl +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/eidas-cleanup.xsl +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/entity2html.xsl +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/kalmar2.xsl +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/pp.xsl +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/pubinfo.xsl +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/regauth.xsl +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/tidy.xsl +0 -0
- {pyff-3.0.0 → pyff-3.1.0}/src/pyff/xslt/unsign.xsl +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyFF
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: Federation Feeder
|
|
5
5
|
Author: Leif Johansson, Fredrik Thulin, Enrique Pérez Arnaud, Mikael Frykholm
|
|
6
6
|
Author-email: Leif Johansson <leifj@sunet.se>, Fredrik Thulin <redrik@thulin.net>, Mikael Frykholm <mifr@sunet.se>
|
|
@@ -40,8 +40,8 @@ Requires-Dist: pyconfig>=3.3.0
|
|
|
40
40
|
Requires-Dist: pydantic>=2.8
|
|
41
41
|
Requires-Dist: pyramid>=2.1
|
|
42
42
|
Requires-Dist: pytz>=2025.2
|
|
43
|
-
Requires-Dist: pyuppsala>=0.13.
|
|
44
|
-
Requires-Dist: pybergshamra>=0.11.
|
|
43
|
+
Requires-Dist: pyuppsala>=0.13.1
|
|
44
|
+
Requires-Dist: pybergshamra>=0.11.1
|
|
45
45
|
Requires-Dist: pyyaml>=3.10
|
|
46
46
|
Requires-Dist: redis>=5.3.0
|
|
47
47
|
Requires-Dist: redis-collections>=0.13.0
|
|
@@ -101,8 +101,8 @@ Features
|
|
|
101
101
|
Dependencies
|
|
102
102
|
============
|
|
103
103
|
|
|
104
|
-
* pybergshamra >= 0.11.
|
|
105
|
-
* pyuppsala >= 0.
|
|
104
|
+
* pybergshamra >= 0.11.1 (XML signatures and PKCS#11 support)
|
|
105
|
+
* pyuppsala >= 0.13.1 (XML processing)
|
|
106
106
|
* pygments
|
|
107
107
|
* gunicorn (for the standalone pyffd server)
|
|
108
108
|
* ... cf pyproject.toml
|
|
@@ -41,8 +41,8 @@ Features
|
|
|
41
41
|
Dependencies
|
|
42
42
|
============
|
|
43
43
|
|
|
44
|
-
* pybergshamra >= 0.11.
|
|
45
|
-
* pyuppsala >= 0.
|
|
44
|
+
* pybergshamra >= 0.11.1 (XML signatures and PKCS#11 support)
|
|
45
|
+
* pyuppsala >= 0.13.1 (XML processing)
|
|
46
46
|
* pygments
|
|
47
47
|
* gunicorn (for the standalone pyffd server)
|
|
48
48
|
* ... cf pyproject.toml
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyFF"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.1.0"
|
|
4
4
|
readme = "README.rst"
|
|
5
5
|
description = "Federation Feeder"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -15,8 +15,8 @@ dependencies = [
|
|
|
15
15
|
"pydantic>=2.8",
|
|
16
16
|
"pyramid>=2.1",
|
|
17
17
|
"pytz>=2025.2",
|
|
18
|
-
"pyuppsala>=0.13.
|
|
19
|
-
"pybergshamra>=0.11.
|
|
18
|
+
"pyuppsala>=0.13.1",
|
|
19
|
+
"pybergshamra>=0.11.1",
|
|
20
20
|
"pyyaml>=3.10",
|
|
21
21
|
"redis>=5.3.0",
|
|
22
22
|
"redis-collections>=0.13.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyFF"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.1.0"
|
|
4
4
|
readme = "README.rst"
|
|
5
5
|
description = "Federation Feeder"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -26,8 +26,8 @@ dependencies = [
|
|
|
26
26
|
"pydantic>=2.8",
|
|
27
27
|
"pyramid>=2.1",
|
|
28
28
|
"pytz>=2025.2",
|
|
29
|
-
"pyuppsala>=0.13.
|
|
30
|
-
"pybergshamra>=0.11.
|
|
29
|
+
"pyuppsala>=0.13.1",
|
|
30
|
+
"pybergshamra>=0.11.1",
|
|
31
31
|
"pyyaml>=3.10",
|
|
32
32
|
"redis>=5.3.0",
|
|
33
33
|
"redis-collections>=0.13.0",
|
|
@@ -693,7 +693,8 @@ def load(req: Plumbing.Request, *opts):
|
|
|
693
693
|
- ``as``: name the resource, e.g. to refer to it in later pipeline steps.
|
|
694
694
|
- ``verify``: check the XML signature of the resource, using either a PEM certificate file or a SHA-1
|
|
695
695
|
fingerprint of the signing certificate. With a fingerprint, the certificate embedded in the signature is
|
|
696
|
-
used and the fingerprint pins it.
|
|
696
|
+
used and the fingerprint pins it. ``verify`` may be repeated to support key rollovers; a signature by any
|
|
697
|
+
one of the given certificates (or fingerprints) is accepted. Certificates and fingerprints cannot be mixed.
|
|
697
698
|
- ``via``: name of a ``when`` clause in the same pipeline that is applied to the loaded metadata, for
|
|
698
699
|
example to remove entities with ``fork merge remove``.
|
|
699
700
|
- ``cleanup``: name of a ``when`` clause used to pre-process the parsed metadata before it is validated.
|
|
@@ -718,7 +719,7 @@ def load(req: Plumbing.Request, *opts):
|
|
|
718
719
|
log.debug(f"load parsing '{stripped}'")
|
|
719
720
|
r = stripped.split()
|
|
720
721
|
|
|
721
|
-
assert len(r) in range(1,
|
|
722
|
+
assert len(r) in range(1, 12), PipeException(
|
|
722
723
|
"Usage: load resource [as url] [[verify] verification] [via pipeline] [cleanup pipeline]"
|
|
723
724
|
)
|
|
724
725
|
|
|
@@ -736,7 +737,7 @@ def load(req: Plumbing.Request, *opts):
|
|
|
736
737
|
if elt == "as":
|
|
737
738
|
child_opts.alias = value
|
|
738
739
|
elif elt == "verify":
|
|
739
|
-
child_opts.verify = value
|
|
740
|
+
child_opts.verify = [*(child_opts.verify or []), value]
|
|
740
741
|
elif elt == "via":
|
|
741
742
|
child_opts.via.append(PipelineCallback(value, req, store=req.md.store))
|
|
742
743
|
elif elt == "cleanup":
|
|
@@ -748,7 +749,7 @@ def load(req: Plumbing.Request, *opts):
|
|
|
748
749
|
"Usage: load resource [as url] [[verify] verification] [via pipeline]* [cleanup pipeline]*"
|
|
749
750
|
)
|
|
750
751
|
else:
|
|
751
|
-
child_opts.verify = elt
|
|
752
|
+
child_opts.verify = [*(child_opts.verify or []), elt]
|
|
752
753
|
|
|
753
754
|
# override anything in child_opts with what is in opts
|
|
754
755
|
child_opts = child_opts.model_copy(update=_opts)
|
|
@@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Any, Callable
|
|
|
18
18
|
from urllib.parse import quote as urlescape
|
|
19
19
|
|
|
20
20
|
import requests
|
|
21
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
21
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
22
22
|
from pyuppsala.etree import ElementTree
|
|
23
23
|
from requests.adapters import Response
|
|
24
24
|
|
|
@@ -204,8 +204,9 @@ class _NativeResponse:
|
|
|
204
204
|
|
|
205
205
|
class ResourceOpts(BaseModel):
|
|
206
206
|
alias: str | None = Field(None, alias='as') # TODO: Rename to 'name'?
|
|
207
|
-
#
|
|
208
|
-
|
|
207
|
+
# certificates (file or PEM text) or SHA1 fingerprints to use for signature verification; a signature by any
|
|
208
|
+
# one of them is accepted, which allows key rollovers
|
|
209
|
+
verify: list[str] | None = None
|
|
209
210
|
# TODO: move classes to make the correct typing work: Iterable[Union[Lambda, PipelineCallback]] = Field([])
|
|
210
211
|
via: list[Callable] = Field([])
|
|
211
212
|
# A list of callbacks that can be used to pre-process parsed metadata before validation. Use as a clue-bat.
|
|
@@ -219,6 +220,11 @@ class ResourceOpts(BaseModel):
|
|
|
219
220
|
verify_tls: bool = False
|
|
220
221
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
221
222
|
|
|
223
|
+
@field_validator('verify', mode='before')
|
|
224
|
+
@classmethod
|
|
225
|
+
def _wrap_single_verify(cls, value):
|
|
226
|
+
return [value] if isinstance(value, str) else value
|
|
227
|
+
|
|
222
228
|
def to_dict(self) -> dict[str, Any]:
|
|
223
229
|
res = self.model_dump()
|
|
224
230
|
# Compensate for the 'alias' field options
|
|
@@ -20,6 +20,7 @@ from pyff.parse import ParserInfo, PyffParser, add_parser
|
|
|
20
20
|
from pyff.utils import (
|
|
21
21
|
Lambda,
|
|
22
22
|
b2u,
|
|
23
|
+
b64_to_pem,
|
|
23
24
|
cert_dict,
|
|
24
25
|
check_signature,
|
|
25
26
|
collect_attr,
|
|
@@ -112,7 +113,7 @@ def parse_saml_metadata(
|
|
|
112
113
|
|
|
113
114
|
expire_time_offset = metadata_expiration(t)
|
|
114
115
|
|
|
115
|
-
t = check_signature(t, opts.verify)
|
|
116
|
+
t = check_signature(t, opts.verify, source=base_url)
|
|
116
117
|
|
|
117
118
|
trust_info = None
|
|
118
119
|
extensions = t.find('{{{}}}Extensions'.format(NS['md']))
|
|
@@ -305,23 +306,22 @@ class MDServiceListParser(PyffParser):
|
|
|
305
306
|
if location:
|
|
306
307
|
certs = cert_dict(ml)
|
|
307
308
|
fingerprints = list(certs.keys())
|
|
308
|
-
fp = None
|
|
309
|
-
if len(fingerprints) > 0:
|
|
310
|
-
fp = fingerprints[0]
|
|
311
309
|
|
|
312
310
|
ep = ml.find("{{{}}}Endpoint".format(NS['ser']))
|
|
313
|
-
if ep is not None and
|
|
311
|
+
if ep is not None and fingerprints:
|
|
314
312
|
args = dict(
|
|
315
313
|
country_code=mdl.get('Territory'),
|
|
316
314
|
hide_from_discovery=str2bool(ep.get('HideFromDiscovery', 'false')),
|
|
317
315
|
)
|
|
318
316
|
log.debug(
|
|
319
317
|
"MDSL[{}]: {} verified by {} for country {}".format(
|
|
320
|
-
info.scheme_territory, location,
|
|
318
|
+
info.scheme_territory, location, ', '.join(fingerprints), args.get('country_code')
|
|
321
319
|
)
|
|
322
320
|
)
|
|
323
321
|
child_opts = resource.opts.model_copy(update={'alias': None}, deep=True)
|
|
324
|
-
|
|
322
|
+
# The listed certificates are the trusted signers; all of them are accepted so that a
|
|
323
|
+
# key rollover can list both the old and the new certificate.
|
|
324
|
+
child_opts.verify = [b64_to_pem(cert_b64) for cert_b64 in certs.values()]
|
|
325
325
|
r = resource.add_child(location, child_opts)
|
|
326
326
|
|
|
327
327
|
# this is specific post-processing for MDSL files
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
|
-
name: pyff-3.
|
|
2
|
+
name: pyff-3.1.0/src/pyff/test/data/metadata/edugain-trustinfo-2.0.xml
|
|
3
3
|
size: 83107927 bytes
|
|
4
4
|
sha256: 9646f2c1428ee2522e2c8f493daa3b80d11825e23d827a2d6e16dabdc58ca466
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:eidas="http://eidas.europa.eu/saml-extensions" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="_eumiddleware" entityID="https://test.demw.eidas.swedenconnect.se/eidas-middleware/Metadata" validUntil="2026-12-09T09:46:59.594Z">
|
|
3
|
+
<ds:Signature>
|
|
4
|
+
<ds:SignedInfo>
|
|
5
|
+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
|
6
|
+
<ds:SignatureMethod Algorithm="http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1"/>
|
|
7
|
+
<ds:Reference URI="#_eumiddleware">
|
|
8
|
+
<ds:Transforms>
|
|
9
|
+
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
|
|
10
|
+
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
|
|
11
|
+
<ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="xsd"/>
|
|
12
|
+
</ds:Transform>
|
|
13
|
+
</ds:Transforms>
|
|
14
|
+
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
|
15
|
+
<ds:DigestValue>gVjWffnS2gYWlNyqxkCbQynU425D1pqZiHJvNQr1xtU=</ds:DigestValue>
|
|
16
|
+
</ds:Reference>
|
|
17
|
+
</ds:SignedInfo>
|
|
18
|
+
<ds:SignatureValue>MgE/0IKAPG2+TkzbVOj6NZpcMMMJVN+1pqdqF6nKPNCE0Lfy4vLdap9qxtzDpbQpl7Ece0zYQnHYn6J/x9FjRm6N/jBth+YNxwmNtyE5GyxsVmNIAhSvMkerKqgHBxKmkTrp17au7bQzxHexvGEnkA0OzmuHstO0cKiCZ7B6ykCCOnh8ydSXkUeUAFfSzpoBraIz0FqcSnoK5LfxzZfSiA3Q1JzaPkmdLf5tIzw1OxwR1ggyIXLsh3q8buWV4otEFRS6cPF9iRvwlIIEqLn8klIlzFTNC5mVcoVUNuShdEsU03xkTATfnJMKdbFa4P3ArIMDtEV3KEoi5SY5mnpXSPtfstYyVn0Y0nDnBhWpDCdy/2xEq/0B6JxMlqwuWlI8XP+ayn5c7pw7Y2m/3tBZIr8IQftQBYGBrpGQk1eYJmKH9SeT6iX5mXi9ElqGee5UPIb/ogAMdSBWpp2wQwcPC+Xe3vNlt8o1P4XAehElzBhj7loMvqbC+BUVMCLDHJ58Zvy8bE5d54mb4M+tlpZnYU+sgqqm3DHmCZJw4sQ0odXoYQFsQeQOlMXFxN+mMZvDNi0eOoirN8euwLWy/sSbhpP0zp6ELT/kurmqxx//QGfGWHANu8Ab+qhmBm374frL4ehd8dLx28JYCP+0It5br0pJrNi3uaLNKhGexslyCOk=</ds:SignatureValue>
|
|
19
|
+
<ds:KeyInfo>
|
|
20
|
+
<ds:X509Data>
|
|
21
|
+
<ds:X509Certificate>MIIE5TCCAs2gAwIBAgIBfTANBgkqhkiG9w0BAQsFADAsMQ8wDQYDVQQKEwZTQ1RFU1QxGTAXBgNVBAMTEFNBTUwgU2lnbmVyIFRlc3QwHhcNMjMxMjEzMDAwMDAwWhcNMjcxMjEyMDAwMDAwWjAsMQ8wDQYDVQQKEwZTQ1RFU1QxGTAXBgNVBAMTEFNBTUwgU2lnbmVyIFRlc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpRC2dXoyg26aWMvWshKF6LEGBgti6frNnBDehY/xPtSsaJgfA3Ba413GgGbFS6sxYUlzLR1XljnNvOaPIMfENYVN/JxMCtLZs2/UiqD06F7PwGQzargHgTWxfApEyJFxl08IkDYNILxd7r7kFOiDEAYhHOFqtaCAFz1w8xAAGq5M499TfyPTkbXp/hgBLu2OcNqwjztV3W8WIp8XwwLS7iBW5yZOzTHlWy/SHpl9N1tZscDVPIAumLrDAGHOxrKgVKdRmUqscpe+XoqpubE0841C/Bt/3YQbXxYLTn7R5nqHi59y5MzmlokIQYJ4cS90OQeJZ10yXXuQSyWDBUEKqi3KMzPpQCs0Bnn8yHEtDjFdkpkTVf/iHupsvb4cPwt/XDt0z0pzRXIA+/jFfI/UrvjHskaoBJvBc/Nhi9sisHKtoIuLWfbVQGkkjZgTaIkUCB2X192cuPTNDnwPoHjHYmI364rQ99oWuxAt/U6hs+ipnOP6U2CBW+4+ynqu9GZYji5SR9RgKdG/j5e9uVhe0eKT2rAN88TfIfBz2fRzNU4HV2jExIb6L13SAyO/9WyNC5uv7cJA6VYHG8ygZjoY79HfnZb3wPW3W7a32hr4YD36vKXi+6exFVel1uJNWu3rdGRdTL6OiVF2HW1Gl1iqHwdvh7OR8Upv6TnnlnxlodwIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADggIBAEXwkfny+YEBO9ALqTrTUK/1baIKi0CLQ/CZ0We/5BFjIp6KQphTGy5lzqtBG3Y68FO6JjOHbcDPZJfyjfniYjuRWh4bHgaBdu7NSdHZW9t6PeT14by6r9/pSjx9llDKchG2gSObuMuH5uI4t3c/hNhW17gv8m5/MzxxBJrR5lULPvMC3+v0Uy60MnycxcPDAi4HQfNkdHf+t3MLwH/HF60OAZ+pXHkahEJn3/F4nKQR8j8rdrn2ZT5mbYwjG27MZ7bhmFLfBtV6jXfQZqDfcxvOP8waEyxyMNah/a5LVQz+PJT+RHF4wqsigiMa248z8YCiz3oj6irpW4Ln/7YnJ8UTatRIOP7xK8hR2gUGtYxHxuGHASqn7tOGRlIdIZFROd28y2HSqgfApg5KUh2eupWMAnXlkcpxiLPz4rx+FX3kditH1z24HzcH2g3ytvL90j/7Gh1cp7BD3e3lf76wzLHUlIH+O573V8XPe1fAyutxsaHIY5S2VHv8fmFODqAS7uPyuZ1pc8gVAJEERGzbLL5WwZETXONrYpRoVIX8ojAU4sOCN8RRnrF5k1obgYj0B72ziXXZ8D/or9AP1BE7npmURLZ2qKM+FeTInZxKcYyLrvpkqVgyX6YM6cV+/XlG3LwS5D8gpKvD5+DIcQMwKXVEXNg3xncodVl2tdPeM8/D</ds:X509Certificate>
|
|
22
|
+
</ds:X509Data>
|
|
23
|
+
</ds:KeyInfo>
|
|
24
|
+
</ds:Signature>
|
|
25
|
+
<md:Extensions>
|
|
26
|
+
<mdattr:EntityAttributes xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute">
|
|
27
|
+
<saml2:Attribute Name="urn:oasis:names:tc:SAML:attribute:assurance-certification" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
|
28
|
+
<saml2:AttributeValue>http://eidas.europa.eu/LoA/high</saml2:AttributeValue>
|
|
29
|
+
</saml2:Attribute>
|
|
30
|
+
<saml2:Attribute Name="http://eidas.europa.eu/entity-attributes/protocol-version" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
|
31
|
+
<saml2:AttributeValue xsi:type="xsd:string">1.4.1</saml2:AttributeValue>
|
|
32
|
+
</saml2:Attribute>
|
|
33
|
+
<saml2:Attribute Name="http://eidas.europa.eu/entity-attributes/application-identifier" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
|
34
|
+
<saml2:AttributeValue xsi:type="xsd:string">German eIDAS Middleware version: 4.1.0</saml2:AttributeValue>
|
|
35
|
+
</saml2:Attribute>
|
|
36
|
+
<saml2:Attribute Name="http://macedir.org/entity-category" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
|
|
37
|
+
<saml2:AttributeValue>http://eidas.europa.eu/entity-attributes/termsofaccess/requesterid</saml2:AttributeValue>
|
|
38
|
+
</saml2:Attribute>
|
|
39
|
+
</mdattr:EntityAttributes>
|
|
40
|
+
<alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
|
41
|
+
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" MinKeySize="256"/>
|
|
42
|
+
<alg:SigningMethod Algorithm="http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1" MaxKeySize="4096" MinKeySize="3072"/>
|
|
43
|
+
</md:Extensions>
|
|
44
|
+
<md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
45
|
+
<md:Extensions>
|
|
46
|
+
<eidas:NodeCountry>DE</eidas:NodeCountry>
|
|
47
|
+
</md:Extensions>
|
|
48
|
+
<md:KeyDescriptor use="signing">
|
|
49
|
+
<ds:KeyInfo>
|
|
50
|
+
<ds:X509Data>
|
|
51
|
+
<ds:X509Certificate>MIIE5TCCAs2gAwIBAgIBfTANBgkqhkiG9w0BAQsFADAsMQ8wDQYDVQQKEwZTQ1RFU1QxGTAXBgNVBAMTEFNBTUwgU2lnbmVyIFRlc3QwHhcNMjMxMjEzMDAwMDAwWhcNMjcxMjEyMDAwMDAwWjAsMQ8wDQYDVQQKEwZTQ1RFU1QxGTAXBgNVBAMTEFNBTUwgU2lnbmVyIFRlc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpRC2dXoyg26aWMvWshKF6LEGBgti6frNnBDehY/xPtSsaJgfA3Ba413GgGbFS6sxYUlzLR1XljnNvOaPIMfENYVN/JxMCtLZs2/UiqD06F7PwGQzargHgTWxfApEyJFxl08IkDYNILxd7r7kFOiDEAYhHOFqtaCAFz1w8xAAGq5M499TfyPTkbXp/hgBLu2OcNqwjztV3W8WIp8XwwLS7iBW5yZOzTHlWy/SHpl9N1tZscDVPIAumLrDAGHOxrKgVKdRmUqscpe+XoqpubE0841C/Bt/3YQbXxYLTn7R5nqHi59y5MzmlokIQYJ4cS90OQeJZ10yXXuQSyWDBUEKqi3KMzPpQCs0Bnn8yHEtDjFdkpkTVf/iHupsvb4cPwt/XDt0z0pzRXIA+/jFfI/UrvjHskaoBJvBc/Nhi9sisHKtoIuLWfbVQGkkjZgTaIkUCB2X192cuPTNDnwPoHjHYmI364rQ99oWuxAt/U6hs+ipnOP6U2CBW+4+ynqu9GZYji5SR9RgKdG/j5e9uVhe0eKT2rAN88TfIfBz2fRzNU4HV2jExIb6L13SAyO/9WyNC5uv7cJA6VYHG8ygZjoY79HfnZb3wPW3W7a32hr4YD36vKXi+6exFVel1uJNWu3rdGRdTL6OiVF2HW1Gl1iqHwdvh7OR8Upv6TnnlnxlodwIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADggIBAEXwkfny+YEBO9ALqTrTUK/1baIKi0CLQ/CZ0We/5BFjIp6KQphTGy5lzqtBG3Y68FO6JjOHbcDPZJfyjfniYjuRWh4bHgaBdu7NSdHZW9t6PeT14by6r9/pSjx9llDKchG2gSObuMuH5uI4t3c/hNhW17gv8m5/MzxxBJrR5lULPvMC3+v0Uy60MnycxcPDAi4HQfNkdHf+t3MLwH/HF60OAZ+pXHkahEJn3/F4nKQR8j8rdrn2ZT5mbYwjG27MZ7bhmFLfBtV6jXfQZqDfcxvOP8waEyxyMNah/a5LVQz+PJT+RHF4wqsigiMa248z8YCiz3oj6irpW4Ln/7YnJ8UTatRIOP7xK8hR2gUGtYxHxuGHASqn7tOGRlIdIZFROd28y2HSqgfApg5KUh2eupWMAnXlkcpxiLPz4rx+FX3kditH1z24HzcH2g3ytvL90j/7Gh1cp7BD3e3lf76wzLHUlIH+O573V8XPe1fAyutxsaHIY5S2VHv8fmFODqAS7uPyuZ1pc8gVAJEERGzbLL5WwZETXONrYpRoVIX8ojAU4sOCN8RRnrF5k1obgYj0B72ziXXZ8D/or9AP1BE7npmURLZ2qKM+FeTInZxKcYyLrvpkqVgyX6YM6cV+/XlG3LwS5D8gpKvD5+DIcQMwKXVEXNg3xncodVl2tdPeM8/D</ds:X509Certificate>
|
|
52
|
+
</ds:X509Data>
|
|
53
|
+
</ds:KeyInfo>
|
|
54
|
+
</md:KeyDescriptor>
|
|
55
|
+
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
|
|
56
|
+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://test.demw.eidas.swedenconnect.se/eidas-middleware/RequestReceiver"/>
|
|
57
|
+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://test.demw.eidas.swedenconnect.se/eidas-middleware/RequestReceiver"/>
|
|
58
|
+
<saml2:Attribute FriendlyName="FamilyName" Name="http://eidas.europa.eu/attributes/naturalperson/CurrentFamilyName"/>
|
|
59
|
+
<saml2:Attribute FriendlyName="FirstName" Name="http://eidas.europa.eu/attributes/naturalperson/CurrentGivenName"/>
|
|
60
|
+
<saml2:Attribute FriendlyName="CurrentAddress" Name="http://eidas.europa.eu/attributes/naturalperson/CurrentAddress"/>
|
|
61
|
+
<saml2:Attribute FriendlyName="PersonIdentifier" Name="http://eidas.europa.eu/attributes/naturalperson/PersonIdentifier"/>
|
|
62
|
+
<saml2:Attribute FriendlyName="BirthName" Name="http://eidas.europa.eu/attributes/naturalperson/BirthName"/>
|
|
63
|
+
<saml2:Attribute FriendlyName="PlaceOfBirth" Name="http://eidas.europa.eu/attributes/naturalperson/PlaceOfBirth"/>
|
|
64
|
+
<saml2:Attribute FriendlyName="DateOfBirth" Name="http://eidas.europa.eu/attributes/naturalperson/DateOfBirth"/>
|
|
65
|
+
<saml2:Attribute FriendlyName="Nationality" Name="http://eidas.europa.eu/attributes/naturalperson/Nationality"/>
|
|
66
|
+
</md:IDPSSODescriptor>
|
|
67
|
+
<md:Organization>
|
|
68
|
+
<md:OrganizationName xml:lang="sv">Sweden Connect</md:OrganizationName>
|
|
69
|
+
<md:OrganizationDisplayName xml:lang="sv">Sweden Connect</md:OrganizationDisplayName>
|
|
70
|
+
<md:OrganizationURL xml:lang="sv">https://swedenconnect.se</md:OrganizationURL>
|
|
71
|
+
</md:Organization>
|
|
72
|
+
<md:ContactPerson contactType="technical">
|
|
73
|
+
<md:Company>Sweden Connect</md:Company>
|
|
74
|
+
<md:GivenName>Sweden Connect</md:GivenName>
|
|
75
|
+
<md:SurName>Operations</md:SurName>
|
|
76
|
+
<md:EmailAddress>operations@swedenconnect.se</md:EmailAddress>
|
|
77
|
+
<md:TelephoneNumber>+46105742100</md:TelephoneNumber>
|
|
78
|
+
</md:ContactPerson>
|
|
79
|
+
<md:ContactPerson contactType="support">
|
|
80
|
+
<md:Company>Sweden Connect</md:Company>
|
|
81
|
+
<md:GivenName>Sweden Connect</md:GivenName>
|
|
82
|
+
<md:SurName>Operations</md:SurName>
|
|
83
|
+
<md:EmailAddress>operations@swedenconnect.se</md:EmailAddress>
|
|
84
|
+
<md:TelephoneNumber>+46105742100</md:TelephoneNumber>
|
|
85
|
+
</md:ContactPerson>
|
|
86
|
+
</md:EntityDescriptor>
|
|
@@ -770,6 +770,85 @@ class SigningTest(PipeLineTest):
|
|
|
770
770
|
with pytest.raises(OSError):
|
|
771
771
|
self.exec_pipeline("- read_xml:\n file: /nonexistent.xml\n")
|
|
772
772
|
|
|
773
|
+
def test_verify_with_multiple_certificates_and_fingerprints(self):
|
|
774
|
+
"""A key rollover lists several signers; any one of them must verify."""
|
|
775
|
+
import datetime
|
|
776
|
+
import hashlib
|
|
777
|
+
|
|
778
|
+
from cryptography import x509
|
|
779
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
780
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
781
|
+
from cryptography.x509.oid import NameOID
|
|
782
|
+
|
|
783
|
+
from pyff.constants import NS
|
|
784
|
+
from pyff.exceptions import MetadataException
|
|
785
|
+
from pyff.utils import check_signature
|
|
786
|
+
|
|
787
|
+
other_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
788
|
+
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, 'other')])
|
|
789
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
790
|
+
other_cert = (
|
|
791
|
+
x509.CertificateBuilder()
|
|
792
|
+
.subject_name(name)
|
|
793
|
+
.issuer_name(name)
|
|
794
|
+
.public_key(other_key.public_key())
|
|
795
|
+
.serial_number(x509.random_serial_number())
|
|
796
|
+
.not_valid_before(now)
|
|
797
|
+
.not_valid_after(now + datetime.timedelta(days=2))
|
|
798
|
+
.sign(other_key, hashes.SHA256())
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
with open(self.public_keyspec, 'rb') as fd:
|
|
802
|
+
signer_cert = x509.load_pem_x509_certificate(fd.read())
|
|
803
|
+
|
|
804
|
+
def fingerprint(cert):
|
|
805
|
+
return hashlib.sha1(cert.public_bytes(serialization.Encoding.DER)).hexdigest()
|
|
806
|
+
|
|
807
|
+
tmpdir = tempfile.mkdtemp()
|
|
808
|
+
try:
|
|
809
|
+
src = os.path.join(tmpdir, 'mdsl.xml')
|
|
810
|
+
other_pem = os.path.join(tmpdir, 'other.pem')
|
|
811
|
+
with open(src, 'w') as fd:
|
|
812
|
+
fd.write(
|
|
813
|
+
f'<ser:MetadataServiceList xmlns:ser="{NS["ser"]}" Version="1">'
|
|
814
|
+
'<ser:IssuerName>Test</ser:IssuerName></ser:MetadataServiceList>'
|
|
815
|
+
)
|
|
816
|
+
with open(other_pem, 'wb') as fd:
|
|
817
|
+
fd.write(other_cert.public_bytes(serialization.Encoding.PEM))
|
|
818
|
+
res, _md = self.exec_pipeline(
|
|
819
|
+
f"""
|
|
820
|
+
- read_xml:
|
|
821
|
+
file: {src}
|
|
822
|
+
- sign:
|
|
823
|
+
key: {self.private_keyspec}
|
|
824
|
+
cert: {self.public_keyspec}
|
|
825
|
+
"""
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
t = root(res)
|
|
829
|
+
other_pem_text = other_cert.public_bytes(serialization.Encoding.PEM).decode()
|
|
830
|
+
signer_pem_text = signer_cert.public_bytes(serialization.Encoding.PEM).decode()
|
|
831
|
+
for keys in (
|
|
832
|
+
[other_pem, self.public_keyspec],
|
|
833
|
+
[self.public_keyspec, other_pem],
|
|
834
|
+
[other_pem_text, signer_pem_text],
|
|
835
|
+
[signer_pem_text, other_pem_text],
|
|
836
|
+
[fingerprint(other_cert), fingerprint(signer_cert)],
|
|
837
|
+
[fingerprint(signer_cert), fingerprint(other_cert)],
|
|
838
|
+
):
|
|
839
|
+
check_signature(t, keys, only_one_signature=True)
|
|
840
|
+
|
|
841
|
+
with pytest.raises(MetadataException):
|
|
842
|
+
check_signature(t, other_pem)
|
|
843
|
+
with pytest.raises(MetadataException):
|
|
844
|
+
check_signature(t, [other_pem_text])
|
|
845
|
+
with pytest.raises(MetadataException, match="does not match"):
|
|
846
|
+
check_signature(t, fingerprint(other_cert))
|
|
847
|
+
with pytest.raises(MetadataException, match="cannot be mixed"):
|
|
848
|
+
check_signature(t, [other_pem, fingerprint(signer_cert)])
|
|
849
|
+
finally:
|
|
850
|
+
shutil.rmtree(tmpdir)
|
|
851
|
+
|
|
773
852
|
def test_signing_and_validation(self):
|
|
774
853
|
self.output = tempfile.NamedTemporaryFile('w').name
|
|
775
854
|
_res_s, _md_s, _ctx_s = self.run_pipeline("signer.fd", self)
|
|
@@ -1226,6 +1305,44 @@ class SigningTest(PipeLineTest):
|
|
|
1226
1305
|
except OSError:
|
|
1227
1306
|
pass
|
|
1228
1307
|
|
|
1308
|
+
def test_xslt_cleanup_keeps_xsi_type_prefixes(self):
|
|
1309
|
+
"""XSLT-rewritten metadata must stay well-formed when attributes use xsi:/xsd: prefixes.
|
|
1310
|
+
|
|
1311
|
+
eIDAS middleware metadata (test07-xsi-type-middleware.xml) uses
|
|
1312
|
+
xsi:type="xsd:string". Running it through an xslt step must not produce output
|
|
1313
|
+
with an undeclared prefix ("Undeclared namespace prefix: xsi"), which broke
|
|
1314
|
+
loading in pyuppsala (https://github.com/kushaldas/pyuppsala/issues/36).
|
|
1315
|
+
"""
|
|
1316
|
+
entity = 'https://test.demw.eidas.swedenconnect.se/eidas-middleware/Metadata'
|
|
1317
|
+
xsi_type = '{http://www.w3.org/2001/XMLSchema-instance}type'
|
|
1318
|
+
tmpfile = tempfile.NamedTemporaryFile('w').name
|
|
1319
|
+
try:
|
|
1320
|
+
self.exec_pipeline(
|
|
1321
|
+
f"""
|
|
1322
|
+
- when clean:
|
|
1323
|
+
- xslt:
|
|
1324
|
+
stylesheet: tidy.xsl
|
|
1325
|
+
- check_xml_namespaces
|
|
1326
|
+
- break
|
|
1327
|
+
- load:
|
|
1328
|
+
- file://{self.datadir}/metadata/test07-xsi-type-middleware.xml cleanup clean
|
|
1329
|
+
- select
|
|
1330
|
+
- publish: {tmpfile}
|
|
1331
|
+
"""
|
|
1332
|
+
)
|
|
1333
|
+
t2 = parse_xml(tmpfile)
|
|
1334
|
+
assert t2 is not None
|
|
1335
|
+
entity_elt = t2.find(".//{{{}}}EntityDescriptor[@entityID='{}']".format(NS['md'], entity))
|
|
1336
|
+
assert entity_elt is not None, "entity was dropped after the xslt step"
|
|
1337
|
+
typed = entity_elt.findall(".//*[@{http://www.w3.org/2001/XMLSchema-instance}type]")
|
|
1338
|
+
assert typed, "xsi:type attributes were lost or lost their namespace"
|
|
1339
|
+
assert all(e.get(xsi_type) == 'xsd:string' for e in typed)
|
|
1340
|
+
finally:
|
|
1341
|
+
try:
|
|
1342
|
+
os.unlink(tmpfile)
|
|
1343
|
+
except OSError:
|
|
1344
|
+
pass
|
|
1345
|
+
|
|
1229
1346
|
def test_wsfed_type_prefix_survives(self):
|
|
1230
1347
|
"""A complex-type xsi:type prefix (as opposed to a datatype prefix like xs/xsd)
|
|
1231
1348
|
needs no special-casing: real WS-Fed metadata also carries elements in that
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
|
|
3
|
+
from pyuppsala import etree
|
|
4
|
+
|
|
5
|
+
XSI = "http://www.w3.org/2001/XMLSchema-instance"
|
|
6
|
+
XSL_HEAD = b'<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _transform(stylesheet: bytes, source: bytes):
|
|
10
|
+
result = etree.XSLT(etree.fromstring(stylesheet))(etree.fromstring(source))
|
|
11
|
+
# getroot() reparses the serialized output, so an undeclared prefix raises here
|
|
12
|
+
return result.getroot()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestXSLTNamespaceDeclarations(TestCase):
|
|
16
|
+
"""Created attributes must carry the namespace declaration they need (XSLT 1.0 7.1.3).
|
|
17
|
+
|
|
18
|
+
Regression tests for https://github.com/kushaldas/pyuppsala/issues/36.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def test_attribute_copied_into_literal_result_element(self):
|
|
22
|
+
xsl = (
|
|
23
|
+
XSL_HEAD
|
|
24
|
+
+ b"""
|
|
25
|
+
<xsl:template match="/root"><out><xsl:apply-templates select="node()|@*"/></out></xsl:template>
|
|
26
|
+
<xsl:template match="*"><xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy></xsl:template>
|
|
27
|
+
<xsl:template match="text()|@*"><xsl:copy/></xsl:template>
|
|
28
|
+
</xsl:stylesheet>"""
|
|
29
|
+
)
|
|
30
|
+
src = b'<root xmlns:xsi="%s" xsi:type="xsd:string"><a xsi:type="x"/></root>' % XSI.encode()
|
|
31
|
+
out = _transform(xsl, src)
|
|
32
|
+
assert out.get('{%s}type' % XSI) == 'xsd:string'
|
|
33
|
+
assert out[0].get('{%s}type' % XSI) == 'x'
|
|
34
|
+
|
|
35
|
+
def test_prefix_declared_only_on_dropped_element(self):
|
|
36
|
+
xsl = (
|
|
37
|
+
XSL_HEAD
|
|
38
|
+
+ b"""
|
|
39
|
+
<xsl:template match="/"><out><xsl:copy-of select="//a/@*"/><b><xsl:copy-of select="//a/@*"/></b></out></xsl:template>
|
|
40
|
+
</xsl:stylesheet>"""
|
|
41
|
+
)
|
|
42
|
+
src = b'<root><a xmlns:xsi="%s" xsi:type="t"/></root>' % XSI.encode()
|
|
43
|
+
out = _transform(xsl, src)
|
|
44
|
+
assert out.get('{%s}type' % XSI) == 't'
|
|
45
|
+
assert out[0].get('{%s}type' % XSI) == 't'
|
|
@@ -17,7 +17,7 @@ import threading
|
|
|
17
17
|
import time
|
|
18
18
|
import traceback
|
|
19
19
|
from _collections_abc import Mapping, MutableMapping
|
|
20
|
-
from collections.abc import Sequence
|
|
20
|
+
from collections.abc import Iterable, Sequence
|
|
21
21
|
from copy import copy
|
|
22
22
|
from datetime import datetime, timedelta, timezone
|
|
23
23
|
from email.utils import parsedate
|
|
@@ -271,6 +271,9 @@ def redis():
|
|
|
271
271
|
return thread_data.redis
|
|
272
272
|
|
|
273
273
|
|
|
274
|
+
PEM_CERT_PREFIX = '-----BEGIN CERTIFICATE-----'
|
|
275
|
+
|
|
276
|
+
|
|
274
277
|
def _looks_like_sha1_fingerprint(key: str) -> bool:
|
|
275
278
|
"""Return True if ``key`` looks like a SHA1 certificate fingerprint.
|
|
276
279
|
|
|
@@ -283,39 +286,71 @@ def _looks_like_sha1_fingerprint(key: str) -> bool:
|
|
|
283
286
|
return len(candidate) == 40 and all(c in '0123456789abcdefABCDEF' for c in candidate)
|
|
284
287
|
|
|
285
288
|
|
|
286
|
-
def check_signature(
|
|
289
|
+
def check_signature(
|
|
290
|
+
t: ElementTree,
|
|
291
|
+
key: Optional[Union[str, Iterable[str]]],
|
|
292
|
+
only_one_signature: bool = False,
|
|
293
|
+
source: Optional[str] = None,
|
|
294
|
+
) -> ElementTree:
|
|
287
295
|
"""Verify the XML signature on ``t`` using pybergshamra.
|
|
288
296
|
|
|
289
|
-
``key`` is
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
297
|
+
``key`` is one verifier or an iterable of them, which allows key rollovers:
|
|
298
|
+
the signature is accepted if it was made by any one of them. A verifier is
|
|
299
|
+
either a path to a PEM-encoded X.509 certificate file, the PEM text of a
|
|
300
|
+
certificate, or a SHA1 fingerprint of the expected signing certificate.
|
|
301
|
+
Fingerprints cannot be mixed with certificates.
|
|
302
|
+
|
|
303
|
+
Certificates are the trusted signing keys. When fingerprints are supplied
|
|
304
|
+
the certificate embedded in the signature's KeyInfo is used to verify, and
|
|
305
|
+
the fingerprint is checked against it afterwards.
|
|
306
|
+
|
|
307
|
+
``source`` (typically the URL) is only used to identify the signed document in log messages.
|
|
293
308
|
"""
|
|
294
309
|
if key is None:
|
|
295
310
|
return t
|
|
296
311
|
|
|
297
|
-
|
|
312
|
+
of_source = f" of {source}" if source else ""
|
|
313
|
+
|
|
314
|
+
items = [key] if isinstance(key, str) else list(key)
|
|
315
|
+
items = [item.strip() for item in items if item.strip()]
|
|
316
|
+
if not items:
|
|
317
|
+
raise MetadataException("No certificate or fingerprint given to verify the signature")
|
|
318
|
+
|
|
319
|
+
log.debug(f"verifying signature{of_source} using {len(items)} verifier(s)")
|
|
298
320
|
|
|
299
321
|
mgr = pybergshamra.KeysManager()
|
|
300
322
|
ctx = pybergshamra.DsigContext(mgr)
|
|
301
323
|
|
|
302
|
-
|
|
303
|
-
|
|
324
|
+
is_fingerprint = [
|
|
325
|
+
not item.startswith(PEM_CERT_PREFIX) and _looks_like_sha1_fingerprint(item) and not os.path.exists(item)
|
|
326
|
+
for item in items
|
|
327
|
+
]
|
|
328
|
+
if any(is_fingerprint) and not all(is_fingerprint):
|
|
329
|
+
raise MetadataException("Certificates and fingerprints cannot be mixed in a single verification")
|
|
330
|
+
|
|
331
|
+
pinned_fingerprints: set[str] = set()
|
|
332
|
+
if all(is_fingerprint):
|
|
304
333
|
# Fingerprint pinning: trust the cert carried inside the signature's
|
|
305
|
-
# KeyInfo/X509Data, then compare its SHA1 to the pinned
|
|
334
|
+
# KeyInfo/X509Data, then compare its SHA1 to the pinned fingerprints.
|
|
306
335
|
# The fingerprint is the trust anchor here, so PKI chain validation is
|
|
307
336
|
# disabled (insecure) - the embedded signing cert is typically
|
|
308
337
|
# self-signed and would otherwise fail to chain to a trusted root.
|
|
309
|
-
|
|
338
|
+
pinned_fingerprints = {item.replace(':', '').replace(' ', '').lower() for item in items}
|
|
310
339
|
ctx.enabled_key_data_x509 = True
|
|
311
340
|
ctx.trusted_keys_only = False
|
|
312
341
|
ctx.insecure = True
|
|
313
342
|
else:
|
|
314
|
-
# Trusted-cert path: load the PEM
|
|
315
|
-
# only acceptable signing
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
343
|
+
# Trusted-cert path: load the PEM certificates and require them as the
|
|
344
|
+
# only acceptable signing keys (skip inline KeyInfo extraction).
|
|
345
|
+
cert_keys = []
|
|
346
|
+
for item in items:
|
|
347
|
+
if item.startswith(PEM_CERT_PREFIX):
|
|
348
|
+
pem = item.encode()
|
|
349
|
+
else:
|
|
350
|
+
with open(item, 'rb') as fd:
|
|
351
|
+
pem = fd.read()
|
|
352
|
+
cert_keys.append(pybergshamra.load_x509_cert_pem(pem))
|
|
353
|
+
mgr.add_keys(cert_keys)
|
|
319
354
|
ctx.trusted_keys_only = True
|
|
320
355
|
|
|
321
356
|
relt = root(t)
|
|
@@ -340,16 +375,25 @@ def check_signature(t: ElementTree, key: Optional[str], only_one_signature: bool
|
|
|
340
375
|
"XML metadata contains %d signatures - exactly 1 is required" % len(references)
|
|
341
376
|
)
|
|
342
377
|
|
|
343
|
-
|
|
378
|
+
# The leaf certificate that verified the signature: the matching trusted key
|
|
379
|
+
# in certificate mode, the one embedded in KeyInfo in fingerprint mode.
|
|
380
|
+
chain = result.key_info.x509_chain if result.key_info is not None else []
|
|
381
|
+
actual_fingerprint = hashlib.sha1(chain[0]).hexdigest().lower() if chain else None
|
|
382
|
+
if actual_fingerprint is not None:
|
|
383
|
+
log.debug(
|
|
384
|
+
f"signature{of_source} verified by certificate "
|
|
385
|
+
+ ":".join(actual_fingerprint[i : i + 2] for i in range(0, len(actual_fingerprint), 2))
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if pinned_fingerprints:
|
|
344
389
|
# Confirm the certificate that actually verified the signature matches
|
|
345
|
-
# the pinned
|
|
346
|
-
|
|
347
|
-
if not chain:
|
|
390
|
+
# one of the pinned fingerprints (SHA1 over the DER-encoded leaf certificate).
|
|
391
|
+
if actual_fingerprint is None:
|
|
348
392
|
raise MetadataException("No certificate found in signature to match fingerprint")
|
|
349
|
-
actual_fingerprint
|
|
350
|
-
if actual_fingerprint != pinned_fingerprint:
|
|
393
|
+
if actual_fingerprint not in pinned_fingerprints:
|
|
351
394
|
raise MetadataException(
|
|
352
|
-
f"Signing certificate fingerprint {actual_fingerprint} does not match expected
|
|
395
|
+
f"Signing certificate fingerprint {actual_fingerprint} does not match expected "
|
|
396
|
+
f"{', '.join(sorted(pinned_fingerprints))}"
|
|
353
397
|
)
|
|
354
398
|
|
|
355
399
|
# Anti-wrapping: the old code returned the verified reference subtree as the
|
|
@@ -396,6 +440,13 @@ def cert_dict(t):
|
|
|
396
440
|
return certs
|
|
397
441
|
|
|
398
442
|
|
|
443
|
+
def b64_to_pem(cert_b64: str) -> str:
|
|
444
|
+
"""Wrap the base64 text of a DER certificate (as found in ``X509Certificate``) as PEM."""
|
|
445
|
+
body = "".join(cert_b64.split())
|
|
446
|
+
lines = [body[i : i + 64] for i in range(0, len(body), 64)]
|
|
447
|
+
return "\n".join([PEM_CERT_PREFIX, *lines, "-----END CERTIFICATE-----", ""])
|
|
448
|
+
|
|
449
|
+
|
|
399
450
|
class CertInfo:
|
|
400
451
|
"""Lightweight wrapper around a parsed X.509 certificate.
|
|
401
452
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/bad_metadata/stockholmstad-c1107b3ceb78c5e9.xml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyff-3.0.0 → pyff-3.1.0}/src/pyff/test/data/metadata/test-sp-trustinfo-in-attr-broken-base64.xml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|