diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 85bd3f32f..da54bbd48 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -5,7 +5,6 @@ import logging import os import string -from collections import OrderedDict from datetime import datetime from hashlib import sha256 from random import SystemRandom @@ -27,6 +26,7 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques from endpoints.api.logs import get_logs, get_aggregate_logs from data import model from data.database import ServiceKeyApprovalType +from util import canonicalize from util.useremails import send_confirmation_email, send_recovery_email @@ -543,9 +543,7 @@ class SuperUserServiceKeyManagement(ApiResource): private_key = RSA.generate(2048) jwk = RSAKey(key=private_key.publickey()).serialize() - canonical_jwk = OrderedDict(sorted(jwk.items())) - kid = sha256(json.dumps(canonical_jwk)).hexdigest() - + kid = sha256(json.dumps(canonicalize(jwk), separators=(',', ':'))).hexdigest() model.service_keys.create_service_key(body.get('name', ''), kid, body['service'], jwk, metadata, expiration_date) diff --git a/endpoints/key_server.py b/endpoints/key_server.py index cfebcdeb6..bc8c231af 100644 --- a/endpoints/key_server.py +++ b/endpoints/key_server.py @@ -1,5 +1,3 @@ -import json - from datetime import datetime import jwt @@ -68,7 +66,14 @@ def get_service_keys(service): @key_server.route('/services//keys/', methods=['GET']) def get_service_key(kid): - key = data.model.service_keys.get_service_key(kid) + try: + key = data.model.service_keys.get_service_key(kid) + except data.model.ServiceKeyDoesNotExist: + abort(404) + + if key.approval is None: + abort(404) + return jsonify(key.jwk) @@ -92,26 +97,30 @@ def put_service_keys(service, kid): _validate_jwk(jwk, kid) - + metadata = {'ip': request.remote_addr} signer_kid = _signer_kid(encoded_jwt) - signer_key = _signer_key(signer_kid) + if kid == signer_kid: + # The key is self-signed. Create a new instance and await approval. + _validate_jwt(encoded_jwt, jwk, service) + data.model.service_keys.create_service_key('', kid, service, jwk, metadata, expiration_date) + return make_response('', 202) + + metadata.update({'created_by': 'Key Rotation'}) + signer_key = _signer_key(signer_kid) + signer_jwk = signer_key.jwk if signer_key.service != service: abort(403) - _validate_jwt(encoded_jwt, signer_key.jwk, service) - - metadata = { - 'ip': request.remote_addr, - 'created_by': 'Key Rotation', - 'rotated_by': json.dumps(signer_key), - } + _validate_jwt(encoded_jwt, signer_jwk, service) try: data.model.service_keys.replace_service_key(kid, jwk, metadata, expiration_date) except data.model.ServiceKeyDoesNotExist: abort(404) + return make_response('', 200) + @key_server.route('/services//keys/', methods=['DELETE']) def delete_service_key(service, kid): diff --git a/util/__init__.py b/util/__init__.py index c45aad78f..e30fafb53 100644 --- a/util/__init__.py +++ b/util/__init__.py @@ -1,3 +1,6 @@ +import collections + + def get_app_url(config): """ Returns the application's URL, based on the given config. """ return '%s://%s' % (config['PREFERRED_URL_SCHEME'], config['SERVER_HOSTNAME']) @@ -16,3 +19,13 @@ def slash_join(*args): args = [rmslash(path) for path in args] return '/'.join(args) + + +def canonicalize(json_obj): + """ Returns a JSON object sorted by key. """ + if isinstance(json_obj, collections.MutableMapping): + sorted_obj = sorted({key: canonicalize(val) for key, val in json_obj}.items()) + return collections.OrderedDict(sorted_obj) + elif isinstance(json_obj, (list, tuple)): + return [canonicalize(val) for val in json_obj] + return json_obj