diff --git a/lotl/tl_entries/wrpac-provider/raidiam.json b/lotl/tl_entries/wrpac-provider/raidiam.json new file mode 100644 index 0000000..04f7b1d --- /dev/null +++ b/lotl/tl_entries/wrpac-provider/raidiam.json @@ -0,0 +1,8 @@ +{ + "tl_url": "https://webuild-consortium.github.io/wp4-trust-group/lotl/wrpac-providers-lote.json", + "trust_anchor": "-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n", + "metadata": { + "operator_name": "Raidiam Services Ltd", + "country": "EU" + } +} diff --git a/lotl/wrpac-providers-lote.json b/lotl/wrpac-providers-lote.json new file mode 100644 index 0000000..4d32e92 --- /dev/null +++ b/lotl/wrpac-providers-lote.json @@ -0,0 +1,187 @@ +{ + "LoTE": { + "ListAndSchemeInformation": { + "LoTEVersionIdentifier": 1, + "LoTESequenceNumber": 1, + "LoTEType": "http://uri.etsi.org/19602/LoTEType/EUWRPACProvidersList", + "SchemeOperatorName": [ + { + "lang": "en", + "value": "WP4" + } + ], + "SchemeOperatorAddress": { + "SchemeOperatorPostalAddress": [ + { + "lang": "en", + "StreetAddress": "New Broad Street House, 35 New Broad Street", + "Locality": "London", + "PostalCode": "EC2", + "Country": "GB" + } + ], + "SchemeOperatorElectronicAddress": [ + { + "lang": "en", + "uriValue": "mailto:sales@raidiam.com" + }, + { + "lang": "en", + "uriValue": "https://www.raidiam.com" + } + ] + }, + "SchemeName": [ + { + "lang": "en", + "value": "Raidiam WRPAC Providers List" + } + ], + "SchemeInformationURI": [ + { + "lang": "en", + "uriValue": "https://webuild-consortium.github.io/wp4-trust-group/" + }, + { + "lang": "en", + "uriValue": "https://webuild-consortium.github.io/wp4-trust-group/lotl/archive/" + } + ], + "StatusDeterminationApproach": "http://uri.etsi.org/19602/WRPACProvidersList/StatusDetn/EU", + "SchemeTypeCommunityRules": [ + { + "lang": "en", + "uriValue": "http://uri.etsi.org/19602/WRPACProvidersList/schemerules/EU" + } + ], + "SchemeTerritory": "EU", + "PointersToOtherLoTE": [ + { + "LoTELocation": "https://webuild-consortium.github.io/wp4-trust-group/lotl/wrpac-providers-lote.json", + "ServiceDigitalIdentities": [ + { + "X509Certificates": [ + { + "val": "MIIBijCCATCgAwIBAgIUGXbK+13fCUh2t9wXdSdRXM5dgXYwCgYIKoZIzj0EAwIwGzELMAkGA1UEBhMCRVUxDDAKBgNVBAoMA1dQNDAeFw0yNjA1MDYxMTE1MTRaFw0yOTA1MDUxMTE1MTRaMBsxCzAJBgNVBAYTAkVVMQwwCgYDVQQKDANXUDQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASw9STaruujfO6V+GkB0xWp/u2Bilm0zmoIWhE3ITLCVH8RCHsssH9zsVce4Hkb3Umjklo0rbr45sBa1LZaoEWCo1IwUDAOBgNVHQ8BAf8EBAMCBsAwEQYDVR0lBAowCAYGBACRNwMAMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFMsK9Wn1/l86EhoxMktNb9CKAzwIMAoGCCqGSM49BAMCA0gAMEUCIQDriudDdhwGrY6y6F1NbAo1NmlHajCdFxwT4URnPSwagwIgbqsokmnnBScGe3Z7BmgDNxNRucpodAamv9nL5pjhTA4=" + } + ] + } + ], + "LoTEQualifiers": [ + { + "LoTEType": "http://uri.etsi.org/19602/LoTEType/EUWRPACProvidersList", + "SchemeOperatorName": [ + { + "lang": "en", + "value": "Raidiam Services Limited" + } + ], + "SchemeTerritory": "EU", + "MimeType": "application/json" + } + ] + } + ], + "ListIssueDateTime": "2026-05-06T00:00:00Z", + "NextUpdate": "2026-11-06T00:00:00Z", + "DistributionPoints": [ + "https://webuild-consortium.github.io/wp4-trust-group/lotl/wrpac-providers-lote.json" + ] + }, + "TrustedEntitiesList": [ + { + "TrustedEntityInformation": { + "TEName": [ + { + "lang": "en", + "value": "Raidiam Services Ltd" + } + ], + "TETradeName": [ + { + "lang": "en", + "value": "NTRGB-10742851" + } + ], + "TEAddress": { + "TEPostalAddress": [ + { + "lang": "en", + "StreetAddress": "New Broad Street House, 35 New Broad Street", + "Locality": "London", + "PostalCode": "EC2 1NH", + "Country": "GB" + } + ], + "TEElectronicAddress": [ + { + "lang": "en", + "uriValue": "mailto:sales@raidiam.com" + }, + { + "lang": "en", + "uriValue": "tel:+44020245836770" + } + ] + }, + "TEInformationURI": [ + { + "lang": "en", + "uriValue": "https://auth.sandbox.eudi.raidiam.io/policies/terms-of-use.pdf" + }, + { + "lang": "en", + "uriValue": "https://www.raidiam.com/" + }, + { + "lang": "en", + "uriValue": "http://uri.etsi.org/19602/ListOfTrustedEntities/WRPACProvider/PL" + } + ] + }, + "TrustedEntityServices": [ + { + "ServiceInformation": { + "ServiceTypeIdentifier": "http://uri.etsi.org/19602/SvcType/WRPAC/Issuance", + "ServiceName": [ + { + "lang": "en", + "value": "Raidiam WRPAC Issuance Service" + } + ], + "ServiceDigitalIdentity": { + "X509Certificates": [ + { + "val": "MIIFujCCA6KgAwIBAgIUUaTwXC17upDsPSF7y2a15o3IzuMwDQYJKoZIhvcNAQENBQAwdTELMAkGA1UEBhMCR0IxDTALBgNVBAoTBEVVREkxMzAxBgNVBAsTKkVVIERpZ2l0YWwgSWRlbnRpdHkgV2FsbGV0IFRydXN0IEZyYW1ld29yazEiMCAGA1UEAxMZRVVESSBTYW5kYm94IFJvb3QgQ0EgLSBHMTAeFw0yNjAzMzAxMzM4MDBaFw00MTAzMjYxMzM4MDBaMHUxCzAJBgNVBAYTAkdCMQ0wCwYDVQQKEwRFVURJMTMwMQYDVQQLEypFVSBEaWdpdGFsIElkZW50aXR5IFdhbGxldCBUcnVzdCBGcmFtZXdvcmsxIjAgBgNVBAMTGUVVREkgU2FuZGJveCBSb290IENBIC0gRzEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDd/j1a7EK31Zhr5ulG46QJOkXXmfhaLsvMJRcOV5bKR9m7MeBotIvdSCV0MkbDWaXtTQxQZxhGqMgeYa6CCNBrTD64Nri2ypnxGdMsrOZe0N+D4I3U/3G4AGaZ9RY5khzSTWaY1kc3u5FoCKnGfPO7DAjOvhomI3xo1v0aLPBoG6C/euIu3HOcA8LTvpdCj8Kd/8+Qv3cspKTk3dVB7b3QTn44vXQTna+F8wuUyt4XcS/hz+AJr/QYZlwKNQZa22tpVWK651quMyFPNyfNSN89bd39uA2Dc8HjtgxupxGHdlspbbLsK9YLerNI7gus6rWYUzj95ZdTvYQEUSFNB4uTgo3YiG6fO5i7sWglmsjmfFX8ylBHsnD7Mu0jcYNM34j/zpVPK69XZTb14sJzDFllzX3thvKGIhO2rS7/3p6Dmkrx5tAs/WbJeAhBhX4WU1sAn8k1lZKbO1RcY8ZisLLnDPJaloy4B3c5tp/YGH0ZYGPsok/sIeDAfwACkwNP/D3dtMGMMSMDpuL05eCVwEPBMrSP0jCwsUFZjdumN28M47H/PAoVPYj90vVPLGVR2/gUJEGtgD/jnl3uMDomwg1j0+QwTJ2fYFW0c1RuXnTSYUL7kT8qBYmXI2ipcTUUtyq8VCFphmtW9rHFbuLDkWC6uGEzNlZ03D2fePhDWhrnawIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUnQIZYJE9Ibg5jGQiDMJZ/FFWfrIwDQYJKoZIhvcNAQENBQADggIBAFoDml/joIZZmJfChgul6/gAIYEE/3dc6yIbUF4x+F6rFd3qsCY+uqBapqJZPAlPKV9SNzcqJuDZKQyH2MVPwGGacy71ywF4f0D7Gn+45+B+AhPdTX8lLXagtQeAgTv298Q637pZFlZCsv0W7RfWTVAhe6ZS0q0sEE+TRRHPwYmuAvTV3ZutX9pdcxtGwHd1LJi71O3LbJLHebI1lbJBNs6KLpTSmq3qN+P1s8a4MTtiUfbY+zjULnUHwqUjkY1hw69aewGTRwQvrizUOzz8/u3YZRN2f5OKtLSfozae5SM97nRRdm4fLcNKEsftkn+rifFXr/Aze7Kacn5Q5vp+aR4TSx5tBRf7Uo/sn/vsIRDXxftPQst763uokeki6HjGkT1fT2SVs7p2VzhgcjDnCsoe6G+FNgwXsTaBnqALbLEwo+ellN6dKj3FNVThJIJtMG8v96XK2TSPBBtwy0LG7IhXcj6AU/w/9BAJRvjfDCceKojJaNVbOyDc3UMZ8xwnCSNGkIPNuYLAFtRQkcwOVmiFnPtZs+hGLy/yhWDwBO1nfupzKMlG62F4LxThJ28RJ6LrMwWGpyJ2SVs1mbo/UR2xXLM7beIWJHlgOoLAhtKBZOeDcOVPMmh/xiiiwpXGwr7ArHsZ07jBhltCZZlMwkuEIjBsvNxOY3DaA7VjZUlB" + } + ] + } + } + }, + { + "ServiceInformation": { + "ServiceTypeIdentifier": "http://uri.etsi.org/19602/SvcType/WRPAC/Revocation", + "ServiceName": [ + { + "lang": "en", + "value": "Raidiam WRPAC Revocation Service" + } + ], + "ServiceDigitalIdentity": { + "X509Certificates": [ + { + "val": "MIIFujCCA6KgAwIBAgIUUaTwXC17upDsPSF7y2a15o3IzuMwDQYJKoZIhvcNAQENBQAwdTELMAkGA1UEBhMCR0IxDTALBgNVBAoTBEVVREkxMzAxBgNVBAsTKkVVIERpZ2l0YWwgSWRlbnRpdHkgV2FsbGV0IFRydXN0IEZyYW1ld29yazEiMCAGA1UEAxMZRVVESSBTYW5kYm94IFJvb3QgQ0EgLSBHMTAeFw0yNjAzMzAxMzM4MDBaFw00MTAzMjYxMzM4MDBaMHUxCzAJBgNVBAYTAkdCMQ0wCwYDVQQKEwRFVURJMTMwMQYDVQQLEypFVSBEaWdpdGFsIElkZW50aXR5IFdhbGxldCBUcnVzdCBGcmFtZXdvcmsxIjAgBgNVBAMTGUVVREkgU2FuZGJveCBSb290IENBIC0gRzEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDd/j1a7EK31Zhr5ulG46QJOkXXmfhaLsvMJRcOV5bKR9m7MeBotIvdSCV0MkbDWaXtTQxQZxhGqMgeYa6CCNBrTD64Nri2ypnxGdMsrOZe0N+D4I3U/3G4AGaZ9RY5khzSTWaY1kc3u5FoCKnGfPO7DAjOvhomI3xo1v0aLPBoG6C/euIu3HOcA8LTvpdCj8Kd/8+Qv3cspKTk3dVB7b3QTn44vXQTna+F8wuUyt4XcS/hz+AJr/QYZlwKNQZa22tpVWK651quMyFPNyfNSN89bd39uA2Dc8HjtgxupxGHdlspbbLsK9YLerNI7gus6rWYUzj95ZdTvYQEUSFNB4uTgo3YiG6fO5i7sWglmsjmfFX8ylBHsnD7Mu0jcYNM34j/zpVPK69XZTb14sJzDFllzX3thvKGIhO2rS7/3p6Dmkrx5tAs/WbJeAhBhX4WU1sAn8k1lZKbO1RcY8ZisLLnDPJaloy4B3c5tp/YGH0ZYGPsok/sIeDAfwACkwNP/D3dtMGMMSMDpuL05eCVwEPBMrSP0jCwsUFZjdumN28M47H/PAoVPYj90vVPLGVR2/gUJEGtgD/jnl3uMDomwg1j0+QwTJ2fYFW0c1RuXnTSYUL7kT8qBYmXI2ipcTUUtyq8VCFphmtW9rHFbuLDkWC6uGEzNlZ03D2fePhDWhrnawIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUnQIZYJE9Ibg5jGQiDMJZ/FFWfrIwDQYJKoZIhvcNAQENBQADggIBAFoDml/joIZZmJfChgul6/gAIYEE/3dc6yIbUF4x+F6rFd3qsCY+uqBapqJZPAlPKV9SNzcqJuDZKQyH2MVPwGGacy71ywF4f0D7Gn+45+B+AhPdTX8lLXagtQeAgTv298Q637pZFlZCsv0W7RfWTVAhe6ZS0q0sEE+TRRHPwYmuAvTV3ZutX9pdcxtGwHd1LJi71O3LbJLHebI1lbJBNs6KLpTSmq3qN+P1s8a4MTtiUfbY+zjULnUHwqUjkY1hw69aewGTRwQvrizUOzz8/u3YZRN2f5OKtLSfozae5SM97nRRdm4fLcNKEsftkn+rifFXr/Aze7Kacn5Q5vp+aR4TSx5tBRf7Uo/sn/vsIRDXxftPQst763uokeki6HjGkT1fT2SVs7p2VzhgcjDnCsoe6G+FNgwXsTaBnqALbLEwo+ellN6dKj3FNVThJIJtMG8v96XK2TSPBBtwy0LG7IhXcj6AU/w/9BAJRvjfDCceKojJaNVbOyDc3UMZ8xwnCSNGkIPNuYLAFtRQkcwOVmiFnPtZs+hGLy/yhWDwBO1nfupzKMlG62F4LxThJ28RJ6LrMwWGpyJ2SVs1mbo/UR2xXLM7beIWJHlgOoLAhtKBZOeDcOVPMmh/xiiiwpXGwr7ArHsZ07jBhltCZZlMwkuEIjBsvNxOY3DaA7VjZUlB" + } + ] + } + } + } + ] + } + ] + }, + "signature": { + "protected": "eyJhbGciOiJFUzI1NiIsImlhdCI6MTc3ODIyNjQyMywieDVjIjpbIk1JSUJpakNDQVRDZ0F3SUJBZ0lVR1hiSysxM2ZDVWgydDl3WGRTZFJYTTVkZ1hZd0NnWUlLb1pJemowRUF3SXdHekVMTUFrR0ExVUVCaE1DUlZVeEREQUtCZ05WQkFvTUExZFFOREFlRncweU5qQTFNRFl4TVRFMU1UUmFGdzB5T1RBMU1EVXhNVEUxTVRSYU1Cc3hDekFKQmdOVkJBWVRBa1ZWTVF3d0NnWURWUVFLREFOWFVEUXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBU3c5U1RhcnV1amZPNlYrR2tCMHhXcC91MkJpbG0wem1vSVdoRTNJVExDVkg4UkNIc3NzSDl6c1ZjZTRIa2IzVW1qa2xvMHJicjQ1c0JhMUxaYW9FV0NvMUl3VURBT0JnTlZIUThCQWY4RUJBTUNCc0F3RVFZRFZSMGxCQW93Q0FZR0JBQ1JOd01BTUF3R0ExVWRFd0VCL3dRQ01BQXdIUVlEVlIwT0JCWUVGTXNLOVduMS9sODZFaG94TWt0TmI5Q0tBendJTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSVFEcml1ZERkaHdHclk2eTZGMU5iQW8xTm1sSGFqQ2RGeHdUNFVSblBTd2Fnd0lnYnFzb2ttbm5CU2NHZTNaN0JtZ0ROeE5SdWNwb2RBYW12OW5MNXBqaFRBND0iXX0", + "signature": "I0puAxSzSU_WNTS7bCLr48kd-z2_LnZ8U5cAzu5_trTafxzk01DhjGNuG1VXKiP9qxa13ora5Ssc_uadTIIboA" + } +} \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 50974b5..2323705 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,5 +6,5 @@ pytest-cov jsonschema>=4.20 signxml lxml -jwcrypto +jwcrypto>=1.5.6 cryptography diff --git a/tools/lotl/jades_signer.py b/tools/lotl/jades_signer.py index 9b2886e..ce106e3 100644 --- a/tools/lotl/jades_signer.py +++ b/tools/lotl/jades_signer.py @@ -8,6 +8,7 @@ from jwcrypto import jwk, jws from jwcrypto.common import json_encode +from tools.lotl.key_alg import infer_jws_algorithm_from_private_key_pem from tools.lotl.log import get_logger logger = get_logger(__name__) @@ -40,7 +41,7 @@ def sign_json( payload: dict[str, Any], key_pem: Union[bytes, str, Path], cert_pem: Union[bytes, str, Path], - algorithm: str = "RS256", + algorithm: str | None = None, ) -> dict[str, Any]: """Sign JSON payload with JAdES Compact Baseline B. @@ -50,13 +51,16 @@ def sign_json( payload: Unsigned JSON-serializable dict (will be modified). key_pem: Private key in PEM format. cert_pem: Signing certificate in PEM format. - algorithm: JWS algorithm (RS256, ES256, PS256). + algorithm: JWS ``alg`` (e.g. ES256, RS256). If omitted, inferred from the private key + (EC P-256 defaults to ES256; RSA defaults to RS256). Returns: Payload dict with 'signature' key added. """ key_data = _load_pem(key_pem) cert_data = _load_pem(cert_pem) + if algorithm is None: + algorithm = infer_jws_algorithm_from_private_key_pem(key_data) key = jwk.JWK.from_pem(key_data) cert_b64 = _cert_to_b64(cert_data) diff --git a/tools/lotl/key_alg.py b/tools/lotl/key_alg.py new file mode 100644 index 0000000..06f5bf8 --- /dev/null +++ b/tools/lotl/key_alg.py @@ -0,0 +1,46 @@ +"""Map private key material to JWS and XML-DSig signature algorithms.""" + +from __future__ import annotations + +from typing import Union + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec, rsa + + +def infer_jws_algorithm_from_private_key_pem(key_pem: Union[bytes, str]) -> str: + """Return JWS ``alg`` (e.g. ES256, RS256) for the given PEM private key.""" + data = key_pem.encode("utf-8") if isinstance(key_pem, str) else key_pem + key = serialization.load_pem_private_key(data, password=None) + if isinstance(key, rsa.RSAPrivateKey): + return "RS256" + if isinstance(key, ec.EllipticCurvePrivateKey): + name = key.curve.name + if name == "secp256r1": + return "ES256" + if name == "secp384r1": + return "ES384" + if name == "secp521r1": + return "ES512" + raise ValueError(f"Unsupported EC curve for JWS: {name}") + raise ValueError(f"Unsupported private key type for JWS: {type(key).__name__}") + + +def infer_xml_signature_algorithm_fragment_from_private_key_pem( + key_pem: Union[bytes, str], +) -> str: + """Return SignXML ``signature_algorithm`` fragment (e.g. ``rsa-sha256``, ``ecdsa-sha256``).""" + data = key_pem.encode("utf-8") if isinstance(key_pem, str) else key_pem + key = serialization.load_pem_private_key(data, password=None) + if isinstance(key, rsa.RSAPrivateKey): + return "rsa-sha256" + if isinstance(key, ec.EllipticCurvePrivateKey): + name = key.curve.name + if name == "secp256r1": + return "ecdsa-sha256" + if name == "secp384r1": + return "ecdsa-sha384" + if name == "secp521r1": + return "ecdsa-sha512" + raise ValueError(f"Unsupported EC curve for XML-DSig: {name}") + raise ValueError(f"Unsupported private key type for XML-DSig: {type(key).__name__}") diff --git a/tools/lotl/tests/conftest.py b/tools/lotl/tests/conftest.py index ef1d2e7..6ad15d8 100644 --- a/tools/lotl/tests/conftest.py +++ b/tools/lotl/tests/conftest.py @@ -17,15 +17,15 @@ @pytest.fixture def signing_key_and_cert(tmp_path: Path) -> tuple[Path, Path]: - """Generate a temporary RSA key and self-signed certificate for signing tests.""" + """Generate a temporary EC P-256 key and self-signed certificate (LoTL default key type).""" import datetime from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization - from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.oid import NameOID - key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + key = ec.generate_private_key(ec.SECP256R1()) pubkey = key.public_key() subject = issuer = x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, "EU"), diff --git a/tools/lotl/tests/test_key_alg.py b/tools/lotl/tests/test_key_alg.py new file mode 100644 index 0000000..7e5e4d8 --- /dev/null +++ b/tools/lotl/tests/test_key_alg.py @@ -0,0 +1,91 @@ +"""Tests for JWS / XML-DSig algorithm inference from private keys.""" + +import pytest +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec, rsa + +from tools.lotl.key_alg import ( + infer_jws_algorithm_from_private_key_pem, + infer_xml_signature_algorithm_fragment_from_private_key_pem, +) + + +def _pem_ec_p256() -> bytes: + key = ec.generate_private_key(ec.SECP256R1()) + return key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + +def _pem_rsa() -> bytes: + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + return key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + +def test_infer_ec_p256_defaults() -> None: + pem = _pem_ec_p256() + assert infer_jws_algorithm_from_private_key_pem(pem) == "ES256" + assert infer_xml_signature_algorithm_fragment_from_private_key_pem(pem) == "ecdsa-sha256" + assert infer_jws_algorithm_from_private_key_pem(pem.decode("utf-8")) == "ES256" + + +def test_infer_rsa_defaults() -> None: + pem = _pem_rsa() + assert infer_jws_algorithm_from_private_key_pem(pem) == "RS256" + assert infer_xml_signature_algorithm_fragment_from_private_key_pem(pem) == "rsa-sha256" + + +def test_infer_ec_secp384() -> None: + key = ec.generate_private_key(ec.SECP384R1()) + pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + assert infer_jws_algorithm_from_private_key_pem(pem) == "ES384" + assert infer_xml_signature_algorithm_fragment_from_private_key_pem(pem) == "ecdsa-sha384" + + +def test_infer_unsupported_ec_curve() -> None: + key = ec.generate_private_key(ec.SECP256K1()) + pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + with pytest.raises(ValueError, match="Unsupported EC curve"): + infer_jws_algorithm_from_private_key_pem(pem) + with pytest.raises(ValueError, match="Unsupported EC curve"): + infer_xml_signature_algorithm_fragment_from_private_key_pem(pem) + + +def test_infer_ec_secp521() -> None: + key = ec.generate_private_key(ec.SECP521R1()) + pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + assert infer_jws_algorithm_from_private_key_pem(pem) == "ES512" + assert infer_xml_signature_algorithm_fragment_from_private_key_pem(pem) == "ecdsa-sha512" + + +def test_infer_rejects_ed25519() -> None: + from cryptography.hazmat.primitives.asymmetric import ed25519 + + key = ed25519.Ed25519PrivateKey.generate() + pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + with pytest.raises(ValueError, match="Unsupported private key type for JWS"): + infer_jws_algorithm_from_private_key_pem(pem) + with pytest.raises(ValueError, match="Unsupported private key type for XML-DSig"): + infer_xml_signature_algorithm_fragment_from_private_key_pem(pem) diff --git a/tools/lotl/xades_signer.py b/tools/lotl/xades_signer.py index e0c8b56..11b7a83 100644 --- a/tools/lotl/xades_signer.py +++ b/tools/lotl/xades_signer.py @@ -6,6 +6,7 @@ from lxml import etree from signxml import XMLSigner, XMLVerifier +from tools.lotl.key_alg import infer_xml_signature_algorithm_fragment_from_private_key_pem from tools.lotl.log import get_logger logger = get_logger(__name__) @@ -41,9 +42,11 @@ def sign_xml( if isinstance(cert_pem, bytes): cert_pem = cert_pem.decode("utf-8") if isinstance(cert_pem, bytes) else cert_pem + sig_alg = infer_xml_signature_algorithm_fragment_from_private_key_pem(key_pem) + root = etree.fromstring(xml_content) signer = XMLSigner( - signature_algorithm="rsa-sha256", + signature_algorithm=sig_alg, digest_algorithm="sha256", c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#", )