| 1 | # -*- test-case-name: twisted.conch.test.test_keys -*- |
|---|
| 2 | # Copyright (c) Twisted Matrix Laboratories. |
|---|
| 3 | # See LICENSE for details. |
|---|
| 4 | |
|---|
| 5 | """ |
|---|
| 6 | Handling of RSA and DSA keys. |
|---|
| 7 | |
|---|
| 8 | Maintainer: U{Paul Swartz} |
|---|
| 9 | """ |
|---|
| 10 | |
|---|
| 11 | # base library imports |
|---|
| 12 | import base64 |
|---|
| 13 | import warnings |
|---|
| 14 | import itertools |
|---|
| 15 | |
|---|
| 16 | # external library imports |
|---|
| 17 | from Crypto.Cipher import DES3 |
|---|
| 18 | from Crypto.PublicKey import RSA, DSA |
|---|
| 19 | from Crypto import Util |
|---|
| 20 | from pyasn1.type import univ |
|---|
| 21 | from pyasn1.codec.ber import decoder as berDecoder |
|---|
| 22 | from pyasn1.codec.ber import encoder as berEncoder |
|---|
| 23 | |
|---|
| 24 | # twisted |
|---|
| 25 | from twisted.python import randbytes |
|---|
| 26 | from twisted.python.hashlib import md5, sha1 |
|---|
| 27 | |
|---|
| 28 | # sibling imports |
|---|
| 29 | from twisted.conch.ssh import common, sexpy |
|---|
| 30 | |
|---|
| 31 | |
|---|
| 32 | class BadKeyError(Exception): |
|---|
| 33 | """ |
|---|
| 34 | Raised when a key isn't what we expected from it. |
|---|
| 35 | |
|---|
| 36 | XXX: we really need to check for bad keys |
|---|
| 37 | """ |
|---|
| 38 | |
|---|
| 39 | class EncryptedKeyError(Exception): |
|---|
| 40 | """ |
|---|
| 41 | Raised when an encrypted key is presented to fromString/fromFile without |
|---|
| 42 | a password. |
|---|
| 43 | """ |
|---|
| 44 | |
|---|
| 45 | class Key(object): |
|---|
| 46 | """ |
|---|
| 47 | An object representing a key. A key can be either a public or |
|---|
| 48 | private key. A public key can verify a signature; a private key can |
|---|
| 49 | create or verify a signature. To generate a string that can be stored |
|---|
| 50 | on disk, use the toString method. If you have a private key, but want |
|---|
| 51 | the string representation of the public key, use Key.public().toString(). |
|---|
| 52 | |
|---|
| 53 | @ivar keyObject: The C{Crypto.PublicKey.pubkey.pubkey} object that |
|---|
| 54 | operations are performed with. |
|---|
| 55 | """ |
|---|
| 56 | |
|---|
| 57 | def fromFile(Class, filename, type=None, passphrase=None): |
|---|
| 58 | """ |
|---|
| 59 | Return a Key object corresponding to the data in filename. type |
|---|
| 60 | and passphrase function as they do in fromString. |
|---|
| 61 | """ |
|---|
| 62 | return Class.fromString(file(filename, 'rb').read(), type, passphrase) |
|---|
| 63 | fromFile = classmethod(fromFile) |
|---|
| 64 | |
|---|
| 65 | def fromString(Class, data, type=None, passphrase=None): |
|---|
| 66 | """ |
|---|
| 67 | Return a Key object corresponding to the string data. |
|---|
| 68 | type is optionally the type of string, matching a _fromString_* |
|---|
| 69 | method. Otherwise, the _guessStringType() classmethod will be used |
|---|
| 70 | to guess a type. If the key is encrypted, passphrase is used as |
|---|
| 71 | the decryption key. |
|---|
| 72 | |
|---|
| 73 | @type data: C{str} |
|---|
| 74 | @type type: C{None}/C{str} |
|---|
| 75 | @type passphrase: C{None}/C{str} |
|---|
| 76 | @rtype: C{Key} |
|---|
| 77 | """ |
|---|
| 78 | if type is None: |
|---|
| 79 | type = Class._guessStringType(data) |
|---|
| 80 | if type is None: |
|---|
| 81 | raise BadKeyError('cannot guess the type of %r' % data) |
|---|
| 82 | method = getattr(Class, '_fromString_%s' % type.upper(), None) |
|---|
| 83 | if method is None: |
|---|
| 84 | raise BadKeyError('no _fromString method for %s' % type) |
|---|
| 85 | if method.func_code.co_argcount == 2: # no passphrase |
|---|
| 86 | if passphrase: |
|---|
| 87 | raise BadKeyError('key not encrypted') |
|---|
| 88 | return method(data) |
|---|
| 89 | else: |
|---|
| 90 | return method(data, passphrase) |
|---|
| 91 | fromString = classmethod(fromString) |
|---|
| 92 | |
|---|
| 93 | def _fromString_BLOB(Class, blob): |
|---|
| 94 | """ |
|---|
| 95 | Return a public key object corresponding to this public key blob. |
|---|
| 96 | The format of a RSA public key blob is:: |
|---|
| 97 | string 'ssh-rsa' |
|---|
| 98 | integer e |
|---|
| 99 | integer n |
|---|
| 100 | |
|---|
| 101 | The format of a DSA public key blob is:: |
|---|
| 102 | string 'ssh-dss' |
|---|
| 103 | integer p |
|---|
| 104 | integer q |
|---|
| 105 | integer g |
|---|
| 106 | integer y |
|---|
| 107 | |
|---|
| 108 | @type blob: C{str} |
|---|
| 109 | @return: a C{Crypto.PublicKey.pubkey.pubkey} object |
|---|
| 110 | @raises BadKeyError: if the key type (the first string) is unknown. |
|---|
| 111 | """ |
|---|
| 112 | keyType, rest = common.getNS(blob) |
|---|
| 113 | if keyType == 'ssh-rsa': |
|---|
| 114 | e, n, rest = common.getMP(rest, 2) |
|---|
| 115 | return Class(RSA.construct((n, e))) |
|---|
| 116 | elif keyType == 'ssh-dss': |
|---|
| 117 | p, q, g, y, rest = common.getMP(rest, 4) |
|---|
| 118 | return Class(DSA.construct((y, g, p, q))) |
|---|
| 119 | else: |
|---|
| 120 | raise BadKeyError('unknown blob type: %s' % keyType) |
|---|
| 121 | _fromString_BLOB = classmethod(_fromString_BLOB) |
|---|
| 122 | |
|---|
| 123 | def _fromString_PRIVATE_BLOB(Class, blob): |
|---|
| 124 | """ |
|---|
| 125 | Return a private key object corresponding to this private key blob. |
|---|
| 126 | The blob formats are as follows: |
|---|
| 127 | |
|---|
| 128 | RSA keys:: |
|---|
| 129 | string 'ssh-rsa' |
|---|
| 130 | integer n |
|---|
| 131 | integer e |
|---|
| 132 | integer d |
|---|
| 133 | integer u |
|---|
| 134 | integer p |
|---|
| 135 | integer q |
|---|
| 136 | |
|---|
| 137 | DSA keys:: |
|---|
| 138 | string 'ssh-dss' |
|---|
| 139 | integer p |
|---|
| 140 | integer q |
|---|
| 141 | integer g |
|---|
| 142 | integer y |
|---|
| 143 | integer x |
|---|
| 144 | |
|---|
| 145 | @type blob: C{str} |
|---|
| 146 | @return: a C{Crypto.PublicKey.pubkey.pubkey} object |
|---|
| 147 | @raises BadKeyError: if the key type (the first string) is unknown. |
|---|
| 148 | """ |
|---|
| 149 | keyType, rest = common.getNS(blob) |
|---|
| 150 | |
|---|
| 151 | if keyType == 'ssh-rsa': |
|---|
| 152 | n, e, d, u, p, q, rest = common.getMP(rest, 6) |
|---|
| 153 | rsakey = Class(RSA.construct((n, e, d, p, q, u))) |
|---|
| 154 | return rsakey |
|---|
| 155 | elif keyType == 'ssh-dss': |
|---|
| 156 | p, q, g, y, x, rest = common.getMP(rest, 5) |
|---|
| 157 | dsakey = Class(DSA.construct((y, g, p, q, x))) |
|---|
| 158 | return dsakey |
|---|
| 159 | else: |
|---|
| 160 | raise BadKeyError('unknown blob type: %s' % keyType) |
|---|
| 161 | _fromString_PRIVATE_BLOB = classmethod(_fromString_PRIVATE_BLOB) |
|---|
| 162 | |
|---|
| 163 | def _fromString_PUBLIC_OPENSSH(Class, data): |
|---|
| 164 | """ |
|---|
| 165 | Return a public key object corresponding to this OpenSSH public key |
|---|
| 166 | string. The format of an OpenSSH public key string is:: |
|---|
| 167 | <key type> <base64-encoded public key blob> |
|---|
| 168 | |
|---|
| 169 | @type data: C{str} |
|---|
| 170 | @return: A {Crypto.PublicKey.pubkey.pubkey} object |
|---|
| 171 | @raises BadKeyError: if the blob type is unknown. |
|---|
| 172 | """ |
|---|
| 173 | blob = base64.decodestring(data.split()[1]) |
|---|
| 174 | return Class._fromString_BLOB(blob) |
|---|
| 175 | _fromString_PUBLIC_OPENSSH = classmethod(_fromString_PUBLIC_OPENSSH) |
|---|
| 176 | |
|---|
| 177 | def _fromString_PRIVATE_OPENSSH(Class, data, passphrase): |
|---|
| 178 | """ |
|---|
| 179 | Return a private key object corresponding to this OpenSSH private key |
|---|
| 180 | string. If the key is encrypted, passphrase MUST be provided. |
|---|
| 181 | Providing a passphrase for an unencrypted key is an error. |
|---|
| 182 | |
|---|
| 183 | The format of an OpenSSH private key string is:: |
|---|
| 184 | -----BEGIN <key type> PRIVATE KEY----- |
|---|
| 185 | [Proc-Type: 4,ENCRYPTED |
|---|
| 186 | DEK-Info: DES-EDE3-CBC,<initialization value>] |
|---|
| 187 | <base64-encoded ASN.1 structure> |
|---|
| 188 | ------END <key type> PRIVATE KEY------ |
|---|
| 189 | |
|---|
| 190 | The ASN.1 structure of a RSA key is:: |
|---|
| 191 | (0, n, e, d, p, q) |
|---|
| 192 | |
|---|
| 193 | The ASN.1 structure of a DSA key is:: |
|---|
| 194 | (0, p, q, g, y, x) |
|---|
| 195 | |
|---|
| 196 | @type data: C{str} |
|---|
| 197 | @type passphrase: C{str} |
|---|
| 198 | @return: a C{Crypto.PublicKey.pubkey.pubkey} object |
|---|
| 199 | @raises BadKeyError: if |
|---|
| 200 | * a passphrase is provided for an unencrypted key |
|---|
| 201 | * a passphrase is not provided for an encrypted key |
|---|
| 202 | * the ASN.1 encoding is incorrect |
|---|
| 203 | """ |
|---|
| 204 | lines = [x + '\n' for x in data.split('\n')] |
|---|
| 205 | kind = lines[0][11:14] |
|---|
| 206 | if lines[1].startswith('Proc-Type: 4,ENCRYPTED'): # encrypted key |
|---|
| 207 | ivdata = lines[2].split(',')[1][:-1] |
|---|
| 208 | iv = ''.join([chr(int(ivdata[i:i + 2], 16)) for i in range(0, |
|---|
| 209 | len(ivdata), 2)]) |
|---|
| 210 | if not passphrase: |
|---|
| 211 | raise EncryptedKeyError('encrypted key with no passphrase') |
|---|
| 212 | ba = md5(passphrase + iv).digest() |
|---|
| 213 | bb = md5(ba + passphrase + iv).digest() |
|---|
| 214 | decKey = (ba + bb)[:24] |
|---|
| 215 | b64Data = base64.decodestring(''.join(lines[3:-1])) |
|---|
| 216 | keyData = DES3.new(decKey, DES3.MODE_CBC, iv).decrypt(b64Data) |
|---|
| 217 | removeLen = ord(keyData[-1]) |
|---|
| 218 | keyData = keyData[:-removeLen] |
|---|
| 219 | else: |
|---|
| 220 | b64Data = ''.join(lines[1:-1]) |
|---|
| 221 | keyData = base64.decodestring(b64Data) |
|---|
| 222 | try: |
|---|
| 223 | decodedKey = berDecoder.decode(keyData)[0] |
|---|
| 224 | except Exception, e: |
|---|
| 225 | raise BadKeyError, 'something wrong with decode' |
|---|
| 226 | if kind == 'RSA': |
|---|
| 227 | if len(decodedKey) == 2: # alternate RSA key |
|---|
| 228 | decodedKey = decodedKey[0] |
|---|
| 229 | if len(decodedKey) < 6: |
|---|
| 230 | raise BadKeyError('RSA key failed to decode properly') |
|---|
| 231 | n, e, d, p, q = [long(value) for value in decodedKey[1:6]] |
|---|
| 232 | if p > q: # make p smaller than q |
|---|
| 233 | p, q = q, p |
|---|
| 234 | return Class(RSA.construct((n, e, d, p, q))) |
|---|
| 235 | elif kind == 'DSA': |
|---|
| 236 | p, q, g, y, x = [long(value) for value in decodedKey[1: 6]] |
|---|
| 237 | if len(decodedKey) < 6: |
|---|
| 238 | raise BadKeyError('DSA key failed to decode properly') |
|---|
| 239 | return Class(DSA.construct((y, g, p, q, x))) |
|---|
| 240 | _fromString_PRIVATE_OPENSSH = classmethod(_fromString_PRIVATE_OPENSSH) |
|---|
| 241 | |
|---|
| 242 | def _fromString_PUBLIC_LSH(Class, data): |
|---|
| 243 | """ |
|---|
| 244 | Return a public key corresponding to this LSH public key string. |
|---|
| 245 | The LSH public key string format is:: |
|---|
| 246 | <s-expression: ('public-key', (<key type>, (<name, <value>)+))> |
|---|
| 247 | |
|---|
| 248 | The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e. |
|---|
| 249 | The names for a DSA (key type 'dsa') key are: y, g, p, q. |
|---|
| 250 | |
|---|
| 251 | @type data: C{str} |
|---|
| 252 | @return: a C{Crypto.PublicKey.pubkey.pubkey} object |
|---|
| 253 | @raises BadKeyError: if the key type is unknown |
|---|
| 254 | """ |
|---|
| 255 | sexp = sexpy.parse(base64.decodestring(data[1:-1])) |
|---|
| 256 | assert sexp[0] == 'public-key' |
|---|
| 257 | kd = {} |
|---|
| 258 | for name, data in sexp[1][1:]: |
|---|
| 259 | kd[name] = common.getMP(common.NS(data))[0] |
|---|
| 260 | if sexp[1][0] == 'dsa': |
|---|
| 261 | return Class(DSA.construct((kd['y'], kd['g'], kd['p'], kd['q']))) |
|---|
| 262 | elif sexp[1][0] == 'rsa-pkcs1-sha1': |
|---|
| 263 | return Class(RSA.construct((kd['n'], kd['e']))) |
|---|
| 264 | else: |
|---|
| 265 | raise BadKeyError('unknown lsh key type %s' % sexp[1][0]) |
|---|
| 266 | _fromString_PUBLIC_LSH = classmethod(_fromString_PUBLIC_LSH) |
|---|
| 267 | |
|---|
| 268 | def _fromString_PRIVATE_LSH(Class, data): |
|---|
| 269 | """ |
|---|
| 270 | Return a private key corresponding to this LSH private key string. |
|---|
| 271 | The LSH private key string format is:: |
|---|
| 272 | <s-expression: ('private-key', (<key type>, (<name>, <value>)+))> |
|---|
| 273 | |
|---|
| 274 | The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e, d, p, q. |
|---|
| 275 | The names for a DSA (key type 'dsa') key are: y, g, p, q, x. |
|---|
| 276 | |
|---|
| 277 | @type data: C{str} |
|---|
| 278 | @return: a {Crypto.PublicKey.pubkey.pubkey} object |
|---|
| 279 | @raises BadKeyError: if the key type is unknown |
|---|
| 280 | """ |
|---|
| 281 | sexp = sexpy.parse(data) |
|---|
| 282 | assert sexp[0] == 'private-key' |
|---|
| 283 | kd = {} |
|---|
| 284 | for name, data in sexp[1][1:]: |
|---|
| 285 | kd[name] = common.getMP(common.NS(data))[0] |
|---|
| 286 | if sexp[1][0] == 'dsa': |
|---|
| 287 | assert len(kd) == 5, len(kd) |
|---|
| 288 | return Class(DSA.construct((kd['y'], kd['g'], kd['p'], |
|---|
| 289 | kd['q'], kd['x']))) |
|---|
| 290 | elif sexp[1][0] == 'rsa-pkcs1': |
|---|
| 291 | assert len(kd) == 8, len(kd) |
|---|
| 292 | if kd['p'] > kd['q']: # make p smaller than q |
|---|
| 293 | kd['p'], kd['q'] = kd['q'], kd['p'] |
|---|
| 294 | return Class(RSA.construct((kd['n'], kd['e'], kd['d'], |
|---|
| 295 | kd['p'], kd['q']))) |
|---|
| 296 | else: |
|---|
| 297 | raise BadKeyError('unknown lsh key type %s' % sexp[1][0]) |
|---|
| 298 | _fromString_PRIVATE_LSH = classmethod(_fromString_PRIVATE_LSH) |
|---|
| 299 | |
|---|
| 300 | def _fromString_AGENTV3(Class, data): |
|---|
| 301 | """ |
|---|
| 302 | Return a private key object corresponsing to the Secure Shell Key |
|---|
| 303 | Agent v3 format. |
|---|
| 304 | |
|---|
| 305 | The SSH Key Agent v3 format for a RSA key is:: |
|---|
| 306 | string 'ssh-rsa' |
|---|
| 307 | integer e |
|---|
| 308 | integer d |
|---|
| 309 | integer n |
|---|
| 310 | integer u |
|---|
| 311 | integer p |
|---|
| 312 | integer q |
|---|
| 313 | |
|---|
| 314 | The SSH Key Agent v3 format for a DSA key is:: |
|---|
| 315 | string 'ssh-dss' |
|---|
| 316 | integer p |
|---|
| 317 | integer q |
|---|
| 318 | integer g |
|---|
| 319 | integer y |
|---|
| 320 | integer x |
|---|
| 321 | |
|---|
| 322 | @type data: C{str} |
|---|
| 323 | @return: a C{Crypto.PublicKey.pubkey.pubkey} object |
|---|
| 324 | @raises BadKeyError: if the key type (the first string) is unknown |
|---|
| 325 | """ |
|---|
| 326 | keyType, data = common.getNS(data) |
|---|
| 327 | if keyType == 'ssh-dss': |
|---|
| 328 | p, data = common.getMP(data) |
|---|
| 329 | q, data = common.getMP(data) |
|---|
| 330 | g, data = common.getMP(data) |
|---|
| 331 | y, data = common.getMP(data) |
|---|
| 332 | x, data = common.getMP(data) |
|---|
| 333 | return Class(DSA.construct((y,g,p,q,x))) |
|---|
| 334 | elif keyType == 'ssh-rsa': |
|---|
| 335 | e, data = common.getMP(data) |
|---|
| 336 | d, data = common.getMP(data) |
|---|
| 337 | n, data = common.getMP(data) |
|---|
| 338 | u, data = common.getMP(data) |
|---|
| 339 | p, data = common.getMP(data) |
|---|
| 340 | q, data = common.getMP(data) |
|---|
| 341 | return Class(RSA.construct((n,e,d,p,q,u))) |
|---|
| 342 | else: |
|---|
| 343 | raise BadKeyError("unknown key type %s" % keyType) |
|---|
| 344 | _fromString_AGENTV3 = classmethod(_fromString_AGENTV3) |
|---|
| 345 | |
|---|
| 346 | def _guessStringType(Class, data): |
|---|
| 347 | """ |
|---|
| 348 | Guess the type of key in data. The types map to _fromString_* |
|---|
| 349 | methods. |
|---|
| 350 | """ |
|---|
| 351 | if data.startswith('ssh-'): |
|---|
| 352 | return 'public_openssh' |
|---|
| 353 | elif data.startswith('-----BEGIN'): |
|---|
| 354 | return 'private_openssh' |
|---|
| 355 | elif data.startswith('{'): |
|---|
| 356 | return 'public_lsh' |
|---|
| 357 | elif data.startswith('('): |
|---|
| 358 | return 'private_lsh' |
|---|
| 359 | elif data.startswith('\x00\x00\x00\x07ssh-'): |
|---|
| 360 | ignored, rest = common.getNS(data) |
|---|
| 361 | count = 0 |
|---|
| 362 | while rest: |
|---|
| 363 | count += 1 |
|---|
| 364 | ignored, rest = common.getMP(rest) |
|---|
| 365 | if count > 4: |
|---|
| 366 | return 'agentv3' |
|---|
| 367 | else: |
|---|
| 368 | return 'blob' |
|---|
| 369 | _guessStringType = classmethod(_guessStringType) |
|---|
| 370 | |
|---|
| 371 | def __init__(self, keyObject): |
|---|
| 372 | """ |
|---|
| 373 | Initialize a PublicKey with a C{Crypto.PublicKey.pubkey.pubkey} |
|---|
| 374 | object. |
|---|
| 375 | |
|---|
| 376 | @type keyObject: C{Crypto.PublicKey.pubkey.pubkey} |
|---|
| 377 | """ |
|---|
| 378 | self.keyObject = keyObject |
|---|
| 379 | |
|---|
| 380 | def __eq__(self, other): |
|---|
| 381 | """ |
|---|
| 382 | Return True if other represents an object with the same key. |
|---|
| 383 | """ |
|---|
| 384 | if type(self) == type(other): |
|---|
| 385 | return self.type() == other.type() and self.data() == other.data() |
|---|
| 386 | else: |
|---|
| 387 | return NotImplemented |
|---|
| 388 | |
|---|
| 389 | def __ne__(self, other): |
|---|
| 390 | """ |
|---|
| 391 | Return True if other represents anything other than this key. |
|---|
| 392 | """ |
|---|
| 393 | result = self.__eq__(other) |
|---|
| 394 | if result == NotImplemented: |
|---|
| 395 | return result |
|---|
| 396 | return not result |
|---|
| 397 | |
|---|
| 398 | def __repr__(self): |
|---|
| 399 | """ |
|---|
| 400 | Return a pretty representation of this object. |
|---|
| 401 | """ |
|---|
| 402 | lines = ['<%s %s (%s bits)' % (self.type(), |
|---|
| 403 | self.isPublic() and 'Public Key' or 'Private Key', |
|---|
| 404 | self.keyObject.size())] |
|---|
| 405 | for k, v in self.data().items(): |
|---|
| 406 | lines.append('attr %s:' % k) |
|---|
| 407 | by = common.MP(v)[4:] |
|---|
| 408 | while by: |
|---|
| 409 | m = by[:15] |
|---|
| 410 | by = by[15:] |
|---|
| 411 | o = '' |
|---|
| 412 | for c in m: |
|---|
| 413 | o = o + '%02x:' % ord(c) |
|---|
| 414 | if len(m) < 15: |
|---|
| 415 | o = o[:-1] |
|---|
| 416 | lines.append('\t' + o) |
|---|
| 417 | lines[-1] = lines[-1] + '>' |
|---|
| 418 | return '\n'.join(lines) |
|---|
| 419 | |
|---|
| 420 | def isPublic(self): |
|---|
| 421 | """ |
|---|
| 422 | Returns True if this Key is a public key. |
|---|
| 423 | """ |
|---|
| 424 | return not self.keyObject.has_private() |
|---|
| 425 | |
|---|
| 426 | def public(self): |
|---|
| 427 | """ |
|---|
| 428 | Returns a version of this key containing only the public key data. |
|---|
| 429 | If this is a public key, this may or may not be the same object |
|---|
| 430 | as self. |
|---|
| 431 | """ |
|---|
| 432 | return Key(self.keyObject.publickey()) |
|---|
| 433 | |
|---|
| 434 | |
|---|
| 435 | def fingerprint(self): |
|---|
| 436 | """ |
|---|
| 437 | Get the user presentation of the fingerprint of this L{Key}. As |
|---|
| 438 | described by U{RFC 4716 section |
|---|
| 439 | 4<http://tools.ietf.org/html/rfc4716#section-4>}:: |
|---|
| 440 | |
|---|
| 441 | The fingerprint of a public key consists of the output of the MD5 |
|---|
| 442 | message-digest algorithm [RFC1321]. The input to the algorithm is |
|---|
| 443 | the public key data as specified by [RFC4253]. (...) The output |
|---|
| 444 | of the (MD5) algorithm is presented to the user as a sequence of 16 |
|---|
| 445 | octets printed as hexadecimal with lowercase letters and separated |
|---|
| 446 | by colons. |
|---|
| 447 | |
|---|
| 448 | @since: 8.2 |
|---|
| 449 | |
|---|
| 450 | @return: the user presentation of this L{Key}'s fingerprint, as a |
|---|
| 451 | string. |
|---|
| 452 | |
|---|
| 453 | @rtype: L{str} |
|---|
| 454 | """ |
|---|
| 455 | return ':'.join([x.encode('hex') for x in md5(self.blob()).digest()]) |
|---|
| 456 | |
|---|
| 457 | |
|---|
| 458 | def type(self): |
|---|
| 459 | """ |
|---|
| 460 | Return the type of the object we wrap. Currently this can only be |
|---|
| 461 | 'RSA' or 'DSA'. |
|---|
| 462 | """ |
|---|
| 463 | # the class is Crypto.PublicKey.<type>.<stuff we don't care about> |
|---|
| 464 | mod = self.keyObject.__class__.__module__ |
|---|
| 465 | if mod.startswith('Crypto.PublicKey'): |
|---|
| 466 | type = mod.split('.')[2] |
|---|
| 467 | else: |
|---|
| 468 | raise RuntimeError('unknown type of object: %r' % self.keyObject) |
|---|
| 469 | if type in ('RSA', 'DSA'): |
|---|
| 470 | return type |
|---|
| 471 | else: |
|---|
| 472 | raise RuntimeError('unknown type of key: %s' % type) |
|---|
| 473 | |
|---|
| 474 | def sshType(self): |
|---|
| 475 | """ |
|---|
| 476 | Return the type of the object we wrap as defined in the ssh protocol. |
|---|
| 477 | Currently this can only be 'ssh-rsa' or 'ssh-dss'. |
|---|
| 478 | """ |
|---|
| 479 | return {'RSA':'ssh-rsa', 'DSA':'ssh-dss'}[self.type()] |
|---|
| 480 | |
|---|
| 481 | def data(self): |
|---|
| 482 | """ |
|---|
| 483 | Return the values of the public key as a dictionary. |
|---|
| 484 | |
|---|
| 485 | @rtype: C{dict} |
|---|
| 486 | """ |
|---|
| 487 | keyData = {} |
|---|
| 488 | for name in self.keyObject.keydata: |
|---|
| 489 | value = getattr(self.keyObject, name, None) |
|---|
| 490 | if value is not None: |
|---|
| 491 | keyData[name] = value |
|---|
| 492 | return keyData |
|---|
| 493 | |
|---|
| 494 | def blob(self): |
|---|
| 495 | """ |
|---|
| 496 | Return the public key blob for this key. The blob is the |
|---|
| 497 | over-the-wire format for public keys: |
|---|
| 498 | |
|---|
| 499 | RSA keys:: |
|---|
| 500 | string 'ssh-rsa' |
|---|
| 501 | integer e |
|---|
| 502 | integer n |
|---|
| 503 | |
|---|
| 504 | DSA keys:: |
|---|
| 505 | string 'ssh-dss' |
|---|
| 506 | integer p |
|---|
| 507 | integer q |
|---|
| 508 | integer g |
|---|
| 509 | integer y |
|---|
| 510 | |
|---|
| 511 | @rtype: C{str} |
|---|
| 512 | """ |
|---|
| 513 | type = self.type() |
|---|
| 514 | data = self.data() |
|---|
| 515 | if type == 'RSA': |
|---|
| 516 | return (common.NS('ssh-rsa') + common.MP(data['e']) + |
|---|
| 517 | common.MP(data['n'])) |
|---|
| 518 | elif type == 'DSA': |
|---|
| 519 | return (common.NS('ssh-dss') + common.MP(data['p']) + |
|---|
| 520 | common.MP(data['q']) + common.MP(data['g']) + |
|---|
| 521 | common.MP(data['y'])) |
|---|
| 522 | |
|---|
| 523 | def privateBlob(self): |
|---|
| 524 | """ |
|---|
| 525 | Return the private key blob for this key. The blob is the |
|---|
| 526 | over-the-wire format for private keys: |
|---|
| 527 | |
|---|
| 528 | RSA keys:: |
|---|
| 529 | string 'ssh-rsa' |
|---|
| 530 | integer n |
|---|
| 531 | integer e |
|---|
| 532 | integer d |
|---|
| 533 | integer u |
|---|
| 534 | integer p |
|---|
| 535 | integer q |
|---|
| 536 | |
|---|
| 537 | DSA keys:: |
|---|
| 538 | string 'ssh-dss' |
|---|
| 539 | integer p |
|---|
| 540 | integer q |
|---|
| 541 | integer g |
|---|
| 542 | integer y |
|---|
| 543 | integer x |
|---|
| 544 | """ |
|---|
| 545 | type = self.type() |
|---|
| 546 | data = self.data() |
|---|
| 547 | if type == 'RSA': |
|---|
| 548 | return (common.NS('ssh-rsa') + common.MP(data['n']) + |
|---|
| 549 | common.MP(data['e']) + common.MP(data['d']) + |
|---|
| 550 | common.MP(data['u']) + common.MP(data['p']) + |
|---|
| 551 | common.MP(data['q'])) |
|---|
| 552 | elif type == 'DSA': |
|---|
| 553 | return (common.NS('ssh-dss') + common.MP(data['p']) + |
|---|
| 554 | common.MP(data['q']) + common.MP(data['g']) + |
|---|
| 555 | common.MP(data['y']) + common.MP(data['x'])) |
|---|
| 556 | |
|---|
| 557 | def toString(self, type, extra=None): |
|---|
| 558 | """ |
|---|
| 559 | Create a string representation of this key. If the key is a private |
|---|
| 560 | key and you want the represenation of its public key, use |
|---|
| 561 | C{key.public().toString()}. type maps to a _toString_* method. |
|---|
| 562 | |
|---|
| 563 | @param type: The type of string to emit. Currently supported values |
|---|
| 564 | are C{'OPENSSH'}, C{'LSH'}, and C{'AGENTV3'}. |
|---|
| 565 | @type type: L{str} |
|---|
| 566 | |
|---|
| 567 | @param extra: Any extra data supported by the selected format which |
|---|
| 568 | is not part of the key itself. For public OpenSSH keys, this is |
|---|
| 569 | a comment. For private OpenSSH keys, this is a passphrase to |
|---|
| 570 | encrypt with. |
|---|
| 571 | @type extra: L{str} or L{NoneType} |
|---|
| 572 | |
|---|
| 573 | @rtype: L{str} |
|---|
| 574 | """ |
|---|
| 575 | method = getattr(self, '_toString_%s' % type.upper(), None) |
|---|
| 576 | if method is None: |
|---|
| 577 | raise BadKeyError('unknown type: %s' % type) |
|---|
| 578 | if method.func_code.co_argcount == 2: |
|---|
| 579 | return method(extra) |
|---|
| 580 | else: |
|---|
| 581 | return method() |
|---|
| 582 | |
|---|
| 583 | def _toString_OPENSSH(self, extra): |
|---|
| 584 | """ |
|---|
| 585 | Return a public or private OpenSSH string. See |
|---|
| 586 | _fromString_PUBLIC_OPENSSH and _fromString_PRIVATE_OPENSSH for the |
|---|
| 587 | string formats. If extra is present, it represents a comment for a |
|---|
| 588 | public key, or a passphrase for a private key. |
|---|
| 589 | |
|---|
| 590 | @type extra: C{str} |
|---|
| 591 | @rtype: C{str} |
|---|
| 592 | """ |
|---|
| 593 | data = self.data() |
|---|
| 594 | if self.isPublic(): |
|---|
| 595 | b64Data = base64.encodestring(self.blob()).replace('\n', '') |
|---|
| 596 | if not extra: |
|---|
| 597 | extra = '' |
|---|
| 598 | return ('%s %s %s' % (self.sshType(), b64Data, extra)).strip() |
|---|
| 599 | else: |
|---|
| 600 | lines = ['-----BEGIN %s PRIVATE KEY-----' % self.type()] |
|---|
| 601 | if self.type() == 'RSA': |
|---|
| 602 | p, q = data['p'], data['q'] |
|---|
| 603 | objData = (0, data['n'], data['e'], data['d'], q, p, |
|---|
| 604 | data['d'] % (q - 1), data['d'] % (p - 1), |
|---|
| 605 | data['u']) |
|---|
| 606 | else: |
|---|
| 607 | objData = (0, data['p'], data['q'], data['g'], data['y'], |
|---|
| 608 | data['x']) |
|---|
| 609 | asn1Sequence = univ.Sequence() |
|---|
| 610 | for index, value in itertools.izip(itertools.count(), objData): |
|---|
| 611 | asn1Sequence.setComponentByPosition(index, univ.Integer(value)) |
|---|
| 612 | asn1Data = berEncoder.encode(asn1Sequence) |
|---|
| 613 | if extra: |
|---|
| 614 | iv = randbytes.secureRandom(8) |
|---|
| 615 | hexiv = ''.join(['%02X' % ord(x) for x in iv]) |
|---|
| 616 | lines.append('Proc-Type: 4,ENCRYPTED') |
|---|
| 617 | lines.append('DEK-Info: DES-EDE3-CBC,%s\n' % hexiv) |
|---|
| 618 | ba = md5(extra + iv).digest() |
|---|
| 619 | bb = md5(ba + extra + iv).digest() |
|---|
| 620 | encKey = (ba + bb)[:24] |
|---|
| 621 | padLen = 8 - (len(asn1Data) % 8) |
|---|
| 622 | asn1Data += (chr(padLen) * padLen) |
|---|
| 623 | asn1Data = DES3.new(encKey, DES3.MODE_CBC, |
|---|
| 624 | iv).encrypt(asn1Data) |
|---|
| 625 | b64Data = base64.encodestring(asn1Data).replace('\n', '') |
|---|
| 626 | lines += [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)] |
|---|
| 627 | lines.append('-----END %s PRIVATE KEY-----' % self.type()) |
|---|
| 628 | return '\n'.join(lines) |
|---|
| 629 | |
|---|
| 630 | def _toString_LSH(self): |
|---|
| 631 | """ |
|---|
| 632 | Return a public or private LSH key. See _fromString_PUBLIC_LSH and |
|---|
| 633 | _fromString_PRIVATE_LSH for the key formats. |
|---|
| 634 | |
|---|
| 635 | @rtype: C{str} |
|---|
| 636 | """ |
|---|
| 637 | data = self.data() |
|---|
| 638 | if self.isPublic(): |
|---|
| 639 | if self.type() == 'RSA': |
|---|
| 640 | keyData = sexpy.pack([['public-key', ['rsa-pkcs1-sha1', |
|---|
| 641 | ['n', common.MP(data['n'])[4:]], |
|---|
| 642 | ['e', common.MP(data['e'])[4:]]]]]) |
|---|
| 643 | elif self.type() == 'DSA': |
|---|
| 644 | keyData = sexpy.pack([['public-key', ['dsa', |
|---|
| 645 | ['p', common.MP(data['p'])[4:]], |
|---|
| 646 | ['q', common.MP(data['q'])[4:]], |
|---|
| 647 | ['g', common.MP(data['g'])[4:]], |
|---|
| 648 | ['y', common.MP(data['y'])[4:]]]]]) |
|---|
| 649 | return '{' + base64.encodestring(keyData).replace('\n', '') + '}' |
|---|
| 650 | else: |
|---|
| 651 | if self.type() == 'RSA': |
|---|
| 652 | p, q = data['p'], data['q'] |
|---|
| 653 | return sexpy.pack([['private-key', ['rsa-pkcs1', |
|---|
| 654 | ['n', common.MP(data['n'])[4:]], |
|---|
| 655 | ['e', common.MP(data['e'])[4:]], |
|---|
| 656 | ['d', common.MP(data['d'])[4:]], |
|---|
| 657 | ['p', common.MP(q)[4:]], |
|---|
| 658 | ['q', common.MP(p)[4:]], |
|---|
| 659 | ['a', common.MP(data['d'] % (q - 1))[4:]], |
|---|
| 660 | ['b', common.MP(data['d'] % (p - 1))[4:]], |
|---|
| 661 | ['c', common.MP(data['u'])[4:]]]]]) |
|---|
| 662 | elif self.type() == 'DSA': |
|---|
| 663 | return sexpy.pack([['private-key', ['dsa', |
|---|
| 664 | ['p', common.MP(data['p'])[4:]], |
|---|
| 665 | ['q', common.MP(data['q'])[4:]], |
|---|
| 666 | ['g', common.MP(data['g'])[4:]], |
|---|
| 667 | ['y', common.MP(data['y'])[4:]], |
|---|
| 668 | ['x', common.MP(data['x'])[4:]]]]]) |
|---|
| 669 | |
|---|
| 670 | def _toString_AGENTV3(self): |
|---|
| 671 | """ |
|---|
| 672 | Return a private Secure Shell Agent v3 key. See |
|---|
| 673 | _fromString_AGENTV3 for the key format. |
|---|
| 674 | |
|---|
| 675 | @rtype: C{str} |
|---|
| 676 | """ |
|---|
| 677 | data = self.data() |
|---|
| 678 | if not self.isPublic(): |
|---|
| 679 | if self.type() == 'RSA': |
|---|
| 680 | values = (data['e'], data['d'], data['n'], data['u'], |
|---|
| 681 | data['p'], data['q']) |
|---|
| 682 | elif self.type() == 'DSA': |
|---|
| 683 | values = (data['p'], data['q'], data['g'], data['y'], |
|---|
| 684 | data['x']) |
|---|
| 685 | return common.NS(self.sshType()) + ''.join(map(common.MP, values)) |
|---|
| 686 | |
|---|
| 687 | |
|---|
| 688 | def sign(self, data): |
|---|
| 689 | """ |
|---|
| 690 | Returns a signature with this Key. |
|---|
| 691 | |
|---|
| 692 | @type data: C{str} |
|---|
| 693 | @rtype: C{str} |
|---|
| 694 | """ |
|---|
| 695 | if self.type() == 'RSA': |
|---|
| 696 | digest = pkcs1Digest(data, self.keyObject.size()/8) |
|---|
| 697 | signature = self.keyObject.sign(digest, '')[0] |
|---|
| 698 | ret = common.NS(Util.number.long_to_bytes(signature)) |
|---|
| 699 | elif self.type() == 'DSA': |
|---|
| 700 | digest = sha1(data).digest() |
|---|
| 701 | randomBytes = randbytes.secureRandom(19) |
|---|
| 702 | sig = self.keyObject.sign(digest, randomBytes) |
|---|
| 703 | # SSH insists that the DSS signature blob be two 160-bit integers |
|---|
| 704 | # concatenated together. The sig[0], [1] numbers from obj.sign |
|---|
| 705 | # are just numbers, and could be any length from 0 to 160 bits. |
|---|
| 706 | # Make sure they are padded out to 160 bits (20 bytes each) |
|---|
| 707 | ret = common.NS(Util.number.long_to_bytes(sig[0], 20) + |
|---|
| 708 | Util.number.long_to_bytes(sig[1], 20)) |
|---|
| 709 | return common.NS(self.sshType()) + ret |
|---|
| 710 | |
|---|
| 711 | def verify(self, signature, data): |
|---|
| 712 | """ |
|---|
| 713 | Returns true if the signature for data is valid for this Key. |
|---|
| 714 | |
|---|
| 715 | @type signature: C{str} |
|---|
| 716 | @type data: C{str} |
|---|
| 717 | @rtype: C{bool} |
|---|
| 718 | """ |
|---|
| 719 | signatureType, signature = common.getNS(signature) |
|---|
| 720 | if signatureType != self.sshType(): |
|---|
| 721 | return False |
|---|
| 722 | if self.type() == 'RSA': |
|---|
| 723 | numbers = common.getMP(signature) |
|---|
| 724 | digest = pkcs1Digest(data, self.keyObject.size() / 8) |
|---|
| 725 | elif self.type() == 'DSA': |
|---|
| 726 | signature = common.getNS(signature)[0] |
|---|
| 727 | numbers = [Util.number.bytes_to_long(n) for n in signature[:20], |
|---|
| 728 | signature[20:]] |
|---|
| 729 | digest = sha1(data).digest() |
|---|
| 730 | return self.keyObject.verify(digest, numbers) |
|---|
| 731 | |
|---|
| 732 | |
|---|
| 733 | def objectType(obj): |
|---|
| 734 | """ |
|---|
| 735 | Return the SSH key type corresponding to a C{Crypto.PublicKey.pubkey.pubkey} |
|---|
| 736 | object. |
|---|
| 737 | |
|---|
| 738 | @type obj: C{Crypto.PublicKey.pubkey.pubkey} |
|---|
| 739 | @rtype: C{str} |
|---|
| 740 | """ |
|---|
| 741 | keyDataMapping = { |
|---|
| 742 | ('n', 'e', 'd', 'p', 'q'): 'ssh-rsa', |
|---|
| 743 | ('n', 'e', 'd', 'p', 'q', 'u'): 'ssh-rsa', |
|---|
| 744 | ('y', 'g', 'p', 'q', 'x'): 'ssh-dss' |
|---|
| 745 | } |
|---|
| 746 | try: |
|---|
| 747 | return keyDataMapping[tuple(obj.keydata)] |
|---|
| 748 | except (KeyError, AttributeError): |
|---|
| 749 | raise BadKeyError("invalid key object", obj) |
|---|
| 750 | |
|---|
| 751 | def pkcs1Pad(data, messageLength): |
|---|
| 752 | """ |
|---|
| 753 | Pad out data to messageLength according to the PKCS#1 standard. |
|---|
| 754 | @type data: C{str} |
|---|
| 755 | @type messageLength: C{int} |
|---|
| 756 | """ |
|---|
| 757 | lenPad = messageLength - 2 - len(data) |
|---|
| 758 | return '\x01' + ('\xff' * lenPad) + '\x00' + data |
|---|
| 759 | |
|---|
| 760 | def pkcs1Digest(data, messageLength): |
|---|
| 761 | """ |
|---|
| 762 | Create a message digest using the SHA1 hash algorithm according to the |
|---|
| 763 | PKCS#1 standard. |
|---|
| 764 | @type data: C{str} |
|---|
| 765 | @type messageLength: C{str} |
|---|
| 766 | """ |
|---|
| 767 | digest = sha1(data).digest() |
|---|
| 768 | return pkcs1Pad(ID_SHA1+digest, messageLength) |
|---|
| 769 | |
|---|
| 770 | def lenSig(obj): |
|---|
| 771 | """ |
|---|
| 772 | Return the length of the signature in bytes for a key object. |
|---|
| 773 | |
|---|
| 774 | @type obj: C{Crypto.PublicKey.pubkey.pubkey} |
|---|
| 775 | @rtype: C{long} |
|---|
| 776 | """ |
|---|
| 777 | return obj.size()/8 |
|---|
| 778 | |
|---|
| 779 | |
|---|
| 780 | ID_SHA1 = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' |
|---|