├── requirements.txt ├── .gitignore ├── LICENSE ├── README.md └── aws-kms-sign-csr.py /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.10.34 2 | pyasn1==0.4.8 3 | pyasn1-modules==0.2.7 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intelij IDE 2 | .idea/ 3 | 4 | # Python virtual environment 5 | venv 6 | 7 | # Keys, CSRs and Certs 8 | *.pem 9 | *.csr 10 | *.key -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Guy Davies 4 | Portions Copyright (c) 2021 Colin Coleman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-kms-sign-csr 2 | 3 | Given an existing CSR (in PEM format) and a keypair in AWS KMS, this script: 4 | * updates the public key to the public key of the asymmetric keypair 5 | * signs the CSR with the private key of the asymmetric keypair 6 | 7 | ## Why would I want to do this? 8 | 9 | You may have a use-case where you're signing arbitrary data using KMS, but checking 10 | this signature against a certificate (or, by extension, checking that the certificate 11 | has been chained from a trusted root or intermediate). 12 | 13 | This script allows you to generate a CSR which uses the private key in KMS, which 14 | can then be signed by your PKI. From here you can sign your arbitrary data using 15 | KMS and you've maintained the security of your private key, as it has never left 16 | KMS. 17 | 18 | Note that this does NOT sign the CSR with a CA to make it into a bona fide certificate: 19 | a CSR is signed with the private key of the generator so that the CA can ensure 20 | that the public key is owned by the person who is requesting the certificate, and 21 | this script re-signs with the private key held in KMS. 22 | 23 | ## Installation 24 | 25 | # create a new virtualenv 26 | python3 -m venv aws-kms-sign-csr 27 | . aws-kms-sign-csr/bin/activate 28 | # install prerequisite modules 29 | pip3 install -r requirements.txt 30 | 31 | ## Usage 32 | 33 | ### RSA 34 | 35 | # generate a PEM csr - the key doesn't matter as it will be replaced 36 | openssl req -new -newkey rsa:2048 -keyout /dev/null -nodes -out test.csr 37 | ./aws-kms-sign-csr.py --region eu-west-1 --keyid alias/mykeyalias --hashalgo sha256 test.csr > new.csr 38 | 39 | ### ECDSA 40 | # Create a fake key 41 | openssl ecparam -genkey -name secp256k1 -out fake.key -genkey 42 | # Create CSR from fake key 43 | openssl req -new -key fake.key -out test.csr 44 | # Update CSR using KMS key 45 | ./aws-kms-sign-csr.py --region eu-west-1 --keyid alias/mykeyalias --hashalgo sha256 --signalgo ECDSA test.csr > new.csr 46 | 47 | 48 | The script will use your existing AWS credentials: to override use environment variables per https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html 49 | 50 | The key ID can be a key ARN, an actual key ID, a key alias (prefixed with alias/), or a key alias ARN. See https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kms.html#KMS.Client.sign for more info. 51 | 52 | ## Limitations 53 | 54 | * only supports RSA with sha256, sha384 and sha512 and ECDSA with sha224, sha256, sha384, sha512 at time of writing 55 | * should have better error handling 56 | * should have better handling of boto profiles 57 | -------------------------------------------------------------------------------- /aws-kms-sign-csr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | python script to re-sign an existing CSR with an asymmetric keypair held in AWS KMS 4 | """ 5 | 6 | from pyasn1.codec.der import decoder, encoder 7 | from pyasn1.type import univ 8 | import pyasn1_modules.pem 9 | import pyasn1_modules.rfc2986 10 | import pyasn1_modules.rfc2314 11 | import hashlib 12 | import base64 13 | import textwrap 14 | import argparse 15 | import boto3 16 | 17 | start_marker = '-----BEGIN CERTIFICATE REQUEST-----' 18 | end_marker = '-----END CERTIFICATE REQUEST-----' 19 | 20 | 21 | def sign_certification_request_info(kms, key_id, csr, digest_algorithm, signing_algorithm): 22 | certificationRequestInfo = csr['certificationRequestInfo'] 23 | der_bytes = encoder.encode(certificationRequestInfo) 24 | digest = hashlib.new(digest_algorithm) 25 | digest.update(der_bytes) 26 | digest = digest.digest() 27 | response = kms.sign(KeyId=key_id, Message=digest, MessageType='DIGEST', SigningAlgorithm=signing_algorithm) 28 | return response['Signature'] 29 | 30 | 31 | def output_csr(csr): 32 | print(start_marker) 33 | b64 = base64.b64encode(encoder.encode(csr)).decode('ascii') 34 | for line in textwrap.wrap(b64, width=64): 35 | print(line) 36 | print(end_marker) 37 | 38 | 39 | def signing_algorithm(hashalgo, signalgo): 40 | # Signature Algorithm OIDs retrieved from 41 | # https://www.ibm.com/docs/en/linux-on-systems?topic=linuxonibm/com.ibm.linux.z.wskc.doc/wskc_pka_pim_restrictions.html 42 | if hashalgo == 'sha512' and signalgo == 'ECDSA': 43 | return 'ECDSA_SHA_512', '1.2.840.10045.4.3.4' 44 | elif hashalgo == 'sha384' and signalgo == 'ECDSA': 45 | return 'ECDSA_SHA_384', '1.2.840.10045.4.3.3' 46 | elif hashalgo == 'sha256' and signalgo == 'ECDSA': 47 | return 'ECDSA_SHA_256', '1.2.840.10045.4.3.2' 48 | elif hashalgo == 'sha224' and signalgo == 'ECDSA': 49 | return 'ECDSA_SHA_224', '1.2.840.10045.4.3.1' 50 | elif hashalgo == 'sha512' and signalgo == 'RSA': 51 | return 'RSASSA_PKCS1_V1_5_SHA_512', '1.2.840.113549.1.1.13' 52 | elif hashalgo == 'sha384' and signalgo == 'RSA': 53 | return 'RSASSA_PKCS1_V1_5_SHA_384', '1.2.840.113549.1.1.12' 54 | elif hashalgo == 'sha256' and signalgo == 'RSA': 55 | return 'RSASSA_PKCS1_V1_5_SHA_256', '1.2.840.113549.1.1.11' 56 | else: 57 | raise Exception('unknown hash algorithm, please specify one of sha224, sha256, sha384, or sha512') 58 | 59 | 60 | def main(args): 61 | with open(args.csr, 'r') as f: 62 | substrate = pyasn1_modules.pem.readPemFromFile(f, startMarker=start_marker, endMarker=end_marker) 63 | csr = decoder.decode(substrate, asn1Spec=pyasn1_modules.rfc2986.CertificationRequest())[0] 64 | if not csr: 65 | raise Exception('file does not look like a CSR') 66 | 67 | # now get the key 68 | if not args.region: 69 | args.region = boto3.session.Session().region_name 70 | 71 | if args.profile: 72 | boto3.setup_default_session(profile_name=args.profile) 73 | kms = boto3.client('kms', region_name=args.region) 74 | 75 | response = kms.get_public_key(KeyId=args.keyid) 76 | pubkey_der = response['PublicKey'] 77 | csr['certificationRequestInfo']['subjectPKInfo'] = \ 78 | decoder.decode(pubkey_der, pyasn1_modules.rfc2314.SubjectPublicKeyInfo())[0] 79 | 80 | signatureBytes = sign_certification_request_info(kms, args.keyid, csr, args.hashalgo, 81 | signing_algorithm(args.hashalgo, args.signalgo)[0]) 82 | csr.setComponentByName('signature', univ.BitString.fromOctetString(signatureBytes)) 83 | 84 | sigAlgIdentifier = pyasn1_modules.rfc2314.SignatureAlgorithmIdentifier() 85 | sigAlgIdentifier.setComponentByName('algorithm', 86 | univ.ObjectIdentifier(signing_algorithm(args.hashalgo, args.signalgo)[1])) 87 | csr.setComponentByName('signatureAlgorithm', sigAlgIdentifier) 88 | 89 | output_csr(csr) 90 | 91 | 92 | if __name__ == '__main__': 93 | parser = argparse.ArgumentParser() 94 | parser.add_argument('csr', help="Source CSR (can be signed with any key)") 95 | parser.add_argument('--keyid', action='store', dest='keyid', help='key ID in AWS KMS') 96 | parser.add_argument('--region', action='store', dest='region', help='AWS region') 97 | parser.add_argument('--profile', action='store', dest='profile', help='AWS profile') 98 | parser.add_argument('--hashalgo', choices=['sha224', 'sha256', 'sha512', 'sha384'], default="sha256", 99 | help='hash algorithm to choose') 100 | parser.add_argument('--signalgo', choices=['ECDSA', 'RSA'], default="RSA", help='signing algorithm to choose') 101 | args = parser.parse_args() 102 | main(args) 103 | --------------------------------------------------------------------------------