Projet

Général

Profil

0001-idp_oidc-add-ecdsa-support-26253.patch

Paul Marillonnet, 09 avril 2020 09:51

Télécharger (9,99 ko)

Voir les différences:

Subject: [PATCH] idp_oidc: add ecdsa support (#26253)

 src/authentic2_idp_oidc/models.py |  4 +-
 src/authentic2_idp_oidc/utils.py  | 28 ++++++++++----
 src/authentic2_idp_oidc/views.py  |  2 +-
 tests/test_idp_oidc.py            | 61 ++++++++++++++++++++++++++-----
 4 files changed, 76 insertions(+), 19 deletions(-)
src/authentic2_idp_oidc/models.py
72 72

  
73 73
    ALGO_RSA = 1
74 74
    ALGO_HMAC = 2
75
    ALGO_EC = 3
75 76
    ALGO_CHOICES = [
76 77
        (ALGO_HMAC, _('HMAC')),
77 78
        (ALGO_RSA, _('RSA')),
79
        (ALGO_EC, _('EC')),
78 80
    ]
79 81
    FLOW_AUTHORIZATION_CODE = 1
80 82
    FLOW_IMPLICIT = 2
......
148 150
            utils.get_jwkset()
149 151
        except ImproperlyConfigured:
150 152
            return [(algo_id, algo_name) for algo_id, algo_name in OIDCClient.ALGO_CHOICES
151
                    if algo_id != OIDCClient.ALGO_RSA]
153
                    if algo_id not in (OIDCClient.ALGO_RSA, OIDCClient.ALGO_EC)]
152 154
        return OIDCClient.ALGO_CHOICES
153 155

  
154 156
    idtoken_algo = models.PositiveIntegerField(
src/authentic2_idp_oidc/utils.py
53 53
    return jwkset
54 54

  
55 55

  
56
def get_first_rsa_sig_key():
57
    for key in get_jwkset()['keys']:
58
        if key._params['kty'] != 'RSA':
59
            continue
60
        use = key._params.get('use')
61
        if use is None or use == 'sig':
62
            return key
56
def get_first_sig_key_by_type(kty=None):
57
    if kty:
58
        for key in get_jwkset()['keys']:
59
            if key._params['kty'] != kty:
60
                continue
61
            use = key._params.get('use')
62
            if use is None or use == 'sig':
63
                return key
63 64
    return None
64 65

  
65 66

  
67
def get_first_rsa_sig_key():
68
    return get_first_sig_key_by_type('RSA')
69

  
70

  
71
def get_first_ec_sig_key():
72
    return get_first_sig_key_by_type('EC')
73

  
74

  
66 75
def make_idtoken(client, claims):
67 76
    '''Make a serialized JWT targeted for this client'''
68 77
    if client.idtoken_algo == client.ALGO_HMAC:
......
75 84
        header['kid'] = jwk.key_id
76 85
        if jwk is None:
77 86
            raise ImproperlyConfigured('no RSA key for signature operation in A2_IDP_OIDC_JWKSET')
87
    elif client.idtoken_algo == client.ALGO_EC:
88
        header = {'alg': 'ES256'}
89
        jwk = get_first_ec_sig_key()
90
        if jwk is None:
91
            raise ImproperlyConfigured('no EC key for signature operation in A2_IDP_OIDC_JWKSET')
78 92
    else:
79 93
        raise NotImplementedError
80 94
    jwt = JWT(header=header, claims=claims)
src/authentic2_idp_oidc/views.py
63 63
            'clien_secret_post', 'client_secret_basic',
64 64
        ],
65 65
        'id_token_signing_alg_values_supported': [
66
            'RS256', 'HS256',
66
            'RS256', 'HS256', 'ES256',
67 67
        ],
68 68
        'userinfo_endpoint': request.build_absolute_uri(reverse('oidc-user-info')),
69 69
        'frontchannel_logout_supported': True,
