diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index 03e2477096..b27b5e75dd 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -8,6 +8,7 @@ Invoking ``keystone-manage`` by itself will give you some usage information. Available commands: * ``bootstrap``: Perform the basic bootstrap process. +* ``create_jws_keypair``: Create an ECDSA key pair for JWS token signing. * ``credential_migrate``: Encrypt credentials using a new primary key. * ``credential_rotate``: Rotate Fernet keys for credential encryption. * ``credential_setup``: Setup a Fernet key repository for credential encryption. diff --git a/keystone/cmd/cli.py b/keystone/cmd/cli.py index 55b5ff4986..e15df72dfb 100644 --- a/keystone/cmd/cli.py +++ b/keystone/cmd/cli.py @@ -31,6 +31,7 @@ from keystone.cmd import bootstrap from keystone.cmd import doctor from keystone.common import driver_hints from keystone.common import fernet_utils +from keystone.common import jwt_utils from keystone.common import sql from keystone.common.sql import upgrades from keystone.common import utils @@ -478,6 +479,43 @@ class FernetRotate(BasePermissionsSetup): keystone_user_id, keystone_group_id, 'fernet_receipts') +class CreateJWSKeyPair(BasePermissionsSetup): + """Create a key pair for signing and validating JWS tokens. + + This command creates a public and private key pair to use for signing and + validating JWS token signatures. The key pair is written to the directory + where the command is invoked. + + """ + + name = 'create_jws_keypair' + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(CreateJWSKeyPair, cls).add_argument_parser(subparsers) + + parser.add_argument( + '--force', action='store_true', + help=('Forcibly overwrite keys if they already exist') + ) + return parser + + @classmethod + def main(cls): + current_directory = os.getcwd() + private_key_path = os.path.join(current_directory, 'private.pem') + public_key_path = os.path.join(current_directory, 'public.pem') + + if os.path.isfile(private_key_path) and not CONF.command.force: + raise SystemExit(_('Private key %(path)s already exists') + % {'path': private_key_path}) + if os.path.isfile(public_key_path) and not CONF.command.force: + raise SystemExit(_('Public key %(path)s already exists') + % {'path': public_key_path}) + + jwt_utils.create_jws_keypair(private_key_path, public_key_path) + + class TokenSetup(BasePermissionsSetup): """Setup a key repository for tokens. @@ -1253,6 +1291,7 @@ CMDS = [ DomainConfigUpload, FernetRotate, FernetSetup, + CreateJWSKeyPair, MappingPopulate, MappingPurge, MappingEngineTester, diff --git a/keystone/common/jwt_utils.py b/keystone/common/jwt_utils.py new file mode 100644 index 0000000000..9f0aa9b093 --- /dev/null +++ b/keystone/common/jwt_utils.py @@ -0,0 +1,43 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import serialization + + +def create_jws_keypair(private_key_path, public_key_path): + """Create an ECDSA key pair using an secp256r1, or NIST P-256, curve. + + :param private_key_path: location to save the private key + :param public_key_path: location to save the public key + + """ + private_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) + + with open(private_key_path, 'wb') as f: + f.write( + private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + ) + ) + + public_key = private_key.public_key() + with open(public_key_path, 'wb') as f: + f.write( + public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + )