From 8887f09ba8ed020d26a95edc560c3b868f0917ee Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 31 May 2016 16:48:19 -0400 Subject: [PATCH] Use the instance service key for registry JWT signing --- app.py | 3 + auth/registry_jwt_auth.py | 53 ++------ boot.py | 9 +- config.py | 22 +-- data/users/externaljwt.py | 12 +- endpoints/key_server.py | 28 +--- endpoints/oauthlogin.py | 2 +- endpoints/v2/v2auth.py | 12 +- endpoints/web.py | 1 - initdb.py | 14 ++ test/data/registry_v2_auth.crt | 20 --- test/data/registry_v2_auth_private.key | 27 ---- test/data/test.db | Bin 1175552 -> 1175552 bytes test/data/test.kid | 1 + test/data/test.pem | 52 +++---- test/data/test.pem.pub | 1 - test/registry_tests.py | 33 +---- test/test_registry_v2_auth.py | 149 +++++++++++++++------ test/testconfig.py | 6 +- util/expiresdict.py | 55 ++++++++ util/registry/torrent.py | 16 +-- util/secscan/api.py | 11 +- util/security/instancekeys.py | 81 +++++++++++ util/security/{strictjwt.py => jwtutil.py} | 26 ++++ util/security/registry_jwt.py | 90 ++++++++++--- workers/service_key_worker.py | 11 +- 26 files changed, 457 insertions(+), 278 deletions(-) delete mode 100644 test/data/registry_v2_auth.crt delete mode 100644 test/data/registry_v2_auth_private.key create mode 100644 test/data/test.kid delete mode 100644 test/data/test.pem.pub create mode 100644 util/expiresdict.py create mode 100644 util/security/instancekeys.py rename util/security/{strictjwt.py => jwtutil.py} (66%) diff --git a/app.py b/app.py index 88272056d..d01a7ada9 100644 --- a/app.py +++ b/app.py @@ -32,6 +32,7 @@ from util.config.oauth import (GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuth DexOAuthConfig) from util.security.signing import Signer +from util.security.instancekeys import InstanceKeys from util.saas.cloudwatch import start_cloudwatch_sender from util.saas.metricqueue import MetricQueue from util.config.provider import get_config_provider @@ -178,6 +179,8 @@ authentication = UserAuthentication(app, config_provider, OVERRIDE_CONFIG_DIRECT userevents = UserEventsBuilderModule(app) superusers = SuperUserManager(app) signer = Signer(app, config_provider) +instance_keys = InstanceKeys(app) + start_cloudwatch_sender(metric_queue, app) tf = app.config['DB_TRANSACTION_FACTORY'] diff --git a/auth/registry_jwt_auth.py b/auth/registry_jwt_auth.py index 79f240187..a4c48d749 100644 --- a/auth/registry_jwt_auth.py +++ b/auth/registry_jwt_auth.py @@ -1,29 +1,23 @@ import logging -import re from functools import wraps from jsonschema import validate, ValidationError from flask import request, url_for from flask.ext.principal import identity_changed, Identity -from cryptography.x509 import load_pem_x509_certificate -from cryptography.hazmat.backends import default_backend -from cachetools import lru_cache -from app import app, get_app_url +from app import app, get_app_url, instance_keys from .auth_context import set_grant_context, get_grant_context from .permissions import repository_read_grant, repository_write_grant from util.names import parse_namespace_repository from util.http import abort -from util.security import strictjwt -from util.security.registry_jwt import ANONYMOUS_SUB +from util.security.registry_jwt import (ANONYMOUS_SUB, decode_bearer_token, + InvalidBearerTokenException) from data import model logger = logging.getLogger(__name__) - -TOKEN_REGEX = re.compile(r'^Bearer (([a-zA-Z0-9+/]+\.)+[a-zA-Z0-9+-_/]+)$') CONTEXT_KINDS = ['user', 'token', 'oauth'] ACCESS_SCHEMA = { @@ -142,34 +136,18 @@ def get_auth_headers(repository=None, scopes=None): return headers -def identity_from_bearer_token(bearer_token, max_signed_s, public_key): +def identity_from_bearer_token(bearer_token): """ Process a bearer token and return the loaded identity, or raise InvalidJWTException if an identity could not be loaded. Expects tokens and grants in the format of the Docker registry v2 auth spec: https://docs.docker.com/registry/spec/auth/token/ """ logger.debug('Validating auth header: %s', bearer_token) - # Extract the jwt token from the header - match = TOKEN_REGEX.match(bearer_token) - if match is None: - raise InvalidJWTException('Invalid bearer token format') - - encoded = match.group(1) - logger.debug('encoded JWT: %s', encoded) - - # Load the JWT returned. try: - expected_issuer = app.config['JWT_AUTH_TOKEN_ISSUER'] - audience = app.config['SERVER_HOSTNAME'] - max_exp = strictjwt.exp_max_s_option(max_signed_s) - payload = strictjwt.decode(encoded, public_key, algorithms=['RS256'], audience=audience, - issuer=expected_issuer, options=max_exp) - except strictjwt.InvalidTokenError: - logger.exception('Invalid token reason') - raise InvalidJWTException('Invalid token') - - if not 'sub' in payload: - raise InvalidJWTException('Missing sub field in JWT') + payload = decode_bearer_token(bearer_token, instance_keys) + except InvalidBearerTokenException as bte: + logger.exception('Invalid bearer token: %s', bte) + raise InvalidJWTException(bte) loaded_identity = Identity(payload['sub'], 'signed_jwt') @@ -203,13 +181,6 @@ def identity_from_bearer_token(bearer_token, max_signed_s, public_key): return loaded_identity, payload.get('context', default_context) -@lru_cache(maxsize=1) -def load_public_key(certificate_file_path): - with open(certificate_file_path) as cert_file: - cert_obj = load_pem_x509_certificate(cert_file.read(), default_backend()) - return cert_obj.public_key() - - def process_registry_jwt_auth(scopes=None): def inner(func): @wraps(func) @@ -217,14 +188,8 @@ def process_registry_jwt_auth(scopes=None): logger.debug('Called with params: %s, %s', args, kwargs) auth = request.headers.get('authorization', '').strip() if auth: - max_signature_seconds = app.config.get('JWT_AUTH_MAX_FRESH_S', 3660) - certificate_file_path = app.config['JWT_AUTH_CERTIFICATE_PATH'] - public_key = load_public_key(certificate_file_path) - try: - extracted_identity, context = identity_from_bearer_token(auth, max_signature_seconds, - public_key) - + extracted_identity, context = identity_from_bearer_token(auth) identity_changed.send(app, identity=extracted_identity) set_grant_context(context) logger.debug('Identity changed to %s', extracted_identity.id) diff --git a/boot.py b/boot.py index a1990d253..4fd826425 100644 --- a/boot.py +++ b/boot.py @@ -47,15 +47,16 @@ def setup_jwt_proxy(): return # Generate the key for this Quay instance to use. - minutes_until_expiration = app.config.get('QUAY_SERVICE_KEY_EXPIRATION', 120) + minutes_until_expiration = app.config.get('INSTANCE_SERVICE_KEY_EXPIRATION', 120) expiration = datetime.now() + timedelta(minutes=minutes_until_expiration) - quay_key, quay_key_id = generate_key('quay', get_audience(), expiration_date=expiration) + quay_key, quay_key_id = generate_key(app.config['INSTANCE_SERVICE_KEY_SERVICE'], + get_audience(), expiration_date=expiration) - with open('conf/quay.kid', mode='w') as f: + with open(app.config['INSTANCE_SERVICE_KEY_KID_LOCATION'], mode='w') as f: f.truncate(0) f.write(quay_key_id) - with open('conf/quay.pem', mode='w') as f: + with open(app.config['INSTANCE_SERVICE_KEY_LOCATION'], mode='w') as f: f.truncate(0) f.write(quay_key.exportKey()) diff --git a/config.py b/config.py index f4b59f208..1c6cb554c 100644 --- a/config.py +++ b/config.py @@ -260,10 +260,7 @@ class DefaultConfig(object): SIGNED_GRANT_EXPIRATION_SEC = 60 * 60 * 24 # One day to complete a push/pull # Registry v2 JWT Auth config - JWT_AUTH_MAX_FRESH_S = 60 * 60 + 60 # At most signed for one hour, accounting for clock skew - JWT_AUTH_TOKEN_ISSUER = 'quay-test-issuer' - JWT_AUTH_CERTIFICATE_PATH = None - JWT_AUTH_PRIVATE_KEY_PATH = None + REGISTRY_JWT_AUTH_MAX_FRESH_S = 60 * 60 + 60 # At most signed one hour, accounting for clock skew # The URL endpoint to which we redirect OAuth when generating a token locally. LOCAL_OAUTH_HANDLER = '/oauth/localapp' @@ -340,14 +337,23 @@ class DefaultConfig(object): # lowest user in the database will be used. SERVICE_LOG_ACCOUNT_ID = None - # The location of the private key generated for this instance + # The service key ID for the instance service. + # NOTE: If changed, jwtproxy_conf.yaml.jnj must also be updated. + INSTANCE_SERVICE_KEY_SERVICE = 'quay' + + # The location of the key ID file generated for this instance. + INSTANCE_SERVICE_KEY_KID_LOCATION = 'conf/quay.kid' + + # The location of the private key generated for this instance. + # NOTE: If changed, jwtproxy_conf.yaml.jnj must also be updated. INSTANCE_SERVICE_KEY_LOCATION = 'conf/quay.pem' - # This instance's service key expiration in minutes + # This instance's service key expiration in minutes. INSTANCE_SERVICE_KEY_EXPIRATION = 120 - # Number of minutes between expiration refresh in minutes - INSTANCE_SERVICE_KEY_REFRESH = 60 + # Number of minutes between expiration refresh in minutes. Should be the expiration / 2 minus + # some additional window time. + INSTANCE_SERVICE_KEY_REFRESH = 55 # The whitelist of client IDs for OAuth applications that allow for direct login. DIRECT_OAUTH_CLIENTID_WHITELIST = [] diff --git a/data/users/externaljwt.py b/data/users/externaljwt.py index 55008aa9d..59740efd5 100644 --- a/data/users/externaljwt.py +++ b/data/users/externaljwt.py @@ -3,7 +3,7 @@ import json import os from data.users.federated import FederatedUsers, VerifiedCredentials -from util.security import strictjwt +from util.security import jwtutil logger = logging.getLogger(__name__) @@ -45,12 +45,12 @@ class ExternalJWTAuthN(FederatedUsers): # Load the JWT returned. encoded = result_data.get('token', '') - exp_limit_options = strictjwt.exp_max_s_option(self.max_fresh_s) + exp_limit_options = jwtutil.exp_max_s_option(self.max_fresh_s) try: - payload = strictjwt.decode(encoded, self.public_key, algorithms=['RS256'], - audience='quay.io/jwtauthn', issuer=self.issuer, - options=exp_limit_options) - except strictjwt.InvalidTokenError: + payload = jwtutil.decode(encoded, self.public_key, algorithms=['RS256'], + audience='quay.io/jwtauthn', issuer=self.issuer, + options=exp_limit_options) + except jwtutil.InvalidTokenError: logger.exception('Exception when decoding returned JWT') return (None, 'Invalid username or password') diff --git a/endpoints/key_server.py b/endpoints/key_server.py index 37b29e167..70c0da0eb 100644 --- a/endpoints/key_server.py +++ b/endpoints/key_server.py @@ -1,12 +1,7 @@ import logging - from datetime import datetime, timedelta -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers -from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers from flask import Blueprint, jsonify, abort, request, make_response -from jwkest.jwk import keyrep, RSAKey, ECKey from jwt import get_unverified_header import data.model @@ -14,8 +9,7 @@ import data.model.service_keys from data.model.log import log_action from app import app -from auth.registry_jwt_auth import TOKEN_REGEX -from util.security import strictjwt +from util.security import jwtutil logger = logging.getLogger(__name__) @@ -39,23 +33,13 @@ def _validate_jwk(jwk): abort(400) -def _jwk_dict_to_public_key(jwk): - jwkest_key = keyrep(jwk) - if isinstance(jwkest_key, RSAKey): - pycrypto_key = jwkest_key.key - return RSAPublicNumbers(e=pycrypto_key.e, n=pycrypto_key.n).public_key(default_backend()) - elif isinstance(jwkest_key, ECKey): - x, y = jwkest_key.get_key() - return EllipticCurvePublicNumbers(x, y, jwkest_key.curve).public_key(default_backend()) - - def _validate_jwt(encoded_jwt, jwk, service): - public_key = _jwk_dict_to_public_key(jwk) + public_key = jwtutil.jwk_dict_to_public_key(jwk) try: - strictjwt.decode(encoded_jwt, public_key, algorithms=['RS256'], + jwtutil.decode(encoded_jwt, public_key, algorithms=['RS256'], audience=JWT_AUDIENCE, issuer=service) - except strictjwt.InvalidTokenError: + except jwtutil.InvalidTokenError: logger.exception('JWT validation failure') abort(400) @@ -122,7 +106,7 @@ def put_service_key(service, kid): abort(400) jwt_header = request.headers.get(JWT_HEADER_NAME, '') - match = TOKEN_REGEX.match(jwt_header) + match = jwtutil.TOKEN_REGEX.match(jwt_header) if match is None: logger.error('Could not find matching bearer token') abort(400) @@ -180,7 +164,7 @@ def put_service_key(service, kid): @key_server.route('/services//keys/', methods=['DELETE']) def delete_service_key(service, kid): jwt_header = request.headers.get(JWT_HEADER_NAME, '') - match = TOKEN_REGEX.match(jwt_header) + match = jwtutil.TOKEN_REGEX.match(jwt_header) if match is None: abort(400) diff --git a/endpoints/oauthlogin.py b/endpoints/oauthlogin.py index 085134743..21f13bbd9 100644 --- a/endpoints/oauthlogin.py +++ b/endpoints/oauthlogin.py @@ -12,7 +12,7 @@ from auth.auth import require_session_login from data import model from endpoints.common import common_login, route_show_if from endpoints.web import render_page_template_with_routedata -from util.security.strictjwt import decode, InvalidTokenError +from util.security.jwtutil import decode, InvalidTokenError from util.validation import generate_valid_usernames diff --git a/endpoints/v2/v2auth.py b/endpoints/v2/v2auth.py index 7f857b70c..abae60b65 100644 --- a/endpoints/v2/v2auth.py +++ b/endpoints/v2/v2auth.py @@ -3,7 +3,7 @@ import re from flask import request, jsonify, abort -from app import app, userevents +from app import app, userevents, instance_keys from data import model from auth.auth import process_auth from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token @@ -14,7 +14,7 @@ from endpoints.v2 import v2_bp from endpoints.decorators import anon_protect from util.cache import no_cache from util.names import parse_namespace_repository, REPOSITORY_NAME_REGEX -from util.security.registry_jwt import generate_jwt_object, build_context_and_subject +from util.security.registry_jwt import generate_bearer_token, build_context_and_subject logger = logging.getLogger(__name__) @@ -152,8 +152,6 @@ def generate_registry_jwt(): # Build the signed JWT. context, subject = build_context_and_subject(user, token, oauthtoken) - - jwt_obj = generate_jwt_object(audience_param, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, - app.config) - - return jsonify({'token': jwt_obj}) + token = generate_bearer_token(audience_param, subject, context, access, + TOKEN_VALIDITY_LIFETIME_S, instance_keys) + return jsonify({'token': token}) diff --git a/endpoints/web.py b/endpoints/web.py index 15510c00d..ebe2bdc51 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -53,7 +53,6 @@ logging.captureWarnings(True) web = Blueprint('web', __name__) STATUS_TAGS = app.config['STATUS_TAGS'] -JWT_ISSUER = app.config.get('JWT_AUTH_TOKEN_ISSUER') @web.route('/', methods=['GET'], defaults={'path': ''}) diff --git a/initdb.py b/initdb.py index 871ef22e5..e4d042490 100644 --- a/initdb.py +++ b/initdb.py @@ -673,6 +673,20 @@ def populate_database(minimal=False, with_storage=False): __generate_service_key('kid7', 'somewayexpiredkey', new_user_1, week_ago, ServiceKeyApprovalType.SUPERUSER, today - timedelta(days=30)) + # Add the test pull key as pre-approved for local and unittest registry testing. + # Note: this must match the private key found in the local/test config. + _TEST_JWK = { + 'e': 'AQAB', + 'kty': 'RSA', + 'n': 'yqdQgnelhAPMSeyH0kr3UGePK9oFOmNfwD0Ymnh7YYXr21VHWwyM2eVW3cnLd9KXywDFtGSe9oFDbnOuMCdUowdkBcaHju-isbv5KEbNSoy_T2Rip-6L0cY63YzcMJzv1nEYztYXS8wz76pSK81BKBCLapqOCmcPeCvV9yaoFZYvZEsXCl5jjXN3iujSzSF5Z6PpNFlJWTErMT2Z4QfbDKX2Nw6vJN6JnGpTNHZvgvcyNX8vkSgVpQ8DFnFkBEx54PvRV5KpHAq6AsJxKONMo11idQS2PfCNpa2hvz9O6UZe-eIX8jPo5NW8TuGZJumbdPT_nxTDLfCqfiZboeI0Pw' + } + + key = model.service_keys.create_service_key('test_service_key', 'test_service_key', 'quay', + _TEST_JWK, {}, None) + + model.service_keys.approve_service_key(key.kid, new_user_1, ServiceKeyApprovalType.SUPERUSER, + notes='Test service key for local/test registry testing') + model.log.log_action('org_create_team', org.username, performer=new_user_1, timestamp=week_ago, metadata={'team': 'readers'}) diff --git a/test/data/registry_v2_auth.crt b/test/data/registry_v2_auth.crt deleted file mode 100644 index 88c783efa..000000000 --- a/test/data/registry_v2_auth.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDVDCCAjwCCQDNYtlT1+tGbzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQGEwJV -UzERMA8GA1UECBMITmV3IFlvcmsxETAPBgNVBAcTCE5ldyBZb3JrMRQwEgYDVQQK -EwtDb3JlT1MsIEluYzENMAsGA1UECxMEUXVheTESMBAGA1UEAxMJMTI3LjAuMC4x -MB4XDTE2MDUyMzE1MjUxOVoXDTI2MDUyMTE1MjUxOVowbDELMAkGA1UEBhMCVVMx -ETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQHEwhOZXcgWW9yazEUMBIGA1UEChML -Q29yZU9TLCBJbmMxDTALBgNVBAsTBFF1YXkxEjAQBgNVBAMTCTEyNy4wLjAuMTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKRvOt/XGNIovlr1BWxl2oqs -KDlgnESj6bFENDjs9+YLrB3mSWX6w4Dk2IdNU0EKHeVnnsAuBs83jaFsIVJxrC99 -ndv0PaejBovUbWyYN3zCMur8iNGse/FT4WRqks2m0Wr0jmEAX5piX/eWo/7OQdea -wNAGyH7wE0voMpyVSZMBmxRw07zWnwWBihvhOiiCnXZh32GQMplq0wxk4DkBf3hC -SEaAqsFHKfEFPxVXfdPGeiKKK+P2SAh+uN4miJpGf7Xkuj/Mmzxr1ajNczhPT6OM -pw0R3h/mok1S8zcp8lN/eDdKwjMeP4Rx+Lc0cRluZNa8otq9qYPNSCIkvsSz5b8C -AwEAATANBgkqhkiG9w0BAQUFAAOCAQEAZaaD8fLWEh4RGZ7X38IM/ocwDKaXWpDp -0EC3KMEuar1MET3MtVIXy/k/BLr0HmLRQ2KSV3wFfyOInseVeCvIcKZZo/JF28gR -LJVBcjExSIr6X8RoPgmKt7AdjlUjPV5XpRzDpfYcMaqpjJa75x6RoxC2ybh5Apyk -EzL3Naysk6TVPi5ckUYMLfw3JEbCeaEY4KNwVgsNcs447EcBxwGHTBqGOYtpIfku -SMas81oniMo9LMKv19Bn1oOforaqh8P2c57yregDsCDmP6j0gqkYjhJFCj5JNAKK -KT35QIfTbVFeCXAoLw0+o9Ma1Q+j7LfwdxnikUHNVZmlmjQmTBMwqg== ------END CERTIFICATE----- diff --git a/test/data/registry_v2_auth_private.key b/test/data/registry_v2_auth_private.key deleted file mode 100644 index 739c5fa0c..000000000 --- a/test/data/registry_v2_auth_private.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEApG8639cY0ii+WvUFbGXaiqwoOWCcRKPpsUQ0OOz35gusHeZJ -ZfrDgOTYh01TQQod5WeewC4GzzeNoWwhUnGsL32d2/Q9p6MGi9RtbJg3fMIy6vyI -0ax78VPhZGqSzabRavSOYQBfmmJf95aj/s5B15rA0AbIfvATS+gynJVJkwGbFHDT -vNafBYGKG+E6KIKddmHfYZAymWrTDGTgOQF/eEJIRoCqwUcp8QU/FVd908Z6Ioor -4/ZICH643iaImkZ/teS6P8ybPGvVqM1zOE9Po4ynDRHeH+aiTVLzNynyU394N0rC -Mx4/hHH4tzRxGW5k1ryi2r2pg81IIiS+xLPlvwIDAQABAoIBAANdV0oPP63FMafw -zYybRO6DeUs7Q9dPt09uQtdLWgM2B+6QsL3KdMelZxzVozd4eoYgKaprBq6kx1wf -N0tVkh1ip6FBjSVp+49O6HJJZxFBdANE6ZPIwLx+Z+VDHP/iQvS6TlODy3EARFBv -n6luFQDRZNKc4OtgBDUQakCz+U5tuJLqoR8wk/WGQP4FJiZlVwJqNPXMA1A2Mrri -n6WkhfpB30Z5dl9zsR+zJRbwRBjgJCYN37YC7zdHRfIhBPBvDT+8ApR50BGvPGN3 -sLQuH2FsskbgPsIrWMfCxtWr2xbw028GOe7TSjEG63EG7oGAT0O2eQmAcuPc4Dqj -Urn8saECgYEA2LkCe6MysmOtattC/gi3B/rIoOCd+4l9yTnW7S7nk/hdeOzxyqX1 -P7OgVeoYLLk3UJy3qTrNDnc0eGTJz0XyPhLlX0f9lduiSMH92XpNsBG7ngnyMCQF -eAZz8ZlDZC39I8y9CzdcHSLxuHKmQ9jhgUm+EIuf8OlrkjchPdE06i8CgYEAwjxG -cDA5X1hKYgQTObq245vR3txkvETmLVB7hWkjWLzR//a4hXHJT1fg2LxD5EMtCKZ2 -WXKhcy3tbja+c/IEI1L1wA2v/aWlEvi9n354EQ1QzkvCBDFP5enLnItAUzJQ0IgE -dtSUskK+li8aY2LB0EPt0eJmYU0cZUJXbl/ZKXECgYAAtttjPO512A5CQ+a8n5q6 -1ADFRvg+U/2uJBqpPXZV7oOgWmeRm2prg1QL9HGP9CxSf7G7RQ5X9dyeaPahUEG0 -IqvO3JXhYI/wXXNQvC51XhmYM8AwmG3ML3lCWpb2RZCIBay51Lzg+7SAPyB9KMHV -g0C1HUCxspNAMB5T7dSW0QKBgGkxRaCarWeypE4jENpyAXyRNf8xcyj3U4V1EgB1 -qVv0nvK2BsbWkgTzfeVDSK2FqA0IQg49Y6zCUdUfttOKXa1Xz5ocj5SaMiVtKx0G -3DW39WxUYRXuMuw8SzZTwBmOpW/aSjik9ob4WMlzZyIuKPMG5vSFXZcSsO8yF7HC -HRUxAoGBAKtCRLT9I5Ap37gWT8W6AAZygoUqhlYO9qygQrBDaJsHj0ZSHM0TO3ig -Bwq/UxDHBKFV3hmqx5Zmpoa9ZrURb4cBw/+TLq2ppXPLEU+XmEVmqL2323Vyr/Ih -CAIVWFsY3EGQL7TArOfag+v0Nxq3pypOhjweqIWEMDg+gV2+GHhQ ------END RSA PRIVATE KEY----- diff --git a/test/data/test.db b/test/data/test.db index d9741741c65c0a58f42de43705a6131c9361fceb..9fa42fdf8b1cb01793cd66e7e0bac0bdee4e9494 100644 GIT binary patch delta 65565 zcmeFa2bg3>btpW&6MJU5d-Ja1u9j8aJ9~4GK-$i?bKlP0NHjNg&fT|zkXkDdY-1yd zdEy!{L4XM+2p&d*ZIHmv@cfKz$x&bn1$gEWgd}4Owgu=vJ+rggs9k^h^!fijYo+gg zJyoYpovJ!@>YP)js&0SMd;62#yDu^g@447%8h-AL@4S2Ax<}sf7qRqZUo<{9-Wp!z z??D%q^G8;<#jYe*Z@(vV5Y^$q2@Xd{mCz7TLOBG*Xhy>*f}uri^%qd;GUJba z^YHu!uQ49qZws&9@lJGse$Df52z=w;?=+rwS;MOftLVax2jy!o`pChCao?LEMWQ1c;5|MF|W)wogWsW{{ z;U+XEbC@DQwkA<3LQ0B^u(Zq}3WGt4MkyMxYI+y8u>0-T4EEi1-#;4SZCjzXy_@9~ zi6bQzR}c-uNQC5M72$9Keu)%=(HKRls&Z)0CNzgabu>#L5`}9}8^IzX$6yGJ~7$LDT!D=c+9on&3DRc)$;RJ@zkcp5Kg(H$CYY2mK8qMNBys94Bz6q`37$-uj z5nLgGL{!5NR%BE}mNbkdFiBD{6S+G2ZRr1tAO6OUuRi+)1MO}Nzw_Pb!tU?hd}#mD z#f!#0yMfTRLJBHA_ATd^sKdtXJs7m*_n-?aw)8zmZh7)o2DWAiuf7vfu4N9s>$SIQ zmDOK;DSXqR;fA915y^Fw>@ude(r0=Q!SwJ z_TK>-zk6-b{*lJN8Fx1!W$u0G0uq_~;EnYUJz-ongp}WW-(n0?C<#|(0(wz}3K<4E zRm5R#C?Y4JEJJ7tacFjvJO(96=w%ot4rLIM1uhX44hCOkC0vqOQl`b#??Xk`zu|c_ ze(5_OTiyTu+ZHTe{KXABXyiHL?Q6^7)t|l}diW<-&hOv%$m>=Qec-kQ+x}<1xNGGi zlQI0_GBGnXWwNdQ-~*S8|9T33nrweP>oK_(g!vb2-<I)|Lkr zcbflr;loRdTb`J0nBP6~jm6i^?O2|gd+qeE7N6RpSl(m3+jjHv&t`sVBdxop?=&~( zo?IAN3$vG4{@3y?3x7UyrR}XVQPWrE&C4H?j1A3 z>A#!Dr~YMn!Th7yuP)sL^iREOZtK-oT{WFFsVYY^Du*H(F<}Eu;-C*H2~jne$1DY3 z3|qbEzVzjoM&T-k62P(q%n3qO5Dq0_d^infa84#@mU+EVwQNOJ|Kz@p9Hd2zRv`ls zQ5?WE7DqS|%HWbBYMexq8peG7H$Nm#hZKz#Ig&+iLW5~d;=oiY%v(aG7)oIXiV*1= zCzMhoL((b;MV6ibG8Bf0EQldmB?;hRnWAy^2IH6iYTIa-DB>ca$PyyU6o@H~QV@31 zIu*x7XanUU%8w`VFpo(ir_R(LlK2VIFXpFF&8<n9*&)l5Z}9w@Z9p7*fRlKeePWcS3mrzmGctSJ0~U1M@sLQ0G@|{ zCbv%j6kA;krmmh&ZZU3oWb45#Q=h%#vk!dM^z=hJDFmnXW6X6Fc^$^A&@{u4_yf%?PwzW( z&fQy{K78h!uWb2|ZO1w`9GhSx$Q2p|d9n7ZhqpX);~8^auubp0cT8fef@vr$y&}ya z@Rtbdy+Xm#OA;Ea2O7*zsW+@w#dblxR&J|ex6zgw-E1Tm3I$O#8wt4BblT5HNnh5H zvWL=+pv&&gINN{!Wxb#BFsJz46F(?qYNc88Ua#)L0_b_ z(XXo$I-R(e!Xr_rK8{Bmq$3=4p#eUeMQJ7yNv6hRj^YA-(uv_*#BKMYE+LEZL?}l) z!X$-}Bz26?Q|LrlYMst2Z9pe;5}hck5H$RUs3a(uq%4DclVO!qDHYc=LSaRPJWl8M z+Pd0rvqjNu^==z$RUKxfMtyP9_yUK+&?sfvASHA65+vlp~Pu@C&C=meVMS zB@OIj(>un|4CQvAZkC9qs1OzKbM6F>hujn)pji&hQEs<89jCIf0F!k&`~n|!2QYgO za|JVjxQodOXQYj!L6ZgAT9&O-$&03LTmXG{W3UxXlq4Ec5S7M&Es2Xn5fLd^hg6Ot zIZ#JHypz?E)E3)AqxsDA1s9&N0)nDY)Y$g%>0JkbJ5YIo5!O!NAz&C#RaG1TeIM6w zj*%&pGW;K(#!s9q6Q0CalrZl4`1IbgN-7kAF|bn5B*=dXrU|QwEP_fTMw2K=15R%A zx}B1u!jK-s#e*1r(Bo~ng~KlIKyD_57T;;8n8P1&^=m2Fj#iqTxUVtn(kq#8gint! zCD&IQQLnM>6Vp4#C(LJzTVW{@l-|imzGe|d@}i@&SDUAxyVfTMEao^)!7`~R8ZaGB zX$Y*O7^1NhOJbl2NEmZ$@*blr(G`Xwoeb6pA?KFLMI86_E4@%M>d>kgl&N#=>+!>+ z*WpOJl3u!;F2%il)LC!HN4iC^Ug@mJjoN8_4rIdcfDU0i@rmg@V~}PHMR78s!O{p* z7R(Y(V`0@48JX2c6;xRoB#NT8j}etw(FpgDNVc?;nnHU%y;`(;RByYjXJhWt3LfAa zrBOK(HL^J79KARcQ() ztO&yjrXdXQME}J^9F$Oa*x#D(2JS%tx zkEqzl+0eX6F2nPDq3Tseb^+t@$YF22L7kb@W0bNh9D#uaV<4Y|?jtEq#7T)pR8ZJJ z0Rn>zG#?qEKv4qzR}_rI+G1U4)OwxcU5M9fo&@3d*FwqI$R8sLU8z}78(9#8;h+#C zV=cbPS2aGuPy<1<_n1L7g4Y}5>2!r(lzAAtLIYjIy3D(34!V##NswR?gJJ~C3`G=y zQb~rHfudkuO9U-plZl5)#)m|TpiX<9}emw-|~x2 zys8Ec7Y`>^;?1b99XCv$p5AxT6lGU%mSb3Y!e)nNppPy%I{Ug4rYOS@3K&SBZmSaL zy|jjK6bB3kqY1Nwz%-6&_M~b_KE`4$H7FHJ4XMH?N<-+@va~%a_B=tw9}@IfhX_}- z?aC%cV7g`6~YERX{BB=i%5Lyr-l z6LTudfG!P8MiK;tGK`|?T~Vs4*DBrhm5K`c1je6WylIyQcQ`Q85wph}FmCoV3X7;P z#;|NC?DYGcxfGM-3BiT(4wom#@>Dd(Vu|qS!#JT5#kh3=M{EHqFF88v-R!-EDU4%8 zS_CPgs1leLEXZC-qbKVDj;V@7a~L5irOt#%n4;IMi{3%D%np4Xu3;ZW568=7&#i|N zD~hul3{>jT$V#+EMF!cHMyC#!B#)Z-Y4fjv)DH6b4 zI0f1Vj)IXzf{O&<2u1{DgaI{0rW6>|%}zTuoFTzQ6p73VHxNn^-r!QP(jXmiM#*Nn zq^BDFLcOFARi$3`7lmA;v_cGBc-;8%{nLjwRK8;(f>|Lb4(M3><^9u3=(6p0uy0?m zUSWCCl33gMpGg3tJB!lvHWMWkDX^yKa9#V z)9-e_T;Vf!$@Gn;$;7|Fke;1>&5?6WgLBP-wXZ!pJy?0!19r=;f!TTRksP&b+dBQ9 zAGh(z3NK)rIfl(P{c?v7kWC*$W}QCED2~rti3Dt0j$xadez}7Q$V}_VT$XLq(=U4{ zf3F)Kawp~7F+Bg-;T@l9&&(b}^m0db z<`|x{4C>~Xj?ZZsOdmt^a>sLvXVTN=Q$`X$eQ`S1tb8Ck^R<1~$2kGZc`0Wso6E+c zX-{|9Y#iAX>8B{aCs^os z)z5-k5@o-{{6SkQ3->mmp{g!{R9EC|- zu`D#_=cdddd(HUVLvvSnPO!_s&q7f&ScnNAW79WqS@X8-Kmi$7WM?+?N84g7!O;DP^hb5!$w1Kb;zTYHj(tI z(E!)*L~3EJ??SV+dXdRyGi@A8@#+3BgsZ7EA=iTn7sw3zE>G7RR{`85)g!%-Q%N@Z zO2r|Ie7T2|I+9KW+)_B+80Hgrtkgv4AS!2RB0G{p{?S;+>hA2Qo6W@8{)nNQ@c@fC z0uEX4q5prxWgWD1qll zL6cm83?A;&x<5?&>)vJ}%;)riE)T;QRRf=`{Ga?G)rw&ff1{e z6vnvq={a;b9OiF3)nHF>(tMZG2BBea7|m7cWQj&YBi`%4@ro=BGoFBtaYrj2 zT%rVZpcm|l8@4*Qm@{kdxCWz!GNcDeT`Kk!)feh9B{!uBPIqjCC?P!5Bkqn)fOghkxr zC7FV!9>bB?sDnfu=|s_AD3=-)!5t{{+C-$=X-B&vft3nnzMMws0NYIYN|CCQ#d$qP z@pfmYu7^PGvRl5qiAA)gBedC-9BCGi|JIi zaU zLa`Xd5_5OnJhwb`^{%T=7`u}Oj@}r!n(pC>aV$!yOgR#f%Ps#9?+*$oG?uF9!&In0 zNYn-$B8bLoHNH^DwX0a%H_(!LfWh!ErbHw*rUvZ6h=$q~{mwMtRFmV;g8DmfA; z$55~IGCe1vrm~#g3SyBO?bAbL*rpvubmurh4ol(EP-;ZDAl?t+L{o~ph!`UeoN6uw z*7%StR2w;*Cf&uD81mp%p7AhQ|0w4ga-ICB7kB9KWV7Px(|on-Y>9F*lVdvVf{S4_ zPio+osv~l=+q*e?Ip;0c~7)$DKnD#_c7&g)+nM^XCkvrM-@RC2< zZSuY%TPFjI&(ozicc`l+1!ptraX1GET1A5zkt(8`o^t16jeI^7BC({u(C`5}A!i*p zjD}r);`pT7P9EE}I6iQ*##g>Kr;KSN(qsv|mBpQ?&(Y$$E|I&WikX4uQW4mZ(rm<+;K@VulmO=x!%h%0!E9Tgi;kWU!@=78E1^w+alri z>wUJ2hiNtCC^chQwQetD(@mjD)Fm|8&}y+hhP0Wo?h1KW-rb8xrF1G^bO`mch`h;y zPT=@NTY}9L_bMyH^ZRr0F;?IMn!kp4I#iEVYXYam5uI_>J4L+Xc2*E7Qcy-w zR89rUNIPBb#e>20z)>COU8+hZySYrbl**B=0@GAoY@iTJlnaeIxI04W$^eZHTtQT9 z`m=dw14e``46EIS9<>KEkwWv}*ZdWBD zSZ>u+do|(q-BlvX=;?-@ zE#`Ab4z?l68H9FK34yKQTFIBf3b{g(jgLDySJ)jyIk&|5R37cMTx2_+^mhoJZwPtL zJ>ZePp6n&$;V9}!34E(ANudxj$d#Q9M-WfOf+?&i2U3oHO7kH~kG1D3`O@et(`BM3 zVJ!9HblFCqHY}?@oJ)?&zRpnf*zI|5sO_v)10?2Gq4hei<;q;Ss+9(4df*c)p-dK$ zHJ6lXi==(jl_fIgBRRF`j;rZJIzW`2*_2iziL6v)khm|?PS9?uTO^OhB}49IF}U50v79sLY|P1@+Agb(MPnqfqbWvsL=9|Zn#3(ZrGF5 z0zQ4jWJdOCi)`@@w$!O{j!?YUF4${TurNm5NCD)FFoq=D&7<;MK9ZKQmiYE z*aMm@bhHo#4qVO|A4gHe?go2Kr<jceos^LZ~8;b-Z zARj%YmbdCH#no=Po9PB}&43&bG%kg>vPcux28AS9sb>>*6ivtZoItsTeIHnmlB-BY zO5J9nh9*_7SLx+@u2F8B1B*<`hRVHkB2h{5QL)hp$54N)o)@XS8|{~BZmp*4T|HcH zlQAk#(z0=TF`kJ=!+i#gHKKts=_t3H?z*EF4#wD;x0pl+zB7xgiCsW)92lZ6A+oN2 zW= z8gtO$v@y8jsb#)Oc6z3`-qH|`Eg-=W{g5=1FwY6X?P1$U}Z7bQH zvHhd%PiXi`}EbOj#z6})lE}q{`91|i>F^_GQ@v0?=^n^ zg}La?1>4_D+5Q==kJ<*ulaOJJLbs+wOJ$!

7}%+T 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 ITnz;NKdv1NWB>pF delta 63101 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 self._expiration + + +class ExpiresDict(object): + """ ExpiresDict defines a dictionary-like class whose keys have expiration. The rebuilder is + a function that returns the full contents of the cached dictionary as a dict of the keys + and whose values are TTLEntry's. + """ + def __init__(self, rebuilder): + self._rebuilder = rebuilder + self._items = {} + + def __getitem__(self, key): + found = self.get(key) + if found is None: + raise KeyError + + return found + + def get(self, key, default_value=None): + # Check the cache first. If the key is found and it has not yet expired, + # return it. + found = self._items.get(key) + if found is not None and not found.expired: + return found.value + + # Otherwise the key has expired or was not found. Rebuild the cache and check it again. + self._rebuild() + found = self._items.get(key) + if found is None: + return default_value + + return found.value + + def __contains__(self, key): + return self.get(key) is not None + + def _rebuild(self): + self._items = self._rebuilder() + + def set(self, key, value, expires=None): + self._items[key] = ExpiresEntry(value, expires=expires) diff --git a/util/registry/torrent.py b/util/registry/torrent.py index 61e25e05e..d81caa162 100644 --- a/util/registry/torrent.py +++ b/util/registry/torrent.py @@ -7,15 +7,12 @@ import urllib from cachetools import lru_cache -from app import app +from app import app, instance_keys -ANNOUNCE_URL = app.config.get('BITTORRENT_ANNOUNCE_URL') -PRIVATE_KEY_LOCATION = app.config.get('INSTANCE_SERVICE_KEY_LOCATION') -FILENAME_PEPPER = app.config.get('BITTORRENT_FILENAME_PEPPER') -REGISTRY_TITLE = app.config.get('REGISTRY_TITLE') -JWT_ISSUER = app.config.get('JWT_AUTH_TOKEN_ISSUER') - +ANNOUNCE_URL = app.config['BITTORRENT_ANNOUNCE_URL'] +FILENAME_PEPPER = app.config['BITTORRENT_FILENAME_PEPPER'] +REGISTRY_TITLE = app.config['REGISTRY_TITLE'] @lru_cache(maxsize=1) def _load_private_key(private_key_file_path): @@ -24,13 +21,12 @@ def _load_private_key(private_key_file_path): def _torrent_jwt(info_dict): token_data = { - 'iss': JWT_ISSUER, + 'iss': instance_keys.service_name, 'aud': ANNOUNCE_URL, 'infohash': _infohash(info_dict), } - private_key = _load_private_key(PRIVATE_KEY_LOCATION) - return jwt.encode(token_data, private_key, 'RS256') + return jwt.encode(token_data, instance_keys.local_private_key, 'RS256') def _infohash(infodict): digest = hashlib.sha1() diff --git a/util/secscan/api.py b/util/secscan/api.py index a07fd9d0b..cbe97745f 100644 --- a/util/secscan/api.py +++ b/util/secscan/api.py @@ -8,7 +8,8 @@ from data.database import CloseForLongOperation from data import model from data.model.storage import get_storage_locations from util.secscan.validator import SecurityConfigValidator -from util.security.registry_jwt import generate_jwt_object, build_context_and_subject +from util.security.instancekeys import InstanceKeys +from util.security.registry_jwt import generate_bearer_token, build_context_and_subject from util import get_app_url @@ -43,6 +44,7 @@ class SecurityScannerAPI(object): self._app = app self._config = config + self._instance_keys = InstanceKeys(app) self._client = client or config['HTTPCLIENT'] self._storage = storage self._default_storage_locations = config['DISTRIBUTED_STORAGE_PREFERENCE'] @@ -80,9 +82,10 @@ class SecurityScannerAPI(object): 'name': repository_and_namespace, 'actions': ['pull'], }] - auth_jwt = generate_jwt_object(audience, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, - self._config) - auth_header = 'Bearer {}'.format(auth_jwt) + + auth_token = generate_bearer_token(audience, subject, context, access, + TOKEN_VALIDITY_LIFETIME_S, self._instance_keys) + auth_header = 'Bearer ' + auth_token with self._app.test_request_context('/'): relative_layer_url = url_for('v2.download_blob', repository=repository_and_namespace, diff --git a/util/security/instancekeys.py b/util/security/instancekeys.py new file mode 100644 index 000000000..0f9bef8e6 --- /dev/null +++ b/util/security/instancekeys.py @@ -0,0 +1,81 @@ +from cachetools import lru_cache +from data import model +from util.expiresdict import ExpiresDict, ExpiresEntry +from util.security import jwtutil + + +class InstanceKeys(object): + """ InstanceKeys defines a helper class for interacting with the Quay instance service keys + used for JWT signing of registry tokens as well as requests from Quay to other services + such as Clair. Each container will have a single registered instance key. + """ + def __init__(self, app): + self.app = app + self.instance_keys = ExpiresDict(self._load_instance_keys) + self.public_keys = {} + + def clear_cache(self): + """ Clears the cache of instance keys. """ + self.instance_keys = ExpiresDict(self._load_instance_keys) + self.public_keys = {} + + def _load_instance_keys(self): + # Load all the instance keys. + keys = {} + for key in model.service_keys.list_service_keys(self.service_name): + keys[key.kid] = ExpiresEntry(key, key.expiration_date) + + # Remove any expired or deleted keys from the public keys cache. + for key in self.public_keys: + if key not in keys: + self.public_keys.pop(key) + + return keys + + @property + def service_name(self): + """ Returns the name of the instance key's service (i.e. 'quay'). """ + return self.app.config['INSTANCE_SERVICE_KEY_SERVICE'] + + @property + def service_key_expiration(self): + """ Returns the defined expiration for instance service keys, in minutes. """ + return self.app.config.get('INSTANCE_SERVICE_KEY_EXPIRATION', 120) + + @property + @lru_cache(maxsize=1) + def local_key_id(self): + """ Returns the ID of the local instance service key. """ + return _load_file_contents(self.app.config['INSTANCE_SERVICE_KEY_KID_LOCATION']) + + @property + @lru_cache(maxsize=1) + def local_private_key(self): + """ Returns the private key of the local instance service key. """ + return _load_file_contents(self.app.config['INSTANCE_SERVICE_KEY_LOCATION']) + + def get_service_key_public_key(self, kid): + """ Returns the public key associated with the given instance service key or None if none. """ + + # Note: We do the lookup via instance_keys *first* to ensure that if a key has expired, we + # don't use the entry in the public key cache. + service_key = self.instance_keys.get(kid) + if service_key is None: + # Remove the kid from the cache just to be sure. + self.public_keys.pop(kid, None) + return None + + public_key = self.public_keys.get(kid) + if public_key is not None: + return public_key + + # Convert the JWK into a public key and cache it (since the conversion can take > 200ms). + public_key = jwtutil.jwk_dict_to_public_key(service_key.jwk) + self.public_keys[kid] = public_key + return public_key + + +def _load_file_contents(path): + """ Returns the contents of the specified file path. """ + with open(path) as f: + return f.read() diff --git a/util/security/strictjwt.py b/util/security/jwtutil.py similarity index 66% rename from util/security/strictjwt.py rename to util/security/jwtutil.py index 61bb61454..dc190838f 100644 --- a/util/security/strictjwt.py +++ b/util/security/jwtutil.py @@ -1,3 +1,5 @@ +import re + from datetime import datetime, timedelta from jwt import PyJWT from jwt.exceptions import ( @@ -5,8 +7,19 @@ from jwt.exceptions import ( ImmatureSignatureError, InvalidIssuedAtError, InvalidIssuerError, MissingRequiredClaimError ) +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicNumbers +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers +from jwkest.jwk import keyrep, RSAKey, ECKey + + +# TOKEN_REGEX defines a regular expression for matching JWT bearer tokens. +TOKEN_REGEX = re.compile(r'\ABearer (([a-zA-Z0-9+\-_/]+\.)+[a-zA-Z0-9+\-_/]+)\Z') + class StrictJWT(PyJWT): + """ StrictJWT defines a JWT decoder with extra checks. """ + @staticmethod def _get_default_options(): # Weird syntax to call super on a staticmethod @@ -53,3 +66,16 @@ def exp_max_s_option(max_exp_s): decode = StrictJWT().decode + + +def jwk_dict_to_public_key(jwk): + """ Converts the specified JWK into a public key. """ + jwkest_key = keyrep(jwk) + if isinstance(jwkest_key, RSAKey): + pycrypto_key = jwkest_key.key + return RSAPublicNumbers(e=pycrypto_key.e, n=pycrypto_key.n).public_key(default_backend()) + elif isinstance(jwkest_key, ECKey): + x, y = jwkest_key.get_key() + return EllipticCurvePublicNumbers(x, y, jwkest_key.curve).public_key(default_backend()) + + raise Exception('Unsupported kind of JWK: %s', str(type(jwkest_key))) diff --git a/util/security/registry_jwt.py b/util/security/registry_jwt.py index bdf877dc8..be110f1a7 100644 --- a/util/security/registry_jwt.py +++ b/util/security/registry_jwt.py @@ -1,17 +1,80 @@ import time import jwt +import logging -from cachetools import lru_cache +from util.security import jwtutil +logger = logging.getLogger(__name__) ANONYMOUS_SUB = '(anonymous)' +ALGORITHM = 'RS256' -def generate_jwt_object(audience, subject, context, access, lifetime_s, app_config): - """ Generates a compact encoded JWT with the values specified. +class InvalidBearerTokenException(Exception): + pass + + +def decode_bearer_token(bearer_token, instance_keys): + """ decode_bearer_token decodes the given bearer token that contains both a Key ID as well as the + encoded JWT and returns the decoded and validated JWT. On any error, raises an + InvalidBearerTokenException with the reason for failure. """ + app_config = instance_keys.app.config + + # Extract the jwt token from the header + match = jwtutil.TOKEN_REGEX.match(bearer_token) + if match is None: + raise InvalidBearerTokenException('Invalid bearer token format') + + encoded_jwt = match.group(1) + logger.debug('encoded JWT: %s', encoded_jwt) + + # Decode the key ID. + headers = jwt.get_unverified_header(encoded_jwt) + kid = headers.get('kid', None) + if kid is None: + logger.error('Missing kid header on encoded JWT: %s', encoded_jwt) + raise InvalidBearerTokenException('Missing kid header') + + # Find the matching public key. + public_key = instance_keys.get_service_key_public_key(kid) + if public_key is None: + logger.error('Could not find requested service key %s', kid) + raise InvalidBearerTokenException('Unknown service key') + + # Load the JWT returned. + try: + expected_issuer = instance_keys.service_name + audience = app_config['SERVER_HOSTNAME'] + max_signed_s = app_config.get('REGISTRY_JWT_AUTH_MAX_FRESH_S', 3660) + + max_exp = jwtutil.exp_max_s_option(max_signed_s) + payload = jwtutil.decode(encoded_jwt, public_key, algorithms=[ALGORITHM], audience=audience, + issuer=expected_issuer, options=max_exp) + except jwtutil.InvalidTokenError as ite: + logger.exception('Invalid token reason: %s', ite) + raise InvalidBearerTokenException(ite) + + if not 'sub' in payload: + raise InvalidBearerTokenException('Missing sub field in JWT') + + return payload + + +def generate_bearer_token(audience, subject, context, access, lifetime_s, instance_keys): + """ Generates a registry bearer token (without the 'Bearer ' portion) based on the given + information. + """ + return _generate_jwt_object(audience, subject, context, access, lifetime_s, + instance_keys.service_name, instance_keys.local_key_id, + instance_keys.local_private_key) + + +def _generate_jwt_object(audience, subject, context, access, lifetime_s, issuer, key_id, + private_key): + """ Generates a compact encoded JWT with the values specified. """ token_data = { - 'iss': app_config['JWT_AUTH_TOKEN_ISSUER'], + 'iss': issuer, 'aud': audience, 'nbf': int(time.time()), 'iat': int(time.time()), @@ -21,15 +84,11 @@ def generate_jwt_object(audience, subject, context, access, lifetime_s, app_conf 'context': context, } - certificate = _load_certificate_bytes(app_config['JWT_AUTH_CERTIFICATE_PATH']) - token_headers = { - 'x5c': [certificate], + 'kid': key_id, } - private_key = _load_private_key(app_config['JWT_AUTH_PRIVATE_KEY_PATH']) - - return jwt.encode(token_data, private_key, 'RS256', headers=token_headers) + return jwt.encode(token_data, private_key, ALGORITHM, headers=token_headers) def build_context_and_subject(user, token, oauthtoken): @@ -64,14 +123,3 @@ def build_context_and_subject(user, token, oauthtoken): return (context, ANONYMOUS_SUB) -@lru_cache(maxsize=1) -def _load_certificate_bytes(certificate_file_path): - with open(certificate_file_path) as cert_file: - cert_lines = cert_file.readlines()[1:-1] - return ''.join([cert_line.rstrip('\n') for cert_line in cert_lines]) - - -@lru_cache(maxsize=1) -def _load_private_key(private_key_file_path): - with open(private_key_file_path) as private_key_file: - return private_key_file.read() diff --git a/workers/service_key_worker.py b/workers/service_key_worker.py index a21ef3f69..adbcf5465 100644 --- a/workers/service_key_worker.py +++ b/workers/service_key_worker.py @@ -1,7 +1,7 @@ import logging from datetime import datetime, timedelta -from app import app +from app import app, instance_keys from data.model.service_keys import set_key_expiration from workers.worker import Worker @@ -17,14 +17,9 @@ class ServiceKeyWorker(Worker): """ Refreshes active service keys so they don't get garbage collected. """ - with open("/conf/quay.kid") as f: - kid = f.read() - - minutes_until_expiration = app.config.get('INSTANCE_SERVICE_KEY_EXPIRATION', 120) - expiration = timedelta(minutes=minutes_until_expiration) - + expiration = timedelta(minutes=instance_keys.service_key_expiration) logger.debug('Starting refresh of automatic service keys') - set_key_expiration(kid, datetime.now() + expiration) + set_key_expiration(instance_keys.local_key_id, datetime.now() + expiration) logger.debug('Finished refresh of automatic service keys') if __name__ == "__main__":