1# Copyright 2021 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Facilities for keys generation, importing, signing and verification. 15 16IMPORTANT: THESE FACILITIES ARE FOR LOCAL NON-PRODUCTION USE ONLY!! 17 18These are not suited for production use because: 19 201. The private keys are not generated without ANY supervision or authorization. 212. The private keys are not stored securely. 223. The underlying crypto library is not audited. 23""" 24 25import argparse 26import hashlib 27from pathlib import Path 28 29from cryptography.hazmat.primitives import hashes 30from cryptography.hazmat.primitives.asymmetric import ec 31from cryptography.hazmat.primitives.asymmetric.utils import ( 32 decode_dss_signature, encode_dss_signature) 33from cryptography.hazmat.primitives.serialization import ( 34 Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key, 35 load_pem_public_key) 36 37from pw_software_update.tuf_pb2 import (Key, KeyMapping, KeyScheme, KeyType, 38 Signature) 39 40 41def parse_args(): 42 """Parse CLI arguments.""" 43 parser = argparse.ArgumentParser(description=__doc__) 44 parser.add_argument('-o', 45 '--out', 46 type=Path, 47 required=True, 48 help='Output path for the generated key') 49 return parser.parse_args() 50 51 52def gen_ecdsa_keypair(out: Path) -> None: 53 """Generates and writes to disk a NIST-P256 EC key pair. 54 55 Args: 56 out: The path to write the private key to. The public key is written 57 to the same path as the private key using the suffix '.pub'. 58 """ 59 private_key = ec.generate_private_key(ec.SECP256R1()) 60 public_key = private_key.public_key() 61 private_pem = private_key.private_bytes( 62 encoding=Encoding.PEM, 63 format=PrivateFormat.PKCS8, 64 encryption_algorithm=NoEncryption()) 65 public_pem = public_key.public_bytes( 66 encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo) 67 68 out.write_bytes(private_pem) 69 public_out = (out.parent / f'{out.name}.pub') 70 public_out.write_bytes(public_pem) 71 72 73def gen_key_id(key: Key) -> bytes: 74 """Computes the key ID of a Key object.""" 75 sha = hashlib.sha256() 76 sha.update(key.key_type.to_bytes(1, 'big')) 77 sha.update(key.scheme.to_bytes(1, 'big')) 78 sha.update(key.keyval) 79 return sha.digest() 80 81 82def import_ecdsa_public_key(pem: bytes) -> KeyMapping: 83 """Imports an EC NIST-P256 public key in pem format.""" 84 ec_key = load_pem_public_key(pem) 85 86 if not isinstance(ec_key, ec.EllipticCurvePublicKey): 87 raise TypeError( 88 f'Not an elliptic curve public key type: {type(ec_key)}.' 89 'Try generate a key with gen_ecdsa_keypair()?') 90 91 # pylint: disable=no-member 92 if not (ec_key.curve.name == 'secp256r1' and ec_key.key_size == 256): 93 raise TypeError(f'Unsupported curve: {ec_key.curve.name}.' 94 'Try generate a key with gen_ecdsa_keypair()?') 95 # pylint: enable=no-member 96 97 tuf_key = Key(key_type=KeyType.ECDSA_SHA2_NISTP256, 98 scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME, 99 keyval=ec_key.public_bytes(Encoding.X962, 100 PublicFormat.UncompressedPoint)) 101 return KeyMapping(key_id=gen_key_id(tuf_key), key=tuf_key) 102 103 104def create_ecdsa_signature(data: bytes, key: bytes) -> Signature: 105 """Creates an ECDSA-SHA2-NISTP256 signature.""" 106 ec_key = load_pem_private_key(key, password=None) 107 if not isinstance(ec_key, ec.EllipticCurvePrivateKey): 108 raise TypeError(f'Not an elliptic curve private key: {type(ec_key)}.' 109 'Try generate a key with gen_ecdsa_keypair()?') 110 111 tuf_key = Key(key_type=KeyType.ECDSA_SHA2_NISTP256, 112 scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME, 113 keyval=ec_key.public_key().public_bytes( 114 Encoding.X962, PublicFormat.UncompressedPoint)) 115 116 der_signature = ec_key.sign(data, ec.ECDSA(hashes.SHA256())) # pylint: disable=no-value-for-parameter 117 int_r, int_s = decode_dss_signature(der_signature) 118 sig_bytes = int_r.to_bytes(32, 'big') + int_s.to_bytes(32, 'big') 119 120 return Signature(key_id=gen_key_id(tuf_key), sig=sig_bytes) 121 122 123def verify_ecdsa_signature(sig: bytes, data: bytes, key: Key) -> bool: 124 """Verifies an ECDSA-SHA2-NISTP256 signature with a given public key. 125 126 Args: 127 sig: the ECDSA signature as raw bytes (r||s). 128 data: the message as plain text. 129 key: the ECDSA-NISTP256 public key. 130 131 Returns: 132 True if the signature is verified. False otherwise. 133 """ 134 ec_key = ec.EllipticCurvePublicKey.from_encoded_point( 135 ec.SECP256R1(), key.keyval) 136 try: 137 dss_sig = encode_dss_signature(int.from_bytes(sig[:32], 'big'), 138 int.from_bytes(sig[-32:], 'big')) 139 ec_key.verify(dss_sig, data, ec.ECDSA(hashes.SHA256())) 140 except: # pylint: disable=bare-except 141 return False 142 143 return True 144 145 146def main(out: Path) -> None: 147 """Generates and writes to disk key pairs for development use.""" 148 149 # Currently only supports the "ecdsa-sha2-nistp256" key scheme. 150 # 151 # TODO(alizhang): Add support for "rsassa-pss-sha256" and "ed25519" key 152 # schemes. 153 gen_ecdsa_keypair(out) 154 155 156if __name__ == '__main__': 157 main(**vars(parse_args())) 158