tests/test_idp_oidc.py
38 38

  
39 39
from authentic2.models import Attribute, AuthorizedRole
40 40
from authentic2_idp_oidc.models import OIDCClient, OIDCAuthorization, OIDCCode, OIDCAccessToken, OIDCClaim
41
from authentic2_idp_oidc.utils import make_sub, get_first_rsa_sig_key, base64url
41
from authentic2_idp_oidc.utils import base64url
42
from authentic2_idp_oidc.utils import get_first_rsa_sig_key
43
from authentic2_idp_oidc.utils import get_first_ec_sig_key
44
from authentic2_idp_oidc.utils import make_sub
42 45
from authentic2.a2_rbac.utils import get_default_ou
43 46
from authentic2.utils import make_url
44 47
from authentic2_auth_oidc.utils import parse_timestamp
......
60 63
            "n": "0lN6CiJGFD8BSPV_azLoEl6Nq-WlHkU743D5rqvzw1sOaxstMGxAhVk2YIhWwfvapV6XjO_yvc4778VBTELOdjRw6BGUdBJepdwkL__TPyjEVhqMQj9MKhEU4GUy9w0Lsilb5D01kfrOKpmdcYw4jhcDvb0H4-LZgh1Vk84vF4WaQCUg_AX4drVDQOjoU8kuWIM8gz9w6zEsbIw-gtMRpFwS8ncA0zDX5VfyC77iMxzFftDIP2gM5GvdevMzvP9IRkRRBhP9vV4JchBFPHSA9OPJcnySjJJNW6aAJn6P6JasN1z68khjufM09J8UzmLAZYOq7gUG95Ox1KsV-g337Q",
61 64
            "e": "AQAB",
62 65
            "p": "-Nyj_Sw3f2HUqSssCZv84y7b3blOtGGAhfYN_JtGfcTQv2bOtxrIUzeonCi-Z_1W4hO10tqxJcOB0ibtDqkDlLhnLaIYOBfriITRFK83EJG5sC-0KTmFzUXFTA2aMc1QgP-Fu6gUfQpPqLgWxhx8EFhkBlBZshKU5-C-385Sco0"
66
        },
67
        {
68
            "kty": "EC",
69
            "d": "wwULaR9UYWZW6U2oEbkz3sO1lhPSj6DyA6e7PiUfhog",
70
            "use": "sig",
71
            "crv": "P-256",
72
            "x": "HZMHZkX-63heqA5pvWn-UR7bgcXZNEcQa5wfvG_BzTw",
73
            "y": "SUCuwjjiyKvGq5Odr0sjDqjha_CBqks0JQFrR7Ei5OQ",
74
            "alg": "ES256"
63 75
        }
64 76
    ]
65 77
}
......
94 106
    {
95 107
        'idtoken_algo': OIDCClient.ALGO_HMAC,
96 108
    },
109
    {
110
        'idtoken_algo': OIDCClient.ALGO_EC,
111
    },
97 112
    {
98 113
        'authorization_mode': OIDCClient.AUTHORIZATION_MODE_NONE,
99 114
    },
