1""" 2ECDSA key management 3""" 4 5# SPDX-License-Identifier: Apache-2.0 6import os.path 7import hashlib 8 9from cryptography.hazmat.backends import default_backend 10from cryptography.hazmat.primitives import serialization 11from cryptography.hazmat.primitives.asymmetric import ec 12from cryptography.hazmat.primitives.hashes import SHA256, SHA384 13 14from .general import KeyClass 15from .privatebytes import PrivateBytesMixin 16 17 18class ECDSAUsageError(Exception): 19 pass 20 21 22class ECDSAPublicKey(KeyClass): 23 """ 24 Wrapper around an ECDSA public key. 25 """ 26 def __init__(self, key): 27 self.key = key 28 29 def _unsupported(self, name): 30 raise ECDSAUsageError("Operation {} requires private key".format(name)) 31 32 def _get_public(self): 33 return self.key 34 35 def get_public_bytes(self): 36 # The key is embedded into MBUboot in "SubjectPublicKeyInfo" format 37 return self._get_public().public_bytes( 38 encoding=serialization.Encoding.DER, 39 format=serialization.PublicFormat.SubjectPublicKeyInfo) 40 41 def get_public_pem(self): 42 return self._get_public().public_bytes( 43 encoding=serialization.Encoding.PEM, 44 format=serialization.PublicFormat.SubjectPublicKeyInfo) 45 46 def get_private_bytes(self, minimal, format): 47 self._unsupported('get_private_bytes') 48 49 def export_private(self, path, passwd=None): 50 self._unsupported('export_private') 51 52 def export_public(self, path): 53 """Write the public key to the given file.""" 54 pem = self._get_public().public_bytes( 55 encoding=serialization.Encoding.PEM, 56 format=serialization.PublicFormat.SubjectPublicKeyInfo) 57 with open(path, 'wb') as f: 58 f.write(pem) 59 60 61class ECDSAPrivateKey(PrivateBytesMixin): 62 """ 63 Wrapper around an ECDSA private key. 64 """ 65 def __init__(self, key): 66 self.key = key 67 68 def _get_public(self): 69 return self.key.public_key() 70 71 def _build_minimal_ecdsa_privkey(self, der, format): 72 ''' 73 Builds a new DER that only includes the EC private key, removing the 74 public key that is added as an "optional" BITSTRING. 75 ''' 76 77 if format == serialization.PrivateFormat.OpenSSH: 78 print(os.path.basename(__file__) + 79 ': Warning: --minimal is supported only for PKCS8 ' 80 'or TraditionalOpenSSL formats') 81 return bytearray(der) 82 83 EXCEPTION_TEXT = "Error parsing ecdsa key. Please submit an issue!" 84 if format == serialization.PrivateFormat.PKCS8: 85 offset_PUB = 68 # where the context specific TLV starts (tag 0xA1) 86 if der[offset_PUB] != 0xa1: 87 raise ECDSAUsageError(EXCEPTION_TEXT) 88 len_PUB = der[offset_PUB + 1] + 2 # + 2 for 0xA1 0x44 bytes 89 b = bytearray(der[:offset_PUB]) # remove the TLV with the PUB key 90 offset_SEQ = 29 91 if b[offset_SEQ] != 0x30: 92 raise ECDSAUsageError(EXCEPTION_TEXT) 93 b[offset_SEQ + 1] -= len_PUB 94 offset_OCT_STR = 27 95 if b[offset_OCT_STR] != 0x04: 96 raise ECDSAUsageError(EXCEPTION_TEXT) 97 b[offset_OCT_STR + 1] -= len_PUB 98 if b[0] != 0x30 or b[1] != 0x81: 99 raise ECDSAUsageError(EXCEPTION_TEXT) 100 # as b[1] has bit7 set, the length is on b[2] 101 b[2] -= len_PUB 102 if b[2] < 0x80: 103 del(b[1]) 104 105 elif format == serialization.PrivateFormat.TraditionalOpenSSL: 106 offset_PUB = 51 107 if der[offset_PUB] != 0xA1: 108 raise ECDSAUsageError(EXCEPTION_TEXT) 109 len_PUB = der[offset_PUB + 1] + 2 110 b = bytearray(der[0:offset_PUB]) 111 b[1] -= len_PUB 112 113 return b 114 115 _VALID_FORMATS = { 116 'pkcs8': serialization.PrivateFormat.PKCS8, 117 'openssl': serialization.PrivateFormat.TraditionalOpenSSL 118 } 119 _DEFAULT_FORMAT = 'pkcs8' 120 121 def get_private_bytes(self, minimal, format): 122 format, priv = self._get_private_bytes(minimal, 123 format, ECDSAUsageError) 124 if minimal: 125 priv = self._build_minimal_ecdsa_privkey( 126 priv, self._VALID_FORMATS[format]) 127 return priv 128 129 def export_private(self, path, passwd=None): 130 """Write the private key to the given file, protecting it with ' 131 'the optional password.""" 132 if passwd is None: 133 enc = serialization.NoEncryption() 134 else: 135 enc = serialization.BestAvailableEncryption(passwd) 136 pem = self.key.private_bytes( 137 encoding=serialization.Encoding.PEM, 138 format=serialization.PrivateFormat.PKCS8, 139 encryption_algorithm=enc) 140 with open(path, 'wb') as f: 141 f.write(pem) 142 143 144class ECDSA256P1Public(ECDSAPublicKey): 145 """ 146 Wrapper around an ECDSA (p256) public key. 147 """ 148 def __init__(self, key): 149 super().__init__(key) 150 self.key = key 151 152 def shortname(self): 153 return "ecdsa" 154 155 def sig_type(self): 156 return "ECDSA256_SHA256" 157 158 def sig_tlv(self): 159 return "ECDSASIG" 160 161 def sig_len(self): 162 # Early versions of MCUboot (< v1.5.0) required ECDSA 163 # signatures to be padded to 72 bytes. Because the DER 164 # encoding is done with signed integers, the size of the 165 # signature will vary depending on whether the high bit is set 166 # in each value. This padding was done in a 167 # not-easily-reversible way (by just adding zeros). 168 # 169 # The signing code no longer requires this padding, and newer 170 # versions of MCUboot don't require it. But, continue to 171 # return the total length so that the padding can be done if 172 # requested. 173 return 72 174 175 def verify(self, signature, payload): 176 # strip possible paddings added during sign 177 signature = signature[:signature[1] + 2] 178 k = self.key 179 if isinstance(self.key, ec.EllipticCurvePrivateKey): 180 k = self.key.public_key() 181 return k.verify(signature=signature, data=payload, 182 signature_algorithm=ec.ECDSA(SHA256())) 183 184 185class ECDSA256P1(ECDSAPrivateKey, ECDSA256P1Public): 186 """ 187 Wrapper around an ECDSA (p256) private key. 188 """ 189 def __init__(self, key): 190 super().__init__(key) 191 self.key = key 192 self.pad_sig = False 193 194 @staticmethod 195 def generate(): 196 pk = ec.generate_private_key( 197 ec.SECP256R1(), 198 backend=default_backend()) 199 return ECDSA256P1(pk) 200 201 def raw_sign(self, payload): 202 """Return the actual signature""" 203 return self.key.sign( 204 data=payload, 205 signature_algorithm=ec.ECDSA(SHA256())) 206 207 def sign(self, payload): 208 sig = self.raw_sign(payload) 209 if self.pad_sig: 210 # To make fixed length, pad with one or two zeros. 211 sig += b'\000' * (self.sig_len() - len(sig)) 212 return sig 213 else: 214 return sig 215 216 217class ECDSA384P1Public(ECDSAPublicKey): 218 """ 219 Wrapper around an ECDSA (p384) public key. 220 """ 221 def __init__(self, key): 222 super().__init__(key) 223 self.key = key 224 225 def shortname(self): 226 return "ecdsap384" 227 228 def sig_type(self): 229 return "ECDSA384_SHA384" 230 231 def sig_tlv(self): 232 return "ECDSASIG" 233 234 def sig_len(self): 235 # Early versions of MCUboot (< v1.5.0) required ECDSA 236 # signatures to be padded to a fixed length. Because the DER 237 # encoding is done with signed integers, the size of the 238 # signature will vary depending on whether the high bit is set 239 # in each value. This padding was done in a 240 # not-easily-reversible way (by just adding zeros). 241 # 242 # The signing code no longer requires this padding, and newer 243 # versions of MCUboot don't require it. But, continue to 244 # return the total length so that the padding can be done if 245 # requested. 246 return 103 247 248 def verify(self, signature, payload): 249 # strip possible paddings added during sign 250 signature = signature[:signature[1] + 2] 251 k = self.key 252 if isinstance(self.key, ec.EllipticCurvePrivateKey): 253 k = self.key.public_key() 254 return k.verify(signature=signature, data=payload, 255 signature_algorithm=ec.ECDSA(SHA384())) 256 257 258class ECDSA384P1(ECDSAPrivateKey, ECDSA384P1Public): 259 """ 260 Wrapper around an ECDSA (p384) private key. 261 """ 262 263 def __init__(self, key): 264 """key should be an instance of EllipticCurvePrivateKey""" 265 super().__init__(key) 266 self.key = key 267 self.pad_sig = False 268 269 @staticmethod 270 def generate(): 271 pk = ec.generate_private_key( 272 ec.SECP384R1(), 273 backend=default_backend()) 274 return ECDSA384P1(pk) 275 276 def raw_sign(self, payload): 277 """Return the actual signature""" 278 return self.key.sign( 279 data=payload, 280 signature_algorithm=ec.ECDSA(SHA384())) 281 282 def sign(self, payload): 283 sig = self.raw_sign(payload) 284 if self.pad_sig: 285 # To make fixed length, pad with one or two zeros. 286 sig += b'\000' * (self.sig_len() - len(sig)) 287 return sig 288 else: 289 return sig 290