From 356797034459d44bc84dc036564ae92925855a53 Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 12 May 2020 08:47:51 +0200 Subject: [PATCH 01/50] Astract storage applied. --- aslist_tests/private_jwks.json | 1 + aslist_tests/rsa.key | 15 + aslist_tests/test_43_key_bundle.py | 1014 +++++++++++++++++++ aslist_tests/test_44_key_issuer.py | 585 +++++++++++ aslist_tests/test_45_key_jar.py | 998 ++++++++++++++++++ aslist_tests/test_keys/cert.key | 27 + aslist_tests/test_keys/ec-p256-private.pem | 6 + aslist_tests/test_keys/ec-p256-public.pem | 5 + aslist_tests/test_keys/ec-p256.json | 8 + aslist_tests/test_keys/ec-p384-private.pem | 7 + aslist_tests/test_keys/ec-p384-public.pem | 6 + aslist_tests/test_keys/ec-p384.json | 8 + aslist_tests/test_keys/ec.key | 5 + aslist_tests/test_keys/jwk.json | 12 + aslist_tests/test_keys/rsa-1024-private.pem | 17 + aslist_tests/test_keys/rsa-1024-public.pem | 7 + aslist_tests/test_keys/rsa-1024.json | 9 + aslist_tests/test_keys/rsa-1280-private.pem | 20 + aslist_tests/test_keys/rsa-1280-public.pem | 8 + aslist_tests/test_keys/rsa-1280.json | 9 + aslist_tests/test_keys/rsa-2048-private.pem | 29 + aslist_tests/test_keys/rsa-2048-public.pem | 10 + aslist_tests/test_keys/rsa-2048.json | 9 + aslist_tests/test_keys/rsa-3072-private.pem | 41 + aslist_tests/test_keys/rsa-3072-public.pem | 12 + aslist_tests/test_keys/rsa-3072.json | 9 + aslist_tests/test_keys/rsa-4096-private.pem | 53 + aslist_tests/test_keys/rsa-4096-public.pem | 15 + aslist_tests/test_keys/rsa-4096.json | 9 + aslist_tests/test_keys/rsa.key | 15 + setup.py | 3 +- src/cryptojwt/__init__.py | 2 +- src/cryptojwt/exception.py | 12 + src/cryptojwt/jwt.py | 37 +- src/cryptojwt/key_bundle.py | 126 ++- src/cryptojwt/key_issuer.py | 489 +++++++++ src/cryptojwt/key_jar.py | 573 ++++++----- src/cryptojwt/serialize/__init__.py | 47 + tests/test_04_key_jar.py | 196 ++-- tests/test_09_jwt.py | 4 +- 40 files changed, 4026 insertions(+), 432 deletions(-) create mode 100644 aslist_tests/private_jwks.json create mode 100644 aslist_tests/rsa.key create mode 100755 aslist_tests/test_43_key_bundle.py create mode 100755 aslist_tests/test_44_key_issuer.py create mode 100755 aslist_tests/test_45_key_jar.py create mode 100755 aslist_tests/test_keys/cert.key create mode 100644 aslist_tests/test_keys/ec-p256-private.pem create mode 100644 aslist_tests/test_keys/ec-p256-public.pem create mode 100644 aslist_tests/test_keys/ec-p256.json create mode 100644 aslist_tests/test_keys/ec-p384-private.pem create mode 100644 aslist_tests/test_keys/ec-p384-public.pem create mode 100644 aslist_tests/test_keys/ec-p384.json create mode 100644 aslist_tests/test_keys/ec.key create mode 100755 aslist_tests/test_keys/jwk.json create mode 100644 aslist_tests/test_keys/rsa-1024-private.pem create mode 100644 aslist_tests/test_keys/rsa-1024-public.pem create mode 100644 aslist_tests/test_keys/rsa-1024.json create mode 100644 aslist_tests/test_keys/rsa-1280-private.pem create mode 100644 aslist_tests/test_keys/rsa-1280-public.pem create mode 100644 aslist_tests/test_keys/rsa-1280.json create mode 100644 aslist_tests/test_keys/rsa-2048-private.pem create mode 100644 aslist_tests/test_keys/rsa-2048-public.pem create mode 100644 aslist_tests/test_keys/rsa-2048.json create mode 100644 aslist_tests/test_keys/rsa-3072-private.pem create mode 100644 aslist_tests/test_keys/rsa-3072-public.pem create mode 100644 aslist_tests/test_keys/rsa-3072.json create mode 100644 aslist_tests/test_keys/rsa-4096-private.pem create mode 100644 aslist_tests/test_keys/rsa-4096-public.pem create mode 100644 aslist_tests/test_keys/rsa-4096.json create mode 100755 aslist_tests/test_keys/rsa.key create mode 100755 src/cryptojwt/key_issuer.py create mode 100644 src/cryptojwt/serialize/__init__.py diff --git a/aslist_tests/private_jwks.json b/aslist_tests/private_jwks.json new file mode 100644 index 0000000..b1471f1 --- /dev/null +++ b/aslist_tests/private_jwks.json @@ -0,0 +1 @@ +{"keys": [{"kty": "RSA", "use": "sig", "kid": "ZG5wZjlUYW11MkZSVzA5MTRuaTdDYUtnV0xQS0pwWUpPTm1YMTJNeFlQYw", "n": "qlDQeEoGfykMrV3WupWIMHGrs4AHVTld-C00qBcVCBNptef6T2UESVSurMITmgCdJrwEVfHwkd2is1xSev2pIjQy8m9CehBexxq0hlNmUhPzNPixbCMqUyxCzGi1bms9qSxg2zJr0pmDFK_EiOyM47B48eYcypUk-PxzX4d1L-jBT6F8B9fT8YjS4OHzs__Yq6EDzJ4gubaqOLjsYQsLkYj3fKz-b4trb9n4eWJkkbvtgCN6gZVVKgmxPpQHVhDIj7jHXfl9QmTooWxObvO9LK8DFK63V-E-Ce5iKNcYmeB4TeOJmdZfasFa4TPOg22jZaE9UOnnZPyXx7VOCkiHsw", "e": "AQAB", "d": "KsaZVVziPNXGhVRoNfyQc_pYsYCaVuFNpKNV8lG5yol1p2ZYC9DHPtOx-1nTKn60-aGHRT66uSf9UScC4DkNXbXWheVDwPyTkVY3uPUBYeP41XkQtqQuYS1gqY4y40Sz--VVfjgvtHkx3uQ2bF1dFWKhPcAZwxeqbY6aO4f9-sYFtgIIINe_vDKw6W8RPJMK4keK_V-8Hw9-t1A_gja2_34U9cSw0yO9BDBNJiE88mX-Nu4djfAqr-MVOXpHEW2FIA7Ge6laZlGR_pn_kaqVbEnG8DLJqJGp_mJIg1tx08kHTVXw8TmqKIeMVZ8uU3Vrfjy6D6PmTkEwZK1_EgvxoQ", "p": "15-5UXO89cFmYFbzW9IRfHp7g8VQw88Uphf3J5IjgI8GL80QqFT7r7uBx1b8uKvJIWnG6OxygVJm9-cy9HPQzCBQQ1e_R5OKAaNvMM_nTVAHAy5KZlt_qFJw7vb5tfRkj8u_0jDt1qp6BYXKBMPWzpxwwKG-aj9PyPrkZyFDn6c", "q": "yjUq8rrtQgnGS4fBQwgf44sg8F47PHX-D7fCmImg6gjW8e4Ll51yDlKHFBskvLNzAWNfy8_GTzTs3Nr2zuQrWrkhXlk4T7FIJATVBI4n4uiNEvfYr17fViLM6T8d0WUM_9zy6EI8CBBRdeAeI8obW5p3F6Wu6UygkrysgyFV-RU"}, {"kty": "EC", "use": "sig", "kid": "aDFHbmtvZldackkyUjJ5aTluSWNaVmJnUXkwYmd2Zk5sOGxCM3dhQVgwRQ", "crv": "P-256", "x": "s4o9jPfgErHOuPBdeWp8U_XUGUs7uSntAu5M4GjTCjQ", "y": "y0YE3V9E-UzEAekBLPhxYqrPPo6Abm-JRFL2Sia6Q0s", "d": "BNXlINQCl4O5vJkdqV13gOqzZJVt34RxN8njWq7Lvlc"}, {"kty": "EC", "use": "sig", "kid": "S0hSaHVmb0phaE1tbW9NcHZjVGRNS291VDlNOE5vWl9UaTNwQVhlUHhOZw", "crv": "P-384", "x": "Lu2wHapme_MnXlhpzH5M5ntqx83j6xYZ2P8u7ZoVOWdvRnmxYC1GrV2IA7feLOUv", "y": "f8S10urWzTkaq1JARY0LgLhZsXmcoTFj5Hd3tyCd6h4XlUU3iZDSfuXahF_xqJ24", "d": "SoRVsBruJ8gmBraOC0bzWD13IOUef3dGRtpIWzmEWm9uWScXxmt6AzgcTAhYP29l"}]} \ No newline at end of file diff --git a/aslist_tests/rsa.key b/aslist_tests/rsa.key new file mode 100644 index 0000000..d34432d --- /dev/null +++ b/aslist_tests/rsa.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQD6vqn19W/VB215DBADRakfPmCtFBf8/+YyhGqixWIwDiEl/L6L +w5HKZCUPVgrC0ADhJfvAbn4fte5MWBCTkqgepKL3BySMA0LMaBF12pbHlPSUbmQG +BJmTX4NNXuUel6TbPYJAU2Nh5Nan0Mb7Bmb8QpFvS0Hw7qZRW8y2eIttfwIDAQAB +AoGBAJVf9FxkRKUB8cOE3h006JWGUY2KROghgn9hxy0ErYO3RyQcN1+HuFh75GAI +gAyiYYO/XwS6TkSR2057wBRJ8ABzcL3+v5g+16Vbh0BjXVE+cv1WGdNGujyzl6ji +jlyF4cb6tXDyqWTLkMAtV20NfO/CGsfii6YEkZb2P90usthRAkEA/oG7a9EvQ7eR +gSEqppzW7KCwidPjnZTr/ROIZQU33nwkIJ0ElTjMNYKP8DerSuixR9skw2ZY8Q8I +1PTBnocHwwJBAPw3SAQYwxZwQMu1trVPMNOGIbSY4rQlMZGXrCZSu/TnozczFLA8 +qNM84g5veyJOzHKmYkIsMG1gwg5VNniG45UCQF6SlLOW0upl70K9sVyiUVcyywcc +Xqty6FJtjLSFQOKC3OXlkwtkRLXpo1UPSq6WUzIxY7LceFZzUMPZg41F/gMCQHNr +POqbBlPzZMOUUZthNP/nhu8lc8Fqr+dnmGElRVxK0JdHKfWInN2mI/DlNV064Dar +S5XqsPKs78EtX7MCT40CQFQZiry8m7ROubOU4+HDG9o1w9zcKXCkmbD9hBCGvTAj +BQNuGE0DtC6FEWTs8bXybLM5yBRq1XiKLdmi5N+3n4g= +-----END RSA PRIVATE KEY----- diff --git a/aslist_tests/test_43_key_bundle.py b/aslist_tests/test_43_key_bundle.py new file mode 100755 index 0000000..b25d681 --- /dev/null +++ b/aslist_tests/test_43_key_bundle.py @@ -0,0 +1,1014 @@ +# pylint: disable=missing-docstring,no-self-use +import json +import os +import shutil +import time + +import pytest +import requests +import responses +from abstorage.storages.absqlalchemy import AbstractStorageSQLAlchemy +from cryptography.hazmat.primitives.asymmetric import rsa +from requests_mock import GET + +from cryptojwt.jwk.ec import ECKey +from cryptojwt.jwk.ec import new_ec_key +from cryptojwt.jwk.hmac import SYMKey +from cryptojwt.jwk.rsa import RSAKey +from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file +from cryptojwt.jwk.rsa import new_rsa_key +from cryptojwt.key_bundle import KeyBundle +from cryptojwt.key_bundle import build_key_bundle +from cryptojwt.key_bundle import dump_jwks +from cryptojwt.key_bundle import init_key +from cryptojwt.key_bundle import key_diff +from cryptojwt.key_bundle import key_gen +from cryptojwt.key_bundle import key_rollover +from cryptojwt.key_bundle import keybundle_from_local_file +from cryptojwt.key_bundle import rsa_init +from cryptojwt.key_bundle import unique_keys +from cryptojwt.key_bundle import update_key_bundle + +__author__ = 'Roland Hedberg' + +BASE_PATH = os.path.abspath( + os.path.join(os.path.dirname(__file__), "test_keys")) + +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +def full_path(local_file): + return os.path.join(BASEDIR, local_file) + + +RSAKEY = os.path.join(BASE_PATH, "cert.key") +RSA0 = os.path.join(BASE_PATH, "rsa.key") +EC0 = os.path.join(BASE_PATH, 'ec.key') +CERT = full_path("../tests/cert.pem") + +JWK0 = {"keys": [ + {'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", + 'n': + 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY' + '2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfK' + 'qoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8'} +]} + +JWK1 = {"keys": [ + { + "n": + 'zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S' + '_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFY' + 'Inq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVb' + 'CGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znan' + 'LwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MX' + 'sGxBHf3AKT5w', + "e": "AQAB", "kty": "RSA", "kid": "rsa1"}, + { + "k": + 'YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNT' + 'Y0NzMzYjE', + "kty": "oct"}, +]} + +JWK2 = { + "keys": [ + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", + "kid": "kriMPdmBvx68skT8-mPAB3BseeA", + "kty": "RSA", + "n": + 'kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS' + '_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1uml94fLyq_AXbwo9yAduf4dCHT' + 'P8CWR1dnDR-Qnz_4PYlWVEuuHHONOw_blbfdMjhY-C_BYM2E3pRxbohBb3x__C' + 'fueV7ddz2LYiH3wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_' + 'GTgWN8A-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQtSsjQtI--Xs' + 'okxRDqYLwus1I1SihgbV_STTg5enufuw', + "use": "sig", + "x5c": [ + 'MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKz' + 'ApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcN' + 'MTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW' + '50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEF' + 'AAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs' + '5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94f' + 'Lyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C' + '/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHF' + 'i3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp' + '+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2Iw' + 'YDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYW' + 'Njb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49Y' + 'D0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDb' + 'dNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajy' + 'vlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5Uqn' + 'I7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF4' + '6aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODY' + 'RMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ' + ], + "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", + "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", + "kty": "RSA", + "n": + 'vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhz' + 'h23V9Tkq-RtwN1Vs_z57hO82kkzL-cQHZX3bMJD-GEGOKXCEXURN7VMyZWMAuz' + 'QoW9vFb1k3cR1RW_EW_P-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5y' + 'Cw5T_Vuwqqsio3V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_K' + 'AS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IAbsk1wRtWDn' + 'dhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ', + "use": "sig", + "x5c": [ + "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" + "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" + "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" + "/uPhEf7zVizjfcr/ISGFe9+yUO" + "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" + "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" + "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" + "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" + "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" + "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" + "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" + "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" + "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" + "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" + "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" + "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" + "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" + "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" + ], + "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" + "-b112-36a304b66dad/v2.0/", + "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", + "kty": "RSA", + "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" + "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" + "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", + "use": "sig", + "x5c": [ + "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" + "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" + "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" + "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" + "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" + "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" + "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" + "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" + "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" + "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" + "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" + "+Iqzmn" + "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" + "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" + "qA==" + ], + "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" + "-b112-36a304b66dad/v2.0/", + "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", + "kty": "RSA", + "n": + "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" + "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" + "-KXVD02jdmX2HNdYJdPyc0c", + "use": "sig", + "x5c": [ + "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" + "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" + "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" + "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" + "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" + "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" + "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" + "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" + "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" + "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" + "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" + "ig==" + ], + "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" + } + ] +} + +JWKS_DICT = {"keys": [ + { + "n": + u"zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVbCGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": u"AQAB", + "kty": "RSA", + "kid": "5-VBFv40P8D4I-7SFz7hMugTbPs", + "use": "enc" + }, + { + "k": u"YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "oct", + "use": "enc" + }, + { + "kty": "EC", + "kid": "7snis", + "use": "sig", + "x": u'q0WbWhflRbxyQZKFuQvh2nZvg98ak-twRoO5uo2L7Po', + "y": u'GOd2jL_6wa0cfnyA0SmEhok9fkYEnAHFKLLM79BZ8_E', + "crv": "P-256" + } +]} + +if os.path.isdir('keys'): + shutil.rmtree('keys') + +ABS_STORAGE_SQLALCHEMY = dict( + driver='sqlalchemy', + url='sqlite:///:memory:', + params=dict(table='Thing'), + handler=AbstractStorageSQLAlchemy +) + +STORAGE_CONFIG = { + 'name': '', + 'class': 'abstorage.type.list.ASList', + 'kwargs': { + 'io_class': 'cryptojwt.serialize.item.JWK', + 'storage_config': ABS_STORAGE_SQLALCHEMY + } +} + + +def test_with_sym_key(): + kc = KeyBundle({"kty": "oct", "key": "highestsupersecret", "use": "sig"}, + storage_conf=STORAGE_CONFIG) + assert len(kc.get("oct")) == 1 + assert len(kc.get("rsa")) == 0 + assert kc.remote is False + assert kc.source is None + + +def test_with_2_sym_key(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) + assert len(kb.get("oct")) == 2 + assert len(kb) == 2 + + assert kb.get_key_with_kid('kid') is None + assert len(kb.kids()) == 2 + + +def test_remove_sym(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) + assert len(kb) == 2 + keys = kb.get('oct') + kb.remove(keys[0]) + assert len(kb) == 1 + + +def test_remove_key_sym(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) + assert len(kb) == 2 + keys = kb.get('oct') + kb.remove(keys[0]) + assert len(kb) == 1 + + # This should not work + kb.remove_keys_by_type('rsa') + # should still be one + assert len(kb) == 1 + + +def test_rsa_init(): + kb = rsa_init( + {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) + assert kb + assert len(kb) == 2 + assert len(kb.get('rsa')) == 2 + + +def test_rsa_init_under_spec(): + kb = rsa_init( + {'use': ['enc', 'sig'], 'size': 1024}, storage_conf=STORAGE_CONFIG) + assert kb + assert len(kb) == 2 + assert len(kb.get('rsa')) == 2 + + +def test_unknown_source(): + with pytest.raises(ImportError): + KeyBundle(source='foobar', storage_conf=STORAGE_CONFIG) + + +def test_ignore_unknown_types(): + kb = KeyBundle({ + "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" + "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", + "use": "sig", + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" + }, + storage_conf=STORAGE_CONFIG) + + assert len(kb) == 0 + + +def test_remove_rsa(): + kb = rsa_init( + {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, + storage_conf=STORAGE_CONFIG) + assert len(kb) == 2 + keys = kb.get('rsa') + assert len(keys) == 2 + kb.remove(keys[0]) + assert len(kb) == 1 + + +def test_key_mix(): + kb = rsa_init( + {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, + storage_conf=STORAGE_CONFIG) + _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) + kb.append(_sym) + assert len(kb) == 3 + assert len(kb.get('rsa')) == 2 + assert len(kb.get('oct')) == 1 + + kb.remove(_sym) + + assert len(kb) == 2 + assert len(kb.get('rsa')) == 2 + assert len(kb.get('oct')) == 0 + + +def test_get_all(): + kb = rsa_init( + {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, + storage_conf=STORAGE_CONFIG + ) + _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) + kb.append(_sym) + assert len(kb.get()) == 3 + + _k = kb.keys() + assert len(_k) == 3 + + +def test_keybundle_from_local_der(): + kb = keybundle_from_local_file( + "{}".format(RSA0), + "der", ['enc'], storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + keys = kb.get('rsa') + assert len(keys) == 1 + _key = keys[0] + assert isinstance(_key, RSAKey) + assert _key.kid + + +def test_ec_keybundle_from_local_der(): + kb = keybundle_from_local_file( + "{}".format(EC0), + "der", ['enc'], keytype='EC', storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + keys = kb.get('ec') + assert len(keys) == 1 + _key = keys[0] + assert _key.kid + assert isinstance(_key, ECKey) + + +def test_keybundle_from_local_der_update(): + kb = keybundle_from_local_file( + "file://{}".format(RSA0), + "der", ['enc'], storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + keys = kb.get('rsa') + assert len(keys) == 1 + _key = keys[0] + assert _key.kid + assert isinstance(_key, RSAKey) + + kb.update() + + # Nothing should change + assert len(kb) == 1 + keys = kb.get('rsa') + assert len(keys) == 1 + _key = keys[0] + assert _key.kid + assert isinstance(_key, RSAKey) + + +def test_creat_jwks_sym(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + kb = KeyBundle([a], storage_conf=STORAGE_CONFIG) + _jwks = kb.jwks() + _loc = json.loads(_jwks) + assert list(_loc.keys()) == ["keys"] + assert set(_loc['keys'][0].keys()) == {'kty', 'use', 'k', 'kid'} + + +def test_keybundle_from_local_jwks_file(): + kb = keybundle_from_local_file( + "file://{}".format(os.path.join(BASE_PATH, "jwk.json")), "jwks", ["sig"], + storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + + +def test_keybundle_from_local_jwks(): + kb = keybundle_from_local_file( + "{}".format(os.path.join(BASE_PATH, "jwk.json")), "jwks", ["sig"], + storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + + +def test_update(): + kc = KeyBundle([{"kty": "oct", "key": "highestsupersecret", "use": "sig"}], + storage_conf=STORAGE_CONFIG) + assert len(kc.get("oct")) == 1 + assert len(kc.get("rsa")) == 0 + assert kc.remote is False + assert kc.source is None + + kc.update() # Nothing should happen + assert len(kc.get("oct")) == 1 + assert len(kc.get("rsa")) == 0 + assert kc.remote is False + assert kc.source is None + + +def test_update_RSA(): + kc = keybundle_from_local_file(RSAKEY, "der", ["sig"], storage_conf=STORAGE_CONFIG) + assert kc.remote is False + assert len(kc.get("oct")) == 0 + assert len(kc.get("RSA")) == 1 + + key = kc.get("RSA")[0] + assert isinstance(key, RSAKey) + + kc.update() + assert kc.remote is False + assert len(kc.get("oct")) == 0 + assert len(kc.get("RSA")) == 1 + + key = kc.get("RSA")[0] + assert isinstance(key, RSAKey) + + +def test_outdated(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) + keys = kb.keys() + now = time.time() + keys[0].inactive_since = now - 60 + kb.set(keys) + kb.remove_outdated(30) + assert len(kb) == 1 + + +def test_dump_jwks(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb2 = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) + + kb1 = rsa_init({'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) + + # Will not dump symmetric keys + dump_jwks([kb1, kb2], 'jwks_combo') + + # Now read it + + nkb = KeyBundle(source='file://jwks_combo', fileformat='jwks', storage_conf=STORAGE_CONFIG) + + assert len(nkb) == 2 + # both RSA keys + assert len(nkb.get('rsa')) == 2 + + # Will dump symmetric keys + dump_jwks([kb1, kb2], 'jwks_combo', symmetric_too=True) + + # Now read it + nkb = KeyBundle(source='file://jwks_combo', fileformat='jwks', storage_conf=STORAGE_CONFIG) + + assert len(nkb) == 4 + # two RSA keys + assert len(nkb.get('rsa')) == 2 + # two symmetric keys + assert len(nkb.get('oct')) == 2 + + +def test_mark_as_inactive(): + desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) + assert len(kb.keys()) == 1 + for k in kb.keys(): + kb.mark_as_inactive(k.kid) + desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb.do_keys([desc]) + assert len(kb.keys()) == 2 + assert len(kb.active_keys()) == 1 + + +def test_copy(): + desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) + assert len(kb.keys()) == 1 + for k in kb.keys(): + kb.mark_as_inactive(k.kid) + desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb.do_keys([desc]) + + kbc = kb.copy() + assert len(kbc.keys()) == 2 + assert len(kbc.active_keys()) == 1 + + +def test_local_jwk(): + _path = full_path('../tests/jwk_private_key.json') + kb = KeyBundle(source='file://{}'.format(_path), storage_conf=STORAGE_CONFIG) + assert kb + + +def test_local_jwk_copy(): + _path = full_path('../tests/jwk_private_key.json') + kb = KeyBundle(source='file://{}'.format(_path), storage_conf=STORAGE_CONFIG) + kb2 = kb.copy() + assert kb2.source == kb.source + + +@pytest.fixture() +def mocked_jwks_response(): + with responses.RequestsMock() as rsps: + yield rsps + + +def test_httpc_params_1(): + source = 'https://login.salesforce.com/id/keys' # From test_jwks_url() + # Mock response + with responses.RequestsMock() as rsps: + rsps.add(method=GET, url=source, json=JWKS_DICT, status=200) + httpc_params = {'timeout': (2, 2)} # connect, read timeouts in seconds + kb = KeyBundle(source=source, httpc=requests.request, + httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) + assert kb.do_remote() + + +def test_httpc_params_2(): + httpc_params = {'timeout': 0} + kb = KeyBundle(source='https://login.salesforce.com/id/keys', + httpc=requests.request, httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) + # Will always fail to fetch the JWKS because the timeout cannot be set + # to 0s + assert not kb.update() + + +def test_update_2(): + rsa_key = new_rsa_key() + _jwks = {"keys": [rsa_key.serialize()]} + fname = 'tmp_jwks.json' + with open(fname, 'w') as fp: + fp.write(json.dumps(_jwks)) + + kb = KeyBundle(source="file://{}".format(fname), fileformat='jwks', storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + + # Added one more key + ec_key = new_ec_key(crv='P-256', key_ops=["sign"]) + _jwks = {'keys': [rsa_key.serialize(), ec_key.serialize()]} + + with open(fname, 'w') as fp: + fp.write(json.dumps(_jwks)) + + kb.update() + assert len(kb) == 2 + + +def test_update_mark_inactive(): + rsa_key = new_rsa_key() + _jwks = {"keys": [rsa_key.serialize()]} + fname = 'tmp_jwks.json' + with open(fname, 'w') as fp: + fp.write(json.dumps(_jwks)) + + kb = KeyBundle(source="file://{}".format(fname), fileformat='jwks', storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + + # new set of keys + rsa_key = new_rsa_key(alg="RS256") + ec_key = new_ec_key(crv='P-256') + _jwks = {'keys': [rsa_key.serialize(), ec_key.serialize()]} + + with open(fname, 'w') as fp: + fp.write(json.dumps(_jwks)) + + kb.update() + # 2 active and 1 inactive + assert len(kb) == 3 + assert len(kb.active_keys()) == 2 + + assert len(kb.get('rsa')) == 1 + assert len(kb.get('rsa', only_active=False)) == 2 + + +def test_loads_0(): + kb = KeyBundle(JWK0, storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + key = kb.get("rsa")[0] + assert key.kid == 'abc' + assert key.kty == 'RSA' + + +def test_loads_1(): + jwks = { + "keys": [ + { + 'kty': 'RSA', + 'use': 'sig', + 'e': 'AQAB', + "n": + 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8', + 'kid': "1" + }, { + 'kty': 'RSA', + 'use': 'enc', + 'e': 'AQAB', + "n": + 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8', + 'kid': "2" + } + ] + } + + kb = KeyBundle(jwks, storage_conf=STORAGE_CONFIG) + + assert len(kb) == 2 + assert set(kb.kids()) == {'1', '2'} + + +def test_dump_jwk(): + kb = KeyBundle(storage_conf=STORAGE_CONFIG) + kb.append(RSAKey(pub_key=import_rsa_key_from_cert_file(CERT))) + jwks = kb.jwks() + + _wk = json.loads(jwks) + assert list(_wk.keys()) == ["keys"] + assert len(_wk["keys"]) == 1 + assert set(_wk["keys"][0].keys()) == {"kty", "e", "n"} + + kb2 = KeyBundle(_wk, storage_conf=STORAGE_CONFIG) + + assert len(kb2) == 1 + key = kb2.get("rsa")[0] + assert key.kty == 'RSA' + assert isinstance(key.public_key(), rsa.RSAPublicKey) + + +JWKS_DICT = {"keys": [ + { + "n": + u"zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVbCGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": u"AQAB", + "kty": "RSA", + "kid": "5-VBFv40P8D4I-7SFz7hMugTbPs", + "use": "enc" + }, + { + "k": u"YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "oct", + "use": "enc" + }, + { + "kty": "EC", + "kid": "7snis", + "use": "sig", + "x": u'q0WbWhflRbxyQZKFuQvh2nZvg98ak-twRoO5uo2L7Po', + "y": u'GOd2jL_6wa0cfnyA0SmEhok9fkYEnAHFKLLM79BZ8_E', + "crv": "P-256" + } +]} + + +def test_keys(): + kb = KeyBundle(JWKS_DICT, storage_conf=STORAGE_CONFIG) + + assert len(kb) == 3 + + assert len(kb.get('rsa')) == 1 + assert len(kb.get('oct')) == 1 + assert len(kb.get('ec')) == 1 + + +EXPECTED = [ + b'iA7PvG_DfJIeeqQcuXFmvUGjqBkda8In_uMpZrcodVA', + b'akXzyGlXg8yLhsCczKb_r8VERLx7-iZBUMIVgg2K7p4', + b'kLsuyGef1kfw5-t-N9CJLIHx_dpZ79-KemwqjwdrvTI' +] + + +def test_thumbprint(): + kb = KeyBundle(JWKS_DICT, storage_conf=STORAGE_CONFIG) + for key in kb: + txt = key.thumbprint('SHA-256') + assert txt in EXPECTED + + +@pytest.mark.network +def test_jwks_url(): + keys = KeyBundle(source='https://login.salesforce.com/id/keys', storage_conf=STORAGE_CONFIG) + # Forces read from the network + keys.update() + assert len(keys) + + +KEYSPEC = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]} +] + +KEYSPEC_2 = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]} +] + +KEYSPEC_3 = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]}, + {"type": "EC", "crv": "P-521", "use": ["sig"]} +] + +KEYSPEC_4 = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]} +] + +KEYSPEC_5 = [ + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]} +] + +KEYSPEC_6 = [ + {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'code'}, + {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'token'}, + {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'refresh_token'} +] + + +def test_key_diff_none(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + + diff = key_diff(_kb, KEYSPEC) + assert not diff + + +def test_key_diff_add_one_ec(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + + diff = key_diff(_kb, KEYSPEC_2) + assert diff + assert set(diff.keys()) == {'add'} + assert len(diff['add']) == 1 + assert diff['add'][0].kty == 'EC' + + +def test_key_diff_add_two_ec(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + + diff = key_diff(_kb, KEYSPEC_3) + assert diff + assert set(diff.keys()) == {'add'} + assert len(diff['add']) == 2 + assert diff['add'][0].kty == 'EC' + + +def test_key_diff_add_ec_and_rsa(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + + diff = key_diff(_kb, KEYSPEC_4) + assert diff + assert set(diff.keys()) == {'add'} + assert len(diff['add']) == 2 + assert set([k.kty for k in diff['add']]) == {'EC', 'RSA'} + + +def test_key_diff_add_ec_del_rsa(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + + diff = key_diff(_kb, KEYSPEC_5) + assert diff + assert set(diff.keys()) == {'add', 'del'} + assert len(diff['add']) == 1 + assert len(diff['del']) == 1 + assert diff['add'][0].kty == 'EC' + assert diff['del'][0].kty == 'RSA' + + +def test_key_bundle_update_1(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + diff = key_diff(_kb, KEYSPEC_2) + update_key_bundle(_kb, diff) + + # There should be 3 keys + assert len(_kb) == 3 + + # one RSA + assert len(_kb.get('RSA')) == 1 + + # 2 EC + assert len(_kb.get('EC')) == 2 + + +def test_key_bundle_update_2(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + diff = key_diff(_kb, KEYSPEC_4) + update_key_bundle(_kb, diff) + + # There should be 3 keys + assert len(_kb) == 4 + + # one RSA + assert len(_kb.get('RSA')) == 2 + + # 2 EC + assert len(_kb.get('EC')) == 2 + + +def test_key_bundle_update_3(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + diff = key_diff(_kb, KEYSPEC_5) + assert set(diff.keys()) == {'add', 'del'} # Add an EC and delete an RSA key + update_key_bundle(_kb, diff) + + # There should be 3 keys + assert len(_kb) == 3 + + # One inactive. Only active is implicit + assert len(_kb.get()) == 2 + + # one inactive RSA + assert len(_kb.get('RSA', only_active=False)) == 1 + assert len(_kb.get('RSA')) == 0 + + # 2 EC + assert len(_kb.get('EC')) == 2 + assert len(_kb.get('EC', only_active=False)) == 2 + + +def test_key_rollover(): + kb_0 = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + assert len(kb_0.get(only_active=False)) == 2 + assert len(kb_0.get()) == 2 + + kb_1 = key_rollover(kb_0) + + assert len(kb_1.get(only_active=False)) == 4 + assert len(kb_1.get()) == 2 + + +def test_build_key_bundle_sym(): + _kb = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + assert len(_kb) == 3 + + assert len(_kb.get('RSA')) == 0 + assert len(_kb.get('EC')) == 0 + assert len(_kb.get('oct')) == 3 + + +def test_key_bundle_difference_none(): + _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + _kb1 = KeyBundle(storage_conf=STORAGE_CONFIG) + _kb1.extend(_kb0.keys()) + + assert _kb0.difference(_kb1) == [] + + +def test_key_bundle_difference(): + _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + _kb1 = build_key_bundle(key_conf=KEYSPEC_2, storage_conf=STORAGE_CONFIG) + + assert _kb0.difference(_kb1) == _kb0.keys() + assert _kb1.difference(_kb0) == _kb1.keys() + + +def test_unique_keys_1(): + _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + _kb1 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + + keys = _kb0.keys() + keys.extend(_kb1.keys()) + + # All of them + assert len(unique_keys(keys)) == 6 + + +def test_unique_keys_2(): + _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + _kb1 = KeyBundle(storage_conf=STORAGE_CONFIG) + _kb1.extend(_kb0.keys()) + + keys = _kb0.keys() + keys.extend(_kb1.keys()) + + # 3 of 6 + assert len(unique_keys(keys)) == 3 + + +def test_key_gen_rsa(): + _jwk = key_gen("RSA", kid="kid1") + assert _jwk + assert _jwk.kty == "RSA" + assert _jwk.kid == 'kid1' + + assert isinstance(_jwk, RSAKey) + + +def test_init_key(): + spec = { + "type": "RSA", + "kid": "one" + } + + filename = full_path("../tests/tmp_jwk.json") + if os.path.isfile(filename): + os.unlink(filename) + + _key = init_key(filename, **spec) + assert _key.kty == "RSA" + assert _key.kid == 'one' + + assert os.path.isfile(filename) + + # Should not lead to any change + _jwk2 = init_key(filename, **spec) + assert _key == _jwk2 + + _jwk3 = init_key(filename, "RSA", "two") + assert _key != _jwk3 + + # Now _jwk3 is stored in the file + _jwk4 = init_key(filename, "RSA") + assert _jwk4 == _jwk3 + + +def test_export_inactive(): + desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) + assert len(kb.keys()) == 1 + for k in kb.keys(): + kb.mark_as_inactive(k.kid) + desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb.do_keys([desc]) + res = kb.dump() + assert set(res.keys()) == {'cache_time', + 'fileformat', + 'httpc_params', + 'imp_jwks', + 'keys', + 'last_updated', + 'remote', + 'time_out'} + + kb2 = KeyBundle(storage_conf=STORAGE_CONFIG).load(res) + assert len(kb2.keys()) == 2 + assert len(kb2.active_keys()) == 1 + + +def test_remote(): + source = 'https://example.com/keys.json' + # Mock response + with responses.RequestsMock() as rsps: + rsps.add(method="GET", url=source, json=JWKS_DICT, status=200) + httpc_params = {'timeout': (2, 2)} # connect, read timeouts in seconds + kb = KeyBundle(source=source, httpc=requests.request, + httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) + kb.do_remote() + + exp = kb.dump() + kb2 = KeyBundle(storage_conf=STORAGE_CONFIG).load(exp) + assert kb2.source == source + assert len(kb2.keys()) == 3 + assert len(kb2.get("rsa")) == 1 + assert len(kb2.get("oct")) == 1 + assert len(kb2.get("ec")) == 1 + assert kb2.httpc_params == {'timeout': (2, 2)} + assert kb2.imp_jwks + assert kb2.last_updated diff --git a/aslist_tests/test_44_key_issuer.py b/aslist_tests/test_44_key_issuer.py new file mode 100755 index 0000000..8bacecf --- /dev/null +++ b/aslist_tests/test_44_key_issuer.py @@ -0,0 +1,585 @@ +import os +import time + +import pytest +from abstorage.storages.absqlalchemy import AbstractStorageSQLAlchemy + +from cryptojwt.exception import JWKESTException +from cryptojwt.key_bundle import KeyBundle +from cryptojwt.key_bundle import keybundle_from_local_file +from cryptojwt.key_issuer import KeyIssuer +from cryptojwt.key_issuer import build_keyissuer + +__author__ = 'Roland Hedberg' + +BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + "test_keys")) +RSAKEY = os.path.join(BASE_PATH, "cert.key") +RSA0 = os.path.join(BASE_PATH, "rsa.key") +EC0 = os.path.join(BASE_PATH, "ec.key") +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +def full_path(local_file): + return os.path.join(BASEDIR, local_file) + + +JWK0 = { + "keys": [ + { + 'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", + 'n': + 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5' + 'B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8' + } + ] +} + +JWK1 = { + "keys": [ + { + "n": + "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" + "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" + "-NvS-aG_jN5cstVbCGWE20H0vF" + "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" + "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" + "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": "AQAB", "kty": "RSA", "kid": "rsa1" + }, + { + "k": + "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "oct" + }, + ] +} + +JWK2 = { + "keys": [ + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", + "kid": "kriMPdmBvx68skT8-mPAB3BseeA", + "kty": "RSA", + "n": + "kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1um" + "l94fLyq_AXbwo9yAduf4dCHTP8CWR1dnDR" + "-Qnz_4PYlWVEuuHHONOw_blbfdMjhY" + "-C_BYM2E3pRxbohBb3x__CfueV7ddz2LYiH3" + "wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_GTgWN8A" + "-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQt" + "SsjQtI--XsokxRDqYLwus1I1SihgbV_STTg5enufuw", + "use": "sig", + "x5c": [ + "MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb" + "2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb2" + "50cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipg" + "H0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6" + "/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Q" + "nz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x" + "//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13S" + "QwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp" + "+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5en" + "ufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJ" + "vbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdN" + "VGKCmSf8M65b8h0NwlIjGGGy" + "/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADD" + "kN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5" + "+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8y" + "PJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW" + "+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ" + ], + "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", + "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", + "kty": "RSA", + "n": + "vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhzh23V9Tkq" + "-RtwN1Vs_z57hO82kkzL-cQHZX3bMJ" + "D-GEGOKXCEXURN7VMyZWMAuzQoW9vFb1k3cR1RW_EW_P" + "-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5yCw5T_Vuwqqsio3" + "V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_KAS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IA" + "bsk1wRtWDndhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ", + "use": "sig", + "x5c": [ + "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" + "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" + "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" + "/uPhEf7zVizjfcr/ISGFe9+yUO" + "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" + "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" + "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" + "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" + "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" + "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" + "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" + "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" + "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" + "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" + "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" + "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" + "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" + "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" + ], + "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" + "-b112-36a304b66dad/v2.0/", + "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", + "kty": "RSA", + "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" + "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" + "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", + "use": "sig", + "x5c": [ + "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" + "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" + "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" + "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" + "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" + "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" + "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" + "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" + "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" + "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" + "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" + "+Iqzmn" + "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" + "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" + "qA==" + ], + "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" + "-b112-36a304b66dad/v2.0/", + "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", + "kty": "RSA", + "n": + "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" + "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" + "-KXVD02jdmX2HNdYJdPyc0c", + "use": "sig", + "x5c": [ + "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" + "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" + "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" + "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" + "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" + "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" + "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" + "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" + "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" + "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" + "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" + "ig==" + ], + "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" + } + ] +} + +ABS_STORAGE_SQLALCHEMY = dict( + driver='sqlalchemy', + url='sqlite:///:memory:', + params=dict(table='Thing'), + handler=AbstractStorageSQLAlchemy +) + +STORAGE_CONFIG = { + 'KeyIssuer': { + 'name': '', + 'class': 'abstorage.type.list.ASList', + 'kwargs': { + 'io_class': 'cryptojwt.serialize.item.KeyBundle', + 'storage_config': ABS_STORAGE_SQLALCHEMY + } + }, + 'KeyBundle': { + 'name': '', + 'class': 'abstorage.type.list.ASList', + 'kwargs': { + 'io_class': 'cryptojwt.serialize.item.JWK', + 'storage_config': ABS_STORAGE_SQLALCHEMY + } + } +} + + +def test_build_key_issuer(): + keys = [ + {"type": "RSA", "use": ["enc", "sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + ] + + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + + assert len(key_issuer) == 3 # A total of 3 keys + assert len(key_issuer.get('sig')) == 2 # 2 for signing + assert len(key_issuer.get('enc')) == 1 # 1 for encryption + + +def test_build_keyissuer_usage(): + keys = [ + {"type": "RSA", "use": ["enc", "sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "oct", "use": ["enc"]}, + {"type": "oct", "use": ["enc"]}, + ] + + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + jwks_sig = key_issuer.export_jwks(usage='sig') + jwks_enc = key_issuer.export_jwks(usage='enc') + assert len(jwks_sig.get('keys')) == 2 # A total of 2 keys with use=sig + assert len(jwks_enc.get('keys')) == 3 # A total of 3 keys with use=enc + + for key in jwks_sig["keys"]: + assert "d" not in key # the JWKS shouldn't contain the private part of the keys + for key in jwks_enc["keys"]: + assert "d" not in key # the JWKS shouldn't contain the private part of the keys + + +def test_build_keyissuer_missing(tmpdir): + keys = [ + { + "type": "RSA", "key": os.path.join(tmpdir.dirname, "missing_file"), + "use": ["enc", "sig"] + }] + + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + assert key_issuer is None + + +def test_build_RSA_keyissuer_from_file(tmpdir): + keys = [{"type": "RSA", "key": RSA0, "use": ["enc", "sig"]}] + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + assert len(key_issuer) == 2 + + +def test_build_EC_keyissuer_missing(tmpdir): + keys = [ + { + "type": "EC", "key": os.path.join(tmpdir.dirname, "missing_file"), + "use": ["enc", "sig"] + }] + + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + assert key_issuer is None + + +def test_build_EC_keyissuer_from_file(tmpdir): + keys = [ + { + "type": "EC", "key": EC0, + "use": ["enc", "sig"] + }] + + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + + assert len(key_issuer) == 2 + + +class TestKeyIssuer(object): + def test_add_kb(self): + issuer = KeyIssuer(name='https://issuer.example.com', storage_conf=STORAGE_CONFIG) + kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG) + issuer.add_kb(kb) + assert len(issuer.all_keys()) == 1 + + def test_add_symmetric(self): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.add_symmetric('abcdefghijklmnop', ['sig']) + assert len(issuer.get('oct')) == 1 + + def test_add(self): + issuer = KeyIssuer(name='https://issuer.example.com', storage_conf=STORAGE_CONFIG) + kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG) + issuer.add(kb) + assert len(issuer.all_keys()) == 1 + + def test_items(self): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.add(KeyBundle( + [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, + {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}]), storage_conf=STORAGE_CONFIG) + issuer.add( + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG)) + + assert len(issuer.items()) == 2 + + def test_get_enc(self): + issuer = KeyIssuer() + issuer.add(KeyBundle( + [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, + {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}]), storage_conf=STORAGE_CONFIG) + issuer.add( + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG)) + + assert issuer.get('enc', 'oct') + + def test_dump_issuer_keys(self): + kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", + ["sig"], storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.add(kb) + _jwks_dict = issuer.export_jwks() + + _info = _jwks_dict['keys'][0] + assert _info == { + 'use': 'sig', + 'e': 'AQAB', + 'kty': 'RSA', + 'alg': 'RS256', + 'n': 'pKybs0WaHU_y4cHxWbm8Wzj66HtcyFn7Fh3n' + '-99qTXu5yNa30MRYIYfSDwe9JVc1JUoGw41yq2StdGBJ40HxichjE' + '-Yopfu3B58Q' + 'lgJvToUbWD4gmTDGgMGxQxtv1En2yedaynQ73sDpIK-12JJDY55pvf' + '-PCiSQ9OjxZLiVGKlClDus44_uv2370b9IN2JiEOF-a7JB' + 'qaTEYLPpXaoKWDSnJNonr79tL0T7iuJmO1l705oO3Y0TQ' + '-INLY6jnKG_RpsvyvGNnwP9pMvcP1phKsWZ10ofuuhJGRp8IxQL9Rfz' + 'T87OvF0RBSO1U73h09YP-corWDsnKIi6TbzRpN5YDw', + 'kid': 'abc' + } + + def test_no_use(self): + kb = KeyBundle(JWK0["keys"]) + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.add(kb) + enc_key = issuer.get('enc', "RSA") + assert enc_key != [] + + @pytest.mark.network + def test_provider(self): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.load_keys(jwks_uri="https://connect-op.herokuapp.com/jwks.json") + + assert issuer.all_keys() + + +def test_import_jwks(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWK1) + assert len(issuer.all_keys()) == 2 + + +def test_get_signing_key_use_undefined(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWK1) + keys = issuer.get('sig', kid='rsa1') + assert len(keys) == 1 + + keys = issuer.get('sig', key_type='rsa') + assert len(keys) == 1 + + keys = issuer.get('sig', key_type='rsa', kid='rsa1') + assert len(keys) == 1 + + +KEYDEFS = [ + {"type": "RSA", "key": '', "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]} +] + + +def test_remove_after(): + # initial key_issuer + key_issuer = build_keyissuer(KEYDEFS, storage_conf=STORAGE_CONFIG) + _old = [k.kid for k in key_issuer.all_keys() if k.kid] + assert len(_old) == 2 + + # rotate_keys = create new keys + make the old as inactive + key_issuer = build_keyissuer(KEYDEFS, key_issuer=key_issuer) + + key_issuer.remove_after = 1 + # None are remove since none are marked as inactive yet + key_issuer.remove_outdated() + + _interm = [k.kid for k in key_issuer.all_keys() if k.kid] + assert len(_interm) == 4 + + # Now mark the keys to be inactivated + _now = time.time() + for kid in _old: + key_issuer.mark_as_inactive(kid) + + key_issuer.remove_outdated(_now + 5) + + # The remainder are the new keys + _new = [k.kid for k in key_issuer.all_keys() if k.kid] + assert len(_new) == 2 + + # should not be any overlap between old and new + assert set(_new).intersection(set(_old)) == set() + + +JWK_UK = { + "keys": [ + { + "n": + "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" + "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" + "-NvS-aG_jN5cstVbCGWE20H0vF" + "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" + "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" + "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": "AQAB", "kty": "RSA", "kid": "rsa1" + }, + { + "k": + "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "buz" + }, + ] +} + + +def test_load_unknown_keytype(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWK_UK) + assert len(issuer.all_keys()) == 1 + + +JWK_FP = { + "keys": [ + {"e": "AQAB", "kty": "RSA", "kid": "rsa1"}, + ] +} + + +def test_load_missing_key_parameter(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + with pytest.raises(JWKESTException): + issuer.import_jwks(JWK_FP) + + +JWKS_SPO = { + "keys": [ + { + "kid": + "BfxfnahEtkRBG3Hojc9XGLGht_5rDBj49Wh3sBDVnzRpulMqYwMRmpizA0aSPT1fhCHYivTiaucWUqFu_GwTqA", + "use": "sig", + "alg": "ES256", + "kty": "EC", + "crv": "P-256", + "x": "1XXUXq75gOPZ4bEj1o2Z5XKJWSs6LmL6fAOK3vyMzSc", + "y": "ac1h_DwyuUxhkrD9oKMJ-b_KuiVvvSARIwT-XoEmDXs" + }, + { + "kid": + "91pD1H81rXUvrfg9mkngIG-tXjnldykKUVbITDIU1SgJvq91b8clOcJuEHNAq61eIvg8owpEvWcWAtlbV2awyA", + "use": "sig", + "alg": "ES256", + "kty": "EC", + "crv": "P-256", + "x": "2DfQoLpZS2j3hHEcHDkzV8ISx-RdLt6Opy8YZYVm4AQ", + "y": "ycvkFMBIzgsowiaf6500YlG4vaMSK4OF7WVtQpUbEE0" + }, + { + "kid": "0sIEl3MUJiCxrqleEBBF-_bZq5uClE84xp-wpt8oOI" + "-WIeNxBjSR4ak_OTOmLdndB0EfDLtC7X1JrnfZILJkxA", + "use": "sig", + "alg": "RS256", + "kty": "RSA", + "n": + "yG9914Q1j63Os4jX5dBQbUfImGq4zsXJD4R59XNjGJlEt5ek6NoiDl0ucJO3_7_R9e5my2ONTSqZhtzFW6MImnIn8idWYzJzO2EhUPCHTvw_2oOGjeYTE2VltIyY_ogIxGwY66G0fVPRRH9tCxnkGOrIvmVgkhCCGkamqeXuWvx9MCHL_gJbZJVwogPSRN_SjA1gDlvsyCdA6__CkgAFcSt1sGgiZ_4cQheKexxf1-7l8R91ZYetz53drk2FS3SfuMZuwMM4KbXt6CifNhzh1Ye-5Tr_ZENXdAvuBRDzfy168xnk9m0JBtvul9GoVIqvCVECB4MPUb7zU6FTIcwRAw", + "e": "AQAB" + }, + { + "kid": + "zyDfdEU7pvH0xEROK156ik8G7vLO1MIL9TKyL631kSPtr9tnvs9XOIiq5jafK2hrGr2qqvJdejmoonlGqWWZRA", + "use": "sig", + "alg": "RS256", + "kty": "RSA", + "n": + "68be-nJp46VLj4Ci1V36IrVGYqkuBfYNyjQTZD_7yRYcERZebowOnwr3w0DoIQpl8iL2X8OXUo7rUW_LMzLxKx2hEmdJfUn4LL2QqA3KPgjYz8hZJQPG92O14w9IZ-8bdDUgXrg9216H09yq6ZvJrn5Nwvap3MXgECEzsZ6zQLRKdb_R96KFFgCiI3bEiZKvZJRA7hM2ePyTm15D9En_Wzzfn_JLMYgE_DlVpoKR1MsTinfACOlwwdO9U5Dm-5elapovILTyVTgjN75i-wsPU2TqzdHFKA-4hJNiWGrYPiihlAFbA2eUSXuEYFkX43ahoQNpeaf0mc17Jt5kp7pM2w", + "e": "AQAB" + }, + { + "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" + "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", + "use": "sig", + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" + }, + { + "kid": + "bL33HthM3fWaYkY2_pDzUd7a65FV2R2LHAKCOsye8eNmAPDgRgpHWPYpWFVmeaujUUEXRyDLHN" + "-Up4QH_sFcmw", + "use": "sig", + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "CS01DGXDBPV9cFmd8tgFu3E7eHn1UcP7N1UCgd_JgZo" + } + ] +} + + +def test_load_spomky_keys(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWKS_SPO) + assert len(issuer.all_keys()) == 4 + + +def test_get_ec(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWKS_SPO) + k = issuer.get('sig', 'EC', alg='ES256') + assert k + + +def test_get_ec_wrong_alg(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWKS_SPO) + k = issuer.get('sig', 'EC', alg='ES512') + assert k == [] + + +def test_keys_by_alg_and_usage(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWKS_SPO) + k = issuer.get('sig', alg='RS256') + assert len(k) == 2 + + +def test_copy(): + issuer = KeyIssuer('Alice', storage_conf=STORAGE_CONFIG) + issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) + issuer_copy = issuer.copy() + + assert len(issuer_copy.get('sig', 'oct')) == 0 + assert len(issuer_copy.get('sig', 'rsa')) == 1 + + +def test_repr(): + issuer = KeyIssuer('Alice', storage_conf=STORAGE_CONFIG) + issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) + txt = issuer.__repr__() + assert " str: + return ''.format(self._bundles) + + def __getitem__(self, item): + return self.get_bundles()[item] + + def set(self, items): + self._bundles.set(items) + + def get_bundles(self): + return [kb for kb in self._bundles] + + def add_url(self, url, **kwargs): + """ + Add a set of keys by url. This method will create a + :py:class:`oidcmsg.key_bundle.KeyBundle` instance with the + url as source specification. If no file format is given it's assumed + that what's on the other side is a JWKS. + + :param issuer: Who issued the keys + :param url: Where can the key/-s be found + :param kwargs: extra parameters for instantiating KeyBundle + :return: A :py:class:`oidcmsg.oauth2.keybundle.KeyBundle` instance + """ + + if not url: + raise KeyError("No url given") + + if "/localhost:" in url or "/localhost/" in url: + _params = self.httpc_params.copy() + _params['verify'] = False + kb = self.keybundle_cls(source=url, httpc=self.httpc, httpc_params=_params, + storage_conf=self.storage_conf, **kwargs) + else: + kb = self.keybundle_cls(source=url, httpc=self.httpc, httpc_params=self.httpc_params, + storage_conf=self.storage_conf, **kwargs) + + kb.update() + self._bundles.append(kb) + + return kb + + def add_symmetric(self, key, usage=None): + """ + Add a symmetric key. This is done by wrapping it in a key bundle + cloak since KeyJar does not handle keys directly but only through + key bundles. + + :param key: The key + :param usage: What the key can be used for signing/signature + verification (sig) and/or encryption/decryption (enc) + """ + + if usage is None: + self._bundles.append(self.keybundle_cls([{"kty": "oct", "key": key}])) + else: + for use in usage: + self._bundles.append(self.keybundle_cls([{"kty": "oct", "key": key, "use": use}])) + + def add_kb(self, kb): + """ + Add a key bundle. + + :param kb: A :py:class:`oidcmsg.key_bundle.KeyBundle` instance + """ + self._bundles.append(kb) + + def add(self, item, **kwargs): + if isinstance(item, KeyBundle): + self.add_kb(item) + elif item.startswith('http://') or item.startswith('file://') or item.startswith( + 'https://'): + self.add_url(item, **kwargs) + else: + self.add_symmetric(item, **kwargs) + + def all_keys(self): + """ + Get all the keys that belong to an entity. + + :return: A possibly empty list of keys + """ + res = [] + for kb in self._bundles: + res.extend(kb.keys()) + return res + + def __contains__(self, item): + for kb in self._bundles: + if item in kb: + return True + return False + + def items(self): + _res = {} + for kb in self._bundles: + if kb.source in _res: + _res[kb.source].append(kb) + else: + _res[kb.source] = [kb] + return _res + + def __str__(self): + _res = {} + for kb in self._bundles: + key_list = [] + for key in kb.keys(): + if key.inactive_since: + key_list.append( + '*{}:{}:{}'.format(key.kty, key.use, key.kid)) + else: + key_list.append( + '{}:{}:{}'.format(key.kty, key.use, key.kid)) + if kb.source in _res: + _res[kb.source] += ', ' + ', '.join(key_list) + else: + _res[kb.source] = ', '.join(key_list) + return json.dumps(_res) + + def load_keys(self, jwks_uri='', jwks=None): + """ + Fetch keys from another server + + :param jwks_uri: A URL pointing to a site that will return a JWKS + :param jwks: A dictionary representation of a JWKS + :return: Dictionary with usage as key and keys as values + """ + + if jwks_uri: + self.add_url(jwks_uri) + elif jwks: + # jwks should only be considered if no jwks_uri is present + _keys = jwks['keys'] + self._bundles.append(self.keybundle_cls(_keys)) + + def find(self, source): + """ + Find a key bundle based on the source of the keys + + :param source: A source url + :return: A list of :py:class:`oidcmsg.key_bundle.KeyBundle` instances, possibly empty + """ + return [kb for kb in self._bundles if kb.source == source] + + def export_jwks(self, private=False, usage=None): + """ + Produces a dictionary that later can be easily mapped into a + JSON string representing a JWKS. + + :param private: Whether it should be the private keys or the public + :param usage: If only keys for a special usage should be included + :return: A dictionary with one key: 'keys' + """ + keys = [] + for kb in self._bundles: + keys.extend([k.serialize(private) for k in kb.keys() if + k.inactive_since == 0 and ( + usage is None or (hasattr(k, 'use') and k.use == usage))]) + return {"keys": keys} + + def export_jwks_as_json(self, private=False, usage=None): + """ + Export a JWKS as a JSON document. + + :param private: Whether it should be the private keys or the public + :return: A JSON representation of a JWKS + """ + return json.dumps(self.export_jwks(private, usage=usage)) + + def import_jwks(self, jwks): + """ + Imports all the keys that are represented in a JWKS + + :param jwks: Dictionary representation of a JWKS + """ + try: + _keys = jwks["keys"] + except KeyError: + raise ValueError('Not a proper JWKS') + else: + self._bundles.append( + self.keybundle_cls(_keys, httpc=self.httpc, httpc_params=self.httpc_params)) + + def import_jwks_as_json(self, jwks, issuer): + """ + Imports all the keys that are represented in a JWKS expressed as a + JSON object + + :param jwks: JSON representation of a JWKS + :param issuer: Who 'owns' the JWKS + """ + return self.import_jwks(json.loads(jwks)) + + def import_jwks_from_file(self, filename, issuer): + with open(filename) as jwks_file: + self.import_jwks_as_json(jwks_file.read(), issuer) + + def remove_outdated(self, when=0): + """ + Goes through the complete list of issuers and for each of them removes + outdated keys. + Outdated keys are keys that has been marked as inactive at a time that + is longer ago then some set number of seconds (when). If when=0 the + the base time is set to now. + The number of seconds are carried in the remove_after parameter in the + key jar. + + :param when: To facilitate testing + """ + kbl = [] + changed = False + for kb in self._bundles: + if kb.remove_outdated(self.remove_after, when=when): + changed = True + kbl.append(kb) + if changed: + self._bundles.set(kbl) + + def get(self, key_use, key_type="", kid=None, alg='', **kwargs): + """ + Get all keys that matches a set of search criteria + + :param key_use: A key useful for this usage (enc, dec, sig, ver) + :param key_type: Type of key (rsa, ec, oct, ..) + :param kid: A Key Identifier + :return: A possibly empty list of keys + """ + + if key_use in ["dec", "enc"]: + use = "enc" + else: + use = "sig" + + if not key_type: + if alg: + if use == 'sig': + key_type = jws_alg2keytype(alg) + else: + key_type = jwe_alg2keytype(alg) + + lst = [] + for bundle in self._bundles: + if key_type: + if key_use in ['ver', 'dec']: + _bkeys = bundle.get(key_type, only_active=False) + else: + _bkeys = bundle.get(key_type) + else: + _bkeys = bundle.keys() + for key in _bkeys: + if key.inactive_since and key_use != "sig": + # Skip inactive keys unless for signature verification + continue + if not key.use or use == key.use: + if kid: + if key.kid == kid: + lst.append(key) + break + else: + continue + else: + lst.append(key) + + # If key algorithm is defined only return keys that can be used. + if alg: + lst = [key for key in lst if not key.alg or key.alg == alg] + + # if elliptic curve, have to check if I have a key of the right curve + if key_type == "EC" and "alg" in kwargs: + name = "P-{}".format(kwargs["alg"][2:]) # the type + _lst = [] + for key in lst: + if name != key.crv: + continue + _lst.append(key) + lst = _lst + + return lst + + def copy(self): + """ + Make deep copy of this key jar. + + :return: A :py:class:`oidcmsg.key_jar.KeyJar` instance + """ + ki = KeyIssuer() + ki._bundles = [kb.copy() for kb in self._bundles] + ki.httpc_params = self.httpc_params + ki.httpc = self.httpc + ki.storage_conf = self.storage_conf + ki.keybundle_cls = self.keybundle_cls + return ki + + def __len__(self): + nr = 0 + for kb in self._bundles: + nr += len(kb) + return nr + + def dump(self, exclude=None): + """ + Returns the key issuer content as a dictionary. + + :return: A dictionary + """ + + _bundles = [] + for kb in self._bundles: + _bundles.append(kb.dump()) + + info = { + 'name': self.name, + 'bundles': _bundles, + # 'storage_conf': self.storage_conf, + 'keybundle_cls': qualified_name(self.keybundle_cls), + 'spec2key': self.spec2key, + 'ca_certs': self.ca_certs, + 'remove_after': self.remove_after, + 'httpc_params': self.httpc_params + } + return info + + def load(self, info): + """ + + :param items: A list with the information + :return: + """ + self.name = info['name'] + # self.storage_conf = info['storage_conf'] + self.keybundle_cls = importer(info['keybundle_cls']) + self.spec2key = info['spec2key'] + self.ca_certs = info['ca_certs'] + self.remove_after = info['remove_after'] + self.httpc_params = info['httpc_params'] + self._bundles = [KeyBundle(storage_conf=self.storage_conf).load(val) for val in + info['bundles']] + return self + + def update(self): + for kb in self._bundles: + kb.update() + + def mark_as_inactive(self, kid): + kbl = [] + changed = False + for kb in self._bundles: + if kb.mark_as_inactive(kid): + changed = True + kbl.append(kb) + if changed: + self._bundles.set(kbl) + + def mark_all_keys_as_inactive(self): + kbl = [] + for kb in self._bundles: + kb.mark_all_as_inactive() + kbl.append(kb) + + self._bundles.set(kbl) + + def key_summary(self): + """ + Return a text representation of all the keys. + + :return: A text representation of the keys + """ + key_list = [] + for kb in self._bundles: + for key in kb.keys(): + if key.inactive_since: + key_list.append( + '*{}:{}:{}'.format(key.kty, key.use, key.kid)) + else: + key_list.append( + '{}:{}:{}'.format(key.kty, key.use, key.kid)) + return ', '.join(key_list) + + def __iter__(self): + for bundle in self._bundles: + yield bundle + + +# ============================================================================= + + +def build_keyissuer(key_conf, kid_template="", key_issuer=None, storage_conf=None, + issuer_id=''): + """ + Builds a :py:class:`oidcmsg.key_issuer.KeyIssuer` instance or adds keys to + an existing KeyIssuer instance based on a key specification. + + An example of such a specification:: + + keys = [ + {"type": "RSA", "key": "cp_keys/key.pem", "use": ["enc", "sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"], "kid": "ec.1"}, + {"type": "EC", "crv": "P-256", "use": ["enc"], "kid": "ec.2"} + {"type": "oct", "bytes": 32, "use":["sig"]} + ] + + Keys in this specification are: + + type + The type of key. Presently only 'rsa', 'oct' and 'ec' supported. + + key + A name of a file where a key can be found. Works with PEM encoded + RSA and EC private keys. + + use + What the key should be used for + + crv + The elliptic curve that should be used. Only applies to elliptic curve + keys :-) + + kid + Key ID, can only be used with one usage type is specified. If there + are more the one usage type specified 'kid' will just be ignored. + + :param key_conf: The key configuration + :param kid_template: A template by which to build the key IDs. If no + kid_template is given then the built-in function add_kid() will be used. + :param key_issuer: If an keyIssuer instance the new keys are added to this key issuer. + :param storage_conf: + :return: A KeyIssuer instance + """ + + bundle = build_key_bundle(key_conf, kid_template, storage_conf=storage_conf) + if bundle is None: + return None + + if key_issuer is None: + key_issuer = KeyIssuer(name=issuer_id, storage_conf=storage_conf) + + key_issuer.add(bundle) + + return key_issuer diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 69962f2..9f96351 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -1,21 +1,23 @@ import json import logging import os +from typing import List +from abstorage.utils import importer +from abstorage.utils import init_storage +from abstorage.utils import qualified_name from requests import request from .jwe.jwe import alg2keytype as jwe_alg2keytype from .jws.utils import alg2keytype as jws_alg2keytype from .key_bundle import KeyBundle -from .key_bundle import build_key_bundle from .key_bundle import key_diff from .key_bundle import update_key_bundle __author__ = 'Roland Hedberg' -KEYLOADERR = "Failed to load %s key from '%s' (%s)" -REMOTE_FAILED = "Remote key update from '{}' failed, HTTP status {}" -MALFORMED = "Remote key update from {} failed, malformed JWKS." +from .key_issuer import KeyIssuer +from .key_issuer import build_keyissuer logger = logging.getLogger(__name__) @@ -36,7 +38,7 @@ class KeyJar(object): """ A keyjar contains a number of KeyBundles sorted by owner/issuer """ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, - remove_after=3600, httpc=None, httpc_params=None): + remove_after=3600, httpc=None, httpc_params=None, storage_conf=None): """ KeyJar init function @@ -48,8 +50,11 @@ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, :param httpc_params: HTTP request parameters :return: Keyjar instance """ + + self._issuers = init_storage(storage_conf, self.__class__.__name__) + + self.storage_conf = storage_conf self.spec2key = {} - self.issuer_keys = {} self.ca_certs = ca_certs self.keybundle_cls = keybundle_cls self.remove_after = remove_after @@ -57,14 +62,53 @@ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, self.httpc_params = httpc_params or {} # Now part of httpc_params # self.verify_ssl = verify_ssl - if not self.httpc_params: # backward compatibility + if not self.httpc_params: # backward compatibility self.httpc_params["verify"] = verify_ssl + def _issuer_ids(self) -> List[str]: + """ + Returns a list of issuer identifiers + + :return: + """ + return [i.name for i in self._issuers] + + def _get_issuer(self, issuer_id): + """ + Return the KeyIssuer instance that has name == issuer_id + + :param issuer_id: The issuer identifiers + :return: A KeyIssuer instance or None + """ + _i = [i for i in self._issuers if i.name == issuer_id] + if _i: + return _i[0] # should only be one + else: + return None + def __repr__(self): - issuers = list(self.issuer_keys.keys()) + issuers = self._issuer_ids() return ''.format(issuers) - def add_url(self, issuer, url, **kwargs): + def return_issuer(self, issuer_id): + """ + Return a KeyIssuer instance with name == issuer_id. + If none such was already initiated, create one. + + :param issuer_id: The issuer ID + :return: A KeyIssuer instance + """ + _iss = self._get_issuer(issuer_id) + if not _iss: + _iss = KeyIssuer(ca_certs=self.ca_certs, name=issuer_id, + keybundle_cls=self.keybundle_cls, + remove_after=self.remove_after, + httpc=self.httpc, httpc_params=self.httpc_params, + storage_conf=self.storage_conf) + self._issuers.append(_iss) + return _iss + + def add_url(self, issuer_id, url, **kwargs): """ Add a set of keys by url. This method will create a :py:class:`oidcmsg.key_bundle.KeyBundle` instance with the @@ -77,24 +121,11 @@ def add_url(self, issuer, url, **kwargs): :return: A :py:class:`oidcmsg.oauth2.keybundle.KeyBundle` instance """ - if not url: - raise KeyError("No url given") - - if "/localhost:" in url or "/localhost/" in url: - _params = self.httpc_params.copy() - _params['verify'] = False - kb = self.keybundle_cls(source=url, httpc=self.httpc, - httpc_params=_params, **kwargs) - else: - kb = self.keybundle_cls(source=url, httpc=self.httpc, - httpc_params=self.httpc_params, **kwargs) - - kb.update() - self.add_kb(issuer, kb) - + issuer = self.return_issuer(issuer_id) + kb = issuer.add_url(url, **kwargs) return kb - def add_symmetric(self, issuer, key, usage=None): + def add_symmetric(self, issuer_id, key, usage=None): """ Add a symmetric key. This is done by wrapping it in a key bundle cloak since KeyJar does not handle keys directly but only through @@ -105,61 +136,52 @@ def add_symmetric(self, issuer, key, usage=None): :param usage: What the key can be used for signing/signature verification (sig) and/or encryption/decryption (enc) """ - if issuer not in self.issuer_keys: - self.issuer_keys[issuer] = [] + issuer = self.return_issuer(issuer_id) + issuer.add_symmetric(key, usage=usage) - if usage is None: - self.issuer_keys[issuer].append( - self.keybundle_cls([{"kty": "oct", "key": key}])) - else: - for use in usage: - self.issuer_keys[issuer].append( - self.keybundle_cls([{"kty": "oct", "key": key, "use": use}])) - - def add_kb(self, issuer, kb): + def add_kb(self, issuer_id, kb): """ Add a key bundle and bind it to an identifier - :param issuer: Owner of the keys in the key bundle + :param issuer_id: Owner of the keys in the key bundle :param kb: A :py:class:`oidcmsg.key_bundle.KeyBundle` instance """ - try: - self.issuer_keys[issuer].append(kb) - except KeyError: - self.issuer_keys[issuer] = [kb] - - def __setitem__(self, issuer, val): - """ - Bind one or a list of key bundles to a special identifier. - Will overwrite whatever was there before !! - - :param issuer: The owner of the keys in the key bundle/-s - :param val: A single or a list of KeyBundle instance - """ - if not isinstance(val, list): - val = [val] - - for kb in val: - if not isinstance(kb, KeyBundle): - raise ValueError('{} not an KeyBundle instance'.format(kb)) - - self.issuer_keys[issuer] = val + issuer = self.return_issuer(issuer_id) + issuer.add_kb(kb) + + # def __setitem__(self, issuer_id, val): + # """ + # Bind one or a list of key bundles to a special identifier. + # Will overwrite whatever was there before !! + # + # :param issuer_id: The owner of the keys in the key bundle/-s + # :param val: A single or a list of KeyBundle instance + # """ + # if not isinstance(val, list): + # val = [val] + # + # for kb in val: + # if not isinstance(kb, KeyBundle): + # raise ValueError('{} not an KeyBundle instance'.format(kb)) + # + # issuer = self.return_issuer(issuer_id) + # issuer.set(val) def items(self): """ - Get all owner ID's and their key bundles + Get all owner ID's and their keys :return: list of 2-tuples (Owner ID., list of KeyBundles) """ - return self.issuer_keys.items() + return [(i.name, i.get_bundles()) for i in self._issuers] - def get(self, key_use, key_type="", owner="", kid=None, **kwargs): + def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs): """ Get all keys that matches a set of search criteria :param key_use: A key useful for this usage (enc, dec, sig, ver) :param key_type: Type of key (rsa, ec, oct, ..) - :param owner: Who is the owner of the keys, "" == me (default) + :param issuer_id: Who is the owner of the keys, "" == me (default) :param kid: A Key Identifier :return: A possibly empty list of keys """ @@ -169,32 +191,22 @@ def get(self, key_use, key_type="", owner="", kid=None, **kwargs): else: use = "sig" - _kj = None - if owner != "": - try: - _kj = self.issuer_keys[owner] - except KeyError: - if owner.endswith("/"): - try: - _kj = self.issuer_keys[owner[:-1]] - except KeyError: - pass + _issuer = None + if issuer_id != "": + _issuer = self._get_issuer(issuer_id) + if _issuer is None: + if issuer_id.endswith("/"): + _issuer = self._get_issuer(issuer_id[:-1]) else: - try: - _kj = self.issuer_keys[owner + "/"] - except KeyError: - pass + _issuer = self._get_issuer(issuer_id + "/") else: - try: - _kj = self.issuer_keys[owner] - except KeyError: - pass + _issuer = self._get_issuer(issuer_id) - if _kj is None: + if _issuer is None: return [] lst = [] - for bundle in _kj: + for bundle in _issuer: if key_type: if key_use in ['ver', 'dec']: _bkeys = bundle.get(key_type, only_active=False) @@ -226,44 +238,46 @@ def get(self, key_use, key_type="", owner="", kid=None, **kwargs): _lst.append(key) lst = _lst - if use == 'enc' and key_type == 'oct' and owner != '': + if use == 'enc' and key_type == 'oct' and issuer_id != '': # Add my symmetric keys - for kb in self.issuer_keys['']: - for key in kb.get(key_type): - if key.inactive_since: - continue - if not key.use or key.use == use: - lst.append(key) + _issuer = self._get_issuer('') + if _issuer: + for kb in _issuer: + for key in kb.get(key_type): + if key.inactive_since: + continue + if not key.use or key.use == use: + lst.append(key) return lst - def get_signing_key(self, key_type="", owner="", kid=None, **kwargs): + def get_signing_key(self, key_type="", issuer_id="", kid=None, **kwargs): """ Shortcut to use for signing keys only. :param key_type: Type of key (rsa, ec, oct, ..) - :param owner: Who is the owner of the keys, "" == me (default) + :param issuer_id: Who is the owner of the keys, "" == me (default) :param kid: A Key Identifier :param kwargs: Extra key word arguments :return: A possibly empty list of keys """ - return self.get("sig", key_type, owner, kid, **kwargs) + return self.get("sig", key_type, issuer_id, kid, **kwargs) - def get_verify_key(self, key_type="", owner="", kid=None, **kwargs): - return self.get("ver", key_type, owner, kid, **kwargs) + def get_verify_key(self, key_type="", issuer_id="", kid=None, **kwargs): + return self.get("ver", key_type, issuer_id, kid, **kwargs) - def get_encrypt_key(self, key_type="", owner="", kid=None, **kwargs): - return self.get("enc", key_type, owner, kid, **kwargs) + def get_encrypt_key(self, key_type="", issuer_id="", kid=None, **kwargs): + return self.get("enc", key_type, issuer_id, kid, **kwargs) - def get_decrypt_key(self, key_type="", owner="", kid=None, **kwargs): - return self.get("dec", key_type, owner, kid, **kwargs) + def get_decrypt_key(self, key_type="", issuer_id="", kid=None, **kwargs): + return self.get("dec", key_type, issuer_id, kid, **kwargs) - def keys_by_alg_and_usage(self, issuer, alg, usage): + def keys_by_alg_and_usage(self, issuer_id, alg, usage): """ Find all keys that can be used for a specific crypto algorithm and usage by key owner. - :param issuer: Key owner + :param issuer_id: Key owner :param alg: a crypto algorithm :param usage: What the key should be used for :return: A possibly empty list of keys @@ -273,40 +287,35 @@ def keys_by_alg_and_usage(self, issuer, alg, usage): else: ktype = jwe_alg2keytype(alg) - return self.get(usage, ktype, issuer) + return self.get(usage, ktype, issuer_id) - def get_issuer_keys(self, issuer): + def get_issuer_keys(self, issuer_id): """ Get all the keys that belong to an entity. :param issuer: The entity ID :return: A possibly empty list of keys """ - res = [] - for kbl in self.issuer_keys[issuer]: - res.extend(kbl.keys()) - return res + _issuer = self._get_issuer(issuer_id) + if _issuer: + return _issuer.all_keys() + else: + return [] - def __contains__(self, item): - if item in self.issuer_keys: + def __contains__(self, issuer_id): + if self._get_issuer(issuer_id): return True else: return False - def __getitem__(self, owner=''): + def __getitem__(self, issuer_id=''): """ - Get all the key bundles that belong to an entity. + Get all the KeyIssuer with the name == issuer_id - :param owner: The entity ID - :return: A possibly empty list of key bundles + :param issuer_id: The entity ID + :return: A KeyIssuer instance """ - try: - return self.issuer_keys[owner] - except KeyError: - logger.debug( - "Owner '{}' not found, available key owners: {}".format( - owner, list(self.issuer_keys.keys()))) - raise + return self._get_issuer(issuer_id) def owners(self): """ @@ -314,7 +323,7 @@ def owners(self): :return: A list of entity IDs """ - return list(self.issuer_keys.keys()) + return self._issuer_ids() def match_owner(self, url): """ @@ -325,22 +334,19 @@ def match_owner(self, url): :param url: A URL :return: An issue entity ID that exists in the Key jar """ - for owner in self.issuer_keys.keys(): - if owner.startswith(url): - return owner + _iss = [i for i in self._issuers if i.name.startswith(url)] + if _iss: + return _iss[0].name raise KeyError("No keys for '{}' in this keyjar".format(url)) def __str__(self): _res = {} - for _id, kbs in self.issuer_keys.items(): - _l = [] - for kb in kbs: - _l.extend(json.loads(kb.jwks())["keys"]) - _res[_id] = {"keys": _l} + for _issuer in self._issuers: + _res[_issuer.name] = _issuer.key_summary() return json.dumps(_res) - def load_keys(self, issuer, jwks_uri='', jwks=None, replace=False): + def load_keys(self, issuer_id, jwks_uri='', jwks=None, replace=False): """ Fetch keys from another server @@ -352,36 +358,43 @@ def load_keys(self, issuer, jwks_uri='', jwks=None, replace=False): :return: Dictionary with usage as key and keys as values """ - logger.debug("Initiating key bundle for issuer: %s" % issuer) + logger.debug("Initiating key bundle for issuer: %s" % issuer_id) - if replace or issuer not in self.issuer_keys: - self.issuer_keys[issuer] = [] + _issuer = self.return_issuer(issuer_id) + if replace: + _issuer.set([]) if jwks_uri: - self.add_url(issuer, jwks_uri) + _issuer.add_url(jwks_uri) elif jwks: # jwks should only be considered if no jwks_uri is present _keys = jwks['keys'] - self.issuer_keys[issuer].append(self.keybundle_cls(_keys)) + _issuer.add_kb(self.keybundle_cls(_keys)) - def find(self, source, issuer): + def find(self, source, issuer_id=None): """ Find a key bundle based on the source of the keys :param source: A source url :param issuer: The issuer of keys - :return: A :py:class:`oidcmsg.key_bundle.KeyBundle` instance or None - """ - try: - for kb in self.issuer_keys[issuer]: - if kb.source == source: - return kb - except KeyError: - return None + :return: List of :py:class:`oidcmsg.key_bundle.KeyBundle` instances or None + """ + if issuer_id is None: + res = {} + for _issuer in self._issuers: + kbs = _issuer.find(source) + if kbs: + res[_issuer.name] = kbs + else: + _issuer = self._get_issuer(issuer_id) + if _issuer is None: + return [] + else: + res = _issuer.find(source) - return None + return res - def export_jwks(self, private=False, issuer="", usage=None): + def export_jwks(self, private=False, issuer_id="", usage=None): """ Produces a dictionary that later can be easily mapped into a JSON string representing a JWKS. @@ -390,42 +403,45 @@ def export_jwks(self, private=False, issuer="", usage=None): :param issuer: The entity ID. :return: A dictionary with one key: 'keys' """ + _issuer = self._get_issuer(issuer_id=issuer_id) + if _issuer is None: + return {} + keys = [] - for kb in self.issuer_keys[issuer]: + for kb in _issuer: keys.extend([k.serialize(private) for k in kb.keys() if - k.inactive_since == 0 and (usage is None or (hasattr(k, 'use') and k.use == usage))]) + k.inactive_since == 0 and ( + usage is None or (hasattr(k, 'use') and k.use == usage))]) return {"keys": keys} - def export_jwks_as_json(self, private=False, issuer=""): + def export_jwks_as_json(self, private=False, issuer_id=""): """ Export a JWKS as a JSON document. :param private: Whether it should be the private keys or the public - :param issuer: The entity ID. + :param issuer_id: The entity ID. :return: A JSON representation of a JWKS """ - return json.dumps(self.export_jwks(private, issuer)) + return json.dumps(self.export_jwks(private, issuer_id)) - def import_jwks(self, jwks, issuer): + def import_jwks(self, jwks, issuer_id): """ Imports all the keys that are represented in a JWKS :param jwks: Dictionary representation of a JWKS - :param issuer: Who 'owns' the JWKS + :param issuer_id: Who 'owns' the JWKS """ try: _keys = jwks["keys"] except KeyError: raise ValueError('Not a proper JWKS') else: - try: - self.issuer_keys[issuer].append( - self.keybundle_cls(_keys, httpc=self.httpc, httpc_params=self.httpc_params)) - except KeyError: - self.issuer_keys[issuer] = [self.keybundle_cls( - _keys, httpc=self.httpc, httpc_params=self.httpc_params)] + _issuer = self.return_issuer(issuer_id=issuer_id) + _issuer.add(self.keybundle_cls(_keys, httpc=self.httpc, + httpc_params=self.httpc_params, + storage_conf=self.storage_conf)) - def import_jwks_as_json(self, jwks, issuer): + def import_jwks_as_json(self, jwks, issuer_id): """ Imports all the keys that are represented in a JWKS expressed as a JSON object @@ -433,11 +449,11 @@ def import_jwks_as_json(self, jwks, issuer): :param jwks: JSON representation of a JWKS :param issuer: Who 'owns' the JWKS """ - return self.import_jwks(json.loads(jwks), issuer) + return self.import_jwks(json.loads(jwks), issuer_id) - def import_jwks_from_file(self, filename, issuer): + def import_jwks_from_file(self, filename, issuer_id): with open(filename) as jwks_file: - self.import_jwks_as_json(jwks_file.read(), issuer) + self.import_jwks_as_json(jwks_file.read(), issuer_id) def __eq__(self, other): if not isinstance(other, KeyJar): @@ -459,6 +475,11 @@ def __eq__(self, other): return True + def __delitem__(self, key): + _issuer = self._get_issuer(key) + if _issuer: + self._issuers.remove(_issuer) + def remove_outdated(self, when=0): """ Goes through the complete list of issuers and for each of them removes @@ -471,56 +492,53 @@ def remove_outdated(self, when=0): :param when: To facilitate testing """ - for iss in list(self.owners()): - _kbl = [] - for kb in self.issuer_keys[iss]: - kb.remove_outdated(self.remove_after, when=when) - if len(kb): - _kbl.append(kb) - if _kbl: - self.issuer_keys[iss] = _kbl - else: - del self.issuer_keys[iss] + _ids = self._issuer_ids() + for _id in _ids: + _issuer = self[_id] + _before = len(_issuer) + _issuer.remove_outdated(when) + if len(_issuer) != _before: + del self[_id] + self.append(_issuer) - def _add_key(self, keys, issuer, use, key_type='', kid='', + def _add_key(self, keys, issuer_id, use, key_type='', kid='', no_kid_issuer=None, allow_missing_kid=False): - if issuer not in self: - logger.error('Issuer "{}" not in keyjar'.format(issuer)) + _issuer = self._get_issuer(issuer_id) + if _issuer is None: + logger.error('Issuer "{}" not in keyjar'.format(issuer_id)) return keys - logger.debug('Key set summary for {}: {}'.format( - issuer, key_summary(self, issuer))) + logger.debug('Key summary for {}: {}'.format(issuer_id, _issuer.key_summary())) if kid: - for _key in self.get(key_use=use, owner=issuer, kid=kid, key_type=key_type): + for _key in _issuer.get(use, kid=kid, key_type=key_type): if _key and _key not in keys: keys.append(_key) return keys else: try: - kl = self.get(key_use=use, owner=issuer, key_type=key_type) + _add_keys = _issuer.get(use, key_type=key_type) except KeyError: pass else: - if len(kl) == 0: + if len(_add_keys) == 0: return keys - elif len(kl) == 1: - if kl[0] not in keys: - keys.append(kl[0]) + elif len(_add_keys) == 1: + if _add_keys[0] not in keys: + keys.append(_add_keys[0]) elif allow_missing_kid: - keys.extend(kl) + keys.extend(_add_keys) elif no_kid_issuer: try: - allowed_kids = no_kid_issuer[issuer] + allowed_kids = no_kid_issuer[issuer_id] except KeyError: return keys else: if allowed_kids: - keys.extend( - [k for k in kl if k.kid in allowed_kids]) + keys.extend([k for k in _add_keys if k.kid in allowed_kids]) else: - keys.extend(kl) + keys.extend(_add_keys) return keys def get_jwt_decrypt_keys(self, jwt, **kwargs): @@ -544,7 +562,7 @@ def get_jwt_decrypt_keys(self, jwt, **kwargs): logger.info('Missing kid') _kid = '' - keys = self.get(key_use='enc', owner='', key_type=_key_type) + keys = self.get(key_use='enc', issuer_id='', key_type=_key_type) try: _aud = kwargs['aud'] @@ -607,10 +625,10 @@ def get_jwt_verify_keys(self, jwt, **kwargs): _kid, nki, allow_missing_kid) if _key_type == 'oct': - keys.extend(self.get(key_use='sig', owner='', + keys.extend(self.get(key_use='sig', issuer_id='', key_type=_key_type)) else: # No issuer, just use all keys I have - keys = self.get(key_use='sig', owner='', key_type=_key_type) + keys = self.get(key_use='sig', issuer_id='', key_type=_key_type) # Only want the appropriate keys. keys = [k for k in keys if k.appropriate_for('verify')] @@ -623,33 +641,39 @@ def copy(self): :return: A :py:class:`oidcmsg.key_jar.KeyJar` instance """ kj = KeyJar() - for issuer in self.owners(): - kj[issuer] = [kb.copy() for kb in self[issuer]] + for _issuer in self._issuers: + _iss = kj.return_issuer(_issuer.name) + _iss.set([kb.copy() for kb in _issuer]) kj.httpc_params = self.httpc_params kj.httpc = self.httpc return kj def __len__(self): - keys = 0 - for iss in list(self.owners()): - for kb in self.issuer_keys[iss]: - if len(kb): - keys += len(kb) - return keys + return len(self._issuers) - def dump(self): + def dump(self, exclude=None): """ Returns the key jar content as dictionary :return: A dictionary """ - info = {} - for iss in list(self.owners()): - info[iss] = [] - for kb in self.issuer_keys[iss]: - info[iss].append(kb.dump()) + info = { + # 'storage_conf': self.storage_conf, + 'spec2key': self.spec2key, + 'ca_certs': self.ca_certs, + 'keybundle_cls': qualified_name(self.keybundle_cls), + 'remove_after': self.remove_after, + 'httpc_params': self.httpc_params} + + _issuers = [] + for _issuer in self._issuers: + if exclude and _issuer.name in exclude: + continue + _issuers.append(_issuer.dump()) + info['issuers'] = _issuers + return info def load(self, info): @@ -658,15 +682,45 @@ def load(self, info): :param info: A dictionary with the information :return: """ - for iss, kbs in info.items(): - self.issuer_keys[iss] = [KeyBundle().load(val) for val in kbs] + # self.storage_conf = info['storage_conf'] + self.spec2key = info['spec2key'] + self.ca_certs = info['ca_certs'] + self.keybundle_cls = importer(info['keybundle_cls']) + self.remove_after = info['remove_after'] + self.httpc_params = info['httpc_params'] + + for _issuer_desc in info['issuers']: + self._issuers.append(KeyIssuer(storage_conf=self.storage_conf).load(_issuer_desc)) return self + def append(self, issuer): + self._issuers.append(issuer) + + def key_summary(self, issuer_id): + _issuer = self._get_issuer(issuer_id) + if _issuer: + return _issuer.key_summary() + + raise KeyError('Unknown Issuer ID: "{}"'.format(issuer_id)) + + def update(self): + """ + Go through the whole key jar, key bundle by key bundle and update them one + by one. + + :param keyjar: The key jar to update + """ + for _id in self._issuer_ids(): + _issuer = self._get_issuer(_id) + self._issuers.remove(_issuer) + _issuer.update() + self._issuers.append(_issuer) + # ============================================================================= -def build_keyjar(key_conf, kid_template="", keyjar=None, owner=''): +def build_keyjar(key_conf, kid_template="", keyjar=None, issuer_id='', storage_conf=None): """ Builds a :py:class:`oidcmsg.key_jar.KeyJar` instance or adds keys to an existing KeyJar based on a key specification. @@ -704,59 +758,26 @@ def build_keyjar(key_conf, kid_template="", keyjar=None, owner=''): :param kid_template: A template by which to build the key IDs. If no kid_template is given then the built-in function add_kid() will be used. :param keyjar: If an KeyJar instance the new keys are added to this key jar. - :param owner: The default owner of the keys in the key jar. + :param issuer_id: The default owner of the keys in the key jar. + :param storage_conf: Storage configuration :return: A KeyJar instance """ + _issuer = build_keyissuer(key_conf, kid_template, storage_conf=storage_conf, + issuer_id=issuer_id) + if _issuer is None: + return None + if keyjar is None: keyjar = KeyJar() - bundle = build_key_bundle(key_conf, kid_template) - - keyjar.add_kb(owner, bundle) + keyjar.append(_issuer) return keyjar -def update_keyjar(keyjar): - """ - Go through the whole key jar, key bundle by key bundle and update them one - by one. - - :param keyjar: The key jar to update - """ - for iss, kbl in keyjar.items(): - for kb in kbl: - kb.update() - - -def key_summary(keyjar, issuer): - """ - Return a text representation of the keyjar. - - :param keyjar: A :py:class:`oidcmsg.key_jar.KeyJar` instance - :param issuer: Which key owner that we are looking at - :return: A text representation of the keys - """ - try: - kbl = keyjar[issuer] - except KeyError: - return '' - else: - key_list = [] - for kb in kbl: - for key in kb.keys(): - if key.inactive_since: - key_list.append( - '*{}:{}:{}'.format(key.kty, key.use, key.kid)) - else: - key_list.append( - '{}:{}:{}'.format(key.kty, key.use, key.kid)) - return ', '.join(key_list) - - -def init_key_jar(public_path='', private_path='', key_defs='', owner='', - read_only=True): +def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', read_only=True, + storage_conf=None): """ A number of cases here: @@ -794,7 +815,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', owner='', private keys. :param key_defs: A definition of what keys should be created if they are not already available - :param owner: The owner of the keys + :param issuer_id: The owner of the keys :param read_only: This function should not attempt to write anything to a file system. :return: An instantiated :py:class;`oidcmsg.key_jar.KeyJar` instance @@ -803,25 +824,25 @@ def init_key_jar(public_path='', private_path='', key_defs='', owner='', if private_path: if os.path.isfile(private_path): _jwks = open(private_path, 'r').read() - _kj = KeyJar() - _kj.import_jwks(json.loads(_jwks), owner) + _issuer = KeyIssuer(name=issuer_id, storage_conf=storage_conf) + _issuer.import_jwks(json.loads(_jwks)) if key_defs: - _kb = _kj.issuer_keys[owner][0] + _kb = _issuer[0] _diff = key_diff(_kb, key_defs) if _diff: update_key_bundle(_kb, _diff) if read_only: logger.error('Not allowed to write to disc!') else: - _kj.issuer_keys[owner] = [_kb] - jwks = _kj.export_jwks(private=True, issuer=owner) + _issuer.set([_kb]) + jwks = _issuer.export_jwks(private=True) fp = open(private_path, 'w') fp.write(json.dumps(jwks)) fp.close() else: - _kj = build_keyjar(key_defs, owner=owner) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) if not read_only: - jwks = _kj.export_jwks(private=True, issuer=owner) + jwks = _issuer.export_jwks(private=True) head, tail = os.path.split(private_path) if head and not os.path.isdir(head): os.makedirs(head) @@ -830,7 +851,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', owner='', fp.close() if public_path and not read_only: - jwks = _kj.export_jwks(issuer=owner) # public part + jwks = _issuer.export_jwks() # public part head, tail = os.path.split(public_path) if head and not os.path.isdir(head): os.makedirs(head) @@ -840,25 +861,25 @@ def init_key_jar(public_path='', private_path='', key_defs='', owner='', elif public_path: if os.path.isfile(public_path): _jwks = open(public_path, 'r').read() - _kj = KeyJar() - _kj.import_jwks(json.loads(_jwks), owner) + _issuer = KeyIssuer(name=issuer_id, storage_conf=storage_conf) + _issuer.import_jwks(json.loads(_jwks)) if key_defs: - _kb = _kj.issuer_keys[owner][0] + _kb = _issuer[0] _diff = key_diff(_kb, key_defs) if _diff: if read_only: logger.error('Not allowed to write to disc!') else: update_key_bundle(_kb, _diff) - _kj.issuer_keys[owner] = [_kb] - jwks = _kj.export_jwks(issuer=owner) + _issuer.set([_kb]) + jwks = _issuer.export_jwks() fp = open(public_path, 'w') fp.write(json.dumps(jwks)) fp.close() else: - _kj = build_keyjar(key_defs, owner=owner) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) if not read_only: - _jwks = _kj.export_jwks(issuer=owner) + _jwks = _issuer.export_jwks(issuer=issuer_id) head, tail = os.path.split(public_path) if head and not os.path.isdir(head): os.makedirs(head) @@ -866,6 +887,20 @@ def init_key_jar(public_path='', private_path='', key_defs='', owner='', fp.write(json.dumps(_jwks)) fp.close() else: - _kj = build_keyjar(key_defs, owner=owner) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) + + keyjar = KeyJar(storage_conf=storage_conf) + keyjar.append(_issuer) + return keyjar + - return _kj +def rotate_keys(key_conf, keyjar, kid_template="", issuer_id='', storage_conf=None): + new_keys = build_keyissuer(key_conf, kid_template, storage_conf=storage_conf, + issuer_id=issuer_id) + _issuer = keyjar[issuer_id] + _issuer.mark_all_keys_as_inactive() + for kb in new_keys: + _issuer.add_kb(kb) + del keyjar[_issuer.name] + keyjar.append(_issuer) + return keyjar diff --git a/src/cryptojwt/serialize/__init__.py b/src/cryptojwt/serialize/__init__.py new file mode 100644 index 0000000..7d7ec6b --- /dev/null +++ b/src/cryptojwt/serialize/__init__.py @@ -0,0 +1,47 @@ + +class SimpleList(): + def __init__(self, value=None): + if value is None: + self.db = [] + else: + self.set(value) + + def __len__(self): + return len(self.db) + + def __contains__(self, item): + return item in self.db + + def __del__(self): + del self.db + + def __iter__(self): + for i in self.db: + yield i + + def __str__(self): + return str(self.db) + + def append(self, item): + self.db.append(item) + + def extend(self, items): + self.db.extend(items) + + def remove(self, item): + self.db.remove(item) + + def get(self): + return self.db + + def set(self, value): + if isinstance(value, list): + self.db = value + else: + raise ValueError("Wrong value type") + + def copy(self): + return self.db[:] + + def close(self): + return diff --git a/tests/test_04_key_jar.py b/tests/test_04_key_jar.py index 23cd06a..0ffa1f5 100755 --- a/tests/test_04_key_jar.py +++ b/tests/test_04_key_jar.py @@ -15,11 +15,11 @@ from cryptojwt.key_jar import KeyJar from cryptojwt.key_jar import build_keyjar from cryptojwt.key_jar import init_key_jar -from cryptojwt.key_jar import key_summary -from cryptojwt.key_jar import update_keyjar __author__ = 'Roland Hedberg' +from cryptojwt.key_jar import rotate_keys + BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "test_keys")) RSAKEY = os.path.join(BASE_PATH, "cert.key") @@ -207,7 +207,7 @@ def test_build_keyjar(): assert "d" not in key # the JWKS shouldn't contain the private part # of the keys - assert len(keyjar[""]) == 1 # One key bundle + assert len(keyjar[""]) == 3 # 3 keys assert len(keyjar.get_issuer_keys('')) == 3 # A total of 3 keys assert len(keyjar.get('sig')) == 2 # 2 for signing assert len(keyjar.get('enc')) == 1 # 1 for encryption @@ -237,7 +237,7 @@ def test_build_keyjar_missing(tmpdir): key_jar = build_keyjar(keys) - assert len(key_jar[""]) == 1 + assert key_jar is None def test_build_RSA_keyjar_from_file(tmpdir): @@ -249,7 +249,7 @@ def test_build_RSA_keyjar_from_file(tmpdir): key_jar = build_keyjar(keys) - assert len(key_jar[""]) == 1 + assert len(key_jar[""]) == 2 def test_build_EC_keyjar_missing(tmpdir): @@ -261,7 +261,7 @@ def test_build_EC_keyjar_missing(tmpdir): key_jar = build_keyjar(keys) - assert len(key_jar[""]) == 1 + assert key_jar is None def test_build_EC_keyjar_from_file(tmpdir): @@ -273,7 +273,7 @@ def test_build_EC_keyjar_from_file(tmpdir): key_jar = build_keyjar(keys) - assert len(key_jar[""]) == 1 + assert len(key_jar[""]) == 2 class TestKeyJar(object): @@ -286,7 +286,7 @@ def test_keyjar_add(self): def test_setitem(self): kj = KeyJar() kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"]) - kj['https://issuer.example.com'] = kb + kj.add_kb('https://issuer.example.com', kb) assert list(kj.owners()) == ['https://issuer.example.com'] def test_add_symmetric(self): @@ -297,66 +297,66 @@ def test_add_symmetric(self): def test_items(self): ks = KeyJar() - ks[""] = KeyBundle( + ks.add_kb("", KeyBundle( [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, - {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}]) - ks["http://www.example.org"] = KeyBundle([ + {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}])) + ks.add_kb("http://www.example.org", KeyBundle([ {"kty": "oct", "key": "0123456789012345", "use": "sig"}, - {"kty": "oct", "key": "1234567890123456", "use": "enc"}]) - ks["http://www.example.org"].append( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + {"kty": "oct", "key": "1234567890123456", "use": "enc"}])) + ks.add_kb("http://www.example.org", + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) assert len(ks.items()) == 2 def test_issuer_extra_slash(self): ks = KeyJar() - ks[""] = KeyBundle( + ks.add_kb("", KeyBundle( [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, - {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}]) - ks["http://www.example.org"] = KeyBundle([ + {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}])) + ks.add_kb("http://www.example.org", KeyBundle([ {"kty": "oct", "key": "0123456789012345", "use": "sig"}, - {"kty": "oct", "key": "1234567890123456", "use": "enc"}]) - ks["http://www.example.org"].append( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + {"kty": "oct", "key": "1234567890123456", "use": "enc"}])) + ks.add_kb("http://www.example.org", + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) assert ks.get('sig', 'RSA', 'http://www.example.org/') def test_issuer_missing_slash(self): ks = KeyJar() - ks[""] = KeyBundle( + ks.add_kb("", KeyBundle( [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}]) - ks["http://www.example.org/"] = KeyBundle([ + {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) + ks.add_kb("http://www.example.org/", KeyBundle([ {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}]) - ks["http://www.example.org/"].append( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}])) + ks.add_kb("http://www.example.org/", + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) assert ks.get('sig', 'RSA', 'http://www.example.org') def test_get_enc(self): ks = KeyJar() - ks[""] = KeyBundle( + ks.add_kb("", KeyBundle( [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}]) - ks["http://www.example.org/"] = KeyBundle([ + {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) + ks.add_kb("http://www.example.org/", KeyBundle([ {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}]) - ks["http://www.example.org/"].append( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}])) + ks.add_kb("http://www.example.org/", + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) assert ks.get('enc', 'oct') def test_get_enc_not_mine(self): ks = KeyJar() - ks[""] = KeyBundle( + ks.add_kb("", KeyBundle( [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}]) - ks["http://www.example.org/"] = KeyBundle([ + {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) + ks.add_kb("http://www.example.org/", KeyBundle([ {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "ver"}]) - ks["http://www.example.org/"].append( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "ver"}])) + ks.add_kb("http://www.example.org/", + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) assert ks.get('enc', 'oct', 'http://www.example.org/') @@ -365,7 +365,7 @@ def test_dump_issuer_keys(self): ["sig"]) assert len(kb) == 1 kj = KeyJar() - kj.issuer_keys[""] = [kb] + kj.add_kb("", kb) _jwks_dict = kj.export_jwks() _info = _jwks_dict['keys'][0] @@ -388,17 +388,17 @@ def test_dump_issuer_keys(self): def test_no_use(self): kb = KeyBundle(JWK0["keys"]) kj = KeyJar() - kj.issuer_keys["abcdefgh"] = [kb] + kj.add_kb("abcdefgh", kb) enc_key = kj.get_encrypt_key("RSA", "abcdefgh") assert enc_key != [] @pytest.mark.network def test_provider(self): - ks = KeyJar() - ks.load_keys("https://connect-op.heroku.com", + kj = KeyJar() + kj.load_keys("https://connect-op.heroku.com", jwks_uri="https://connect-op.herokuapp.com/jwks.json") - assert ks["https://connect-op.heroku.com"][0].keys() + assert kj.get_issuer_keys("https://connect-op.heroku.com")[0].keys() def test_import_jwks(): @@ -432,24 +432,14 @@ def test_remove_after(): _old = [k.kid for k in keyjar.get_issuer_keys('') if k.kid] assert len(_old) == 2 + keyjar.remove_after = 1 # rotate_keys = create new keys + make the old as inactive - keyjar = build_keyjar(KEYDEFS, keyjar=keyjar) + keyjar = rotate_keys(KEYDEFS, keyjar=keyjar) - keyjar.remove_after = 1 - # None are remove since none are marked as inactive yet - keyjar.remove_outdated() + keyjar.remove_outdated(time.time() + 3600) _interm = [k.kid for k in keyjar.get_issuer_keys('') if k.kid] - assert len(_interm) == 4 - - # Now mark the keys to be inactivated - _now = time.time() - for k in keyjar.get_issuer_keys(''): - if k.kid in _old: - if not k.inactive_since: - k.inactive_since = _now - - keyjar.remove_outdated(_now + 5) + assert len(_interm) == 2 # The remainder are the new keys _new = [k.kid for k in keyjar.get_issuer_keys('') if k.kid] @@ -618,23 +608,24 @@ def setup(self): self.alice_keyjar = build_keyjar(mkey) # Bob has one single keys self.bob_keyjar = build_keyjar(skey) - self.alice_keyjar['Alice'] = self.alice_keyjar[''] - self.bob_keyjar['Bob'] = self.bob_keyjar[''] + self.alice_keyjar.import_jwks(self.alice_keyjar.export_jwks(private=True, issuer_id=''), + 'Alice') + self.bob_keyjar.import_jwks(self.bob_keyjar.export_jwks(private=True, issuer_id=''), 'Bob') # To Alice's keyjar add Bob's public keys self.alice_keyjar.import_jwks( - self.bob_keyjar.export_jwks(issuer='Bob'), 'Bob') + self.bob_keyjar.export_jwks(issuer_id='Bob'), 'Bob') # To Bob's keyjar add Alice's public keys self.bob_keyjar.import_jwks( - self.alice_keyjar.export_jwks(issuer='Alice'), 'Alice') + self.alice_keyjar.export_jwks(issuer_id='Alice'), 'Alice') _jws = JWS('{"aud": "Bob", "iss": "Alice"}', alg='RS256') - sig_key = self.alice_keyjar.get_signing_key('rsa', owner='Alice')[0] + sig_key = self.alice_keyjar.get_signing_key('rsa', issuer_id='Alice')[0] self.sjwt_a = _jws.sign_compact([sig_key]) _jws = JWS('{"aud": "Alice", "iss": "Bob"}', alg='RS256') - sig_key = self.bob_keyjar.get_signing_key('rsa', owner='Bob')[0] + sig_key = self.bob_keyjar.get_signing_key('rsa', issuer_id='Bob')[0] self.sjwt_b = _jws.sign_compact([sig_key]) def test_no_kid_multiple_keys(self): @@ -656,7 +647,7 @@ def test_no_kid_single_key(self): def test_no_kid_multiple_keys_no_kid_issuer(self): a_kids = [k.kid for k in - self.alice_keyjar.get_verify_key(owner='Alice', + self.alice_keyjar.get_verify_key(issuer_id='Alice', key_type='RSA')] no_kid_issuer = {'Alice': a_kids} _jwt = factory(self.sjwt_a) @@ -685,11 +676,11 @@ def test_no_matching_kid(self): assert keys == [] def test_aud(self): - self.alice_keyjar.import_jwks(JWK1, issuer='D') - self.bob_keyjar.import_jwks(JWK1, issuer='D') + self.alice_keyjar.import_jwks(JWK1, issuer_id='D') + self.bob_keyjar.import_jwks(JWK1, issuer_id='D') _jws = JWS('{"iss": "D", "aud": "A"}', alg='HS256') - sig_key = self.alice_keyjar.get_signing_key('oct', owner='D')[0] + sig_key = self.alice_keyjar.get_signing_key('oct', issuer_id='D')[0] _sjwt = _jws.sign_compact([sig_key]) no_kid_issuer = {'D': []} @@ -703,9 +694,9 @@ def test_aud(self): def test_copy(): kj = KeyJar() - kj['Alice'] = [KeyBundle(JWK0['keys'])] - kj['Bob'] = [KeyBundle(JWK1['keys'])] - kj['C'] = [KeyBundle(JWK2['keys'])] + kj.add_kb('Alice', KeyBundle(JWK0['keys'])) + kj.add_kb('Bob', KeyBundle(JWK1['keys'])) + kj.add_kb('C', KeyBundle(JWK2['keys'])) kjc = kj.copy() @@ -723,9 +714,9 @@ def test_copy(): def test_repr(): kj = KeyJar() - kj['Alice'] = [KeyBundle(JWK0['keys'])] - kj['Bob'] = [KeyBundle(JWK1['keys'])] - kj['C'] = [KeyBundle(JWK2['keys'])] + kj.add_kb('Alice', KeyBundle(JWK0['keys'])) + kj.add_kb('Bob', KeyBundle(JWK1['keys'])) + kj.add_kb('C', KeyBundle(JWK2['keys'])) txt = kj.__repr__() assert " Date: Tue, 12 May 2020 08:48:19 +0200 Subject: [PATCH 02/50] Major change. --- src/cryptojwt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cryptojwt/__init__.py b/src/cryptojwt/__init__.py index ee621f2..fb25595 100644 --- a/src/cryptojwt/__init__.py +++ b/src/cryptojwt/__init__.py @@ -21,7 +21,7 @@ except ImportError: pass -__version__ = '0.9.0' +__version__ = '1.0.0' logger = logging.getLogger(__name__) From 281d5a363a2cfa9be028eea6aea434f9c18d0b2f Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 12 May 2020 09:02:12 +0200 Subject: [PATCH 03/50] Missing file. --- src/cryptojwt/serialize/item.py | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/cryptojwt/serialize/item.py diff --git a/src/cryptojwt/serialize/item.py b/src/cryptojwt/serialize/item.py new file mode 100644 index 0000000..e63cc4e --- /dev/null +++ b/src/cryptojwt/serialize/item.py @@ -0,0 +1,50 @@ +from cryptojwt import jwk +from cryptojwt.jwk.jwk import key_from_jwk_dict +from cryptojwt import key_bundle +from cryptojwt import key_issuer + + +class JWK: + @staticmethod + def serialize(key: jwk.JWK) -> dict: + _dict = key.serialize() + inactive = key.inactive_since + if inactive: + _dict['inactive_since'] = inactive + return _dict + + @staticmethod + def deserialize(jwk: dict) -> jwk.JWK: + k = key_from_jwk_dict(jwk) + inactive = jwk.get("inactive_since", 0) + if inactive: + k.inactive_since = inactive + return k + + +class KeyBundle: + def __init__(self, storage_conf=None): + self.storage_conf = storage_conf + + @staticmethod + def serialize(item: key_bundle.KeyBundle) -> dict: + _dict = item.dump() + return _dict + + def deserialize(self, spec: dict) -> key_bundle.KeyBundle: + bundle = key_bundle.KeyBundle(storage_conf=self.storage_conf).load(spec) + return bundle + + +class KeyIssuer: + def __init__(self, storage_conf=None): + self.storage_conf = storage_conf + + @staticmethod + def serialize(item: key_issuer.KeyIssuer) -> dict: + _dict = item.dump() + return _dict + + def deserialize(self, spec: dict) -> key_issuer.KeyIssuer: + issuer = key_issuer.KeyIssuer(storage_conf=self.storage_conf).load(spec) + return issuer From 0fa5db945d85bfe3508509ab0b4198a81909944e Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:19:22 +0200 Subject: [PATCH 04/50] Name should probably be first parameter. --- aslist_tests/test_44_key_issuer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aslist_tests/test_44_key_issuer.py b/aslist_tests/test_44_key_issuer.py index 8bacecf..0526865 100755 --- a/aslist_tests/test_44_key_issuer.py +++ b/aslist_tests/test_44_key_issuer.py @@ -553,7 +553,7 @@ def test_keys_by_alg_and_usage(): def test_copy(): - issuer = KeyIssuer('Alice', storage_conf=STORAGE_CONFIG) + issuer = KeyIssuer(name='Alice', storage_conf=STORAGE_CONFIG) issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) issuer_copy = issuer.copy() @@ -562,10 +562,10 @@ def test_copy(): def test_repr(): - issuer = KeyIssuer('Alice', storage_conf=STORAGE_CONFIG) + issuer = KeyIssuer(name='Alice', storage_conf=STORAGE_CONFIG) issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) txt = issuer.__repr__() - assert " Date: Tue, 19 May 2020 11:21:15 +0200 Subject: [PATCH 05/50] Handed over to KeyIssuer. --- src/cryptojwt/tools/jwtpeek.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cryptojwt/tools/jwtpeek.py b/src/cryptojwt/tools/jwtpeek.py index df0450f..4eb1b51 100755 --- a/src/cryptojwt/tools/jwtpeek.py +++ b/src/cryptojwt/tools/jwtpeek.py @@ -7,6 +7,8 @@ import os import sys +from cryptojwt.key_issuer import KeyIssuer + from cryptojwt.jwe import jwe from cryptojwt.jwk.hmac import SYMKey from cryptojwt.jwk.jwk import key_from_jwk_dict @@ -114,9 +116,9 @@ def main(): keys.append(_key) if args.jwks: - _k = KeyJar() - _k.import_jwks(open(args.jwks).read(), "") - keys.extend(_k.issuer_keys("")) + _iss = KeyIssuer() + _iss.import_jwks(open(args.jwks).read()) + keys.extend(_iss.all_keys()) if args.jwks_url: _kb = KeyBundle(source=args.jwks_url) From 54aad6a64f67034df3326c85e49b6da0a4ed24e3 Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:21:32 +0200 Subject: [PATCH 06/50] Changed repr --- src/cryptojwt/key_issuer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cryptojwt/key_issuer.py b/src/cryptojwt/key_issuer.py index b42d7c4..bb6dcf7 100755 --- a/src/cryptojwt/key_issuer.py +++ b/src/cryptojwt/key_issuer.py @@ -49,7 +49,7 @@ def __init__(self, ca_certs=None, keybundle_cls=KeyBundle, self.httpc_params = httpc_params or {} def __repr__(self) -> str: - return ''.format(self._bundles) + return ''.format(self.name, self._bundles) def __getitem__(self, item): return self.get_bundles()[item] From 3e6a0e2d6c346ed47aacbd055c0e8963880057db Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:22:10 +0200 Subject: [PATCH 07/50] Finger print of certificates. --- tests/test_02_jwk.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_02_jwk.py b/tests/test_02_jwk.py index e248346..b6d87e1 100644 --- a/tests/test_02_jwk.py +++ b/tests/test_02_jwk.py @@ -16,6 +16,7 @@ from cryptojwt.exception import UnsupportedAlgorithm from cryptojwt.exception import WrongUsage from cryptojwt.jwk import JWK +from cryptojwt.jwk import certificate_fingerprint from cryptojwt.jwk import pem_hash from cryptojwt.jwk import pems_to_x5c from cryptojwt.jwk.ec import NIST2SEC @@ -28,6 +29,7 @@ from cryptojwt.jwk.jwk import jwk_wrap from cryptojwt.jwk.jwk import key_from_jwk_dict from cryptojwt.jwk.rsa import RSAKey +from cryptojwt.jwk.rsa import generate_and_store_rsa_key from cryptojwt.jwk.rsa import import_private_rsa_key_from_file from cryptojwt.jwk.rsa import import_public_rsa_key_from_file from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file @@ -654,4 +656,26 @@ def test_pem_to_x5c(): def test_pem_hash(): _hash = pem_hash(full_path("cert.pem")) - assert _hash \ No newline at end of file + assert _hash + + +def test_certificate_fingerprint(): + with open(full_path('cert.der'), 'rb') as cert_file: + der = cert_file.read() + + res = certificate_fingerprint(der) + assert res == '01:DF:F1:D4:5F:21:7B:2E:3A:A2:D8:CA:13:4C:41:66:03:A1:EF:3E:7B:5E:8B:69:04:5E:80:8B:55:49:F1:48' + + res = certificate_fingerprint(der, 'sha1') + assert res == 'CA:CF:21:9E:72:00:CD:1C:CA:FD:4F:6D:84:6B:9E:E8:74:80:47:64' + + res = certificate_fingerprint(der, 'md5') + assert res == '1B:2B:3B:F8:49:EE:2A:2C:C1:C7:6C:88:86:AB:C6:EE' + + with pytest.raises(UnsupportedAlgorithm): + certificate_fingerprint(der, 'foo') + + +def test_generate_and_store_rsa_key(): + priv_key = generate_and_store_rsa_key(filename=full_path('temp_rsa.key')) + From 0997327bf3eccb10e489214dbb0b4b549bb0a978 Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:22:58 +0200 Subject: [PATCH 08/50] Test item serializing --- aslist_tests/private_jwks.json | 2 +- tests/test_03_key_bundle.py | 2 ++ tests/test_40_serialize.py | 56 ++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/test_40_serialize.py diff --git a/aslist_tests/private_jwks.json b/aslist_tests/private_jwks.json index b1471f1..14fbc2b 100644 --- a/aslist_tests/private_jwks.json +++ b/aslist_tests/private_jwks.json @@ -1 +1 @@ -{"keys": [{"kty": "RSA", "use": "sig", "kid": "ZG5wZjlUYW11MkZSVzA5MTRuaTdDYUtnV0xQS0pwWUpPTm1YMTJNeFlQYw", "n": "qlDQeEoGfykMrV3WupWIMHGrs4AHVTld-C00qBcVCBNptef6T2UESVSurMITmgCdJrwEVfHwkd2is1xSev2pIjQy8m9CehBexxq0hlNmUhPzNPixbCMqUyxCzGi1bms9qSxg2zJr0pmDFK_EiOyM47B48eYcypUk-PxzX4d1L-jBT6F8B9fT8YjS4OHzs__Yq6EDzJ4gubaqOLjsYQsLkYj3fKz-b4trb9n4eWJkkbvtgCN6gZVVKgmxPpQHVhDIj7jHXfl9QmTooWxObvO9LK8DFK63V-E-Ce5iKNcYmeB4TeOJmdZfasFa4TPOg22jZaE9UOnnZPyXx7VOCkiHsw", "e": "AQAB", "d": "KsaZVVziPNXGhVRoNfyQc_pYsYCaVuFNpKNV8lG5yol1p2ZYC9DHPtOx-1nTKn60-aGHRT66uSf9UScC4DkNXbXWheVDwPyTkVY3uPUBYeP41XkQtqQuYS1gqY4y40Sz--VVfjgvtHkx3uQ2bF1dFWKhPcAZwxeqbY6aO4f9-sYFtgIIINe_vDKw6W8RPJMK4keK_V-8Hw9-t1A_gja2_34U9cSw0yO9BDBNJiE88mX-Nu4djfAqr-MVOXpHEW2FIA7Ge6laZlGR_pn_kaqVbEnG8DLJqJGp_mJIg1tx08kHTVXw8TmqKIeMVZ8uU3Vrfjy6D6PmTkEwZK1_EgvxoQ", "p": "15-5UXO89cFmYFbzW9IRfHp7g8VQw88Uphf3J5IjgI8GL80QqFT7r7uBx1b8uKvJIWnG6OxygVJm9-cy9HPQzCBQQ1e_R5OKAaNvMM_nTVAHAy5KZlt_qFJw7vb5tfRkj8u_0jDt1qp6BYXKBMPWzpxwwKG-aj9PyPrkZyFDn6c", "q": "yjUq8rrtQgnGS4fBQwgf44sg8F47PHX-D7fCmImg6gjW8e4Ll51yDlKHFBskvLNzAWNfy8_GTzTs3Nr2zuQrWrkhXlk4T7FIJATVBI4n4uiNEvfYr17fViLM6T8d0WUM_9zy6EI8CBBRdeAeI8obW5p3F6Wu6UygkrysgyFV-RU"}, {"kty": "EC", "use": "sig", "kid": "aDFHbmtvZldackkyUjJ5aTluSWNaVmJnUXkwYmd2Zk5sOGxCM3dhQVgwRQ", "crv": "P-256", "x": "s4o9jPfgErHOuPBdeWp8U_XUGUs7uSntAu5M4GjTCjQ", "y": "y0YE3V9E-UzEAekBLPhxYqrPPo6Abm-JRFL2Sia6Q0s", "d": "BNXlINQCl4O5vJkdqV13gOqzZJVt34RxN8njWq7Lvlc"}, {"kty": "EC", "use": "sig", "kid": "S0hSaHVmb0phaE1tbW9NcHZjVGRNS291VDlNOE5vWl9UaTNwQVhlUHhOZw", "crv": "P-384", "x": "Lu2wHapme_MnXlhpzH5M5ntqx83j6xYZ2P8u7ZoVOWdvRnmxYC1GrV2IA7feLOUv", "y": "f8S10urWzTkaq1JARY0LgLhZsXmcoTFj5Hd3tyCd6h4XlUU3iZDSfuXahF_xqJ24", "d": "SoRVsBruJ8gmBraOC0bzWD13IOUef3dGRtpIWzmEWm9uWScXxmt6AzgcTAhYP29l"}]} \ No newline at end of file +{"keys": [{"kty": "RSA", "use": "sig", "kid": "ZVp5MEtSdGQ5MjNjUGozUktMZV9Sd0c4N25PUGlQcUtIOVo5b0l0S3dZMA", "n": "oQfNSj9XSCBg5oBztL2TixZT-R8CVvSE0Y1onK40x1VDPfMTx7ezoFo72tiTASsdpAsChG9nkzT0iD5iFcv4c1LCTkqbaSdOzmZFu79Bjv1LVLboFsoVZNYhjRxCyoq1PooR7pKiaI1t531EKrnm4CrjuHk5egWG1cmp61_M3zMCrWcSODXGaFbJOfYfWSIVhD5cd7J4Hpl7rlA-TgD_gBfzL0kILDqvXln2oig_0yOXc7cpneLw9dSZiatyv_deCID5O0Cwg832vwrXN1az-No-ln37onxE03M9VZXqP54Nl5OPQ_cMg0UAGObxr5iyNGe_i6hTUJea0RuJ6sEnyw", "e": "AQAB", "d": "Sm2DukSKgADPKNrIIArbbhb02xk1CKHd3clBR-HQ7S0Adlqqks3ajUwHjEA7ufeGrLKWCEZBli2MtIg456AuBoeC3ZLoP_L2HrnwkzV0BLYYImCj5xyiRMggG8urJ1hzKyO_5AgMXsy3tp4Uarcf-g540GPfaAGz745VJkBSPfrnGYNPWIJbtbwlGJJz8TPaJRpJnAlolq6VeH5BibfoQhU1T5p0q4kLr_yAehQiKW4Y6sVnkuZjjf4g1P6GKqlnUIWS4swmbf1rjibDkysb9bXB1nV6YypVFWX6Cv_WrDUfSSH1MdR6zjOZIoZ_EHDONfP3VjABsefkv6Matz1qOQ", "p": "zG2BfrmeN11HJP43paO8wJR6FzQotv0RgdmLc6iRmPfcCyENtTWXPJo3EPxwrDX5zPbH7i814Q2tCeDp_gPqtBfbLCEV3Gla5cvuDO3K3MW6YhPLDfujoDnojelV7bcLGK0nDnQUyal9KhX4Wk7LddlQ3W8NsB2ymoKuSVF3Pic", "q": "yaeVP0lm1biaOnETbruxnoXeF4W4c4nPm7mBx9CzzCsSHqCpcWNtPUtOQc3wvVM2MdLoTJPalfNYyIkpWN5u5w-Un-SNNXY1BSwOFCTt4BETiwmjmU9rWzxfnQhsbSw6UrtoKjRQZNHD64-1lyRb1HlLBGfx6mb3gQ7qg-2Fs70"}, {"kty": "EC", "use": "sig", "kid": "bzJuTlhLSms0Wmh1TVkzbDRYVHEtN01CUnItemhiQWo0a0hiYVpWSGp4VQ", "crv": "P-256", "x": "AYZz_krBfwtrsXHJ_q3cgvZPQCY1nHgQWUn9bUNCgvU", "y": "iVmtoxf34F3jVlO3mOaHJDa_x1ehiCiB-fDM5iAq0fM", "d": "gPe7wnZMWJ3iFq2rYIIu7d5rWXHdYql3PJoDuxfFSrY"}, {"kty": "EC", "use": "sig", "kid": "WTNtS1dpVlBxN2Q2Q3hOb0dsTF8xenZWUm9Xa0pNTDRvTUdlQmN0aWhZcw", "crv": "P-384", "x": "aKHmXvAowb7XY8iMT_LtbXFHq99JYEo9anfdMdnQ5wpzRKusUznfA0aTHI6Mul3_", "y": "b_kV4fPElf9mDss6RkU31BZPCs4w9zAIxpbj7Rk74LK8q0BxWAYoyUfMs6d_Ywme", "d": "IHXdLJakx41Rw7FXbYLQHuczwALL2EZWboNEGGF8wP-fuG7NOCrggPam-oQR-QML"}]} \ No newline at end of file diff --git a/tests/test_03_key_bundle.py b/tests/test_03_key_bundle.py index 2acc712..4834f08 100755 --- a/tests/test_03_key_bundle.py +++ b/tests/test_03_key_bundle.py @@ -542,6 +542,7 @@ def test_httpc_params_1(): httpc_params=httpc_params) assert kb.do_remote() + @pytest.mark.network def test_httpc_params_2(): httpc_params = {'timeout': 0} @@ -989,6 +990,7 @@ def test_remote(): assert kb2.imp_jwks assert kb2.last_updated + def test_remote_not_modified(): source = 'https://example.com/keys.json' headers = { diff --git a/tests/test_40_serialize.py b/tests/test_40_serialize.py new file mode 100644 index 0000000..034dc5c --- /dev/null +++ b/tests/test_40_serialize.py @@ -0,0 +1,56 @@ +import os + +from cryptojwt.jwk.hmac import SYMKey +from cryptojwt.jwk.rsa import RSAKey +from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file +from cryptojwt.key_bundle import keybundle_from_local_file +from cryptojwt.key_bundle import rsa_init +from cryptojwt.key_issuer import KeyIssuer +from cryptojwt.serialize import item +from cryptojwt.serialize.item import JWK +from cryptojwt.serialize.item import KeyBundle + + +def full_path(local_file): + return os.path.join(BASEDIR, local_file) + + +BASEDIR = os.path.abspath(os.path.dirname(__file__)) +BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + "test_keys")) +CERT = full_path("cert.pem") + + +def test_jwks(): + _key = RSAKey() + _key.load_key(import_rsa_key_from_cert_file(CERT)) + + _item = JWK().serialize(_key) + _nkey = JWK().deserialize(_item) + assert _key == _nkey + + +def test_key_bundle(): + kb = rsa_init({'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) + _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) + kb.append(_sym) + _item = KeyBundle().serialize(kb) + _nkb = KeyBundle().deserialize(_item) + assert len(kb) == 3 + assert len(kb.get('rsa')) == 2 + assert len(kb.get('oct')) == 1 + + +def test_key_issuer(): + kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", ["sig"]) + assert len(kb) == 1 + issuer = KeyIssuer() + issuer.add(kb) + + _item = item.KeyIssuer().serialize(issuer) + _iss = item.KeyIssuer().deserialize(_item) + + assert len(_iss) == 1 # 1 key + assert len(_iss.get('sig', 'rsa')) == 1 # 1 RSA key + _kb = _iss[0] + assert kb.difference(_kb) == [] # no difference From 1d77678a5e3f69a88f0a8419e614358a32832d3b Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:23:22 +0200 Subject: [PATCH 09/50] Missing arguments. --- aslist_tests/test_43_key_bundle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aslist_tests/test_43_key_bundle.py b/aslist_tests/test_43_key_bundle.py index b25d681..3a66ebd 100755 --- a/aslist_tests/test_43_key_bundle.py +++ b/aslist_tests/test_43_key_bundle.py @@ -984,6 +984,9 @@ def test_export_inactive(): 'imp_jwks', 'keys', 'last_updated', + 'last_local', + 'last_remote', + 'local', 'remote', 'time_out'} From 1d3ecc82029c9b65391e3f7ab1dd2ac08289d4a5 Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:43:24 +0200 Subject: [PATCH 10/50] Repr similar to KeyJar. --- src/cryptojwt/key_issuer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cryptojwt/key_issuer.py b/src/cryptojwt/key_issuer.py index bb6dcf7..11fbee4 100755 --- a/src/cryptojwt/key_issuer.py +++ b/src/cryptojwt/key_issuer.py @@ -49,7 +49,7 @@ def __init__(self, ca_certs=None, keybundle_cls=KeyBundle, self.httpc_params = httpc_params or {} def __repr__(self) -> str: - return ''.format(self.name, self._bundles) + return ''.format(self.name, self.key_summary()) def __getitem__(self, item): return self.get_bundles()[item] From 14c4c212a561846fdcc6b1dea37e946031cf9172 Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 28 May 2020 10:10:44 +0200 Subject: [PATCH 11/50] Worked on persistent storage. --- aslist_tests/private_jwks.json | 2 +- aslist_tests/test_43_key_bundle.py | 1017 ---------------------------- aslist_tests/test_44_key_issuer.py | 585 ---------------- aslist_tests/test_45_key_jar.py | 732 ++++++++++---------- src/cryptojwt/jwk/__init__.py | 6 + src/cryptojwt/jwk/jwk.py | 2 +- src/cryptojwt/key_bundle.py | 58 +- src/cryptojwt/key_issuer.py | 44 +- src/cryptojwt/key_jar.py | 187 +++-- src/cryptojwt/serialize/item.py | 60 +- src/cryptojwt/utils.py | 26 + tests/test_02_jwk.py | 33 +- tests/test_40_serialize.py | 22 - 13 files changed, 557 insertions(+), 2217 deletions(-) delete mode 100755 aslist_tests/test_43_key_bundle.py delete mode 100755 aslist_tests/test_44_key_issuer.py diff --git a/aslist_tests/private_jwks.json b/aslist_tests/private_jwks.json index 14fbc2b..e4a3b0a 100644 --- a/aslist_tests/private_jwks.json +++ b/aslist_tests/private_jwks.json @@ -1 +1 @@ -{"keys": [{"kty": "RSA", "use": "sig", "kid": "ZVp5MEtSdGQ5MjNjUGozUktMZV9Sd0c4N25PUGlQcUtIOVo5b0l0S3dZMA", "n": "oQfNSj9XSCBg5oBztL2TixZT-R8CVvSE0Y1onK40x1VDPfMTx7ezoFo72tiTASsdpAsChG9nkzT0iD5iFcv4c1LCTkqbaSdOzmZFu79Bjv1LVLboFsoVZNYhjRxCyoq1PooR7pKiaI1t531EKrnm4CrjuHk5egWG1cmp61_M3zMCrWcSODXGaFbJOfYfWSIVhD5cd7J4Hpl7rlA-TgD_gBfzL0kILDqvXln2oig_0yOXc7cpneLw9dSZiatyv_deCID5O0Cwg832vwrXN1az-No-ln37onxE03M9VZXqP54Nl5OPQ_cMg0UAGObxr5iyNGe_i6hTUJea0RuJ6sEnyw", "e": "AQAB", "d": "Sm2DukSKgADPKNrIIArbbhb02xk1CKHd3clBR-HQ7S0Adlqqks3ajUwHjEA7ufeGrLKWCEZBli2MtIg456AuBoeC3ZLoP_L2HrnwkzV0BLYYImCj5xyiRMggG8urJ1hzKyO_5AgMXsy3tp4Uarcf-g540GPfaAGz745VJkBSPfrnGYNPWIJbtbwlGJJz8TPaJRpJnAlolq6VeH5BibfoQhU1T5p0q4kLr_yAehQiKW4Y6sVnkuZjjf4g1P6GKqlnUIWS4swmbf1rjibDkysb9bXB1nV6YypVFWX6Cv_WrDUfSSH1MdR6zjOZIoZ_EHDONfP3VjABsefkv6Matz1qOQ", "p": "zG2BfrmeN11HJP43paO8wJR6FzQotv0RgdmLc6iRmPfcCyENtTWXPJo3EPxwrDX5zPbH7i814Q2tCeDp_gPqtBfbLCEV3Gla5cvuDO3K3MW6YhPLDfujoDnojelV7bcLGK0nDnQUyal9KhX4Wk7LddlQ3W8NsB2ymoKuSVF3Pic", "q": "yaeVP0lm1biaOnETbruxnoXeF4W4c4nPm7mBx9CzzCsSHqCpcWNtPUtOQc3wvVM2MdLoTJPalfNYyIkpWN5u5w-Un-SNNXY1BSwOFCTt4BETiwmjmU9rWzxfnQhsbSw6UrtoKjRQZNHD64-1lyRb1HlLBGfx6mb3gQ7qg-2Fs70"}, {"kty": "EC", "use": "sig", "kid": "bzJuTlhLSms0Wmh1TVkzbDRYVHEtN01CUnItemhiQWo0a0hiYVpWSGp4VQ", "crv": "P-256", "x": "AYZz_krBfwtrsXHJ_q3cgvZPQCY1nHgQWUn9bUNCgvU", "y": "iVmtoxf34F3jVlO3mOaHJDa_x1ehiCiB-fDM5iAq0fM", "d": "gPe7wnZMWJ3iFq2rYIIu7d5rWXHdYql3PJoDuxfFSrY"}, {"kty": "EC", "use": "sig", "kid": "WTNtS1dpVlBxN2Q2Q3hOb0dsTF8xenZWUm9Xa0pNTDRvTUdlQmN0aWhZcw", "crv": "P-384", "x": "aKHmXvAowb7XY8iMT_LtbXFHq99JYEo9anfdMdnQ5wpzRKusUznfA0aTHI6Mul3_", "y": "b_kV4fPElf9mDss6RkU31BZPCs4w9zAIxpbj7Rk74LK8q0BxWAYoyUfMs6d_Ywme", "d": "IHXdLJakx41Rw7FXbYLQHuczwALL2EZWboNEGGF8wP-fuG7NOCrggPam-oQR-QML"}]} \ No newline at end of file +{"keys": [{"kty": "RSA", "use": "sig", "kid": "dTZKdDFabEJoSEVKaGQ4anRLd2x3YTJWQW93ejVGUWV3Z0JJZFpvZnhUTQ", "n": "1uPVViKYxyTJ1B1_wiQCSQJrwkLKXQTg6zYy2I1JX3W3gq6i5QD8XxShR62GFdcOj0NkqR2ZiyobVqhKJ3TKErRH1mtxjFuEf-o9h6B5j6Rou6GN4TNv4h-Ed9mG5KyTI364aLlpVT-LcthnihUYwgaq7iKN-OQQD0FHPa0HBouk4QJTLRIkIF2QmSRGAJlnFevzZr0O9vAycGJH1ksTsUioP-oeNtxThlQEOi0lIF5dPGI-ZcQDpdhAHj-C5iBiyi5s4_yFM2UXXF2ZkIVkezfWPKKSQPq7yfCnFAtdeQ5stpfpG0mMXtv1zfTk-Y8-WYMnExwen7F024ZCavjlqw", "e": "AQAB", "d": "JJe3hGtvyLmjBNPhJZYsLXKUFwh4nU5vXp5kGiw1CmRpU3-ZjZWVZDuHG0WZR67Pc-XuBj5cHy6UaTVPK1jf8D9y3Dh_pX8QGRgyUh4plSRSEWF5X5f6vW7Qh_gq2FXq2GiDzpGENlgTzwK63vCovqGUCekoc_GiKnbbQs1sHNjq0WKCSrXZcfsdpdjsjksiQmmfdblPplTuYG4t2GvfYzGXgepUw2LLyJ8KCdSsXyebK9-J8laxk3El0wFbxbI2C7Y8H5BPCN7Cp6zcaenYkYYV7IGy1Z21kv-lTaBXzu23PO5vhH2bzyklvG_a0RbmrrTHr5OCWnl1-BALuYfpQQ", "p": "8_tdp7rJyRrpq4wEPxJrcOvrcrknMG03cjEePV5ozRFFeBVg2nkuESR-JHoxgF3bBOzyurB5eVDT11seZEiisThMzk6OZSnoNsDgk3WTm0c_Yl7qn0dwZANOCYcEIOUe9ouR3kGr1iJ3DZ9o7tYLJ3sb6-oCH1Cakf53iR_RXy8", "q": "4XmdnKVLvzkZP4A4YQ2ROquoAbPOoomd2aHkGndLP9tDuPxur4PiZVrMFjuzMob2uJMW7Y1FT-0WLk9aCVl0P6f893RL1yfRZVKlHopgYxe9jshwk-5VawdLn6HRtbZ-F8gp6KZMaK8oyTk-50CnsizeACcxcVEFQkFNB_v6IkU"}, {"kty": "EC", "use": "sig", "kid": "WDQzSTBkY21kZVZ0YXAzbXZVTUFPQTdLWXpQQ3QzODF6dHJMQk5LSlBpRQ", "crv": "P-256", "x": "PhbU6LJ7Xc75T8YvCcmuBLfWGQ91AVakWlbr0Dk3wgo", "y": "Gg_W4TuhRB4nN8jAXWuOyEg36gbKErLL406Ngh6Cgk8", "d": "y4Sle7C9FHcBnXInlnIjPjKx73Djq0YDpH7FgCihA-g"}, {"kty": "EC", "use": "sig", "kid": "aVdwOVAyVWlzYUhwMHpGRVgxc2hkRDRXWGdIQUp1TVJfc1diVkdjb2w4VQ", "crv": "P-384", "x": "pwaV9kgGNyu7mEkCxj_lRssXgmT-kdIhoK-8fhJn6s1tbEEgBbPQj8Pubqdq_fld", "y": "9VQIWfNfusIb2ZDxwViSzGMNeT_QZCu16HqHx8s5F_l--VfTwqdP0NTtWogT-soO", "d": "kHsAXZL6uhzVCKZQzU0iRiH3SnY9a6MufFIJW9jLL_RSyC5QbUasQGRlZm9j2gSI"}]} \ No newline at end of file diff --git a/aslist_tests/test_43_key_bundle.py b/aslist_tests/test_43_key_bundle.py deleted file mode 100755 index 3a66ebd..0000000 --- a/aslist_tests/test_43_key_bundle.py +++ /dev/null @@ -1,1017 +0,0 @@ -# pylint: disable=missing-docstring,no-self-use -import json -import os -import shutil -import time - -import pytest -import requests -import responses -from abstorage.storages.absqlalchemy import AbstractStorageSQLAlchemy -from cryptography.hazmat.primitives.asymmetric import rsa -from requests_mock import GET - -from cryptojwt.jwk.ec import ECKey -from cryptojwt.jwk.ec import new_ec_key -from cryptojwt.jwk.hmac import SYMKey -from cryptojwt.jwk.rsa import RSAKey -from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file -from cryptojwt.jwk.rsa import new_rsa_key -from cryptojwt.key_bundle import KeyBundle -from cryptojwt.key_bundle import build_key_bundle -from cryptojwt.key_bundle import dump_jwks -from cryptojwt.key_bundle import init_key -from cryptojwt.key_bundle import key_diff -from cryptojwt.key_bundle import key_gen -from cryptojwt.key_bundle import key_rollover -from cryptojwt.key_bundle import keybundle_from_local_file -from cryptojwt.key_bundle import rsa_init -from cryptojwt.key_bundle import unique_keys -from cryptojwt.key_bundle import update_key_bundle - -__author__ = 'Roland Hedberg' - -BASE_PATH = os.path.abspath( - os.path.join(os.path.dirname(__file__), "test_keys")) - -BASEDIR = os.path.abspath(os.path.dirname(__file__)) - - -def full_path(local_file): - return os.path.join(BASEDIR, local_file) - - -RSAKEY = os.path.join(BASE_PATH, "cert.key") -RSA0 = os.path.join(BASE_PATH, "rsa.key") -EC0 = os.path.join(BASE_PATH, 'ec.key') -CERT = full_path("../tests/cert.pem") - -JWK0 = {"keys": [ - {'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", - 'n': - 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY' - '2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfK' - 'qoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8'} -]} - -JWK1 = {"keys": [ - { - "n": - 'zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S' - '_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFY' - 'Inq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVb' - 'CGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znan' - 'LwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MX' - 'sGxBHf3AKT5w', - "e": "AQAB", "kty": "RSA", "kid": "rsa1"}, - { - "k": - 'YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNT' - 'Y0NzMzYjE', - "kty": "oct"}, -]} - -JWK2 = { - "keys": [ - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "kriMPdmBvx68skT8-mPAB3BseeA", - "kty": "RSA", - "n": - 'kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS' - '_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1uml94fLyq_AXbwo9yAduf4dCHT' - 'P8CWR1dnDR-Qnz_4PYlWVEuuHHONOw_blbfdMjhY-C_BYM2E3pRxbohBb3x__C' - 'fueV7ddz2LYiH3wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_' - 'GTgWN8A-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQtSsjQtI--Xs' - 'okxRDqYLwus1I1SihgbV_STTg5enufuw', - "use": "sig", - "x5c": [ - 'MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKz' - 'ApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcN' - 'MTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW' - '50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEF' - 'AAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs' - '5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94f' - 'Lyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C' - '/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHF' - 'i3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp' - '+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2Iw' - 'YDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYW' - 'Njb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49Y' - 'D0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDb' - 'dNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajy' - 'vlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5Uqn' - 'I7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF4' - '6aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODY' - 'RMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ' - ], - "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", - "kty": "RSA", - "n": - 'vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhz' - 'h23V9Tkq-RtwN1Vs_z57hO82kkzL-cQHZX3bMJD-GEGOKXCEXURN7VMyZWMAuz' - 'QoW9vFb1k3cR1RW_EW_P-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5y' - 'Cw5T_Vuwqqsio3V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_K' - 'AS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IAbsk1wRtWDn' - 'dhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ', - "use": "sig", - "x5c": [ - "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" - "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" - "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" - "/uPhEf7zVizjfcr/ISGFe9+yUO" - "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" - "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" - "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" - "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" - "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" - "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" - "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" - "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" - "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" - "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" - "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" - "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" - "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" - "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" - ], - "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", - "kty": "RSA", - "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" - "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" - "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" - "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" - "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" - "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" - "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" - "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" - "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" - "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" - "+Iqzmn" - "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" - "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" - "qA==" - ], - "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", - "kty": "RSA", - "n": - "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" - "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" - "-KXVD02jdmX2HNdYJdPyc0c", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" - "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" - "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" - "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" - "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" - "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" - "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" - "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" - "ig==" - ], - "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" - } - ] -} - -JWKS_DICT = {"keys": [ - { - "n": - u"zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVbCGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": u"AQAB", - "kty": "RSA", - "kid": "5-VBFv40P8D4I-7SFz7hMugTbPs", - "use": "enc" - }, - { - "k": u"YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "oct", - "use": "enc" - }, - { - "kty": "EC", - "kid": "7snis", - "use": "sig", - "x": u'q0WbWhflRbxyQZKFuQvh2nZvg98ak-twRoO5uo2L7Po', - "y": u'GOd2jL_6wa0cfnyA0SmEhok9fkYEnAHFKLLM79BZ8_E', - "crv": "P-256" - } -]} - -if os.path.isdir('keys'): - shutil.rmtree('keys') - -ABS_STORAGE_SQLALCHEMY = dict( - driver='sqlalchemy', - url='sqlite:///:memory:', - params=dict(table='Thing'), - handler=AbstractStorageSQLAlchemy -) - -STORAGE_CONFIG = { - 'name': '', - 'class': 'abstorage.type.list.ASList', - 'kwargs': { - 'io_class': 'cryptojwt.serialize.item.JWK', - 'storage_config': ABS_STORAGE_SQLALCHEMY - } -} - - -def test_with_sym_key(): - kc = KeyBundle({"kty": "oct", "key": "highestsupersecret", "use": "sig"}, - storage_conf=STORAGE_CONFIG) - assert len(kc.get("oct")) == 1 - assert len(kc.get("rsa")) == 0 - assert kc.remote is False - assert kc.source is None - - -def test_with_2_sym_key(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) - assert len(kb.get("oct")) == 2 - assert len(kb) == 2 - - assert kb.get_key_with_kid('kid') is None - assert len(kb.kids()) == 2 - - -def test_remove_sym(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) - assert len(kb) == 2 - keys = kb.get('oct') - kb.remove(keys[0]) - assert len(kb) == 1 - - -def test_remove_key_sym(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) - assert len(kb) == 2 - keys = kb.get('oct') - kb.remove(keys[0]) - assert len(kb) == 1 - - # This should not work - kb.remove_keys_by_type('rsa') - # should still be one - assert len(kb) == 1 - - -def test_rsa_init(): - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) - assert kb - assert len(kb) == 2 - assert len(kb.get('rsa')) == 2 - - -def test_rsa_init_under_spec(): - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024}, storage_conf=STORAGE_CONFIG) - assert kb - assert len(kb) == 2 - assert len(kb.get('rsa')) == 2 - - -def test_unknown_source(): - with pytest.raises(ImportError): - KeyBundle(source='foobar', storage_conf=STORAGE_CONFIG) - - -def test_ignore_unknown_types(): - kb = KeyBundle({ - "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" - "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", - "use": "sig", - "alg": "EdDSA", - "kty": "OKP", - "crv": "Ed25519", - "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" - }, - storage_conf=STORAGE_CONFIG) - - assert len(kb) == 0 - - -def test_remove_rsa(): - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, - storage_conf=STORAGE_CONFIG) - assert len(kb) == 2 - keys = kb.get('rsa') - assert len(keys) == 2 - kb.remove(keys[0]) - assert len(kb) == 1 - - -def test_key_mix(): - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, - storage_conf=STORAGE_CONFIG) - _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) - kb.append(_sym) - assert len(kb) == 3 - assert len(kb.get('rsa')) == 2 - assert len(kb.get('oct')) == 1 - - kb.remove(_sym) - - assert len(kb) == 2 - assert len(kb.get('rsa')) == 2 - assert len(kb.get('oct')) == 0 - - -def test_get_all(): - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, - storage_conf=STORAGE_CONFIG - ) - _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) - kb.append(_sym) - assert len(kb.get()) == 3 - - _k = kb.keys() - assert len(_k) == 3 - - -def test_keybundle_from_local_der(): - kb = keybundle_from_local_file( - "{}".format(RSA0), - "der", ['enc'], storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - keys = kb.get('rsa') - assert len(keys) == 1 - _key = keys[0] - assert isinstance(_key, RSAKey) - assert _key.kid - - -def test_ec_keybundle_from_local_der(): - kb = keybundle_from_local_file( - "{}".format(EC0), - "der", ['enc'], keytype='EC', storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - keys = kb.get('ec') - assert len(keys) == 1 - _key = keys[0] - assert _key.kid - assert isinstance(_key, ECKey) - - -def test_keybundle_from_local_der_update(): - kb = keybundle_from_local_file( - "file://{}".format(RSA0), - "der", ['enc'], storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - keys = kb.get('rsa') - assert len(keys) == 1 - _key = keys[0] - assert _key.kid - assert isinstance(_key, RSAKey) - - kb.update() - - # Nothing should change - assert len(kb) == 1 - keys = kb.get('rsa') - assert len(keys) == 1 - _key = keys[0] - assert _key.kid - assert isinstance(_key, RSAKey) - - -def test_creat_jwks_sym(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - kb = KeyBundle([a], storage_conf=STORAGE_CONFIG) - _jwks = kb.jwks() - _loc = json.loads(_jwks) - assert list(_loc.keys()) == ["keys"] - assert set(_loc['keys'][0].keys()) == {'kty', 'use', 'k', 'kid'} - - -def test_keybundle_from_local_jwks_file(): - kb = keybundle_from_local_file( - "file://{}".format(os.path.join(BASE_PATH, "jwk.json")), "jwks", ["sig"], - storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - - -def test_keybundle_from_local_jwks(): - kb = keybundle_from_local_file( - "{}".format(os.path.join(BASE_PATH, "jwk.json")), "jwks", ["sig"], - storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - - -def test_update(): - kc = KeyBundle([{"kty": "oct", "key": "highestsupersecret", "use": "sig"}], - storage_conf=STORAGE_CONFIG) - assert len(kc.get("oct")) == 1 - assert len(kc.get("rsa")) == 0 - assert kc.remote is False - assert kc.source is None - - kc.update() # Nothing should happen - assert len(kc.get("oct")) == 1 - assert len(kc.get("rsa")) == 0 - assert kc.remote is False - assert kc.source is None - - -def test_update_RSA(): - kc = keybundle_from_local_file(RSAKEY, "der", ["sig"], storage_conf=STORAGE_CONFIG) - assert kc.remote is False - assert len(kc.get("oct")) == 0 - assert len(kc.get("RSA")) == 1 - - key = kc.get("RSA")[0] - assert isinstance(key, RSAKey) - - kc.update() - assert kc.remote is False - assert len(kc.get("oct")) == 0 - assert len(kc.get("RSA")) == 1 - - key = kc.get("RSA")[0] - assert isinstance(key, RSAKey) - - -def test_outdated(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) - keys = kb.keys() - now = time.time() - keys[0].inactive_since = now - 60 - kb.set(keys) - kb.remove_outdated(30) - assert len(kb) == 1 - - -def test_dump_jwks(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb2 = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) - - kb1 = rsa_init({'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) - - # Will not dump symmetric keys - dump_jwks([kb1, kb2], 'jwks_combo') - - # Now read it - - nkb = KeyBundle(source='file://jwks_combo', fileformat='jwks', storage_conf=STORAGE_CONFIG) - - assert len(nkb) == 2 - # both RSA keys - assert len(nkb.get('rsa')) == 2 - - # Will dump symmetric keys - dump_jwks([kb1, kb2], 'jwks_combo', symmetric_too=True) - - # Now read it - nkb = KeyBundle(source='file://jwks_combo', fileformat='jwks', storage_conf=STORAGE_CONFIG) - - assert len(nkb) == 4 - # two RSA keys - assert len(nkb.get('rsa')) == 2 - # two symmetric keys - assert len(nkb.get('oct')) == 2 - - -def test_mark_as_inactive(): - desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) - assert len(kb.keys()) == 1 - for k in kb.keys(): - kb.mark_as_inactive(k.kid) - desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb.do_keys([desc]) - assert len(kb.keys()) == 2 - assert len(kb.active_keys()) == 1 - - -def test_copy(): - desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) - assert len(kb.keys()) == 1 - for k in kb.keys(): - kb.mark_as_inactive(k.kid) - desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb.do_keys([desc]) - - kbc = kb.copy() - assert len(kbc.keys()) == 2 - assert len(kbc.active_keys()) == 1 - - -def test_local_jwk(): - _path = full_path('../tests/jwk_private_key.json') - kb = KeyBundle(source='file://{}'.format(_path), storage_conf=STORAGE_CONFIG) - assert kb - - -def test_local_jwk_copy(): - _path = full_path('../tests/jwk_private_key.json') - kb = KeyBundle(source='file://{}'.format(_path), storage_conf=STORAGE_CONFIG) - kb2 = kb.copy() - assert kb2.source == kb.source - - -@pytest.fixture() -def mocked_jwks_response(): - with responses.RequestsMock() as rsps: - yield rsps - - -def test_httpc_params_1(): - source = 'https://login.salesforce.com/id/keys' # From test_jwks_url() - # Mock response - with responses.RequestsMock() as rsps: - rsps.add(method=GET, url=source, json=JWKS_DICT, status=200) - httpc_params = {'timeout': (2, 2)} # connect, read timeouts in seconds - kb = KeyBundle(source=source, httpc=requests.request, - httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) - assert kb.do_remote() - - -def test_httpc_params_2(): - httpc_params = {'timeout': 0} - kb = KeyBundle(source='https://login.salesforce.com/id/keys', - httpc=requests.request, httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) - # Will always fail to fetch the JWKS because the timeout cannot be set - # to 0s - assert not kb.update() - - -def test_update_2(): - rsa_key = new_rsa_key() - _jwks = {"keys": [rsa_key.serialize()]} - fname = 'tmp_jwks.json' - with open(fname, 'w') as fp: - fp.write(json.dumps(_jwks)) - - kb = KeyBundle(source="file://{}".format(fname), fileformat='jwks', storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - - # Added one more key - ec_key = new_ec_key(crv='P-256', key_ops=["sign"]) - _jwks = {'keys': [rsa_key.serialize(), ec_key.serialize()]} - - with open(fname, 'w') as fp: - fp.write(json.dumps(_jwks)) - - kb.update() - assert len(kb) == 2 - - -def test_update_mark_inactive(): - rsa_key = new_rsa_key() - _jwks = {"keys": [rsa_key.serialize()]} - fname = 'tmp_jwks.json' - with open(fname, 'w') as fp: - fp.write(json.dumps(_jwks)) - - kb = KeyBundle(source="file://{}".format(fname), fileformat='jwks', storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - - # new set of keys - rsa_key = new_rsa_key(alg="RS256") - ec_key = new_ec_key(crv='P-256') - _jwks = {'keys': [rsa_key.serialize(), ec_key.serialize()]} - - with open(fname, 'w') as fp: - fp.write(json.dumps(_jwks)) - - kb.update() - # 2 active and 1 inactive - assert len(kb) == 3 - assert len(kb.active_keys()) == 2 - - assert len(kb.get('rsa')) == 1 - assert len(kb.get('rsa', only_active=False)) == 2 - - -def test_loads_0(): - kb = KeyBundle(JWK0, storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - key = kb.get("rsa")[0] - assert key.kid == 'abc' - assert key.kty == 'RSA' - - -def test_loads_1(): - jwks = { - "keys": [ - { - 'kty': 'RSA', - 'use': 'sig', - 'e': 'AQAB', - "n": - 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8', - 'kid': "1" - }, { - 'kty': 'RSA', - 'use': 'enc', - 'e': 'AQAB', - "n": - 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8', - 'kid': "2" - } - ] - } - - kb = KeyBundle(jwks, storage_conf=STORAGE_CONFIG) - - assert len(kb) == 2 - assert set(kb.kids()) == {'1', '2'} - - -def test_dump_jwk(): - kb = KeyBundle(storage_conf=STORAGE_CONFIG) - kb.append(RSAKey(pub_key=import_rsa_key_from_cert_file(CERT))) - jwks = kb.jwks() - - _wk = json.loads(jwks) - assert list(_wk.keys()) == ["keys"] - assert len(_wk["keys"]) == 1 - assert set(_wk["keys"][0].keys()) == {"kty", "e", "n"} - - kb2 = KeyBundle(_wk, storage_conf=STORAGE_CONFIG) - - assert len(kb2) == 1 - key = kb2.get("rsa")[0] - assert key.kty == 'RSA' - assert isinstance(key.public_key(), rsa.RSAPublicKey) - - -JWKS_DICT = {"keys": [ - { - "n": - u"zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVbCGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": u"AQAB", - "kty": "RSA", - "kid": "5-VBFv40P8D4I-7SFz7hMugTbPs", - "use": "enc" - }, - { - "k": u"YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "oct", - "use": "enc" - }, - { - "kty": "EC", - "kid": "7snis", - "use": "sig", - "x": u'q0WbWhflRbxyQZKFuQvh2nZvg98ak-twRoO5uo2L7Po', - "y": u'GOd2jL_6wa0cfnyA0SmEhok9fkYEnAHFKLLM79BZ8_E', - "crv": "P-256" - } -]} - - -def test_keys(): - kb = KeyBundle(JWKS_DICT, storage_conf=STORAGE_CONFIG) - - assert len(kb) == 3 - - assert len(kb.get('rsa')) == 1 - assert len(kb.get('oct')) == 1 - assert len(kb.get('ec')) == 1 - - -EXPECTED = [ - b'iA7PvG_DfJIeeqQcuXFmvUGjqBkda8In_uMpZrcodVA', - b'akXzyGlXg8yLhsCczKb_r8VERLx7-iZBUMIVgg2K7p4', - b'kLsuyGef1kfw5-t-N9CJLIHx_dpZ79-KemwqjwdrvTI' -] - - -def test_thumbprint(): - kb = KeyBundle(JWKS_DICT, storage_conf=STORAGE_CONFIG) - for key in kb: - txt = key.thumbprint('SHA-256') - assert txt in EXPECTED - - -@pytest.mark.network -def test_jwks_url(): - keys = KeyBundle(source='https://login.salesforce.com/id/keys', storage_conf=STORAGE_CONFIG) - # Forces read from the network - keys.update() - assert len(keys) - - -KEYSPEC = [ - {"type": "RSA", "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]} -] - -KEYSPEC_2 = [ - {"type": "RSA", "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "EC", "crv": "P-384", "use": ["sig"]} -] - -KEYSPEC_3 = [ - {"type": "RSA", "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "EC", "crv": "P-384", "use": ["sig"]}, - {"type": "EC", "crv": "P-521", "use": ["sig"]} -] - -KEYSPEC_4 = [ - {"type": "RSA", "use": ["sig"]}, - {"type": "RSA", "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "EC", "crv": "P-384", "use": ["sig"]} -] - -KEYSPEC_5 = [ - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "EC", "crv": "P-384", "use": ["sig"]} -] - -KEYSPEC_6 = [ - {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'code'}, - {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'token'}, - {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'refresh_token'} -] - - -def test_key_diff_none(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - - diff = key_diff(_kb, KEYSPEC) - assert not diff - - -def test_key_diff_add_one_ec(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - - diff = key_diff(_kb, KEYSPEC_2) - assert diff - assert set(diff.keys()) == {'add'} - assert len(diff['add']) == 1 - assert diff['add'][0].kty == 'EC' - - -def test_key_diff_add_two_ec(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - - diff = key_diff(_kb, KEYSPEC_3) - assert diff - assert set(diff.keys()) == {'add'} - assert len(diff['add']) == 2 - assert diff['add'][0].kty == 'EC' - - -def test_key_diff_add_ec_and_rsa(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - - diff = key_diff(_kb, KEYSPEC_4) - assert diff - assert set(diff.keys()) == {'add'} - assert len(diff['add']) == 2 - assert set([k.kty for k in diff['add']]) == {'EC', 'RSA'} - - -def test_key_diff_add_ec_del_rsa(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - - diff = key_diff(_kb, KEYSPEC_5) - assert diff - assert set(diff.keys()) == {'add', 'del'} - assert len(diff['add']) == 1 - assert len(diff['del']) == 1 - assert diff['add'][0].kty == 'EC' - assert diff['del'][0].kty == 'RSA' - - -def test_key_bundle_update_1(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - diff = key_diff(_kb, KEYSPEC_2) - update_key_bundle(_kb, diff) - - # There should be 3 keys - assert len(_kb) == 3 - - # one RSA - assert len(_kb.get('RSA')) == 1 - - # 2 EC - assert len(_kb.get('EC')) == 2 - - -def test_key_bundle_update_2(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - diff = key_diff(_kb, KEYSPEC_4) - update_key_bundle(_kb, diff) - - # There should be 3 keys - assert len(_kb) == 4 - - # one RSA - assert len(_kb.get('RSA')) == 2 - - # 2 EC - assert len(_kb.get('EC')) == 2 - - -def test_key_bundle_update_3(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - diff = key_diff(_kb, KEYSPEC_5) - assert set(diff.keys()) == {'add', 'del'} # Add an EC and delete an RSA key - update_key_bundle(_kb, diff) - - # There should be 3 keys - assert len(_kb) == 3 - - # One inactive. Only active is implicit - assert len(_kb.get()) == 2 - - # one inactive RSA - assert len(_kb.get('RSA', only_active=False)) == 1 - assert len(_kb.get('RSA')) == 0 - - # 2 EC - assert len(_kb.get('EC')) == 2 - assert len(_kb.get('EC', only_active=False)) == 2 - - -def test_key_rollover(): - kb_0 = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - assert len(kb_0.get(only_active=False)) == 2 - assert len(kb_0.get()) == 2 - - kb_1 = key_rollover(kb_0) - - assert len(kb_1.get(only_active=False)) == 4 - assert len(kb_1.get()) == 2 - - -def test_build_key_bundle_sym(): - _kb = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - assert len(_kb) == 3 - - assert len(_kb.get('RSA')) == 0 - assert len(_kb.get('EC')) == 0 - assert len(_kb.get('oct')) == 3 - - -def test_key_bundle_difference_none(): - _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - _kb1 = KeyBundle(storage_conf=STORAGE_CONFIG) - _kb1.extend(_kb0.keys()) - - assert _kb0.difference(_kb1) == [] - - -def test_key_bundle_difference(): - _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - _kb1 = build_key_bundle(key_conf=KEYSPEC_2, storage_conf=STORAGE_CONFIG) - - assert _kb0.difference(_kb1) == _kb0.keys() - assert _kb1.difference(_kb0) == _kb1.keys() - - -def test_unique_keys_1(): - _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - _kb1 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - - keys = _kb0.keys() - keys.extend(_kb1.keys()) - - # All of them - assert len(unique_keys(keys)) == 6 - - -def test_unique_keys_2(): - _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - _kb1 = KeyBundle(storage_conf=STORAGE_CONFIG) - _kb1.extend(_kb0.keys()) - - keys = _kb0.keys() - keys.extend(_kb1.keys()) - - # 3 of 6 - assert len(unique_keys(keys)) == 3 - - -def test_key_gen_rsa(): - _jwk = key_gen("RSA", kid="kid1") - assert _jwk - assert _jwk.kty == "RSA" - assert _jwk.kid == 'kid1' - - assert isinstance(_jwk, RSAKey) - - -def test_init_key(): - spec = { - "type": "RSA", - "kid": "one" - } - - filename = full_path("../tests/tmp_jwk.json") - if os.path.isfile(filename): - os.unlink(filename) - - _key = init_key(filename, **spec) - assert _key.kty == "RSA" - assert _key.kid == 'one' - - assert os.path.isfile(filename) - - # Should not lead to any change - _jwk2 = init_key(filename, **spec) - assert _key == _jwk2 - - _jwk3 = init_key(filename, "RSA", "two") - assert _key != _jwk3 - - # Now _jwk3 is stored in the file - _jwk4 = init_key(filename, "RSA") - assert _jwk4 == _jwk3 - - -def test_export_inactive(): - desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) - assert len(kb.keys()) == 1 - for k in kb.keys(): - kb.mark_as_inactive(k.kid) - desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb.do_keys([desc]) - res = kb.dump() - assert set(res.keys()) == {'cache_time', - 'fileformat', - 'httpc_params', - 'imp_jwks', - 'keys', - 'last_updated', - 'last_local', - 'last_remote', - 'local', - 'remote', - 'time_out'} - - kb2 = KeyBundle(storage_conf=STORAGE_CONFIG).load(res) - assert len(kb2.keys()) == 2 - assert len(kb2.active_keys()) == 1 - - -def test_remote(): - source = 'https://example.com/keys.json' - # Mock response - with responses.RequestsMock() as rsps: - rsps.add(method="GET", url=source, json=JWKS_DICT, status=200) - httpc_params = {'timeout': (2, 2)} # connect, read timeouts in seconds - kb = KeyBundle(source=source, httpc=requests.request, - httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) - kb.do_remote() - - exp = kb.dump() - kb2 = KeyBundle(storage_conf=STORAGE_CONFIG).load(exp) - assert kb2.source == source - assert len(kb2.keys()) == 3 - assert len(kb2.get("rsa")) == 1 - assert len(kb2.get("oct")) == 1 - assert len(kb2.get("ec")) == 1 - assert kb2.httpc_params == {'timeout': (2, 2)} - assert kb2.imp_jwks - assert kb2.last_updated diff --git a/aslist_tests/test_44_key_issuer.py b/aslist_tests/test_44_key_issuer.py deleted file mode 100755 index 0526865..0000000 --- a/aslist_tests/test_44_key_issuer.py +++ /dev/null @@ -1,585 +0,0 @@ -import os -import time - -import pytest -from abstorage.storages.absqlalchemy import AbstractStorageSQLAlchemy - -from cryptojwt.exception import JWKESTException -from cryptojwt.key_bundle import KeyBundle -from cryptojwt.key_bundle import keybundle_from_local_file -from cryptojwt.key_issuer import KeyIssuer -from cryptojwt.key_issuer import build_keyissuer - -__author__ = 'Roland Hedberg' - -BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), - "test_keys")) -RSAKEY = os.path.join(BASE_PATH, "cert.key") -RSA0 = os.path.join(BASE_PATH, "rsa.key") -EC0 = os.path.join(BASE_PATH, "ec.key") -BASEDIR = os.path.abspath(os.path.dirname(__file__)) - - -def full_path(local_file): - return os.path.join(BASEDIR, local_file) - - -JWK0 = { - "keys": [ - { - 'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", - 'n': - 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5' - 'B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8' - } - ] -} - -JWK1 = { - "keys": [ - { - "n": - "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" - "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" - "-NvS-aG_jN5cstVbCGWE20H0vF" - "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" - "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" - "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": "AQAB", "kty": "RSA", "kid": "rsa1" - }, - { - "k": - "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "oct" - }, - ] -} - -JWK2 = { - "keys": [ - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "kriMPdmBvx68skT8-mPAB3BseeA", - "kty": "RSA", - "n": - "kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1um" - "l94fLyq_AXbwo9yAduf4dCHTP8CWR1dnDR" - "-Qnz_4PYlWVEuuHHONOw_blbfdMjhY" - "-C_BYM2E3pRxbohBb3x__CfueV7ddz2LYiH3" - "wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_GTgWN8A" - "-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQt" - "SsjQtI--XsokxRDqYLwus1I1SihgbV_STTg5enufuw", - "use": "sig", - "x5c": [ - "MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb" - "2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb2" - "50cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipg" - "H0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6" - "/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Q" - "nz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x" - "//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13S" - "QwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp" - "+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5en" - "ufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJ" - "vbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdN" - "VGKCmSf8M65b8h0NwlIjGGGy" - "/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADD" - "kN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5" - "+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8y" - "PJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW" - "+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ" - ], - "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", - "kty": "RSA", - "n": - "vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhzh23V9Tkq" - "-RtwN1Vs_z57hO82kkzL-cQHZX3bMJ" - "D-GEGOKXCEXURN7VMyZWMAuzQoW9vFb1k3cR1RW_EW_P" - "-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5yCw5T_Vuwqqsio3" - "V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_KAS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IA" - "bsk1wRtWDndhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ", - "use": "sig", - "x5c": [ - "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" - "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" - "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" - "/uPhEf7zVizjfcr/ISGFe9+yUO" - "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" - "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" - "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" - "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" - "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" - "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" - "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" - "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" - "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" - "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" - "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" - "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" - "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" - "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" - ], - "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", - "kty": "RSA", - "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" - "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" - "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" - "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" - "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" - "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" - "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" - "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" - "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" - "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" - "+Iqzmn" - "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" - "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" - "qA==" - ], - "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", - "kty": "RSA", - "n": - "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" - "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" - "-KXVD02jdmX2HNdYJdPyc0c", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" - "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" - "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" - "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" - "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" - "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" - "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" - "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" - "ig==" - ], - "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" - } - ] -} - -ABS_STORAGE_SQLALCHEMY = dict( - driver='sqlalchemy', - url='sqlite:///:memory:', - params=dict(table='Thing'), - handler=AbstractStorageSQLAlchemy -) - -STORAGE_CONFIG = { - 'KeyIssuer': { - 'name': '', - 'class': 'abstorage.type.list.ASList', - 'kwargs': { - 'io_class': 'cryptojwt.serialize.item.KeyBundle', - 'storage_config': ABS_STORAGE_SQLALCHEMY - } - }, - 'KeyBundle': { - 'name': '', - 'class': 'abstorage.type.list.ASList', - 'kwargs': { - 'io_class': 'cryptojwt.serialize.item.JWK', - 'storage_config': ABS_STORAGE_SQLALCHEMY - } - } -} - - -def test_build_key_issuer(): - keys = [ - {"type": "RSA", "use": ["enc", "sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - ] - - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - - assert len(key_issuer) == 3 # A total of 3 keys - assert len(key_issuer.get('sig')) == 2 # 2 for signing - assert len(key_issuer.get('enc')) == 1 # 1 for encryption - - -def test_build_keyissuer_usage(): - keys = [ - {"type": "RSA", "use": ["enc", "sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "oct", "use": ["enc"]}, - {"type": "oct", "use": ["enc"]}, - ] - - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - jwks_sig = key_issuer.export_jwks(usage='sig') - jwks_enc = key_issuer.export_jwks(usage='enc') - assert len(jwks_sig.get('keys')) == 2 # A total of 2 keys with use=sig - assert len(jwks_enc.get('keys')) == 3 # A total of 3 keys with use=enc - - for key in jwks_sig["keys"]: - assert "d" not in key # the JWKS shouldn't contain the private part of the keys - for key in jwks_enc["keys"]: - assert "d" not in key # the JWKS shouldn't contain the private part of the keys - - -def test_build_keyissuer_missing(tmpdir): - keys = [ - { - "type": "RSA", "key": os.path.join(tmpdir.dirname, "missing_file"), - "use": ["enc", "sig"] - }] - - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - assert key_issuer is None - - -def test_build_RSA_keyissuer_from_file(tmpdir): - keys = [{"type": "RSA", "key": RSA0, "use": ["enc", "sig"]}] - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - assert len(key_issuer) == 2 - - -def test_build_EC_keyissuer_missing(tmpdir): - keys = [ - { - "type": "EC", "key": os.path.join(tmpdir.dirname, "missing_file"), - "use": ["enc", "sig"] - }] - - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - assert key_issuer is None - - -def test_build_EC_keyissuer_from_file(tmpdir): - keys = [ - { - "type": "EC", "key": EC0, - "use": ["enc", "sig"] - }] - - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - - assert len(key_issuer) == 2 - - -class TestKeyIssuer(object): - def test_add_kb(self): - issuer = KeyIssuer(name='https://issuer.example.com', storage_conf=STORAGE_CONFIG) - kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG) - issuer.add_kb(kb) - assert len(issuer.all_keys()) == 1 - - def test_add_symmetric(self): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.add_symmetric('abcdefghijklmnop', ['sig']) - assert len(issuer.get('oct')) == 1 - - def test_add(self): - issuer = KeyIssuer(name='https://issuer.example.com', storage_conf=STORAGE_CONFIG) - kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG) - issuer.add(kb) - assert len(issuer.all_keys()) == 1 - - def test_items(self): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.add(KeyBundle( - [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, - {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}]), storage_conf=STORAGE_CONFIG) - issuer.add( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG)) - - assert len(issuer.items()) == 2 - - def test_get_enc(self): - issuer = KeyIssuer() - issuer.add(KeyBundle( - [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}]), storage_conf=STORAGE_CONFIG) - issuer.add( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG)) - - assert issuer.get('enc', 'oct') - - def test_dump_issuer_keys(self): - kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", - ["sig"], storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.add(kb) - _jwks_dict = issuer.export_jwks() - - _info = _jwks_dict['keys'][0] - assert _info == { - 'use': 'sig', - 'e': 'AQAB', - 'kty': 'RSA', - 'alg': 'RS256', - 'n': 'pKybs0WaHU_y4cHxWbm8Wzj66HtcyFn7Fh3n' - '-99qTXu5yNa30MRYIYfSDwe9JVc1JUoGw41yq2StdGBJ40HxichjE' - '-Yopfu3B58Q' - 'lgJvToUbWD4gmTDGgMGxQxtv1En2yedaynQ73sDpIK-12JJDY55pvf' - '-PCiSQ9OjxZLiVGKlClDus44_uv2370b9IN2JiEOF-a7JB' - 'qaTEYLPpXaoKWDSnJNonr79tL0T7iuJmO1l705oO3Y0TQ' - '-INLY6jnKG_RpsvyvGNnwP9pMvcP1phKsWZ10ofuuhJGRp8IxQL9Rfz' - 'T87OvF0RBSO1U73h09YP-corWDsnKIi6TbzRpN5YDw', - 'kid': 'abc' - } - - def test_no_use(self): - kb = KeyBundle(JWK0["keys"]) - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.add(kb) - enc_key = issuer.get('enc', "RSA") - assert enc_key != [] - - @pytest.mark.network - def test_provider(self): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.load_keys(jwks_uri="https://connect-op.herokuapp.com/jwks.json") - - assert issuer.all_keys() - - -def test_import_jwks(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWK1) - assert len(issuer.all_keys()) == 2 - - -def test_get_signing_key_use_undefined(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWK1) - keys = issuer.get('sig', kid='rsa1') - assert len(keys) == 1 - - keys = issuer.get('sig', key_type='rsa') - assert len(keys) == 1 - - keys = issuer.get('sig', key_type='rsa', kid='rsa1') - assert len(keys) == 1 - - -KEYDEFS = [ - {"type": "RSA", "key": '', "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]} -] - - -def test_remove_after(): - # initial key_issuer - key_issuer = build_keyissuer(KEYDEFS, storage_conf=STORAGE_CONFIG) - _old = [k.kid for k in key_issuer.all_keys() if k.kid] - assert len(_old) == 2 - - # rotate_keys = create new keys + make the old as inactive - key_issuer = build_keyissuer(KEYDEFS, key_issuer=key_issuer) - - key_issuer.remove_after = 1 - # None are remove since none are marked as inactive yet - key_issuer.remove_outdated() - - _interm = [k.kid for k in key_issuer.all_keys() if k.kid] - assert len(_interm) == 4 - - # Now mark the keys to be inactivated - _now = time.time() - for kid in _old: - key_issuer.mark_as_inactive(kid) - - key_issuer.remove_outdated(_now + 5) - - # The remainder are the new keys - _new = [k.kid for k in key_issuer.all_keys() if k.kid] - assert len(_new) == 2 - - # should not be any overlap between old and new - assert set(_new).intersection(set(_old)) == set() - - -JWK_UK = { - "keys": [ - { - "n": - "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" - "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" - "-NvS-aG_jN5cstVbCGWE20H0vF" - "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" - "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" - "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": "AQAB", "kty": "RSA", "kid": "rsa1" - }, - { - "k": - "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "buz" - }, - ] -} - - -def test_load_unknown_keytype(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWK_UK) - assert len(issuer.all_keys()) == 1 - - -JWK_FP = { - "keys": [ - {"e": "AQAB", "kty": "RSA", "kid": "rsa1"}, - ] -} - - -def test_load_missing_key_parameter(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - with pytest.raises(JWKESTException): - issuer.import_jwks(JWK_FP) - - -JWKS_SPO = { - "keys": [ - { - "kid": - "BfxfnahEtkRBG3Hojc9XGLGht_5rDBj49Wh3sBDVnzRpulMqYwMRmpizA0aSPT1fhCHYivTiaucWUqFu_GwTqA", - "use": "sig", - "alg": "ES256", - "kty": "EC", - "crv": "P-256", - "x": "1XXUXq75gOPZ4bEj1o2Z5XKJWSs6LmL6fAOK3vyMzSc", - "y": "ac1h_DwyuUxhkrD9oKMJ-b_KuiVvvSARIwT-XoEmDXs" - }, - { - "kid": - "91pD1H81rXUvrfg9mkngIG-tXjnldykKUVbITDIU1SgJvq91b8clOcJuEHNAq61eIvg8owpEvWcWAtlbV2awyA", - "use": "sig", - "alg": "ES256", - "kty": "EC", - "crv": "P-256", - "x": "2DfQoLpZS2j3hHEcHDkzV8ISx-RdLt6Opy8YZYVm4AQ", - "y": "ycvkFMBIzgsowiaf6500YlG4vaMSK4OF7WVtQpUbEE0" - }, - { - "kid": "0sIEl3MUJiCxrqleEBBF-_bZq5uClE84xp-wpt8oOI" - "-WIeNxBjSR4ak_OTOmLdndB0EfDLtC7X1JrnfZILJkxA", - "use": "sig", - "alg": "RS256", - "kty": "RSA", - "n": - "yG9914Q1j63Os4jX5dBQbUfImGq4zsXJD4R59XNjGJlEt5ek6NoiDl0ucJO3_7_R9e5my2ONTSqZhtzFW6MImnIn8idWYzJzO2EhUPCHTvw_2oOGjeYTE2VltIyY_ogIxGwY66G0fVPRRH9tCxnkGOrIvmVgkhCCGkamqeXuWvx9MCHL_gJbZJVwogPSRN_SjA1gDlvsyCdA6__CkgAFcSt1sGgiZ_4cQheKexxf1-7l8R91ZYetz53drk2FS3SfuMZuwMM4KbXt6CifNhzh1Ye-5Tr_ZENXdAvuBRDzfy168xnk9m0JBtvul9GoVIqvCVECB4MPUb7zU6FTIcwRAw", - "e": "AQAB" - }, - { - "kid": - "zyDfdEU7pvH0xEROK156ik8G7vLO1MIL9TKyL631kSPtr9tnvs9XOIiq5jafK2hrGr2qqvJdejmoonlGqWWZRA", - "use": "sig", - "alg": "RS256", - "kty": "RSA", - "n": - "68be-nJp46VLj4Ci1V36IrVGYqkuBfYNyjQTZD_7yRYcERZebowOnwr3w0DoIQpl8iL2X8OXUo7rUW_LMzLxKx2hEmdJfUn4LL2QqA3KPgjYz8hZJQPG92O14w9IZ-8bdDUgXrg9216H09yq6ZvJrn5Nwvap3MXgECEzsZ6zQLRKdb_R96KFFgCiI3bEiZKvZJRA7hM2ePyTm15D9En_Wzzfn_JLMYgE_DlVpoKR1MsTinfACOlwwdO9U5Dm-5elapovILTyVTgjN75i-wsPU2TqzdHFKA-4hJNiWGrYPiihlAFbA2eUSXuEYFkX43ahoQNpeaf0mc17Jt5kp7pM2w", - "e": "AQAB" - }, - { - "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" - "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", - "use": "sig", - "alg": "EdDSA", - "kty": "OKP", - "crv": "Ed25519", - "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" - }, - { - "kid": - "bL33HthM3fWaYkY2_pDzUd7a65FV2R2LHAKCOsye8eNmAPDgRgpHWPYpWFVmeaujUUEXRyDLHN" - "-Up4QH_sFcmw", - "use": "sig", - "alg": "EdDSA", - "kty": "OKP", - "crv": "Ed25519", - "x": "CS01DGXDBPV9cFmd8tgFu3E7eHn1UcP7N1UCgd_JgZo" - } - ] -} - - -def test_load_spomky_keys(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWKS_SPO) - assert len(issuer.all_keys()) == 4 - - -def test_get_ec(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWKS_SPO) - k = issuer.get('sig', 'EC', alg='ES256') - assert k - - -def test_get_ec_wrong_alg(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWKS_SPO) - k = issuer.get('sig', 'EC', alg='ES512') - assert k == [] - - -def test_keys_by_alg_and_usage(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWKS_SPO) - k = issuer.get('sig', alg='RS256') - assert len(k) == 2 - - -def test_copy(): - issuer = KeyIssuer(name='Alice', storage_conf=STORAGE_CONFIG) - issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) - issuer_copy = issuer.copy() - - assert len(issuer_copy.get('sig', 'oct')) == 0 - assert len(issuer_copy.get('sig', 'rsa')) == 1 - - -def test_repr(): - issuer = KeyIssuer(name='Alice', storage_conf=STORAGE_CONFIG) - issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) - txt = issuer.__repr__() - assert ' List[str]: :return: """ - return [i.name for i in self._issuers] + return list(self._issuers.keys()) - def _get_issuer(self, issuer_id): + def _get_issuer(self, issuer_id: str) -> Optional[KeyIssuer]: """ Return the KeyIssuer instance that has name == issuer_id :param issuer_id: The issuer identifiers :return: A KeyIssuer instance or None """ - _i = [i for i in self._issuers if i.name == issuer_id] - if _i: - return _i[0] # should only be one - else: - return None + + return self._issuers.get(issuer_id) + + def _add_issuer(self, issuer_id): + _iss = KeyIssuer(ca_certs=self.ca_certs, name=issuer_id, + keybundle_cls=self.keybundle_cls, + remove_after=self.remove_after, + httpc=self.httpc, httpc_params=self.httpc_params) + self._issuers[issuer_id] = _iss + return _iss + + def items(self): + """ + Get all owner ID's and their keys + + :return: list of 2-tuples (Owner ID., list of KeyBundles) + """ + return self._issuers.items() def __repr__(self): issuers = self._issuer_ids() @@ -100,22 +118,17 @@ def return_issuer(self, issuer_id): """ _iss = self._get_issuer(issuer_id) if not _iss: - _iss = KeyIssuer(ca_certs=self.ca_certs, name=issuer_id, - keybundle_cls=self.keybundle_cls, - remove_after=self.remove_after, - httpc=self.httpc, httpc_params=self.httpc_params, - storage_conf=self.storage_conf) - self._issuers.append(_iss) + return self._add_issuer(issuer_id) return _iss - def add_url(self, issuer_id, url, **kwargs): + def add_url(self, issuer_id: str, url: str, **kwargs) -> KeyBundle: """ Add a set of keys by url. This method will create a :py:class:`oidcmsg.key_bundle.KeyBundle` instance with the url as source specification. If no file format is given it's assumed that what's on the other side is a JWKS. - :param issuer: Who issued the keys + :param issuer_id: Who issued the keys :param url: Where can the key/-s be found :param kwargs: extra parameters for instantiating KeyBundle :return: A :py:class:`oidcmsg.oauth2.keybundle.KeyBundle` instance @@ -148,32 +161,7 @@ def add_kb(self, issuer_id, kb): """ issuer = self.return_issuer(issuer_id) issuer.add_kb(kb) - - # def __setitem__(self, issuer_id, val): - # """ - # Bind one or a list of key bundles to a special identifier. - # Will overwrite whatever was there before !! - # - # :param issuer_id: The owner of the keys in the key bundle/-s - # :param val: A single or a list of KeyBundle instance - # """ - # if not isinstance(val, list): - # val = [val] - # - # for kb in val: - # if not isinstance(kb, KeyBundle): - # raise ValueError('{} not an KeyBundle instance'.format(kb)) - # - # issuer = self.return_issuer(issuer_id) - # issuer.set(val) - - def items(self): - """ - Get all owner ID's and their keys - - :return: list of 2-tuples (Owner ID., list of KeyBundles) - """ - return [(i.name, i.get_bundles()) for i in self._issuers] + self[issuer_id] = issuer def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs): """ @@ -317,13 +305,25 @@ def __getitem__(self, issuer_id=''): """ return self._get_issuer(issuer_id) + def __setitem__(self, issuer_id, issuer): + """ + Set a KeyIssuer with the name == issuer_id + + :param issuer_id: The entity ID + :param issuer: KeyIssuer instance + """ + self._issuers[issuer_id] = issuer + + def set(self, issuer_id, issuer): + self[issuer_id] = issuer + def owners(self): """ Return a list of all the entities that has keys in this key jar. :return: A list of entity IDs """ - return self._issuer_ids() + return list(self._issuers.keys()) def match_owner(self, url): """ @@ -334,16 +334,16 @@ def match_owner(self, url): :param url: A URL :return: An issue entity ID that exists in the Key jar """ - _iss = [i for i in self._issuers if i.name.startswith(url)] + _iss = [i for i in self._issuers.keys() if i.startswith(url)] if _iss: - return _iss[0].name + return _iss[0] raise KeyError("No keys for '{}' in this keyjar".format(url)) def __str__(self): _res = {} - for _issuer in self._issuers: - _res[_issuer.name] = _issuer.key_summary() + for _id, _issuer in self._issuers.items(): + _res[_id] = _issuer.key_summary() return json.dumps(_res) def load_keys(self, issuer_id, jwks_uri='', jwks=None, replace=False): @@ -371,6 +371,8 @@ def load_keys(self, issuer_id, jwks_uri='', jwks=None, replace=False): _keys = jwks['keys'] _issuer.add_kb(self.keybundle_cls(_keys)) + self[issuer_id] = _issuer + def find(self, source, issuer_id=None): """ Find a key bundle based on the source of the keys @@ -381,7 +383,7 @@ def find(self, source, issuer_id=None): """ if issuer_id is None: res = {} - for _issuer in self._issuers: + for _, _issuer in self._issuers.items(): kbs = _issuer.find(source) if kbs: res[_issuer.name] = kbs @@ -438,8 +440,8 @@ def import_jwks(self, jwks, issuer_id): else: _issuer = self.return_issuer(issuer_id=issuer_id) _issuer.add(self.keybundle_cls(_keys, httpc=self.httpc, - httpc_params=self.httpc_params, - storage_conf=self.storage_conf)) + httpc_params=self.httpc_params)) + self[issuer_id] = _issuer def import_jwks_as_json(self, jwks, issuer_id): """ @@ -476,9 +478,7 @@ def __eq__(self, other): return True def __delitem__(self, key): - _issuer = self._get_issuer(key) - if _issuer: - self._issuers.remove(_issuer) + del self._issuers[key] def remove_outdated(self, when=0): """ @@ -492,14 +492,9 @@ def remove_outdated(self, when=0): :param when: To facilitate testing """ - _ids = self._issuer_ids() - for _id in _ids: - _issuer = self[_id] + for _id, _issuer in self._issuers.items(): _before = len(_issuer) _issuer.remove_outdated(when) - if len(_issuer) != _before: - del self[_id] - self.append(_issuer) def _add_key(self, keys, issuer_id, use, key_type='', kid='', no_kid_issuer=None, allow_missing_kid=False): @@ -640,10 +635,18 @@ def copy(self): :return: A :py:class:`oidcmsg.key_jar.KeyJar` instance """ - kj = KeyJar() - for _issuer in self._issuers: - _iss = kj.return_issuer(_issuer.name) - _iss.set([kb.copy() for kb in _issuer]) + if self.storage_conf: + _conf = self.storage_conf.get('KeyJar') + if _conf: + _label = self.storage_conf.get('label') + if _label: + self.storage_conf['KeyJar']['label'] = '{}.copy'.format(_label) + + kj = KeyJar(storage_conf=self.storage_conf) + for _id, _issuer in self._issuers.items(): + _issuer_copy = KeyIssuer() + _issuer_copy.set([kb.copy() for kb in _issuer]) + kj[_id] = _issuer_copy kj.httpc_params = self.httpc_params kj.httpc = self.httpc @@ -667,11 +670,11 @@ def dump(self, exclude=None): 'remove_after': self.remove_after, 'httpc_params': self.httpc_params} - _issuers = [] - for _issuer in self._issuers: + _issuers = {} + for _id, _issuer in self._issuers.items(): if exclude and _issuer.name in exclude: continue - _issuers.append(_issuer.dump()) + _issuers[_id] = _issuer.dump() info['issuers'] = _issuers return info @@ -689,13 +692,10 @@ def load(self, info): self.remove_after = info['remove_after'] self.httpc_params = info['httpc_params'] - for _issuer_desc in info['issuers']: - self._issuers.append(KeyIssuer(storage_conf=self.storage_conf).load(_issuer_desc)) + for _issuer_id, _issuer_desc in info['issuers'].items(): + self._issuers[_issuer_id] = KeyIssuer().load(_issuer_desc) return self - def append(self, issuer): - self._issuers.append(issuer) - def key_summary(self, issuer_id): _issuer = self._get_issuer(issuer_id) if _issuer: @@ -705,16 +705,16 @@ def key_summary(self, issuer_id): def update(self): """ - Go through the whole key jar, key bundle by key bundle and update them one + Go through the whole key jar, key issuer by key issuer and update them one by one. :param keyjar: The key jar to update """ - for _id in self._issuer_ids(): - _issuer = self._get_issuer(_id) - self._issuers.remove(_issuer) + ids = self._issuers.keys() + for _id in ids: + _issuer = self[_id] _issuer.update() - self._issuers.append(_issuer) + self[_id] = _issuer # ============================================================================= @@ -763,15 +763,14 @@ def build_keyjar(key_conf, kid_template="", keyjar=None, issuer_id='', storage_c :return: A KeyJar instance """ - _issuer = build_keyissuer(key_conf, kid_template, storage_conf=storage_conf, - issuer_id=issuer_id) + _issuer = build_keyissuer(key_conf, kid_template, issuer_id=issuer_id) if _issuer is None: return None if keyjar is None: - keyjar = KeyJar() + keyjar = KeyJar(storage_conf=storage_conf) - keyjar.append(_issuer) + keyjar[issuer_id] = _issuer return keyjar @@ -824,7 +823,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea if private_path: if os.path.isfile(private_path): _jwks = open(private_path, 'r').read() - _issuer = KeyIssuer(name=issuer_id, storage_conf=storage_conf) + _issuer = KeyIssuer(name=issuer_id) _issuer.import_jwks(json.loads(_jwks)) if key_defs: _kb = _issuer[0] @@ -840,7 +839,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea fp.write(json.dumps(jwks)) fp.close() else: - _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) if not read_only: jwks = _issuer.export_jwks(private=True) head, tail = os.path.split(private_path) @@ -861,7 +860,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea elif public_path: if os.path.isfile(public_path): _jwks = open(public_path, 'r').read() - _issuer = KeyIssuer(name=issuer_id, storage_conf=storage_conf) + _issuer = KeyIssuer(name=issuer_id) _issuer.import_jwks(json.loads(_jwks)) if key_defs: _kb = _issuer[0] @@ -877,7 +876,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea fp.write(json.dumps(jwks)) fp.close() else: - _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) if not read_only: _jwks = _issuer.export_jwks(issuer=issuer_id) head, tail = os.path.split(public_path) @@ -887,20 +886,18 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea fp.write(json.dumps(_jwks)) fp.close() else: - _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) keyjar = KeyJar(storage_conf=storage_conf) - keyjar.append(_issuer) + keyjar[issuer_id] = _issuer return keyjar -def rotate_keys(key_conf, keyjar, kid_template="", issuer_id='', storage_conf=None): - new_keys = build_keyissuer(key_conf, kid_template, storage_conf=storage_conf, - issuer_id=issuer_id) +def rotate_keys(key_conf, keyjar, kid_template="", issuer_id=''): + new_keys = build_keyissuer(key_conf, kid_template, issuer_id=issuer_id) _issuer = keyjar[issuer_id] _issuer.mark_all_keys_as_inactive() for kb in new_keys: _issuer.add_kb(kb) - del keyjar[_issuer.name] - keyjar.append(_issuer) + keyjar[issuer_id] = _issuer return keyjar diff --git a/src/cryptojwt/serialize/item.py b/src/cryptojwt/serialize/item.py index e63cc4e..513dbf3 100644 --- a/src/cryptojwt/serialize/item.py +++ b/src/cryptojwt/serialize/item.py @@ -1,50 +1,28 @@ -from cryptojwt import jwk -from cryptojwt.jwk.jwk import key_from_jwk_dict -from cryptojwt import key_bundle -from cryptojwt import key_issuer - - -class JWK: - @staticmethod - def serialize(key: jwk.JWK) -> dict: - _dict = key.serialize() - inactive = key.inactive_since - if inactive: - _dict['inactive_since'] = inactive - return _dict +import json +from urllib.parse import quote_plus +from urllib.parse import unquote_plus - @staticmethod - def deserialize(jwk: dict) -> jwk.JWK: - k = key_from_jwk_dict(jwk) - inactive = jwk.get("inactive_since", 0) - if inactive: - k.inactive_since = inactive - return k +from cryptojwt import key_issuer -class KeyBundle: - def __init__(self, storage_conf=None): - self.storage_conf = storage_conf +class KeyIssuer: @staticmethod - def serialize(item: key_bundle.KeyBundle) -> dict: - _dict = item.dump() - return _dict - - def deserialize(self, spec: dict) -> key_bundle.KeyBundle: - bundle = key_bundle.KeyBundle(storage_conf=self.storage_conf).load(spec) - return bundle - + def serialize(item: key_issuer.KeyIssuer) -> str: + """ Convert from KeyIssuer to JSON """ + return json.dumps(item.dump()) + + def deserialize(self, spec: str) -> key_issuer.KeyIssuer: + """ Convert from JSON to KeyIssuer """ + _dict = json.loads(spec) + issuer = key_issuer.KeyIssuer().load(_dict) + return issuer -class KeyIssuer: - def __init__(self, storage_conf=None): - self.storage_conf = storage_conf +class QUOTE: @staticmethod - def serialize(item: key_issuer.KeyIssuer) -> dict: - _dict = item.dump() - return _dict + def serialize(item: str) -> str: + return quote_plus(item) - def deserialize(self, spec: dict) -> key_issuer.KeyIssuer: - issuer = key_issuer.KeyIssuer(storage_conf=self.storage_conf).load(spec) - return issuer + def deserialize(self, spec: str) -> str: + return unquote_plus(spec) diff --git a/src/cryptojwt/utils.py b/src/cryptojwt/utils.py index 023c909..abeb727 100644 --- a/src/cryptojwt/utils.py +++ b/src/cryptojwt/utils.py @@ -1,4 +1,5 @@ import base64 +import importlib import json import re import struct @@ -201,3 +202,28 @@ def deser(val): _val = val return base64_to_long(_val) + +def modsplit(name): + """Split importable""" + if ':' in name: + _part = name.split(':') + if len(_part) != 2: + raise ValueError("Syntax error: {s}") + return _part[0], _part[1] + + _part = name.split('.') + if len(_part) < 2: + raise ValueError("Syntax error: {s}") + + return '.'.join(_part[:-1]), _part[-1] + + +def importer(name): + """Import by name""" + _part = modsplit(name) + module = importlib.import_module(_part[0]) + return getattr(module, _part[1]) + + +def qualified_name(cls): + return cls.__module__ + "." + cls.__name__ \ No newline at end of file diff --git a/tests/test_02_jwk.py b/tests/test_02_jwk.py index b6d87e1..ea1e6c2 100644 --- a/tests/test_02_jwk.py +++ b/tests/test_02_jwk.py @@ -12,15 +12,17 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric.ec import generate_private_key + from cryptojwt.exception import DeSerializationNotPossible from cryptojwt.exception import UnsupportedAlgorithm from cryptojwt.exception import WrongUsage from cryptojwt.jwk import JWK +from cryptojwt.jwk import calculate_x5t from cryptojwt.jwk import certificate_fingerprint from cryptojwt.jwk import pem_hash from cryptojwt.jwk import pems_to_x5c -from cryptojwt.jwk.ec import NIST2SEC from cryptojwt.jwk.ec import ECKey +from cryptojwt.jwk.ec import NIST2SEC from cryptojwt.jwk.hmac import SYMKey from cryptojwt.jwk.hmac import new_sym_key from cryptojwt.jwk.hmac import sha256_digest @@ -29,7 +31,6 @@ from cryptojwt.jwk.jwk import jwk_wrap from cryptojwt.jwk.jwk import key_from_jwk_dict from cryptojwt.jwk.rsa import RSAKey -from cryptojwt.jwk.rsa import generate_and_store_rsa_key from cryptojwt.jwk.rsa import import_private_rsa_key_from_file from cryptojwt.jwk.rsa import import_public_rsa_key_from_file from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file @@ -627,7 +628,7 @@ def test_dump_load(): def test_key_ops(): sk = SYMKey( key='df34db91c16613deba460752522d28f6ebc8a73d0d9185836270c26b', - alg = "HS256", + alg="HS256", key_ops=["sign", "verify"] ) @@ -639,9 +640,9 @@ def test_key_ops_and_use(): with pytest.raises(ValueError): SYMKey( key='df34db91c16613deba460752522d28f6ebc8a73d0d9185836270c26b', - alg = "HS256", + alg="HS256", key_ops=["sign", "verify"], - use = "sig" + use="sig" ) @@ -651,7 +652,9 @@ def test_pem_to_x5c(): x5c = pems_to_x5c([cert_chain]) assert len(x5c) == 1 - assert x5c[0] == 'MIIB2jCCAUOgAwIBAgIBATANBgkqhkiG9w0BAQUFADA0MRgwFgYDVQQDEw9UaGUgY29kZSB0ZXN0ZXIxGDAWBgNVBAoTD1VtZWEgVW5pdmVyc2l0eTAeFw0xMjEwMDQwMDIzMDNaFw0xMzEwMDQwMDIzMDNaMDIxCzAJBgNVBAYTAlNFMSMwIQYDVQQDExpPcGVuSUQgQ29ubmVjdCBUZXN0IFNlcnZlcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwf+wiusGhA+gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB/87ds3dy3Rfym/GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8CAwEAATANBgkqhkiG9w0BAQUFAAOBgQCsTntG4dfW5kO/Qle6uBhIhZU+3IreIPmbwzpXoCbcgjRa01z6WiBLwDC1RLAL7ucaF/EVlUq4e0cNXKt4ESGNc1xHISOMLetwvS1SN5tKWA9HNua/SaqRtiShxLUjPjmrtpUgotLNDRvUYnTdTT1vhZar7TSPr1yObirjvz/qLw==' + assert x5c[ + 0] == \ + 'MIIB2jCCAUOgAwIBAgIBATANBgkqhkiG9w0BAQUFADA0MRgwFgYDVQQDEw9UaGUgY29kZSB0ZXN0ZXIxGDAWBgNVBAoTD1VtZWEgVW5pdmVyc2l0eTAeFw0xMjEwMDQwMDIzMDNaFw0xMzEwMDQwMDIzMDNaMDIxCzAJBgNVBAYTAlNFMSMwIQYDVQQDExpPcGVuSUQgQ29ubmVjdCBUZXN0IFNlcnZlcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwf+wiusGhA+gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB/87ds3dy3Rfym/GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8CAwEAATANBgkqhkiG9w0BAQUFAAOBgQCsTntG4dfW5kO/Qle6uBhIhZU+3IreIPmbwzpXoCbcgjRa01z6WiBLwDC1RLAL7ucaF/EVlUq4e0cNXKt4ESGNc1xHISOMLetwvS1SN5tKWA9HNua/SaqRtiShxLUjPjmrtpUgotLNDRvUYnTdTT1vhZar7TSPr1yObirjvz/qLw==' def test_pem_hash(): @@ -664,7 +667,8 @@ def test_certificate_fingerprint(): der = cert_file.read() res = certificate_fingerprint(der) - assert res == '01:DF:F1:D4:5F:21:7B:2E:3A:A2:D8:CA:13:4C:41:66:03:A1:EF:3E:7B:5E:8B:69:04:5E:80:8B:55:49:F1:48' + assert res == '01:DF:F1:D4:5F:21:7B:2E:3A:A2:D8:CA:13:4C:41:66:03:A1:EF:3E:7B:5E:8B:69:04:5E' \ + ':80:8B:55:49:F1:48' res = certificate_fingerprint(der, 'sha1') assert res == 'CA:CF:21:9E:72:00:CD:1C:CA:FD:4F:6D:84:6B:9E:E8:74:80:47:64' @@ -676,6 +680,17 @@ def test_certificate_fingerprint(): certificate_fingerprint(der, 'foo') -def test_generate_and_store_rsa_key(): - priv_key = generate_and_store_rsa_key(filename=full_path('temp_rsa.key')) +# def test_generate_and_store_rsa_key(): +# priv_key = generate_and_store_rsa_key(filename=full_path('temp_rsa.key')) + + +def test_x5t_calculation(): + with open(full_path('cert.der'), 'rb') as cert_file: + der = cert_file.read() + + x5t = calculate_x5t(der) + assert x5t == b'Q0FDRjIxOUU3MjAwQ0QxQ0NBRkQ0RjZEODQ2QjlFRTg3NDgwNDc2NA==' + x5t_s256 = calculate_x5t(der, 'sha256') + assert x5t_s256 == \ + b'MDFERkYxRDQ1RjIxN0IyRTNBQTJEOENBMTM0QzQxNjYwM0ExRUYzRTdCNUU4QjY5MDQ1RTgwOEI1NTQ5RjE0OA==' diff --git a/tests/test_40_serialize.py b/tests/test_40_serialize.py index 034dc5c..e9b9341 100644 --- a/tests/test_40_serialize.py +++ b/tests/test_40_serialize.py @@ -7,8 +7,6 @@ from cryptojwt.key_bundle import rsa_init from cryptojwt.key_issuer import KeyIssuer from cryptojwt.serialize import item -from cryptojwt.serialize.item import JWK -from cryptojwt.serialize.item import KeyBundle def full_path(local_file): @@ -21,26 +19,6 @@ def full_path(local_file): CERT = full_path("cert.pem") -def test_jwks(): - _key = RSAKey() - _key.load_key(import_rsa_key_from_cert_file(CERT)) - - _item = JWK().serialize(_key) - _nkey = JWK().deserialize(_item) - assert _key == _nkey - - -def test_key_bundle(): - kb = rsa_init({'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) - _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) - kb.append(_sym) - _item = KeyBundle().serialize(kb) - _nkb = KeyBundle().deserialize(_item) - assert len(kb) == 3 - assert len(kb.get('rsa')) == 2 - assert len(kb.get('oct')) == 1 - - def test_key_issuer(): kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", ["sig"]) assert len(kb) == 1 From abce36622660035c215a129ba08b7da227f173d0 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Sun, 31 May 2020 20:47:14 +0200 Subject: [PATCH 12/50] replace token, use username --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e5d8bc9..03f9c5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: addons: apt: packages: - - + - install: - pip install codecov - pip install tox @@ -26,6 +26,6 @@ deploy: tags: true distributions: bdist_wheel skip_existing: true - user: __token__ + username: __token__ password: - secure: "kW7wRRnQDkeD/bvDrktoLTLrFV4W0B548PhtXVBD7rfbx09oE+jmoM6Ojc2y+t18XQ0zYIfO1qXdoenzti2YiY8xGgA1EYDpHuT0d+DX3tPuLiXou+pts+dXo5Q0YJRFlmBgxtyT+eHhScojDPvDmCFQodIQhiGK1cyzV1p+jE9W/wjiCOQWSQ0LoIV/ajjIMArEmucqwU1wlXxPPgVaV2EuZ7hXBULA6C1DKJENja+qeORtDdW6DMXPJQ4AD74dR+IHaqX4W+mRjRO8wZHKBxrxQ65nZErcfwjbqWRhPIUWzkVngC21BWrs/5eMvxuSPDj4XUY/wiB/1xR883bM6YypEqlCM5x3CFA6a65iI+dfT4iqQRdwOF04oxDkNf2N1LBx8Pv01KxDs5cblv3i/TQHoBIKH8wY6yxrVT2gtcGOwj5+/Pay3PGUN4S8YyzzmSuX/trQgWl/6BD2UPDRchhz24AfV8vXPr6ljBllNdGtWVWJd0ad7u2nqTrUcec909cAhz9SHP1LO8IqCjh8XGdQATOmZXptXJLLmysN9+OwgyTA4SH0Q4FKPG/xriq2cbMCfBTnBOTb5N9ZCm1VeGZlcMdva9UDcRWjtEStAy+gnYQc10EBD060O0jUDB9qdm5zyiTSqOY701828Z2a5uMWXqWUlEsbF61BN6cuD4s=" + secure: sCvQSnLW2R9i01dTvG1JyZ+lU99jXjEmPeniSTB8SAZMlfIbuo8I5kjkKxxaa4uTOU3mZiWDG/wq/liUuUzxjXYLA47gwVqU6QrkgIh0/QqGIN1+GYtHqZZETueGClSS8drCgYchpknNTA+30JL/kpt4fjq5hSUFII5BuybVYbh5WVnmwjhZOnZdKxVuQrrfzNVe14wr/TY7VvtL7W5bSCigxYh4oSUiFB6WkFyPZFetqYRO5eCTRt4Tn/hT3Y5Aiz033hMFhrjB4XxuWK7P1f6WVVGRMfAwa4XuC3w0oPSp/9I0cMBtLkExs+tdq25lgmJOcdzVwo3QHPs4pn6AjHKPyoTr/jiuysKnSZCwgcMPFpORgrOWQh/qFxHqNFI0QzNasZXhEBDJaaR2pN/GLjW97NiuaCFuZ/me0SWtpdhPuJpjgx9xxwymRRC4aBu6aPrzoxFGABp4Bkica7KsBmWgQ0ZFMPO6bXB6bU5w8a+wlOY/6I8b06/cIOIyMh8Tas7UwO2/e0L65qhAkR3u1evmagex94w9UameP+518yYik0L5uwrn7vPeRNpH1UEctd3RG73HlEdu8wMCo0pPrvFqsQGtIiPTLPx99453z8Z/raWwAa1Zpn2c8mSm9YIOQZ20rsiFy1vfwAm2b7/dfm005hUirmfwtiG7Oq8iLdg= From e595912e1a5736f04b875945d8a14eaa9107cdb2 Mon Sep 17 00:00:00 2001 From: roland Date: Sat, 6 Jun 2020 20:05:14 +0200 Subject: [PATCH 13/50] Remove aslist tests. --- aslist_tests/private_jwks.json | 1 - aslist_tests/rsa.key | 15 - aslist_tests/test_45_key_jar.py | 954 -------------------- aslist_tests/test_keys/cert.key | 27 - aslist_tests/test_keys/ec-p256-private.pem | 6 - aslist_tests/test_keys/ec-p256-public.pem | 5 - aslist_tests/test_keys/ec-p256.json | 8 - aslist_tests/test_keys/ec-p384-private.pem | 7 - aslist_tests/test_keys/ec-p384-public.pem | 6 - aslist_tests/test_keys/ec-p384.json | 8 - aslist_tests/test_keys/ec.key | 5 - aslist_tests/test_keys/jwk.json | 12 - aslist_tests/test_keys/rsa-1024-private.pem | 17 - aslist_tests/test_keys/rsa-1024-public.pem | 7 - aslist_tests/test_keys/rsa-1024.json | 9 - aslist_tests/test_keys/rsa-1280-private.pem | 20 - aslist_tests/test_keys/rsa-1280-public.pem | 8 - aslist_tests/test_keys/rsa-1280.json | 9 - aslist_tests/test_keys/rsa-2048-private.pem | 29 - aslist_tests/test_keys/rsa-2048-public.pem | 10 - aslist_tests/test_keys/rsa-2048.json | 9 - aslist_tests/test_keys/rsa-3072-private.pem | 41 - aslist_tests/test_keys/rsa-3072-public.pem | 12 - aslist_tests/test_keys/rsa-3072.json | 9 - aslist_tests/test_keys/rsa-4096-private.pem | 53 -- aslist_tests/test_keys/rsa-4096-public.pem | 15 - aslist_tests/test_keys/rsa-4096.json | 9 - aslist_tests/test_keys/rsa.key | 15 - 28 files changed, 1326 deletions(-) delete mode 100644 aslist_tests/private_jwks.json delete mode 100644 aslist_tests/rsa.key delete mode 100755 aslist_tests/test_45_key_jar.py delete mode 100755 aslist_tests/test_keys/cert.key delete mode 100644 aslist_tests/test_keys/ec-p256-private.pem delete mode 100644 aslist_tests/test_keys/ec-p256-public.pem delete mode 100644 aslist_tests/test_keys/ec-p256.json delete mode 100644 aslist_tests/test_keys/ec-p384-private.pem delete mode 100644 aslist_tests/test_keys/ec-p384-public.pem delete mode 100644 aslist_tests/test_keys/ec-p384.json delete mode 100644 aslist_tests/test_keys/ec.key delete mode 100755 aslist_tests/test_keys/jwk.json delete mode 100644 aslist_tests/test_keys/rsa-1024-private.pem delete mode 100644 aslist_tests/test_keys/rsa-1024-public.pem delete mode 100644 aslist_tests/test_keys/rsa-1024.json delete mode 100644 aslist_tests/test_keys/rsa-1280-private.pem delete mode 100644 aslist_tests/test_keys/rsa-1280-public.pem delete mode 100644 aslist_tests/test_keys/rsa-1280.json delete mode 100644 aslist_tests/test_keys/rsa-2048-private.pem delete mode 100644 aslist_tests/test_keys/rsa-2048-public.pem delete mode 100644 aslist_tests/test_keys/rsa-2048.json delete mode 100644 aslist_tests/test_keys/rsa-3072-private.pem delete mode 100644 aslist_tests/test_keys/rsa-3072-public.pem delete mode 100644 aslist_tests/test_keys/rsa-3072.json delete mode 100644 aslist_tests/test_keys/rsa-4096-private.pem delete mode 100644 aslist_tests/test_keys/rsa-4096-public.pem delete mode 100644 aslist_tests/test_keys/rsa-4096.json delete mode 100755 aslist_tests/test_keys/rsa.key diff --git a/aslist_tests/private_jwks.json b/aslist_tests/private_jwks.json deleted file mode 100644 index e4a3b0a..0000000 --- a/aslist_tests/private_jwks.json +++ /dev/null @@ -1 +0,0 @@ -{"keys": [{"kty": "RSA", "use": "sig", "kid": "dTZKdDFabEJoSEVKaGQ4anRLd2x3YTJWQW93ejVGUWV3Z0JJZFpvZnhUTQ", "n": "1uPVViKYxyTJ1B1_wiQCSQJrwkLKXQTg6zYy2I1JX3W3gq6i5QD8XxShR62GFdcOj0NkqR2ZiyobVqhKJ3TKErRH1mtxjFuEf-o9h6B5j6Rou6GN4TNv4h-Ed9mG5KyTI364aLlpVT-LcthnihUYwgaq7iKN-OQQD0FHPa0HBouk4QJTLRIkIF2QmSRGAJlnFevzZr0O9vAycGJH1ksTsUioP-oeNtxThlQEOi0lIF5dPGI-ZcQDpdhAHj-C5iBiyi5s4_yFM2UXXF2ZkIVkezfWPKKSQPq7yfCnFAtdeQ5stpfpG0mMXtv1zfTk-Y8-WYMnExwen7F024ZCavjlqw", "e": "AQAB", "d": "JJe3hGtvyLmjBNPhJZYsLXKUFwh4nU5vXp5kGiw1CmRpU3-ZjZWVZDuHG0WZR67Pc-XuBj5cHy6UaTVPK1jf8D9y3Dh_pX8QGRgyUh4plSRSEWF5X5f6vW7Qh_gq2FXq2GiDzpGENlgTzwK63vCovqGUCekoc_GiKnbbQs1sHNjq0WKCSrXZcfsdpdjsjksiQmmfdblPplTuYG4t2GvfYzGXgepUw2LLyJ8KCdSsXyebK9-J8laxk3El0wFbxbI2C7Y8H5BPCN7Cp6zcaenYkYYV7IGy1Z21kv-lTaBXzu23PO5vhH2bzyklvG_a0RbmrrTHr5OCWnl1-BALuYfpQQ", "p": "8_tdp7rJyRrpq4wEPxJrcOvrcrknMG03cjEePV5ozRFFeBVg2nkuESR-JHoxgF3bBOzyurB5eVDT11seZEiisThMzk6OZSnoNsDgk3WTm0c_Yl7qn0dwZANOCYcEIOUe9ouR3kGr1iJ3DZ9o7tYLJ3sb6-oCH1Cakf53iR_RXy8", "q": "4XmdnKVLvzkZP4A4YQ2ROquoAbPOoomd2aHkGndLP9tDuPxur4PiZVrMFjuzMob2uJMW7Y1FT-0WLk9aCVl0P6f893RL1yfRZVKlHopgYxe9jshwk-5VawdLn6HRtbZ-F8gp6KZMaK8oyTk-50CnsizeACcxcVEFQkFNB_v6IkU"}, {"kty": "EC", "use": "sig", "kid": "WDQzSTBkY21kZVZ0YXAzbXZVTUFPQTdLWXpQQ3QzODF6dHJMQk5LSlBpRQ", "crv": "P-256", "x": "PhbU6LJ7Xc75T8YvCcmuBLfWGQ91AVakWlbr0Dk3wgo", "y": "Gg_W4TuhRB4nN8jAXWuOyEg36gbKErLL406Ngh6Cgk8", "d": "y4Sle7C9FHcBnXInlnIjPjKx73Djq0YDpH7FgCihA-g"}, {"kty": "EC", "use": "sig", "kid": "aVdwOVAyVWlzYUhwMHpGRVgxc2hkRDRXWGdIQUp1TVJfc1diVkdjb2w4VQ", "crv": "P-384", "x": "pwaV9kgGNyu7mEkCxj_lRssXgmT-kdIhoK-8fhJn6s1tbEEgBbPQj8Pubqdq_fld", "y": "9VQIWfNfusIb2ZDxwViSzGMNeT_QZCu16HqHx8s5F_l--VfTwqdP0NTtWogT-soO", "d": "kHsAXZL6uhzVCKZQzU0iRiH3SnY9a6MufFIJW9jLL_RSyC5QbUasQGRlZm9j2gSI"}]} \ No newline at end of file diff --git a/aslist_tests/rsa.key b/aslist_tests/rsa.key deleted file mode 100644 index d34432d..0000000 --- a/aslist_tests/rsa.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQD6vqn19W/VB215DBADRakfPmCtFBf8/+YyhGqixWIwDiEl/L6L -w5HKZCUPVgrC0ADhJfvAbn4fte5MWBCTkqgepKL3BySMA0LMaBF12pbHlPSUbmQG -BJmTX4NNXuUel6TbPYJAU2Nh5Nan0Mb7Bmb8QpFvS0Hw7qZRW8y2eIttfwIDAQAB -AoGBAJVf9FxkRKUB8cOE3h006JWGUY2KROghgn9hxy0ErYO3RyQcN1+HuFh75GAI -gAyiYYO/XwS6TkSR2057wBRJ8ABzcL3+v5g+16Vbh0BjXVE+cv1WGdNGujyzl6ji -jlyF4cb6tXDyqWTLkMAtV20NfO/CGsfii6YEkZb2P90usthRAkEA/oG7a9EvQ7eR -gSEqppzW7KCwidPjnZTr/ROIZQU33nwkIJ0ElTjMNYKP8DerSuixR9skw2ZY8Q8I -1PTBnocHwwJBAPw3SAQYwxZwQMu1trVPMNOGIbSY4rQlMZGXrCZSu/TnozczFLA8 -qNM84g5veyJOzHKmYkIsMG1gwg5VNniG45UCQF6SlLOW0upl70K9sVyiUVcyywcc -Xqty6FJtjLSFQOKC3OXlkwtkRLXpo1UPSq6WUzIxY7LceFZzUMPZg41F/gMCQHNr -POqbBlPzZMOUUZthNP/nhu8lc8Fqr+dnmGElRVxK0JdHKfWInN2mI/DlNV064Dar -S5XqsPKs78EtX7MCT40CQFQZiry8m7ROubOU4+HDG9o1w9zcKXCkmbD9hBCGvTAj -BQNuGE0DtC6FEWTs8bXybLM5yBRq1XiKLdmi5N+3n4g= ------END RSA PRIVATE KEY----- diff --git a/aslist_tests/test_45_key_jar.py b/aslist_tests/test_45_key_jar.py deleted file mode 100755 index a53f4ff..0000000 --- a/aslist_tests/test_45_key_jar.py +++ /dev/null @@ -1,954 +0,0 @@ -import copy -import json -import os -import shutil -import time - -import pytest -from abstorage.storages.absqlalchemy import AbstractStorageSQLAlchemy - -from cryptojwt.exception import JWKESTException -from cryptojwt.jwe.jwenc import JWEnc -from cryptojwt.jws.jws import JWS -from cryptojwt.jws.jws import factory -from cryptojwt.key_bundle import KeyBundle -from cryptojwt.key_bundle import keybundle_from_local_file -from cryptojwt.key_bundle import rsa_init -from cryptojwt.key_jar import KeyJar -from cryptojwt.key_jar import build_keyjar -from cryptojwt.key_jar import init_key_jar -from cryptojwt.key_jar import rotate_keys - -__author__ = 'Roland Hedberg' - -BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), - "test_keys")) -RSAKEY = os.path.join(BASE_PATH, "cert.key") -RSA0 = os.path.join(BASE_PATH, "rsa.key") -EC0 = os.path.join(BASE_PATH, "ec.key") -BASEDIR = os.path.abspath(os.path.dirname(__file__)) - - -def full_path(local_file): - return os.path.join(BASEDIR, local_file) - - -ABS_STORAGE_SQLALCHEMY = dict( - driver='sqlalchemy', - url='sqlite:///:memory:', - params=dict(table='Thing'), - handler=AbstractStorageSQLAlchemy -) - -ABS_STORAGE_FILE = { - 'handler': 'abstorage.storages.abfile.AbstractFileSystem', - 'fdir': 'keyjar', - 'key_conv': 'abstorage.converter.QPKey', - 'value_conv': 'cryptojwt.serialize.item.KeyIssuer', - 'label': 'keyjar' -} - - -JWK0 = { - "keys": [ - { - 'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", - 'n': - 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5' - 'B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8' - } - ] -} - -JWK1 = { - "keys": [ - { - "n": - "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" - "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" - "-NvS-aG_jN5cstVbCGWE20H0vF" - "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" - "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" - "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": "AQAB", "kty": "RSA", "kid": "rsa1" - }, - { - "k": - "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "oct" - }, - ] -} - -JWK2 = { - "keys": [ - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "kriMPdmBvx68skT8-mPAB3BseeA", - "kty": "RSA", - "n": - "kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1um" - "l94fLyq_AXbwo9yAduf4dCHTP8CWR1dnDR" - "-Qnz_4PYlWVEuuHHONOw_blbfdMjhY" - "-C_BYM2E3pRxbohBb3x__CfueV7ddz2LYiH3" - "wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_GTgWN8A" - "-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQt" - "SsjQtI--XsokxRDqYLwus1I1SihgbV_STTg5enufuw", - "use": "sig", - "x5c": [ - "MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb" - "2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb2" - "50cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipg" - "H0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6" - "/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Q" - "nz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x" - "//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13S" - "QwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp" - "+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5en" - "ufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJ" - "vbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdN" - "VGKCmSf8M65b8h0NwlIjGGGy" - "/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADD" - "kN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5" - "+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8y" - "PJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW" - "+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ" - ], - "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", - "kty": "RSA", - "n": - "vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhzh23V9Tkq" - "-RtwN1Vs_z57hO82kkzL-cQHZX3bMJ" - "D-GEGOKXCEXURN7VMyZWMAuzQoW9vFb1k3cR1RW_EW_P" - "-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5yCw5T_Vuwqqsio3" - "V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_KAS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IA" - "bsk1wRtWDndhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ", - "use": "sig", - "x5c": [ - "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" - "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" - "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" - "/uPhEf7zVizjfcr/ISGFe9+yUO" - "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" - "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" - "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" - "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" - "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" - "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" - "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" - "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" - "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" - "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" - "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" - "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" - "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" - "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" - ], - "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", - "kty": "RSA", - "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" - "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" - "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" - "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" - "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" - "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" - "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" - "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" - "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" - "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" - "+Iqzmn" - "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" - "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" - "qA==" - ], - "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", - "kty": "RSA", - "n": - "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" - "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" - "-KXVD02jdmX2HNdYJdPyc0c", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" - "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" - "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" - "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" - "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" - "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" - "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" - "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" - "ig==" - ], - "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" - } - ] -} - -JWK_UK = { - "keys": [ - { - "n": - "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" - "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" - "-NvS-aG_jN5cstVbCGWE20H0vF" - "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" - "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" - "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": "AQAB", "kty": "RSA", "kid": "rsa1" - }, - { - "k": - "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "buz" - }, - ] -} - -JWK_FP = { - "keys": [ - {"e": "AQAB", "kty": "RSA", "kid": "rsa1"}, - ] -} - -JWKS_SPO = { - "keys": [ - { - "kid": - "BfxfnahEtkRBG3Hojc9XGLGht_5rDBj49Wh3sBDVnzRpulMqYwMRmpizA0aSPT1fhCHYivTiaucWUqFu_GwTqA", - "use": "sig", - "alg": "ES256", - "kty": "EC", - "crv": "P-256", - "x": "1XXUXq75gOPZ4bEj1o2Z5XKJWSs6LmL6fAOK3vyMzSc", - "y": "ac1h_DwyuUxhkrD9oKMJ-b_KuiVvvSARIwT-XoEmDXs" - }, - { - "kid": - "91pD1H81rXUvrfg9mkngIG-tXjnldykKUVbITDIU1SgJvq91b8clOcJuEHNAq61eIvg8owpEvWcWAtlbV2awyA", - "use": "sig", - "alg": "ES256", - "kty": "EC", - "crv": "P-256", - "x": "2DfQoLpZS2j3hHEcHDkzV8ISx-RdLt6Opy8YZYVm4AQ", - "y": "ycvkFMBIzgsowiaf6500YlG4vaMSK4OF7WVtQpUbEE0" - }, - { - "kid": "0sIEl3MUJiCxrqleEBBF-_bZq5uClE84xp-wpt8oOI" - "-WIeNxBjSR4ak_OTOmLdndB0EfDLtC7X1JrnfZILJkxA", - "use": "sig", - "alg": "RS256", - "kty": "RSA", - "n": - "yG9914Q1j63Os4jX5dBQbUfImGq4zsXJD4R59XNjGJlEt5ek6NoiDl0ucJO3_7_R9e5my2ONTSqZhtzFW6MImnIn8idWYzJzO2EhUPCHTvw_2oOGjeYTE2VltIyY_ogIxGwY66G0fVPRRH9tCxnkGOrIvmVgkhCCGkamqeXuWvx9MCHL_gJbZJVwogPSRN_SjA1gDlvsyCdA6__CkgAFcSt1sGgiZ_4cQheKexxf1-7l8R91ZYetz53drk2FS3SfuMZuwMM4KbXt6CifNhzh1Ye-5Tr_ZENXdAvuBRDzfy168xnk9m0JBtvul9GoVIqvCVECB4MPUb7zU6FTIcwRAw", - "e": "AQAB" - }, - { - "kid": - "zyDfdEU7pvH0xEROK156ik8G7vLO1MIL9TKyL631kSPtr9tnvs9XOIiq5jafK2hrGr2qqvJdejmoonlGqWWZRA", - "use": "sig", - "alg": "RS256", - "kty": "RSA", - "n": - "68be-nJp46VLj4Ci1V36IrVGYqkuBfYNyjQTZD_7yRYcERZebowOnwr3w0DoIQpl8iL2X8OXUo7rUW_LMzLxKx2hEmdJfUn4LL2QqA3KPgjYz8hZJQPG92O14w9IZ-8bdDUgXrg9216H09yq6ZvJrn5Nwvap3MXgECEzsZ6zQLRKdb_R96KFFgCiI3bEiZKvZJRA7hM2ePyTm15D9En_Wzzfn_JLMYgE_DlVpoKR1MsTinfACOlwwdO9U5Dm-5elapovILTyVTgjN75i-wsPU2TqzdHFKA-4hJNiWGrYPiihlAFbA2eUSXuEYFkX43ahoQNpeaf0mc17Jt5kp7pM2w", - "e": "AQAB" - }, - { - "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" - "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", - "use": "sig", - "alg": "EdDSA", - "kty": "OKP", - "crv": "Ed25519", - "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" - }, - { - "kid": - "bL33HthM3fWaYkY2_pDzUd7a65FV2R2LHAKCOsye8eNmAPDgRgpHWPYpWFVmeaujUUEXRyDLHN" - "-Up4QH_sFcmw", - "use": "sig", - "alg": "EdDSA", - "kty": "OKP", - "crv": "Ed25519", - "x": "CS01DGXDBPV9cFmd8tgFu3E7eHn1UcP7N1UCgd_JgZo" - } - ] -} - - -def test_build_keyjar(storage_conf=None): - keys = [ - {"type": "RSA", "use": ["enc", "sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - ] - - keyjar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - jwks = keyjar.export_jwks() - for key in jwks["keys"]: - assert "d" not in key # the JWKS shouldn't contain the private part - # of the keys - - assert len(keyjar.get_issuer_keys('')) == 3 # A total of 3 keys - assert len(keyjar.get('sig')) == 2 # 2 for signing - assert len(keyjar.get('enc')) == 1 # 1 for encryption - - -def test_build_keyjar_usage(): - keys = [ - {"type": "RSA", "use": ["enc", "sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "oct", "use": ["enc"]}, - {"type": "oct", "use": ["enc"]}, - ] - - keyjar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - jwks_sig = keyjar.export_jwks(usage='sig') - jwks_enc = keyjar.export_jwks(usage='enc') - assert len(jwks_sig.get('keys')) == 2 # A total of 2 keys with use=sig - assert len(jwks_enc.get('keys')) == 3 # A total of 3 keys with use=enc - - -def test_build_keyjar_missing(tmpdir): - keys = [ - { - "type": "RSA", "key": os.path.join(tmpdir.dirname, "missing_file"), - "use": ["enc", "sig"] - }] - - key_jar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - - assert key_jar is None - - -def test_build_RSA_keyjar_from_file(tmpdir): - keys = [ - { - "type": "RSA", "key": RSA0, - "use": ["enc", "sig"] - }] - - key_jar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - - assert len(key_jar.get_signing_key('rsa', '')) == 1 - - -def test_build_EC_keyjar_missing(tmpdir): - keys = [ - { - "type": "EC", "key": os.path.join(tmpdir.dirname, "missing_file"), - "use": ["enc", "sig"] - }] - - key_jar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - - assert key_jar is None - - -def test_build_EC_keyjar_from_file(tmpdir): - keys = [ - { - "type": "EC", "key": EC0, - "use": ["enc", "sig"] - }] - - key_jar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - - assert len(key_jar.get_issuer_keys("")) == 2 - - -class TestKeyJar(object): - @pytest.fixture(autouse=True) - def setup(self): - shutil.rmtree('keyjar') - self.keyjar = KeyJar(storage_conf=ABS_STORAGE_FILE) - - def test_keyjar_add(self): - kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"]) - self.keyjar.add_kb('https://issuer.example.com', kb) - assert list(self.keyjar.owners()) == ['https://issuer.example.com'] - - def test_add_item(self): - kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"]) - self.keyjar.add_kb('https://issuer.example.com', kb) - assert list(self.keyjar.owners()) == ['https://issuer.example.com'] - - def test_add_symmetric(self): - self.keyjar.add_symmetric('', 'abcdefghijklmnop', ['sig']) - assert list(self.keyjar.owners()) == [''] - assert len(self.keyjar.get_signing_key('oct', '')) == 1 - - def test_items(self): - self.keyjar.add_kb("", KeyBundle([{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, - {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org", KeyBundle([ - {"kty": "oct", "key": "0123456789012345", "use": "sig"}, - {"kty": "oct", "key": "1234567890123456", "use": "enc"}])) - - self.keyjar.add_kb("http://www.example.org", - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) - - assert len(self.keyjar) == 2 - - def test_issuer_extra_slash(self): - self.keyjar.add_kb("", KeyBundle( - [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, - {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org", KeyBundle([ - {"kty": "oct", "key": "0123456789012345", "use": "sig"}, - {"kty": "oct", "key": "1234567890123456", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org", - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) - - assert self.keyjar.get('sig', key_type='RSA', issuer_id='http://www.example.org/') - - def test_issuer_missing_slash(self): - self.keyjar.add_kb("", KeyBundle( - [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org/", KeyBundle([ - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org/", - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) - - assert self.keyjar.get('sig', key_type='RSA', issuer_id='http://www.example.org') - - def test_get_enc(self): - self.keyjar.add_kb("", KeyBundle( - [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org/", KeyBundle([ - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org/", - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) - - assert self.keyjar.get('enc', key_type='oct') - - def test_get_enc_not_mine(self): - self.keyjar.add_kb("", KeyBundle( - [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org/", KeyBundle([ - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "ver"}])) - self.keyjar.add_kb("http://www.example.org/", - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) - - assert self.keyjar.get('enc', key_type='oct', issuer_id='http://www.example.org/') - - def test_dump_issuer_keys(self): - kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", - ["sig"]) - assert len(kb) == 1 - self.keyjar.add_kb('', kb) - _jwks_dict = self.keyjar.export_jwks() - - _info = _jwks_dict['keys'][0] - assert _info == { - 'use': 'sig', - 'e': 'AQAB', - 'kty': 'RSA', - 'alg': 'RS256', - 'n': 'pKybs0WaHU_y4cHxWbm8Wzj66HtcyFn7Fh3n' - '-99qTXu5yNa30MRYIYfSDwe9JVc1JUoGw41yq2StdGBJ40HxichjE' - '-Yopfu3B58Q' - 'lgJvToUbWD4gmTDGgMGxQxtv1En2yedaynQ73sDpIK-12JJDY55pvf' - '-PCiSQ9OjxZLiVGKlClDus44_uv2370b9IN2JiEOF-a7JB' - 'qaTEYLPpXaoKWDSnJNonr79tL0T7iuJmO1l705oO3Y0TQ' - '-INLY6jnKG_RpsvyvGNnwP9pMvcP1phKsWZ10ofuuhJGRp8IxQL9Rfz' - 'T87OvF0RBSO1U73h09YP-corWDsnKIi6TbzRpN5YDw', - 'kid': 'abc' - } - - def test_no_use(self): - kb = KeyBundle(JWK0["keys"]) - self.keyjar.add_kb("abcdefgh", kb) - enc_key = self.keyjar.get_encrypt_key("RSA", "abcdefgh") - assert enc_key != [] - - @pytest.mark.network - def test_provider(self): - self.keyjar.load_keys("https://connect-op.heroku.com", - jwks_uri="https://connect-op.herokuapp.com/jwks.json") - - assert self.keyjar.get_issuer_keys("https://connect-op.heroku.com") - - def test_import_jwks(self): - self.keyjar.import_jwks(JWK1, '') - assert len(self.keyjar.get_issuer_keys('')) == 2 - - def test_get_signing_key_use_undefined(self): - self.keyjar.import_jwks(JWK1, '') - keys = self.keyjar.get_signing_key(kid='rsa1') - assert len(keys) == 1 - - keys = self.keyjar.get_signing_key(key_type='rsa') - assert len(keys) == 1 - - keys = self.keyjar.get_signing_key(key_type='rsa', kid='rsa1') - assert len(keys) == 1 - - def test_load_unknown_keytype(self): - self.keyjar.import_jwks(JWK_UK, '') - assert len(self.keyjar.get_issuer_keys('')) == 1 - - def test_load_missing_key_parameter(self): - with pytest.raises(JWKESTException): - self.keyjar.import_jwks(JWK_FP, '') - - def test_load_spomky_keys(self): - self.keyjar.import_jwks(JWKS_SPO, '') - assert len(self.keyjar.get_issuer_keys('')) == 4 - - def test_get_ec(self): - self.keyjar.import_jwks(JWKS_SPO, '') - k = self.keyjar.get('sig', 'EC', alg='ES256') - assert k - - def test_get_ec_wrong_alg(self): - self.keyjar.import_jwks(JWKS_SPO, '') - k = self.keyjar.get('sig', 'EC', alg='ES512') - assert k == [] - - def test_keyjar_eq(self): - self.keyjar.import_jwks(JWKS_SPO, '') - - kj2 = KeyJar(storage_conf=ABS_STORAGE_FILE) - kj2.import_jwks(JWKS_SPO, '') - - assert self.keyjar == kj2 - - def test_keys_by_alg_and_usage(self): - self.keyjar.import_jwks(JWKS_SPO, '') - k = self.keyjar.keys_by_alg_and_usage('', 'RS256', 'sig') - assert len(k) == 2 - - def test_match_owner(self): - self.keyjar.add_kb('Alice', KeyBundle(JWK0['keys'])) - self.keyjar.add_kb('Bob', KeyBundle(JWK1['keys'])) - self.keyjar.add_kb('https://delphi.example.com/path', KeyBundle(JWK2['keys'])) - - a = self.keyjar.match_owner('https://delphi.example.com') - assert a == 'https://delphi.example.com/path' - - with pytest.raises(KeyError): - self.keyjar.match_owner('https://example.com') - - def test_str(self): - self.keyjar.add_kb('Alice', KeyBundle(JWK0['keys'])) - - desc = '{}'.format(self.keyjar) - assert desc == '{"Alice": "RSA::abc"}' - _cont = json.loads(desc) - assert set(_cont.keys()) == {'Alice'} - - def test_load_keys(self): - self.keyjar.load_keys('Alice', jwks=JWK1) - - assert self.keyjar.owners() == ['Alice'] - - def test_find(self): - _path = full_path('../tests/jwk_private_key.json') - kb = KeyBundle(source='file://{}'.format(_path)) - self.keyjar.add_kb('Alice', kb) - - assert self.keyjar.find('{}'.format(_path), 'Alice') - assert self.keyjar.find('https://example.com', 'Alice') == [] - assert self.keyjar.find('{}'.format(_path), 'Bob') == [] - - _res = self.keyjar.find('{}'.format(_path)) - assert set(_res.keys()) == {'Alice'} - - def test_get_decrypt_keys(self): - self.keyjar.add_kb('Alice', KeyBundle(JWK0['keys'])) - self.keyjar.add_kb('', KeyBundle(JWK1['keys'])) - self.keyjar.add_kb('C', KeyBundle(JWK2['keys'])) - - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) - self.keyjar.add_kb('', kb) - - jwt = JWEnc() - jwt.headers = {'alg': 'RS256'} - jwt.part = [{'alg': 'RS256'}, '{"aud": "Bob", "iss": "Alice"}', - 'aksjdhaksjbd'] - - keys = self.keyjar.get_jwt_decrypt_keys(jwt) - assert keys - - jwt.part = [{'alg': 'RS256'}, '{"iss": "Alice"}', 'aksjdhaksjbd'] - - keys = self.keyjar.get_jwt_decrypt_keys(jwt) - assert keys - - keys = self.keyjar.get_jwt_decrypt_keys(jwt, aud='Bob') - assert keys - - def test_update_keyjar(self): - _path = full_path('../tests/jwk_private_key.json') - kb = KeyBundle(source='file://{}'.format(_path)) - self.keyjar.add_kb('Alice', kb) - - self.keyjar.update() - - keys = self.keyjar.get_issuer_keys('Alice') - assert len(keys) == 1 - - -KEYDEFS = [ - {"type": "RSA", "key": '', "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]} -] - - -def test_remove_after(): - shutil.rmtree('keyjar') - # initial keyjar - keyjar = build_keyjar(KEYDEFS, storage_conf=ABS_STORAGE_FILE) - _old = [k.kid for k in keyjar.get_issuer_keys('') if k.kid] - assert len(_old) == 2 - - keyjar.remove_after = 1 - # rotate_keys = create new keys + make the old as inactive - rotate_keys(KEYDEFS, keyjar=keyjar) - - keyjar.remove_outdated(time.time() + 3600) - - # The remainder are the new keys - _new = [k.kid for k in keyjar.get_issuer_keys('') if k.kid] - assert len(_new) == 2 - - # should not be any overlap between old and new - assert set(_new).intersection(set(_old)) == set() - - -class TestVerifyJWTKeys(object): - @pytest.fixture(autouse=True) - def setup(self): - shutil.rmtree('keyjar') - - mkey = [ - {"type": "RSA", "use": ["sig"]}, - {"type": "RSA", "use": ["sig"]}, - {"type": "RSA", "use": ["sig"]}, - ] - - skey = [ - {"type": "RSA", "use": ["sig"]}, - ] - - # Alice has multiple keys - _conf = copy.deepcopy(ABS_STORAGE_FILE) - _conf['label'] = '{}{}'.format(_conf['label'], 'Alice') - self.alice_keyjar = build_keyjar(mkey, storage_conf=_conf) - # Bob has one single keys - _conf = copy.deepcopy(ABS_STORAGE_FILE) - _conf['label'] = '{}{}'.format(_conf['label'], 'Bob') - self.bob_keyjar = build_keyjar(skey, storage_conf=_conf) - self.alice_keyjar.import_jwks(self.alice_keyjar[''].export_jwks(), 'Alice') - self.bob_keyjar.import_jwks(self.bob_keyjar[''].export_jwks(), 'Bob') - - # To Alice's keyjar add Bob's public keys - self.alice_keyjar.import_jwks( - self.bob_keyjar.export_jwks(issuer_id='Bob'), 'Bob') - - # To Bob's keyjar add Alice's public keys - self.bob_keyjar.import_jwks( - self.alice_keyjar.export_jwks(issuer_id='Alice'), 'Alice') - - _jws = JWS('{"aud": "Bob", "iss": "Alice"}', alg='RS256') - sig_key = self.alice_keyjar.get_signing_key('rsa', owner='Alice')[0] - self.sjwt_a = _jws.sign_compact([sig_key]) - - _jws = JWS('{"aud": "Alice", "iss": "Bob"}', alg='RS256') - sig_key = self.bob_keyjar.get_signing_key('rsa', owner='Bob')[0] - self.sjwt_b = _jws.sign_compact([sig_key]) - - def test_no_kid_multiple_keys(self): - """ This is extremely strict """ - _jwt = factory(self.sjwt_a) - # remove kid reference - _jwt.jwt.headers['kid'] = '' - keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt) - assert len(keys) == 0 - keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, allow_missing_kid=True) - assert len(keys) == 3 - - def test_no_kid_single_key(self): - _jwt = factory(self.sjwt_b) - _jwt.jwt.headers['kid'] = '' - keys = self.alice_keyjar.get_jwt_verify_keys(_jwt.jwt) - assert len(keys) == 1 - - def test_no_kid_multiple_keys_no_kid_issuer(self): - a_kids = [k.kid for k in - self.alice_keyjar.get_verify_key(owner='Alice', key_type='RSA')] - no_kid_issuer = {'Alice': a_kids} - _jwt = factory(self.sjwt_a) - _jwt.jwt.headers['kid'] = '' - keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, no_kid_issuer=no_kid_issuer) - assert len(keys) == 3 - - def test_no_kid_multiple_keys_no_kid_issuer_lim(self): - no_kid_issuer = {'Alice': []} - _jwt = factory(self.sjwt_a) - _jwt.jwt.headers['kid'] = '' - keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, no_kid_issuer=no_kid_issuer) - assert len(keys) == 3 - - def test_matching_kid(self): - _jwt = factory(self.sjwt_b) - keys = self.alice_keyjar.get_jwt_verify_keys(_jwt.jwt) - assert len(keys) == 1 - - def test_no_matching_kid(self): - _jwt = factory(self.sjwt_b) - _jwt.jwt.headers['kid'] = 'abcdef' - keys = self.alice_keyjar.get_jwt_verify_keys(_jwt.jwt) - assert keys == [] - - def test_aud(self): - self.alice_keyjar.import_jwks(JWK1, issuer_id='D') - self.bob_keyjar.import_jwks(JWK1, issuer_id='D') - - _jws = JWS('{"iss": "D", "aud": "A"}', alg='HS256') - sig_key = self.alice_keyjar.get_signing_key('oct', issuer_id='D')[0] - _sjwt = _jws.sign_compact([sig_key]) - - no_kid_issuer = {'D': []} - - _jwt = factory(_sjwt) - - keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, no_kid_issuer=no_kid_issuer) - assert len(keys) == 1 - - -class TestDiv(object): - @pytest.fixture(autouse=True) - def setup(self): - shutil.rmtree('keyjar') - self.keyjar = KeyJar(storage_conf=ABS_STORAGE_FILE) - self.keyjar.add_kb('Alice', KeyBundle(JWK0['keys'])) - self.keyjar.add_kb('Bob', KeyBundle(JWK1['keys'])) - self.keyjar.add_kb('C', KeyBundle(JWK2['keys'])) - - def test_copy(self): - kjc = self.keyjar.copy() - - assert set(kjc.owners()) == {'Alice', 'Bob', 'C'} - - assert len(kjc.get('sig', 'oct', 'Alice')) == 0 - assert len(kjc.get('sig', 'rsa', 'Alice')) == 1 - - assert len(kjc.get('sig', 'oct', 'Bob')) == 1 - assert len(kjc.get('sig', 'rsa', 'Bob')) == 1 - - assert len(kjc.get('sig', 'oct', 'C')) == 0 - assert len(kjc.get('sig', 'rsa', 'C')) == 4 - - def test_repr(self): - txt = self.keyjar.__repr__() - assert " Date: Sat, 6 Jun 2020 20:07:10 +0200 Subject: [PATCH 14/50] Removed duplicated exceptions. --- src/cryptojwt/exception.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/cryptojwt/exception.py b/src/cryptojwt/exception.py index 596c52e..ae073ea 100644 --- a/src/cryptojwt/exception.py +++ b/src/cryptojwt/exception.py @@ -99,18 +99,10 @@ class WrongKeyType(JWKESTException): pass -class UnknownKeyType(JWKESTException): - pass - - class UnsupportedKeyType(JWKESTException): pass -class UpdateFailed(JWKESTException): - pass - - class WrongUsage(JWKESTException): pass From 970e2845e7ecf10b91fccbddf8d3ab82adb3ae92 Mon Sep 17 00:00:00 2001 From: roland Date: Sat, 6 Jun 2020 20:09:01 +0200 Subject: [PATCH 15/50] No dependency on oidcmsg. KeyJar prepared to be able to use storage system defined on oidcmsg. --- src/cryptojwt/key_jar.py | 19 ++++++++++++------- src/cryptojwt/serialize/item.py | 1 - 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 5a1a47d..06a75a0 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -4,7 +4,6 @@ from typing import List from typing import Optional -from abstorage.base import LabeledAbstractStorage from requests import request from .jwe.jwe import alg2keytype as jwe_alg2keytype @@ -40,7 +39,7 @@ class KeyJar(object): def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, remove_after=3600, httpc=None, httpc_params=None, storage_conf=None, - abstract_storage_cls=LabeledAbstractStorage): + abstract_storage_cls=None): """ KeyJar init function @@ -56,6 +55,8 @@ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, if storage_conf is None: self._issuers = {} else: + if not abstract_storage_cls: + raise ValueError('Missing storage class specification') self._issuers = abstract_storage_cls(storage_conf) self.storage_conf = storage_conf @@ -407,7 +408,7 @@ def export_jwks(self, private=False, issuer_id="", usage=None): """ _issuer = self._get_issuer(issuer_id=issuer_id) if _issuer is None: - return {} + return {"keys": []} keys = [] for kb in _issuer: @@ -437,11 +438,12 @@ def import_jwks(self, jwks, issuer_id): _keys = jwks["keys"] except KeyError: raise ValueError('Not a proper JWKS') - else: + + if _keys: _issuer = self.return_issuer(issuer_id=issuer_id) _issuer.add(self.keybundle_cls(_keys, httpc=self.httpc, httpc_params=self.httpc_params)) - self[issuer_id] = _issuer + self[issuer_id] = _issuer def import_jwks_as_json(self, jwks, issuer_id): """ @@ -776,7 +778,7 @@ def build_keyjar(key_conf, kid_template="", keyjar=None, issuer_id='', storage_c def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', read_only=True, - storage_conf=None): + storage_conf=None, abstract_storage_cls=None): """ A number of cases here: @@ -888,7 +890,10 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea else: _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) - keyjar = KeyJar(storage_conf=storage_conf) + if _issuer is None: + raise ValueError('Could not find any keys') + + keyjar = KeyJar(storage_conf=storage_conf, abstract_storage_cls=abstract_storage_cls) keyjar[issuer_id] = _issuer return keyjar diff --git a/src/cryptojwt/serialize/item.py b/src/cryptojwt/serialize/item.py index 513dbf3..1556f82 100644 --- a/src/cryptojwt/serialize/item.py +++ b/src/cryptojwt/serialize/item.py @@ -6,7 +6,6 @@ class KeyIssuer: - @staticmethod def serialize(item: key_issuer.KeyIssuer) -> str: """ Convert from KeyIssuer to JSON """ From c39ee4999ff46b92739d2d1951268d8668b6da62 Mon Sep 17 00:00:00 2001 From: roland Date: Sat, 6 Jun 2020 20:21:41 +0200 Subject: [PATCH 16/50] A bit of documentation. --- src/cryptojwt/key_jar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 06a75a0..9cc879d 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -49,6 +49,9 @@ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, :param remove_after: How long keys marked as inactive will remain in the key Jar. :param httpc: A HTTP client to use. Default is Requests request. :param httpc_params: HTTP request parameters + :param storage_conf: Storage configuration + :param abstract_storage_cls: Storage class. The only demand on a storage class is that it + should behave like a dictionary. :return: Keyjar instance """ From 22acc84e1b3afdfe5d1f80ccb4cd2fec1dea5eb5 Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 12 May 2020 08:47:51 +0200 Subject: [PATCH 17/50] Astract storage applied. --- aslist_tests/private_jwks.json | 1 + aslist_tests/rsa.key | 15 + aslist_tests/test_43_key_bundle.py | 1014 +++++++++++++++++++ aslist_tests/test_44_key_issuer.py | 585 +++++++++++ aslist_tests/test_45_key_jar.py | 998 ++++++++++++++++++ aslist_tests/test_keys/cert.key | 27 + aslist_tests/test_keys/ec-p256-private.pem | 6 + aslist_tests/test_keys/ec-p256-public.pem | 5 + aslist_tests/test_keys/ec-p256.json | 8 + aslist_tests/test_keys/ec-p384-private.pem | 7 + aslist_tests/test_keys/ec-p384-public.pem | 6 + aslist_tests/test_keys/ec-p384.json | 8 + aslist_tests/test_keys/ec.key | 5 + aslist_tests/test_keys/jwk.json | 12 + aslist_tests/test_keys/rsa-1024-private.pem | 17 + aslist_tests/test_keys/rsa-1024-public.pem | 7 + aslist_tests/test_keys/rsa-1024.json | 9 + aslist_tests/test_keys/rsa-1280-private.pem | 20 + aslist_tests/test_keys/rsa-1280-public.pem | 8 + aslist_tests/test_keys/rsa-1280.json | 9 + aslist_tests/test_keys/rsa-2048-private.pem | 29 + aslist_tests/test_keys/rsa-2048-public.pem | 10 + aslist_tests/test_keys/rsa-2048.json | 9 + aslist_tests/test_keys/rsa-3072-private.pem | 41 + aslist_tests/test_keys/rsa-3072-public.pem | 12 + aslist_tests/test_keys/rsa-3072.json | 9 + aslist_tests/test_keys/rsa-4096-private.pem | 53 + aslist_tests/test_keys/rsa-4096-public.pem | 15 + aslist_tests/test_keys/rsa-4096.json | 9 + aslist_tests/test_keys/rsa.key | 15 + setup.py | 3 +- src/cryptojwt/__init__.py | 2 +- src/cryptojwt/exception.py | 12 + src/cryptojwt/jwt.py | 37 +- src/cryptojwt/key_bundle.py | 126 ++- src/cryptojwt/key_issuer.py | 489 +++++++++ src/cryptojwt/key_jar.py | 573 ++++++----- src/cryptojwt/serialize/__init__.py | 47 + tests/test_04_key_jar.py | 196 ++-- tests/test_09_jwt.py | 4 +- 40 files changed, 4026 insertions(+), 432 deletions(-) create mode 100644 aslist_tests/private_jwks.json create mode 100644 aslist_tests/rsa.key create mode 100755 aslist_tests/test_43_key_bundle.py create mode 100755 aslist_tests/test_44_key_issuer.py create mode 100755 aslist_tests/test_45_key_jar.py create mode 100755 aslist_tests/test_keys/cert.key create mode 100644 aslist_tests/test_keys/ec-p256-private.pem create mode 100644 aslist_tests/test_keys/ec-p256-public.pem create mode 100644 aslist_tests/test_keys/ec-p256.json create mode 100644 aslist_tests/test_keys/ec-p384-private.pem create mode 100644 aslist_tests/test_keys/ec-p384-public.pem create mode 100644 aslist_tests/test_keys/ec-p384.json create mode 100644 aslist_tests/test_keys/ec.key create mode 100755 aslist_tests/test_keys/jwk.json create mode 100644 aslist_tests/test_keys/rsa-1024-private.pem create mode 100644 aslist_tests/test_keys/rsa-1024-public.pem create mode 100644 aslist_tests/test_keys/rsa-1024.json create mode 100644 aslist_tests/test_keys/rsa-1280-private.pem create mode 100644 aslist_tests/test_keys/rsa-1280-public.pem create mode 100644 aslist_tests/test_keys/rsa-1280.json create mode 100644 aslist_tests/test_keys/rsa-2048-private.pem create mode 100644 aslist_tests/test_keys/rsa-2048-public.pem create mode 100644 aslist_tests/test_keys/rsa-2048.json create mode 100644 aslist_tests/test_keys/rsa-3072-private.pem create mode 100644 aslist_tests/test_keys/rsa-3072-public.pem create mode 100644 aslist_tests/test_keys/rsa-3072.json create mode 100644 aslist_tests/test_keys/rsa-4096-private.pem create mode 100644 aslist_tests/test_keys/rsa-4096-public.pem create mode 100644 aslist_tests/test_keys/rsa-4096.json create mode 100755 aslist_tests/test_keys/rsa.key create mode 100755 src/cryptojwt/key_issuer.py create mode 100644 src/cryptojwt/serialize/__init__.py diff --git a/aslist_tests/private_jwks.json b/aslist_tests/private_jwks.json new file mode 100644 index 0000000..b1471f1 --- /dev/null +++ b/aslist_tests/private_jwks.json @@ -0,0 +1 @@ +{"keys": [{"kty": "RSA", "use": "sig", "kid": "ZG5wZjlUYW11MkZSVzA5MTRuaTdDYUtnV0xQS0pwWUpPTm1YMTJNeFlQYw", "n": "qlDQeEoGfykMrV3WupWIMHGrs4AHVTld-C00qBcVCBNptef6T2UESVSurMITmgCdJrwEVfHwkd2is1xSev2pIjQy8m9CehBexxq0hlNmUhPzNPixbCMqUyxCzGi1bms9qSxg2zJr0pmDFK_EiOyM47B48eYcypUk-PxzX4d1L-jBT6F8B9fT8YjS4OHzs__Yq6EDzJ4gubaqOLjsYQsLkYj3fKz-b4trb9n4eWJkkbvtgCN6gZVVKgmxPpQHVhDIj7jHXfl9QmTooWxObvO9LK8DFK63V-E-Ce5iKNcYmeB4TeOJmdZfasFa4TPOg22jZaE9UOnnZPyXx7VOCkiHsw", "e": "AQAB", "d": "KsaZVVziPNXGhVRoNfyQc_pYsYCaVuFNpKNV8lG5yol1p2ZYC9DHPtOx-1nTKn60-aGHRT66uSf9UScC4DkNXbXWheVDwPyTkVY3uPUBYeP41XkQtqQuYS1gqY4y40Sz--VVfjgvtHkx3uQ2bF1dFWKhPcAZwxeqbY6aO4f9-sYFtgIIINe_vDKw6W8RPJMK4keK_V-8Hw9-t1A_gja2_34U9cSw0yO9BDBNJiE88mX-Nu4djfAqr-MVOXpHEW2FIA7Ge6laZlGR_pn_kaqVbEnG8DLJqJGp_mJIg1tx08kHTVXw8TmqKIeMVZ8uU3Vrfjy6D6PmTkEwZK1_EgvxoQ", "p": "15-5UXO89cFmYFbzW9IRfHp7g8VQw88Uphf3J5IjgI8GL80QqFT7r7uBx1b8uKvJIWnG6OxygVJm9-cy9HPQzCBQQ1e_R5OKAaNvMM_nTVAHAy5KZlt_qFJw7vb5tfRkj8u_0jDt1qp6BYXKBMPWzpxwwKG-aj9PyPrkZyFDn6c", "q": "yjUq8rrtQgnGS4fBQwgf44sg8F47PHX-D7fCmImg6gjW8e4Ll51yDlKHFBskvLNzAWNfy8_GTzTs3Nr2zuQrWrkhXlk4T7FIJATVBI4n4uiNEvfYr17fViLM6T8d0WUM_9zy6EI8CBBRdeAeI8obW5p3F6Wu6UygkrysgyFV-RU"}, {"kty": "EC", "use": "sig", "kid": "aDFHbmtvZldackkyUjJ5aTluSWNaVmJnUXkwYmd2Zk5sOGxCM3dhQVgwRQ", "crv": "P-256", "x": "s4o9jPfgErHOuPBdeWp8U_XUGUs7uSntAu5M4GjTCjQ", "y": "y0YE3V9E-UzEAekBLPhxYqrPPo6Abm-JRFL2Sia6Q0s", "d": "BNXlINQCl4O5vJkdqV13gOqzZJVt34RxN8njWq7Lvlc"}, {"kty": "EC", "use": "sig", "kid": "S0hSaHVmb0phaE1tbW9NcHZjVGRNS291VDlNOE5vWl9UaTNwQVhlUHhOZw", "crv": "P-384", "x": "Lu2wHapme_MnXlhpzH5M5ntqx83j6xYZ2P8u7ZoVOWdvRnmxYC1GrV2IA7feLOUv", "y": "f8S10urWzTkaq1JARY0LgLhZsXmcoTFj5Hd3tyCd6h4XlUU3iZDSfuXahF_xqJ24", "d": "SoRVsBruJ8gmBraOC0bzWD13IOUef3dGRtpIWzmEWm9uWScXxmt6AzgcTAhYP29l"}]} \ No newline at end of file diff --git a/aslist_tests/rsa.key b/aslist_tests/rsa.key new file mode 100644 index 0000000..d34432d --- /dev/null +++ b/aslist_tests/rsa.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQD6vqn19W/VB215DBADRakfPmCtFBf8/+YyhGqixWIwDiEl/L6L +w5HKZCUPVgrC0ADhJfvAbn4fte5MWBCTkqgepKL3BySMA0LMaBF12pbHlPSUbmQG +BJmTX4NNXuUel6TbPYJAU2Nh5Nan0Mb7Bmb8QpFvS0Hw7qZRW8y2eIttfwIDAQAB +AoGBAJVf9FxkRKUB8cOE3h006JWGUY2KROghgn9hxy0ErYO3RyQcN1+HuFh75GAI +gAyiYYO/XwS6TkSR2057wBRJ8ABzcL3+v5g+16Vbh0BjXVE+cv1WGdNGujyzl6ji +jlyF4cb6tXDyqWTLkMAtV20NfO/CGsfii6YEkZb2P90usthRAkEA/oG7a9EvQ7eR +gSEqppzW7KCwidPjnZTr/ROIZQU33nwkIJ0ElTjMNYKP8DerSuixR9skw2ZY8Q8I +1PTBnocHwwJBAPw3SAQYwxZwQMu1trVPMNOGIbSY4rQlMZGXrCZSu/TnozczFLA8 +qNM84g5veyJOzHKmYkIsMG1gwg5VNniG45UCQF6SlLOW0upl70K9sVyiUVcyywcc +Xqty6FJtjLSFQOKC3OXlkwtkRLXpo1UPSq6WUzIxY7LceFZzUMPZg41F/gMCQHNr +POqbBlPzZMOUUZthNP/nhu8lc8Fqr+dnmGElRVxK0JdHKfWInN2mI/DlNV064Dar +S5XqsPKs78EtX7MCT40CQFQZiry8m7ROubOU4+HDG9o1w9zcKXCkmbD9hBCGvTAj +BQNuGE0DtC6FEWTs8bXybLM5yBRq1XiKLdmi5N+3n4g= +-----END RSA PRIVATE KEY----- diff --git a/aslist_tests/test_43_key_bundle.py b/aslist_tests/test_43_key_bundle.py new file mode 100755 index 0000000..b25d681 --- /dev/null +++ b/aslist_tests/test_43_key_bundle.py @@ -0,0 +1,1014 @@ +# pylint: disable=missing-docstring,no-self-use +import json +import os +import shutil +import time + +import pytest +import requests +import responses +from abstorage.storages.absqlalchemy import AbstractStorageSQLAlchemy +from cryptography.hazmat.primitives.asymmetric import rsa +from requests_mock import GET + +from cryptojwt.jwk.ec import ECKey +from cryptojwt.jwk.ec import new_ec_key +from cryptojwt.jwk.hmac import SYMKey +from cryptojwt.jwk.rsa import RSAKey +from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file +from cryptojwt.jwk.rsa import new_rsa_key +from cryptojwt.key_bundle import KeyBundle +from cryptojwt.key_bundle import build_key_bundle +from cryptojwt.key_bundle import dump_jwks +from cryptojwt.key_bundle import init_key +from cryptojwt.key_bundle import key_diff +from cryptojwt.key_bundle import key_gen +from cryptojwt.key_bundle import key_rollover +from cryptojwt.key_bundle import keybundle_from_local_file +from cryptojwt.key_bundle import rsa_init +from cryptojwt.key_bundle import unique_keys +from cryptojwt.key_bundle import update_key_bundle + +__author__ = 'Roland Hedberg' + +BASE_PATH = os.path.abspath( + os.path.join(os.path.dirname(__file__), "test_keys")) + +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +def full_path(local_file): + return os.path.join(BASEDIR, local_file) + + +RSAKEY = os.path.join(BASE_PATH, "cert.key") +RSA0 = os.path.join(BASE_PATH, "rsa.key") +EC0 = os.path.join(BASE_PATH, 'ec.key') +CERT = full_path("../tests/cert.pem") + +JWK0 = {"keys": [ + {'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", + 'n': + 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY' + '2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfK' + 'qoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8'} +]} + +JWK1 = {"keys": [ + { + "n": + 'zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S' + '_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFY' + 'Inq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVb' + 'CGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znan' + 'LwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MX' + 'sGxBHf3AKT5w', + "e": "AQAB", "kty": "RSA", "kid": "rsa1"}, + { + "k": + 'YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNT' + 'Y0NzMzYjE', + "kty": "oct"}, +]} + +JWK2 = { + "keys": [ + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", + "kid": "kriMPdmBvx68skT8-mPAB3BseeA", + "kty": "RSA", + "n": + 'kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS' + '_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1uml94fLyq_AXbwo9yAduf4dCHT' + 'P8CWR1dnDR-Qnz_4PYlWVEuuHHONOw_blbfdMjhY-C_BYM2E3pRxbohBb3x__C' + 'fueV7ddz2LYiH3wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_' + 'GTgWN8A-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQtSsjQtI--Xs' + 'okxRDqYLwus1I1SihgbV_STTg5enufuw', + "use": "sig", + "x5c": [ + 'MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKz' + 'ApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcN' + 'MTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW' + '50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEF' + 'AAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs' + '5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94f' + 'Lyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C' + '/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHF' + 'i3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp' + '+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2Iw' + 'YDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYW' + 'Njb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49Y' + 'D0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDb' + 'dNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajy' + 'vlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5Uqn' + 'I7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF4' + '6aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODY' + 'RMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ' + ], + "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", + "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", + "kty": "RSA", + "n": + 'vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhz' + 'h23V9Tkq-RtwN1Vs_z57hO82kkzL-cQHZX3bMJD-GEGOKXCEXURN7VMyZWMAuz' + 'QoW9vFb1k3cR1RW_EW_P-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5y' + 'Cw5T_Vuwqqsio3V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_K' + 'AS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IAbsk1wRtWDn' + 'dhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ', + "use": "sig", + "x5c": [ + "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" + "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" + "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" + "/uPhEf7zVizjfcr/ISGFe9+yUO" + "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" + "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" + "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" + "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" + "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" + "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" + "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" + "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" + "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" + "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" + "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" + "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" + "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" + "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" + ], + "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" + "-b112-36a304b66dad/v2.0/", + "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", + "kty": "RSA", + "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" + "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" + "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", + "use": "sig", + "x5c": [ + "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" + "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" + "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" + "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" + "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" + "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" + "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" + "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" + "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" + "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" + "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" + "+Iqzmn" + "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" + "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" + "qA==" + ], + "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" + "-b112-36a304b66dad/v2.0/", + "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", + "kty": "RSA", + "n": + "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" + "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" + "-KXVD02jdmX2HNdYJdPyc0c", + "use": "sig", + "x5c": [ + "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" + "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" + "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" + "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" + "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" + "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" + "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" + "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" + "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" + "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" + "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" + "ig==" + ], + "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" + } + ] +} + +JWKS_DICT = {"keys": [ + { + "n": + u"zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVbCGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": u"AQAB", + "kty": "RSA", + "kid": "5-VBFv40P8D4I-7SFz7hMugTbPs", + "use": "enc" + }, + { + "k": u"YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "oct", + "use": "enc" + }, + { + "kty": "EC", + "kid": "7snis", + "use": "sig", + "x": u'q0WbWhflRbxyQZKFuQvh2nZvg98ak-twRoO5uo2L7Po', + "y": u'GOd2jL_6wa0cfnyA0SmEhok9fkYEnAHFKLLM79BZ8_E', + "crv": "P-256" + } +]} + +if os.path.isdir('keys'): + shutil.rmtree('keys') + +ABS_STORAGE_SQLALCHEMY = dict( + driver='sqlalchemy', + url='sqlite:///:memory:', + params=dict(table='Thing'), + handler=AbstractStorageSQLAlchemy +) + +STORAGE_CONFIG = { + 'name': '', + 'class': 'abstorage.type.list.ASList', + 'kwargs': { + 'io_class': 'cryptojwt.serialize.item.JWK', + 'storage_config': ABS_STORAGE_SQLALCHEMY + } +} + + +def test_with_sym_key(): + kc = KeyBundle({"kty": "oct", "key": "highestsupersecret", "use": "sig"}, + storage_conf=STORAGE_CONFIG) + assert len(kc.get("oct")) == 1 + assert len(kc.get("rsa")) == 0 + assert kc.remote is False + assert kc.source is None + + +def test_with_2_sym_key(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) + assert len(kb.get("oct")) == 2 + assert len(kb) == 2 + + assert kb.get_key_with_kid('kid') is None + assert len(kb.kids()) == 2 + + +def test_remove_sym(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) + assert len(kb) == 2 + keys = kb.get('oct') + kb.remove(keys[0]) + assert len(kb) == 1 + + +def test_remove_key_sym(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) + assert len(kb) == 2 + keys = kb.get('oct') + kb.remove(keys[0]) + assert len(kb) == 1 + + # This should not work + kb.remove_keys_by_type('rsa') + # should still be one + assert len(kb) == 1 + + +def test_rsa_init(): + kb = rsa_init( + {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) + assert kb + assert len(kb) == 2 + assert len(kb.get('rsa')) == 2 + + +def test_rsa_init_under_spec(): + kb = rsa_init( + {'use': ['enc', 'sig'], 'size': 1024}, storage_conf=STORAGE_CONFIG) + assert kb + assert len(kb) == 2 + assert len(kb.get('rsa')) == 2 + + +def test_unknown_source(): + with pytest.raises(ImportError): + KeyBundle(source='foobar', storage_conf=STORAGE_CONFIG) + + +def test_ignore_unknown_types(): + kb = KeyBundle({ + "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" + "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", + "use": "sig", + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" + }, + storage_conf=STORAGE_CONFIG) + + assert len(kb) == 0 + + +def test_remove_rsa(): + kb = rsa_init( + {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, + storage_conf=STORAGE_CONFIG) + assert len(kb) == 2 + keys = kb.get('rsa') + assert len(keys) == 2 + kb.remove(keys[0]) + assert len(kb) == 1 + + +def test_key_mix(): + kb = rsa_init( + {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, + storage_conf=STORAGE_CONFIG) + _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) + kb.append(_sym) + assert len(kb) == 3 + assert len(kb.get('rsa')) == 2 + assert len(kb.get('oct')) == 1 + + kb.remove(_sym) + + assert len(kb) == 2 + assert len(kb.get('rsa')) == 2 + assert len(kb.get('oct')) == 0 + + +def test_get_all(): + kb = rsa_init( + {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, + storage_conf=STORAGE_CONFIG + ) + _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) + kb.append(_sym) + assert len(kb.get()) == 3 + + _k = kb.keys() + assert len(_k) == 3 + + +def test_keybundle_from_local_der(): + kb = keybundle_from_local_file( + "{}".format(RSA0), + "der", ['enc'], storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + keys = kb.get('rsa') + assert len(keys) == 1 + _key = keys[0] + assert isinstance(_key, RSAKey) + assert _key.kid + + +def test_ec_keybundle_from_local_der(): + kb = keybundle_from_local_file( + "{}".format(EC0), + "der", ['enc'], keytype='EC', storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + keys = kb.get('ec') + assert len(keys) == 1 + _key = keys[0] + assert _key.kid + assert isinstance(_key, ECKey) + + +def test_keybundle_from_local_der_update(): + kb = keybundle_from_local_file( + "file://{}".format(RSA0), + "der", ['enc'], storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + keys = kb.get('rsa') + assert len(keys) == 1 + _key = keys[0] + assert _key.kid + assert isinstance(_key, RSAKey) + + kb.update() + + # Nothing should change + assert len(kb) == 1 + keys = kb.get('rsa') + assert len(keys) == 1 + _key = keys[0] + assert _key.kid + assert isinstance(_key, RSAKey) + + +def test_creat_jwks_sym(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + kb = KeyBundle([a], storage_conf=STORAGE_CONFIG) + _jwks = kb.jwks() + _loc = json.loads(_jwks) + assert list(_loc.keys()) == ["keys"] + assert set(_loc['keys'][0].keys()) == {'kty', 'use', 'k', 'kid'} + + +def test_keybundle_from_local_jwks_file(): + kb = keybundle_from_local_file( + "file://{}".format(os.path.join(BASE_PATH, "jwk.json")), "jwks", ["sig"], + storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + + +def test_keybundle_from_local_jwks(): + kb = keybundle_from_local_file( + "{}".format(os.path.join(BASE_PATH, "jwk.json")), "jwks", ["sig"], + storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + + +def test_update(): + kc = KeyBundle([{"kty": "oct", "key": "highestsupersecret", "use": "sig"}], + storage_conf=STORAGE_CONFIG) + assert len(kc.get("oct")) == 1 + assert len(kc.get("rsa")) == 0 + assert kc.remote is False + assert kc.source is None + + kc.update() # Nothing should happen + assert len(kc.get("oct")) == 1 + assert len(kc.get("rsa")) == 0 + assert kc.remote is False + assert kc.source is None + + +def test_update_RSA(): + kc = keybundle_from_local_file(RSAKEY, "der", ["sig"], storage_conf=STORAGE_CONFIG) + assert kc.remote is False + assert len(kc.get("oct")) == 0 + assert len(kc.get("RSA")) == 1 + + key = kc.get("RSA")[0] + assert isinstance(key, RSAKey) + + kc.update() + assert kc.remote is False + assert len(kc.get("oct")) == 0 + assert len(kc.get("RSA")) == 1 + + key = kc.get("RSA")[0] + assert isinstance(key, RSAKey) + + +def test_outdated(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) + keys = kb.keys() + now = time.time() + keys[0].inactive_since = now - 60 + kb.set(keys) + kb.remove_outdated(30) + assert len(kb) == 1 + + +def test_dump_jwks(): + a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb2 = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) + + kb1 = rsa_init({'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) + + # Will not dump symmetric keys + dump_jwks([kb1, kb2], 'jwks_combo') + + # Now read it + + nkb = KeyBundle(source='file://jwks_combo', fileformat='jwks', storage_conf=STORAGE_CONFIG) + + assert len(nkb) == 2 + # both RSA keys + assert len(nkb.get('rsa')) == 2 + + # Will dump symmetric keys + dump_jwks([kb1, kb2], 'jwks_combo', symmetric_too=True) + + # Now read it + nkb = KeyBundle(source='file://jwks_combo', fileformat='jwks', storage_conf=STORAGE_CONFIG) + + assert len(nkb) == 4 + # two RSA keys + assert len(nkb.get('rsa')) == 2 + # two symmetric keys + assert len(nkb.get('oct')) == 2 + + +def test_mark_as_inactive(): + desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) + assert len(kb.keys()) == 1 + for k in kb.keys(): + kb.mark_as_inactive(k.kid) + desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb.do_keys([desc]) + assert len(kb.keys()) == 2 + assert len(kb.active_keys()) == 1 + + +def test_copy(): + desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) + assert len(kb.keys()) == 1 + for k in kb.keys(): + kb.mark_as_inactive(k.kid) + desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb.do_keys([desc]) + + kbc = kb.copy() + assert len(kbc.keys()) == 2 + assert len(kbc.active_keys()) == 1 + + +def test_local_jwk(): + _path = full_path('../tests/jwk_private_key.json') + kb = KeyBundle(source='file://{}'.format(_path), storage_conf=STORAGE_CONFIG) + assert kb + + +def test_local_jwk_copy(): + _path = full_path('../tests/jwk_private_key.json') + kb = KeyBundle(source='file://{}'.format(_path), storage_conf=STORAGE_CONFIG) + kb2 = kb.copy() + assert kb2.source == kb.source + + +@pytest.fixture() +def mocked_jwks_response(): + with responses.RequestsMock() as rsps: + yield rsps + + +def test_httpc_params_1(): + source = 'https://login.salesforce.com/id/keys' # From test_jwks_url() + # Mock response + with responses.RequestsMock() as rsps: + rsps.add(method=GET, url=source, json=JWKS_DICT, status=200) + httpc_params = {'timeout': (2, 2)} # connect, read timeouts in seconds + kb = KeyBundle(source=source, httpc=requests.request, + httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) + assert kb.do_remote() + + +def test_httpc_params_2(): + httpc_params = {'timeout': 0} + kb = KeyBundle(source='https://login.salesforce.com/id/keys', + httpc=requests.request, httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) + # Will always fail to fetch the JWKS because the timeout cannot be set + # to 0s + assert not kb.update() + + +def test_update_2(): + rsa_key = new_rsa_key() + _jwks = {"keys": [rsa_key.serialize()]} + fname = 'tmp_jwks.json' + with open(fname, 'w') as fp: + fp.write(json.dumps(_jwks)) + + kb = KeyBundle(source="file://{}".format(fname), fileformat='jwks', storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + + # Added one more key + ec_key = new_ec_key(crv='P-256', key_ops=["sign"]) + _jwks = {'keys': [rsa_key.serialize(), ec_key.serialize()]} + + with open(fname, 'w') as fp: + fp.write(json.dumps(_jwks)) + + kb.update() + assert len(kb) == 2 + + +def test_update_mark_inactive(): + rsa_key = new_rsa_key() + _jwks = {"keys": [rsa_key.serialize()]} + fname = 'tmp_jwks.json' + with open(fname, 'w') as fp: + fp.write(json.dumps(_jwks)) + + kb = KeyBundle(source="file://{}".format(fname), fileformat='jwks', storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + + # new set of keys + rsa_key = new_rsa_key(alg="RS256") + ec_key = new_ec_key(crv='P-256') + _jwks = {'keys': [rsa_key.serialize(), ec_key.serialize()]} + + with open(fname, 'w') as fp: + fp.write(json.dumps(_jwks)) + + kb.update() + # 2 active and 1 inactive + assert len(kb) == 3 + assert len(kb.active_keys()) == 2 + + assert len(kb.get('rsa')) == 1 + assert len(kb.get('rsa', only_active=False)) == 2 + + +def test_loads_0(): + kb = KeyBundle(JWK0, storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + key = kb.get("rsa")[0] + assert key.kid == 'abc' + assert key.kty == 'RSA' + + +def test_loads_1(): + jwks = { + "keys": [ + { + 'kty': 'RSA', + 'use': 'sig', + 'e': 'AQAB', + "n": + 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8', + 'kid': "1" + }, { + 'kty': 'RSA', + 'use': 'enc', + 'e': 'AQAB', + "n": + 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8', + 'kid': "2" + } + ] + } + + kb = KeyBundle(jwks, storage_conf=STORAGE_CONFIG) + + assert len(kb) == 2 + assert set(kb.kids()) == {'1', '2'} + + +def test_dump_jwk(): + kb = KeyBundle(storage_conf=STORAGE_CONFIG) + kb.append(RSAKey(pub_key=import_rsa_key_from_cert_file(CERT))) + jwks = kb.jwks() + + _wk = json.loads(jwks) + assert list(_wk.keys()) == ["keys"] + assert len(_wk["keys"]) == 1 + assert set(_wk["keys"][0].keys()) == {"kty", "e", "n"} + + kb2 = KeyBundle(_wk, storage_conf=STORAGE_CONFIG) + + assert len(kb2) == 1 + key = kb2.get("rsa")[0] + assert key.kty == 'RSA' + assert isinstance(key.public_key(), rsa.RSAPublicKey) + + +JWKS_DICT = {"keys": [ + { + "n": + u"zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVbCGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": u"AQAB", + "kty": "RSA", + "kid": "5-VBFv40P8D4I-7SFz7hMugTbPs", + "use": "enc" + }, + { + "k": u"YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "oct", + "use": "enc" + }, + { + "kty": "EC", + "kid": "7snis", + "use": "sig", + "x": u'q0WbWhflRbxyQZKFuQvh2nZvg98ak-twRoO5uo2L7Po', + "y": u'GOd2jL_6wa0cfnyA0SmEhok9fkYEnAHFKLLM79BZ8_E', + "crv": "P-256" + } +]} + + +def test_keys(): + kb = KeyBundle(JWKS_DICT, storage_conf=STORAGE_CONFIG) + + assert len(kb) == 3 + + assert len(kb.get('rsa')) == 1 + assert len(kb.get('oct')) == 1 + assert len(kb.get('ec')) == 1 + + +EXPECTED = [ + b'iA7PvG_DfJIeeqQcuXFmvUGjqBkda8In_uMpZrcodVA', + b'akXzyGlXg8yLhsCczKb_r8VERLx7-iZBUMIVgg2K7p4', + b'kLsuyGef1kfw5-t-N9CJLIHx_dpZ79-KemwqjwdrvTI' +] + + +def test_thumbprint(): + kb = KeyBundle(JWKS_DICT, storage_conf=STORAGE_CONFIG) + for key in kb: + txt = key.thumbprint('SHA-256') + assert txt in EXPECTED + + +@pytest.mark.network +def test_jwks_url(): + keys = KeyBundle(source='https://login.salesforce.com/id/keys', storage_conf=STORAGE_CONFIG) + # Forces read from the network + keys.update() + assert len(keys) + + +KEYSPEC = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]} +] + +KEYSPEC_2 = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]} +] + +KEYSPEC_3 = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]}, + {"type": "EC", "crv": "P-521", "use": ["sig"]} +] + +KEYSPEC_4 = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]} +] + +KEYSPEC_5 = [ + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]} +] + +KEYSPEC_6 = [ + {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'code'}, + {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'token'}, + {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'refresh_token'} +] + + +def test_key_diff_none(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + + diff = key_diff(_kb, KEYSPEC) + assert not diff + + +def test_key_diff_add_one_ec(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + + diff = key_diff(_kb, KEYSPEC_2) + assert diff + assert set(diff.keys()) == {'add'} + assert len(diff['add']) == 1 + assert diff['add'][0].kty == 'EC' + + +def test_key_diff_add_two_ec(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + + diff = key_diff(_kb, KEYSPEC_3) + assert diff + assert set(diff.keys()) == {'add'} + assert len(diff['add']) == 2 + assert diff['add'][0].kty == 'EC' + + +def test_key_diff_add_ec_and_rsa(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + + diff = key_diff(_kb, KEYSPEC_4) + assert diff + assert set(diff.keys()) == {'add'} + assert len(diff['add']) == 2 + assert set([k.kty for k in diff['add']]) == {'EC', 'RSA'} + + +def test_key_diff_add_ec_del_rsa(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + + diff = key_diff(_kb, KEYSPEC_5) + assert diff + assert set(diff.keys()) == {'add', 'del'} + assert len(diff['add']) == 1 + assert len(diff['del']) == 1 + assert diff['add'][0].kty == 'EC' + assert diff['del'][0].kty == 'RSA' + + +def test_key_bundle_update_1(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + diff = key_diff(_kb, KEYSPEC_2) + update_key_bundle(_kb, diff) + + # There should be 3 keys + assert len(_kb) == 3 + + # one RSA + assert len(_kb.get('RSA')) == 1 + + # 2 EC + assert len(_kb.get('EC')) == 2 + + +def test_key_bundle_update_2(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + diff = key_diff(_kb, KEYSPEC_4) + update_key_bundle(_kb, diff) + + # There should be 3 keys + assert len(_kb) == 4 + + # one RSA + assert len(_kb.get('RSA')) == 2 + + # 2 EC + assert len(_kb.get('EC')) == 2 + + +def test_key_bundle_update_3(): + _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + diff = key_diff(_kb, KEYSPEC_5) + assert set(diff.keys()) == {'add', 'del'} # Add an EC and delete an RSA key + update_key_bundle(_kb, diff) + + # There should be 3 keys + assert len(_kb) == 3 + + # One inactive. Only active is implicit + assert len(_kb.get()) == 2 + + # one inactive RSA + assert len(_kb.get('RSA', only_active=False)) == 1 + assert len(_kb.get('RSA')) == 0 + + # 2 EC + assert len(_kb.get('EC')) == 2 + assert len(_kb.get('EC', only_active=False)) == 2 + + +def test_key_rollover(): + kb_0 = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) + assert len(kb_0.get(only_active=False)) == 2 + assert len(kb_0.get()) == 2 + + kb_1 = key_rollover(kb_0) + + assert len(kb_1.get(only_active=False)) == 4 + assert len(kb_1.get()) == 2 + + +def test_build_key_bundle_sym(): + _kb = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + assert len(_kb) == 3 + + assert len(_kb.get('RSA')) == 0 + assert len(_kb.get('EC')) == 0 + assert len(_kb.get('oct')) == 3 + + +def test_key_bundle_difference_none(): + _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + _kb1 = KeyBundle(storage_conf=STORAGE_CONFIG) + _kb1.extend(_kb0.keys()) + + assert _kb0.difference(_kb1) == [] + + +def test_key_bundle_difference(): + _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + _kb1 = build_key_bundle(key_conf=KEYSPEC_2, storage_conf=STORAGE_CONFIG) + + assert _kb0.difference(_kb1) == _kb0.keys() + assert _kb1.difference(_kb0) == _kb1.keys() + + +def test_unique_keys_1(): + _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + _kb1 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + + keys = _kb0.keys() + keys.extend(_kb1.keys()) + + # All of them + assert len(unique_keys(keys)) == 6 + + +def test_unique_keys_2(): + _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) + _kb1 = KeyBundle(storage_conf=STORAGE_CONFIG) + _kb1.extend(_kb0.keys()) + + keys = _kb0.keys() + keys.extend(_kb1.keys()) + + # 3 of 6 + assert len(unique_keys(keys)) == 3 + + +def test_key_gen_rsa(): + _jwk = key_gen("RSA", kid="kid1") + assert _jwk + assert _jwk.kty == "RSA" + assert _jwk.kid == 'kid1' + + assert isinstance(_jwk, RSAKey) + + +def test_init_key(): + spec = { + "type": "RSA", + "kid": "one" + } + + filename = full_path("../tests/tmp_jwk.json") + if os.path.isfile(filename): + os.unlink(filename) + + _key = init_key(filename, **spec) + assert _key.kty == "RSA" + assert _key.kid == 'one' + + assert os.path.isfile(filename) + + # Should not lead to any change + _jwk2 = init_key(filename, **spec) + assert _key == _jwk2 + + _jwk3 = init_key(filename, "RSA", "two") + assert _key != _jwk3 + + # Now _jwk3 is stored in the file + _jwk4 = init_key(filename, "RSA") + assert _jwk4 == _jwk3 + + +def test_export_inactive(): + desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} + kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) + assert len(kb.keys()) == 1 + for k in kb.keys(): + kb.mark_as_inactive(k.kid) + desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} + kb.do_keys([desc]) + res = kb.dump() + assert set(res.keys()) == {'cache_time', + 'fileformat', + 'httpc_params', + 'imp_jwks', + 'keys', + 'last_updated', + 'remote', + 'time_out'} + + kb2 = KeyBundle(storage_conf=STORAGE_CONFIG).load(res) + assert len(kb2.keys()) == 2 + assert len(kb2.active_keys()) == 1 + + +def test_remote(): + source = 'https://example.com/keys.json' + # Mock response + with responses.RequestsMock() as rsps: + rsps.add(method="GET", url=source, json=JWKS_DICT, status=200) + httpc_params = {'timeout': (2, 2)} # connect, read timeouts in seconds + kb = KeyBundle(source=source, httpc=requests.request, + httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) + kb.do_remote() + + exp = kb.dump() + kb2 = KeyBundle(storage_conf=STORAGE_CONFIG).load(exp) + assert kb2.source == source + assert len(kb2.keys()) == 3 + assert len(kb2.get("rsa")) == 1 + assert len(kb2.get("oct")) == 1 + assert len(kb2.get("ec")) == 1 + assert kb2.httpc_params == {'timeout': (2, 2)} + assert kb2.imp_jwks + assert kb2.last_updated diff --git a/aslist_tests/test_44_key_issuer.py b/aslist_tests/test_44_key_issuer.py new file mode 100755 index 0000000..8bacecf --- /dev/null +++ b/aslist_tests/test_44_key_issuer.py @@ -0,0 +1,585 @@ +import os +import time + +import pytest +from abstorage.storages.absqlalchemy import AbstractStorageSQLAlchemy + +from cryptojwt.exception import JWKESTException +from cryptojwt.key_bundle import KeyBundle +from cryptojwt.key_bundle import keybundle_from_local_file +from cryptojwt.key_issuer import KeyIssuer +from cryptojwt.key_issuer import build_keyissuer + +__author__ = 'Roland Hedberg' + +BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + "test_keys")) +RSAKEY = os.path.join(BASE_PATH, "cert.key") +RSA0 = os.path.join(BASE_PATH, "rsa.key") +EC0 = os.path.join(BASE_PATH, "ec.key") +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +def full_path(local_file): + return os.path.join(BASEDIR, local_file) + + +JWK0 = { + "keys": [ + { + 'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", + 'n': + 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5' + 'B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8' + } + ] +} + +JWK1 = { + "keys": [ + { + "n": + "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" + "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" + "-NvS-aG_jN5cstVbCGWE20H0vF" + "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" + "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" + "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": "AQAB", "kty": "RSA", "kid": "rsa1" + }, + { + "k": + "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "oct" + }, + ] +} + +JWK2 = { + "keys": [ + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", + "kid": "kriMPdmBvx68skT8-mPAB3BseeA", + "kty": "RSA", + "n": + "kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1um" + "l94fLyq_AXbwo9yAduf4dCHTP8CWR1dnDR" + "-Qnz_4PYlWVEuuHHONOw_blbfdMjhY" + "-C_BYM2E3pRxbohBb3x__CfueV7ddz2LYiH3" + "wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_GTgWN8A" + "-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQt" + "SsjQtI--XsokxRDqYLwus1I1SihgbV_STTg5enufuw", + "use": "sig", + "x5c": [ + "MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb" + "2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb2" + "50cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipg" + "H0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6" + "/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Q" + "nz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x" + "//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13S" + "QwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp" + "+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5en" + "ufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJ" + "vbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdN" + "VGKCmSf8M65b8h0NwlIjGGGy" + "/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADD" + "kN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5" + "+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8y" + "PJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW" + "+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ" + ], + "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", + "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", + "kty": "RSA", + "n": + "vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhzh23V9Tkq" + "-RtwN1Vs_z57hO82kkzL-cQHZX3bMJ" + "D-GEGOKXCEXURN7VMyZWMAuzQoW9vFb1k3cR1RW_EW_P" + "-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5yCw5T_Vuwqqsio3" + "V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_KAS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IA" + "bsk1wRtWDndhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ", + "use": "sig", + "x5c": [ + "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" + "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" + "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" + "/uPhEf7zVizjfcr/ISGFe9+yUO" + "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" + "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" + "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" + "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" + "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" + "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" + "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" + "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" + "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" + "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" + "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" + "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" + "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" + "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" + ], + "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" + "-b112-36a304b66dad/v2.0/", + "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", + "kty": "RSA", + "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" + "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" + "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", + "use": "sig", + "x5c": [ + "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" + "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" + "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" + "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" + "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" + "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" + "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" + "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" + "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" + "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" + "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" + "+Iqzmn" + "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" + "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" + "qA==" + ], + "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" + "-b112-36a304b66dad/v2.0/", + "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", + "kty": "RSA", + "n": + "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" + "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" + "-KXVD02jdmX2HNdYJdPyc0c", + "use": "sig", + "x5c": [ + "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" + "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" + "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" + "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" + "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" + "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" + "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" + "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" + "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" + "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" + "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" + "ig==" + ], + "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" + } + ] +} + +ABS_STORAGE_SQLALCHEMY = dict( + driver='sqlalchemy', + url='sqlite:///:memory:', + params=dict(table='Thing'), + handler=AbstractStorageSQLAlchemy +) + +STORAGE_CONFIG = { + 'KeyIssuer': { + 'name': '', + 'class': 'abstorage.type.list.ASList', + 'kwargs': { + 'io_class': 'cryptojwt.serialize.item.KeyBundle', + 'storage_config': ABS_STORAGE_SQLALCHEMY + } + }, + 'KeyBundle': { + 'name': '', + 'class': 'abstorage.type.list.ASList', + 'kwargs': { + 'io_class': 'cryptojwt.serialize.item.JWK', + 'storage_config': ABS_STORAGE_SQLALCHEMY + } + } +} + + +def test_build_key_issuer(): + keys = [ + {"type": "RSA", "use": ["enc", "sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + ] + + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + + assert len(key_issuer) == 3 # A total of 3 keys + assert len(key_issuer.get('sig')) == 2 # 2 for signing + assert len(key_issuer.get('enc')) == 1 # 1 for encryption + + +def test_build_keyissuer_usage(): + keys = [ + {"type": "RSA", "use": ["enc", "sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "oct", "use": ["enc"]}, + {"type": "oct", "use": ["enc"]}, + ] + + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + jwks_sig = key_issuer.export_jwks(usage='sig') + jwks_enc = key_issuer.export_jwks(usage='enc') + assert len(jwks_sig.get('keys')) == 2 # A total of 2 keys with use=sig + assert len(jwks_enc.get('keys')) == 3 # A total of 3 keys with use=enc + + for key in jwks_sig["keys"]: + assert "d" not in key # the JWKS shouldn't contain the private part of the keys + for key in jwks_enc["keys"]: + assert "d" not in key # the JWKS shouldn't contain the private part of the keys + + +def test_build_keyissuer_missing(tmpdir): + keys = [ + { + "type": "RSA", "key": os.path.join(tmpdir.dirname, "missing_file"), + "use": ["enc", "sig"] + }] + + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + assert key_issuer is None + + +def test_build_RSA_keyissuer_from_file(tmpdir): + keys = [{"type": "RSA", "key": RSA0, "use": ["enc", "sig"]}] + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + assert len(key_issuer) == 2 + + +def test_build_EC_keyissuer_missing(tmpdir): + keys = [ + { + "type": "EC", "key": os.path.join(tmpdir.dirname, "missing_file"), + "use": ["enc", "sig"] + }] + + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + assert key_issuer is None + + +def test_build_EC_keyissuer_from_file(tmpdir): + keys = [ + { + "type": "EC", "key": EC0, + "use": ["enc", "sig"] + }] + + key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) + + assert len(key_issuer) == 2 + + +class TestKeyIssuer(object): + def test_add_kb(self): + issuer = KeyIssuer(name='https://issuer.example.com', storage_conf=STORAGE_CONFIG) + kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG) + issuer.add_kb(kb) + assert len(issuer.all_keys()) == 1 + + def test_add_symmetric(self): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.add_symmetric('abcdefghijklmnop', ['sig']) + assert len(issuer.get('oct')) == 1 + + def test_add(self): + issuer = KeyIssuer(name='https://issuer.example.com', storage_conf=STORAGE_CONFIG) + kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG) + issuer.add(kb) + assert len(issuer.all_keys()) == 1 + + def test_items(self): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.add(KeyBundle( + [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, + {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}]), storage_conf=STORAGE_CONFIG) + issuer.add( + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG)) + + assert len(issuer.items()) == 2 + + def test_get_enc(self): + issuer = KeyIssuer() + issuer.add(KeyBundle( + [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, + {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}]), storage_conf=STORAGE_CONFIG) + issuer.add( + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG)) + + assert issuer.get('enc', 'oct') + + def test_dump_issuer_keys(self): + kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", + ["sig"], storage_conf=STORAGE_CONFIG) + assert len(kb) == 1 + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.add(kb) + _jwks_dict = issuer.export_jwks() + + _info = _jwks_dict['keys'][0] + assert _info == { + 'use': 'sig', + 'e': 'AQAB', + 'kty': 'RSA', + 'alg': 'RS256', + 'n': 'pKybs0WaHU_y4cHxWbm8Wzj66HtcyFn7Fh3n' + '-99qTXu5yNa30MRYIYfSDwe9JVc1JUoGw41yq2StdGBJ40HxichjE' + '-Yopfu3B58Q' + 'lgJvToUbWD4gmTDGgMGxQxtv1En2yedaynQ73sDpIK-12JJDY55pvf' + '-PCiSQ9OjxZLiVGKlClDus44_uv2370b9IN2JiEOF-a7JB' + 'qaTEYLPpXaoKWDSnJNonr79tL0T7iuJmO1l705oO3Y0TQ' + '-INLY6jnKG_RpsvyvGNnwP9pMvcP1phKsWZ10ofuuhJGRp8IxQL9Rfz' + 'T87OvF0RBSO1U73h09YP-corWDsnKIi6TbzRpN5YDw', + 'kid': 'abc' + } + + def test_no_use(self): + kb = KeyBundle(JWK0["keys"]) + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.add(kb) + enc_key = issuer.get('enc', "RSA") + assert enc_key != [] + + @pytest.mark.network + def test_provider(self): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.load_keys(jwks_uri="https://connect-op.herokuapp.com/jwks.json") + + assert issuer.all_keys() + + +def test_import_jwks(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWK1) + assert len(issuer.all_keys()) == 2 + + +def test_get_signing_key_use_undefined(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWK1) + keys = issuer.get('sig', kid='rsa1') + assert len(keys) == 1 + + keys = issuer.get('sig', key_type='rsa') + assert len(keys) == 1 + + keys = issuer.get('sig', key_type='rsa', kid='rsa1') + assert len(keys) == 1 + + +KEYDEFS = [ + {"type": "RSA", "key": '', "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]} +] + + +def test_remove_after(): + # initial key_issuer + key_issuer = build_keyissuer(KEYDEFS, storage_conf=STORAGE_CONFIG) + _old = [k.kid for k in key_issuer.all_keys() if k.kid] + assert len(_old) == 2 + + # rotate_keys = create new keys + make the old as inactive + key_issuer = build_keyissuer(KEYDEFS, key_issuer=key_issuer) + + key_issuer.remove_after = 1 + # None are remove since none are marked as inactive yet + key_issuer.remove_outdated() + + _interm = [k.kid for k in key_issuer.all_keys() if k.kid] + assert len(_interm) == 4 + + # Now mark the keys to be inactivated + _now = time.time() + for kid in _old: + key_issuer.mark_as_inactive(kid) + + key_issuer.remove_outdated(_now + 5) + + # The remainder are the new keys + _new = [k.kid for k in key_issuer.all_keys() if k.kid] + assert len(_new) == 2 + + # should not be any overlap between old and new + assert set(_new).intersection(set(_old)) == set() + + +JWK_UK = { + "keys": [ + { + "n": + "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" + "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" + "-NvS-aG_jN5cstVbCGWE20H0vF" + "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" + "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" + "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": "AQAB", "kty": "RSA", "kid": "rsa1" + }, + { + "k": + "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "buz" + }, + ] +} + + +def test_load_unknown_keytype(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWK_UK) + assert len(issuer.all_keys()) == 1 + + +JWK_FP = { + "keys": [ + {"e": "AQAB", "kty": "RSA", "kid": "rsa1"}, + ] +} + + +def test_load_missing_key_parameter(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + with pytest.raises(JWKESTException): + issuer.import_jwks(JWK_FP) + + +JWKS_SPO = { + "keys": [ + { + "kid": + "BfxfnahEtkRBG3Hojc9XGLGht_5rDBj49Wh3sBDVnzRpulMqYwMRmpizA0aSPT1fhCHYivTiaucWUqFu_GwTqA", + "use": "sig", + "alg": "ES256", + "kty": "EC", + "crv": "P-256", + "x": "1XXUXq75gOPZ4bEj1o2Z5XKJWSs6LmL6fAOK3vyMzSc", + "y": "ac1h_DwyuUxhkrD9oKMJ-b_KuiVvvSARIwT-XoEmDXs" + }, + { + "kid": + "91pD1H81rXUvrfg9mkngIG-tXjnldykKUVbITDIU1SgJvq91b8clOcJuEHNAq61eIvg8owpEvWcWAtlbV2awyA", + "use": "sig", + "alg": "ES256", + "kty": "EC", + "crv": "P-256", + "x": "2DfQoLpZS2j3hHEcHDkzV8ISx-RdLt6Opy8YZYVm4AQ", + "y": "ycvkFMBIzgsowiaf6500YlG4vaMSK4OF7WVtQpUbEE0" + }, + { + "kid": "0sIEl3MUJiCxrqleEBBF-_bZq5uClE84xp-wpt8oOI" + "-WIeNxBjSR4ak_OTOmLdndB0EfDLtC7X1JrnfZILJkxA", + "use": "sig", + "alg": "RS256", + "kty": "RSA", + "n": + "yG9914Q1j63Os4jX5dBQbUfImGq4zsXJD4R59XNjGJlEt5ek6NoiDl0ucJO3_7_R9e5my2ONTSqZhtzFW6MImnIn8idWYzJzO2EhUPCHTvw_2oOGjeYTE2VltIyY_ogIxGwY66G0fVPRRH9tCxnkGOrIvmVgkhCCGkamqeXuWvx9MCHL_gJbZJVwogPSRN_SjA1gDlvsyCdA6__CkgAFcSt1sGgiZ_4cQheKexxf1-7l8R91ZYetz53drk2FS3SfuMZuwMM4KbXt6CifNhzh1Ye-5Tr_ZENXdAvuBRDzfy168xnk9m0JBtvul9GoVIqvCVECB4MPUb7zU6FTIcwRAw", + "e": "AQAB" + }, + { + "kid": + "zyDfdEU7pvH0xEROK156ik8G7vLO1MIL9TKyL631kSPtr9tnvs9XOIiq5jafK2hrGr2qqvJdejmoonlGqWWZRA", + "use": "sig", + "alg": "RS256", + "kty": "RSA", + "n": + "68be-nJp46VLj4Ci1V36IrVGYqkuBfYNyjQTZD_7yRYcERZebowOnwr3w0DoIQpl8iL2X8OXUo7rUW_LMzLxKx2hEmdJfUn4LL2QqA3KPgjYz8hZJQPG92O14w9IZ-8bdDUgXrg9216H09yq6ZvJrn5Nwvap3MXgECEzsZ6zQLRKdb_R96KFFgCiI3bEiZKvZJRA7hM2ePyTm15D9En_Wzzfn_JLMYgE_DlVpoKR1MsTinfACOlwwdO9U5Dm-5elapovILTyVTgjN75i-wsPU2TqzdHFKA-4hJNiWGrYPiihlAFbA2eUSXuEYFkX43ahoQNpeaf0mc17Jt5kp7pM2w", + "e": "AQAB" + }, + { + "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" + "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", + "use": "sig", + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" + }, + { + "kid": + "bL33HthM3fWaYkY2_pDzUd7a65FV2R2LHAKCOsye8eNmAPDgRgpHWPYpWFVmeaujUUEXRyDLHN" + "-Up4QH_sFcmw", + "use": "sig", + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "CS01DGXDBPV9cFmd8tgFu3E7eHn1UcP7N1UCgd_JgZo" + } + ] +} + + +def test_load_spomky_keys(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWKS_SPO) + assert len(issuer.all_keys()) == 4 + + +def test_get_ec(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWKS_SPO) + k = issuer.get('sig', 'EC', alg='ES256') + assert k + + +def test_get_ec_wrong_alg(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWKS_SPO) + k = issuer.get('sig', 'EC', alg='ES512') + assert k == [] + + +def test_keys_by_alg_and_usage(): + issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) + issuer.import_jwks(JWKS_SPO) + k = issuer.get('sig', alg='RS256') + assert len(k) == 2 + + +def test_copy(): + issuer = KeyIssuer('Alice', storage_conf=STORAGE_CONFIG) + issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) + issuer_copy = issuer.copy() + + assert len(issuer_copy.get('sig', 'oct')) == 0 + assert len(issuer_copy.get('sig', 'rsa')) == 1 + + +def test_repr(): + issuer = KeyIssuer('Alice', storage_conf=STORAGE_CONFIG) + issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) + txt = issuer.__repr__() + assert " str: + return ''.format(self._bundles) + + def __getitem__(self, item): + return self.get_bundles()[item] + + def set(self, items): + self._bundles.set(items) + + def get_bundles(self): + return [kb for kb in self._bundles] + + def add_url(self, url, **kwargs): + """ + Add a set of keys by url. This method will create a + :py:class:`oidcmsg.key_bundle.KeyBundle` instance with the + url as source specification. If no file format is given it's assumed + that what's on the other side is a JWKS. + + :param issuer: Who issued the keys + :param url: Where can the key/-s be found + :param kwargs: extra parameters for instantiating KeyBundle + :return: A :py:class:`oidcmsg.oauth2.keybundle.KeyBundle` instance + """ + + if not url: + raise KeyError("No url given") + + if "/localhost:" in url or "/localhost/" in url: + _params = self.httpc_params.copy() + _params['verify'] = False + kb = self.keybundle_cls(source=url, httpc=self.httpc, httpc_params=_params, + storage_conf=self.storage_conf, **kwargs) + else: + kb = self.keybundle_cls(source=url, httpc=self.httpc, httpc_params=self.httpc_params, + storage_conf=self.storage_conf, **kwargs) + + kb.update() + self._bundles.append(kb) + + return kb + + def add_symmetric(self, key, usage=None): + """ + Add a symmetric key. This is done by wrapping it in a key bundle + cloak since KeyJar does not handle keys directly but only through + key bundles. + + :param key: The key + :param usage: What the key can be used for signing/signature + verification (sig) and/or encryption/decryption (enc) + """ + + if usage is None: + self._bundles.append(self.keybundle_cls([{"kty": "oct", "key": key}])) + else: + for use in usage: + self._bundles.append(self.keybundle_cls([{"kty": "oct", "key": key, "use": use}])) + + def add_kb(self, kb): + """ + Add a key bundle. + + :param kb: A :py:class:`oidcmsg.key_bundle.KeyBundle` instance + """ + self._bundles.append(kb) + + def add(self, item, **kwargs): + if isinstance(item, KeyBundle): + self.add_kb(item) + elif item.startswith('http://') or item.startswith('file://') or item.startswith( + 'https://'): + self.add_url(item, **kwargs) + else: + self.add_symmetric(item, **kwargs) + + def all_keys(self): + """ + Get all the keys that belong to an entity. + + :return: A possibly empty list of keys + """ + res = [] + for kb in self._bundles: + res.extend(kb.keys()) + return res + + def __contains__(self, item): + for kb in self._bundles: + if item in kb: + return True + return False + + def items(self): + _res = {} + for kb in self._bundles: + if kb.source in _res: + _res[kb.source].append(kb) + else: + _res[kb.source] = [kb] + return _res + + def __str__(self): + _res = {} + for kb in self._bundles: + key_list = [] + for key in kb.keys(): + if key.inactive_since: + key_list.append( + '*{}:{}:{}'.format(key.kty, key.use, key.kid)) + else: + key_list.append( + '{}:{}:{}'.format(key.kty, key.use, key.kid)) + if kb.source in _res: + _res[kb.source] += ', ' + ', '.join(key_list) + else: + _res[kb.source] = ', '.join(key_list) + return json.dumps(_res) + + def load_keys(self, jwks_uri='', jwks=None): + """ + Fetch keys from another server + + :param jwks_uri: A URL pointing to a site that will return a JWKS + :param jwks: A dictionary representation of a JWKS + :return: Dictionary with usage as key and keys as values + """ + + if jwks_uri: + self.add_url(jwks_uri) + elif jwks: + # jwks should only be considered if no jwks_uri is present + _keys = jwks['keys'] + self._bundles.append(self.keybundle_cls(_keys)) + + def find(self, source): + """ + Find a key bundle based on the source of the keys + + :param source: A source url + :return: A list of :py:class:`oidcmsg.key_bundle.KeyBundle` instances, possibly empty + """ + return [kb for kb in self._bundles if kb.source == source] + + def export_jwks(self, private=False, usage=None): + """ + Produces a dictionary that later can be easily mapped into a + JSON string representing a JWKS. + + :param private: Whether it should be the private keys or the public + :param usage: If only keys for a special usage should be included + :return: A dictionary with one key: 'keys' + """ + keys = [] + for kb in self._bundles: + keys.extend([k.serialize(private) for k in kb.keys() if + k.inactive_since == 0 and ( + usage is None or (hasattr(k, 'use') and k.use == usage))]) + return {"keys": keys} + + def export_jwks_as_json(self, private=False, usage=None): + """ + Export a JWKS as a JSON document. + + :param private: Whether it should be the private keys or the public + :return: A JSON representation of a JWKS + """ + return json.dumps(self.export_jwks(private, usage=usage)) + + def import_jwks(self, jwks): + """ + Imports all the keys that are represented in a JWKS + + :param jwks: Dictionary representation of a JWKS + """ + try: + _keys = jwks["keys"] + except KeyError: + raise ValueError('Not a proper JWKS') + else: + self._bundles.append( + self.keybundle_cls(_keys, httpc=self.httpc, httpc_params=self.httpc_params)) + + def import_jwks_as_json(self, jwks, issuer): + """ + Imports all the keys that are represented in a JWKS expressed as a + JSON object + + :param jwks: JSON representation of a JWKS + :param issuer: Who 'owns' the JWKS + """ + return self.import_jwks(json.loads(jwks)) + + def import_jwks_from_file(self, filename, issuer): + with open(filename) as jwks_file: + self.import_jwks_as_json(jwks_file.read(), issuer) + + def remove_outdated(self, when=0): + """ + Goes through the complete list of issuers and for each of them removes + outdated keys. + Outdated keys are keys that has been marked as inactive at a time that + is longer ago then some set number of seconds (when). If when=0 the + the base time is set to now. + The number of seconds are carried in the remove_after parameter in the + key jar. + + :param when: To facilitate testing + """ + kbl = [] + changed = False + for kb in self._bundles: + if kb.remove_outdated(self.remove_after, when=when): + changed = True + kbl.append(kb) + if changed: + self._bundles.set(kbl) + + def get(self, key_use, key_type="", kid=None, alg='', **kwargs): + """ + Get all keys that matches a set of search criteria + + :param key_use: A key useful for this usage (enc, dec, sig, ver) + :param key_type: Type of key (rsa, ec, oct, ..) + :param kid: A Key Identifier + :return: A possibly empty list of keys + """ + + if key_use in ["dec", "enc"]: + use = "enc" + else: + use = "sig" + + if not key_type: + if alg: + if use == 'sig': + key_type = jws_alg2keytype(alg) + else: + key_type = jwe_alg2keytype(alg) + + lst = [] + for bundle in self._bundles: + if key_type: + if key_use in ['ver', 'dec']: + _bkeys = bundle.get(key_type, only_active=False) + else: + _bkeys = bundle.get(key_type) + else: + _bkeys = bundle.keys() + for key in _bkeys: + if key.inactive_since and key_use != "sig": + # Skip inactive keys unless for signature verification + continue + if not key.use or use == key.use: + if kid: + if key.kid == kid: + lst.append(key) + break + else: + continue + else: + lst.append(key) + + # If key algorithm is defined only return keys that can be used. + if alg: + lst = [key for key in lst if not key.alg or key.alg == alg] + + # if elliptic curve, have to check if I have a key of the right curve + if key_type == "EC" and "alg" in kwargs: + name = "P-{}".format(kwargs["alg"][2:]) # the type + _lst = [] + for key in lst: + if name != key.crv: + continue + _lst.append(key) + lst = _lst + + return lst + + def copy(self): + """ + Make deep copy of this key jar. + + :return: A :py:class:`oidcmsg.key_jar.KeyJar` instance + """ + ki = KeyIssuer() + ki._bundles = [kb.copy() for kb in self._bundles] + ki.httpc_params = self.httpc_params + ki.httpc = self.httpc + ki.storage_conf = self.storage_conf + ki.keybundle_cls = self.keybundle_cls + return ki + + def __len__(self): + nr = 0 + for kb in self._bundles: + nr += len(kb) + return nr + + def dump(self, exclude=None): + """ + Returns the key issuer content as a dictionary. + + :return: A dictionary + """ + + _bundles = [] + for kb in self._bundles: + _bundles.append(kb.dump()) + + info = { + 'name': self.name, + 'bundles': _bundles, + # 'storage_conf': self.storage_conf, + 'keybundle_cls': qualified_name(self.keybundle_cls), + 'spec2key': self.spec2key, + 'ca_certs': self.ca_certs, + 'remove_after': self.remove_after, + 'httpc_params': self.httpc_params + } + return info + + def load(self, info): + """ + + :param items: A list with the information + :return: + """ + self.name = info['name'] + # self.storage_conf = info['storage_conf'] + self.keybundle_cls = importer(info['keybundle_cls']) + self.spec2key = info['spec2key'] + self.ca_certs = info['ca_certs'] + self.remove_after = info['remove_after'] + self.httpc_params = info['httpc_params'] + self._bundles = [KeyBundle(storage_conf=self.storage_conf).load(val) for val in + info['bundles']] + return self + + def update(self): + for kb in self._bundles: + kb.update() + + def mark_as_inactive(self, kid): + kbl = [] + changed = False + for kb in self._bundles: + if kb.mark_as_inactive(kid): + changed = True + kbl.append(kb) + if changed: + self._bundles.set(kbl) + + def mark_all_keys_as_inactive(self): + kbl = [] + for kb in self._bundles: + kb.mark_all_as_inactive() + kbl.append(kb) + + self._bundles.set(kbl) + + def key_summary(self): + """ + Return a text representation of all the keys. + + :return: A text representation of the keys + """ + key_list = [] + for kb in self._bundles: + for key in kb.keys(): + if key.inactive_since: + key_list.append( + '*{}:{}:{}'.format(key.kty, key.use, key.kid)) + else: + key_list.append( + '{}:{}:{}'.format(key.kty, key.use, key.kid)) + return ', '.join(key_list) + + def __iter__(self): + for bundle in self._bundles: + yield bundle + + +# ============================================================================= + + +def build_keyissuer(key_conf, kid_template="", key_issuer=None, storage_conf=None, + issuer_id=''): + """ + Builds a :py:class:`oidcmsg.key_issuer.KeyIssuer` instance or adds keys to + an existing KeyIssuer instance based on a key specification. + + An example of such a specification:: + + keys = [ + {"type": "RSA", "key": "cp_keys/key.pem", "use": ["enc", "sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"], "kid": "ec.1"}, + {"type": "EC", "crv": "P-256", "use": ["enc"], "kid": "ec.2"} + {"type": "oct", "bytes": 32, "use":["sig"]} + ] + + Keys in this specification are: + + type + The type of key. Presently only 'rsa', 'oct' and 'ec' supported. + + key + A name of a file where a key can be found. Works with PEM encoded + RSA and EC private keys. + + use + What the key should be used for + + crv + The elliptic curve that should be used. Only applies to elliptic curve + keys :-) + + kid + Key ID, can only be used with one usage type is specified. If there + are more the one usage type specified 'kid' will just be ignored. + + :param key_conf: The key configuration + :param kid_template: A template by which to build the key IDs. If no + kid_template is given then the built-in function add_kid() will be used. + :param key_issuer: If an keyIssuer instance the new keys are added to this key issuer. + :param storage_conf: + :return: A KeyIssuer instance + """ + + bundle = build_key_bundle(key_conf, kid_template, storage_conf=storage_conf) + if bundle is None: + return None + + if key_issuer is None: + key_issuer = KeyIssuer(name=issuer_id, storage_conf=storage_conf) + + key_issuer.add(bundle) + + return key_issuer diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 69962f2..9f96351 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -1,21 +1,23 @@ import json import logging import os +from typing import List +from abstorage.utils import importer +from abstorage.utils import init_storage +from abstorage.utils import qualified_name from requests import request from .jwe.jwe import alg2keytype as jwe_alg2keytype from .jws.utils import alg2keytype as jws_alg2keytype from .key_bundle import KeyBundle -from .key_bundle import build_key_bundle from .key_bundle import key_diff from .key_bundle import update_key_bundle __author__ = 'Roland Hedberg' -KEYLOADERR = "Failed to load %s key from '%s' (%s)" -REMOTE_FAILED = "Remote key update from '{}' failed, HTTP status {}" -MALFORMED = "Remote key update from {} failed, malformed JWKS." +from .key_issuer import KeyIssuer +from .key_issuer import build_keyissuer logger = logging.getLogger(__name__) @@ -36,7 +38,7 @@ class KeyJar(object): """ A keyjar contains a number of KeyBundles sorted by owner/issuer """ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, - remove_after=3600, httpc=None, httpc_params=None): + remove_after=3600, httpc=None, httpc_params=None, storage_conf=None): """ KeyJar init function @@ -48,8 +50,11 @@ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, :param httpc_params: HTTP request parameters :return: Keyjar instance """ + + self._issuers = init_storage(storage_conf, self.__class__.__name__) + + self.storage_conf = storage_conf self.spec2key = {} - self.issuer_keys = {} self.ca_certs = ca_certs self.keybundle_cls = keybundle_cls self.remove_after = remove_after @@ -57,14 +62,53 @@ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, self.httpc_params = httpc_params or {} # Now part of httpc_params # self.verify_ssl = verify_ssl - if not self.httpc_params: # backward compatibility + if not self.httpc_params: # backward compatibility self.httpc_params["verify"] = verify_ssl + def _issuer_ids(self) -> List[str]: + """ + Returns a list of issuer identifiers + + :return: + """ + return [i.name for i in self._issuers] + + def _get_issuer(self, issuer_id): + """ + Return the KeyIssuer instance that has name == issuer_id + + :param issuer_id: The issuer identifiers + :return: A KeyIssuer instance or None + """ + _i = [i for i in self._issuers if i.name == issuer_id] + if _i: + return _i[0] # should only be one + else: + return None + def __repr__(self): - issuers = list(self.issuer_keys.keys()) + issuers = self._issuer_ids() return ''.format(issuers) - def add_url(self, issuer, url, **kwargs): + def return_issuer(self, issuer_id): + """ + Return a KeyIssuer instance with name == issuer_id. + If none such was already initiated, create one. + + :param issuer_id: The issuer ID + :return: A KeyIssuer instance + """ + _iss = self._get_issuer(issuer_id) + if not _iss: + _iss = KeyIssuer(ca_certs=self.ca_certs, name=issuer_id, + keybundle_cls=self.keybundle_cls, + remove_after=self.remove_after, + httpc=self.httpc, httpc_params=self.httpc_params, + storage_conf=self.storage_conf) + self._issuers.append(_iss) + return _iss + + def add_url(self, issuer_id, url, **kwargs): """ Add a set of keys by url. This method will create a :py:class:`oidcmsg.key_bundle.KeyBundle` instance with the @@ -77,24 +121,11 @@ def add_url(self, issuer, url, **kwargs): :return: A :py:class:`oidcmsg.oauth2.keybundle.KeyBundle` instance """ - if not url: - raise KeyError("No url given") - - if "/localhost:" in url or "/localhost/" in url: - _params = self.httpc_params.copy() - _params['verify'] = False - kb = self.keybundle_cls(source=url, httpc=self.httpc, - httpc_params=_params, **kwargs) - else: - kb = self.keybundle_cls(source=url, httpc=self.httpc, - httpc_params=self.httpc_params, **kwargs) - - kb.update() - self.add_kb(issuer, kb) - + issuer = self.return_issuer(issuer_id) + kb = issuer.add_url(url, **kwargs) return kb - def add_symmetric(self, issuer, key, usage=None): + def add_symmetric(self, issuer_id, key, usage=None): """ Add a symmetric key. This is done by wrapping it in a key bundle cloak since KeyJar does not handle keys directly but only through @@ -105,61 +136,52 @@ def add_symmetric(self, issuer, key, usage=None): :param usage: What the key can be used for signing/signature verification (sig) and/or encryption/decryption (enc) """ - if issuer not in self.issuer_keys: - self.issuer_keys[issuer] = [] + issuer = self.return_issuer(issuer_id) + issuer.add_symmetric(key, usage=usage) - if usage is None: - self.issuer_keys[issuer].append( - self.keybundle_cls([{"kty": "oct", "key": key}])) - else: - for use in usage: - self.issuer_keys[issuer].append( - self.keybundle_cls([{"kty": "oct", "key": key, "use": use}])) - - def add_kb(self, issuer, kb): + def add_kb(self, issuer_id, kb): """ Add a key bundle and bind it to an identifier - :param issuer: Owner of the keys in the key bundle + :param issuer_id: Owner of the keys in the key bundle :param kb: A :py:class:`oidcmsg.key_bundle.KeyBundle` instance """ - try: - self.issuer_keys[issuer].append(kb) - except KeyError: - self.issuer_keys[issuer] = [kb] - - def __setitem__(self, issuer, val): - """ - Bind one or a list of key bundles to a special identifier. - Will overwrite whatever was there before !! - - :param issuer: The owner of the keys in the key bundle/-s - :param val: A single or a list of KeyBundle instance - """ - if not isinstance(val, list): - val = [val] - - for kb in val: - if not isinstance(kb, KeyBundle): - raise ValueError('{} not an KeyBundle instance'.format(kb)) - - self.issuer_keys[issuer] = val + issuer = self.return_issuer(issuer_id) + issuer.add_kb(kb) + + # def __setitem__(self, issuer_id, val): + # """ + # Bind one or a list of key bundles to a special identifier. + # Will overwrite whatever was there before !! + # + # :param issuer_id: The owner of the keys in the key bundle/-s + # :param val: A single or a list of KeyBundle instance + # """ + # if not isinstance(val, list): + # val = [val] + # + # for kb in val: + # if not isinstance(kb, KeyBundle): + # raise ValueError('{} not an KeyBundle instance'.format(kb)) + # + # issuer = self.return_issuer(issuer_id) + # issuer.set(val) def items(self): """ - Get all owner ID's and their key bundles + Get all owner ID's and their keys :return: list of 2-tuples (Owner ID., list of KeyBundles) """ - return self.issuer_keys.items() + return [(i.name, i.get_bundles()) for i in self._issuers] - def get(self, key_use, key_type="", owner="", kid=None, **kwargs): + def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs): """ Get all keys that matches a set of search criteria :param key_use: A key useful for this usage (enc, dec, sig, ver) :param key_type: Type of key (rsa, ec, oct, ..) - :param owner: Who is the owner of the keys, "" == me (default) + :param issuer_id: Who is the owner of the keys, "" == me (default) :param kid: A Key Identifier :return: A possibly empty list of keys """ @@ -169,32 +191,22 @@ def get(self, key_use, key_type="", owner="", kid=None, **kwargs): else: use = "sig" - _kj = None - if owner != "": - try: - _kj = self.issuer_keys[owner] - except KeyError: - if owner.endswith("/"): - try: - _kj = self.issuer_keys[owner[:-1]] - except KeyError: - pass + _issuer = None + if issuer_id != "": + _issuer = self._get_issuer(issuer_id) + if _issuer is None: + if issuer_id.endswith("/"): + _issuer = self._get_issuer(issuer_id[:-1]) else: - try: - _kj = self.issuer_keys[owner + "/"] - except KeyError: - pass + _issuer = self._get_issuer(issuer_id + "/") else: - try: - _kj = self.issuer_keys[owner] - except KeyError: - pass + _issuer = self._get_issuer(issuer_id) - if _kj is None: + if _issuer is None: return [] lst = [] - for bundle in _kj: + for bundle in _issuer: if key_type: if key_use in ['ver', 'dec']: _bkeys = bundle.get(key_type, only_active=False) @@ -226,44 +238,46 @@ def get(self, key_use, key_type="", owner="", kid=None, **kwargs): _lst.append(key) lst = _lst - if use == 'enc' and key_type == 'oct' and owner != '': + if use == 'enc' and key_type == 'oct' and issuer_id != '': # Add my symmetric keys - for kb in self.issuer_keys['']: - for key in kb.get(key_type): - if key.inactive_since: - continue - if not key.use or key.use == use: - lst.append(key) + _issuer = self._get_issuer('') + if _issuer: + for kb in _issuer: + for key in kb.get(key_type): + if key.inactive_since: + continue + if not key.use or key.use == use: + lst.append(key) return lst - def get_signing_key(self, key_type="", owner="", kid=None, **kwargs): + def get_signing_key(self, key_type="", issuer_id="", kid=None, **kwargs): """ Shortcut to use for signing keys only. :param key_type: Type of key (rsa, ec, oct, ..) - :param owner: Who is the owner of the keys, "" == me (default) + :param issuer_id: Who is the owner of the keys, "" == me (default) :param kid: A Key Identifier :param kwargs: Extra key word arguments :return: A possibly empty list of keys """ - return self.get("sig", key_type, owner, kid, **kwargs) + return self.get("sig", key_type, issuer_id, kid, **kwargs) - def get_verify_key(self, key_type="", owner="", kid=None, **kwargs): - return self.get("ver", key_type, owner, kid, **kwargs) + def get_verify_key(self, key_type="", issuer_id="", kid=None, **kwargs): + return self.get("ver", key_type, issuer_id, kid, **kwargs) - def get_encrypt_key(self, key_type="", owner="", kid=None, **kwargs): - return self.get("enc", key_type, owner, kid, **kwargs) + def get_encrypt_key(self, key_type="", issuer_id="", kid=None, **kwargs): + return self.get("enc", key_type, issuer_id, kid, **kwargs) - def get_decrypt_key(self, key_type="", owner="", kid=None, **kwargs): - return self.get("dec", key_type, owner, kid, **kwargs) + def get_decrypt_key(self, key_type="", issuer_id="", kid=None, **kwargs): + return self.get("dec", key_type, issuer_id, kid, **kwargs) - def keys_by_alg_and_usage(self, issuer, alg, usage): + def keys_by_alg_and_usage(self, issuer_id, alg, usage): """ Find all keys that can be used for a specific crypto algorithm and usage by key owner. - :param issuer: Key owner + :param issuer_id: Key owner :param alg: a crypto algorithm :param usage: What the key should be used for :return: A possibly empty list of keys @@ -273,40 +287,35 @@ def keys_by_alg_and_usage(self, issuer, alg, usage): else: ktype = jwe_alg2keytype(alg) - return self.get(usage, ktype, issuer) + return self.get(usage, ktype, issuer_id) - def get_issuer_keys(self, issuer): + def get_issuer_keys(self, issuer_id): """ Get all the keys that belong to an entity. :param issuer: The entity ID :return: A possibly empty list of keys """ - res = [] - for kbl in self.issuer_keys[issuer]: - res.extend(kbl.keys()) - return res + _issuer = self._get_issuer(issuer_id) + if _issuer: + return _issuer.all_keys() + else: + return [] - def __contains__(self, item): - if item in self.issuer_keys: + def __contains__(self, issuer_id): + if self._get_issuer(issuer_id): return True else: return False - def __getitem__(self, owner=''): + def __getitem__(self, issuer_id=''): """ - Get all the key bundles that belong to an entity. + Get all the KeyIssuer with the name == issuer_id - :param owner: The entity ID - :return: A possibly empty list of key bundles + :param issuer_id: The entity ID + :return: A KeyIssuer instance """ - try: - return self.issuer_keys[owner] - except KeyError: - logger.debug( - "Owner '{}' not found, available key owners: {}".format( - owner, list(self.issuer_keys.keys()))) - raise + return self._get_issuer(issuer_id) def owners(self): """ @@ -314,7 +323,7 @@ def owners(self): :return: A list of entity IDs """ - return list(self.issuer_keys.keys()) + return self._issuer_ids() def match_owner(self, url): """ @@ -325,22 +334,19 @@ def match_owner(self, url): :param url: A URL :return: An issue entity ID that exists in the Key jar """ - for owner in self.issuer_keys.keys(): - if owner.startswith(url): - return owner + _iss = [i for i in self._issuers if i.name.startswith(url)] + if _iss: + return _iss[0].name raise KeyError("No keys for '{}' in this keyjar".format(url)) def __str__(self): _res = {} - for _id, kbs in self.issuer_keys.items(): - _l = [] - for kb in kbs: - _l.extend(json.loads(kb.jwks())["keys"]) - _res[_id] = {"keys": _l} + for _issuer in self._issuers: + _res[_issuer.name] = _issuer.key_summary() return json.dumps(_res) - def load_keys(self, issuer, jwks_uri='', jwks=None, replace=False): + def load_keys(self, issuer_id, jwks_uri='', jwks=None, replace=False): """ Fetch keys from another server @@ -352,36 +358,43 @@ def load_keys(self, issuer, jwks_uri='', jwks=None, replace=False): :return: Dictionary with usage as key and keys as values """ - logger.debug("Initiating key bundle for issuer: %s" % issuer) + logger.debug("Initiating key bundle for issuer: %s" % issuer_id) - if replace or issuer not in self.issuer_keys: - self.issuer_keys[issuer] = [] + _issuer = self.return_issuer(issuer_id) + if replace: + _issuer.set([]) if jwks_uri: - self.add_url(issuer, jwks_uri) + _issuer.add_url(jwks_uri) elif jwks: # jwks should only be considered if no jwks_uri is present _keys = jwks['keys'] - self.issuer_keys[issuer].append(self.keybundle_cls(_keys)) + _issuer.add_kb(self.keybundle_cls(_keys)) - def find(self, source, issuer): + def find(self, source, issuer_id=None): """ Find a key bundle based on the source of the keys :param source: A source url :param issuer: The issuer of keys - :return: A :py:class:`oidcmsg.key_bundle.KeyBundle` instance or None - """ - try: - for kb in self.issuer_keys[issuer]: - if kb.source == source: - return kb - except KeyError: - return None + :return: List of :py:class:`oidcmsg.key_bundle.KeyBundle` instances or None + """ + if issuer_id is None: + res = {} + for _issuer in self._issuers: + kbs = _issuer.find(source) + if kbs: + res[_issuer.name] = kbs + else: + _issuer = self._get_issuer(issuer_id) + if _issuer is None: + return [] + else: + res = _issuer.find(source) - return None + return res - def export_jwks(self, private=False, issuer="", usage=None): + def export_jwks(self, private=False, issuer_id="", usage=None): """ Produces a dictionary that later can be easily mapped into a JSON string representing a JWKS. @@ -390,42 +403,45 @@ def export_jwks(self, private=False, issuer="", usage=None): :param issuer: The entity ID. :return: A dictionary with one key: 'keys' """ + _issuer = self._get_issuer(issuer_id=issuer_id) + if _issuer is None: + return {} + keys = [] - for kb in self.issuer_keys[issuer]: + for kb in _issuer: keys.extend([k.serialize(private) for k in kb.keys() if - k.inactive_since == 0 and (usage is None or (hasattr(k, 'use') and k.use == usage))]) + k.inactive_since == 0 and ( + usage is None or (hasattr(k, 'use') and k.use == usage))]) return {"keys": keys} - def export_jwks_as_json(self, private=False, issuer=""): + def export_jwks_as_json(self, private=False, issuer_id=""): """ Export a JWKS as a JSON document. :param private: Whether it should be the private keys or the public - :param issuer: The entity ID. + :param issuer_id: The entity ID. :return: A JSON representation of a JWKS """ - return json.dumps(self.export_jwks(private, issuer)) + return json.dumps(self.export_jwks(private, issuer_id)) - def import_jwks(self, jwks, issuer): + def import_jwks(self, jwks, issuer_id): """ Imports all the keys that are represented in a JWKS :param jwks: Dictionary representation of a JWKS - :param issuer: Who 'owns' the JWKS + :param issuer_id: Who 'owns' the JWKS """ try: _keys = jwks["keys"] except KeyError: raise ValueError('Not a proper JWKS') else: - try: - self.issuer_keys[issuer].append( - self.keybundle_cls(_keys, httpc=self.httpc, httpc_params=self.httpc_params)) - except KeyError: - self.issuer_keys[issuer] = [self.keybundle_cls( - _keys, httpc=self.httpc, httpc_params=self.httpc_params)] + _issuer = self.return_issuer(issuer_id=issuer_id) + _issuer.add(self.keybundle_cls(_keys, httpc=self.httpc, + httpc_params=self.httpc_params, + storage_conf=self.storage_conf)) - def import_jwks_as_json(self, jwks, issuer): + def import_jwks_as_json(self, jwks, issuer_id): """ Imports all the keys that are represented in a JWKS expressed as a JSON object @@ -433,11 +449,11 @@ def import_jwks_as_json(self, jwks, issuer): :param jwks: JSON representation of a JWKS :param issuer: Who 'owns' the JWKS """ - return self.import_jwks(json.loads(jwks), issuer) + return self.import_jwks(json.loads(jwks), issuer_id) - def import_jwks_from_file(self, filename, issuer): + def import_jwks_from_file(self, filename, issuer_id): with open(filename) as jwks_file: - self.import_jwks_as_json(jwks_file.read(), issuer) + self.import_jwks_as_json(jwks_file.read(), issuer_id) def __eq__(self, other): if not isinstance(other, KeyJar): @@ -459,6 +475,11 @@ def __eq__(self, other): return True + def __delitem__(self, key): + _issuer = self._get_issuer(key) + if _issuer: + self._issuers.remove(_issuer) + def remove_outdated(self, when=0): """ Goes through the complete list of issuers and for each of them removes @@ -471,56 +492,53 @@ def remove_outdated(self, when=0): :param when: To facilitate testing """ - for iss in list(self.owners()): - _kbl = [] - for kb in self.issuer_keys[iss]: - kb.remove_outdated(self.remove_after, when=when) - if len(kb): - _kbl.append(kb) - if _kbl: - self.issuer_keys[iss] = _kbl - else: - del self.issuer_keys[iss] + _ids = self._issuer_ids() + for _id in _ids: + _issuer = self[_id] + _before = len(_issuer) + _issuer.remove_outdated(when) + if len(_issuer) != _before: + del self[_id] + self.append(_issuer) - def _add_key(self, keys, issuer, use, key_type='', kid='', + def _add_key(self, keys, issuer_id, use, key_type='', kid='', no_kid_issuer=None, allow_missing_kid=False): - if issuer not in self: - logger.error('Issuer "{}" not in keyjar'.format(issuer)) + _issuer = self._get_issuer(issuer_id) + if _issuer is None: + logger.error('Issuer "{}" not in keyjar'.format(issuer_id)) return keys - logger.debug('Key set summary for {}: {}'.format( - issuer, key_summary(self, issuer))) + logger.debug('Key summary for {}: {}'.format(issuer_id, _issuer.key_summary())) if kid: - for _key in self.get(key_use=use, owner=issuer, kid=kid, key_type=key_type): + for _key in _issuer.get(use, kid=kid, key_type=key_type): if _key and _key not in keys: keys.append(_key) return keys else: try: - kl = self.get(key_use=use, owner=issuer, key_type=key_type) + _add_keys = _issuer.get(use, key_type=key_type) except KeyError: pass else: - if len(kl) == 0: + if len(_add_keys) == 0: return keys - elif len(kl) == 1: - if kl[0] not in keys: - keys.append(kl[0]) + elif len(_add_keys) == 1: + if _add_keys[0] not in keys: + keys.append(_add_keys[0]) elif allow_missing_kid: - keys.extend(kl) + keys.extend(_add_keys) elif no_kid_issuer: try: - allowed_kids = no_kid_issuer[issuer] + allowed_kids = no_kid_issuer[issuer_id] except KeyError: return keys else: if allowed_kids: - keys.extend( - [k for k in kl if k.kid in allowed_kids]) + keys.extend([k for k in _add_keys if k.kid in allowed_kids]) else: - keys.extend(kl) + keys.extend(_add_keys) return keys def get_jwt_decrypt_keys(self, jwt, **kwargs): @@ -544,7 +562,7 @@ def get_jwt_decrypt_keys(self, jwt, **kwargs): logger.info('Missing kid') _kid = '' - keys = self.get(key_use='enc', owner='', key_type=_key_type) + keys = self.get(key_use='enc', issuer_id='', key_type=_key_type) try: _aud = kwargs['aud'] @@ -607,10 +625,10 @@ def get_jwt_verify_keys(self, jwt, **kwargs): _kid, nki, allow_missing_kid) if _key_type == 'oct': - keys.extend(self.get(key_use='sig', owner='', + keys.extend(self.get(key_use='sig', issuer_id='', key_type=_key_type)) else: # No issuer, just use all keys I have - keys = self.get(key_use='sig', owner='', key_type=_key_type) + keys = self.get(key_use='sig', issuer_id='', key_type=_key_type) # Only want the appropriate keys. keys = [k for k in keys if k.appropriate_for('verify')] @@ -623,33 +641,39 @@ def copy(self): :return: A :py:class:`oidcmsg.key_jar.KeyJar` instance """ kj = KeyJar() - for issuer in self.owners(): - kj[issuer] = [kb.copy() for kb in self[issuer]] + for _issuer in self._issuers: + _iss = kj.return_issuer(_issuer.name) + _iss.set([kb.copy() for kb in _issuer]) kj.httpc_params = self.httpc_params kj.httpc = self.httpc return kj def __len__(self): - keys = 0 - for iss in list(self.owners()): - for kb in self.issuer_keys[iss]: - if len(kb): - keys += len(kb) - return keys + return len(self._issuers) - def dump(self): + def dump(self, exclude=None): """ Returns the key jar content as dictionary :return: A dictionary """ - info = {} - for iss in list(self.owners()): - info[iss] = [] - for kb in self.issuer_keys[iss]: - info[iss].append(kb.dump()) + info = { + # 'storage_conf': self.storage_conf, + 'spec2key': self.spec2key, + 'ca_certs': self.ca_certs, + 'keybundle_cls': qualified_name(self.keybundle_cls), + 'remove_after': self.remove_after, + 'httpc_params': self.httpc_params} + + _issuers = [] + for _issuer in self._issuers: + if exclude and _issuer.name in exclude: + continue + _issuers.append(_issuer.dump()) + info['issuers'] = _issuers + return info def load(self, info): @@ -658,15 +682,45 @@ def load(self, info): :param info: A dictionary with the information :return: """ - for iss, kbs in info.items(): - self.issuer_keys[iss] = [KeyBundle().load(val) for val in kbs] + # self.storage_conf = info['storage_conf'] + self.spec2key = info['spec2key'] + self.ca_certs = info['ca_certs'] + self.keybundle_cls = importer(info['keybundle_cls']) + self.remove_after = info['remove_after'] + self.httpc_params = info['httpc_params'] + + for _issuer_desc in info['issuers']: + self._issuers.append(KeyIssuer(storage_conf=self.storage_conf).load(_issuer_desc)) return self + def append(self, issuer): + self._issuers.append(issuer) + + def key_summary(self, issuer_id): + _issuer = self._get_issuer(issuer_id) + if _issuer: + return _issuer.key_summary() + + raise KeyError('Unknown Issuer ID: "{}"'.format(issuer_id)) + + def update(self): + """ + Go through the whole key jar, key bundle by key bundle and update them one + by one. + + :param keyjar: The key jar to update + """ + for _id in self._issuer_ids(): + _issuer = self._get_issuer(_id) + self._issuers.remove(_issuer) + _issuer.update() + self._issuers.append(_issuer) + # ============================================================================= -def build_keyjar(key_conf, kid_template="", keyjar=None, owner=''): +def build_keyjar(key_conf, kid_template="", keyjar=None, issuer_id='', storage_conf=None): """ Builds a :py:class:`oidcmsg.key_jar.KeyJar` instance or adds keys to an existing KeyJar based on a key specification. @@ -704,59 +758,26 @@ def build_keyjar(key_conf, kid_template="", keyjar=None, owner=''): :param kid_template: A template by which to build the key IDs. If no kid_template is given then the built-in function add_kid() will be used. :param keyjar: If an KeyJar instance the new keys are added to this key jar. - :param owner: The default owner of the keys in the key jar. + :param issuer_id: The default owner of the keys in the key jar. + :param storage_conf: Storage configuration :return: A KeyJar instance """ + _issuer = build_keyissuer(key_conf, kid_template, storage_conf=storage_conf, + issuer_id=issuer_id) + if _issuer is None: + return None + if keyjar is None: keyjar = KeyJar() - bundle = build_key_bundle(key_conf, kid_template) - - keyjar.add_kb(owner, bundle) + keyjar.append(_issuer) return keyjar -def update_keyjar(keyjar): - """ - Go through the whole key jar, key bundle by key bundle and update them one - by one. - - :param keyjar: The key jar to update - """ - for iss, kbl in keyjar.items(): - for kb in kbl: - kb.update() - - -def key_summary(keyjar, issuer): - """ - Return a text representation of the keyjar. - - :param keyjar: A :py:class:`oidcmsg.key_jar.KeyJar` instance - :param issuer: Which key owner that we are looking at - :return: A text representation of the keys - """ - try: - kbl = keyjar[issuer] - except KeyError: - return '' - else: - key_list = [] - for kb in kbl: - for key in kb.keys(): - if key.inactive_since: - key_list.append( - '*{}:{}:{}'.format(key.kty, key.use, key.kid)) - else: - key_list.append( - '{}:{}:{}'.format(key.kty, key.use, key.kid)) - return ', '.join(key_list) - - -def init_key_jar(public_path='', private_path='', key_defs='', owner='', - read_only=True): +def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', read_only=True, + storage_conf=None): """ A number of cases here: @@ -794,7 +815,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', owner='', private keys. :param key_defs: A definition of what keys should be created if they are not already available - :param owner: The owner of the keys + :param issuer_id: The owner of the keys :param read_only: This function should not attempt to write anything to a file system. :return: An instantiated :py:class;`oidcmsg.key_jar.KeyJar` instance @@ -803,25 +824,25 @@ def init_key_jar(public_path='', private_path='', key_defs='', owner='', if private_path: if os.path.isfile(private_path): _jwks = open(private_path, 'r').read() - _kj = KeyJar() - _kj.import_jwks(json.loads(_jwks), owner) + _issuer = KeyIssuer(name=issuer_id, storage_conf=storage_conf) + _issuer.import_jwks(json.loads(_jwks)) if key_defs: - _kb = _kj.issuer_keys[owner][0] + _kb = _issuer[0] _diff = key_diff(_kb, key_defs) if _diff: update_key_bundle(_kb, _diff) if read_only: logger.error('Not allowed to write to disc!') else: - _kj.issuer_keys[owner] = [_kb] - jwks = _kj.export_jwks(private=True, issuer=owner) + _issuer.set([_kb]) + jwks = _issuer.export_jwks(private=True) fp = open(private_path, 'w') fp.write(json.dumps(jwks)) fp.close() else: - _kj = build_keyjar(key_defs, owner=owner) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) if not read_only: - jwks = _kj.export_jwks(private=True, issuer=owner) + jwks = _issuer.export_jwks(private=True) head, tail = os.path.split(private_path) if head and not os.path.isdir(head): os.makedirs(head) @@ -830,7 +851,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', owner='', fp.close() if public_path and not read_only: - jwks = _kj.export_jwks(issuer=owner) # public part + jwks = _issuer.export_jwks() # public part head, tail = os.path.split(public_path) if head and not os.path.isdir(head): os.makedirs(head) @@ -840,25 +861,25 @@ def init_key_jar(public_path='', private_path='', key_defs='', owner='', elif public_path: if os.path.isfile(public_path): _jwks = open(public_path, 'r').read() - _kj = KeyJar() - _kj.import_jwks(json.loads(_jwks), owner) + _issuer = KeyIssuer(name=issuer_id, storage_conf=storage_conf) + _issuer.import_jwks(json.loads(_jwks)) if key_defs: - _kb = _kj.issuer_keys[owner][0] + _kb = _issuer[0] _diff = key_diff(_kb, key_defs) if _diff: if read_only: logger.error('Not allowed to write to disc!') else: update_key_bundle(_kb, _diff) - _kj.issuer_keys[owner] = [_kb] - jwks = _kj.export_jwks(issuer=owner) + _issuer.set([_kb]) + jwks = _issuer.export_jwks() fp = open(public_path, 'w') fp.write(json.dumps(jwks)) fp.close() else: - _kj = build_keyjar(key_defs, owner=owner) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) if not read_only: - _jwks = _kj.export_jwks(issuer=owner) + _jwks = _issuer.export_jwks(issuer=issuer_id) head, tail = os.path.split(public_path) if head and not os.path.isdir(head): os.makedirs(head) @@ -866,6 +887,20 @@ def init_key_jar(public_path='', private_path='', key_defs='', owner='', fp.write(json.dumps(_jwks)) fp.close() else: - _kj = build_keyjar(key_defs, owner=owner) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) + + keyjar = KeyJar(storage_conf=storage_conf) + keyjar.append(_issuer) + return keyjar + - return _kj +def rotate_keys(key_conf, keyjar, kid_template="", issuer_id='', storage_conf=None): + new_keys = build_keyissuer(key_conf, kid_template, storage_conf=storage_conf, + issuer_id=issuer_id) + _issuer = keyjar[issuer_id] + _issuer.mark_all_keys_as_inactive() + for kb in new_keys: + _issuer.add_kb(kb) + del keyjar[_issuer.name] + keyjar.append(_issuer) + return keyjar diff --git a/src/cryptojwt/serialize/__init__.py b/src/cryptojwt/serialize/__init__.py new file mode 100644 index 0000000..7d7ec6b --- /dev/null +++ b/src/cryptojwt/serialize/__init__.py @@ -0,0 +1,47 @@ + +class SimpleList(): + def __init__(self, value=None): + if value is None: + self.db = [] + else: + self.set(value) + + def __len__(self): + return len(self.db) + + def __contains__(self, item): + return item in self.db + + def __del__(self): + del self.db + + def __iter__(self): + for i in self.db: + yield i + + def __str__(self): + return str(self.db) + + def append(self, item): + self.db.append(item) + + def extend(self, items): + self.db.extend(items) + + def remove(self, item): + self.db.remove(item) + + def get(self): + return self.db + + def set(self, value): + if isinstance(value, list): + self.db = value + else: + raise ValueError("Wrong value type") + + def copy(self): + return self.db[:] + + def close(self): + return diff --git a/tests/test_04_key_jar.py b/tests/test_04_key_jar.py index 23cd06a..0ffa1f5 100755 --- a/tests/test_04_key_jar.py +++ b/tests/test_04_key_jar.py @@ -15,11 +15,11 @@ from cryptojwt.key_jar import KeyJar from cryptojwt.key_jar import build_keyjar from cryptojwt.key_jar import init_key_jar -from cryptojwt.key_jar import key_summary -from cryptojwt.key_jar import update_keyjar __author__ = 'Roland Hedberg' +from cryptojwt.key_jar import rotate_keys + BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "test_keys")) RSAKEY = os.path.join(BASE_PATH, "cert.key") @@ -207,7 +207,7 @@ def test_build_keyjar(): assert "d" not in key # the JWKS shouldn't contain the private part # of the keys - assert len(keyjar[""]) == 1 # One key bundle + assert len(keyjar[""]) == 3 # 3 keys assert len(keyjar.get_issuer_keys('')) == 3 # A total of 3 keys assert len(keyjar.get('sig')) == 2 # 2 for signing assert len(keyjar.get('enc')) == 1 # 1 for encryption @@ -237,7 +237,7 @@ def test_build_keyjar_missing(tmpdir): key_jar = build_keyjar(keys) - assert len(key_jar[""]) == 1 + assert key_jar is None def test_build_RSA_keyjar_from_file(tmpdir): @@ -249,7 +249,7 @@ def test_build_RSA_keyjar_from_file(tmpdir): key_jar = build_keyjar(keys) - assert len(key_jar[""]) == 1 + assert len(key_jar[""]) == 2 def test_build_EC_keyjar_missing(tmpdir): @@ -261,7 +261,7 @@ def test_build_EC_keyjar_missing(tmpdir): key_jar = build_keyjar(keys) - assert len(key_jar[""]) == 1 + assert key_jar is None def test_build_EC_keyjar_from_file(tmpdir): @@ -273,7 +273,7 @@ def test_build_EC_keyjar_from_file(tmpdir): key_jar = build_keyjar(keys) - assert len(key_jar[""]) == 1 + assert len(key_jar[""]) == 2 class TestKeyJar(object): @@ -286,7 +286,7 @@ def test_keyjar_add(self): def test_setitem(self): kj = KeyJar() kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"]) - kj['https://issuer.example.com'] = kb + kj.add_kb('https://issuer.example.com', kb) assert list(kj.owners()) == ['https://issuer.example.com'] def test_add_symmetric(self): @@ -297,66 +297,66 @@ def test_add_symmetric(self): def test_items(self): ks = KeyJar() - ks[""] = KeyBundle( + ks.add_kb("", KeyBundle( [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, - {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}]) - ks["http://www.example.org"] = KeyBundle([ + {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}])) + ks.add_kb("http://www.example.org", KeyBundle([ {"kty": "oct", "key": "0123456789012345", "use": "sig"}, - {"kty": "oct", "key": "1234567890123456", "use": "enc"}]) - ks["http://www.example.org"].append( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + {"kty": "oct", "key": "1234567890123456", "use": "enc"}])) + ks.add_kb("http://www.example.org", + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) assert len(ks.items()) == 2 def test_issuer_extra_slash(self): ks = KeyJar() - ks[""] = KeyBundle( + ks.add_kb("", KeyBundle( [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, - {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}]) - ks["http://www.example.org"] = KeyBundle([ + {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}])) + ks.add_kb("http://www.example.org", KeyBundle([ {"kty": "oct", "key": "0123456789012345", "use": "sig"}, - {"kty": "oct", "key": "1234567890123456", "use": "enc"}]) - ks["http://www.example.org"].append( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + {"kty": "oct", "key": "1234567890123456", "use": "enc"}])) + ks.add_kb("http://www.example.org", + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) assert ks.get('sig', 'RSA', 'http://www.example.org/') def test_issuer_missing_slash(self): ks = KeyJar() - ks[""] = KeyBundle( + ks.add_kb("", KeyBundle( [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}]) - ks["http://www.example.org/"] = KeyBundle([ + {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) + ks.add_kb("http://www.example.org/", KeyBundle([ {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}]) - ks["http://www.example.org/"].append( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}])) + ks.add_kb("http://www.example.org/", + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) assert ks.get('sig', 'RSA', 'http://www.example.org') def test_get_enc(self): ks = KeyJar() - ks[""] = KeyBundle( + ks.add_kb("", KeyBundle( [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}]) - ks["http://www.example.org/"] = KeyBundle([ + {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) + ks.add_kb("http://www.example.org/", KeyBundle([ {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}]) - ks["http://www.example.org/"].append( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}])) + ks.add_kb("http://www.example.org/", + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) assert ks.get('enc', 'oct') def test_get_enc_not_mine(self): ks = KeyJar() - ks[""] = KeyBundle( + ks.add_kb("", KeyBundle( [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}]) - ks["http://www.example.org/"] = KeyBundle([ + {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) + ks.add_kb("http://www.example.org/", KeyBundle([ {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "ver"}]) - ks["http://www.example.org/"].append( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "ver"}])) + ks.add_kb("http://www.example.org/", + keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) assert ks.get('enc', 'oct', 'http://www.example.org/') @@ -365,7 +365,7 @@ def test_dump_issuer_keys(self): ["sig"]) assert len(kb) == 1 kj = KeyJar() - kj.issuer_keys[""] = [kb] + kj.add_kb("", kb) _jwks_dict = kj.export_jwks() _info = _jwks_dict['keys'][0] @@ -388,17 +388,17 @@ def test_dump_issuer_keys(self): def test_no_use(self): kb = KeyBundle(JWK0["keys"]) kj = KeyJar() - kj.issuer_keys["abcdefgh"] = [kb] + kj.add_kb("abcdefgh", kb) enc_key = kj.get_encrypt_key("RSA", "abcdefgh") assert enc_key != [] @pytest.mark.network def test_provider(self): - ks = KeyJar() - ks.load_keys("https://connect-op.heroku.com", + kj = KeyJar() + kj.load_keys("https://connect-op.heroku.com", jwks_uri="https://connect-op.herokuapp.com/jwks.json") - assert ks["https://connect-op.heroku.com"][0].keys() + assert kj.get_issuer_keys("https://connect-op.heroku.com")[0].keys() def test_import_jwks(): @@ -432,24 +432,14 @@ def test_remove_after(): _old = [k.kid for k in keyjar.get_issuer_keys('') if k.kid] assert len(_old) == 2 + keyjar.remove_after = 1 # rotate_keys = create new keys + make the old as inactive - keyjar = build_keyjar(KEYDEFS, keyjar=keyjar) + keyjar = rotate_keys(KEYDEFS, keyjar=keyjar) - keyjar.remove_after = 1 - # None are remove since none are marked as inactive yet - keyjar.remove_outdated() + keyjar.remove_outdated(time.time() + 3600) _interm = [k.kid for k in keyjar.get_issuer_keys('') if k.kid] - assert len(_interm) == 4 - - # Now mark the keys to be inactivated - _now = time.time() - for k in keyjar.get_issuer_keys(''): - if k.kid in _old: - if not k.inactive_since: - k.inactive_since = _now - - keyjar.remove_outdated(_now + 5) + assert len(_interm) == 2 # The remainder are the new keys _new = [k.kid for k in keyjar.get_issuer_keys('') if k.kid] @@ -618,23 +608,24 @@ def setup(self): self.alice_keyjar = build_keyjar(mkey) # Bob has one single keys self.bob_keyjar = build_keyjar(skey) - self.alice_keyjar['Alice'] = self.alice_keyjar[''] - self.bob_keyjar['Bob'] = self.bob_keyjar[''] + self.alice_keyjar.import_jwks(self.alice_keyjar.export_jwks(private=True, issuer_id=''), + 'Alice') + self.bob_keyjar.import_jwks(self.bob_keyjar.export_jwks(private=True, issuer_id=''), 'Bob') # To Alice's keyjar add Bob's public keys self.alice_keyjar.import_jwks( - self.bob_keyjar.export_jwks(issuer='Bob'), 'Bob') + self.bob_keyjar.export_jwks(issuer_id='Bob'), 'Bob') # To Bob's keyjar add Alice's public keys self.bob_keyjar.import_jwks( - self.alice_keyjar.export_jwks(issuer='Alice'), 'Alice') + self.alice_keyjar.export_jwks(issuer_id='Alice'), 'Alice') _jws = JWS('{"aud": "Bob", "iss": "Alice"}', alg='RS256') - sig_key = self.alice_keyjar.get_signing_key('rsa', owner='Alice')[0] + sig_key = self.alice_keyjar.get_signing_key('rsa', issuer_id='Alice')[0] self.sjwt_a = _jws.sign_compact([sig_key]) _jws = JWS('{"aud": "Alice", "iss": "Bob"}', alg='RS256') - sig_key = self.bob_keyjar.get_signing_key('rsa', owner='Bob')[0] + sig_key = self.bob_keyjar.get_signing_key('rsa', issuer_id='Bob')[0] self.sjwt_b = _jws.sign_compact([sig_key]) def test_no_kid_multiple_keys(self): @@ -656,7 +647,7 @@ def test_no_kid_single_key(self): def test_no_kid_multiple_keys_no_kid_issuer(self): a_kids = [k.kid for k in - self.alice_keyjar.get_verify_key(owner='Alice', + self.alice_keyjar.get_verify_key(issuer_id='Alice', key_type='RSA')] no_kid_issuer = {'Alice': a_kids} _jwt = factory(self.sjwt_a) @@ -685,11 +676,11 @@ def test_no_matching_kid(self): assert keys == [] def test_aud(self): - self.alice_keyjar.import_jwks(JWK1, issuer='D') - self.bob_keyjar.import_jwks(JWK1, issuer='D') + self.alice_keyjar.import_jwks(JWK1, issuer_id='D') + self.bob_keyjar.import_jwks(JWK1, issuer_id='D') _jws = JWS('{"iss": "D", "aud": "A"}', alg='HS256') - sig_key = self.alice_keyjar.get_signing_key('oct', owner='D')[0] + sig_key = self.alice_keyjar.get_signing_key('oct', issuer_id='D')[0] _sjwt = _jws.sign_compact([sig_key]) no_kid_issuer = {'D': []} @@ -703,9 +694,9 @@ def test_aud(self): def test_copy(): kj = KeyJar() - kj['Alice'] = [KeyBundle(JWK0['keys'])] - kj['Bob'] = [KeyBundle(JWK1['keys'])] - kj['C'] = [KeyBundle(JWK2['keys'])] + kj.add_kb('Alice', KeyBundle(JWK0['keys'])) + kj.add_kb('Bob', KeyBundle(JWK1['keys'])) + kj.add_kb('C', KeyBundle(JWK2['keys'])) kjc = kj.copy() @@ -723,9 +714,9 @@ def test_copy(): def test_repr(): kj = KeyJar() - kj['Alice'] = [KeyBundle(JWK0['keys'])] - kj['Bob'] = [KeyBundle(JWK1['keys'])] - kj['C'] = [KeyBundle(JWK2['keys'])] + kj.add_kb('Alice', KeyBundle(JWK0['keys'])) + kj.add_kb('Bob', KeyBundle(JWK1['keys'])) + kj.add_kb('C', KeyBundle(JWK2['keys'])) txt = kj.__repr__() assert " Date: Tue, 12 May 2020 08:48:19 +0200 Subject: [PATCH 18/50] Major change. --- src/cryptojwt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cryptojwt/__init__.py b/src/cryptojwt/__init__.py index ee621f2..fb25595 100644 --- a/src/cryptojwt/__init__.py +++ b/src/cryptojwt/__init__.py @@ -21,7 +21,7 @@ except ImportError: pass -__version__ = '0.9.0' +__version__ = '1.0.0' logger = logging.getLogger(__name__) From 0c791cbb4bd4c90d807030c5cde787855504ce2b Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 12 May 2020 09:02:12 +0200 Subject: [PATCH 19/50] Missing file. --- src/cryptojwt/serialize/item.py | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/cryptojwt/serialize/item.py diff --git a/src/cryptojwt/serialize/item.py b/src/cryptojwt/serialize/item.py new file mode 100644 index 0000000..e63cc4e --- /dev/null +++ b/src/cryptojwt/serialize/item.py @@ -0,0 +1,50 @@ +from cryptojwt import jwk +from cryptojwt.jwk.jwk import key_from_jwk_dict +from cryptojwt import key_bundle +from cryptojwt import key_issuer + + +class JWK: + @staticmethod + def serialize(key: jwk.JWK) -> dict: + _dict = key.serialize() + inactive = key.inactive_since + if inactive: + _dict['inactive_since'] = inactive + return _dict + + @staticmethod + def deserialize(jwk: dict) -> jwk.JWK: + k = key_from_jwk_dict(jwk) + inactive = jwk.get("inactive_since", 0) + if inactive: + k.inactive_since = inactive + return k + + +class KeyBundle: + def __init__(self, storage_conf=None): + self.storage_conf = storage_conf + + @staticmethod + def serialize(item: key_bundle.KeyBundle) -> dict: + _dict = item.dump() + return _dict + + def deserialize(self, spec: dict) -> key_bundle.KeyBundle: + bundle = key_bundle.KeyBundle(storage_conf=self.storage_conf).load(spec) + return bundle + + +class KeyIssuer: + def __init__(self, storage_conf=None): + self.storage_conf = storage_conf + + @staticmethod + def serialize(item: key_issuer.KeyIssuer) -> dict: + _dict = item.dump() + return _dict + + def deserialize(self, spec: dict) -> key_issuer.KeyIssuer: + issuer = key_issuer.KeyIssuer(storage_conf=self.storage_conf).load(spec) + return issuer From 14fbc43fa0eb3a77171d68583d165b8b6ddbe5db Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:19:22 +0200 Subject: [PATCH 20/50] Name should probably be first parameter. --- aslist_tests/test_44_key_issuer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aslist_tests/test_44_key_issuer.py b/aslist_tests/test_44_key_issuer.py index 8bacecf..0526865 100755 --- a/aslist_tests/test_44_key_issuer.py +++ b/aslist_tests/test_44_key_issuer.py @@ -553,7 +553,7 @@ def test_keys_by_alg_and_usage(): def test_copy(): - issuer = KeyIssuer('Alice', storage_conf=STORAGE_CONFIG) + issuer = KeyIssuer(name='Alice', storage_conf=STORAGE_CONFIG) issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) issuer_copy = issuer.copy() @@ -562,10 +562,10 @@ def test_copy(): def test_repr(): - issuer = KeyIssuer('Alice', storage_conf=STORAGE_CONFIG) + issuer = KeyIssuer(name='Alice', storage_conf=STORAGE_CONFIG) issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) txt = issuer.__repr__() - assert " Date: Tue, 19 May 2020 11:21:15 +0200 Subject: [PATCH 21/50] Handed over to KeyIssuer. --- src/cryptojwt/tools/jwtpeek.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cryptojwt/tools/jwtpeek.py b/src/cryptojwt/tools/jwtpeek.py index df0450f..4eb1b51 100755 --- a/src/cryptojwt/tools/jwtpeek.py +++ b/src/cryptojwt/tools/jwtpeek.py @@ -7,6 +7,8 @@ import os import sys +from cryptojwt.key_issuer import KeyIssuer + from cryptojwt.jwe import jwe from cryptojwt.jwk.hmac import SYMKey from cryptojwt.jwk.jwk import key_from_jwk_dict @@ -114,9 +116,9 @@ def main(): keys.append(_key) if args.jwks: - _k = KeyJar() - _k.import_jwks(open(args.jwks).read(), "") - keys.extend(_k.issuer_keys("")) + _iss = KeyIssuer() + _iss.import_jwks(open(args.jwks).read()) + keys.extend(_iss.all_keys()) if args.jwks_url: _kb = KeyBundle(source=args.jwks_url) From 43ce1aa3b1d7c4945e12c8131e1df32d28b0233f Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:21:32 +0200 Subject: [PATCH 22/50] Changed repr --- src/cryptojwt/key_issuer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cryptojwt/key_issuer.py b/src/cryptojwt/key_issuer.py index b42d7c4..bb6dcf7 100755 --- a/src/cryptojwt/key_issuer.py +++ b/src/cryptojwt/key_issuer.py @@ -49,7 +49,7 @@ def __init__(self, ca_certs=None, keybundle_cls=KeyBundle, self.httpc_params = httpc_params or {} def __repr__(self) -> str: - return ''.format(self._bundles) + return ''.format(self.name, self._bundles) def __getitem__(self, item): return self.get_bundles()[item] From e056a3909fb0d93883dd93a413370fd3503b7aee Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:22:10 +0200 Subject: [PATCH 23/50] Finger print of certificates. --- tests/test_02_jwk.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_02_jwk.py b/tests/test_02_jwk.py index e248346..b6d87e1 100644 --- a/tests/test_02_jwk.py +++ b/tests/test_02_jwk.py @@ -16,6 +16,7 @@ from cryptojwt.exception import UnsupportedAlgorithm from cryptojwt.exception import WrongUsage from cryptojwt.jwk import JWK +from cryptojwt.jwk import certificate_fingerprint from cryptojwt.jwk import pem_hash from cryptojwt.jwk import pems_to_x5c from cryptojwt.jwk.ec import NIST2SEC @@ -28,6 +29,7 @@ from cryptojwt.jwk.jwk import jwk_wrap from cryptojwt.jwk.jwk import key_from_jwk_dict from cryptojwt.jwk.rsa import RSAKey +from cryptojwt.jwk.rsa import generate_and_store_rsa_key from cryptojwt.jwk.rsa import import_private_rsa_key_from_file from cryptojwt.jwk.rsa import import_public_rsa_key_from_file from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file @@ -654,4 +656,26 @@ def test_pem_to_x5c(): def test_pem_hash(): _hash = pem_hash(full_path("cert.pem")) - assert _hash \ No newline at end of file + assert _hash + + +def test_certificate_fingerprint(): + with open(full_path('cert.der'), 'rb') as cert_file: + der = cert_file.read() + + res = certificate_fingerprint(der) + assert res == '01:DF:F1:D4:5F:21:7B:2E:3A:A2:D8:CA:13:4C:41:66:03:A1:EF:3E:7B:5E:8B:69:04:5E:80:8B:55:49:F1:48' + + res = certificate_fingerprint(der, 'sha1') + assert res == 'CA:CF:21:9E:72:00:CD:1C:CA:FD:4F:6D:84:6B:9E:E8:74:80:47:64' + + res = certificate_fingerprint(der, 'md5') + assert res == '1B:2B:3B:F8:49:EE:2A:2C:C1:C7:6C:88:86:AB:C6:EE' + + with pytest.raises(UnsupportedAlgorithm): + certificate_fingerprint(der, 'foo') + + +def test_generate_and_store_rsa_key(): + priv_key = generate_and_store_rsa_key(filename=full_path('temp_rsa.key')) + From ceba266f8e0df0f44ce6fa8a2bae5282cba02740 Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:22:58 +0200 Subject: [PATCH 24/50] Test item serializing --- aslist_tests/private_jwks.json | 2 +- tests/test_03_key_bundle.py | 2 ++ tests/test_40_serialize.py | 56 ++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/test_40_serialize.py diff --git a/aslist_tests/private_jwks.json b/aslist_tests/private_jwks.json index b1471f1..14fbc2b 100644 --- a/aslist_tests/private_jwks.json +++ b/aslist_tests/private_jwks.json @@ -1 +1 @@ -{"keys": [{"kty": "RSA", "use": "sig", "kid": "ZG5wZjlUYW11MkZSVzA5MTRuaTdDYUtnV0xQS0pwWUpPTm1YMTJNeFlQYw", "n": "qlDQeEoGfykMrV3WupWIMHGrs4AHVTld-C00qBcVCBNptef6T2UESVSurMITmgCdJrwEVfHwkd2is1xSev2pIjQy8m9CehBexxq0hlNmUhPzNPixbCMqUyxCzGi1bms9qSxg2zJr0pmDFK_EiOyM47B48eYcypUk-PxzX4d1L-jBT6F8B9fT8YjS4OHzs__Yq6EDzJ4gubaqOLjsYQsLkYj3fKz-b4trb9n4eWJkkbvtgCN6gZVVKgmxPpQHVhDIj7jHXfl9QmTooWxObvO9LK8DFK63V-E-Ce5iKNcYmeB4TeOJmdZfasFa4TPOg22jZaE9UOnnZPyXx7VOCkiHsw", "e": "AQAB", "d": "KsaZVVziPNXGhVRoNfyQc_pYsYCaVuFNpKNV8lG5yol1p2ZYC9DHPtOx-1nTKn60-aGHRT66uSf9UScC4DkNXbXWheVDwPyTkVY3uPUBYeP41XkQtqQuYS1gqY4y40Sz--VVfjgvtHkx3uQ2bF1dFWKhPcAZwxeqbY6aO4f9-sYFtgIIINe_vDKw6W8RPJMK4keK_V-8Hw9-t1A_gja2_34U9cSw0yO9BDBNJiE88mX-Nu4djfAqr-MVOXpHEW2FIA7Ge6laZlGR_pn_kaqVbEnG8DLJqJGp_mJIg1tx08kHTVXw8TmqKIeMVZ8uU3Vrfjy6D6PmTkEwZK1_EgvxoQ", "p": "15-5UXO89cFmYFbzW9IRfHp7g8VQw88Uphf3J5IjgI8GL80QqFT7r7uBx1b8uKvJIWnG6OxygVJm9-cy9HPQzCBQQ1e_R5OKAaNvMM_nTVAHAy5KZlt_qFJw7vb5tfRkj8u_0jDt1qp6BYXKBMPWzpxwwKG-aj9PyPrkZyFDn6c", "q": "yjUq8rrtQgnGS4fBQwgf44sg8F47PHX-D7fCmImg6gjW8e4Ll51yDlKHFBskvLNzAWNfy8_GTzTs3Nr2zuQrWrkhXlk4T7FIJATVBI4n4uiNEvfYr17fViLM6T8d0WUM_9zy6EI8CBBRdeAeI8obW5p3F6Wu6UygkrysgyFV-RU"}, {"kty": "EC", "use": "sig", "kid": "aDFHbmtvZldackkyUjJ5aTluSWNaVmJnUXkwYmd2Zk5sOGxCM3dhQVgwRQ", "crv": "P-256", "x": "s4o9jPfgErHOuPBdeWp8U_XUGUs7uSntAu5M4GjTCjQ", "y": "y0YE3V9E-UzEAekBLPhxYqrPPo6Abm-JRFL2Sia6Q0s", "d": "BNXlINQCl4O5vJkdqV13gOqzZJVt34RxN8njWq7Lvlc"}, {"kty": "EC", "use": "sig", "kid": "S0hSaHVmb0phaE1tbW9NcHZjVGRNS291VDlNOE5vWl9UaTNwQVhlUHhOZw", "crv": "P-384", "x": "Lu2wHapme_MnXlhpzH5M5ntqx83j6xYZ2P8u7ZoVOWdvRnmxYC1GrV2IA7feLOUv", "y": "f8S10urWzTkaq1JARY0LgLhZsXmcoTFj5Hd3tyCd6h4XlUU3iZDSfuXahF_xqJ24", "d": "SoRVsBruJ8gmBraOC0bzWD13IOUef3dGRtpIWzmEWm9uWScXxmt6AzgcTAhYP29l"}]} \ No newline at end of file +{"keys": [{"kty": "RSA", "use": "sig", "kid": "ZVp5MEtSdGQ5MjNjUGozUktMZV9Sd0c4N25PUGlQcUtIOVo5b0l0S3dZMA", "n": "oQfNSj9XSCBg5oBztL2TixZT-R8CVvSE0Y1onK40x1VDPfMTx7ezoFo72tiTASsdpAsChG9nkzT0iD5iFcv4c1LCTkqbaSdOzmZFu79Bjv1LVLboFsoVZNYhjRxCyoq1PooR7pKiaI1t531EKrnm4CrjuHk5egWG1cmp61_M3zMCrWcSODXGaFbJOfYfWSIVhD5cd7J4Hpl7rlA-TgD_gBfzL0kILDqvXln2oig_0yOXc7cpneLw9dSZiatyv_deCID5O0Cwg832vwrXN1az-No-ln37onxE03M9VZXqP54Nl5OPQ_cMg0UAGObxr5iyNGe_i6hTUJea0RuJ6sEnyw", "e": "AQAB", "d": "Sm2DukSKgADPKNrIIArbbhb02xk1CKHd3clBR-HQ7S0Adlqqks3ajUwHjEA7ufeGrLKWCEZBli2MtIg456AuBoeC3ZLoP_L2HrnwkzV0BLYYImCj5xyiRMggG8urJ1hzKyO_5AgMXsy3tp4Uarcf-g540GPfaAGz745VJkBSPfrnGYNPWIJbtbwlGJJz8TPaJRpJnAlolq6VeH5BibfoQhU1T5p0q4kLr_yAehQiKW4Y6sVnkuZjjf4g1P6GKqlnUIWS4swmbf1rjibDkysb9bXB1nV6YypVFWX6Cv_WrDUfSSH1MdR6zjOZIoZ_EHDONfP3VjABsefkv6Matz1qOQ", "p": "zG2BfrmeN11HJP43paO8wJR6FzQotv0RgdmLc6iRmPfcCyENtTWXPJo3EPxwrDX5zPbH7i814Q2tCeDp_gPqtBfbLCEV3Gla5cvuDO3K3MW6YhPLDfujoDnojelV7bcLGK0nDnQUyal9KhX4Wk7LddlQ3W8NsB2ymoKuSVF3Pic", "q": "yaeVP0lm1biaOnETbruxnoXeF4W4c4nPm7mBx9CzzCsSHqCpcWNtPUtOQc3wvVM2MdLoTJPalfNYyIkpWN5u5w-Un-SNNXY1BSwOFCTt4BETiwmjmU9rWzxfnQhsbSw6UrtoKjRQZNHD64-1lyRb1HlLBGfx6mb3gQ7qg-2Fs70"}, {"kty": "EC", "use": "sig", "kid": "bzJuTlhLSms0Wmh1TVkzbDRYVHEtN01CUnItemhiQWo0a0hiYVpWSGp4VQ", "crv": "P-256", "x": "AYZz_krBfwtrsXHJ_q3cgvZPQCY1nHgQWUn9bUNCgvU", "y": "iVmtoxf34F3jVlO3mOaHJDa_x1ehiCiB-fDM5iAq0fM", "d": "gPe7wnZMWJ3iFq2rYIIu7d5rWXHdYql3PJoDuxfFSrY"}, {"kty": "EC", "use": "sig", "kid": "WTNtS1dpVlBxN2Q2Q3hOb0dsTF8xenZWUm9Xa0pNTDRvTUdlQmN0aWhZcw", "crv": "P-384", "x": "aKHmXvAowb7XY8iMT_LtbXFHq99JYEo9anfdMdnQ5wpzRKusUznfA0aTHI6Mul3_", "y": "b_kV4fPElf9mDss6RkU31BZPCs4w9zAIxpbj7Rk74LK8q0BxWAYoyUfMs6d_Ywme", "d": "IHXdLJakx41Rw7FXbYLQHuczwALL2EZWboNEGGF8wP-fuG7NOCrggPam-oQR-QML"}]} \ No newline at end of file diff --git a/tests/test_03_key_bundle.py b/tests/test_03_key_bundle.py index afe9f18..d61cb6c 100755 --- a/tests/test_03_key_bundle.py +++ b/tests/test_03_key_bundle.py @@ -542,6 +542,7 @@ def test_httpc_params_1(): httpc_params=httpc_params) assert kb.do_remote() + @pytest.mark.network def test_httpc_params_2(): httpc_params = {'timeout': 0} @@ -989,6 +990,7 @@ def test_remote(): assert kb2.imp_jwks assert kb2.last_updated + def test_remote_not_modified(): source = 'https://example.com/keys.json' headers = { diff --git a/tests/test_40_serialize.py b/tests/test_40_serialize.py new file mode 100644 index 0000000..034dc5c --- /dev/null +++ b/tests/test_40_serialize.py @@ -0,0 +1,56 @@ +import os + +from cryptojwt.jwk.hmac import SYMKey +from cryptojwt.jwk.rsa import RSAKey +from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file +from cryptojwt.key_bundle import keybundle_from_local_file +from cryptojwt.key_bundle import rsa_init +from cryptojwt.key_issuer import KeyIssuer +from cryptojwt.serialize import item +from cryptojwt.serialize.item import JWK +from cryptojwt.serialize.item import KeyBundle + + +def full_path(local_file): + return os.path.join(BASEDIR, local_file) + + +BASEDIR = os.path.abspath(os.path.dirname(__file__)) +BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + "test_keys")) +CERT = full_path("cert.pem") + + +def test_jwks(): + _key = RSAKey() + _key.load_key(import_rsa_key_from_cert_file(CERT)) + + _item = JWK().serialize(_key) + _nkey = JWK().deserialize(_item) + assert _key == _nkey + + +def test_key_bundle(): + kb = rsa_init({'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) + _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) + kb.append(_sym) + _item = KeyBundle().serialize(kb) + _nkb = KeyBundle().deserialize(_item) + assert len(kb) == 3 + assert len(kb.get('rsa')) == 2 + assert len(kb.get('oct')) == 1 + + +def test_key_issuer(): + kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", ["sig"]) + assert len(kb) == 1 + issuer = KeyIssuer() + issuer.add(kb) + + _item = item.KeyIssuer().serialize(issuer) + _iss = item.KeyIssuer().deserialize(_item) + + assert len(_iss) == 1 # 1 key + assert len(_iss.get('sig', 'rsa')) == 1 # 1 RSA key + _kb = _iss[0] + assert kb.difference(_kb) == [] # no difference From 3e26237d00215a9449985932acf65f48600fd8af Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:23:22 +0200 Subject: [PATCH 25/50] Missing arguments. --- aslist_tests/test_43_key_bundle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aslist_tests/test_43_key_bundle.py b/aslist_tests/test_43_key_bundle.py index b25d681..3a66ebd 100755 --- a/aslist_tests/test_43_key_bundle.py +++ b/aslist_tests/test_43_key_bundle.py @@ -984,6 +984,9 @@ def test_export_inactive(): 'imp_jwks', 'keys', 'last_updated', + 'last_local', + 'last_remote', + 'local', 'remote', 'time_out'} From 4e3153fdcf220daae5269661e88ab8cdf62ce6a2 Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 19 May 2020 11:43:24 +0200 Subject: [PATCH 26/50] Repr similar to KeyJar. --- src/cryptojwt/key_issuer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cryptojwt/key_issuer.py b/src/cryptojwt/key_issuer.py index bb6dcf7..11fbee4 100755 --- a/src/cryptojwt/key_issuer.py +++ b/src/cryptojwt/key_issuer.py @@ -49,7 +49,7 @@ def __init__(self, ca_certs=None, keybundle_cls=KeyBundle, self.httpc_params = httpc_params or {} def __repr__(self) -> str: - return ''.format(self.name, self._bundles) + return ''.format(self.name, self.key_summary()) def __getitem__(self, item): return self.get_bundles()[item] From c1e48931f5372fc17344c7f0b96f3e8d7525a7d6 Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 28 May 2020 10:10:44 +0200 Subject: [PATCH 27/50] Worked on persistent storage. --- aslist_tests/private_jwks.json | 2 +- aslist_tests/test_43_key_bundle.py | 1017 ---------------------------- aslist_tests/test_44_key_issuer.py | 585 ---------------- aslist_tests/test_45_key_jar.py | 732 ++++++++++---------- src/cryptojwt/jwk/__init__.py | 6 + src/cryptojwt/jwk/jwk.py | 2 +- src/cryptojwt/key_bundle.py | 58 +- src/cryptojwt/key_issuer.py | 44 +- src/cryptojwt/key_jar.py | 187 +++-- src/cryptojwt/serialize/item.py | 60 +- src/cryptojwt/utils.py | 26 + tests/test_02_jwk.py | 33 +- tests/test_40_serialize.py | 22 - 13 files changed, 557 insertions(+), 2217 deletions(-) delete mode 100755 aslist_tests/test_43_key_bundle.py delete mode 100755 aslist_tests/test_44_key_issuer.py diff --git a/aslist_tests/private_jwks.json b/aslist_tests/private_jwks.json index 14fbc2b..e4a3b0a 100644 --- a/aslist_tests/private_jwks.json +++ b/aslist_tests/private_jwks.json @@ -1 +1 @@ -{"keys": [{"kty": "RSA", "use": "sig", "kid": "ZVp5MEtSdGQ5MjNjUGozUktMZV9Sd0c4N25PUGlQcUtIOVo5b0l0S3dZMA", "n": "oQfNSj9XSCBg5oBztL2TixZT-R8CVvSE0Y1onK40x1VDPfMTx7ezoFo72tiTASsdpAsChG9nkzT0iD5iFcv4c1LCTkqbaSdOzmZFu79Bjv1LVLboFsoVZNYhjRxCyoq1PooR7pKiaI1t531EKrnm4CrjuHk5egWG1cmp61_M3zMCrWcSODXGaFbJOfYfWSIVhD5cd7J4Hpl7rlA-TgD_gBfzL0kILDqvXln2oig_0yOXc7cpneLw9dSZiatyv_deCID5O0Cwg832vwrXN1az-No-ln37onxE03M9VZXqP54Nl5OPQ_cMg0UAGObxr5iyNGe_i6hTUJea0RuJ6sEnyw", "e": "AQAB", "d": "Sm2DukSKgADPKNrIIArbbhb02xk1CKHd3clBR-HQ7S0Adlqqks3ajUwHjEA7ufeGrLKWCEZBli2MtIg456AuBoeC3ZLoP_L2HrnwkzV0BLYYImCj5xyiRMggG8urJ1hzKyO_5AgMXsy3tp4Uarcf-g540GPfaAGz745VJkBSPfrnGYNPWIJbtbwlGJJz8TPaJRpJnAlolq6VeH5BibfoQhU1T5p0q4kLr_yAehQiKW4Y6sVnkuZjjf4g1P6GKqlnUIWS4swmbf1rjibDkysb9bXB1nV6YypVFWX6Cv_WrDUfSSH1MdR6zjOZIoZ_EHDONfP3VjABsefkv6Matz1qOQ", "p": "zG2BfrmeN11HJP43paO8wJR6FzQotv0RgdmLc6iRmPfcCyENtTWXPJo3EPxwrDX5zPbH7i814Q2tCeDp_gPqtBfbLCEV3Gla5cvuDO3K3MW6YhPLDfujoDnojelV7bcLGK0nDnQUyal9KhX4Wk7LddlQ3W8NsB2ymoKuSVF3Pic", "q": "yaeVP0lm1biaOnETbruxnoXeF4W4c4nPm7mBx9CzzCsSHqCpcWNtPUtOQc3wvVM2MdLoTJPalfNYyIkpWN5u5w-Un-SNNXY1BSwOFCTt4BETiwmjmU9rWzxfnQhsbSw6UrtoKjRQZNHD64-1lyRb1HlLBGfx6mb3gQ7qg-2Fs70"}, {"kty": "EC", "use": "sig", "kid": "bzJuTlhLSms0Wmh1TVkzbDRYVHEtN01CUnItemhiQWo0a0hiYVpWSGp4VQ", "crv": "P-256", "x": "AYZz_krBfwtrsXHJ_q3cgvZPQCY1nHgQWUn9bUNCgvU", "y": "iVmtoxf34F3jVlO3mOaHJDa_x1ehiCiB-fDM5iAq0fM", "d": "gPe7wnZMWJ3iFq2rYIIu7d5rWXHdYql3PJoDuxfFSrY"}, {"kty": "EC", "use": "sig", "kid": "WTNtS1dpVlBxN2Q2Q3hOb0dsTF8xenZWUm9Xa0pNTDRvTUdlQmN0aWhZcw", "crv": "P-384", "x": "aKHmXvAowb7XY8iMT_LtbXFHq99JYEo9anfdMdnQ5wpzRKusUznfA0aTHI6Mul3_", "y": "b_kV4fPElf9mDss6RkU31BZPCs4w9zAIxpbj7Rk74LK8q0BxWAYoyUfMs6d_Ywme", "d": "IHXdLJakx41Rw7FXbYLQHuczwALL2EZWboNEGGF8wP-fuG7NOCrggPam-oQR-QML"}]} \ No newline at end of file +{"keys": [{"kty": "RSA", "use": "sig", "kid": "dTZKdDFabEJoSEVKaGQ4anRLd2x3YTJWQW93ejVGUWV3Z0JJZFpvZnhUTQ", "n": "1uPVViKYxyTJ1B1_wiQCSQJrwkLKXQTg6zYy2I1JX3W3gq6i5QD8XxShR62GFdcOj0NkqR2ZiyobVqhKJ3TKErRH1mtxjFuEf-o9h6B5j6Rou6GN4TNv4h-Ed9mG5KyTI364aLlpVT-LcthnihUYwgaq7iKN-OQQD0FHPa0HBouk4QJTLRIkIF2QmSRGAJlnFevzZr0O9vAycGJH1ksTsUioP-oeNtxThlQEOi0lIF5dPGI-ZcQDpdhAHj-C5iBiyi5s4_yFM2UXXF2ZkIVkezfWPKKSQPq7yfCnFAtdeQ5stpfpG0mMXtv1zfTk-Y8-WYMnExwen7F024ZCavjlqw", "e": "AQAB", "d": "JJe3hGtvyLmjBNPhJZYsLXKUFwh4nU5vXp5kGiw1CmRpU3-ZjZWVZDuHG0WZR67Pc-XuBj5cHy6UaTVPK1jf8D9y3Dh_pX8QGRgyUh4plSRSEWF5X5f6vW7Qh_gq2FXq2GiDzpGENlgTzwK63vCovqGUCekoc_GiKnbbQs1sHNjq0WKCSrXZcfsdpdjsjksiQmmfdblPplTuYG4t2GvfYzGXgepUw2LLyJ8KCdSsXyebK9-J8laxk3El0wFbxbI2C7Y8H5BPCN7Cp6zcaenYkYYV7IGy1Z21kv-lTaBXzu23PO5vhH2bzyklvG_a0RbmrrTHr5OCWnl1-BALuYfpQQ", "p": "8_tdp7rJyRrpq4wEPxJrcOvrcrknMG03cjEePV5ozRFFeBVg2nkuESR-JHoxgF3bBOzyurB5eVDT11seZEiisThMzk6OZSnoNsDgk3WTm0c_Yl7qn0dwZANOCYcEIOUe9ouR3kGr1iJ3DZ9o7tYLJ3sb6-oCH1Cakf53iR_RXy8", "q": "4XmdnKVLvzkZP4A4YQ2ROquoAbPOoomd2aHkGndLP9tDuPxur4PiZVrMFjuzMob2uJMW7Y1FT-0WLk9aCVl0P6f893RL1yfRZVKlHopgYxe9jshwk-5VawdLn6HRtbZ-F8gp6KZMaK8oyTk-50CnsizeACcxcVEFQkFNB_v6IkU"}, {"kty": "EC", "use": "sig", "kid": "WDQzSTBkY21kZVZ0YXAzbXZVTUFPQTdLWXpQQ3QzODF6dHJMQk5LSlBpRQ", "crv": "P-256", "x": "PhbU6LJ7Xc75T8YvCcmuBLfWGQ91AVakWlbr0Dk3wgo", "y": "Gg_W4TuhRB4nN8jAXWuOyEg36gbKErLL406Ngh6Cgk8", "d": "y4Sle7C9FHcBnXInlnIjPjKx73Djq0YDpH7FgCihA-g"}, {"kty": "EC", "use": "sig", "kid": "aVdwOVAyVWlzYUhwMHpGRVgxc2hkRDRXWGdIQUp1TVJfc1diVkdjb2w4VQ", "crv": "P-384", "x": "pwaV9kgGNyu7mEkCxj_lRssXgmT-kdIhoK-8fhJn6s1tbEEgBbPQj8Pubqdq_fld", "y": "9VQIWfNfusIb2ZDxwViSzGMNeT_QZCu16HqHx8s5F_l--VfTwqdP0NTtWogT-soO", "d": "kHsAXZL6uhzVCKZQzU0iRiH3SnY9a6MufFIJW9jLL_RSyC5QbUasQGRlZm9j2gSI"}]} \ No newline at end of file diff --git a/aslist_tests/test_43_key_bundle.py b/aslist_tests/test_43_key_bundle.py deleted file mode 100755 index 3a66ebd..0000000 --- a/aslist_tests/test_43_key_bundle.py +++ /dev/null @@ -1,1017 +0,0 @@ -# pylint: disable=missing-docstring,no-self-use -import json -import os -import shutil -import time - -import pytest -import requests -import responses -from abstorage.storages.absqlalchemy import AbstractStorageSQLAlchemy -from cryptography.hazmat.primitives.asymmetric import rsa -from requests_mock import GET - -from cryptojwt.jwk.ec import ECKey -from cryptojwt.jwk.ec import new_ec_key -from cryptojwt.jwk.hmac import SYMKey -from cryptojwt.jwk.rsa import RSAKey -from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file -from cryptojwt.jwk.rsa import new_rsa_key -from cryptojwt.key_bundle import KeyBundle -from cryptojwt.key_bundle import build_key_bundle -from cryptojwt.key_bundle import dump_jwks -from cryptojwt.key_bundle import init_key -from cryptojwt.key_bundle import key_diff -from cryptojwt.key_bundle import key_gen -from cryptojwt.key_bundle import key_rollover -from cryptojwt.key_bundle import keybundle_from_local_file -from cryptojwt.key_bundle import rsa_init -from cryptojwt.key_bundle import unique_keys -from cryptojwt.key_bundle import update_key_bundle - -__author__ = 'Roland Hedberg' - -BASE_PATH = os.path.abspath( - os.path.join(os.path.dirname(__file__), "test_keys")) - -BASEDIR = os.path.abspath(os.path.dirname(__file__)) - - -def full_path(local_file): - return os.path.join(BASEDIR, local_file) - - -RSAKEY = os.path.join(BASE_PATH, "cert.key") -RSA0 = os.path.join(BASE_PATH, "rsa.key") -EC0 = os.path.join(BASE_PATH, 'ec.key') -CERT = full_path("../tests/cert.pem") - -JWK0 = {"keys": [ - {'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", - 'n': - 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY' - '2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfK' - 'qoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8'} -]} - -JWK1 = {"keys": [ - { - "n": - 'zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S' - '_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFY' - 'Inq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVb' - 'CGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znan' - 'LwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MX' - 'sGxBHf3AKT5w', - "e": "AQAB", "kty": "RSA", "kid": "rsa1"}, - { - "k": - 'YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNT' - 'Y0NzMzYjE', - "kty": "oct"}, -]} - -JWK2 = { - "keys": [ - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "kriMPdmBvx68skT8-mPAB3BseeA", - "kty": "RSA", - "n": - 'kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS' - '_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1uml94fLyq_AXbwo9yAduf4dCHT' - 'P8CWR1dnDR-Qnz_4PYlWVEuuHHONOw_blbfdMjhY-C_BYM2E3pRxbohBb3x__C' - 'fueV7ddz2LYiH3wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_' - 'GTgWN8A-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQtSsjQtI--Xs' - 'okxRDqYLwus1I1SihgbV_STTg5enufuw', - "use": "sig", - "x5c": [ - 'MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKz' - 'ApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcN' - 'MTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW' - '50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEF' - 'AAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs' - '5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94f' - 'Lyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C' - '/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHF' - 'i3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp' - '+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2Iw' - 'YDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYW' - 'Njb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49Y' - 'D0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDb' - 'dNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajy' - 'vlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5Uqn' - 'I7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF4' - '6aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODY' - 'RMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ' - ], - "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", - "kty": "RSA", - "n": - 'vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhz' - 'h23V9Tkq-RtwN1Vs_z57hO82kkzL-cQHZX3bMJD-GEGOKXCEXURN7VMyZWMAuz' - 'QoW9vFb1k3cR1RW_EW_P-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5y' - 'Cw5T_Vuwqqsio3V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_K' - 'AS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IAbsk1wRtWDn' - 'dhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ', - "use": "sig", - "x5c": [ - "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" - "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" - "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" - "/uPhEf7zVizjfcr/ISGFe9+yUO" - "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" - "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" - "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" - "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" - "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" - "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" - "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" - "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" - "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" - "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" - "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" - "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" - "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" - "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" - ], - "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", - "kty": "RSA", - "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" - "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" - "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" - "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" - "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" - "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" - "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" - "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" - "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" - "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" - "+Iqzmn" - "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" - "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" - "qA==" - ], - "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", - "kty": "RSA", - "n": - "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" - "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" - "-KXVD02jdmX2HNdYJdPyc0c", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" - "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" - "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" - "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" - "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" - "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" - "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" - "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" - "ig==" - ], - "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" - } - ] -} - -JWKS_DICT = {"keys": [ - { - "n": - u"zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVbCGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": u"AQAB", - "kty": "RSA", - "kid": "5-VBFv40P8D4I-7SFz7hMugTbPs", - "use": "enc" - }, - { - "k": u"YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "oct", - "use": "enc" - }, - { - "kty": "EC", - "kid": "7snis", - "use": "sig", - "x": u'q0WbWhflRbxyQZKFuQvh2nZvg98ak-twRoO5uo2L7Po', - "y": u'GOd2jL_6wa0cfnyA0SmEhok9fkYEnAHFKLLM79BZ8_E', - "crv": "P-256" - } -]} - -if os.path.isdir('keys'): - shutil.rmtree('keys') - -ABS_STORAGE_SQLALCHEMY = dict( - driver='sqlalchemy', - url='sqlite:///:memory:', - params=dict(table='Thing'), - handler=AbstractStorageSQLAlchemy -) - -STORAGE_CONFIG = { - 'name': '', - 'class': 'abstorage.type.list.ASList', - 'kwargs': { - 'io_class': 'cryptojwt.serialize.item.JWK', - 'storage_config': ABS_STORAGE_SQLALCHEMY - } -} - - -def test_with_sym_key(): - kc = KeyBundle({"kty": "oct", "key": "highestsupersecret", "use": "sig"}, - storage_conf=STORAGE_CONFIG) - assert len(kc.get("oct")) == 1 - assert len(kc.get("rsa")) == 0 - assert kc.remote is False - assert kc.source is None - - -def test_with_2_sym_key(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) - assert len(kb.get("oct")) == 2 - assert len(kb) == 2 - - assert kb.get_key_with_kid('kid') is None - assert len(kb.kids()) == 2 - - -def test_remove_sym(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) - assert len(kb) == 2 - keys = kb.get('oct') - kb.remove(keys[0]) - assert len(kb) == 1 - - -def test_remove_key_sym(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) - assert len(kb) == 2 - keys = kb.get('oct') - kb.remove(keys[0]) - assert len(kb) == 1 - - # This should not work - kb.remove_keys_by_type('rsa') - # should still be one - assert len(kb) == 1 - - -def test_rsa_init(): - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) - assert kb - assert len(kb) == 2 - assert len(kb.get('rsa')) == 2 - - -def test_rsa_init_under_spec(): - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024}, storage_conf=STORAGE_CONFIG) - assert kb - assert len(kb) == 2 - assert len(kb.get('rsa')) == 2 - - -def test_unknown_source(): - with pytest.raises(ImportError): - KeyBundle(source='foobar', storage_conf=STORAGE_CONFIG) - - -def test_ignore_unknown_types(): - kb = KeyBundle({ - "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" - "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", - "use": "sig", - "alg": "EdDSA", - "kty": "OKP", - "crv": "Ed25519", - "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" - }, - storage_conf=STORAGE_CONFIG) - - assert len(kb) == 0 - - -def test_remove_rsa(): - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, - storage_conf=STORAGE_CONFIG) - assert len(kb) == 2 - keys = kb.get('rsa') - assert len(keys) == 2 - kb.remove(keys[0]) - assert len(kb) == 1 - - -def test_key_mix(): - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, - storage_conf=STORAGE_CONFIG) - _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) - kb.append(_sym) - assert len(kb) == 3 - assert len(kb.get('rsa')) == 2 - assert len(kb.get('oct')) == 1 - - kb.remove(_sym) - - assert len(kb) == 2 - assert len(kb.get('rsa')) == 2 - assert len(kb.get('oct')) == 0 - - -def test_get_all(): - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}, - storage_conf=STORAGE_CONFIG - ) - _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) - kb.append(_sym) - assert len(kb.get()) == 3 - - _k = kb.keys() - assert len(_k) == 3 - - -def test_keybundle_from_local_der(): - kb = keybundle_from_local_file( - "{}".format(RSA0), - "der", ['enc'], storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - keys = kb.get('rsa') - assert len(keys) == 1 - _key = keys[0] - assert isinstance(_key, RSAKey) - assert _key.kid - - -def test_ec_keybundle_from_local_der(): - kb = keybundle_from_local_file( - "{}".format(EC0), - "der", ['enc'], keytype='EC', storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - keys = kb.get('ec') - assert len(keys) == 1 - _key = keys[0] - assert _key.kid - assert isinstance(_key, ECKey) - - -def test_keybundle_from_local_der_update(): - kb = keybundle_from_local_file( - "file://{}".format(RSA0), - "der", ['enc'], storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - keys = kb.get('rsa') - assert len(keys) == 1 - _key = keys[0] - assert _key.kid - assert isinstance(_key, RSAKey) - - kb.update() - - # Nothing should change - assert len(kb) == 1 - keys = kb.get('rsa') - assert len(keys) == 1 - _key = keys[0] - assert _key.kid - assert isinstance(_key, RSAKey) - - -def test_creat_jwks_sym(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - kb = KeyBundle([a], storage_conf=STORAGE_CONFIG) - _jwks = kb.jwks() - _loc = json.loads(_jwks) - assert list(_loc.keys()) == ["keys"] - assert set(_loc['keys'][0].keys()) == {'kty', 'use', 'k', 'kid'} - - -def test_keybundle_from_local_jwks_file(): - kb = keybundle_from_local_file( - "file://{}".format(os.path.join(BASE_PATH, "jwk.json")), "jwks", ["sig"], - storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - - -def test_keybundle_from_local_jwks(): - kb = keybundle_from_local_file( - "{}".format(os.path.join(BASE_PATH, "jwk.json")), "jwks", ["sig"], - storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - - -def test_update(): - kc = KeyBundle([{"kty": "oct", "key": "highestsupersecret", "use": "sig"}], - storage_conf=STORAGE_CONFIG) - assert len(kc.get("oct")) == 1 - assert len(kc.get("rsa")) == 0 - assert kc.remote is False - assert kc.source is None - - kc.update() # Nothing should happen - assert len(kc.get("oct")) == 1 - assert len(kc.get("rsa")) == 0 - assert kc.remote is False - assert kc.source is None - - -def test_update_RSA(): - kc = keybundle_from_local_file(RSAKEY, "der", ["sig"], storage_conf=STORAGE_CONFIG) - assert kc.remote is False - assert len(kc.get("oct")) == 0 - assert len(kc.get("RSA")) == 1 - - key = kc.get("RSA")[0] - assert isinstance(key, RSAKey) - - kc.update() - assert kc.remote is False - assert len(kc.get("oct")) == 0 - assert len(kc.get("RSA")) == 1 - - key = kc.get("RSA")[0] - assert isinstance(key, RSAKey) - - -def test_outdated(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) - keys = kb.keys() - now = time.time() - keys[0].inactive_since = now - 60 - kb.set(keys) - kb.remove_outdated(30) - assert len(kb) == 1 - - -def test_dump_jwks(): - a = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - b = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb2 = KeyBundle([a, b], storage_conf=STORAGE_CONFIG) - - kb1 = rsa_init({'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) - - # Will not dump symmetric keys - dump_jwks([kb1, kb2], 'jwks_combo') - - # Now read it - - nkb = KeyBundle(source='file://jwks_combo', fileformat='jwks', storage_conf=STORAGE_CONFIG) - - assert len(nkb) == 2 - # both RSA keys - assert len(nkb.get('rsa')) == 2 - - # Will dump symmetric keys - dump_jwks([kb1, kb2], 'jwks_combo', symmetric_too=True) - - # Now read it - nkb = KeyBundle(source='file://jwks_combo', fileformat='jwks', storage_conf=STORAGE_CONFIG) - - assert len(nkb) == 4 - # two RSA keys - assert len(nkb.get('rsa')) == 2 - # two symmetric keys - assert len(nkb.get('oct')) == 2 - - -def test_mark_as_inactive(): - desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) - assert len(kb.keys()) == 1 - for k in kb.keys(): - kb.mark_as_inactive(k.kid) - desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb.do_keys([desc]) - assert len(kb.keys()) == 2 - assert len(kb.active_keys()) == 1 - - -def test_copy(): - desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) - assert len(kb.keys()) == 1 - for k in kb.keys(): - kb.mark_as_inactive(k.kid) - desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb.do_keys([desc]) - - kbc = kb.copy() - assert len(kbc.keys()) == 2 - assert len(kbc.active_keys()) == 1 - - -def test_local_jwk(): - _path = full_path('../tests/jwk_private_key.json') - kb = KeyBundle(source='file://{}'.format(_path), storage_conf=STORAGE_CONFIG) - assert kb - - -def test_local_jwk_copy(): - _path = full_path('../tests/jwk_private_key.json') - kb = KeyBundle(source='file://{}'.format(_path), storage_conf=STORAGE_CONFIG) - kb2 = kb.copy() - assert kb2.source == kb.source - - -@pytest.fixture() -def mocked_jwks_response(): - with responses.RequestsMock() as rsps: - yield rsps - - -def test_httpc_params_1(): - source = 'https://login.salesforce.com/id/keys' # From test_jwks_url() - # Mock response - with responses.RequestsMock() as rsps: - rsps.add(method=GET, url=source, json=JWKS_DICT, status=200) - httpc_params = {'timeout': (2, 2)} # connect, read timeouts in seconds - kb = KeyBundle(source=source, httpc=requests.request, - httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) - assert kb.do_remote() - - -def test_httpc_params_2(): - httpc_params = {'timeout': 0} - kb = KeyBundle(source='https://login.salesforce.com/id/keys', - httpc=requests.request, httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) - # Will always fail to fetch the JWKS because the timeout cannot be set - # to 0s - assert not kb.update() - - -def test_update_2(): - rsa_key = new_rsa_key() - _jwks = {"keys": [rsa_key.serialize()]} - fname = 'tmp_jwks.json' - with open(fname, 'w') as fp: - fp.write(json.dumps(_jwks)) - - kb = KeyBundle(source="file://{}".format(fname), fileformat='jwks', storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - - # Added one more key - ec_key = new_ec_key(crv='P-256', key_ops=["sign"]) - _jwks = {'keys': [rsa_key.serialize(), ec_key.serialize()]} - - with open(fname, 'w') as fp: - fp.write(json.dumps(_jwks)) - - kb.update() - assert len(kb) == 2 - - -def test_update_mark_inactive(): - rsa_key = new_rsa_key() - _jwks = {"keys": [rsa_key.serialize()]} - fname = 'tmp_jwks.json' - with open(fname, 'w') as fp: - fp.write(json.dumps(_jwks)) - - kb = KeyBundle(source="file://{}".format(fname), fileformat='jwks', storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - - # new set of keys - rsa_key = new_rsa_key(alg="RS256") - ec_key = new_ec_key(crv='P-256') - _jwks = {'keys': [rsa_key.serialize(), ec_key.serialize()]} - - with open(fname, 'w') as fp: - fp.write(json.dumps(_jwks)) - - kb.update() - # 2 active and 1 inactive - assert len(kb) == 3 - assert len(kb.active_keys()) == 2 - - assert len(kb.get('rsa')) == 1 - assert len(kb.get('rsa', only_active=False)) == 2 - - -def test_loads_0(): - kb = KeyBundle(JWK0, storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - key = kb.get("rsa")[0] - assert key.kid == 'abc' - assert key.kty == 'RSA' - - -def test_loads_1(): - jwks = { - "keys": [ - { - 'kty': 'RSA', - 'use': 'sig', - 'e': 'AQAB', - "n": - 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8', - 'kid': "1" - }, { - 'kty': 'RSA', - 'use': 'enc', - 'e': 'AQAB', - "n": - 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8', - 'kid': "2" - } - ] - } - - kb = KeyBundle(jwks, storage_conf=STORAGE_CONFIG) - - assert len(kb) == 2 - assert set(kb.kids()) == {'1', '2'} - - -def test_dump_jwk(): - kb = KeyBundle(storage_conf=STORAGE_CONFIG) - kb.append(RSAKey(pub_key=import_rsa_key_from_cert_file(CERT))) - jwks = kb.jwks() - - _wk = json.loads(jwks) - assert list(_wk.keys()) == ["keys"] - assert len(_wk["keys"]) == 1 - assert set(_wk["keys"][0].keys()) == {"kty", "e", "n"} - - kb2 = KeyBundle(_wk, storage_conf=STORAGE_CONFIG) - - assert len(kb2) == 1 - key = kb2.get("rsa")[0] - assert key.kty == 'RSA' - assert isinstance(key.public_key(), rsa.RSAPublicKey) - - -JWKS_DICT = {"keys": [ - { - "n": - u"zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta-NvS-aG_jN5cstVbCGWE20H0vFVrJKNx0Zf-u-aA-syM4uX7wdWgQ-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1kleiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": u"AQAB", - "kty": "RSA", - "kid": "5-VBFv40P8D4I-7SFz7hMugTbPs", - "use": "enc" - }, - { - "k": u"YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "oct", - "use": "enc" - }, - { - "kty": "EC", - "kid": "7snis", - "use": "sig", - "x": u'q0WbWhflRbxyQZKFuQvh2nZvg98ak-twRoO5uo2L7Po', - "y": u'GOd2jL_6wa0cfnyA0SmEhok9fkYEnAHFKLLM79BZ8_E', - "crv": "P-256" - } -]} - - -def test_keys(): - kb = KeyBundle(JWKS_DICT, storage_conf=STORAGE_CONFIG) - - assert len(kb) == 3 - - assert len(kb.get('rsa')) == 1 - assert len(kb.get('oct')) == 1 - assert len(kb.get('ec')) == 1 - - -EXPECTED = [ - b'iA7PvG_DfJIeeqQcuXFmvUGjqBkda8In_uMpZrcodVA', - b'akXzyGlXg8yLhsCczKb_r8VERLx7-iZBUMIVgg2K7p4', - b'kLsuyGef1kfw5-t-N9CJLIHx_dpZ79-KemwqjwdrvTI' -] - - -def test_thumbprint(): - kb = KeyBundle(JWKS_DICT, storage_conf=STORAGE_CONFIG) - for key in kb: - txt = key.thumbprint('SHA-256') - assert txt in EXPECTED - - -@pytest.mark.network -def test_jwks_url(): - keys = KeyBundle(source='https://login.salesforce.com/id/keys', storage_conf=STORAGE_CONFIG) - # Forces read from the network - keys.update() - assert len(keys) - - -KEYSPEC = [ - {"type": "RSA", "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]} -] - -KEYSPEC_2 = [ - {"type": "RSA", "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "EC", "crv": "P-384", "use": ["sig"]} -] - -KEYSPEC_3 = [ - {"type": "RSA", "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "EC", "crv": "P-384", "use": ["sig"]}, - {"type": "EC", "crv": "P-521", "use": ["sig"]} -] - -KEYSPEC_4 = [ - {"type": "RSA", "use": ["sig"]}, - {"type": "RSA", "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "EC", "crv": "P-384", "use": ["sig"]} -] - -KEYSPEC_5 = [ - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "EC", "crv": "P-384", "use": ["sig"]} -] - -KEYSPEC_6 = [ - {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'code'}, - {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'token'}, - {"type": "oct", "bytes": "24", "use": ["enc"], 'kid': 'refresh_token'} -] - - -def test_key_diff_none(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - - diff = key_diff(_kb, KEYSPEC) - assert not diff - - -def test_key_diff_add_one_ec(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - - diff = key_diff(_kb, KEYSPEC_2) - assert diff - assert set(diff.keys()) == {'add'} - assert len(diff['add']) == 1 - assert diff['add'][0].kty == 'EC' - - -def test_key_diff_add_two_ec(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - - diff = key_diff(_kb, KEYSPEC_3) - assert diff - assert set(diff.keys()) == {'add'} - assert len(diff['add']) == 2 - assert diff['add'][0].kty == 'EC' - - -def test_key_diff_add_ec_and_rsa(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - - diff = key_diff(_kb, KEYSPEC_4) - assert diff - assert set(diff.keys()) == {'add'} - assert len(diff['add']) == 2 - assert set([k.kty for k in diff['add']]) == {'EC', 'RSA'} - - -def test_key_diff_add_ec_del_rsa(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - - diff = key_diff(_kb, KEYSPEC_5) - assert diff - assert set(diff.keys()) == {'add', 'del'} - assert len(diff['add']) == 1 - assert len(diff['del']) == 1 - assert diff['add'][0].kty == 'EC' - assert diff['del'][0].kty == 'RSA' - - -def test_key_bundle_update_1(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - diff = key_diff(_kb, KEYSPEC_2) - update_key_bundle(_kb, diff) - - # There should be 3 keys - assert len(_kb) == 3 - - # one RSA - assert len(_kb.get('RSA')) == 1 - - # 2 EC - assert len(_kb.get('EC')) == 2 - - -def test_key_bundle_update_2(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - diff = key_diff(_kb, KEYSPEC_4) - update_key_bundle(_kb, diff) - - # There should be 3 keys - assert len(_kb) == 4 - - # one RSA - assert len(_kb.get('RSA')) == 2 - - # 2 EC - assert len(_kb.get('EC')) == 2 - - -def test_key_bundle_update_3(): - _kb = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - diff = key_diff(_kb, KEYSPEC_5) - assert set(diff.keys()) == {'add', 'del'} # Add an EC and delete an RSA key - update_key_bundle(_kb, diff) - - # There should be 3 keys - assert len(_kb) == 3 - - # One inactive. Only active is implicit - assert len(_kb.get()) == 2 - - # one inactive RSA - assert len(_kb.get('RSA', only_active=False)) == 1 - assert len(_kb.get('RSA')) == 0 - - # 2 EC - assert len(_kb.get('EC')) == 2 - assert len(_kb.get('EC', only_active=False)) == 2 - - -def test_key_rollover(): - kb_0 = build_key_bundle(key_conf=KEYSPEC, storage_conf=STORAGE_CONFIG) - assert len(kb_0.get(only_active=False)) == 2 - assert len(kb_0.get()) == 2 - - kb_1 = key_rollover(kb_0) - - assert len(kb_1.get(only_active=False)) == 4 - assert len(kb_1.get()) == 2 - - -def test_build_key_bundle_sym(): - _kb = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - assert len(_kb) == 3 - - assert len(_kb.get('RSA')) == 0 - assert len(_kb.get('EC')) == 0 - assert len(_kb.get('oct')) == 3 - - -def test_key_bundle_difference_none(): - _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - _kb1 = KeyBundle(storage_conf=STORAGE_CONFIG) - _kb1.extend(_kb0.keys()) - - assert _kb0.difference(_kb1) == [] - - -def test_key_bundle_difference(): - _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - _kb1 = build_key_bundle(key_conf=KEYSPEC_2, storage_conf=STORAGE_CONFIG) - - assert _kb0.difference(_kb1) == _kb0.keys() - assert _kb1.difference(_kb0) == _kb1.keys() - - -def test_unique_keys_1(): - _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - _kb1 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - - keys = _kb0.keys() - keys.extend(_kb1.keys()) - - # All of them - assert len(unique_keys(keys)) == 6 - - -def test_unique_keys_2(): - _kb0 = build_key_bundle(key_conf=KEYSPEC_6, storage_conf=STORAGE_CONFIG) - _kb1 = KeyBundle(storage_conf=STORAGE_CONFIG) - _kb1.extend(_kb0.keys()) - - keys = _kb0.keys() - keys.extend(_kb1.keys()) - - # 3 of 6 - assert len(unique_keys(keys)) == 3 - - -def test_key_gen_rsa(): - _jwk = key_gen("RSA", kid="kid1") - assert _jwk - assert _jwk.kty == "RSA" - assert _jwk.kid == 'kid1' - - assert isinstance(_jwk, RSAKey) - - -def test_init_key(): - spec = { - "type": "RSA", - "kid": "one" - } - - filename = full_path("../tests/tmp_jwk.json") - if os.path.isfile(filename): - os.unlink(filename) - - _key = init_key(filename, **spec) - assert _key.kty == "RSA" - assert _key.kid == 'one' - - assert os.path.isfile(filename) - - # Should not lead to any change - _jwk2 = init_key(filename, **spec) - assert _key == _jwk2 - - _jwk3 = init_key(filename, "RSA", "two") - assert _key != _jwk3 - - # Now _jwk3 is stored in the file - _jwk4 = init_key(filename, "RSA") - assert _jwk4 == _jwk3 - - -def test_export_inactive(): - desc = {"kty": "oct", "key": "highestsupersecret", "use": "sig"} - kb = KeyBundle([desc], storage_conf=STORAGE_CONFIG) - assert len(kb.keys()) == 1 - for k in kb.keys(): - kb.mark_as_inactive(k.kid) - desc = {"kty": "oct", "key": "highestsupersecret", "use": "enc"} - kb.do_keys([desc]) - res = kb.dump() - assert set(res.keys()) == {'cache_time', - 'fileformat', - 'httpc_params', - 'imp_jwks', - 'keys', - 'last_updated', - 'last_local', - 'last_remote', - 'local', - 'remote', - 'time_out'} - - kb2 = KeyBundle(storage_conf=STORAGE_CONFIG).load(res) - assert len(kb2.keys()) == 2 - assert len(kb2.active_keys()) == 1 - - -def test_remote(): - source = 'https://example.com/keys.json' - # Mock response - with responses.RequestsMock() as rsps: - rsps.add(method="GET", url=source, json=JWKS_DICT, status=200) - httpc_params = {'timeout': (2, 2)} # connect, read timeouts in seconds - kb = KeyBundle(source=source, httpc=requests.request, - httpc_params=httpc_params, storage_conf=STORAGE_CONFIG) - kb.do_remote() - - exp = kb.dump() - kb2 = KeyBundle(storage_conf=STORAGE_CONFIG).load(exp) - assert kb2.source == source - assert len(kb2.keys()) == 3 - assert len(kb2.get("rsa")) == 1 - assert len(kb2.get("oct")) == 1 - assert len(kb2.get("ec")) == 1 - assert kb2.httpc_params == {'timeout': (2, 2)} - assert kb2.imp_jwks - assert kb2.last_updated diff --git a/aslist_tests/test_44_key_issuer.py b/aslist_tests/test_44_key_issuer.py deleted file mode 100755 index 0526865..0000000 --- a/aslist_tests/test_44_key_issuer.py +++ /dev/null @@ -1,585 +0,0 @@ -import os -import time - -import pytest -from abstorage.storages.absqlalchemy import AbstractStorageSQLAlchemy - -from cryptojwt.exception import JWKESTException -from cryptojwt.key_bundle import KeyBundle -from cryptojwt.key_bundle import keybundle_from_local_file -from cryptojwt.key_issuer import KeyIssuer -from cryptojwt.key_issuer import build_keyissuer - -__author__ = 'Roland Hedberg' - -BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), - "test_keys")) -RSAKEY = os.path.join(BASE_PATH, "cert.key") -RSA0 = os.path.join(BASE_PATH, "rsa.key") -EC0 = os.path.join(BASE_PATH, "ec.key") -BASEDIR = os.path.abspath(os.path.dirname(__file__)) - - -def full_path(local_file): - return os.path.join(BASEDIR, local_file) - - -JWK0 = { - "keys": [ - { - 'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", - 'n': - 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5' - 'B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8' - } - ] -} - -JWK1 = { - "keys": [ - { - "n": - "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" - "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" - "-NvS-aG_jN5cstVbCGWE20H0vF" - "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" - "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" - "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": "AQAB", "kty": "RSA", "kid": "rsa1" - }, - { - "k": - "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "oct" - }, - ] -} - -JWK2 = { - "keys": [ - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "kriMPdmBvx68skT8-mPAB3BseeA", - "kty": "RSA", - "n": - "kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1um" - "l94fLyq_AXbwo9yAduf4dCHTP8CWR1dnDR" - "-Qnz_4PYlWVEuuHHONOw_blbfdMjhY" - "-C_BYM2E3pRxbohBb3x__CfueV7ddz2LYiH3" - "wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_GTgWN8A" - "-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQt" - "SsjQtI--XsokxRDqYLwus1I1SihgbV_STTg5enufuw", - "use": "sig", - "x5c": [ - "MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb" - "2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb2" - "50cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipg" - "H0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6" - "/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Q" - "nz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x" - "//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13S" - "QwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp" - "+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5en" - "ufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJ" - "vbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdN" - "VGKCmSf8M65b8h0NwlIjGGGy" - "/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADD" - "kN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5" - "+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8y" - "PJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW" - "+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ" - ], - "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", - "kty": "RSA", - "n": - "vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhzh23V9Tkq" - "-RtwN1Vs_z57hO82kkzL-cQHZX3bMJ" - "D-GEGOKXCEXURN7VMyZWMAuzQoW9vFb1k3cR1RW_EW_P" - "-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5yCw5T_Vuwqqsio3" - "V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_KAS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IA" - "bsk1wRtWDndhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ", - "use": "sig", - "x5c": [ - "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" - "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" - "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" - "/uPhEf7zVizjfcr/ISGFe9+yUO" - "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" - "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" - "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" - "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" - "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" - "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" - "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" - "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" - "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" - "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" - "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" - "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" - "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" - "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" - ], - "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", - "kty": "RSA", - "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" - "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" - "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" - "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" - "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" - "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" - "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" - "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" - "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" - "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" - "+Iqzmn" - "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" - "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" - "qA==" - ], - "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", - "kty": "RSA", - "n": - "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" - "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" - "-KXVD02jdmX2HNdYJdPyc0c", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" - "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" - "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" - "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" - "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" - "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" - "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" - "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" - "ig==" - ], - "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" - } - ] -} - -ABS_STORAGE_SQLALCHEMY = dict( - driver='sqlalchemy', - url='sqlite:///:memory:', - params=dict(table='Thing'), - handler=AbstractStorageSQLAlchemy -) - -STORAGE_CONFIG = { - 'KeyIssuer': { - 'name': '', - 'class': 'abstorage.type.list.ASList', - 'kwargs': { - 'io_class': 'cryptojwt.serialize.item.KeyBundle', - 'storage_config': ABS_STORAGE_SQLALCHEMY - } - }, - 'KeyBundle': { - 'name': '', - 'class': 'abstorage.type.list.ASList', - 'kwargs': { - 'io_class': 'cryptojwt.serialize.item.JWK', - 'storage_config': ABS_STORAGE_SQLALCHEMY - } - } -} - - -def test_build_key_issuer(): - keys = [ - {"type": "RSA", "use": ["enc", "sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - ] - - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - - assert len(key_issuer) == 3 # A total of 3 keys - assert len(key_issuer.get('sig')) == 2 # 2 for signing - assert len(key_issuer.get('enc')) == 1 # 1 for encryption - - -def test_build_keyissuer_usage(): - keys = [ - {"type": "RSA", "use": ["enc", "sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "oct", "use": ["enc"]}, - {"type": "oct", "use": ["enc"]}, - ] - - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - jwks_sig = key_issuer.export_jwks(usage='sig') - jwks_enc = key_issuer.export_jwks(usage='enc') - assert len(jwks_sig.get('keys')) == 2 # A total of 2 keys with use=sig - assert len(jwks_enc.get('keys')) == 3 # A total of 3 keys with use=enc - - for key in jwks_sig["keys"]: - assert "d" not in key # the JWKS shouldn't contain the private part of the keys - for key in jwks_enc["keys"]: - assert "d" not in key # the JWKS shouldn't contain the private part of the keys - - -def test_build_keyissuer_missing(tmpdir): - keys = [ - { - "type": "RSA", "key": os.path.join(tmpdir.dirname, "missing_file"), - "use": ["enc", "sig"] - }] - - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - assert key_issuer is None - - -def test_build_RSA_keyissuer_from_file(tmpdir): - keys = [{"type": "RSA", "key": RSA0, "use": ["enc", "sig"]}] - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - assert len(key_issuer) == 2 - - -def test_build_EC_keyissuer_missing(tmpdir): - keys = [ - { - "type": "EC", "key": os.path.join(tmpdir.dirname, "missing_file"), - "use": ["enc", "sig"] - }] - - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - assert key_issuer is None - - -def test_build_EC_keyissuer_from_file(tmpdir): - keys = [ - { - "type": "EC", "key": EC0, - "use": ["enc", "sig"] - }] - - key_issuer = build_keyissuer(keys, storage_conf=STORAGE_CONFIG) - - assert len(key_issuer) == 2 - - -class TestKeyIssuer(object): - def test_add_kb(self): - issuer = KeyIssuer(name='https://issuer.example.com', storage_conf=STORAGE_CONFIG) - kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG) - issuer.add_kb(kb) - assert len(issuer.all_keys()) == 1 - - def test_add_symmetric(self): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.add_symmetric('abcdefghijklmnop', ['sig']) - assert len(issuer.get('oct')) == 1 - - def test_add(self): - issuer = KeyIssuer(name='https://issuer.example.com', storage_conf=STORAGE_CONFIG) - kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG) - issuer.add(kb) - assert len(issuer.all_keys()) == 1 - - def test_items(self): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.add(KeyBundle( - [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, - {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}]), storage_conf=STORAGE_CONFIG) - issuer.add( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG)) - - assert len(issuer.items()) == 2 - - def test_get_enc(self): - issuer = KeyIssuer() - issuer.add(KeyBundle( - [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}]), storage_conf=STORAGE_CONFIG) - issuer.add( - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"], storage_conf=STORAGE_CONFIG)) - - assert issuer.get('enc', 'oct') - - def test_dump_issuer_keys(self): - kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", - ["sig"], storage_conf=STORAGE_CONFIG) - assert len(kb) == 1 - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.add(kb) - _jwks_dict = issuer.export_jwks() - - _info = _jwks_dict['keys'][0] - assert _info == { - 'use': 'sig', - 'e': 'AQAB', - 'kty': 'RSA', - 'alg': 'RS256', - 'n': 'pKybs0WaHU_y4cHxWbm8Wzj66HtcyFn7Fh3n' - '-99qTXu5yNa30MRYIYfSDwe9JVc1JUoGw41yq2StdGBJ40HxichjE' - '-Yopfu3B58Q' - 'lgJvToUbWD4gmTDGgMGxQxtv1En2yedaynQ73sDpIK-12JJDY55pvf' - '-PCiSQ9OjxZLiVGKlClDus44_uv2370b9IN2JiEOF-a7JB' - 'qaTEYLPpXaoKWDSnJNonr79tL0T7iuJmO1l705oO3Y0TQ' - '-INLY6jnKG_RpsvyvGNnwP9pMvcP1phKsWZ10ofuuhJGRp8IxQL9Rfz' - 'T87OvF0RBSO1U73h09YP-corWDsnKIi6TbzRpN5YDw', - 'kid': 'abc' - } - - def test_no_use(self): - kb = KeyBundle(JWK0["keys"]) - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.add(kb) - enc_key = issuer.get('enc', "RSA") - assert enc_key != [] - - @pytest.mark.network - def test_provider(self): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.load_keys(jwks_uri="https://connect-op.herokuapp.com/jwks.json") - - assert issuer.all_keys() - - -def test_import_jwks(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWK1) - assert len(issuer.all_keys()) == 2 - - -def test_get_signing_key_use_undefined(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWK1) - keys = issuer.get('sig', kid='rsa1') - assert len(keys) == 1 - - keys = issuer.get('sig', key_type='rsa') - assert len(keys) == 1 - - keys = issuer.get('sig', key_type='rsa', kid='rsa1') - assert len(keys) == 1 - - -KEYDEFS = [ - {"type": "RSA", "key": '', "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]} -] - - -def test_remove_after(): - # initial key_issuer - key_issuer = build_keyissuer(KEYDEFS, storage_conf=STORAGE_CONFIG) - _old = [k.kid for k in key_issuer.all_keys() if k.kid] - assert len(_old) == 2 - - # rotate_keys = create new keys + make the old as inactive - key_issuer = build_keyissuer(KEYDEFS, key_issuer=key_issuer) - - key_issuer.remove_after = 1 - # None are remove since none are marked as inactive yet - key_issuer.remove_outdated() - - _interm = [k.kid for k in key_issuer.all_keys() if k.kid] - assert len(_interm) == 4 - - # Now mark the keys to be inactivated - _now = time.time() - for kid in _old: - key_issuer.mark_as_inactive(kid) - - key_issuer.remove_outdated(_now + 5) - - # The remainder are the new keys - _new = [k.kid for k in key_issuer.all_keys() if k.kid] - assert len(_new) == 2 - - # should not be any overlap between old and new - assert set(_new).intersection(set(_old)) == set() - - -JWK_UK = { - "keys": [ - { - "n": - "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" - "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" - "-NvS-aG_jN5cstVbCGWE20H0vF" - "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" - "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" - "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": "AQAB", "kty": "RSA", "kid": "rsa1" - }, - { - "k": - "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "buz" - }, - ] -} - - -def test_load_unknown_keytype(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWK_UK) - assert len(issuer.all_keys()) == 1 - - -JWK_FP = { - "keys": [ - {"e": "AQAB", "kty": "RSA", "kid": "rsa1"}, - ] -} - - -def test_load_missing_key_parameter(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - with pytest.raises(JWKESTException): - issuer.import_jwks(JWK_FP) - - -JWKS_SPO = { - "keys": [ - { - "kid": - "BfxfnahEtkRBG3Hojc9XGLGht_5rDBj49Wh3sBDVnzRpulMqYwMRmpizA0aSPT1fhCHYivTiaucWUqFu_GwTqA", - "use": "sig", - "alg": "ES256", - "kty": "EC", - "crv": "P-256", - "x": "1XXUXq75gOPZ4bEj1o2Z5XKJWSs6LmL6fAOK3vyMzSc", - "y": "ac1h_DwyuUxhkrD9oKMJ-b_KuiVvvSARIwT-XoEmDXs" - }, - { - "kid": - "91pD1H81rXUvrfg9mkngIG-tXjnldykKUVbITDIU1SgJvq91b8clOcJuEHNAq61eIvg8owpEvWcWAtlbV2awyA", - "use": "sig", - "alg": "ES256", - "kty": "EC", - "crv": "P-256", - "x": "2DfQoLpZS2j3hHEcHDkzV8ISx-RdLt6Opy8YZYVm4AQ", - "y": "ycvkFMBIzgsowiaf6500YlG4vaMSK4OF7WVtQpUbEE0" - }, - { - "kid": "0sIEl3MUJiCxrqleEBBF-_bZq5uClE84xp-wpt8oOI" - "-WIeNxBjSR4ak_OTOmLdndB0EfDLtC7X1JrnfZILJkxA", - "use": "sig", - "alg": "RS256", - "kty": "RSA", - "n": - "yG9914Q1j63Os4jX5dBQbUfImGq4zsXJD4R59XNjGJlEt5ek6NoiDl0ucJO3_7_R9e5my2ONTSqZhtzFW6MImnIn8idWYzJzO2EhUPCHTvw_2oOGjeYTE2VltIyY_ogIxGwY66G0fVPRRH9tCxnkGOrIvmVgkhCCGkamqeXuWvx9MCHL_gJbZJVwogPSRN_SjA1gDlvsyCdA6__CkgAFcSt1sGgiZ_4cQheKexxf1-7l8R91ZYetz53drk2FS3SfuMZuwMM4KbXt6CifNhzh1Ye-5Tr_ZENXdAvuBRDzfy168xnk9m0JBtvul9GoVIqvCVECB4MPUb7zU6FTIcwRAw", - "e": "AQAB" - }, - { - "kid": - "zyDfdEU7pvH0xEROK156ik8G7vLO1MIL9TKyL631kSPtr9tnvs9XOIiq5jafK2hrGr2qqvJdejmoonlGqWWZRA", - "use": "sig", - "alg": "RS256", - "kty": "RSA", - "n": - "68be-nJp46VLj4Ci1V36IrVGYqkuBfYNyjQTZD_7yRYcERZebowOnwr3w0DoIQpl8iL2X8OXUo7rUW_LMzLxKx2hEmdJfUn4LL2QqA3KPgjYz8hZJQPG92O14w9IZ-8bdDUgXrg9216H09yq6ZvJrn5Nwvap3MXgECEzsZ6zQLRKdb_R96KFFgCiI3bEiZKvZJRA7hM2ePyTm15D9En_Wzzfn_JLMYgE_DlVpoKR1MsTinfACOlwwdO9U5Dm-5elapovILTyVTgjN75i-wsPU2TqzdHFKA-4hJNiWGrYPiihlAFbA2eUSXuEYFkX43ahoQNpeaf0mc17Jt5kp7pM2w", - "e": "AQAB" - }, - { - "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" - "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", - "use": "sig", - "alg": "EdDSA", - "kty": "OKP", - "crv": "Ed25519", - "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" - }, - { - "kid": - "bL33HthM3fWaYkY2_pDzUd7a65FV2R2LHAKCOsye8eNmAPDgRgpHWPYpWFVmeaujUUEXRyDLHN" - "-Up4QH_sFcmw", - "use": "sig", - "alg": "EdDSA", - "kty": "OKP", - "crv": "Ed25519", - "x": "CS01DGXDBPV9cFmd8tgFu3E7eHn1UcP7N1UCgd_JgZo" - } - ] -} - - -def test_load_spomky_keys(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWKS_SPO) - assert len(issuer.all_keys()) == 4 - - -def test_get_ec(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWKS_SPO) - k = issuer.get('sig', 'EC', alg='ES256') - assert k - - -def test_get_ec_wrong_alg(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWKS_SPO) - k = issuer.get('sig', 'EC', alg='ES512') - assert k == [] - - -def test_keys_by_alg_and_usage(): - issuer = KeyIssuer(storage_conf=STORAGE_CONFIG) - issuer.import_jwks(JWKS_SPO) - k = issuer.get('sig', alg='RS256') - assert len(k) == 2 - - -def test_copy(): - issuer = KeyIssuer(name='Alice', storage_conf=STORAGE_CONFIG) - issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) - issuer_copy = issuer.copy() - - assert len(issuer_copy.get('sig', 'oct')) == 0 - assert len(issuer_copy.get('sig', 'rsa')) == 1 - - -def test_repr(): - issuer = KeyIssuer(name='Alice', storage_conf=STORAGE_CONFIG) - issuer.add_kb(KeyBundle(JWK0['keys'], storage_conf=STORAGE_CONFIG)) - txt = issuer.__repr__() - assert ' List[str]: :return: """ - return [i.name for i in self._issuers] + return list(self._issuers.keys()) - def _get_issuer(self, issuer_id): + def _get_issuer(self, issuer_id: str) -> Optional[KeyIssuer]: """ Return the KeyIssuer instance that has name == issuer_id :param issuer_id: The issuer identifiers :return: A KeyIssuer instance or None """ - _i = [i for i in self._issuers if i.name == issuer_id] - if _i: - return _i[0] # should only be one - else: - return None + + return self._issuers.get(issuer_id) + + def _add_issuer(self, issuer_id): + _iss = KeyIssuer(ca_certs=self.ca_certs, name=issuer_id, + keybundle_cls=self.keybundle_cls, + remove_after=self.remove_after, + httpc=self.httpc, httpc_params=self.httpc_params) + self._issuers[issuer_id] = _iss + return _iss + + def items(self): + """ + Get all owner ID's and their keys + + :return: list of 2-tuples (Owner ID., list of KeyBundles) + """ + return self._issuers.items() def __repr__(self): issuers = self._issuer_ids() @@ -100,22 +118,17 @@ def return_issuer(self, issuer_id): """ _iss = self._get_issuer(issuer_id) if not _iss: - _iss = KeyIssuer(ca_certs=self.ca_certs, name=issuer_id, - keybundle_cls=self.keybundle_cls, - remove_after=self.remove_after, - httpc=self.httpc, httpc_params=self.httpc_params, - storage_conf=self.storage_conf) - self._issuers.append(_iss) + return self._add_issuer(issuer_id) return _iss - def add_url(self, issuer_id, url, **kwargs): + def add_url(self, issuer_id: str, url: str, **kwargs) -> KeyBundle: """ Add a set of keys by url. This method will create a :py:class:`oidcmsg.key_bundle.KeyBundle` instance with the url as source specification. If no file format is given it's assumed that what's on the other side is a JWKS. - :param issuer: Who issued the keys + :param issuer_id: Who issued the keys :param url: Where can the key/-s be found :param kwargs: extra parameters for instantiating KeyBundle :return: A :py:class:`oidcmsg.oauth2.keybundle.KeyBundle` instance @@ -148,32 +161,7 @@ def add_kb(self, issuer_id, kb): """ issuer = self.return_issuer(issuer_id) issuer.add_kb(kb) - - # def __setitem__(self, issuer_id, val): - # """ - # Bind one or a list of key bundles to a special identifier. - # Will overwrite whatever was there before !! - # - # :param issuer_id: The owner of the keys in the key bundle/-s - # :param val: A single or a list of KeyBundle instance - # """ - # if not isinstance(val, list): - # val = [val] - # - # for kb in val: - # if not isinstance(kb, KeyBundle): - # raise ValueError('{} not an KeyBundle instance'.format(kb)) - # - # issuer = self.return_issuer(issuer_id) - # issuer.set(val) - - def items(self): - """ - Get all owner ID's and their keys - - :return: list of 2-tuples (Owner ID., list of KeyBundles) - """ - return [(i.name, i.get_bundles()) for i in self._issuers] + self[issuer_id] = issuer def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs): """ @@ -317,13 +305,25 @@ def __getitem__(self, issuer_id=''): """ return self._get_issuer(issuer_id) + def __setitem__(self, issuer_id, issuer): + """ + Set a KeyIssuer with the name == issuer_id + + :param issuer_id: The entity ID + :param issuer: KeyIssuer instance + """ + self._issuers[issuer_id] = issuer + + def set(self, issuer_id, issuer): + self[issuer_id] = issuer + def owners(self): """ Return a list of all the entities that has keys in this key jar. :return: A list of entity IDs """ - return self._issuer_ids() + return list(self._issuers.keys()) def match_owner(self, url): """ @@ -334,16 +334,16 @@ def match_owner(self, url): :param url: A URL :return: An issue entity ID that exists in the Key jar """ - _iss = [i for i in self._issuers if i.name.startswith(url)] + _iss = [i for i in self._issuers.keys() if i.startswith(url)] if _iss: - return _iss[0].name + return _iss[0] raise KeyError("No keys for '{}' in this keyjar".format(url)) def __str__(self): _res = {} - for _issuer in self._issuers: - _res[_issuer.name] = _issuer.key_summary() + for _id, _issuer in self._issuers.items(): + _res[_id] = _issuer.key_summary() return json.dumps(_res) def load_keys(self, issuer_id, jwks_uri='', jwks=None, replace=False): @@ -371,6 +371,8 @@ def load_keys(self, issuer_id, jwks_uri='', jwks=None, replace=False): _keys = jwks['keys'] _issuer.add_kb(self.keybundle_cls(_keys)) + self[issuer_id] = _issuer + def find(self, source, issuer_id=None): """ Find a key bundle based on the source of the keys @@ -381,7 +383,7 @@ def find(self, source, issuer_id=None): """ if issuer_id is None: res = {} - for _issuer in self._issuers: + for _, _issuer in self._issuers.items(): kbs = _issuer.find(source) if kbs: res[_issuer.name] = kbs @@ -438,8 +440,8 @@ def import_jwks(self, jwks, issuer_id): else: _issuer = self.return_issuer(issuer_id=issuer_id) _issuer.add(self.keybundle_cls(_keys, httpc=self.httpc, - httpc_params=self.httpc_params, - storage_conf=self.storage_conf)) + httpc_params=self.httpc_params)) + self[issuer_id] = _issuer def import_jwks_as_json(self, jwks, issuer_id): """ @@ -476,9 +478,7 @@ def __eq__(self, other): return True def __delitem__(self, key): - _issuer = self._get_issuer(key) - if _issuer: - self._issuers.remove(_issuer) + del self._issuers[key] def remove_outdated(self, when=0): """ @@ -492,14 +492,9 @@ def remove_outdated(self, when=0): :param when: To facilitate testing """ - _ids = self._issuer_ids() - for _id in _ids: - _issuer = self[_id] + for _id, _issuer in self._issuers.items(): _before = len(_issuer) _issuer.remove_outdated(when) - if len(_issuer) != _before: - del self[_id] - self.append(_issuer) def _add_key(self, keys, issuer_id, use, key_type='', kid='', no_kid_issuer=None, allow_missing_kid=False): @@ -640,10 +635,18 @@ def copy(self): :return: A :py:class:`oidcmsg.key_jar.KeyJar` instance """ - kj = KeyJar() - for _issuer in self._issuers: - _iss = kj.return_issuer(_issuer.name) - _iss.set([kb.copy() for kb in _issuer]) + if self.storage_conf: + _conf = self.storage_conf.get('KeyJar') + if _conf: + _label = self.storage_conf.get('label') + if _label: + self.storage_conf['KeyJar']['label'] = '{}.copy'.format(_label) + + kj = KeyJar(storage_conf=self.storage_conf) + for _id, _issuer in self._issuers.items(): + _issuer_copy = KeyIssuer() + _issuer_copy.set([kb.copy() for kb in _issuer]) + kj[_id] = _issuer_copy kj.httpc_params = self.httpc_params kj.httpc = self.httpc @@ -667,11 +670,11 @@ def dump(self, exclude=None): 'remove_after': self.remove_after, 'httpc_params': self.httpc_params} - _issuers = [] - for _issuer in self._issuers: + _issuers = {} + for _id, _issuer in self._issuers.items(): if exclude and _issuer.name in exclude: continue - _issuers.append(_issuer.dump()) + _issuers[_id] = _issuer.dump() info['issuers'] = _issuers return info @@ -689,13 +692,10 @@ def load(self, info): self.remove_after = info['remove_after'] self.httpc_params = info['httpc_params'] - for _issuer_desc in info['issuers']: - self._issuers.append(KeyIssuer(storage_conf=self.storage_conf).load(_issuer_desc)) + for _issuer_id, _issuer_desc in info['issuers'].items(): + self._issuers[_issuer_id] = KeyIssuer().load(_issuer_desc) return self - def append(self, issuer): - self._issuers.append(issuer) - def key_summary(self, issuer_id): _issuer = self._get_issuer(issuer_id) if _issuer: @@ -705,16 +705,16 @@ def key_summary(self, issuer_id): def update(self): """ - Go through the whole key jar, key bundle by key bundle and update them one + Go through the whole key jar, key issuer by key issuer and update them one by one. :param keyjar: The key jar to update """ - for _id in self._issuer_ids(): - _issuer = self._get_issuer(_id) - self._issuers.remove(_issuer) + ids = self._issuers.keys() + for _id in ids: + _issuer = self[_id] _issuer.update() - self._issuers.append(_issuer) + self[_id] = _issuer # ============================================================================= @@ -763,15 +763,14 @@ def build_keyjar(key_conf, kid_template="", keyjar=None, issuer_id='', storage_c :return: A KeyJar instance """ - _issuer = build_keyissuer(key_conf, kid_template, storage_conf=storage_conf, - issuer_id=issuer_id) + _issuer = build_keyissuer(key_conf, kid_template, issuer_id=issuer_id) if _issuer is None: return None if keyjar is None: - keyjar = KeyJar() + keyjar = KeyJar(storage_conf=storage_conf) - keyjar.append(_issuer) + keyjar[issuer_id] = _issuer return keyjar @@ -824,7 +823,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea if private_path: if os.path.isfile(private_path): _jwks = open(private_path, 'r').read() - _issuer = KeyIssuer(name=issuer_id, storage_conf=storage_conf) + _issuer = KeyIssuer(name=issuer_id) _issuer.import_jwks(json.loads(_jwks)) if key_defs: _kb = _issuer[0] @@ -840,7 +839,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea fp.write(json.dumps(jwks)) fp.close() else: - _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) if not read_only: jwks = _issuer.export_jwks(private=True) head, tail = os.path.split(private_path) @@ -861,7 +860,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea elif public_path: if os.path.isfile(public_path): _jwks = open(public_path, 'r').read() - _issuer = KeyIssuer(name=issuer_id, storage_conf=storage_conf) + _issuer = KeyIssuer(name=issuer_id) _issuer.import_jwks(json.loads(_jwks)) if key_defs: _kb = _issuer[0] @@ -877,7 +876,7 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea fp.write(json.dumps(jwks)) fp.close() else: - _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) if not read_only: _jwks = _issuer.export_jwks(issuer=issuer_id) head, tail = os.path.split(public_path) @@ -887,20 +886,18 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea fp.write(json.dumps(_jwks)) fp.close() else: - _issuer = build_keyissuer(key_defs, issuer_id=issuer_id, storage_conf=storage_conf) + _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) keyjar = KeyJar(storage_conf=storage_conf) - keyjar.append(_issuer) + keyjar[issuer_id] = _issuer return keyjar -def rotate_keys(key_conf, keyjar, kid_template="", issuer_id='', storage_conf=None): - new_keys = build_keyissuer(key_conf, kid_template, storage_conf=storage_conf, - issuer_id=issuer_id) +def rotate_keys(key_conf, keyjar, kid_template="", issuer_id=''): + new_keys = build_keyissuer(key_conf, kid_template, issuer_id=issuer_id) _issuer = keyjar[issuer_id] _issuer.mark_all_keys_as_inactive() for kb in new_keys: _issuer.add_kb(kb) - del keyjar[_issuer.name] - keyjar.append(_issuer) + keyjar[issuer_id] = _issuer return keyjar diff --git a/src/cryptojwt/serialize/item.py b/src/cryptojwt/serialize/item.py index e63cc4e..513dbf3 100644 --- a/src/cryptojwt/serialize/item.py +++ b/src/cryptojwt/serialize/item.py @@ -1,50 +1,28 @@ -from cryptojwt import jwk -from cryptojwt.jwk.jwk import key_from_jwk_dict -from cryptojwt import key_bundle -from cryptojwt import key_issuer - - -class JWK: - @staticmethod - def serialize(key: jwk.JWK) -> dict: - _dict = key.serialize() - inactive = key.inactive_since - if inactive: - _dict['inactive_since'] = inactive - return _dict +import json +from urllib.parse import quote_plus +from urllib.parse import unquote_plus - @staticmethod - def deserialize(jwk: dict) -> jwk.JWK: - k = key_from_jwk_dict(jwk) - inactive = jwk.get("inactive_since", 0) - if inactive: - k.inactive_since = inactive - return k +from cryptojwt import key_issuer -class KeyBundle: - def __init__(self, storage_conf=None): - self.storage_conf = storage_conf +class KeyIssuer: @staticmethod - def serialize(item: key_bundle.KeyBundle) -> dict: - _dict = item.dump() - return _dict - - def deserialize(self, spec: dict) -> key_bundle.KeyBundle: - bundle = key_bundle.KeyBundle(storage_conf=self.storage_conf).load(spec) - return bundle - + def serialize(item: key_issuer.KeyIssuer) -> str: + """ Convert from KeyIssuer to JSON """ + return json.dumps(item.dump()) + + def deserialize(self, spec: str) -> key_issuer.KeyIssuer: + """ Convert from JSON to KeyIssuer """ + _dict = json.loads(spec) + issuer = key_issuer.KeyIssuer().load(_dict) + return issuer -class KeyIssuer: - def __init__(self, storage_conf=None): - self.storage_conf = storage_conf +class QUOTE: @staticmethod - def serialize(item: key_issuer.KeyIssuer) -> dict: - _dict = item.dump() - return _dict + def serialize(item: str) -> str: + return quote_plus(item) - def deserialize(self, spec: dict) -> key_issuer.KeyIssuer: - issuer = key_issuer.KeyIssuer(storage_conf=self.storage_conf).load(spec) - return issuer + def deserialize(self, spec: str) -> str: + return unquote_plus(spec) diff --git a/src/cryptojwt/utils.py b/src/cryptojwt/utils.py index 023c909..abeb727 100644 --- a/src/cryptojwt/utils.py +++ b/src/cryptojwt/utils.py @@ -1,4 +1,5 @@ import base64 +import importlib import json import re import struct @@ -201,3 +202,28 @@ def deser(val): _val = val return base64_to_long(_val) + +def modsplit(name): + """Split importable""" + if ':' in name: + _part = name.split(':') + if len(_part) != 2: + raise ValueError("Syntax error: {s}") + return _part[0], _part[1] + + _part = name.split('.') + if len(_part) < 2: + raise ValueError("Syntax error: {s}") + + return '.'.join(_part[:-1]), _part[-1] + + +def importer(name): + """Import by name""" + _part = modsplit(name) + module = importlib.import_module(_part[0]) + return getattr(module, _part[1]) + + +def qualified_name(cls): + return cls.__module__ + "." + cls.__name__ \ No newline at end of file diff --git a/tests/test_02_jwk.py b/tests/test_02_jwk.py index b6d87e1..ea1e6c2 100644 --- a/tests/test_02_jwk.py +++ b/tests/test_02_jwk.py @@ -12,15 +12,17 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric.ec import generate_private_key + from cryptojwt.exception import DeSerializationNotPossible from cryptojwt.exception import UnsupportedAlgorithm from cryptojwt.exception import WrongUsage from cryptojwt.jwk import JWK +from cryptojwt.jwk import calculate_x5t from cryptojwt.jwk import certificate_fingerprint from cryptojwt.jwk import pem_hash from cryptojwt.jwk import pems_to_x5c -from cryptojwt.jwk.ec import NIST2SEC from cryptojwt.jwk.ec import ECKey +from cryptojwt.jwk.ec import NIST2SEC from cryptojwt.jwk.hmac import SYMKey from cryptojwt.jwk.hmac import new_sym_key from cryptojwt.jwk.hmac import sha256_digest @@ -29,7 +31,6 @@ from cryptojwt.jwk.jwk import jwk_wrap from cryptojwt.jwk.jwk import key_from_jwk_dict from cryptojwt.jwk.rsa import RSAKey -from cryptojwt.jwk.rsa import generate_and_store_rsa_key from cryptojwt.jwk.rsa import import_private_rsa_key_from_file from cryptojwt.jwk.rsa import import_public_rsa_key_from_file from cryptojwt.jwk.rsa import import_rsa_key_from_cert_file @@ -627,7 +628,7 @@ def test_dump_load(): def test_key_ops(): sk = SYMKey( key='df34db91c16613deba460752522d28f6ebc8a73d0d9185836270c26b', - alg = "HS256", + alg="HS256", key_ops=["sign", "verify"] ) @@ -639,9 +640,9 @@ def test_key_ops_and_use(): with pytest.raises(ValueError): SYMKey( key='df34db91c16613deba460752522d28f6ebc8a73d0d9185836270c26b', - alg = "HS256", + alg="HS256", key_ops=["sign", "verify"], - use = "sig" + use="sig" ) @@ -651,7 +652,9 @@ def test_pem_to_x5c(): x5c = pems_to_x5c([cert_chain]) assert len(x5c) == 1 - assert x5c[0] == 'MIIB2jCCAUOgAwIBAgIBATANBgkqhkiG9w0BAQUFADA0MRgwFgYDVQQDEw9UaGUgY29kZSB0ZXN0ZXIxGDAWBgNVBAoTD1VtZWEgVW5pdmVyc2l0eTAeFw0xMjEwMDQwMDIzMDNaFw0xMzEwMDQwMDIzMDNaMDIxCzAJBgNVBAYTAlNFMSMwIQYDVQQDExpPcGVuSUQgQ29ubmVjdCBUZXN0IFNlcnZlcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwf+wiusGhA+gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB/87ds3dy3Rfym/GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8CAwEAATANBgkqhkiG9w0BAQUFAAOBgQCsTntG4dfW5kO/Qle6uBhIhZU+3IreIPmbwzpXoCbcgjRa01z6WiBLwDC1RLAL7ucaF/EVlUq4e0cNXKt4ESGNc1xHISOMLetwvS1SN5tKWA9HNua/SaqRtiShxLUjPjmrtpUgotLNDRvUYnTdTT1vhZar7TSPr1yObirjvz/qLw==' + assert x5c[ + 0] == \ + 'MIIB2jCCAUOgAwIBAgIBATANBgkqhkiG9w0BAQUFADA0MRgwFgYDVQQDEw9UaGUgY29kZSB0ZXN0ZXIxGDAWBgNVBAoTD1VtZWEgVW5pdmVyc2l0eTAeFw0xMjEwMDQwMDIzMDNaFw0xMzEwMDQwMDIzMDNaMDIxCzAJBgNVBAYTAlNFMSMwIQYDVQQDExpPcGVuSUQgQ29ubmVjdCBUZXN0IFNlcnZlcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwf+wiusGhA+gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB/87ds3dy3Rfym/GUSc5B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8CAwEAATANBgkqhkiG9w0BAQUFAAOBgQCsTntG4dfW5kO/Qle6uBhIhZU+3IreIPmbwzpXoCbcgjRa01z6WiBLwDC1RLAL7ucaF/EVlUq4e0cNXKt4ESGNc1xHISOMLetwvS1SN5tKWA9HNua/SaqRtiShxLUjPjmrtpUgotLNDRvUYnTdTT1vhZar7TSPr1yObirjvz/qLw==' def test_pem_hash(): @@ -664,7 +667,8 @@ def test_certificate_fingerprint(): der = cert_file.read() res = certificate_fingerprint(der) - assert res == '01:DF:F1:D4:5F:21:7B:2E:3A:A2:D8:CA:13:4C:41:66:03:A1:EF:3E:7B:5E:8B:69:04:5E:80:8B:55:49:F1:48' + assert res == '01:DF:F1:D4:5F:21:7B:2E:3A:A2:D8:CA:13:4C:41:66:03:A1:EF:3E:7B:5E:8B:69:04:5E' \ + ':80:8B:55:49:F1:48' res = certificate_fingerprint(der, 'sha1') assert res == 'CA:CF:21:9E:72:00:CD:1C:CA:FD:4F:6D:84:6B:9E:E8:74:80:47:64' @@ -676,6 +680,17 @@ def test_certificate_fingerprint(): certificate_fingerprint(der, 'foo') -def test_generate_and_store_rsa_key(): - priv_key = generate_and_store_rsa_key(filename=full_path('temp_rsa.key')) +# def test_generate_and_store_rsa_key(): +# priv_key = generate_and_store_rsa_key(filename=full_path('temp_rsa.key')) + + +def test_x5t_calculation(): + with open(full_path('cert.der'), 'rb') as cert_file: + der = cert_file.read() + + x5t = calculate_x5t(der) + assert x5t == b'Q0FDRjIxOUU3MjAwQ0QxQ0NBRkQ0RjZEODQ2QjlFRTg3NDgwNDc2NA==' + x5t_s256 = calculate_x5t(der, 'sha256') + assert x5t_s256 == \ + b'MDFERkYxRDQ1RjIxN0IyRTNBQTJEOENBMTM0QzQxNjYwM0ExRUYzRTdCNUU4QjY5MDQ1RTgwOEI1NTQ5RjE0OA==' diff --git a/tests/test_40_serialize.py b/tests/test_40_serialize.py index 034dc5c..e9b9341 100644 --- a/tests/test_40_serialize.py +++ b/tests/test_40_serialize.py @@ -7,8 +7,6 @@ from cryptojwt.key_bundle import rsa_init from cryptojwt.key_issuer import KeyIssuer from cryptojwt.serialize import item -from cryptojwt.serialize.item import JWK -from cryptojwt.serialize.item import KeyBundle def full_path(local_file): @@ -21,26 +19,6 @@ def full_path(local_file): CERT = full_path("cert.pem") -def test_jwks(): - _key = RSAKey() - _key.load_key(import_rsa_key_from_cert_file(CERT)) - - _item = JWK().serialize(_key) - _nkey = JWK().deserialize(_item) - assert _key == _nkey - - -def test_key_bundle(): - kb = rsa_init({'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) - _sym = SYMKey(**{"kty": "oct", "key": "highestsupersecret", "use": "enc"}) - kb.append(_sym) - _item = KeyBundle().serialize(kb) - _nkb = KeyBundle().deserialize(_item) - assert len(kb) == 3 - assert len(kb.get('rsa')) == 2 - assert len(kb.get('oct')) == 1 - - def test_key_issuer(): kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", ["sig"]) assert len(kb) == 1 From 03ca4ae57edba079d59cd384cd6215e5b4a07877 Mon Sep 17 00:00:00 2001 From: roland Date: Sat, 6 Jun 2020 20:05:14 +0200 Subject: [PATCH 28/50] Remove aslist tests. --- aslist_tests/private_jwks.json | 1 - aslist_tests/rsa.key | 15 - aslist_tests/test_45_key_jar.py | 954 -------------------- aslist_tests/test_keys/cert.key | 27 - aslist_tests/test_keys/ec-p256-private.pem | 6 - aslist_tests/test_keys/ec-p256-public.pem | 5 - aslist_tests/test_keys/ec-p256.json | 8 - aslist_tests/test_keys/ec-p384-private.pem | 7 - aslist_tests/test_keys/ec-p384-public.pem | 6 - aslist_tests/test_keys/ec-p384.json | 8 - aslist_tests/test_keys/ec.key | 5 - aslist_tests/test_keys/jwk.json | 12 - aslist_tests/test_keys/rsa-1024-private.pem | 17 - aslist_tests/test_keys/rsa-1024-public.pem | 7 - aslist_tests/test_keys/rsa-1024.json | 9 - aslist_tests/test_keys/rsa-1280-private.pem | 20 - aslist_tests/test_keys/rsa-1280-public.pem | 8 - aslist_tests/test_keys/rsa-1280.json | 9 - aslist_tests/test_keys/rsa-2048-private.pem | 29 - aslist_tests/test_keys/rsa-2048-public.pem | 10 - aslist_tests/test_keys/rsa-2048.json | 9 - aslist_tests/test_keys/rsa-3072-private.pem | 41 - aslist_tests/test_keys/rsa-3072-public.pem | 12 - aslist_tests/test_keys/rsa-3072.json | 9 - aslist_tests/test_keys/rsa-4096-private.pem | 53 -- aslist_tests/test_keys/rsa-4096-public.pem | 15 - aslist_tests/test_keys/rsa-4096.json | 9 - aslist_tests/test_keys/rsa.key | 15 - 28 files changed, 1326 deletions(-) delete mode 100644 aslist_tests/private_jwks.json delete mode 100644 aslist_tests/rsa.key delete mode 100755 aslist_tests/test_45_key_jar.py delete mode 100755 aslist_tests/test_keys/cert.key delete mode 100644 aslist_tests/test_keys/ec-p256-private.pem delete mode 100644 aslist_tests/test_keys/ec-p256-public.pem delete mode 100644 aslist_tests/test_keys/ec-p256.json delete mode 100644 aslist_tests/test_keys/ec-p384-private.pem delete mode 100644 aslist_tests/test_keys/ec-p384-public.pem delete mode 100644 aslist_tests/test_keys/ec-p384.json delete mode 100644 aslist_tests/test_keys/ec.key delete mode 100755 aslist_tests/test_keys/jwk.json delete mode 100644 aslist_tests/test_keys/rsa-1024-private.pem delete mode 100644 aslist_tests/test_keys/rsa-1024-public.pem delete mode 100644 aslist_tests/test_keys/rsa-1024.json delete mode 100644 aslist_tests/test_keys/rsa-1280-private.pem delete mode 100644 aslist_tests/test_keys/rsa-1280-public.pem delete mode 100644 aslist_tests/test_keys/rsa-1280.json delete mode 100644 aslist_tests/test_keys/rsa-2048-private.pem delete mode 100644 aslist_tests/test_keys/rsa-2048-public.pem delete mode 100644 aslist_tests/test_keys/rsa-2048.json delete mode 100644 aslist_tests/test_keys/rsa-3072-private.pem delete mode 100644 aslist_tests/test_keys/rsa-3072-public.pem delete mode 100644 aslist_tests/test_keys/rsa-3072.json delete mode 100644 aslist_tests/test_keys/rsa-4096-private.pem delete mode 100644 aslist_tests/test_keys/rsa-4096-public.pem delete mode 100644 aslist_tests/test_keys/rsa-4096.json delete mode 100755 aslist_tests/test_keys/rsa.key diff --git a/aslist_tests/private_jwks.json b/aslist_tests/private_jwks.json deleted file mode 100644 index e4a3b0a..0000000 --- a/aslist_tests/private_jwks.json +++ /dev/null @@ -1 +0,0 @@ -{"keys": [{"kty": "RSA", "use": "sig", "kid": "dTZKdDFabEJoSEVKaGQ4anRLd2x3YTJWQW93ejVGUWV3Z0JJZFpvZnhUTQ", "n": "1uPVViKYxyTJ1B1_wiQCSQJrwkLKXQTg6zYy2I1JX3W3gq6i5QD8XxShR62GFdcOj0NkqR2ZiyobVqhKJ3TKErRH1mtxjFuEf-o9h6B5j6Rou6GN4TNv4h-Ed9mG5KyTI364aLlpVT-LcthnihUYwgaq7iKN-OQQD0FHPa0HBouk4QJTLRIkIF2QmSRGAJlnFevzZr0O9vAycGJH1ksTsUioP-oeNtxThlQEOi0lIF5dPGI-ZcQDpdhAHj-C5iBiyi5s4_yFM2UXXF2ZkIVkezfWPKKSQPq7yfCnFAtdeQ5stpfpG0mMXtv1zfTk-Y8-WYMnExwen7F024ZCavjlqw", "e": "AQAB", "d": "JJe3hGtvyLmjBNPhJZYsLXKUFwh4nU5vXp5kGiw1CmRpU3-ZjZWVZDuHG0WZR67Pc-XuBj5cHy6UaTVPK1jf8D9y3Dh_pX8QGRgyUh4plSRSEWF5X5f6vW7Qh_gq2FXq2GiDzpGENlgTzwK63vCovqGUCekoc_GiKnbbQs1sHNjq0WKCSrXZcfsdpdjsjksiQmmfdblPplTuYG4t2GvfYzGXgepUw2LLyJ8KCdSsXyebK9-J8laxk3El0wFbxbI2C7Y8H5BPCN7Cp6zcaenYkYYV7IGy1Z21kv-lTaBXzu23PO5vhH2bzyklvG_a0RbmrrTHr5OCWnl1-BALuYfpQQ", "p": "8_tdp7rJyRrpq4wEPxJrcOvrcrknMG03cjEePV5ozRFFeBVg2nkuESR-JHoxgF3bBOzyurB5eVDT11seZEiisThMzk6OZSnoNsDgk3WTm0c_Yl7qn0dwZANOCYcEIOUe9ouR3kGr1iJ3DZ9o7tYLJ3sb6-oCH1Cakf53iR_RXy8", "q": "4XmdnKVLvzkZP4A4YQ2ROquoAbPOoomd2aHkGndLP9tDuPxur4PiZVrMFjuzMob2uJMW7Y1FT-0WLk9aCVl0P6f893RL1yfRZVKlHopgYxe9jshwk-5VawdLn6HRtbZ-F8gp6KZMaK8oyTk-50CnsizeACcxcVEFQkFNB_v6IkU"}, {"kty": "EC", "use": "sig", "kid": "WDQzSTBkY21kZVZ0YXAzbXZVTUFPQTdLWXpQQ3QzODF6dHJMQk5LSlBpRQ", "crv": "P-256", "x": "PhbU6LJ7Xc75T8YvCcmuBLfWGQ91AVakWlbr0Dk3wgo", "y": "Gg_W4TuhRB4nN8jAXWuOyEg36gbKErLL406Ngh6Cgk8", "d": "y4Sle7C9FHcBnXInlnIjPjKx73Djq0YDpH7FgCihA-g"}, {"kty": "EC", "use": "sig", "kid": "aVdwOVAyVWlzYUhwMHpGRVgxc2hkRDRXWGdIQUp1TVJfc1diVkdjb2w4VQ", "crv": "P-384", "x": "pwaV9kgGNyu7mEkCxj_lRssXgmT-kdIhoK-8fhJn6s1tbEEgBbPQj8Pubqdq_fld", "y": "9VQIWfNfusIb2ZDxwViSzGMNeT_QZCu16HqHx8s5F_l--VfTwqdP0NTtWogT-soO", "d": "kHsAXZL6uhzVCKZQzU0iRiH3SnY9a6MufFIJW9jLL_RSyC5QbUasQGRlZm9j2gSI"}]} \ No newline at end of file diff --git a/aslist_tests/rsa.key b/aslist_tests/rsa.key deleted file mode 100644 index d34432d..0000000 --- a/aslist_tests/rsa.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQD6vqn19W/VB215DBADRakfPmCtFBf8/+YyhGqixWIwDiEl/L6L -w5HKZCUPVgrC0ADhJfvAbn4fte5MWBCTkqgepKL3BySMA0LMaBF12pbHlPSUbmQG -BJmTX4NNXuUel6TbPYJAU2Nh5Nan0Mb7Bmb8QpFvS0Hw7qZRW8y2eIttfwIDAQAB -AoGBAJVf9FxkRKUB8cOE3h006JWGUY2KROghgn9hxy0ErYO3RyQcN1+HuFh75GAI -gAyiYYO/XwS6TkSR2057wBRJ8ABzcL3+v5g+16Vbh0BjXVE+cv1WGdNGujyzl6ji -jlyF4cb6tXDyqWTLkMAtV20NfO/CGsfii6YEkZb2P90usthRAkEA/oG7a9EvQ7eR -gSEqppzW7KCwidPjnZTr/ROIZQU33nwkIJ0ElTjMNYKP8DerSuixR9skw2ZY8Q8I -1PTBnocHwwJBAPw3SAQYwxZwQMu1trVPMNOGIbSY4rQlMZGXrCZSu/TnozczFLA8 -qNM84g5veyJOzHKmYkIsMG1gwg5VNniG45UCQF6SlLOW0upl70K9sVyiUVcyywcc -Xqty6FJtjLSFQOKC3OXlkwtkRLXpo1UPSq6WUzIxY7LceFZzUMPZg41F/gMCQHNr -POqbBlPzZMOUUZthNP/nhu8lc8Fqr+dnmGElRVxK0JdHKfWInN2mI/DlNV064Dar -S5XqsPKs78EtX7MCT40CQFQZiry8m7ROubOU4+HDG9o1w9zcKXCkmbD9hBCGvTAj -BQNuGE0DtC6FEWTs8bXybLM5yBRq1XiKLdmi5N+3n4g= ------END RSA PRIVATE KEY----- diff --git a/aslist_tests/test_45_key_jar.py b/aslist_tests/test_45_key_jar.py deleted file mode 100755 index a53f4ff..0000000 --- a/aslist_tests/test_45_key_jar.py +++ /dev/null @@ -1,954 +0,0 @@ -import copy -import json -import os -import shutil -import time - -import pytest -from abstorage.storages.absqlalchemy import AbstractStorageSQLAlchemy - -from cryptojwt.exception import JWKESTException -from cryptojwt.jwe.jwenc import JWEnc -from cryptojwt.jws.jws import JWS -from cryptojwt.jws.jws import factory -from cryptojwt.key_bundle import KeyBundle -from cryptojwt.key_bundle import keybundle_from_local_file -from cryptojwt.key_bundle import rsa_init -from cryptojwt.key_jar import KeyJar -from cryptojwt.key_jar import build_keyjar -from cryptojwt.key_jar import init_key_jar -from cryptojwt.key_jar import rotate_keys - -__author__ = 'Roland Hedberg' - -BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), - "test_keys")) -RSAKEY = os.path.join(BASE_PATH, "cert.key") -RSA0 = os.path.join(BASE_PATH, "rsa.key") -EC0 = os.path.join(BASE_PATH, "ec.key") -BASEDIR = os.path.abspath(os.path.dirname(__file__)) - - -def full_path(local_file): - return os.path.join(BASEDIR, local_file) - - -ABS_STORAGE_SQLALCHEMY = dict( - driver='sqlalchemy', - url='sqlite:///:memory:', - params=dict(table='Thing'), - handler=AbstractStorageSQLAlchemy -) - -ABS_STORAGE_FILE = { - 'handler': 'abstorage.storages.abfile.AbstractFileSystem', - 'fdir': 'keyjar', - 'key_conv': 'abstorage.converter.QPKey', - 'value_conv': 'cryptojwt.serialize.item.KeyIssuer', - 'label': 'keyjar' -} - - -JWK0 = { - "keys": [ - { - 'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", - 'n': - 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5' - 'B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8' - } - ] -} - -JWK1 = { - "keys": [ - { - "n": - "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" - "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" - "-NvS-aG_jN5cstVbCGWE20H0vF" - "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" - "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" - "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": "AQAB", "kty": "RSA", "kid": "rsa1" - }, - { - "k": - "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "oct" - }, - ] -} - -JWK2 = { - "keys": [ - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "kriMPdmBvx68skT8-mPAB3BseeA", - "kty": "RSA", - "n": - "kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1um" - "l94fLyq_AXbwo9yAduf4dCHTP8CWR1dnDR" - "-Qnz_4PYlWVEuuHHONOw_blbfdMjhY" - "-C_BYM2E3pRxbohBb3x__CfueV7ddz2LYiH3" - "wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_GTgWN8A" - "-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQt" - "SsjQtI--XsokxRDqYLwus1I1SihgbV_STTg5enufuw", - "use": "sig", - "x5c": [ - "MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb" - "2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb2" - "50cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipg" - "H0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6" - "/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Q" - "nz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x" - "//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13S" - "QwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp" - "+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5en" - "ufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJ" - "vbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdN" - "VGKCmSf8M65b8h0NwlIjGGGy" - "/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADD" - "kN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5" - "+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8y" - "PJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW" - "+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ" - ], - "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", - "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", - "kty": "RSA", - "n": - "vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhzh23V9Tkq" - "-RtwN1Vs_z57hO82kkzL-cQHZX3bMJ" - "D-GEGOKXCEXURN7VMyZWMAuzQoW9vFb1k3cR1RW_EW_P" - "-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5yCw5T_Vuwqqsio3" - "V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_KAS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IA" - "bsk1wRtWDndhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ", - "use": "sig", - "x5c": [ - "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" - "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" - "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" - "/uPhEf7zVizjfcr/ISGFe9+yUO" - "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" - "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" - "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" - "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" - "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" - "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" - "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" - "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" - "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" - "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" - "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" - "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" - "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" - "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" - ], - "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", - "kty": "RSA", - "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" - "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" - "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" - "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" - "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" - "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" - "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" - "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" - "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" - "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" - "+Iqzmn" - "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" - "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" - "qA==" - ], - "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" - }, - { - "e": "AQAB", - "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" - "-b112-36a304b66dad/v2.0/", - "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", - "kty": "RSA", - "n": - "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" - "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" - "-KXVD02jdmX2HNdYJdPyc0c", - "use": "sig", - "x5c": [ - "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" - "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" - "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" - "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" - "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" - "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" - "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" - "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" - "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" - "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" - "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" - "ig==" - ], - "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" - } - ] -} - -JWK_UK = { - "keys": [ - { - "n": - "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" - "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" - "-NvS-aG_jN5cstVbCGWE20H0vF" - "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" - "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" - "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", - "e": "AQAB", "kty": "RSA", "kid": "rsa1" - }, - { - "k": - "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", - "kty": "buz" - }, - ] -} - -JWK_FP = { - "keys": [ - {"e": "AQAB", "kty": "RSA", "kid": "rsa1"}, - ] -} - -JWKS_SPO = { - "keys": [ - { - "kid": - "BfxfnahEtkRBG3Hojc9XGLGht_5rDBj49Wh3sBDVnzRpulMqYwMRmpizA0aSPT1fhCHYivTiaucWUqFu_GwTqA", - "use": "sig", - "alg": "ES256", - "kty": "EC", - "crv": "P-256", - "x": "1XXUXq75gOPZ4bEj1o2Z5XKJWSs6LmL6fAOK3vyMzSc", - "y": "ac1h_DwyuUxhkrD9oKMJ-b_KuiVvvSARIwT-XoEmDXs" - }, - { - "kid": - "91pD1H81rXUvrfg9mkngIG-tXjnldykKUVbITDIU1SgJvq91b8clOcJuEHNAq61eIvg8owpEvWcWAtlbV2awyA", - "use": "sig", - "alg": "ES256", - "kty": "EC", - "crv": "P-256", - "x": "2DfQoLpZS2j3hHEcHDkzV8ISx-RdLt6Opy8YZYVm4AQ", - "y": "ycvkFMBIzgsowiaf6500YlG4vaMSK4OF7WVtQpUbEE0" - }, - { - "kid": "0sIEl3MUJiCxrqleEBBF-_bZq5uClE84xp-wpt8oOI" - "-WIeNxBjSR4ak_OTOmLdndB0EfDLtC7X1JrnfZILJkxA", - "use": "sig", - "alg": "RS256", - "kty": "RSA", - "n": - "yG9914Q1j63Os4jX5dBQbUfImGq4zsXJD4R59XNjGJlEt5ek6NoiDl0ucJO3_7_R9e5my2ONTSqZhtzFW6MImnIn8idWYzJzO2EhUPCHTvw_2oOGjeYTE2VltIyY_ogIxGwY66G0fVPRRH9tCxnkGOrIvmVgkhCCGkamqeXuWvx9MCHL_gJbZJVwogPSRN_SjA1gDlvsyCdA6__CkgAFcSt1sGgiZ_4cQheKexxf1-7l8R91ZYetz53drk2FS3SfuMZuwMM4KbXt6CifNhzh1Ye-5Tr_ZENXdAvuBRDzfy168xnk9m0JBtvul9GoVIqvCVECB4MPUb7zU6FTIcwRAw", - "e": "AQAB" - }, - { - "kid": - "zyDfdEU7pvH0xEROK156ik8G7vLO1MIL9TKyL631kSPtr9tnvs9XOIiq5jafK2hrGr2qqvJdejmoonlGqWWZRA", - "use": "sig", - "alg": "RS256", - "kty": "RSA", - "n": - "68be-nJp46VLj4Ci1V36IrVGYqkuBfYNyjQTZD_7yRYcERZebowOnwr3w0DoIQpl8iL2X8OXUo7rUW_LMzLxKx2hEmdJfUn4LL2QqA3KPgjYz8hZJQPG92O14w9IZ-8bdDUgXrg9216H09yq6ZvJrn5Nwvap3MXgECEzsZ6zQLRKdb_R96KFFgCiI3bEiZKvZJRA7hM2ePyTm15D9En_Wzzfn_JLMYgE_DlVpoKR1MsTinfACOlwwdO9U5Dm-5elapovILTyVTgjN75i-wsPU2TqzdHFKA-4hJNiWGrYPiihlAFbA2eUSXuEYFkX43ahoQNpeaf0mc17Jt5kp7pM2w", - "e": "AQAB" - }, - { - "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" - "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", - "use": "sig", - "alg": "EdDSA", - "kty": "OKP", - "crv": "Ed25519", - "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" - }, - { - "kid": - "bL33HthM3fWaYkY2_pDzUd7a65FV2R2LHAKCOsye8eNmAPDgRgpHWPYpWFVmeaujUUEXRyDLHN" - "-Up4QH_sFcmw", - "use": "sig", - "alg": "EdDSA", - "kty": "OKP", - "crv": "Ed25519", - "x": "CS01DGXDBPV9cFmd8tgFu3E7eHn1UcP7N1UCgd_JgZo" - } - ] -} - - -def test_build_keyjar(storage_conf=None): - keys = [ - {"type": "RSA", "use": ["enc", "sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - ] - - keyjar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - jwks = keyjar.export_jwks() - for key in jwks["keys"]: - assert "d" not in key # the JWKS shouldn't contain the private part - # of the keys - - assert len(keyjar.get_issuer_keys('')) == 3 # A total of 3 keys - assert len(keyjar.get('sig')) == 2 # 2 for signing - assert len(keyjar.get('enc')) == 1 # 1 for encryption - - -def test_build_keyjar_usage(): - keys = [ - {"type": "RSA", "use": ["enc", "sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]}, - {"type": "oct", "use": ["enc"]}, - {"type": "oct", "use": ["enc"]}, - ] - - keyjar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - jwks_sig = keyjar.export_jwks(usage='sig') - jwks_enc = keyjar.export_jwks(usage='enc') - assert len(jwks_sig.get('keys')) == 2 # A total of 2 keys with use=sig - assert len(jwks_enc.get('keys')) == 3 # A total of 3 keys with use=enc - - -def test_build_keyjar_missing(tmpdir): - keys = [ - { - "type": "RSA", "key": os.path.join(tmpdir.dirname, "missing_file"), - "use": ["enc", "sig"] - }] - - key_jar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - - assert key_jar is None - - -def test_build_RSA_keyjar_from_file(tmpdir): - keys = [ - { - "type": "RSA", "key": RSA0, - "use": ["enc", "sig"] - }] - - key_jar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - - assert len(key_jar.get_signing_key('rsa', '')) == 1 - - -def test_build_EC_keyjar_missing(tmpdir): - keys = [ - { - "type": "EC", "key": os.path.join(tmpdir.dirname, "missing_file"), - "use": ["enc", "sig"] - }] - - key_jar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - - assert key_jar is None - - -def test_build_EC_keyjar_from_file(tmpdir): - keys = [ - { - "type": "EC", "key": EC0, - "use": ["enc", "sig"] - }] - - key_jar = build_keyjar(keys, storage_conf=ABS_STORAGE_FILE) - - assert len(key_jar.get_issuer_keys("")) == 2 - - -class TestKeyJar(object): - @pytest.fixture(autouse=True) - def setup(self): - shutil.rmtree('keyjar') - self.keyjar = KeyJar(storage_conf=ABS_STORAGE_FILE) - - def test_keyjar_add(self): - kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"]) - self.keyjar.add_kb('https://issuer.example.com', kb) - assert list(self.keyjar.owners()) == ['https://issuer.example.com'] - - def test_add_item(self): - kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"]) - self.keyjar.add_kb('https://issuer.example.com', kb) - assert list(self.keyjar.owners()) == ['https://issuer.example.com'] - - def test_add_symmetric(self): - self.keyjar.add_symmetric('', 'abcdefghijklmnop', ['sig']) - assert list(self.keyjar.owners()) == [''] - assert len(self.keyjar.get_signing_key('oct', '')) == 1 - - def test_items(self): - self.keyjar.add_kb("", KeyBundle([{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, - {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org", KeyBundle([ - {"kty": "oct", "key": "0123456789012345", "use": "sig"}, - {"kty": "oct", "key": "1234567890123456", "use": "enc"}])) - - self.keyjar.add_kb("http://www.example.org", - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) - - assert len(self.keyjar) == 2 - - def test_issuer_extra_slash(self): - self.keyjar.add_kb("", KeyBundle( - [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, - {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org", KeyBundle([ - {"kty": "oct", "key": "0123456789012345", "use": "sig"}, - {"kty": "oct", "key": "1234567890123456", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org", - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) - - assert self.keyjar.get('sig', key_type='RSA', issuer_id='http://www.example.org/') - - def test_issuer_missing_slash(self): - self.keyjar.add_kb("", KeyBundle( - [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org/", KeyBundle([ - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org/", - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) - - assert self.keyjar.get('sig', key_type='RSA', issuer_id='http://www.example.org') - - def test_get_enc(self): - self.keyjar.add_kb("", KeyBundle( - [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org/", KeyBundle([ - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org/", - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) - - assert self.keyjar.get('enc', key_type='oct') - - def test_get_enc_not_mine(self): - self.keyjar.add_kb("", KeyBundle( - [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, - {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) - self.keyjar.add_kb("http://www.example.org/", KeyBundle([ - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "ver"}])) - self.keyjar.add_kb("http://www.example.org/", - keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) - - assert self.keyjar.get('enc', key_type='oct', issuer_id='http://www.example.org/') - - def test_dump_issuer_keys(self): - kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", - ["sig"]) - assert len(kb) == 1 - self.keyjar.add_kb('', kb) - _jwks_dict = self.keyjar.export_jwks() - - _info = _jwks_dict['keys'][0] - assert _info == { - 'use': 'sig', - 'e': 'AQAB', - 'kty': 'RSA', - 'alg': 'RS256', - 'n': 'pKybs0WaHU_y4cHxWbm8Wzj66HtcyFn7Fh3n' - '-99qTXu5yNa30MRYIYfSDwe9JVc1JUoGw41yq2StdGBJ40HxichjE' - '-Yopfu3B58Q' - 'lgJvToUbWD4gmTDGgMGxQxtv1En2yedaynQ73sDpIK-12JJDY55pvf' - '-PCiSQ9OjxZLiVGKlClDus44_uv2370b9IN2JiEOF-a7JB' - 'qaTEYLPpXaoKWDSnJNonr79tL0T7iuJmO1l705oO3Y0TQ' - '-INLY6jnKG_RpsvyvGNnwP9pMvcP1phKsWZ10ofuuhJGRp8IxQL9Rfz' - 'T87OvF0RBSO1U73h09YP-corWDsnKIi6TbzRpN5YDw', - 'kid': 'abc' - } - - def test_no_use(self): - kb = KeyBundle(JWK0["keys"]) - self.keyjar.add_kb("abcdefgh", kb) - enc_key = self.keyjar.get_encrypt_key("RSA", "abcdefgh") - assert enc_key != [] - - @pytest.mark.network - def test_provider(self): - self.keyjar.load_keys("https://connect-op.heroku.com", - jwks_uri="https://connect-op.herokuapp.com/jwks.json") - - assert self.keyjar.get_issuer_keys("https://connect-op.heroku.com") - - def test_import_jwks(self): - self.keyjar.import_jwks(JWK1, '') - assert len(self.keyjar.get_issuer_keys('')) == 2 - - def test_get_signing_key_use_undefined(self): - self.keyjar.import_jwks(JWK1, '') - keys = self.keyjar.get_signing_key(kid='rsa1') - assert len(keys) == 1 - - keys = self.keyjar.get_signing_key(key_type='rsa') - assert len(keys) == 1 - - keys = self.keyjar.get_signing_key(key_type='rsa', kid='rsa1') - assert len(keys) == 1 - - def test_load_unknown_keytype(self): - self.keyjar.import_jwks(JWK_UK, '') - assert len(self.keyjar.get_issuer_keys('')) == 1 - - def test_load_missing_key_parameter(self): - with pytest.raises(JWKESTException): - self.keyjar.import_jwks(JWK_FP, '') - - def test_load_spomky_keys(self): - self.keyjar.import_jwks(JWKS_SPO, '') - assert len(self.keyjar.get_issuer_keys('')) == 4 - - def test_get_ec(self): - self.keyjar.import_jwks(JWKS_SPO, '') - k = self.keyjar.get('sig', 'EC', alg='ES256') - assert k - - def test_get_ec_wrong_alg(self): - self.keyjar.import_jwks(JWKS_SPO, '') - k = self.keyjar.get('sig', 'EC', alg='ES512') - assert k == [] - - def test_keyjar_eq(self): - self.keyjar.import_jwks(JWKS_SPO, '') - - kj2 = KeyJar(storage_conf=ABS_STORAGE_FILE) - kj2.import_jwks(JWKS_SPO, '') - - assert self.keyjar == kj2 - - def test_keys_by_alg_and_usage(self): - self.keyjar.import_jwks(JWKS_SPO, '') - k = self.keyjar.keys_by_alg_and_usage('', 'RS256', 'sig') - assert len(k) == 2 - - def test_match_owner(self): - self.keyjar.add_kb('Alice', KeyBundle(JWK0['keys'])) - self.keyjar.add_kb('Bob', KeyBundle(JWK1['keys'])) - self.keyjar.add_kb('https://delphi.example.com/path', KeyBundle(JWK2['keys'])) - - a = self.keyjar.match_owner('https://delphi.example.com') - assert a == 'https://delphi.example.com/path' - - with pytest.raises(KeyError): - self.keyjar.match_owner('https://example.com') - - def test_str(self): - self.keyjar.add_kb('Alice', KeyBundle(JWK0['keys'])) - - desc = '{}'.format(self.keyjar) - assert desc == '{"Alice": "RSA::abc"}' - _cont = json.loads(desc) - assert set(_cont.keys()) == {'Alice'} - - def test_load_keys(self): - self.keyjar.load_keys('Alice', jwks=JWK1) - - assert self.keyjar.owners() == ['Alice'] - - def test_find(self): - _path = full_path('../tests/jwk_private_key.json') - kb = KeyBundle(source='file://{}'.format(_path)) - self.keyjar.add_kb('Alice', kb) - - assert self.keyjar.find('{}'.format(_path), 'Alice') - assert self.keyjar.find('https://example.com', 'Alice') == [] - assert self.keyjar.find('{}'.format(_path), 'Bob') == [] - - _res = self.keyjar.find('{}'.format(_path)) - assert set(_res.keys()) == {'Alice'} - - def test_get_decrypt_keys(self): - self.keyjar.add_kb('Alice', KeyBundle(JWK0['keys'])) - self.keyjar.add_kb('', KeyBundle(JWK1['keys'])) - self.keyjar.add_kb('C', KeyBundle(JWK2['keys'])) - - kb = rsa_init( - {'use': ['enc', 'sig'], 'size': 1024, 'name': 'rsa', 'path': 'keys'}) - self.keyjar.add_kb('', kb) - - jwt = JWEnc() - jwt.headers = {'alg': 'RS256'} - jwt.part = [{'alg': 'RS256'}, '{"aud": "Bob", "iss": "Alice"}', - 'aksjdhaksjbd'] - - keys = self.keyjar.get_jwt_decrypt_keys(jwt) - assert keys - - jwt.part = [{'alg': 'RS256'}, '{"iss": "Alice"}', 'aksjdhaksjbd'] - - keys = self.keyjar.get_jwt_decrypt_keys(jwt) - assert keys - - keys = self.keyjar.get_jwt_decrypt_keys(jwt, aud='Bob') - assert keys - - def test_update_keyjar(self): - _path = full_path('../tests/jwk_private_key.json') - kb = KeyBundle(source='file://{}'.format(_path)) - self.keyjar.add_kb('Alice', kb) - - self.keyjar.update() - - keys = self.keyjar.get_issuer_keys('Alice') - assert len(keys) == 1 - - -KEYDEFS = [ - {"type": "RSA", "key": '', "use": ["sig"]}, - {"type": "EC", "crv": "P-256", "use": ["sig"]} -] - - -def test_remove_after(): - shutil.rmtree('keyjar') - # initial keyjar - keyjar = build_keyjar(KEYDEFS, storage_conf=ABS_STORAGE_FILE) - _old = [k.kid for k in keyjar.get_issuer_keys('') if k.kid] - assert len(_old) == 2 - - keyjar.remove_after = 1 - # rotate_keys = create new keys + make the old as inactive - rotate_keys(KEYDEFS, keyjar=keyjar) - - keyjar.remove_outdated(time.time() + 3600) - - # The remainder are the new keys - _new = [k.kid for k in keyjar.get_issuer_keys('') if k.kid] - assert len(_new) == 2 - - # should not be any overlap between old and new - assert set(_new).intersection(set(_old)) == set() - - -class TestVerifyJWTKeys(object): - @pytest.fixture(autouse=True) - def setup(self): - shutil.rmtree('keyjar') - - mkey = [ - {"type": "RSA", "use": ["sig"]}, - {"type": "RSA", "use": ["sig"]}, - {"type": "RSA", "use": ["sig"]}, - ] - - skey = [ - {"type": "RSA", "use": ["sig"]}, - ] - - # Alice has multiple keys - _conf = copy.deepcopy(ABS_STORAGE_FILE) - _conf['label'] = '{}{}'.format(_conf['label'], 'Alice') - self.alice_keyjar = build_keyjar(mkey, storage_conf=_conf) - # Bob has one single keys - _conf = copy.deepcopy(ABS_STORAGE_FILE) - _conf['label'] = '{}{}'.format(_conf['label'], 'Bob') - self.bob_keyjar = build_keyjar(skey, storage_conf=_conf) - self.alice_keyjar.import_jwks(self.alice_keyjar[''].export_jwks(), 'Alice') - self.bob_keyjar.import_jwks(self.bob_keyjar[''].export_jwks(), 'Bob') - - # To Alice's keyjar add Bob's public keys - self.alice_keyjar.import_jwks( - self.bob_keyjar.export_jwks(issuer_id='Bob'), 'Bob') - - # To Bob's keyjar add Alice's public keys - self.bob_keyjar.import_jwks( - self.alice_keyjar.export_jwks(issuer_id='Alice'), 'Alice') - - _jws = JWS('{"aud": "Bob", "iss": "Alice"}', alg='RS256') - sig_key = self.alice_keyjar.get_signing_key('rsa', owner='Alice')[0] - self.sjwt_a = _jws.sign_compact([sig_key]) - - _jws = JWS('{"aud": "Alice", "iss": "Bob"}', alg='RS256') - sig_key = self.bob_keyjar.get_signing_key('rsa', owner='Bob')[0] - self.sjwt_b = _jws.sign_compact([sig_key]) - - def test_no_kid_multiple_keys(self): - """ This is extremely strict """ - _jwt = factory(self.sjwt_a) - # remove kid reference - _jwt.jwt.headers['kid'] = '' - keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt) - assert len(keys) == 0 - keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, allow_missing_kid=True) - assert len(keys) == 3 - - def test_no_kid_single_key(self): - _jwt = factory(self.sjwt_b) - _jwt.jwt.headers['kid'] = '' - keys = self.alice_keyjar.get_jwt_verify_keys(_jwt.jwt) - assert len(keys) == 1 - - def test_no_kid_multiple_keys_no_kid_issuer(self): - a_kids = [k.kid for k in - self.alice_keyjar.get_verify_key(owner='Alice', key_type='RSA')] - no_kid_issuer = {'Alice': a_kids} - _jwt = factory(self.sjwt_a) - _jwt.jwt.headers['kid'] = '' - keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, no_kid_issuer=no_kid_issuer) - assert len(keys) == 3 - - def test_no_kid_multiple_keys_no_kid_issuer_lim(self): - no_kid_issuer = {'Alice': []} - _jwt = factory(self.sjwt_a) - _jwt.jwt.headers['kid'] = '' - keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, no_kid_issuer=no_kid_issuer) - assert len(keys) == 3 - - def test_matching_kid(self): - _jwt = factory(self.sjwt_b) - keys = self.alice_keyjar.get_jwt_verify_keys(_jwt.jwt) - assert len(keys) == 1 - - def test_no_matching_kid(self): - _jwt = factory(self.sjwt_b) - _jwt.jwt.headers['kid'] = 'abcdef' - keys = self.alice_keyjar.get_jwt_verify_keys(_jwt.jwt) - assert keys == [] - - def test_aud(self): - self.alice_keyjar.import_jwks(JWK1, issuer_id='D') - self.bob_keyjar.import_jwks(JWK1, issuer_id='D') - - _jws = JWS('{"iss": "D", "aud": "A"}', alg='HS256') - sig_key = self.alice_keyjar.get_signing_key('oct', issuer_id='D')[0] - _sjwt = _jws.sign_compact([sig_key]) - - no_kid_issuer = {'D': []} - - _jwt = factory(_sjwt) - - keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, no_kid_issuer=no_kid_issuer) - assert len(keys) == 1 - - -class TestDiv(object): - @pytest.fixture(autouse=True) - def setup(self): - shutil.rmtree('keyjar') - self.keyjar = KeyJar(storage_conf=ABS_STORAGE_FILE) - self.keyjar.add_kb('Alice', KeyBundle(JWK0['keys'])) - self.keyjar.add_kb('Bob', KeyBundle(JWK1['keys'])) - self.keyjar.add_kb('C', KeyBundle(JWK2['keys'])) - - def test_copy(self): - kjc = self.keyjar.copy() - - assert set(kjc.owners()) == {'Alice', 'Bob', 'C'} - - assert len(kjc.get('sig', 'oct', 'Alice')) == 0 - assert len(kjc.get('sig', 'rsa', 'Alice')) == 1 - - assert len(kjc.get('sig', 'oct', 'Bob')) == 1 - assert len(kjc.get('sig', 'rsa', 'Bob')) == 1 - - assert len(kjc.get('sig', 'oct', 'C')) == 0 - assert len(kjc.get('sig', 'rsa', 'C')) == 4 - - def test_repr(self): - txt = self.keyjar.__repr__() - assert " Date: Sat, 6 Jun 2020 20:07:10 +0200 Subject: [PATCH 29/50] Removed duplicated exceptions. --- src/cryptojwt/exception.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/cryptojwt/exception.py b/src/cryptojwt/exception.py index 596c52e..ae073ea 100644 --- a/src/cryptojwt/exception.py +++ b/src/cryptojwt/exception.py @@ -99,18 +99,10 @@ class WrongKeyType(JWKESTException): pass -class UnknownKeyType(JWKESTException): - pass - - class UnsupportedKeyType(JWKESTException): pass -class UpdateFailed(JWKESTException): - pass - - class WrongUsage(JWKESTException): pass From 3e45bcf15d5d393d061f46bd2ac9b2c40e2a39d3 Mon Sep 17 00:00:00 2001 From: roland Date: Sat, 6 Jun 2020 20:09:01 +0200 Subject: [PATCH 30/50] No dependency on oidcmsg. KeyJar prepared to be able to use storage system defined on oidcmsg. --- src/cryptojwt/key_jar.py | 19 ++++++++++++------- src/cryptojwt/serialize/item.py | 1 - 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 5a1a47d..06a75a0 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -4,7 +4,6 @@ from typing import List from typing import Optional -from abstorage.base import LabeledAbstractStorage from requests import request from .jwe.jwe import alg2keytype as jwe_alg2keytype @@ -40,7 +39,7 @@ class KeyJar(object): def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, remove_after=3600, httpc=None, httpc_params=None, storage_conf=None, - abstract_storage_cls=LabeledAbstractStorage): + abstract_storage_cls=None): """ KeyJar init function @@ -56,6 +55,8 @@ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, if storage_conf is None: self._issuers = {} else: + if not abstract_storage_cls: + raise ValueError('Missing storage class specification') self._issuers = abstract_storage_cls(storage_conf) self.storage_conf = storage_conf @@ -407,7 +408,7 @@ def export_jwks(self, private=False, issuer_id="", usage=None): """ _issuer = self._get_issuer(issuer_id=issuer_id) if _issuer is None: - return {} + return {"keys": []} keys = [] for kb in _issuer: @@ -437,11 +438,12 @@ def import_jwks(self, jwks, issuer_id): _keys = jwks["keys"] except KeyError: raise ValueError('Not a proper JWKS') - else: + + if _keys: _issuer = self.return_issuer(issuer_id=issuer_id) _issuer.add(self.keybundle_cls(_keys, httpc=self.httpc, httpc_params=self.httpc_params)) - self[issuer_id] = _issuer + self[issuer_id] = _issuer def import_jwks_as_json(self, jwks, issuer_id): """ @@ -776,7 +778,7 @@ def build_keyjar(key_conf, kid_template="", keyjar=None, issuer_id='', storage_c def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', read_only=True, - storage_conf=None): + storage_conf=None, abstract_storage_cls=None): """ A number of cases here: @@ -888,7 +890,10 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea else: _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) - keyjar = KeyJar(storage_conf=storage_conf) + if _issuer is None: + raise ValueError('Could not find any keys') + + keyjar = KeyJar(storage_conf=storage_conf, abstract_storage_cls=abstract_storage_cls) keyjar[issuer_id] = _issuer return keyjar diff --git a/src/cryptojwt/serialize/item.py b/src/cryptojwt/serialize/item.py index 513dbf3..1556f82 100644 --- a/src/cryptojwt/serialize/item.py +++ b/src/cryptojwt/serialize/item.py @@ -6,7 +6,6 @@ class KeyIssuer: - @staticmethod def serialize(item: key_issuer.KeyIssuer) -> str: """ Convert from KeyIssuer to JSON """ From f015370f429b3c5f9cc3aad5c96438ad87ca672f Mon Sep 17 00:00:00 2001 From: roland Date: Sat, 6 Jun 2020 20:21:41 +0200 Subject: [PATCH 31/50] A bit of documentation. --- src/cryptojwt/key_jar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 06a75a0..9cc879d 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -49,6 +49,9 @@ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle, :param remove_after: How long keys marked as inactive will remain in the key Jar. :param httpc: A HTTP client to use. Default is Requests request. :param httpc_params: HTTP request parameters + :param storage_conf: Storage configuration + :param abstract_storage_cls: Storage class. The only demand on a storage class is that it + should behave like a dictionary. :return: Keyjar instance """ From eb73ee891f755223fbd8db729da010fcafd22884 Mon Sep 17 00:00:00 2001 From: roland Date: Mon, 8 Jun 2020 19:29:56 +0200 Subject: [PATCH 32/50] Missing file --- tests/cert.der | Bin 0 -> 478 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/cert.der diff --git a/tests/cert.der b/tests/cert.der new file mode 100644 index 0000000000000000000000000000000000000000..a0663c6944c110904a48957120f609bf214eb059 GIT binary patch literal 478 zcmXqLV!UP0#OS<$nTe5!iILHOmyJ`a&7D}#ZFp@e}L8*?ZNvoL>1Myf(` zeoCrBNosLPY7tC^OPD`2H#JcqG%vF(wWv6=q|!i6oY&CE(7?dNz`)4Zz&HxXHAdnZ z7#VUKaDq%_6J`o_HB>fGgjgZvUyz#T>7wAApO=@KT%r&Hv`8U1wFqdgLF0U6w=uFZ zFgNxx7&LY=H8wIF{J){=HCqe+f~n6U9GLr^fm0MdRM4OZEU)oY;ua34xe-83w`^Og8Nz;|DU_NxxDDE_@~()r+A)oyc8SU;3ZwG zHgEZf2Ro(MxAnC}lpm^DwIZ-B>%o2f_NLQ@JlyVU9=({I_c`<6Gk+##Mh5htV+Oiw zjbF9f!|T_cIq!E0-?c-+qjjp?ovwQdKW87d3SXdhr^zJha?GzN1@8j}TU|DAzk4nv z{!w(Q*N$p;-k8-Df{MMxG46`WJ-V+8_UZs-*eT(Z7K_oY*n_iT)l0o j!lFxOd8Mx;mE85U&2OEy`mIU-`k1~vt;hTAU+Dt?g)Xk| literal 0 HcmV?d00001 From 467254c6a016f87b14b1cd8980f7d1d174258eff Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 9 Jun 2020 13:06:07 +0200 Subject: [PATCH 33/50] Clearer demarcation between KeyIssuer and KeyJar. Set of tests for KeyIssuer. --- src/cryptojwt/key_issuer.py | 147 ++++++++ src/cryptojwt/key_jar.py | 180 +++------- tests/test_04_key_issuer.py | 686 ++++++++++++++++++++++++++++++++++++ tests/test_04_key_jar.py | 4 +- 4 files changed, 890 insertions(+), 127 deletions(-) create mode 100755 tests/test_04_key_issuer.py diff --git a/src/cryptojwt/key_issuer.py b/src/cryptojwt/key_issuer.py index 4528e0d..26727f8 100755 --- a/src/cryptojwt/key_issuer.py +++ b/src/cryptojwt/key_issuer.py @@ -1,5 +1,6 @@ import json import logging +import os from requests import request @@ -10,6 +11,9 @@ __author__ = 'Roland Hedberg' +from .key_bundle import key_diff +from .key_bundle import update_key_bundle + from .utils import importer from .utils import qualified_name @@ -424,6 +428,23 @@ def __iter__(self): for bundle in self._bundles: yield bundle + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + + if len(other.all_keys()) != len(self.all_keys()): + return False + + for k in self.all_keys(): + if k not in other: + return False + + for k in other.all_keys(): + if k not in self: + return False + + return True + # ============================================================================= @@ -479,3 +500,129 @@ def build_keyissuer(key_conf, kid_template="", key_issuer=None, issuer_id=''): key_issuer.add(bundle) return key_issuer + + +def rotate_keys(key_conf, issuer, kid_template=""): + new_keys = build_keyissuer(key_conf, kid_template) + issuer.mark_all_keys_as_inactive() + for kb in new_keys: + issuer.add_kb(kb) + return issuer + + +def init_key_issuer(public_path='', private_path='', key_defs='', read_only=True, + storage_conf=None, abstract_storage_cls=None): + """ + A number of cases here: + + 1. A private path is given + + a. The file exists and a JWKS is found there. + From that JWKS a KeyJar instance is built. + b. + If the private path file doesn't exit the key definitions are + used to build a KeyJar instance. A JWKS with the private keys are + written to the file named in private_path. + + If a public path is also provided a JWKS with public keys are written + to that file. + + 2. A public path is given but no private path. + + a. If the public path file exists then the JWKS in that file is used to + construct a KeyJar. + b. If no such file exists then a KeyJar will be built + based on the key_defs specification and a JWKS with the public keys + will be written to the public path file. + + 3. If neither a public path nor a private path is given then a KeyJar is + built based on the key_defs specification and no JWKS will be written + to file. + + In all cases a KeyJar instance is returned + + The keys stored in the KeyJar will be stored under the '' identifier. + + :param public_path: A file path to a file that contains a JWKS with public + keys + :param private_path: A file path to a file that contains a JWKS with + private keys. + :param key_defs: A definition of what keys should be created if they are + not already available + :param read_only: This function should not attempt to write anything + to a file system. + :return: An instantiated :py:class;`oidcmsg.key_jar.KeyJar` instance + """ + + if private_path: + if os.path.isfile(private_path): + _jwks = open(private_path, 'r').read() + _issuer = KeyIssuer() + _issuer.import_jwks(json.loads(_jwks)) + if key_defs: + _kb = _issuer[0] + _diff = key_diff(_kb, key_defs) + if _diff: + update_key_bundle(_kb, _diff) + if read_only: + logger.error('Not allowed to write to disc!') + else: + _issuer.set([_kb]) + jwks = _issuer.export_jwks(private=True) + fp = open(private_path, 'w') + fp.write(json.dumps(jwks)) + fp.close() + else: + _issuer = build_keyissuer(key_defs) + if not read_only: + jwks = _issuer.export_jwks(private=True) + head, tail = os.path.split(private_path) + if head and not os.path.isdir(head): + os.makedirs(head) + fp = open(private_path, 'w') + fp.write(json.dumps(jwks)) + fp.close() + + if public_path and not read_only: + jwks = _issuer.export_jwks() # public part + head, tail = os.path.split(public_path) + if head and not os.path.isdir(head): + os.makedirs(head) + fp = open(public_path, 'w') + fp.write(json.dumps(jwks)) + fp.close() + elif public_path: + if os.path.isfile(public_path): + _jwks = open(public_path, 'r').read() + _issuer = KeyIssuer() + _issuer.import_jwks(json.loads(_jwks)) + if key_defs: + _kb = _issuer[0] + _diff = key_diff(_kb, key_defs) + if _diff: + if read_only: + logger.error('Not allowed to write to disc!') + else: + update_key_bundle(_kb, _diff) + _issuer.set([_kb]) + jwks = _issuer.export_jwks() + fp = open(public_path, 'w') + fp.write(json.dumps(jwks)) + fp.close() + else: + _issuer = build_keyissuer(key_defs) + if not read_only: + _jwks = _issuer.export_jwks() + head, tail = os.path.split(public_path) + if head and not os.path.isdir(head): + os.makedirs(head) + fp = open(public_path, 'w') + fp.write(json.dumps(_jwks)) + fp.close() + else: + _issuer = build_keyissuer(key_defs) + + if _issuer is None: + raise ValueError('Could not find any keys') + + return _issuer diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 9cc879d..be9ab11 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -1,6 +1,5 @@ import json import logging -import os from typing import List from typing import Optional @@ -9,16 +8,14 @@ from .jwe.jwe import alg2keytype as jwe_alg2keytype from .jws.utils import alg2keytype as jws_alg2keytype from .key_bundle import KeyBundle -from .key_bundle import key_diff -from .key_bundle import update_key_bundle - -__author__ = 'Roland Hedberg' - from .key_issuer import KeyIssuer from .key_issuer import build_keyissuer +from .key_issuer import init_key_issuer from .utils import importer from .utils import qualified_name +__author__ = 'Roland Hedberg' + logger = logging.getLogger(__name__) @@ -92,7 +89,7 @@ def _get_issuer(self, issuer_id: str) -> Optional[KeyIssuer]: return self._issuers.get(issuer_id) - def _add_issuer(self, issuer_id): + def _add_issuer(self, issuer_id) -> KeyIssuer: _iss = KeyIssuer(ca_certs=self.ca_certs, name=issuer_id, keybundle_cls=self.keybundle_cls, remove_after=self.remove_after, @@ -197,51 +194,53 @@ def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs): if _issuer is None: return [] - lst = [] - for bundle in _issuer: - if key_type: - if key_use in ['ver', 'dec']: - _bkeys = bundle.get(key_type, only_active=False) - else: - _bkeys = bundle.get(key_type) - else: - _bkeys = bundle.keys() - for key in _bkeys: - if key.inactive_since and key_use != "sig": - # Skip inactive keys unless for signature verification - continue - if not key.use or use == key.use: - if kid: - if key.kid == kid: - lst.append(key) - break - else: - continue - else: - lst.append(key) - - # if elliptic curve, have to check if I have a key of the right curve - if key_type == "EC" and "alg" in kwargs: - name = "P-{}".format(kwargs["alg"][2:]) # the type - _lst = [] - for key in lst: - if name != key.crv: - continue - _lst.append(key) - lst = _lst - - if use == 'enc' and key_type == 'oct' and issuer_id != '': - # Add my symmetric keys - _issuer = self._get_issuer('') - if _issuer: - for kb in _issuer: - for key in kb.get(key_type): - if key.inactive_since: - continue - if not key.use or key.use == use: - lst.append(key) - - return lst + return _issuer.get(key_use=key_use, key_type=key_type, kid=kid, **kwargs) + + # lst = [] + # for bundle in _issuer: + # if key_type: + # if key_use in ['ver', 'dec']: + # _bkeys = bundle.get(key_type, only_active=False) + # else: + # _bkeys = bundle.get(key_type) + # else: + # _bkeys = bundle.keys() + # for key in _bkeys: + # if key.inactive_since and key_use != "sig": + # # Skip inactive keys unless for signature verification + # continue + # if not key.use or use == key.use: + # if kid: + # if key.kid == kid: + # lst.append(key) + # break + # else: + # continue + # else: + # lst.append(key) + # + # # if elliptic curve, have to check if I have a key of the right curve + # if key_type == "EC" and "alg" in kwargs: + # name = "P-{}".format(kwargs["alg"][2:]) # the type + # _lst = [] + # for key in lst: + # if name != key.crv: + # continue + # _lst.append(key) + # lst = _lst + # + # if use == 'enc' and key_type == 'oct' and issuer_id != '': + # # Add my symmetric keys + # _issuer = self._get_issuer('') + # if _issuer: + # for kb in _issuer: + # for key in kb.get(key_type): + # if key.inactive_since: + # continue + # if not key.use or key.use == use: + # lst.append(key) + # + # return lst def get_signing_key(self, key_type="", issuer_id="", kid=None, **kwargs): """ @@ -472,12 +471,7 @@ def __eq__(self, other): # Keys per issuer must be the same for iss in self.owners(): - sk = self.get_issuer_keys(iss) - ok = other.get_issuer_keys(iss) - if len(sk) != len(ok): - return False - - if not any(k in ok for k in sk): + if self[iss] != other[iss]: return False return True @@ -825,73 +819,9 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea :return: An instantiated :py:class;`oidcmsg.key_jar.KeyJar` instance """ - if private_path: - if os.path.isfile(private_path): - _jwks = open(private_path, 'r').read() - _issuer = KeyIssuer(name=issuer_id) - _issuer.import_jwks(json.loads(_jwks)) - if key_defs: - _kb = _issuer[0] - _diff = key_diff(_kb, key_defs) - if _diff: - update_key_bundle(_kb, _diff) - if read_only: - logger.error('Not allowed to write to disc!') - else: - _issuer.set([_kb]) - jwks = _issuer.export_jwks(private=True) - fp = open(private_path, 'w') - fp.write(json.dumps(jwks)) - fp.close() - else: - _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) - if not read_only: - jwks = _issuer.export_jwks(private=True) - head, tail = os.path.split(private_path) - if head and not os.path.isdir(head): - os.makedirs(head) - fp = open(private_path, 'w') - fp.write(json.dumps(jwks)) - fp.close() - - if public_path and not read_only: - jwks = _issuer.export_jwks() # public part - head, tail = os.path.split(public_path) - if head and not os.path.isdir(head): - os.makedirs(head) - fp = open(public_path, 'w') - fp.write(json.dumps(jwks)) - fp.close() - elif public_path: - if os.path.isfile(public_path): - _jwks = open(public_path, 'r').read() - _issuer = KeyIssuer(name=issuer_id) - _issuer.import_jwks(json.loads(_jwks)) - if key_defs: - _kb = _issuer[0] - _diff = key_diff(_kb, key_defs) - if _diff: - if read_only: - logger.error('Not allowed to write to disc!') - else: - update_key_bundle(_kb, _diff) - _issuer.set([_kb]) - jwks = _issuer.export_jwks() - fp = open(public_path, 'w') - fp.write(json.dumps(jwks)) - fp.close() - else: - _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) - if not read_only: - _jwks = _issuer.export_jwks(issuer=issuer_id) - head, tail = os.path.split(public_path) - if head and not os.path.isdir(head): - os.makedirs(head) - fp = open(public_path, 'w') - fp.write(json.dumps(_jwks)) - fp.close() - else: - _issuer = build_keyissuer(key_defs, issuer_id=issuer_id) + _issuer = init_key_issuer(public_path=public_path, private_path=private_path, + key_defs=key_defs, read_only=read_only, + storage_conf=storage_conf, abstract_storage_cls=abstract_storage_cls) if _issuer is None: raise ValueError('Could not find any keys') diff --git a/tests/test_04_key_issuer.py b/tests/test_04_key_issuer.py new file mode 100755 index 0000000..27af29a --- /dev/null +++ b/tests/test_04_key_issuer.py @@ -0,0 +1,686 @@ +import os +import shutil +import time + +import pytest + +from cryptojwt.exception import JWKESTException +from cryptojwt.key_bundle import KeyBundle +from cryptojwt.key_bundle import keybundle_from_local_file +from cryptojwt.key_issuer import KeyIssuer +from cryptojwt.key_issuer import build_keyissuer + +__author__ = 'Roland Hedberg' + +from cryptojwt.key_issuer import init_key_issuer + +from cryptojwt.key_issuer import rotate_keys + +BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + "test_keys")) +RSAKEY = os.path.join(BASE_PATH, "cert.key") +RSA0 = os.path.join(BASE_PATH, "rsa.key") +EC0 = os.path.join(BASE_PATH, "ec.key") +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +def full_path(local_file): + return os.path.join(BASEDIR, local_file) + + +JWK0 = { + "keys": [ + { + 'kty': 'RSA', 'e': 'AQAB', 'kid': "abc", + 'n': + 'wf-wiusGhA-gleZYQAOPQlNUIucPiqXdPVyieDqQbXXOPBe3nuggtVzeq7pVFH1dZz4dY2Q2LA5DaegvP8kRvoSB_87ds3dy3Rfym_GUSc5' + 'B0l1TgEobcyaep8jguRoHto6GWHfCfKqoUYZq4N8vh4LLMQwLR6zi6Jtu82nB5k8' + } + ] +} + +JWK1 = { + "keys": [ + { + "n": + "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" + "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" + "-NvS-aG_jN5cstVbCGWE20H0vF" + "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" + "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" + "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": "AQAB", "kty": "RSA", "kid": "rsa1" + }, + { + "k": + "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "oct" + }, + ] +} + +JWK2 = { + "keys": [ + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", + "kid": "kriMPdmBvx68skT8-mPAB3BseeA", + "kty": "RSA", + "n": + "kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS_AHsBeQPqYygfYVJL6_EgzVuwRk5txr9e3n1um" + "l94fLyq_AXbwo9yAduf4dCHTP8CWR1dnDR" + "-Qnz_4PYlWVEuuHHONOw_blbfdMjhY" + "-C_BYM2E3pRxbohBb3x__CfueV7ddz2LYiH3" + "wjz0QS_7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd_GTgWN8A" + "-6SN1r4hzpjFKFLbZnBt77ACSiYx-IHK4Mp-NaVEi5wQt" + "SsjQtI--XsokxRDqYLwus1I1SihgbV_STTg5enufuw", + "use": "sig", + "x5c": [ + "MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb" + "2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb2" + "50cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipg" + "H0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6" + "/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Q" + "nz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x" + "//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13S" + "QwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp" + "+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5en" + "ufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJ" + "vbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdN" + "VGKCmSf8M65b8h0NwlIjGGGy" + "/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADD" + "kN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5" + "+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8y" + "PJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW" + "+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ" + ], + "x5t": "kriMPdmBvx68skT8-mPAB3BseeA" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0/", + "kid": "MnC_VZcATfM5pOYiJHMba9goEKY", + "kty": "RSA", + "n": + "vIqz-4-ER_vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhzh23V9Tkq" + "-RtwN1Vs_z57hO82kkzL-cQHZX3bMJ" + "D-GEGOKXCEXURN7VMyZWMAuzQoW9vFb1k3cR1RW_EW_P" + "-C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5yCw5T_Vuwqqsio3" + "V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp_KAS_qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3-T-IA" + "bsk1wRtWDndhO6s1Os-dck5TzyZ_dNOhfXgelixLUQ", + "use": "sig", + "x5c": [ + "MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb" + "250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZX" + "NzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs" + "/uPhEf7zVizjfcr/ISGFe9+yUO" + "qwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy" + "/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW" + "9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU" + "/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg" + "0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k" + "/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX" + "14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9" + "+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNG" + "zZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m" + "/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uh" + "bGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN" + "/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrs" + "KsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3" + "/bKkLSuDaKLWSqMhozdhXsIIKvJQ==" + ], + "x5t": "MnC_VZcATfM5pOYiJHMba9goEKY" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" + "-b112-36a304b66dad/v2.0/", + "kid": "GvnPApfWMdLRi8PDmisFn7bprKg", + "kty": "RSA", + "n": "5ymq_xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMRG7sJq" + "-UWOKVOA4MVrd_NdV-ejj1DE5MPSiG" + "-mZK_5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ48thXOTED0oNa6U", + "use": "sig", + "x5c": [ + "MIICWzCCAcSgAwIBAgIJAKVzMH2FfC12MA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" + "GljIEtleTAeFw0xMzExMTExODMzMDhaFw0xNjExMTAxODMzMDhaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" + "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5ymq" + "/xwmst1nstPr8YFOTyD1J5N4idYmrph7AyAv95RbWXfDRqy8CMR" + "G7sJq+UWOKVOA4MVrd/NdV+ejj1DE5MPSiG+mZK" + "/5iqRCDFvPYqOyRj539xaTlARNY4jeXZ0N6irZYKqSfYACjkkKxbLKcijSu1pJ" + "48thXOTED0oNa6UCAwEAAaOBijCBhzAdBgNVHQ4EFgQURCN" + "+4cb0pvkykJCUmpjyfUfnRMowWQYDVR0jBFIwUIAURCN+4cb0pvkyk" + "JCUmpjyfUfnRMqhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAKVzMH2FfC12MAsGA1UdDw" + "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQB8v8G5" + "/vUl8k7xVuTmMTDA878AcBKBrJ/Hp6RShmdqEGVI7SFR7IlBN1//NwD0n" + "+Iqzmn" + "RV2PPZ7iRgMF/Fyvqi96Gd8X53ds/FaiQpZjUUtcO3fk0hDRQPtCYMII5jq" + "+YAYjSybvF84saB7HGtucVRn2nMZc5cAC42QNYIlPM" + "qA==" + ], + "x5t": "GvnPApfWMdLRi8PDmisFn7bprKg" + }, + { + "e": "AQAB", + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b" + "-b112-36a304b66dad/v2.0/", + "kid": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ", + "kty": "RSA", + "n": + "x7HNcD9ZxTFRaAgZ7-gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQeSML7qZPlowb5BUakdLI70ayM4vN36--0ht8-oCHhl8Yj" + "GFQkU-Iv2yahWHEP-1EK6eOEYu6INQP9Lk0HMk3QViLwshwb" + "-KXVD02jdmX2HNdYJdPyc0c", + "use": "sig", + "x5c": [ + "MIICWzCCAcSgAwIBAgIJAL3MzqqEFMYjMA0GCSqGSIb3DQEBBQUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVib" + "GljIEtleTAeFw0xMzExMTExOTA1MDJaFw0xOTExMTAxOTA1MDJaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibG" + "ljIEtleTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAx7HNcD9ZxTFRaAgZ7" + "+gdYLkgQua3zvQseqBJIt8Uq3MimInMZoE9QGQ" + "eSML7qZPlowb5BUakdLI70ayM4vN36++0ht8+oCHhl8YjGFQkU" + "+Iv2yahWHEP+1EK6eOEYu6INQP9Lk0HMk3QViLwshwb+KXVD02j" + "dmX2HNdYJdPyc0cCAwEAAaOBijCBhzAdBgNVHQ4EFgQULR0aj9AtiNMgqIY8ZyXZGsHcJ5gwWQYDVR0jBFIwUIAULR0aj9AtiNMgq" + "IY8ZyXZGsHcJ5ihLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAL3MzqqEFMYjMAsGA1UdDw" + "QEAwIBxjANBgkqhkiG9w0BAQUFAAOBgQBshrsF9yls4ArxOKqXdQPDgHrbynZL8m1iinLI4TeSfmTCDevXVBJrQ6SgDkihl3aCj74" + "IEte2MWN78sHvLLTWTAkiQSlGf1Zb0durw+OvlunQ2AKbK79Qv0Q+wwGuK" + "+oymWc3GSdP1wZqk9dhrQxb3FtdU2tMke01QTut6wr7" + "ig==" + ], + "x5t": "dEtpjbEvbhfgwUI-bdK5xAU_9UQ" + } + ] +} + + +def test_build_keyissuer(): + keys = [ + {"type": "RSA", "use": ["enc", "sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + ] + + key_issuer = build_keyissuer(keys) + jwks = key_issuer.export_jwks() + for key in jwks["keys"]: + assert "d" not in key # the JWKS shouldn't contain the private part + # of the keys + + assert len(key_issuer) == 3 # 3 keys + assert len(key_issuer.get('sig')) == 2 # 2 for signing + assert len(key_issuer.get('enc')) == 1 # 1 for encryption + + +def test_build_keyissuer_usage(): + keys = [ + {"type": "RSA", "use": ["enc", "sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "oct", "use": ["enc"]}, + {"type": "oct", "use": ["enc"]}, + ] + + key_issuer = build_keyissuer(keys) + jwks_sig = key_issuer.export_jwks(usage='sig') + jwks_enc = key_issuer.export_jwks(usage='enc') + assert len(jwks_sig.get('keys')) == 2 # A total of 2 keys with use=sig + assert len(jwks_enc.get('keys')) == 3 # A total of 3 keys with use=enc + + +def test_build_keyissuer_missing(tmpdir): + keys = [ + { + "type": "RSA", "key": os.path.join(tmpdir.dirname, "missing_file"), + "use": ["enc", "sig"] + }] + + key_issuer = build_keyissuer(keys) + + assert key_issuer is None + + +def test_build_RSA_keyjar_from_file(tmpdir): + keys = [ + { + "type": "RSA", "key": RSA0, + "use": ["enc", "sig"] + }] + + key_issuer = build_keyissuer(keys) + + assert len(key_issuer) == 2 + + +def test_build_EC_keyjar_missing(tmpdir): + keys = [ + { + "type": "EC", "key": os.path.join(tmpdir.dirname, "missing_file"), + "use": ["enc", "sig"] + }] + + key_issuer = build_keyissuer(keys) + + assert key_issuer is None + + +def test_build_EC_keyjar_from_file(tmpdir): + keys = [ + { + "type": "EC", "key": EC0, + "use": ["enc", "sig"] + }] + + key_issuer = build_keyissuer(keys) + + assert len(key_issuer) == 2 + + +class TestKeyJar(object): + def test_keyissuer_add(self): + issuer = KeyIssuer() + kb = keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"]) + issuer.add_kb(kb) + assert len(issuer.all_keys()) == 1 + + def test_add_symmetric(self): + issuer = KeyIssuer() + issuer.add_symmetric('abcdefghijklmnop', ['sig']) + assert len(issuer.get('sig', 'oct')) == 1 + + def test_items(self): + issuer = KeyIssuer() + issuer.add_kb(KeyBundle( + [{"kty": "oct", "key": "abcdefghijklmnop", "use": "sig"}, + {"kty": "oct", "key": "ABCDEFGHIJKLMNOP", "use": "enc"}])) + issuer.add_kb(KeyBundle([ + {"kty": "oct", "key": "0123456789012345", "use": "sig"}, + {"kty": "oct", "key": "1234567890123456", "use": "enc"}])) + issuer.add_kb(keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + + assert len(issuer.all_keys()) == 5 + + def test_get_enc(self): + issuer = KeyIssuer() + issuer.add_kb(KeyBundle( + [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, + {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) + issuer.add_kb(KeyBundle([ + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}])) + issuer.add_kb(keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + + assert issuer.get('enc', 'oct') + + def test_get_enc_not_mine(self): + issuer = KeyIssuer() + issuer.add_kb(KeyBundle( + [{"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "sig"}, + {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) + issuer.add_kb(KeyBundle([ + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "ver"}])) + issuer.add_kb(keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) + + assert issuer.get('enc', 'oct') + + def test_dump_issuer_keys(self): + kb = keybundle_from_local_file("file://%s/jwk.json" % BASE_PATH, "jwks", + ["sig"]) + assert len(kb) == 1 + issuer = KeyIssuer() + issuer.add_kb(kb) + _jwks_dict = issuer.export_jwks() + + _info = _jwks_dict['keys'][0] + assert _info == { + 'use': 'sig', + 'e': 'AQAB', + 'kty': 'RSA', + 'alg': 'RS256', + 'n': 'pKybs0WaHU_y4cHxWbm8Wzj66HtcyFn7Fh3n' + '-99qTXu5yNa30MRYIYfSDwe9JVc1JUoGw41yq2StdGBJ40HxichjE' + '-Yopfu3B58Q' + 'lgJvToUbWD4gmTDGgMGxQxtv1En2yedaynQ73sDpIK-12JJDY55pvf' + '-PCiSQ9OjxZLiVGKlClDus44_uv2370b9IN2JiEOF-a7JB' + 'qaTEYLPpXaoKWDSnJNonr79tL0T7iuJmO1l705oO3Y0TQ' + '-INLY6jnKG_RpsvyvGNnwP9pMvcP1phKsWZ10ofuuhJGRp8IxQL9Rfz' + 'T87OvF0RBSO1U73h09YP-corWDsnKIi6TbzRpN5YDw', + 'kid': 'abc' + } + + def test_no_use(self): + kb = KeyBundle(JWK0["keys"]) + issuer = KeyIssuer() + issuer.add_kb(kb) + enc_key = issuer.get('enc', "RSA") + assert enc_key != [] + + # @pytest.mark.network + # def test_provider(self): + # issuer = KeyIssuer() + # issuer.load_keys(jwks_uri="https://connect-op.herokuapp.com/jwks.json") + # + # assert issuer.get("https://connect-op.heroku.com")[0].keys() + + +def test_import_jwks(): + issuer = KeyIssuer() + issuer.import_jwks(JWK1) + assert len(issuer.all_keys()) == 2 + + +def test_get_signing_key_use_undefined(): + issuer = KeyIssuer() + issuer.import_jwks(JWK1) + keys = issuer.get('sig', kid='rsa1') + assert len(keys) == 1 + + keys = issuer.get('sig', key_type='rsa') + assert len(keys) == 1 + + keys = issuer.get('sig', key_type='rsa', kid='rsa1') + assert len(keys) == 1 + + +KEYDEFS = [ + {"type": "RSA", "key": '', "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]} +] + + +def test_remove_after(): + # initial key_issuer + key_issuer = build_keyissuer(KEYDEFS) + _old = [k.kid for k in key_issuer.all_keys() if k.kid] + assert len(_old) == 2 + + key_issuer.remove_after = 1 + # rotate_keys = create new keys + make the old as inactive + key_issuer = rotate_keys(KEYDEFS, issuer=key_issuer) + + key_issuer.remove_outdated(time.time() + 3600) + + _interm = [k.kid for k in key_issuer.all_keys() if k.kid] + assert len(_interm) == 2 + + # The remainder are the new keys + _new = [k.kid for k in key_issuer.all_keys() if k.kid] + assert len(_new) == 2 + + # should not be any overlap between old and new + assert set(_new).intersection(set(_old)) == set() + + +JWK_UK = { + "keys": [ + { + "n": + "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" + "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" + "-NvS-aG_jN5cstVbCGWE20H0vF" + "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" + "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" + "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": "AQAB", "kty": "RSA", "kid": "rsa1" + }, + { + "k": + "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "buz" + }, + ] +} + + +def test_load_unknown_keytype(): + issuer = KeyIssuer() + issuer.import_jwks(JWK_UK) + assert len(issuer.all_keys()) == 1 + + +JWK_FP = { + "keys": [ + {"e": "AQAB", "kty": "RSA", "kid": "rsa1"}, + ] +} + + +def test_load_missing_key_parameter(): + issuer = KeyIssuer() + with pytest.raises(JWKESTException): + issuer.import_jwks(JWK_FP) + + +JWKS_SPO = { + "keys": [ + { + "kid": + "BfxfnahEtkRBG3Hojc9XGLGht_5rDBj49Wh3sBDVnzRpulMqYwMRmpizA0aSPT1fhCHYivTiaucWUqFu_GwTqA", + "use": "sig", + "alg": "ES256", + "kty": "EC", + "crv": "P-256", + "x": "1XXUXq75gOPZ4bEj1o2Z5XKJWSs6LmL6fAOK3vyMzSc", + "y": "ac1h_DwyuUxhkrD9oKMJ-b_KuiVvvSARIwT-XoEmDXs" + }, + { + "kid": + "91pD1H81rXUvrfg9mkngIG-tXjnldykKUVbITDIU1SgJvq91b8clOcJuEHNAq61eIvg8owpEvWcWAtlbV2awyA", + "use": "sig", + "alg": "ES256", + "kty": "EC", + "crv": "P-256", + "x": "2DfQoLpZS2j3hHEcHDkzV8ISx-RdLt6Opy8YZYVm4AQ", + "y": "ycvkFMBIzgsowiaf6500YlG4vaMSK4OF7WVtQpUbEE0" + }, + { + "kid": "0sIEl3MUJiCxrqleEBBF-_bZq5uClE84xp-wpt8oOI" + "-WIeNxBjSR4ak_OTOmLdndB0EfDLtC7X1JrnfZILJkxA", + "use": "sig", + "alg": "RS256", + "kty": "RSA", + "n": + "yG9914Q1j63Os4jX5dBQbUfImGq4zsXJD4R59XNjGJlEt5ek6NoiDl0ucJO3_7_R9e5my2ONTSqZhtzFW6MImnIn8idWYzJzO2EhUPCHTvw_2oOGjeYTE2VltIyY_ogIxGwY66G0fVPRRH9tCxnkGOrIvmVgkhCCGkamqeXuWvx9MCHL_gJbZJVwogPSRN_SjA1gDlvsyCdA6__CkgAFcSt1sGgiZ_4cQheKexxf1-7l8R91ZYetz53drk2FS3SfuMZuwMM4KbXt6CifNhzh1Ye-5Tr_ZENXdAvuBRDzfy168xnk9m0JBtvul9GoVIqvCVECB4MPUb7zU6FTIcwRAw", + "e": "AQAB" + }, + { + "kid": + "zyDfdEU7pvH0xEROK156ik8G7vLO1MIL9TKyL631kSPtr9tnvs9XOIiq5jafK2hrGr2qqvJdejmoonlGqWWZRA", + "use": "sig", + "alg": "RS256", + "kty": "RSA", + "n": + "68be-nJp46VLj4Ci1V36IrVGYqkuBfYNyjQTZD_7yRYcERZebowOnwr3w0DoIQpl8iL2X8OXUo7rUW_LMzLxKx2hEmdJfUn4LL2QqA3KPgjYz8hZJQPG92O14w9IZ-8bdDUgXrg9216H09yq6ZvJrn5Nwvap3MXgECEzsZ6zQLRKdb_R96KFFgCiI3bEiZKvZJRA7hM2ePyTm15D9En_Wzzfn_JLMYgE_DlVpoKR1MsTinfACOlwwdO9U5Dm-5elapovILTyVTgjN75i-wsPU2TqzdHFKA-4hJNiWGrYPiihlAFbA2eUSXuEYFkX43ahoQNpeaf0mc17Jt5kp7pM2w", + "e": "AQAB" + }, + { + "kid": "q-H9y8iuh3BIKZBbK6S0mH_isBlJsk" + "-u6VtZ5rAdBo5fCjjy3LnkrsoK_QWrlKB08j_PcvwpAMfTEDHw5spepw", + "use": "sig", + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "FnbcUAXZ4ySvrmdXK1MrDuiqlqTXvGdAaE4RWZjmFIQ" + }, + { + "kid": + "bL33HthM3fWaYkY2_pDzUd7a65FV2R2LHAKCOsye8eNmAPDgRgpHWPYpWFVmeaujUUEXRyDLHN" + "-Up4QH_sFcmw", + "use": "sig", + "alg": "EdDSA", + "kty": "OKP", + "crv": "Ed25519", + "x": "CS01DGXDBPV9cFmd8tgFu3E7eHn1UcP7N1UCgd_JgZo" + } + ] +} + + +def test_load_spomky_keys(): + issuer = KeyIssuer() + issuer.import_jwks(JWKS_SPO) + assert len(issuer) == 4 + + +def test_get_ec(): + issuer = KeyIssuer() + issuer.import_jwks(JWKS_SPO) + k = issuer.get('sig', 'EC', alg='ES256') + assert k + + +def test_get_ec_wrong_alg(): + issuer = KeyIssuer() + issuer.import_jwks(JWKS_SPO) + k = issuer.get('sig', 'EC', alg='ES512') + assert k == [] + + +def test_keyissuer_eq(): + kj1 = KeyIssuer() + kj1.import_jwks(JWKS_SPO) + + kj2 = KeyIssuer() + kj2.import_jwks(JWKS_SPO) + + assert kj1 == kj2 + + +PUBLIC_FILE = '{}/public_jwks.json'.format(BASEDIR) +PRIVATE_FILE = '{}/private_jwks.json'.format(BASEDIR) +KEYSPEC = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]} +] +KEYSPEC_2 = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]} +] +KEYSPEC_3 = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]}, + {"type": "EC", "crv": "P-521", "use": ["sig"]} +] +KEYSPEC_4 = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]} +] +KEYSPEC_5 = [ + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]} +] + + +def test_init_key_issuer(): + # Nothing written to file + _keyissuer = init_key_issuer(key_defs=KEYSPEC) + assert len(_keyissuer) == 2 + + +def test_init_key_jar_dump_public(): + for _file in [PRIVATE_FILE, PUBLIC_FILE]: + if os.path.isfile(_file): + os.unlink(_file) + + # JWKS with public keys written to file + _keyissuer = init_key_issuer(public_path=PUBLIC_FILE, key_defs=KEYSPEC) + assert len(_keyissuer) == 2 + + # JWKS will be read from disc, not created new + _keyissuer2 = init_key_issuer(public_path=PUBLIC_FILE, key_defs=KEYSPEC) + assert len(_keyissuer2) == 2 + + # verify that the 2 Key jars contains the same keys + + +def test_init_key_issuer_dump_private(): + for _file in [PRIVATE_FILE, PUBLIC_FILE]: + if os.path.isfile(_file): + os.unlink(_file) + + # New set of keys, JWKSs with keys and public written to file + _keyissuer = init_key_issuer(private_path=PRIVATE_FILE, key_defs=KEYSPEC, read_only=False) + + # JWKS will be read from disc, not created new + _keyissuer2 = init_key_issuer(private_path=PRIVATE_FILE, key_defs=KEYSPEC, read_only=False) + assert _keyissuer == _keyissuer2 + + +def test_init_key_issuer_update(): + for _file in [PRIVATE_FILE, PUBLIC_FILE]: + if os.path.isfile(_file): + os.unlink(_file) + + # New set of keys, JWKSs with keys and public written to file + _keyissuer_1 = init_key_issuer(private_path=PRIVATE_FILE, key_defs=KEYSPEC, + public_path=PUBLIC_FILE, read_only=False) + assert len(_keyissuer_1) == 2 + + _keyissuer_2 = init_key_issuer(private_path=PRIVATE_FILE, key_defs=KEYSPEC_2, + public_path=PUBLIC_FILE) + + # Both should contain the same RSA key + rsa1 = _keyissuer_1.get('sig', 'RSA') + rsa2 = _keyissuer_2.get('sig', 'RSA') + + assert len(rsa1) == 1 + assert len(rsa2) == 1 + assert rsa1[0] == rsa2[0] + + # keyjar1 should only contain one EC key while keyjar2 should contain 2. + + ec1 = _keyissuer_1.get('sig','EC') + ec2 = _keyissuer_2.get('sig', 'EC', '') + assert len(ec1) == 1 + assert len(ec2) == 2 + + # The file on disc should not have changed + _keyissuer_3 = init_key_issuer(private_path=PRIVATE_FILE) + + assert len(_keyissuer_3.get('sig','RSA')) == 1 + assert len(_keyissuer_3.get('sig','EC')) == 1 + + _keyissuer_4 = init_key_issuer(private_path=PRIVATE_FILE, key_defs=KEYSPEC_2, + public_path=PUBLIC_FILE, read_only=False) + + # Now it should + _keyissuer_5 = init_key_issuer(private_path=PRIVATE_FILE) + + assert len(_keyissuer_5.get('sig','RSA')) == 1 + assert len(_keyissuer_5.get('sig','EC')) == 2 + + +OIDC_KEYS = { + 'private_path': "{}/priv/jwks.json".format(BASEDIR), + 'key_defs': KEYSPEC, + 'public_path': '{}/public/jwks.json'.format(BASEDIR) +} + + +def test_init_key_issuer_create_directories(): + # make sure the directories are gone + for _dir in ['priv', 'public']: + if os.path.isdir("{}/{}".format(BASEDIR, _dir)): + shutil.rmtree("{}/{}".format(BASEDIR, _dir)) + + _keyissuer = init_key_issuer(**OIDC_KEYS) + assert len(_keyissuer.get('sig','RSA')) == 1 + assert len(_keyissuer.get('sig','EC')) == 1 + + +def test_dump(): + issuer = KeyIssuer() + issuer.add_kb(KeyBundle(JWK2['keys'])) + + res = issuer.dump() + + nkj = KeyIssuer().load(res) + assert nkj.get('sig','rsa', kid="kriMPdmBvx68skT8-mPAB3BseeA") + assert nkj.get('sig','rsa', kid='MnC_VZcATfM5pOYiJHMba9goEKY') + + +def test_contains(): + issuer = KeyIssuer() + issuer.add_kb(KeyBundle(JWK1['keys'])) + for k in issuer.all_keys(): + assert k in issuer diff --git a/tests/test_04_key_jar.py b/tests/test_04_key_jar.py index 0ffa1f5..6430b37 100755 --- a/tests/test_04_key_jar.py +++ b/tests/test_04_key_jar.py @@ -354,7 +354,7 @@ def test_get_enc_not_mine(self): {"kty": "oct", "key": "a1b2c3d4e5f6g7h8", "use": "enc"}])) ks.add_kb("http://www.example.org/", KeyBundle([ {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "sig"}, - {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "ver"}])) + {"kty": "oct", "key": "1a2b3c4d5e6f7g8h", "use": "enc"}])) ks.add_kb("http://www.example.org/", keybundle_from_local_file(RSAKEY, "der", ["ver", "sig"])) @@ -972,4 +972,4 @@ def test_contains(): kj.add_kb('C', KeyBundle(JWK2['keys'])) assert 'Bob' in kj - assert 'David' not in kj \ No newline at end of file + assert 'David' not in kj From 508602d44b44e41098f1f469338af7ae1ce64f5c Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 16 Jun 2020 10:45:39 +0200 Subject: [PATCH 34/50] More logging --- src/cryptojwt/key_issuer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cryptojwt/key_issuer.py b/src/cryptojwt/key_issuer.py index 26727f8..d459c26 100755 --- a/src/cryptojwt/key_issuer.py +++ b/src/cryptojwt/key_issuer.py @@ -78,6 +78,8 @@ def add_url(self, url, **kwargs): if not url: raise KeyError("No url given") + logger.debug('httpc_params: %s', self.httpc_params) + if "/localhost:" in url or "/localhost/" in url: _params = self.httpc_params.copy() _params['verify'] = False From 6b1b9e43235f39869a3cb9d6752fa7a952ffee5e Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 17 Jun 2020 11:10:51 +0200 Subject: [PATCH 35/50] Function made into a class method. --- src/cryptojwt/key_issuer.py | 39 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/cryptojwt/key_issuer.py b/src/cryptojwt/key_issuer.py index d459c26..6d1a61a 100755 --- a/src/cryptojwt/key_issuer.py +++ b/src/cryptojwt/key_issuer.py @@ -22,7 +22,7 @@ class KeyIssuer(object): - """ A issuer contains a number of KeyBundles. """ + """ A key issuer instance contains a number of KeyBundles. """ def __init__(self, ca_certs=None, keybundle_cls=KeyBundle, remove_after=3600, httpc=None, httpc_params=None, @@ -69,7 +69,6 @@ def add_url(self, url, **kwargs): url as source specification. If no file format is given it's assumed that what's on the other side is a JWKS. - :param issuer: Who issued the keys :param url: Where can the key/-s be found :param kwargs: extra parameters for instantiating KeyBundle :return: A :py:class:`oidcmsg.oauth2.keybundle.KeyBundle` instance @@ -234,19 +233,19 @@ def import_jwks(self, jwks): self._bundles.append( self.keybundle_cls(_keys, httpc=self.httpc, httpc_params=self.httpc_params)) - def import_jwks_as_json(self, jwks, issuer): + def import_jwks_as_json(self, jwks, issuer_id): """ Imports all the keys that are represented in a JWKS expressed as a JSON object :param jwks: JSON representation of a JWKS - :param issuer: Who 'owns' the JWKS + :param issuer_id: Who 'owns' the JWKS """ return self.import_jwks(json.loads(jwks)) - def import_jwks_from_file(self, filename, issuer): + def import_jwks_from_file(self, filename, issuer_id): with open(filename) as jwks_file: - self.import_jwks_as_json(jwks_file.read(), issuer) + self.import_jwks_as_json(jwks_file.read(), issuer_id) def remove_outdated(self, when=0): """ @@ -352,7 +351,7 @@ def __len__(self): def dump(self, exclude=None): """ - Returns the key issuer content as a dictionary. + Returns the content as a dictionary. :return: A dictionary """ @@ -447,6 +446,20 @@ def __eq__(self, other): return True + def rotate_keys(self, key_conf, kid_template=""): + """ + + :param key_conf: The configuration for the new keys + :param issuer: KeyIssuer instance + :param kid_template: A key id template + :return: + """ + new_keys = build_keyissuer(key_conf, kid_template) + self.mark_all_keys_as_inactive() + for kb in new_keys: + self.add_kb(kb) + return self + # ============================================================================= @@ -489,6 +502,7 @@ def build_keyissuer(key_conf, kid_template="", key_issuer=None, issuer_id=''): :param kid_template: A template by which to build the key IDs. If no kid_template is given then the built-in function add_kid() will be used. :param key_issuer: If an keyIssuer instance the new keys are added to this key issuer. + :param issuer_id: The identifier of the issuer :return: A KeyIssuer instance """ @@ -504,16 +518,7 @@ def build_keyissuer(key_conf, kid_template="", key_issuer=None, issuer_id=''): return key_issuer -def rotate_keys(key_conf, issuer, kid_template=""): - new_keys = build_keyissuer(key_conf, kid_template) - issuer.mark_all_keys_as_inactive() - for kb in new_keys: - issuer.add_kb(kb) - return issuer - - -def init_key_issuer(public_path='', private_path='', key_defs='', read_only=True, - storage_conf=None, abstract_storage_cls=None): +def init_key_issuer(public_path='', private_path='', key_defs='', read_only=True): """ A number of cases here: From e4782690d3d8945b9bea49e5b9a5f33c354f16f8 Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 17 Jun 2020 16:56:05 +0200 Subject: [PATCH 36/50] Function made into a class method. --- tests/test_04_key_issuer.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/tests/test_04_key_issuer.py b/tests/test_04_key_issuer.py index 27af29a..62bff7b 100755 --- a/tests/test_04_key_issuer.py +++ b/tests/test_04_key_issuer.py @@ -9,12 +9,9 @@ from cryptojwt.key_bundle import keybundle_from_local_file from cryptojwt.key_issuer import KeyIssuer from cryptojwt.key_issuer import build_keyissuer - -__author__ = 'Roland Hedberg' - from cryptojwt.key_issuer import init_key_issuer -from cryptojwt.key_issuer import rotate_keys +__author__ = 'Roland Hedberg' BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "test_keys")) @@ -392,7 +389,7 @@ def test_remove_after(): key_issuer.remove_after = 1 # rotate_keys = create new keys + make the old as inactive - key_issuer = rotate_keys(KEYDEFS, issuer=key_issuer) + key_issuer = key_issuer.rotate_keys(KEYDEFS) key_issuer.remove_outdated(time.time() + 3600) @@ -629,7 +626,7 @@ def test_init_key_issuer_update(): # keyjar1 should only contain one EC key while keyjar2 should contain 2. - ec1 = _keyissuer_1.get('sig','EC') + ec1 = _keyissuer_1.get('sig', 'EC') ec2 = _keyissuer_2.get('sig', 'EC', '') assert len(ec1) == 1 assert len(ec2) == 2 @@ -637,8 +634,8 @@ def test_init_key_issuer_update(): # The file on disc should not have changed _keyissuer_3 = init_key_issuer(private_path=PRIVATE_FILE) - assert len(_keyissuer_3.get('sig','RSA')) == 1 - assert len(_keyissuer_3.get('sig','EC')) == 1 + assert len(_keyissuer_3.get('sig', 'RSA')) == 1 + assert len(_keyissuer_3.get('sig', 'EC')) == 1 _keyissuer_4 = init_key_issuer(private_path=PRIVATE_FILE, key_defs=KEYSPEC_2, public_path=PUBLIC_FILE, read_only=False) @@ -646,8 +643,8 @@ def test_init_key_issuer_update(): # Now it should _keyissuer_5 = init_key_issuer(private_path=PRIVATE_FILE) - assert len(_keyissuer_5.get('sig','RSA')) == 1 - assert len(_keyissuer_5.get('sig','EC')) == 2 + assert len(_keyissuer_5.get('sig', 'RSA')) == 1 + assert len(_keyissuer_5.get('sig', 'EC')) == 2 OIDC_KEYS = { @@ -664,8 +661,8 @@ def test_init_key_issuer_create_directories(): shutil.rmtree("{}/{}".format(BASEDIR, _dir)) _keyissuer = init_key_issuer(**OIDC_KEYS) - assert len(_keyissuer.get('sig','RSA')) == 1 - assert len(_keyissuer.get('sig','EC')) == 1 + assert len(_keyissuer.get('sig', 'RSA')) == 1 + assert len(_keyissuer.get('sig', 'EC')) == 1 def test_dump(): @@ -675,8 +672,8 @@ def test_dump(): res = issuer.dump() nkj = KeyIssuer().load(res) - assert nkj.get('sig','rsa', kid="kriMPdmBvx68skT8-mPAB3BseeA") - assert nkj.get('sig','rsa', kid='MnC_VZcATfM5pOYiJHMba9goEKY') + assert nkj.get('sig', 'rsa', kid="kriMPdmBvx68skT8-mPAB3BseeA") + assert nkj.get('sig', 'rsa', kid='MnC_VZcATfM5pOYiJHMba9goEKY') def test_contains(): From 2c4e16a7f857ae3972fb57a2d8f08adbc08d9300 Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 17 Jun 2020 16:57:12 +0200 Subject: [PATCH 37/50] Added deprecated_alias decorator. --- src/cryptojwt/key_jar.py | 68 +++++++++++++++++++++++++++------------- src/cryptojwt/utils.py | 34 +++++++++++++++++++- 2 files changed, 80 insertions(+), 22 deletions(-) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index be9ab11..98f5f48 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -11,6 +11,7 @@ from .key_issuer import KeyIssuer from .key_issuer import build_keyissuer from .key_issuer import init_key_issuer +from .utils import deprecated_alias from .utils import importer from .utils import qualified_name @@ -79,6 +80,7 @@ def _issuer_ids(self) -> List[str]: """ return list(self._issuers.keys()) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def _get_issuer(self, issuer_id: str) -> Optional[KeyIssuer]: """ Return the KeyIssuer instance that has name == issuer_id @@ -89,6 +91,7 @@ def _get_issuer(self, issuer_id: str) -> Optional[KeyIssuer]: return self._issuers.get(issuer_id) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def _add_issuer(self, issuer_id) -> KeyIssuer: _iss = KeyIssuer(ca_certs=self.ca_certs, name=issuer_id, keybundle_cls=self.keybundle_cls, @@ -109,6 +112,7 @@ def __repr__(self): issuers = self._issuer_ids() return ''.format(issuers) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def return_issuer(self, issuer_id): """ Return a KeyIssuer instance with name == issuer_id. @@ -122,6 +126,7 @@ def return_issuer(self, issuer_id): return self._add_issuer(issuer_id) return _iss + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def add_url(self, issuer_id: str, url: str, **kwargs) -> KeyBundle: """ Add a set of keys by url. This method will create a @@ -139,13 +144,14 @@ def add_url(self, issuer_id: str, url: str, **kwargs) -> KeyBundle: kb = issuer.add_url(url, **kwargs) return kb + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def add_symmetric(self, issuer_id, key, usage=None): """ Add a symmetric key. This is done by wrapping it in a key bundle cloak since KeyJar does not handle keys directly but only through key bundles. - :param issuer: Owner of the key + :param issuer_id: Owner of the key :param key: The key :param usage: What the key can be used for signing/signature verification (sig) and/or encryption/decryption (enc) @@ -153,6 +159,7 @@ def add_symmetric(self, issuer_id, key, usage=None): issuer = self.return_issuer(issuer_id) issuer.add_symmetric(key, usage=usage) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def add_kb(self, issuer_id, kb): """ Add a key bundle and bind it to an identifier @@ -164,6 +171,7 @@ def add_kb(self, issuer_id, kb): issuer.add_kb(kb) self[issuer_id] = issuer + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs): """ Get all keys that matches a set of search criteria @@ -242,6 +250,7 @@ def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs): # # return lst + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def get_signing_key(self, key_type="", issuer_id="", kid=None, **kwargs): """ Shortcut to use for signing keys only. @@ -254,15 +263,19 @@ def get_signing_key(self, key_type="", issuer_id="", kid=None, **kwargs): """ return self.get("sig", key_type, issuer_id, kid, **kwargs) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def get_verify_key(self, key_type="", issuer_id="", kid=None, **kwargs): return self.get("ver", key_type, issuer_id, kid, **kwargs) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def get_encrypt_key(self, key_type="", issuer_id="", kid=None, **kwargs): return self.get("enc", key_type, issuer_id, kid, **kwargs) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def get_decrypt_key(self, key_type="", issuer_id="", kid=None, **kwargs): return self.get("dec", key_type, issuer_id, kid, **kwargs) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def keys_by_alg_and_usage(self, issuer_id, alg, usage): """ Find all keys that can be used for a specific crypto algorithm and @@ -280,11 +293,12 @@ def keys_by_alg_and_usage(self, issuer_id, alg, usage): return self.get(usage, ktype, issuer_id) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def get_issuer_keys(self, issuer_id): """ Get all the keys that belong to an entity. - :param issuer: The entity ID + :param issuer_id: The entity ID :return: A possibly empty list of keys """ _issuer = self._get_issuer(issuer_id) @@ -293,12 +307,14 @@ def get_issuer_keys(self, issuer_id): else: return [] + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def __contains__(self, issuer_id): if self._get_issuer(issuer_id): return True else: return False + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def __getitem__(self, issuer_id=''): """ Get all the KeyIssuer with the name == issuer_id @@ -308,14 +324,15 @@ def __getitem__(self, issuer_id=''): """ return self._get_issuer(issuer_id) - def __setitem__(self, issuer_id, issuer): + @deprecated_alias(issuer='issuer_id', owner='issuer_id') + def __setitem__(self, issuer_id, key_issuer): """ Set a KeyIssuer with the name == issuer_id :param issuer_id: The entity ID - :param issuer: KeyIssuer instance + :param key_issuer: KeyIssuer instance """ - self._issuers[issuer_id] = issuer + self._issuers[issuer_id] = key_issuer def set(self, issuer_id, issuer): self[issuer_id] = issuer @@ -349,13 +366,14 @@ def __str__(self): _res[_id] = _issuer.key_summary() return json.dumps(_res) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def load_keys(self, issuer_id, jwks_uri='', jwks=None, replace=False): """ Fetch keys from another server :param jwks_uri: A URL pointing to a site that will return a JWKS :param jwks: A dictionary representation of a JWKS - :param issuer: The provider URL + :param issuer_id: The provider URL :param replace: If all previously gathered keys from this provider should be replace. :return: Dictionary with usage as key and keys as values @@ -376,12 +394,13 @@ def load_keys(self, issuer_id, jwks_uri='', jwks=None, replace=False): self[issuer_id] = _issuer + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def find(self, source, issuer_id=None): """ Find a key bundle based on the source of the keys :param source: A source url - :param issuer: The issuer of keys + :param issuer_id: The issuer of keys :return: List of :py:class:`oidcmsg.key_bundle.KeyBundle` instances or None """ if issuer_id is None: @@ -399,13 +418,14 @@ def find(self, source, issuer_id=None): return res + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def export_jwks(self, private=False, issuer_id="", usage=None): """ Produces a dictionary that later can be easily mapped into a JSON string representing a JWKS. :param private: Whether it should be the private keys or the public - :param issuer: The entity ID. + :param issuer_id: The entity ID. :return: A dictionary with one key: 'keys' """ _issuer = self._get_issuer(issuer_id=issuer_id) @@ -419,6 +439,7 @@ def export_jwks(self, private=False, issuer_id="", usage=None): usage is None or (hasattr(k, 'use') and k.use == usage))]) return {"keys": keys} + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def export_jwks_as_json(self, private=False, issuer_id=""): """ Export a JWKS as a JSON document. @@ -429,6 +450,7 @@ def export_jwks_as_json(self, private=False, issuer_id=""): """ return json.dumps(self.export_jwks(private, issuer_id)) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def import_jwks(self, jwks, issuer_id): """ Imports all the keys that are represented in a JWKS @@ -447,16 +469,18 @@ def import_jwks(self, jwks, issuer_id): httpc_params=self.httpc_params)) self[issuer_id] = _issuer + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def import_jwks_as_json(self, jwks, issuer_id): """ Imports all the keys that are represented in a JWKS expressed as a JSON object :param jwks: JSON representation of a JWKS - :param issuer: Who 'owns' the JWKS + :param issuer_id: Who 'owns' the JWKS """ return self.import_jwks(json.loads(jwks), issuer_id) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def import_jwks_from_file(self, filename, issuer_id): with open(filename) as jwks_file: self.import_jwks_as_json(jwks_file.read(), issuer_id) @@ -495,6 +519,7 @@ def remove_outdated(self, when=0): _before = len(_issuer) _issuer.remove_outdated(when) + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def _add_key(self, keys, issuer_id, use, key_type='', kid='', no_kid_issuer=None, allow_missing_kid=False): @@ -695,6 +720,7 @@ def load(self, info): self._issuers[_issuer_id] = KeyIssuer().load(_issuer_desc) return self + @deprecated_alias(issuer='issuer_id', owner='issuer_id') def key_summary(self, issuer_id): _issuer = self._get_issuer(issuer_id) if _issuer: @@ -706,8 +732,6 @@ def update(self): """ Go through the whole key jar, key issuer by key issuer and update them one by one. - - :param keyjar: The key jar to update """ ids = self._issuers.keys() for _id in ids: @@ -715,6 +739,13 @@ def update(self): _issuer.update() self[_id] = _issuer + @deprecated_alias(issuer='issuer_id', owner='issuer_id') + def rotate_keys(self, key_conf, kid_template="", issuer_id=''): + _issuer = self[issuer_id] + _issuer.rotate_keys(key_conf=key_conf, kid_template=kid_template) + self[issuer_id] = _issuer + return self + # ============================================================================= @@ -807,21 +838,16 @@ def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', rea The keys stored in the KeyJar will be stored under the '' identifier. - :param public_path: A file path to a file that contains a JWKS with public - keys - :param private_path: A file path to a file that contains a JWKS with - private keys. - :param key_defs: A definition of what keys should be created if they are - not already available + :param public_path: A file path to a file that contains a JWKS with public keys + :param private_path: A file path to a file that contains a JWKS with private keys. + :param key_defs: A definition of what keys should be created if they are not already available :param issuer_id: The owner of the keys - :param read_only: This function should not attempt to write anything - to a file system. + :param read_only: This function should not attempt to write anything to a file system. :return: An instantiated :py:class;`oidcmsg.key_jar.KeyJar` instance """ _issuer = init_key_issuer(public_path=public_path, private_path=private_path, - key_defs=key_defs, read_only=read_only, - storage_conf=storage_conf, abstract_storage_cls=abstract_storage_cls) + key_defs=key_defs, read_only=read_only) if _issuer is None: raise ValueError('Could not find any keys') diff --git a/src/cryptojwt/utils.py b/src/cryptojwt/utils.py index abeb727..563933b 100644 --- a/src/cryptojwt/utils.py +++ b/src/cryptojwt/utils.py @@ -1,13 +1,16 @@ import base64 +import functools import importlib import json import re import struct +import warnings from binascii import unhexlify from typing import List from cryptojwt.exception import BadSyntax + # --------------------------------------------------------------------------- # Helper functions @@ -203,6 +206,7 @@ def deser(val): return base64_to_long(_val) + def modsplit(name): """Split importable""" if ':' in name: @@ -226,4 +230,32 @@ def importer(name): def qualified_name(cls): - return cls.__module__ + "." + cls.__name__ \ No newline at end of file + return cls.__module__ + "." + cls.__name__ + + +# This is borrowed from +# https://stackoverflow.com/questions/49802412/how-to-implement-deprecation-in-python-with +# -argument-alias +# cudos to https://stackoverflow.com/users/2357112/user2357112-supports-monica + +def deprecated_alias(**aliases): + def deco(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + rename_kwargs(f.__name__, kwargs, aliases) + return f(*args, **kwargs) + + return wrapper + + return deco + + +def rename_kwargs(func_name, kwargs, aliases): + for alias, new in aliases.items(): + if alias in kwargs: + if new in kwargs: + raise TypeError('{} received both {} and {}'.format( + func_name, alias, new)) + warnings.warn('{} is deprecated; use {}'.format(alias, new), + DeprecationWarning) + kwargs[new] = kwargs.pop(alias) From d04af74603c72e51a0f2aaafa0ac57645a99e3c8 Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 17 Jun 2020 19:00:45 +0200 Subject: [PATCH 38/50] Use deprecated_alias decorator. --- src/cryptojwt/key_jar.py | 47 +------- tests/test_50_argument_alias.py | 183 ++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 46 deletions(-) create mode 100644 tests/test_50_argument_alias.py diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 98f5f48..ca79f55 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -204,52 +204,6 @@ def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs): return _issuer.get(key_use=key_use, key_type=key_type, kid=kid, **kwargs) - # lst = [] - # for bundle in _issuer: - # if key_type: - # if key_use in ['ver', 'dec']: - # _bkeys = bundle.get(key_type, only_active=False) - # else: - # _bkeys = bundle.get(key_type) - # else: - # _bkeys = bundle.keys() - # for key in _bkeys: - # if key.inactive_since and key_use != "sig": - # # Skip inactive keys unless for signature verification - # continue - # if not key.use or use == key.use: - # if kid: - # if key.kid == kid: - # lst.append(key) - # break - # else: - # continue - # else: - # lst.append(key) - # - # # if elliptic curve, have to check if I have a key of the right curve - # if key_type == "EC" and "alg" in kwargs: - # name = "P-{}".format(kwargs["alg"][2:]) # the type - # _lst = [] - # for key in lst: - # if name != key.crv: - # continue - # _lst.append(key) - # lst = _lst - # - # if use == 'enc' and key_type == 'oct' and issuer_id != '': - # # Add my symmetric keys - # _issuer = self._get_issuer('') - # if _issuer: - # for kb in _issuer: - # for key in kb.get(key_type): - # if key.inactive_since: - # continue - # if not key.use or key.use == use: - # lst.append(key) - # - # return lst - @deprecated_alias(issuer='issuer_id', owner='issuer_id') def get_signing_key(self, key_type="", issuer_id="", kid=None, **kwargs): """ @@ -805,6 +759,7 @@ def build_keyjar(key_conf, kid_template="", keyjar=None, issuer_id='', storage_c return keyjar +@deprecated_alias(issuer='issuer_id', owner='issuer_id') def init_key_jar(public_path='', private_path='', key_defs='', issuer_id='', read_only=True, storage_conf=None, abstract_storage_cls=None): """ diff --git a/tests/test_50_argument_alias.py b/tests/test_50_argument_alias.py new file mode 100644 index 0000000..3b3ad70 --- /dev/null +++ b/tests/test_50_argument_alias.py @@ -0,0 +1,183 @@ +import os + +import pytest + +from cryptojwt.jws.jws import JWS +from cryptojwt.jws.jws import factory +from cryptojwt.key_jar import build_keyjar +from cryptojwt.key_jar import init_key_jar + +__author__ = 'Roland Hedberg' + +BASE_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + "test_keys")) +RSAKEY = os.path.join(BASE_PATH, "cert.key") +RSA0 = os.path.join(BASE_PATH, "rsa.key") +EC0 = os.path.join(BASE_PATH, "ec.key") +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +def full_path(local_file): + return os.path.join(BASEDIR, local_file) + + +JWK1 = { + "keys": [ + { + "n": + "zkpUgEgXICI54blf6iWiD2RbMDCOO1jV0VSff1MFFnujM4othfMsad7H1kRo50YM5S_X9TdvrpdOfpz5aBaKFhT6Ziv0nhtcekq1eRl8" + "mjBlvGKCE5XGk-0LFSDwvqgkJoFYInq7bu0a4JEzKs5AyJY75YlGh879k1Uu2Sv3ZZOunfV1O1Orta" + "-NvS-aG_jN5cstVbCGWE20H0vF" + "VrJKNx0Zf-u-aA-syM4uX7wdWgQ" + "-owoEMHge0GmGgzso2lwOYf_4znanLwEuO3p5aabEaFoKNR4K6GjQcjBcYmDEE4CtfRU9AEmhcD1k" + "leiTB9TjPWkgDmT9MXsGxBHf3AKT5w", + "e": "AQAB", "kty": "RSA", "kid": "rsa1" + }, + { + "k": + "YTEyZjBlMDgxMGI4YWU4Y2JjZDFiYTFlZTBjYzljNDU3YWM0ZWNiNzhmNmFlYTNkNTY0NzMzYjE", + "kty": "oct" + }, + ] +} + +KEYDEFS = [ + {"type": "RSA", "key": '', "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]} +] + + +class TestVerifyJWTKeys(object): + @pytest.fixture(autouse=True) + def setup(self): + mkey = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "RSA", "use": ["sig"]}, + {"type": "RSA", "use": ["sig"]}, + ] + + skey = [ + {"type": "RSA", "use": ["sig"]}, + ] + + # Alice has multiple keys + self.alice_keyjar = build_keyjar(mkey) + # Bob has one single keys + self.bob_keyjar = build_keyjar(skey) + self.alice_keyjar['Alice'] = self.alice_keyjar[''] + self.bob_keyjar['Bob'] = self.bob_keyjar[''] + + # To Alice's keyjar add Bob's public keys + self.alice_keyjar.import_jwks( + self.bob_keyjar.export_jwks(issuer='Bob'), 'Bob') + + # To Bob's keyjar add Alice's public keys + self.bob_keyjar.import_jwks( + self.alice_keyjar.export_jwks(issuer='Alice'), 'Alice') + + _jws = JWS('{"aud": "Bob", "iss": "Alice"}', alg='RS256') + sig_key = self.alice_keyjar.get_signing_key('rsa', owner='Alice')[0] + self.sjwt_a = _jws.sign_compact([sig_key]) + + _jws = JWS('{"aud": "Alice", "iss": "Bob"}', alg='RS256') + sig_key = self.bob_keyjar.get_signing_key('rsa', owner='Bob')[0] + self.sjwt_b = _jws.sign_compact([sig_key]) + + def test_no_kid_multiple_keys_no_kid_issuer(self): + a_kids = [k.kid for k in + self.alice_keyjar.get_verify_key(owner='Alice', + key_type='RSA')] + no_kid_issuer = {'Alice': a_kids} + _jwt = factory(self.sjwt_a) + _jwt.jwt.headers['kid'] = '' + keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, + no_kid_issuer=no_kid_issuer) + assert len(keys) == 3 + + def test_aud(self): + self.alice_keyjar.import_jwks(JWK1, issuer='D') + self.bob_keyjar.import_jwks(JWK1, issuer='D') + + _jws = JWS('{"iss": "D", "aud": "A"}', alg='HS256') + sig_key = self.alice_keyjar.get_signing_key('oct', owner='D')[0] + _sjwt = _jws.sign_compact([sig_key]) + + no_kid_issuer = {'D': []} + + _jwt = factory(_sjwt) + + keys = self.bob_keyjar.get_jwt_verify_keys(_jwt.jwt, + no_kid_issuer=no_kid_issuer) + assert len(keys) == 1 + + +PUBLIC_FILE = '{}/public_jwks.json'.format(BASEDIR) +PRIVATE_FILE = '{}/private_jwks.json'.format(BASEDIR) +KEYSPEC = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]} +] +KEYSPEC_2 = [ + {"type": "RSA", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, + {"type": "EC", "crv": "P-384", "use": ["sig"]} +] + + +def test_init_key_jar_dump_private(): + for _file in [PRIVATE_FILE, PUBLIC_FILE]: + if os.path.isfile(_file): + os.unlink(_file) + + # New set of keys, JWKSs with keys and public written to file + _keyjar = init_key_jar(private_path=PRIVATE_FILE, key_defs=KEYSPEC, owner='https://example.com') + assert list(_keyjar.owners()) == ['https://example.com'] + + # JWKS will be read from disc, not created new + _keyjar2 = init_key_jar(private_path=PRIVATE_FILE, key_defs=KEYSPEC) + assert list(_keyjar2.owners()) == [''] + + +def test_init_key_jar_update(): + for _file in [PRIVATE_FILE, PUBLIC_FILE]: + if os.path.isfile(_file): + os.unlink(_file) + + # New set of keys, JWKSs with keys and public written to file + _keyjar_1 = init_key_jar(private_path=PRIVATE_FILE, key_defs=KEYSPEC, + owner='https://example.com', + public_path=PUBLIC_FILE, read_only=False) + assert list(_keyjar_1.owners()) == ['https://example.com'] + + _keyjar_2 = init_key_jar(private_path=PRIVATE_FILE, key_defs=KEYSPEC_2, + public_path=PUBLIC_FILE) + + # Both should contain the same RSA key + rsa1 = _keyjar_1.get_signing_key('RSA', 'https://example.com') + rsa2 = _keyjar_2.get_signing_key('RSA', '') + + assert len(rsa1) == 1 + assert len(rsa2) == 1 + assert rsa1[0] == rsa2[0] + + # keyjar1 should only contain one EC key while keyjar2 should contain 2. + + ec1 = _keyjar_1.get_signing_key('EC', 'https://example.com') + ec2 = _keyjar_2.get_signing_key('EC', '') + assert len(ec1) == 1 + assert len(ec2) == 2 + + # The file on disc should not have changed + _keyjar_3 = init_key_jar(private_path=PRIVATE_FILE) + + assert len(_keyjar_3.get_signing_key('RSA')) == 1 + assert len(_keyjar_3.get_signing_key('EC')) == 1 + + _keyjar_4 = init_key_jar(private_path=PRIVATE_FILE, key_defs=KEYSPEC_2, + public_path=PUBLIC_FILE, read_only=False) + + # Now it should + _keyjar_5 = init_key_jar(private_path=PRIVATE_FILE) + + assert len(_keyjar_5.get_signing_key('RSA')) == 1 + assert len(_keyjar_5.get_signing_key('EC')) == 2 From 2ee6ba7de741f68efdef7bfe6597f6cfebaea1e8 Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 17 Jun 2020 19:08:40 +0200 Subject: [PATCH 39/50] Removed SimpleList was a leftover. --- src/cryptojwt/serialize/__init__.py | 47 ----------------------------- 1 file changed, 47 deletions(-) diff --git a/src/cryptojwt/serialize/__init__.py b/src/cryptojwt/serialize/__init__.py index 7d7ec6b..e69de29 100644 --- a/src/cryptojwt/serialize/__init__.py +++ b/src/cryptojwt/serialize/__init__.py @@ -1,47 +0,0 @@ - -class SimpleList(): - def __init__(self, value=None): - if value is None: - self.db = [] - else: - self.set(value) - - def __len__(self): - return len(self.db) - - def __contains__(self, item): - return item in self.db - - def __del__(self): - del self.db - - def __iter__(self): - for i in self.db: - yield i - - def __str__(self): - return str(self.db) - - def append(self, item): - self.db.append(item) - - def extend(self, items): - self.db.extend(items) - - def remove(self, item): - self.db.remove(item) - - def get(self): - return self.db - - def set(self, value): - if isinstance(value, list): - self.db = value - else: - raise ValueError("Wrong value type") - - def copy(self): - return self.db[:] - - def close(self): - return From 4085ad4a72c73f8714439aa2d94465a45875d5df Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 17 Jun 2020 19:09:20 +0200 Subject: [PATCH 40/50] Removed QUOTE converter class. Not CryptoJWT specific. --- src/cryptojwt/serialize/item.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/cryptojwt/serialize/item.py b/src/cryptojwt/serialize/item.py index 1556f82..a087c8d 100644 --- a/src/cryptojwt/serialize/item.py +++ b/src/cryptojwt/serialize/item.py @@ -1,6 +1,4 @@ import json -from urllib.parse import quote_plus -from urllib.parse import unquote_plus from cryptojwt import key_issuer @@ -16,12 +14,3 @@ def deserialize(self, spec: str) -> key_issuer.KeyIssuer: _dict = json.loads(spec) issuer = key_issuer.KeyIssuer().load(_dict) return issuer - - -class QUOTE: - @staticmethod - def serialize(item: str) -> str: - return quote_plus(item) - - def deserialize(self, spec: str) -> str: - return unquote_plus(spec) From 7cb595bcc7d41e4bfd6ad320c9f030b410b153d6 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Thu, 18 Jun 2020 09:56:17 +0200 Subject: [PATCH 41/50] raise KeyError if key KeyIssuer is not found --- src/cryptojwt/key_jar.py | 5 ++++- tests/test_04_key_jar.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index ca79f55..449f1ca 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -276,7 +276,10 @@ def __getitem__(self, issuer_id=''): :param issuer_id: The entity ID :return: A KeyIssuer instance """ - return self._get_issuer(issuer_id) + _iss = self._get_issuer(issuer_id) + if _iss is None: + raise KeyError(issuer_id) + return _iss @deprecated_alias(issuer='issuer_id', owner='issuer_id') def __setitem__(self, issuer_id, key_issuer): diff --git a/tests/test_04_key_jar.py b/tests/test_04_key_jar.py index 6430b37..e1ac13d 100755 --- a/tests/test_04_key_jar.py +++ b/tests/test_04_key_jar.py @@ -733,7 +733,8 @@ def test_get_wrong_owner(): assert kj.get('sig', 'rsa') == [] assert 'https://delphi.example.com' not in kj - assert kj['https://delphi.example.com'] == None + with pytest.raises(KeyError): + kj['https://delphi.example.com'] def test_match_owner(): From a7db2990eeaf23400ecafeb8594d3bbca6a3edd6 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Thu, 18 Jun 2020 11:43:01 +0200 Subject: [PATCH 42/50] fix is not check --- src/cryptojwt/key_jar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 449f1ca..2d0a5c5 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -256,7 +256,7 @@ def get_issuer_keys(self, issuer_id): :return: A possibly empty list of keys """ _issuer = self._get_issuer(issuer_id) - if _issuer: + if _issuer is not None: return _issuer.all_keys() else: return [] From 610897a6aea36de195b9db3d4bcff14f2971cffa Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Thu, 18 Jun 2020 11:48:19 +0200 Subject: [PATCH 43/50] consistency did not kill the cat --- src/cryptojwt/key_jar.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 2d0a5c5..81c8221 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -93,12 +93,12 @@ def _get_issuer(self, issuer_id: str) -> Optional[KeyIssuer]: @deprecated_alias(issuer='issuer_id', owner='issuer_id') def _add_issuer(self, issuer_id) -> KeyIssuer: - _iss = KeyIssuer(ca_certs=self.ca_certs, name=issuer_id, - keybundle_cls=self.keybundle_cls, - remove_after=self.remove_after, - httpc=self.httpc, httpc_params=self.httpc_params) - self._issuers[issuer_id] = _iss - return _iss + _issuer = KeyIssuer(ca_certs=self.ca_certs, name=issuer_id, + keybundle_cls=self.keybundle_cls, + remove_after=self.remove_after, + httpc=self.httpc, httpc_params=self.httpc_params) + self._issuers[issuer_id] = _issuer + return _issuer def items(self): """ @@ -121,10 +121,10 @@ def return_issuer(self, issuer_id): :param issuer_id: The issuer ID :return: A KeyIssuer instance """ - _iss = self._get_issuer(issuer_id) - if not _iss: + _issuer = self._get_issuer(issuer_id) + if not _issuer: return self._add_issuer(issuer_id) - return _iss + return _issuer @deprecated_alias(issuer='issuer_id', owner='issuer_id') def add_url(self, issuer_id: str, url: str, **kwargs) -> KeyBundle: @@ -276,10 +276,10 @@ def __getitem__(self, issuer_id=''): :param issuer_id: The entity ID :return: A KeyIssuer instance """ - _iss = self._get_issuer(issuer_id) - if _iss is None: + _issuer = self._get_issuer(issuer_id) + if _issuer is None: raise KeyError(issuer_id) - return _iss + return _issuer @deprecated_alias(issuer='issuer_id', owner='issuer_id') def __setitem__(self, issuer_id, key_issuer): From 7b5d58bb6f802b95174781d295c24bbd3168721a Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Thu, 18 Jun 2020 11:49:10 +0200 Subject: [PATCH 44/50] use is/is not --- src/cryptojwt/key_jar.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 81c8221..887f68d 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -122,7 +122,7 @@ def return_issuer(self, issuer_id): :return: A KeyIssuer instance """ _issuer = self._get_issuer(issuer_id) - if not _issuer: + if _issuer is None: return self._add_issuer(issuer_id) return _issuer @@ -256,10 +256,9 @@ def get_issuer_keys(self, issuer_id): :return: A possibly empty list of keys """ _issuer = self._get_issuer(issuer_id) - if _issuer is not None: - return _issuer.all_keys() - else: + if _issuer is None: return [] + return _issuer.all_keys() @deprecated_alias(issuer='issuer_id', owner='issuer_id') def __contains__(self, issuer_id): @@ -680,7 +679,7 @@ def load(self, info): @deprecated_alias(issuer='issuer_id', owner='issuer_id') def key_summary(self, issuer_id): _issuer = self._get_issuer(issuer_id) - if _issuer: + if _issuer is not None: return _issuer.key_summary() raise KeyError('Unknown Issuer ID: "{}"'.format(issuer_id)) From c49f0e81b547ccb725ad4cc298c025fab72f7e71 Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 18 Jun 2020 13:41:36 +0200 Subject: [PATCH 45/50] Placeholder right now. --- src/cryptojwt/jwk/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cryptojwt/jwk/__init__.py b/src/cryptojwt/jwk/__init__.py index 9cc2275..63582af 100644 --- a/src/cryptojwt/jwk/__init__.py +++ b/src/cryptojwt/jwk/__init__.py @@ -248,6 +248,9 @@ def appropriate_for(self, usage, **kwargs): elif self.key_ops: return usage in self.key_ops + def update(self): + pass + def pems_to_x5c(cert_chain): """ From 9d1eadaecd464bd1c563260232481e5c8d7972c8 Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 18 Jun 2020 13:41:58 +0200 Subject: [PATCH 46/50] Wrong reference --- src/cryptojwt/key_issuer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cryptojwt/key_issuer.py b/src/cryptojwt/key_issuer.py index 6d1a61a..2df6474 100755 --- a/src/cryptojwt/key_issuer.py +++ b/src/cryptojwt/key_issuer.py @@ -332,9 +332,9 @@ def get(self, key_use, key_type="", kid=None, alg='', **kwargs): def copy(self): """ - Make deep copy of this key jar. + Make deep copy of this key issuer. - :return: A :py:class:`oidcmsg.key_jar.KeyJar` instance + :return: A :py:class:`oidcmsg.key_issuer.KeyIssuer` instance """ ki = KeyIssuer() ki._bundles = [kb.copy() for kb in self._bundles] From 6e20a6094ff93ee92223bc38c42f1fc4f2ebe0db Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 18 Jun 2020 13:42:32 +0200 Subject: [PATCH 47/50] Check against None --- src/cryptojwt/key_jar.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index ca79f55..7e27757 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -122,7 +122,7 @@ def return_issuer(self, issuer_id): :return: A KeyIssuer instance """ _iss = self._get_issuer(issuer_id) - if not _iss: + if _iss is None: return self._add_issuer(issuer_id) return _iss @@ -169,7 +169,7 @@ def add_kb(self, issuer_id, kb): """ issuer = self.return_issuer(issuer_id) issuer.add_kb(kb) - self[issuer_id] = issuer + self._issuers[issuer_id] = issuer @deprecated_alias(issuer='issuer_id', owner='issuer_id') def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs): @@ -183,11 +183,6 @@ def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs): :return: A possibly empty list of keys """ - if key_use in ["dec", "enc"]: - use = "enc" - else: - use = "sig" - _issuer = None if issuer_id != "": _issuer = self._get_issuer(issuer_id) @@ -256,22 +251,23 @@ def get_issuer_keys(self, issuer_id): :return: A possibly empty list of keys """ _issuer = self._get_issuer(issuer_id) - if _issuer: - return _issuer.all_keys() + if _issuer is None: + raise KeyError(issuer_id) else: - return [] + return _issuer.all_keys() @deprecated_alias(issuer='issuer_id', owner='issuer_id') def __contains__(self, issuer_id): - if self._get_issuer(issuer_id): - return True - else: + _iss = self._get_issuer(issuer_id) + if _iss is None: return False + else: + return True @deprecated_alias(issuer='issuer_id', owner='issuer_id') def __getitem__(self, issuer_id=''): """ - Get all the KeyIssuer with the name == issuer_id + Get the KeyIssuer with the name == issuer_id :param issuer_id: The entity ID :return: A KeyIssuer instance @@ -677,7 +673,7 @@ def load(self, info): @deprecated_alias(issuer='issuer_id', owner='issuer_id') def key_summary(self, issuer_id): _issuer = self._get_issuer(issuer_id) - if _issuer: + if _issuer is not None: return _issuer.key_summary() raise KeyError('Unknown Issuer ID: "{}"'.format(issuer_id)) From 9cf2593e152011fef6ef7d0ca25dee37d1f6ac35 Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 18 Jun 2020 13:44:30 +0200 Subject: [PATCH 48/50] Both ways should return the same result. --- tests/test_04_key_jar.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_04_key_jar.py b/tests/test_04_key_jar.py index 6430b37..4ff8d54 100755 --- a/tests/test_04_key_jar.py +++ b/tests/test_04_key_jar.py @@ -973,3 +973,15 @@ def test_contains(): assert 'Bob' in kj assert 'David' not in kj + + +def test_similar(): + ISSUER = "xyzzy" + + kj = KeyJar() + kb = KeyBundle(JWK2) + kj.add_kb(issuer=ISSUER, kb=kb) + + keys1 = kj.get_issuer_keys(ISSUER) + keys2 = kj[ISSUER].all_keys() + assert keys1 == keys2 From 4f6d4a5f5cff1f41663175b683701ecc12ef7b4b Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Thu, 18 Jun 2020 15:20:25 +0200 Subject: [PATCH 49/50] raise KeyError if issuer is not found --- src/cryptojwt/key_jar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 11733d7..1f7d9e1 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -252,7 +252,7 @@ def get_issuer_keys(self, issuer_id): """ _issuer = self._get_issuer(issuer_id) if _issuer is None: - return [] + raise KeyError(issuer_id) return _issuer.all_keys() @deprecated_alias(issuer='issuer_id', owner='issuer_id') From 674fff2065d0c795db5324c33ed3c8cad6513199 Mon Sep 17 00:00:00 2001 From: roland Date: Mon, 22 Jun 2020 09:15:09 +0200 Subject: [PATCH 50/50] If no such issuer return None. --- src/cryptojwt/key_jar.py | 2 +- tests/test_04_key_jar.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cryptojwt/key_jar.py b/src/cryptojwt/key_jar.py index 1f7d9e1..a393869 100755 --- a/src/cryptojwt/key_jar.py +++ b/src/cryptojwt/key_jar.py @@ -364,7 +364,7 @@ def find(self, source, issuer_id=None): else: _issuer = self._get_issuer(issuer_id) if _issuer is None: - return [] + return None else: res = _issuer.find(source) diff --git a/tests/test_04_key_jar.py b/tests/test_04_key_jar.py index 8301e83..24e253f 100755 --- a/tests/test_04_key_jar.py +++ b/tests/test_04_key_jar.py @@ -773,7 +773,7 @@ def test_find(): assert kj.find('{}'.format(_path), 'Alice') assert kj.find('https://example.com', 'Alice') == [] - assert kj.find('{}'.format(_path), 'Bob') == [] + assert kj.find('{}'.format(_path), 'Bob') is None def test_get_decrypt_keys():