......
119 134

  
120 135

  
121 136
@pytest.fixture(params=OIDC_CLIENT_PARAMS)
122
def oidc_client(request, superuser, app, simple_user, media):
137
def oidc_client(request, superuser, app, simple_user, media, oidc_settings):
123 138
    Attribute.objects.create(
124 139
        name='cityscape_image',
125 140
        label='cityscape',
......
265 280
        access_token = query['access_token'][0]
266 281
        id_token = query['id_token'][0]
267 282

  
268
    if oidc_client.idtoken_algo == oidc_client.ALGO_RSA:
269
        key = JWKSet.from_json(app.get(reverse('oidc-certs')).content)
283
    if oidc_client.idtoken_algo in (oidc_client.ALGO_RSA, oidc_client.ALGO_EC):
284
        keyset = JWKSet.from_json(app.get(reverse('oidc-certs')).content)
285
        for k in keyset.get('keys'):
286
            if {
287
                'RSA': oidc_client.ALGO_RSA,
288
                'EC': oidc_client.ALGO_EC
289
            }.get(k.key_type) == oidc_client.idtoken_algo:
290
                algs=[{
291
                    oidc_client.ALGO_RSA: 'RS256',
292
                    oidc_client.ALGO_EC: 'ES256'
293
                }.get(oidc_client.idtoken_algo)]
294
                key = k
295
                break
270 296
    elif oidc_client.idtoken_algo == oidc_client.ALGO_HMAC:
271 297
        k = base64.b64encode(oidc_client.client_secret.encode('utf-8'))
272 298
        key = JWK(kty='oct', k=force_text(k))
299
        algs = ['HS256']
273 300
    else:
274 301
        raise NotImplementedError
275
    jwt = JWT(jwt=id_token, key=key)
302
    jwt = JWT(jwt=id_token, key=key, algs=algs)
276 303
    claims = json.loads(jwt.claims)
277 304
    assert set(claims) >= set(['iss', 'sub', 'aud', 'exp', 'iat', 'nonce', 'auth_time', 'acr'])
278 305
    assert claims['nonce'] == 'yyy'
......
871 898
        query = urlparse.parse_qs(location.fragment)
872 899
        id_token = query['id_token'][0]
873 900

  
874
    if oidc_client.idtoken_algo == oidc_client.ALGO_RSA:
875
        key = JWKSet.from_json(app.get(reverse('oidc-certs')).content)
901
    if oidc_client.idtoken_algo in (oidc_client.ALGO_RSA, oidc_client.ALGO_EC):
902
        keyset = JWKSet.from_json(app.get(reverse('oidc-certs')).content)
903
        for k in keyset.get('keys'):
904
            if {
905
                'RSA': oidc_client.ALGO_RSA,
906
                'EC': oidc_client.ALGO_EC
907
            }.get(k.key_type) == oidc_client.idtoken_algo:
908
                algs=[{
909
                    oidc_client.ALGO_RSA: 'RS256',
910
                    oidc_client.ALGO_EC: 'ES256'
911
                }.get(oidc_client.idtoken_algo)]
912
                key = k
913
                break
876 914
    elif oidc_client.idtoken_algo == oidc_client.ALGO_HMAC:
877 915
        k = base64.b64encode(oidc_client.client_secret.encode('utf-8'))
878 916
        key = JWK(kty='oct', k=force_text(k))
917
        algs = ['HS256']
879 918
    else:
880 919
        raise NotImplementedError
881
    jwt = JWT(jwt=id_token, key=key)
920
    jwt = JWT(jwt=id_token, key=key, algs=algs)
882 921
    claims = json.loads(jwt.claims)
883 922
    if login_first:
884 923
        assert claims['acr'] == '0'
......
1261 1300
        jwk = JWK(kty='oct', k=force_text(k))
1262 1301
    elif oidc_client.idtoken_algo == OIDCClient.ALGO_RSA:
1263 1302
        jwk = get_first_rsa_sig_key()
1303
    elif oidc_client.idtoken_algo == OIDCClient.ALGO_EC:
1304
        jwk = get_first_ec_sig_key()
1264 1305

  
1265 1306
    # 1. test in-request client credentials
1266 1307
    params = {
......
1275 1316
    token = response.json['id_token']
1276 1317
    header, payload, signature = token.split('.')
1277 1318
    jwt = JWT()
1319
    # jwt deserialization implicitly checks the token signature:
1278 1320
    jwt.deserialize(token, key=jwk)
1279 1321
    claims = json.loads(jwt.claims)
1280
    # xxx already verified by jwcrypto deserialization?
1281 1322
    assert set(claims) == set(['acr', 'aud', 'auth_time', 'exp', 'iat', 'iss', 'sub'])
1282 1323
    assert all(claims.values())
1283 1324

  
......
1290 1331
    token = response.json['id_token']
1291 1332
    header, payload, signature = token.split('.')
1292 1333
    jwt = JWT()
1334
    # jwt deserialization implicitly checks the token signature:
1293 1335
    jwt.deserialize(token, key=jwk)
1294 1336
    claims = json.loads(jwt.claims)
1295
    # xxx already verified by jwcrypto deserialization?
1296 1337
    assert set(claims) == set(['acr', 'aud', 'auth_time', 'exp', 'iat', 'iss', 'sub'])
1297 1338
    assert all(claims.values())
1298 1339

  
1299
-