From 2bbdaf6e115a62fae6e86c0f5cf79f2034af6527 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Thu, 16 Mar 2023 15:23:59 +0000 Subject: [PATCH 01/41] Initial brute migration. --- chevah/keycert/__init__.py | 12 - chevah/keycert/common.py | 183 -- chevah/keycert/exceptions.py | 41 - chevah/keycert/sexpy.py | 48 - chevah/keycert/ssh.py | 2823 ------------------ chevah/keycert/ssl.py | 417 --- chevah/keycert/tests/__init__.py | 18 - chevah/keycert/tests/helpers.py | 68 - chevah/keycert/tests/keydata.py | 632 ---- chevah/keycert/tests/ssh_common_test_inc.sh | 18 - chevah/keycert/tests/ssh_gen_keys_tests.sh | 165 -- chevah/keycert/tests/ssh_load_keys_tests.sh | 308 -- chevah/keycert/tests/test_exceptions.py | 34 - chevah/keycert/tests/test_ssh.py | 2898 ------------------- chevah/keycert/tests/test_ssl.py | 567 ---- pavement.py | 12 +- setup.py | 7 +- 17 files changed, 9 insertions(+), 8242 deletions(-) delete mode 100644 chevah/keycert/__init__.py delete mode 100644 chevah/keycert/common.py delete mode 100644 chevah/keycert/exceptions.py delete mode 100644 chevah/keycert/sexpy.py delete mode 100644 chevah/keycert/ssh.py delete mode 100644 chevah/keycert/ssl.py delete mode 100644 chevah/keycert/tests/__init__.py delete mode 100644 chevah/keycert/tests/helpers.py delete mode 100644 chevah/keycert/tests/keydata.py delete mode 100644 chevah/keycert/tests/ssh_common_test_inc.sh delete mode 100755 chevah/keycert/tests/ssh_gen_keys_tests.sh delete mode 100755 chevah/keycert/tests/ssh_load_keys_tests.sh delete mode 100644 chevah/keycert/tests/test_exceptions.py delete mode 100644 chevah/keycert/tests/test_ssh.py delete mode 100644 chevah/keycert/tests/test_ssl.py diff --git a/chevah/keycert/__init__.py b/chevah/keycert/__init__.py deleted file mode 100644 index 21f8f11..0000000 --- a/chevah/keycert/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -SSL and SSH key management. -""" -import sys - - -def _path(path, encoding='utf-8'): - if sys.platform.startswith('win'): - # On Windows and OSX we always use unicode. - return path # pragma: no cover - - return path.encode(encoding) diff --git a/chevah/keycert/common.py b/chevah/keycert/common.py deleted file mode 100644 index a7169d9..0000000 --- a/chevah/keycert/common.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -Common functions for the all classes from this package. - -Forked from twisted.conch.ssh.common -""" - -from __future__ import absolute_import, division - -import struct -import itertools - -from cryptography.utils import int_from_bytes, int_to_bytes - -# So we can import from this module -long = long -izip = itertools.izip - - -def iterbytes(originalBytes): - return originalBytes - - -def native_string(s): - return s - - - -def NS(t): - """ - net string - """ - if isinstance(t, unicode): - t = t.encode("utf-8") - return struct.pack('!L', len(t)) + t - - - -def getNS(s, count=1): - """ - get net string - """ - ns = [] - c = 0 - for i in range(count): - l, = struct.unpack('!L', s[c:c + 4]) - ns.append(s[c + 4:4 + l + c]) - c += 4 + l - return tuple(ns) + (s[c:],) - - - -def MP(number): - if number == 0: - return b'\000' * 4 - assert number > 0 - bn = int_to_bytes(number) - if ord(bn[0:1]) & 128: - bn = b'\000' + bn - return struct.pack('>L', len(bn)) + bn - - - -def getMP(data, count=1): - """ - Get multiple precision integer out of the string. A multiple precision - integer is stored as a 4-byte length followed by length bytes of the - integer. If count is specified, get count integers out of the string. - The return value is a tuple of count integers followed by the rest of - the data. - """ - mp = [] - c = 0 - for i in range(count): - length, = struct.unpack('>L', data[c:c + 4]) - mp.append(int_from_bytes(data[c + 4:c + 4 + length], 'big')) - c += 4 + length - return tuple(mp) + (data[c:],) - - - -def ffs(c, s): - """ - first from second - goes through the first list, looking for items in the second, returns the first one - """ - for i in c: - if i in s: - return i - - - -def force_unicode(value): - """ - Decode the `value` to unicode. - - It will try to extract the message from an exception. - - In case there are encoding errors when converting the invalid characters - are replaced. - """ - import errno - - def str_or_repr(value): - - if isinstance(value, unicode): - return value - - try: - return unicode(value, encoding='utf-8') - except Exception: - """ - Not UTF-8 encoded value. - """ - - try: - return unicode(value, encoding='windows-1252') - except Exception: - """ - Not Windows encoded value. - """ - - try: - return unicode(str(value), encoding='utf-8', errors='replace') - except (UnicodeDecodeError, UnicodeEncodeError): - """ - Not UTF-8 encoded value. - """ - - try: - return unicode( - str(value), encoding='windows-1252', errors='replace') - except (UnicodeDecodeError, UnicodeEncodeError): - pass - - # No luck with str, try repr() - return unicode(repr(value), encoding='windows-1252', errors='replace') - - if value is None: - return u'None' - - if isinstance(value, unicode): - return value - - if isinstance(value, EnvironmentError) and value.errno: - # IOError, OSError, WindowsError. - code = value.errno - message = value.strerror - # Convert to Unix message to help with testing. - if code == errno.ENOENT: - # On Windows it is: - # The system cannot find the file specified. - message = b'No such file or directory' - if code == errno.EEXIST: - # On Windows it is: - # Cannot create a file when that file already exists - message = b'File exists' - if code == errno.EBADF: - # On AIX: Bad file number - message = b'Bad file descriptor' - - if code and message: - if value.filename: - return "[Errno %s] %s: '%s'" % ( - code, - str_or_repr(message), - str_or_repr(value.filename), - ) - return '[Errno %s] %s.' % (code, str_or_repr(message)) - - if isinstance(value, Exception): - try: - details = str(value) - except (UnicodeDecodeError, UnicodeEncodeError): - details = getattr(value, 'message', '') - result = str_or_repr(details) - if result: - return result - return str_or_repr(repr(value)) - - return str_or_repr(value) diff --git a/chevah/keycert/exceptions.py b/chevah/keycert/exceptions.py deleted file mode 100644 index 9b78279..0000000 --- a/chevah/keycert/exceptions.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) 2014 Adi Roiban. -# See LICENSE for details. -""" -Public exceptions raised by this package. -""" - - -class KeyCertException(Exception): - """ - Generic exception raised by the package. - - Code calling the public API should handle only this exception. - The other exceptions are just for fine tunning. - """ - def __init__(self, message): - self.message = message - - def __str__(self): - return self.message.encode('utf-8') - - -class BadKeyError(KeyCertException): - """ - Raised when a key isn't what we expected from it. - - XXX: we really need to check for bad keys - """ - - -class BadSignatureAlgorithmError(KeyCertException): - """ - Raised when a public key signature algorithm name isn't defined for this - public key format. - """ - - -class EncryptedKeyError(BadKeyError): - """ - Raised when an encrypted key is presented to fromString/fromFile without - a password. - """ diff --git a/chevah/keycert/sexpy.py b/chevah/keycert/sexpy.py deleted file mode 100644 index e2eb99b..0000000 --- a/chevah/keycert/sexpy.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. -""" -S-expression read / write. - -Forked from twisted.conch.ssh.sexpy -""" - - -def parse(s): - s = s.strip() - expr = [] - while s: - if s[0] == '(': - newSexp = [] - if expr: - expr[-1].append(newSexp) - expr.append(newSexp) - s = s[1:] - continue - if s[0] == ')': - aList = expr.pop() - s = s[1:] - if not expr: - assert not s - return aList - continue - i = 0 - while s[i].isdigit(): - i += 1 - assert i - length = int(s[:i]) - data = s[i + 1:i + 1 + length] - expr[-1].append(data) - s = s[i + 1 + length:] - assert 0, "this should not happen" # pragma: no cover - - -def pack(sexp): - s = "" - for o in sexp: - if type(o) in (type(()), type([])): - s += '(' - s += pack(o) - s += ')' - else: - s += '%i:%s' % (len(o), o) - return s diff --git a/chevah/keycert/ssh.py b/chevah/keycert/ssh.py deleted file mode 100644 index 4634363..0000000 --- a/chevah/keycert/ssh.py +++ /dev/null @@ -1,2823 +0,0 @@ -# Copyright (c) 2014 Adi Roiban. -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -Handling of RSA, DSA, ECDSA, and Ed25519 keys. -""" - -from __future__ import absolute_import, division, unicode_literals - -import binascii -import itertools - -from hashlib import md5, sha1, sha256 -import base64 -import hmac -import unicodedata -import struct -import textwrap - -import bcrypt -from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ( - dsa, ec, ed25519, padding, rsa) -from cryptography.hazmat.primitives.serialization import ( - load_pem_private_key, load_ssh_public_key) -from cryptography import utils - -try: - - from cryptography.hazmat.primitives.asymmetric.utils import ( - encode_dss_signature, decode_dss_signature) -except ImportError: - from cryptography.hazmat.primitives.asymmetric.utils import ( - encode_rfc6979_signature as encode_dss_signature, - decode_rfc6979_signature as decode_dss_signature) -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - -from pyasn1.error import PyAsn1Error -from pyasn1.type import univ -from pyasn1.codec.ber import decoder as berDecoder -from pyasn1.codec.ber import encoder as berEncoder - -import os -import os.path -from os import urandom -from base64 import encodestring as encodebytes -from base64 import decodestring as decodebytes -from cryptography.utils import int_from_bytes, int_to_bytes -from OpenSSL import crypto - -from chevah.keycert import common, sexpy, _path -from chevah.keycert.common import ( - long, - force_unicode, - iterbytes, - izip, - ) -from chevah.keycert.exceptions import ( - BadKeyError, - BadSignatureAlgorithmError, - EncryptedKeyError, - KeyCertException, - ) -from constantly import NamedConstant, Names - -DEFAULT_PUBLIC_KEY_EXTENSION = u'.pub' -DEFAULT_KEY_SIZE = 2048 -DEFAULT_KEY_TYPE = 'rsa' -SSHCOM_MAGIC_NUMBER = int('3f6ff9eb', base=16) -PUTTY_HMAC_KEY = 'putty-private-key-file-mac-key' -ID_SHA1 = b'\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' - -# Curve lookup table -_curveTable = { - b'ecdsa-sha2-nistp256': ec.SECP256R1(), - b'ecdsa-sha2-nistp384': ec.SECP384R1(), - b'ecdsa-sha2-nistp521': ec.SECP521R1(), -} - -_secToNist = { - b'secp256r1' : b'nistp256', - b'secp384r1' : b'nistp384', - b'secp521r1' : b'nistp521', -} - - -_ecSizeTable = { - 256: ec.SECP256R1(), - 384: ec.SECP384R1(), - 521: ec.SECP521R1(), -} - -class BadFingerPrintFormat(Exception): - """ - Raises when unsupported fingerprint formats are presented to fingerprint. - """ - - - -class FingerprintFormats(Names): - """ - Constants representing the supported formats of key fingerprints. - - @cvar MD5_HEX: Named constant representing fingerprint format generated - using md5[RFC1321] algorithm in hexadecimal encoding. - @type MD5_HEX: L{twisted.python.constants.NamedConstant} - - @cvar SHA256_BASE64: Named constant representing fingerprint format - generated using sha256[RFC4634] algorithm in base64 encoding - @type SHA256_BASE64: L{NamedConstant} - @cvar SHA1_BASE64: Named constant representing fingerprint format - generated using sha1[RFC3174] algorithm in base64 encoding - @type SHA1_BASE64: L{NamedConstant} - """ - - MD5_HEX = NamedConstant() - SHA256_BASE64 = NamedConstant() - SHA1_BASE64 = NamedConstant() - - -class PassphraseNormalizationError(Exception): - """ - Raised when a passphrase contains Unicode characters that cannot be - normalized using the available Unicode character database. - """ - - -def _normalizePassphrase(passphrase): - """ - Normalize a passphrase, which may be Unicode. - - If the passphrase is Unicode, this follows the requirements of U{NIST - 800-63B, section - 5.1.1.2} - for Unicode characters in memorized secrets: it applies the - Normalization Process for Stabilized Strings using NFKC normalization. - The passphrase is then encoded using UTF-8. - - @type passphrase: L{bytes} or L{unicode} or L{None} - @param passphrase: The passphrase to normalize. - - @return: The normalized passphrase, if any. - @rtype: L{bytes} or L{None} - @raises PassphraseNormalizationError: if the passphrase is Unicode and - cannot be normalized using the available Unicode character database. - """ - if isinstance(passphrase, unicode): - # The Normalization Process for Stabilized Strings requires aborting - # with an error if the string contains any unassigned code point. - if any(unicodedata.category(c) == "Cn" for c in passphrase): - # Perhaps not very helpful, but we don't want to leak any other - # information about the passphrase. - raise PassphraseNormalizationError() - return unicodedata.normalize("NFKC", passphrase).encode("UTF-8") - else: - return passphrase - - -class Key(object): - """ - An object representing a key. A key can be either a public or - private key. A public key can verify a signature; a private key can - create or verify a signature. To generate a string that can be stored - on disk, use the toString method. If you have a private key, but want - the string representation of the public key, use Key.public().toString(). - - SSH Transport local-peer Key: (PrivateKey) - * fromCryptograpyObject / __init__ - for local peer private key - * blob() - return public blob - for handshake / sign payload - * sign - for local peer private key - - SSH Transport remote-peer key /PublicKey): - * fromPublicBlob() / __init__ - for remote peer public key - * verify - for remote peer - - SSH Key: - * fromString / __init__ - * toString - * getFormat - human readable representation of internal guessed type - * getCryptographyObject - - generate_ssh_key(type, size) -> external helper - """ - - @classmethod - def fromFile(cls, filename, type=None, passphrase=None, encoding='utf-8'): - """ - Load a key from a file. - - @param filename: The path to load key data from. - - @type type: L{str} or L{None} - @param type: A string describing the format the key data is in, or - L{None} to attempt detection of the type. - - @type passphrase: L{bytes} or L{None} - @param passphrase: The passphrase the key is encrypted with, or L{None} - if there is no encryption. - - @rtype: L{Key} - @return: The loaded key. - """ - with open(_path(filename, encoding), 'rb') as file: - return cls.fromString(file.read(), type, passphrase) - - @classmethod - def fromString(cls, data, type=None, passphrase=None): - """ - Return a Key object corresponding to the string data. - type is optionally the type of string, matching a _fromString_* - method. Otherwise, the _guessStringType() classmethod will be used - to guess a type. If the key is encrypted, passphrase is used as - the decryption key. - - @type data: L{bytes} - @param data: The key data. - - @type type: L{str} or L{None} - @param type: A string describing the format the key data is in, or - L{None} to attempt detection of the type. - - @type passphrase: L{bytes} or L{None} - @param passphrase: The passphrase the key is encrypted with, or L{None} - if there is no encryption. - - @rtype: L{Key} - @return: The loaded key. - """ - if isinstance(data, unicode): - data = data.encode("utf-8") - passphrase = _normalizePassphrase(passphrase) - if type is None: - type = cls._guessStringType(data) - if type is None: - raise BadKeyError( - 'Cannot guess the type for "%s"' % force_unicode(data[:80])) - - try: - method = getattr(cls, '_fromString_%s' % type.upper(), None) - if method is None: - raise BadKeyError( - 'no _fromString method for "%s"' % force_unicode(type[:30])) - if method.__code__.co_argcount == 2: # no passphrase - if passphrase: - raise BadKeyError('key not encrypted') - return method(data) - else: - return method(data, passphrase) - except (IndexError): - # Most probably some parts are missing from the key, so - # we consider it too short. - raise BadKeyError('Key is too short.') - except (struct.error, binascii.Error, TypeError): - raise BadKeyError('Fail to parse key content.') - - @classmethod - def _fromString_BLOB(cls, blob): - """ - Return a public key object corresponding to this public key blob. - - The format of a RSA public key blob is:: - string 'ssh-rsa' - integer e - integer n - - The format of a DSA public key blob is:: - string 'ssh-dss' - integer p - integer q - integer g - integer y - - The format of ECDSA-SHA2-* public key blob is:: - string 'ecdsa-sha2-[identifier]' - integer x - integer y - - identifier is the standard NIST curve name. - - The format of an Ed25519 public key blob is:: - string 'ssh-ed25519' - string a - - @type blob: L{bytes} - @param blob: The key data. - - @return: A new key. - @rtype: L{twisted.conch.ssh.keys.Key} - @raises BadKeyError: if the key type (the first string) is unknown. - """ - keyType, rest = common.getNS(blob) - if keyType == b'ssh-rsa': - e, n, rest = common.getMP(rest, 2) - return cls._fromRSAComponents(n, e) - elif keyType == b'ssh-dss': - p, q, g, y, rest = common.getMP(rest, 4) - return cls._fromDSAComponents( y, p, q, g) - elif keyType in _curveTable: - return cls._fromECEncodedPoint( - encodedPoint=common.getNS(rest, 2)[1], - curve=keyType, - ) - elif keyType == b'ssh-ed25519': - a, rest = common.getNS(rest) - return cls._fromEd25519Components(a) - else: - raise BadKeyError('unknown blob type: "{}"'.format( - force_unicode(keyType[:30]))) - - @classmethod - def _fromString_PRIVATE_BLOB(cls, blob): - """ - Return a private key object corresponding to this private key blob. - The blob formats are as follows: - - RSA keys:: - string 'ssh-rsa' - integer n - integer e - integer d - integer u - integer p - integer q - - DSA keys:: - string 'ssh-dss' - integer p - integer q - integer g - integer y - integer x - - EC keys:: - string 'ecdsa-sha2-[identifier]' - string identifier - string q - integer privateValue - - identifier is the standard NIST curve name. - - Ed25519 keys:: - string 'ssh-ed25519' - string a - string k || a - - - @type blob: L{bytes} - @param blob: The key data. - - @return: A new key. - @rtype: L{twisted.conch.ssh.keys.Key} - @raises BadKeyError: if - * the key type (the first string) is unknown - * the curve name of an ECDSA key does not match the key type - """ - keyType, rest = common.getNS(blob) - - if keyType == b'ssh-rsa': - n, e, d, u, p, q, rest = common.getMP(rest, 6) - return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q) - elif keyType == b'ssh-dss': - p, q, g, y, x, rest = common.getMP(rest, 5) - return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) - elif keyType in _curveTable: - curve = _curveTable[keyType] - curveName, q, rest = common.getNS(rest, 2) - if curveName != _secToNist[curve.name.encode('ascii')]: - raise BadKeyError( - 'ECDSA curve name "%s" does not match key type "%s"' % ( - force_unicode(curveName), force_unicode(keyType))) - privateValue, rest = common.getMP(rest) - return cls._fromECEncodedPoint( - encodedPoint=q, curve=keyType, privateValue=privateValue) - elif keyType == b'ssh-ed25519': - # OpenSSH's format repeats the public key bytes for some reason. - # We're only interested in the private key here anyway. - a, combined, rest = common.getNS(rest, 2) - k = combined[:32] - return cls._fromEd25519Components(a, k=k) - else: - raise BadKeyError( - 'Unknown blob type: "%s"' % force_unicode(keyType[:30])) - - - @classmethod - def _fromString_PUBLIC_OPENSSH(cls, data): - """ - Return a public key object corresponding to this OpenSSH public key - string. The format of an OpenSSH public key string is:: - - - @type data: L{bytes} - @param data: The key data. - - @return: A new key. - @rtype: L{twisted.conch.ssh.keys.Key} - @raises BadKeyError: if the blob type is unknown. - """ - # ECDSA keys don't need base64 decoding which is required - # for RSA or DSA key. - if data.startswith(b'ecdsa-sha2'): - return cls(load_ssh_public_key(data, default_backend())) - blob = decodebytes(data.split()[1]) - return cls._fromString_BLOB(blob) - - - @classmethod - def _fromString_PRIVATE_OPENSSH_V1(cls, data, passphrase): - """ - Return a private key object corresponding to this OpenSSH private key - string, in the "openssh-key-v1" format introduced in OpenSSH 6.5. - - The format of an openssh-key-v1 private key string is:: - -----BEGIN OPENSSH PRIVATE KEY----- - - -----END OPENSSH PRIVATE KEY----- - - The SSH protocol string is as described in - U{PROTOCOL.key}. - - @type data: L{bytes} - @param data: The key data. - - @type passphrase: L{bytes} or L{None} - @param passphrase: The passphrase the key is encrypted with, or L{None} - if it is not encrypted. - - @return: A new key. - @rtype: L{twisted.conch.ssh.keys.Key} - @raises BadKeyError: if - * a passphrase is provided for an unencrypted key - * the SSH protocol encoding is incorrect - @raises EncryptedKeyError: if - * a passphrase is not provided for an encrypted key - """ - lines = data.strip().splitlines() - keyList = decodebytes(b''.join(lines[1:-1])) - if not keyList.startswith(b'openssh-key-v1\0'): - raise BadKeyError('unknown OpenSSH private key format') - keyList = keyList[len(b'openssh-key-v1\0'):] - cipher, kdf, kdfOptions, rest = common.getNS(keyList, 3) - n = struct.unpack('!L', rest[:4])[0] - if n != 1: - raise BadKeyError('only OpenSSH private key files containing ' - 'a single key are supported') - # Ignore public key - _, encPrivKeyList, _ = common.getNS(rest[4:], 2) - if cipher != b'none': - if not passphrase: - raise EncryptedKeyError('Passphrase must be provided ' - 'for an encrypted key') - # Determine cipher - if cipher in (b'aes128-ctr', b'aes192-ctr', b'aes256-ctr'): - algorithmClass = algorithms.AES - blockSize = 16 - keySize = int(cipher[3:6]) // 8 - ivSize = blockSize - else: - raise BadKeyError('unknown encryption type "%s"' % ( - force_unicode(cipher),)) - if kdf == b'bcrypt': - salt, rest = common.getNS(kdfOptions) - rounds = struct.unpack('!L', rest[:4])[0] - decKey = bcrypt.kdf( - passphrase, salt, keySize + ivSize, rounds, - # We can only use the number of rounds that OpenSSH used. - ignore_few_rounds=True) - else: - raise BadKeyError( - 'unknown KDF type "%s"' % (force_unicode(kdf),)) - if (len(encPrivKeyList) % blockSize) != 0: - raise BadKeyError('bad padding') - decryptor = Cipher( - algorithmClass(decKey[:keySize]), - modes.CTR(decKey[keySize:keySize + ivSize]), - backend=default_backend() - ).decryptor() - privKeyList = ( - decryptor.update(encPrivKeyList) + decryptor.finalize()) - else: - if kdf != b'none': - raise BadKeyError('private key specifies KDF "%s" but no ' - 'cipher' % (force_unicode(kdf),)) - privKeyList = encPrivKeyList - check1 = struct.unpack('!L', privKeyList[:4])[0] - check2 = struct.unpack('!L', privKeyList[4:8])[0] - if check1 != check2: - raise BadKeyError( - 'Private key sanity check failed. Maybe invalid passphrase.') - return cls._fromString_PRIVATE_BLOB(privKeyList[8:]) - - - @classmethod - def _fromString_PRIVATE_OPENSSH(cls, data, passphrase): - """ - Return a private key object corresponding to this OpenSSH private key - string, in the old PEM-based format. - - The format of a PEM-based OpenSSH private key string is:: - -----BEGIN PRIVATE KEY----- - [Proc-Type: 4,ENCRYPTED - DEK-Info: DES-EDE3-CBC,] - - ------END PRIVATE KEY------ - - The ASN.1 structure of a RSA key is:: - (0, n, e, d, p, q) - - The ASN.1 structure of a DSA key is:: - (0, p, q, g, y, x) - - The ASN.1 structure of a ECDSA key is:: - (ECParameters, OID, NULL) - - @type data: L{bytes} - @param data: The key data. - - @type passphrase: L{bytes} or L{None} - @param passphrase: The passphrase the key is encrypted with, or L{None} - if it is not encrypted. - - @return: A new key. - @rtype: L{twisted.conch.ssh.keys.Key} - @raises BadKeyError: if - * a passphrase is provided for an unencrypted key - * the ASN.1 encoding is incorrect - @raises EncryptedKeyError: if - * a passphrase is not provided for an encrypted key - """ - lines = data.strip().splitlines() - kind = lines[0][11:-17] - if lines[1].startswith(b'Proc-Type: 4,ENCRYPTED'): - if not passphrase: - raise EncryptedKeyError('Passphrase must be provided ' - 'for an encrypted key') - - # Determine cipher and initialization vector - try: - _, cipherIVInfo = lines[2].split(b' ', 1) - cipher, ivdata = cipherIVInfo.rstrip().split(b',', 1) - except ValueError: - raise BadKeyError( - 'invalid DEK-info "%s"' % (force_unicode(lines[2]),)) - - if cipher in (b'AES-128-CBC', b'AES-256-CBC'): - algorithmClass = algorithms.AES - keySize = int(cipher.split(b'-')[1]) // 8 - if len(ivdata) != 32: - raise BadKeyError('AES encrypted key with a bad IV') - elif cipher == b'DES-EDE3-CBC': - algorithmClass = algorithms.TripleDES - keySize = 24 - if len(ivdata) != 16: - raise BadKeyError('DES encrypted key with a bad IV') - else: - raise BadKeyError( - 'unknown encryption type "%s"' % (force_unicode(cipher),)) - - # Extract keyData for decoding - iv = bytes(bytearray([int(ivdata[i:i + 2], 16) - for i in range(0, len(ivdata), 2)])) - ba = md5(passphrase + iv[:8]).digest() - bb = md5(ba + passphrase + iv[:8]).digest() - decKey = (ba + bb)[:keySize] - b64Data = decodebytes(b''.join(lines[3:-1])) - - decryptor = Cipher( - algorithmClass(decKey), - modes.CBC(iv), - backend=default_backend() - ).decryptor() - keyData = decryptor.update(b64Data) + decryptor.finalize() - - removeLen = ord(keyData[-1:]) - keyData = keyData[:-removeLen] - else: - b64Data = b''.join(lines[1:-1]) - keyData = decodebytes(b64Data) - - try: - decodedKey = berDecoder.decode(keyData)[0] - except PyAsn1Error as e: - raise BadKeyError( - 'Failed to decode key (Bad Passphrase?): %s' % ( - force_unicode(e),)) - - if kind == b'EC': - return cls( - load_pem_private_key(data, passphrase, default_backend())) - - if kind == b'RSA': - if len(decodedKey) == 2: # Alternate RSA key - decodedKey = decodedKey[0] - if len(decodedKey) < 6: - raise BadKeyError('RSA key failed to decode properly') - - n, e, d, p, q, dmp1, dmq1, iqmp = [ - long(value) for value in decodedKey[1:9] - ] - return cls( - rsa.RSAPrivateNumbers( - p=p, - q=q, - d=d, - dmp1=dmp1, - dmq1=dmq1, - iqmp=iqmp, - public_numbers=rsa.RSAPublicNumbers(e=e, n=n), - ).private_key(default_backend()) - ) - elif kind == b'DSA': - p, q, g, y, x = [long(value) for value in decodedKey[1: 6]] - if len(decodedKey) < 6: - raise BadKeyError('DSA key failed to decode properly') - return cls._fromDSAComponents(y, p, q, g, x) - else: - raise BadKeyError('unknown key type "%s"' % (force_unicode(kind),)) - - - @classmethod - def _fromString_PUBLIC_LSH(cls, data): - """ - Return a public key corresponding to this LSH public key string. - The LSH public key string format is:: - , ()+))> - - The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e. - The names for a DSA (key type 'dsa') key are: y, g, p, q. - - @type data: L{bytes} - @param data: The key data. - - @return: A new key. - @rtype: L{twisted.conch.ssh.keys.Key} - @raises BadKeyError: if the key type is unknown - """ - sexp = sexpy.parse(decodebytes(data[1:-1])) - assert sexp[0] == b'public-key' - kd = {} - for name, data in sexp[1][1:]: - kd[name] = common.getMP(common.NS(data))[0] - if sexp[1][0] == b'dsa': - return cls._fromDSAComponents( - y=kd[b'y'], g=kd[b'g'], p=kd[b'p'], q=kd[b'q']) - - elif sexp[1][0] == b'rsa-pkcs1-sha1': - return cls._fromRSAComponents(n=kd[b'n'], e=kd[b'e']) - else: - raise BadKeyError('unknown lsh key type "%s"' % ( - force_unicode(sexp[1][0][:30]),)) - - @classmethod - def _fromString_PRIVATE_LSH(cls, data): - """ - Return a private key corresponding to this LSH private key string. - The LSH private key string format is:: - , (, )+))> - - The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e, d, p, q. - The names for a DSA (key type 'dsa') key are: y, g, p, q, x. - - @type data: L{bytes} - @param data: The key data. - - @return: A new key. - @rtype: L{twisted.conch.ssh.keys.Key} - @raises BadKeyError: if the key type is unknown - """ - sexp = sexpy.parse(data) - assert sexp[0] == b'private-key' - kd = {} - for name, data in sexp[1][1:]: - kd[name] = common.getMP(common.NS(data))[0] - if sexp[1][0] == b'dsa': - assert len(kd) == 5, len(kd) - return cls._fromDSAComponents( - y=kd[b'y'], g=kd[b'g'], p=kd[b'p'], q=kd[b'q'], x=kd[b'x']) - elif sexp[1][0] == b'rsa-pkcs1': - assert len(kd) == 8, len(kd) - if kd[b'p'] > kd[b'q']: # Make p smaller than q - kd[b'p'], kd[b'q'] = kd[b'q'], kd[b'p'] - return cls._fromRSAComponents( - n=kd[b'n'], e=kd[b'e'], d=kd[b'd'], p=kd[b'p'], q=kd[b'q']) - - else: - raise BadKeyError( - 'unknown lsh key type "%s"' % (force_unicode(sexp[1][0][:30]),)) - - @classmethod - def _fromString_AGENTV3(cls, data): - """ - Return a private key object corresponsing to the Secure Shell Key - Agent v3 format. - - The SSH Key Agent v3 format for a RSA key is:: - string 'ssh-rsa' - integer e - integer d - integer n - integer u - integer p - integer q - - The SSH Key Agent v3 format for a DSA key is:: - string 'ssh-dss' - integer p - integer q - integer g - integer y - integer x - - @type data: L{bytes} - @param data: The key data. - - @return: A new key. - @rtype: L{twisted.conch.ssh.keys.Key} - @raises BadKeyError: if the key type (the first string) is unknown - """ - keyType, data = common.getNS(data) - if keyType == b'ssh-dss': - p, data = common.getMP(data) - q, data = common.getMP(data) - g, data = common.getMP(data) - y, data = common.getMP(data) - x, data = common.getMP(data) - return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) - elif keyType == b'ssh-rsa': - e, data = common.getMP(data) - d, data = common.getMP(data) - n, data = common.getMP(data) - u, data = common.getMP(data) - p, data = common.getMP(data) - q, data = common.getMP(data) - return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) - else: # pragma: no cover - raise BadKeyError( - 'unknown key type "%s"' % (force_unicode(keyType[:30]),)) - - @classmethod - def _guessStringType(cls, data): - """ - Guess the type of key in data. The types map to _fromString_* - methods. - - @type data: L{bytes} - @param data: The key data. - """ - if data.startswith(b'ssh-') or data.startswith(b'ecdsa-sha2-'): - return 'public_openssh' - elif data.startswith(b'---- BEGIN SSH2 PUBLIC KEY ----'): - return 'public_sshcom' - elif data.startswith(b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----'): - return 'private_sshcom' - elif data.startswith(b'-----BEGIN RSA PUBLIC'): - return 'public_pkcs1_rsa' - elif ( - data.startswith(b'-----BEGIN RSA PRIVATE') or - data.startswith(b'-----BEGIN DSA PRIVATE') or - data.startswith(b'-----BEGIN EC PRIVATE') - ): - # This is also private PKCS#1 format. - return 'private_openssh' - elif data.startswith(b'-----BEGIN OPENSSH PRIVATE KEY-----'): - return 'private_openssh_v1' - - elif data.startswith(b'-----BEGIN CERTIFICATE-----'): - return 'public_x509_certificate' - - elif data.startswith(b'-----BEGIN PUBLIC KEY-----'): - # Public Key in X.509 format it's as follows - return 'public_x509' - - elif data.startswith(b'-----BEGIN PRIVATE KEY-----'): - return 'private_pkcs8' - elif data.startswith(b'-----BEGIN ENCRYPTED PRIVATE KEY-----'): - return 'private_encrypted_pkcs8' - elif data.startswith(b'PuTTY-User-Key-File-2'): - return 'private_putty' - elif data.startswith(b'{'): - return 'public_lsh' - elif data.startswith(b'('): - return 'private_lsh' - elif (data.startswith(b'\x00\x00\x00\x07ssh-') or - data.startswith(b'\x00\x00\x00\x13ecdsa-') or - data.startswith(b'\x00\x00\x00\x0bssh-ed25519')): - ignored, rest = common.getNS(data) - count = 0 - while rest: - count += 1 - ignored, rest = common.getMP(rest) - if count > 4: - return 'agentv3' - else: - return 'blob' - - @classmethod - def _fromRSAComponents(cls, n, e, d=None, p=None, q=None, u=None): - """ - Build a key from RSA numerical components. - - @type n: L{int} - @param n: The 'n' RSA variable. - - @type e: L{int} - @param e: The 'e' RSA variable. - - @type d: L{int} or L{None} - @param d: The 'd' RSA variable (optional for a public key). - - @type p: L{int} or L{None} - @param p: The 'p' RSA variable (optional for a public key). - - @type q: L{int} or L{None} - @param q: The 'q' RSA variable (optional for a public key). - - @type u: L{int} or L{None} - @param u: The 'u' RSA variable. Ignored, as its value is determined by - p and q. - - @rtype: L{Key} - @return: An RSA key constructed from the values as given. - """ - publicNumbers = rsa.RSAPublicNumbers(e=e, n=n) - if d is None: - # We have public components. - keyObject = publicNumbers.public_key(default_backend()) - else: - privateNumbers = rsa.RSAPrivateNumbers( - p=p, - q=q, - d=d, - dmp1=rsa.rsa_crt_dmp1(d, p), - dmq1=rsa.rsa_crt_dmq1(d, q), - iqmp=rsa.rsa_crt_iqmp(p, q), - public_numbers=publicNumbers, - ) - keyObject = privateNumbers.private_key(default_backend()) - - return cls(keyObject) - - @classmethod - def _fromDSAComponents(cls, y, p, q, g, x=None): - """ - Build a key from DSA numerical components. - - @type y: L{int} - @param y: The 'y' DSA variable. - - @type p: L{int} - @param p: The 'p' DSA variable. - - @type q: L{int} - @param q: The 'q' DSA variable. - - @type g: L{int} - @param g: The 'g' DSA variable. - - @type x: L{int} or L{None} - @param x: The 'x' DSA variable (optional for a public key) - - @rtype: L{Key} - @return: A DSA key constructed from the values as given. - """ - publicNumbers = dsa.DSAPublicNumbers( - y=y, parameter_numbers=dsa.DSAParameterNumbers(p=p, q=q, g=g)) - - if x is None: - try: - # We have public components. - keyObject = publicNumbers.public_key(default_backend()) - return cls(keyObject) - except ValueError as error: - raise BadKeyError( - 'Unsupported DSA public key: "%s"' % (force_unicode(error),)) - - try: - privateNumbers = dsa.DSAPrivateNumbers( - x=x, public_numbers=publicNumbers) - keyObject = privateNumbers.private_key(default_backend()) - except ValueError as error: - raise BadKeyError( - 'Unsupported DSA private key: "%s"' % (force_unicode(error),)) - - return cls(keyObject) - - @classmethod - def _fromECComponents(cls, x, y, curve, privateValue=None): - """ - Build a key from EC components. - - @param x: The affine x component of the public point used for verifying. - @type x: L{int} - - @param y: The affine y component of the public point used for verifying. - @type y: L{int} - - @param curve: NIST name of elliptic curve. - @type curve: L{bytes} - - @param privateValue: The private value. - @type privateValue: L{int} - """ - - publicNumbers = ec.EllipticCurvePublicNumbers( - x=x, y=y, curve=_curveTable[curve]) - if privateValue is None: - # We have public components. - keyObject = publicNumbers.public_key(default_backend()) - else: - privateNumbers = ec.EllipticCurvePrivateNumbers( - private_value=privateValue, public_numbers=publicNumbers) - keyObject = privateNumbers.private_key(default_backend()) - - return cls(keyObject) - - @classmethod - def _fromECEncodedPoint(cls, encodedPoint, curve, privateValue=None): - """ - Build a key from an EC encoded point. - - @param encodedPoint: The public point encoded as in SEC 1 v2.0 - section 2.3.3. - @type encodedPoint: L{bytes} - - @param curve: NIST name of elliptic curve. - @type curve: L{bytes} - - @param privateValue: The private value. - @type privateValue: L{int} - """ - - if privateValue is None: - # We have public components. - keyObject = ec.EllipticCurvePublicKey.from_encoded_point( - _curveTable[curve], encodedPoint - ) - else: - keyObject = ec.derive_private_key( - privateValue, _curveTable[curve], default_backend() - ) - - return cls(keyObject) - - @classmethod - def _fromEd25519Components(cls, a, k=None): - """Build a key from Ed25519 components. - - @param a: The Ed25519 public key, as defined in RFC 8032 section - 5.1.5. - @type a: L{bytes} - - @param k: The Ed25519 private key, as defined in RFC 8032 section - 5.1.5. - @type k: L{bytes} - """ - - try: - if k is None: - keyObject = ed25519.Ed25519PublicKey.from_public_bytes(a) - else: - keyObject = ed25519.Ed25519PrivateKey.from_private_bytes(k) - - return cls(keyObject) - except UnsupportedAlgorithm: - raise BadKeyError('Ed25519 keys are not supported.') - - def __init__(self, keyObject): - """ - Initialize with a private or public - C{cryptography.hazmat.primitives.asymmetric} key. - - @param keyObject: Low level key. - @type keyObject: C{cryptography.hazmat.primitives.asymmetric} key. - """ - self._keyObject = keyObject - - def __eq__(self, other): - """ - Return True if other represents an object with the same key. - """ - if type(self) == type(other): - return self.type() == other.type() and self.data() == other.data() - else: - return NotImplemented - - def __ne__(self, other): - """ - Return True if other represents anything other than this key. - """ - result = self.__eq__(other) - if result == NotImplemented: - return result - return not result - - def __repr__(self): - """ - Return a pretty representation of this object. - """ - if self.type() == 'EC': - data = self.data() - name = data['curve'].decode('utf-8') - - if self.isPublic(): - out = '\n" - else: - lines = [ - '<%s %s (%s bits)' % ( - self.type(), - self.isPublic() and 'Public Key' or 'Private Key', - self.size())] - for k, v in sorted(self.data().items()): - lines.append('attr %s:' % (k,)) - by = v if self.type() == 'Ed25519' else common.MP(v)[4:] - while by: - m = by[:15] - by = by[15:] - o = '' - for c in iterbytes(m): - o = o + '%02x:' % (ord(c),) - if len(m) < 15: - o = o[:-1] - lines.append('\t' + o) - lines[-1] = lines[-1] + '>' - return '\n'.join(lines) - - def isPublic(self): - """ - Check if this instance is a public key. - - @return: C{True} if this is a public key. - """ - return isinstance( - self._keyObject, - (rsa.RSAPublicKey, dsa.DSAPublicKey, ec.EllipticCurvePublicKey, - ed25519.Ed25519PublicKey)) - - def public(self): - """ - Returns a version of this key containing only the public key data. - If this is a public key, this may or may not be the same object - as self. - - @rtype: L{Key} - @return: A public key. - """ - if self.isPublic(): - return self - else: - return Key(self._keyObject.public_key()) - - def fingerprint(self, format=FingerprintFormats.MD5_HEX): - """ - The fingerprint of a public key consists of the output of the - message-digest algorithm in the specified format. - Supported formats include L{FingerprintFormats.MD5_HEX}, - L{FingerprintFormats.SHA256_BASE64} and - L{FingerprintFormats.SHA1_BASE64} - - The input to the algorithm is the public key data as specified by [RFC4253]. - - The output of sha256[RFC4634] and sha1[RFC3174] algorithms are - presented to the user in the form of base64 encoded sha256 and sha1 - hashes, respectively. - Examples: - C{US5jTUa0kgX5ZxdqaGF0yGRu8EgKXHNmoT8jHKo1StM=} - C{9CCuTybG5aORtuW4jrFcp0PbK4U=} - - The output of the MD5[RFC1321](default) algorithm is presented to the user as - a sequence of 16 octets printed as hexadecimal with lowercase letters - and separated by colons. - Example: C{c1:b1:30:29:d7:b8:de:6c:97:77:10:d7:46:41:63:87} - - @param format: Format for fingerprint generation. Consists - hash function and representation format. - Default is L{FingerprintFormats.MD5_HEX} - - @since: 8.2 - - @return: the user presentation of this L{Key}'s fingerprint, as a - string. - - @rtype: L{str} - """ - if format is FingerprintFormats.SHA256_BASE64: - return base64.b64encode( - sha256(self.blob()).digest()).decode('ascii') - elif format is FingerprintFormats.SHA1_BASE64: - return base64.b64encode( - sha1(self.blob()).digest()).decode('ascii') - elif format is FingerprintFormats.MD5_HEX: - return ':'.join([binascii.hexlify(x) - for x in iterbytes(md5(self.blob()).digest())]) - else: - raise BadFingerPrintFormat( - 'Unsupported fingerprint format: %s' % (format,)) - - def type(self): - """ - Return the type of the object we wrap. Currently this can only be - 'RSA', 'DSA', 'EC', or 'Ed25519'. - - @rtype: L{str} - @raises RuntimeError: If the object type is unknown. - """ - if isinstance( - self._keyObject, (rsa.RSAPublicKey, rsa.RSAPrivateKey)): - return 'RSA' - elif isinstance( - self._keyObject, (dsa.DSAPublicKey, dsa.DSAPrivateKey)): - return 'DSA' - elif isinstance( - self._keyObject, - (ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey)): - return 'EC' - elif isinstance( - self._keyObject, - (ed25519.Ed25519PublicKey, ed25519.Ed25519PrivateKey)): - return 'Ed25519' - else: - raise RuntimeError( - 'unknown type of object: %r' % (self._keyObject,)) - - def sshType(self): - """ - Get the type of the object we wrap as defined in the SSH protocol, - defined in RFC 4253, Section 6.6. Currently this can only be b'ssh-rsa', - b'ssh-dss' or b'ecdsa-sha2-[identifier]'. - - identifier is the standard NIST curve name - - @return: The key type format. - @rtype: L{bytes} - """ - if self.type() == 'EC': - return ( - b'ecdsa-sha2-' + - _secToNist[self._keyObject.curve.name.encode('ascii')]) - else: - return { - 'RSA': b'ssh-rsa', - 'DSA': b'ssh-dss', - 'Ed25519': b'ssh-ed25519', - }[self.type()] - - def supportedSignatureAlgorithms(self): - """ - Get the public key signature algorithms supported by this key. - @return: A list of supported public key signature algorithm names. - @rtype: L{list} of L{bytes} - """ - if self.type() == "RSA": - return [b"rsa-sha2-512", b"rsa-sha2-256", b"ssh-rsa"] - else: - return [self.sshType()] - - def _getHashAlgorithm(self, signatureType): - """ - Return a hash algorithm for this key type given an SSH signature - algorithm name, or L{None} if no such hash algorithm is defined for - this key type. - """ - if self.type() == "EC": - # Hash algorithm depends on key size - if signatureType == self.sshType(): - keySize = self.size() - if keySize <= 256: - return hashes.SHA256() - elif keySize <= 384: - return hashes.SHA384() - else: - return hashes.SHA512() - else: - return None - else: - return { - ("RSA", b"ssh-rsa"): hashes.SHA1(), - ("RSA", b"rsa-sha2-256"): hashes.SHA256(), - ("RSA", b"rsa-sha2-512"): hashes.SHA512(), - ("DSA", b"ssh-dss"): hashes.SHA1(), - ("Ed25519", b"ssh-ed25519"): hashes.SHA512(), - }.get((self.type(), signatureType)) - - def size(self): - """ - Return the size of the object we wrap. - - @return: The size of the key. - @rtype: L{int} - """ - if self._keyObject is None: - return 0 - elif self.type() == 'EC': - return self._keyObject.curve.key_size - elif self.type() == 'Ed25519': - return 256 - return self._keyObject.key_size - - def data(self): - """ - Return the values of the public key as a dictionary. - - @rtype: L{dict} - """ - if isinstance(self._keyObject, rsa.RSAPublicKey): - numbers = self._keyObject.public_numbers() - return { - "n": numbers.n, - "e": numbers.e, - } - elif isinstance(self._keyObject, rsa.RSAPrivateKey): - numbers = self._keyObject.private_numbers() - return { - "n": numbers.public_numbers.n, - "e": numbers.public_numbers.e, - "d": numbers.d, - "p": numbers.p, - "q": numbers.q, - # Use a trick: iqmp is q^-1 % p, u is p^-1 % q - "u": rsa.rsa_crt_iqmp(numbers.q, numbers.p), - } - elif isinstance(self._keyObject, dsa.DSAPublicKey): - numbers = self._keyObject.public_numbers() - return { - "y": numbers.y, - "g": numbers.parameter_numbers.g, - "p": numbers.parameter_numbers.p, - "q": numbers.parameter_numbers.q, - } - elif isinstance(self._keyObject, dsa.DSAPrivateKey): - numbers = self._keyObject.private_numbers() - return { - "x": numbers.x, - "y": numbers.public_numbers.y, - "g": numbers.public_numbers.parameter_numbers.g, - "p": numbers.public_numbers.parameter_numbers.p, - "q": numbers.public_numbers.parameter_numbers.q, - } - elif isinstance(self._keyObject, ec.EllipticCurvePublicKey): - numbers = self._keyObject.public_numbers() - return { - "x": numbers.x, - "y": numbers.y, - "curve": self.sshType(), - } - elif isinstance(self._keyObject, ec.EllipticCurvePrivateKey): - numbers = self._keyObject.private_numbers() - return { - "x": numbers.public_numbers.x, - "y": numbers.public_numbers.y, - "privateValue": numbers.private_value, - "curve": self.sshType(), - } - elif isinstance(self._keyObject, ed25519.Ed25519PublicKey): - return { - "a": self._keyObject.public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw - ), - } - elif isinstance(self._keyObject, ed25519.Ed25519PrivateKey): - return { - "a": self._keyObject.public_key().public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw - ), - "k": self._keyObject.private_bytes( - serialization.Encoding.Raw, - serialization.PrivateFormat.Raw, - serialization.NoEncryption() - ), - } - - else: - raise RuntimeError("Unexpected key type: %s" % (self._keyObject,)) - - def blob(self): - """ - Return the public key blob for this key. The blob is the - over-the-wire format for public keys. - - SECSH-TRANS RFC 4253 Section 6.6. - - RSA keys:: - string 'ssh-rsa' - integer e - integer n - - DSA keys:: - string 'ssh-dss' - integer p - integer q - integer g - integer y - - EC keys:: - string 'ecdsa-sha2-[identifier]' - integer x - integer y - - identifier is the standard NIST curve name - - Ed25519 keys:: - string 'ssh-ed25519' - string a - - @rtype: L{bytes} - """ - type = self.type() - data = self.data() - if type == 'RSA': - return (common.NS(b'ssh-rsa') + common.MP(data['e']) + - common.MP(data['n'])) - elif type == 'DSA': - return (common.NS(b'ssh-dss') + common.MP(data['p']) + - common.MP(data['q']) + common.MP(data['g']) + - common.MP(data['y'])) - elif type == 'EC': - byteLength = (self._keyObject.curve.key_size + 7) // 8 - return ( - common.NS(data['curve']) + common.NS(data["curve"][-8:]) + - common.NS( - b'\x04' + utils.int_to_bytes(data['x'], byteLength) + - utils.int_to_bytes(data['y'], byteLength))) - elif type == 'Ed25519': - return common.NS(b'ssh-ed25519') + common.NS(data['a']) - else: - raise BadKeyError('unknown key type: "%s"' % (force_unicode(type,))) - - - def privateBlob(self): - """ - Return the private key blob for this key. The blob is the - over-the-wire format for private keys: - - Specification in OpenSSH PROTOCOL.agent - - RSA keys:: - string 'ssh-rsa' - integer n - integer e - integer d - integer u - integer p - integer q - - DSA keys:: - string 'ssh-dss' - integer p - integer q - integer g - integer y - integer x - - EC keys:: - string 'ecdsa-sha2-[identifier]' - integer x - integer y - integer privateValue - - identifier is the NIST standard curve name. - - Ed25519 keys: - string 'ssh-ed25519' - string a - string k || a - """ - type = self.type() - data = self.data() - if type == 'RSA': - iqmp = rsa.rsa_crt_iqmp(data['p'], data['q']) - return (common.NS(b'ssh-rsa') + common.MP(data['n']) + - common.MP(data['e']) + common.MP(data['d']) + - common.MP(iqmp) + common.MP(data['p']) + - common.MP(data['q'])) - elif type == 'DSA': - return (common.NS(b'ssh-dss') + common.MP(data['p']) + - common.MP(data['q']) + common.MP(data['g']) + - common.MP(data['y']) + common.MP(data['x'])) - elif type == 'EC': - encPub = self._keyObject.public_key().public_bytes( - serialization.Encoding.X962, - serialization.PublicFormat.UncompressedPoint - ) - return (common.NS(data['curve']) + common.NS(data['curve'][-8:]) + - common.NS(encPub) + common.MP(data['privateValue'])) - elif type == 'Ed25519': - return (common.NS(b'ssh-ed25519') + common.NS(data['a']) + - common.NS(data['k'] + data['a'])) - else: - raise BadKeyError('unknown key type: "%s"' % (force_unicode(type,))) - - def toString(self, type, extra=None, comment=None, - passphrase=None): - """ - Create a string representation of this key. If the key is a private - key and you want the representation of its public key, use - C{key.public().toString()}. type maps to a _toString_* method. - - @param type: The type of string to emit. Currently supported values - are C{'OPENSSH'}, C{'LSH'}, and C{'AGENTV3'}. - @type type: L{str} - - @param extra: Any extra data supported by the selected format which - is not part of the key itself. For public OpenSSH keys, this is - a comment. For private OpenSSH keys, this is a passphrase to - encrypt with. (Deprecated since Twisted 20.3.0; use C{comment} - or C{passphrase} as appropriate instead.) - @type extra: L{bytes} or L{unicode} or L{None} - - @param comment: A comment to include with the key. Only supported - for OpenSSH keys. - - Present since Twisted 20.3.0. - - @type comment: L{bytes} or L{unicode} or L{None} - - @param passphrase: A passphrase to encrypt the key with. Only - supported for private OpenSSH keys. - - Present since Twisted 20.3.0. - - @type passphrase: L{bytes} or L{unicode} or L{None} - - @rtype: L{bytes} - """ - if extra is not None: - if self.isPublic(): - comment = extra - else: - passphrase = extra - if isinstance(comment, unicode): - comment = comment.encode("utf-8") - if isinstance(passphrase, unicode): - passphrase = passphrase.encode("utf-8") - method = getattr(self, '_toString_%s' % (type.upper(),), None) - if method is None: - raise BadKeyError( - 'unknown key type: "%s"' % (force_unicode(type[:30]),)) - - return method(comment=comment, passphrase=passphrase) - - def _toPublicOpenSSH(self, comment=None): - """ - Return a public OpenSSH key string. - - See _fromString_PUBLIC_OPENSSH for the string format. - - @type comment: L{bytes} or L{None} - @param comment: A comment to include with the key, or L{None} to - omit the comment. - """ - if self.type() == 'EC': - if not comment: - comment = b'' - return (self._keyObject.public_bytes( - serialization.Encoding.OpenSSH, - serialization.PublicFormat.OpenSSH - ) + b' ' + comment).strip() - - b64Data = encodebytes(self.blob()).replace(b'\n', b'') - if not comment: - comment = b'' - return (self.sshType() + b' ' + b64Data + b' ' + comment).strip() - - def _toString_OPENSSH_V1(self, comment=None, passphrase=None): - """ - Return a private OpenSSH key string, in the "openssh-key-v1" format - introduced in OpenSSH 6.5. - - See _fromPrivateOpenSSH_v1 for the string format. - - @type passphrase: L{bytes} or L{None} - @param passphrase: The passphrase to encrypt the key with, or L{None} - if it is not encrypted. - """ - if self.isPublic(): - return self._toPublicOpenSSH(comment=comment) - - if passphrase: - # For now we just hardcode the cipher to the one used by - # OpenSSH. We could make this configurable later if it's - # needed. - cipher = algorithms.AES - cipherName = b'aes256-ctr' - kdfName = b'bcrypt' - blockSize = cipher.block_size // 8 - keySize = 32 - ivSize = blockSize - salt = self.secureRandom(ivSize) - rounds = 100 - kdfOptions = common.NS(salt) + struct.pack('!L', rounds) - else: - cipherName = b'none' - kdfName = b'none' - blockSize = 8 - kdfOptions = b'' - check = self.secureRandom(4) - privKeyList = ( - check + check + self.privateBlob() + common.NS(comment or b'')) - padByte = 0 - while len(privKeyList) % blockSize: - padByte += 1 - privKeyList += chr(padByte & 0xFF) - if passphrase: - encKey = bcrypt.kdf(passphrase, salt, keySize + ivSize, 100) - encryptor = Cipher( - cipher(encKey[:keySize]), - modes.CTR(encKey[keySize:keySize + ivSize]), - backend=default_backend() - ).encryptor() - encPrivKeyList = ( - encryptor.update(privKeyList) + encryptor.finalize()) - else: - encPrivKeyList = privKeyList - blob = ( - b'openssh-key-v1\0' + - common.NS(cipherName) + - common.NS(kdfName) + common.NS(kdfOptions) + - struct.pack('!L', 1) + - common.NS(self.blob()) + - common.NS(encPrivKeyList)) - b64Data = encodebytes(blob).replace(b'\n', b'') - lines = ( - [b'-----BEGIN OPENSSH PRIVATE KEY-----'] + - [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)] + - [b'-----END OPENSSH PRIVATE KEY-----']) - return b'\n'.join(lines) + b'\n' - - def _toString_OPENSSH(self, comment=None, passphrase=None): - """ - Return a private OpenSSH key string, in the old PEM-based format. - - See _fromPrivateOpenSSH_PEM for the string format. - - @type passphrase: L{bytes} or L{None} - @param passphrase: The passphrase to encrypt the key with, or L{None} - if it is not encrypted. - """ - if self.isPublic(): - return self._toPublicOpenSSH(comment=comment) - - if self.type() == 'EC': - # EC keys has complex ASN.1 structure hence we do this this way. - if not passphrase: - # unencrypted private key - encryptor = serialization.NoEncryption() - else: - encryptor = serialization.BestAvailableEncryption(passphrase) - - return self._keyObject.private_bytes( - serialization.Encoding.PEM, - serialization.PrivateFormat.TraditionalOpenSSL, - encryptor) - elif self.type() == 'Ed25519': - raise BadKeyError( - 'Cannot serialize Ed25519 key to openssh format; ' - 'use openssh_v1 instead.' - ) - - data = self.data() - lines = [b''.join((b'-----BEGIN ', self.type().encode('ascii'), - b' PRIVATE KEY-----'))] - if self.type() == 'RSA': - p, q = data['p'], data['q'] - iqmp = rsa.rsa_crt_iqmp(p, q) - objData = (0, data['n'], data['e'], data['d'], p, q, - data['d'] % (p - 1), data['d'] % (q - 1), - iqmp) - else: - objData = (0, data['p'], data['q'], data['g'], data['y'], - data['x']) - asn1Sequence = univ.Sequence() - for index, value in izip(itertools.count(), objData): - asn1Sequence.setComponentByPosition(index, univ.Integer(value)) - asn1Data = berEncoder.encode(asn1Sequence) - if passphrase: - iv = self.secureRandom(8) - hexiv = ''.join(['%02X' % (ord(x),) for x in iterbytes(iv)]) - hexiv = hexiv.encode('ascii') - lines.append(b'Proc-Type: 4,ENCRYPTED') - lines.append(b'DEK-Info: DES-EDE3-CBC,' + hexiv + b'\n') - ba = md5(passphrase + iv).digest() - bb = md5(ba + passphrase + iv).digest() - encKey = (ba + bb)[:24] - padLen = 8 - (len(asn1Data) % 8) - asn1Data += chr(padLen) * padLen - - encryptor = Cipher( - algorithms.TripleDES(encKey), - modes.CBC(iv), - backend=default_backend() - ).encryptor() - - asn1Data = encryptor.update(asn1Data) + encryptor.finalize() - - b64Data = encodebytes(asn1Data).replace(b'\n', b'') - lines += [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)] - lines.append(b''.join((b'-----END ', self.type().encode('ascii'), - b' PRIVATE KEY-----'))) - return b'\n'.join(lines) - - def _toString_LSH(self, **kwargs): - """ - Return a public or private LSH key. See _fromString_PUBLIC_LSH and - _fromString_PRIVATE_LSH for the key formats. - - @rtype: L{bytes} - """ - data = self.data() - type = self.type() - if self.isPublic(): - if type == 'RSA': - keyData = sexpy.pack([[b'public-key', - [b'rsa-pkcs1-sha1', - [b'n', common.MP(data['n'])[4:]], - [b'e', common.MP(data['e'])[4:]]]]]) - elif type == 'DSA': - keyData = sexpy.pack([[b'public-key', - [b'dsa', - [b'p', common.MP(data['p'])[4:]], - [b'q', common.MP(data['q'])[4:]], - [b'g', common.MP(data['g'])[4:]], - [b'y', common.MP(data['y'])[4:]]]]]) - else: - raise BadKeyError( - 'unknown key type "%s"' % (force_unicode(type,))) - return (b'{' + encodebytes(keyData).replace(b'\n', b'') + - b'}') - else: - if type == 'RSA': - p, q = data['p'], data['q'] - iqmp = rsa.rsa_crt_iqmp(p, q) - return sexpy.pack([[b'private-key', - [b'rsa-pkcs1', - [b'n', common.MP(data['n'])[4:]], - [b'e', common.MP(data['e'])[4:]], - [b'd', common.MP(data['d'])[4:]], - [b'p', common.MP(q)[4:]], - [b'q', common.MP(p)[4:]], - [b'a', common.MP( - data['d'] % (q - 1))[4:]], - [b'b', common.MP( - data['d'] % (p - 1))[4:]], - [b'c', common.MP(iqmp)[4:]]]]]) - elif type == 'DSA': - return sexpy.pack([[b'private-key', - [b'dsa', - [b'p', common.MP(data['p'])[4:]], - [b'q', common.MP(data['q'])[4:]], - [b'g', common.MP(data['g'])[4:]], - [b'y', common.MP(data['y'])[4:]], - [b'x', common.MP(data['x'])[4:]]]]]) - else: - raise BadKeyError( - 'unknown key type "%s"' % (force_unicode(type,))) - - def _toString_AGENTV3(self, **kwargs): - """ - Return a private Secure Shell Agent v3 key. See - _fromString_AGENTV3 for the key format. - - @rtype: L{bytes} - """ - data = self.data() - if not self.isPublic(): - if self.type() == 'RSA': - values = (data['e'], data['d'], data['n'], data['u'], - data['p'], data['q']) - elif self.type() == 'DSA': - values = (data['p'], data['q'], data['g'], data['y'], - data['x']) - return common.NS(self.sshType()) + b''.join(map(common.MP, values)) - - def sign(self, data, signatureType=None): - """ - Sign some data with this private key. - - SECSH-TRANS RFC 4253 Section 6.6. - - @type data: L{bytes} - @param data: The data to sign. - - @rtype: L{bytes} - @return: A signature for the given data. - """ - if self.isPublic(): - raise KeyCertException('A private key is require to sign data.') - - keyType = self.type() - - if signatureType is None: - # Use the SSH public key type name by default, since for all - # current key types this can also be used as a public key - # algorithm name. (This exists for compatibility; new code - # should explicitly specify a public key algorithm name.) - signatureType = self.sshType() - - hashAlgorithm = self._getHashAlgorithm(signatureType) - if hashAlgorithm is None: - raise BadSignatureAlgorithmError( - "public key signature algorithm {} is not " - "defined for {} keys".format(signatureType, keyType) - ) - - if keyType == 'RSA': - sig = self._keyObject.sign(data, padding.PKCS1v15(), hashAlgorithm) - ret = common.NS(sig) - - elif keyType == 'DSA': - sig = self._keyObject.sign(data, hashAlgorithm) - (r, s) = decode_dss_signature(sig) - # SSH insists that the DSS signature blob be two 160-bit integers - # concatenated together. The sig[0], [1] numbers from obj.sign - # are just numbers, and could be any length from 0 to 160 bits. - # Make sure they are padded out to 160 bits (20 bytes each) - ret = common.NS(int_to_bytes(r, 20) + int_to_bytes(s, 20)) - - elif keyType == 'EC': # Pragma: no branch - signature = self._keyObject.sign(data, ec.ECDSA(hashAlgorithm)) - (r, s) = decode_dss_signature(signature) - - rb = int_to_bytes(r) - sb = int_to_bytes(s) - - # Int_to_bytes returns rb[0] as a str in python2 - # and an as int in python3 - if type(rb[0]) is str: - rcomp = ord(rb[0]) - else: - rcomp = rb[0] - - # If the MSB is set, prepend a null byte for correct formatting. - if rcomp & 0x80: - rb = b"\x00" + rb - - if type(sb[0]) is str: - scomp = ord(sb[0]) - else: - scomp = sb[0] - - if scomp & 0x80: - sb = b"\x00" + sb - - ret = common.NS(common.NS(rb) + common.NS(sb)) - - elif keyType == 'Ed25519': - ret = common.NS(self._keyObject.sign(data)) - - return common.NS(signatureType) + ret - - def verify(self, signature, data): - """ - Verify a signature using this key. - - @type signature: L{bytes} - @param signature: The signature to verify. - - @type data: L{bytes} - @param data: The signed data. - - @rtype: L{bool} - @return: C{True} if the signature is valid. - """ - if len(signature) == 40: - # DSA key with no padding - signatureType, signature = b'ssh-dss', common.NS(signature) - else: - signatureType, signature = common.getNS(signature) - - hashAlgorithm = self._getHashAlgorithm(signatureType) - if hashAlgorithm is None: - return False - - keyType = self.type() - if keyType == 'RSA': - k = self._keyObject - if not self.isPublic(): - k = k.public_key() - args = ( - common.getNS(signature)[0], - data, - padding.PKCS1v15(), - hashAlgorithm, - ) - elif keyType == 'DSA': - concatenatedSignature = common.getNS(signature)[0] - r = int_from_bytes(concatenatedSignature[:20], 'big') - s = int_from_bytes(concatenatedSignature[20:], 'big') - signature = encode_dss_signature(r, s) - k = self._keyObject - if not self.isPublic(): - k = k.public_key() - args = (signature, data, hashAlgorithm) - - elif keyType == 'EC': # Pragma: no branch - concatenatedSignature = common.getNS(signature)[0] - rstr, sstr, rest = common.getNS(concatenatedSignature, 2) - r = int_from_bytes(rstr, 'big') - s = int_from_bytes(sstr, 'big') - signature = encode_dss_signature(r, s) - - k = self._keyObject - if not self.isPublic(): - k = k.public_key() - - args = (signature, data, ec.ECDSA(hashAlgorithm)) - - elif keyType == 'Ed25519': - k = self._keyObject - if not self.isPublic(): - k = k.public_key() - args = (common.getNS(signature)[0], data) - - try: - k.verify(*args) - except InvalidSignature: - return False - else: - return True - - @staticmethod - def secureRandom(n): # pragma: no cover - return urandom(n) - - @classmethod - def generate(cls, key_type=DEFAULT_KEY_TYPE, key_size=None): - """ - Return a new private key. - - When `key_size` is None, the default value is used. - - `key_size` is ignored for ed25519. - """ - if not key_type: - key_type = 'not-specified' - key_type = key_type.lower() - - if not key_size: - if key_type == 'ecdsa': - key_size = 384 - else: - key_size = DEFAULT_KEY_SIZE - - key = None - try: - if key_type == u'rsa': - key = rsa.generate_private_key( - public_exponent=65537, - key_size=key_size, - ) - elif key_type == u'dsa': - key = dsa.generate_private_key(key_size=key_size) - elif key_type == 'ecdsa': - try: - curve = _ecSizeTable[key_size] - except KeyError: - raise KeyCertException( - 'Wrong key size "%s". Supported: %s.' % ( - key_size, - ', '.join([str(s) for s in _ecSizeTable.keys()]))) - key = ec.generate_private_key(curve) - elif key_type == 'ed25519': - key = ed25519.Ed25519PrivateKey.generate() - else: - raise KeyCertException('Unknown key type "%s".' % (key_type)) - - except ValueError as error: - raise KeyCertException( - u'Wrong key size "%d". %s' % (key_size, error)) - - return cls(key) - - - @classmethod - def getKeyFormat(cls, data): - """ - Return a type of key. - """ - key_type = cls._guessStringType(data) - human_readable = { - 'public_openssh': 'OpenSSH Public', - 'private_openssh': 'OpenSSH Private old format', - 'private_openssh_v1': 'OpenSSH Private new format', - 'public_sshcom': 'SSH.com Public', - 'private_sshcom': 'SSH.com Private', - 'private_putty': 'PuTTY Private', - 'public_lsh': 'LSH Public', - 'private_lsh': 'LSH Private', - 'public_x509_certificate': 'X509 Certificate', - 'public_x509': 'X509 Public', - 'public_pkcs1_rsa': 'PKCS#1 RSA Public', - 'private_pkcs8': 'PKCS#8 Private', - 'private_encrypted_pkcs8': 'PKCS#8 Encrypted Private', - } - - return human_readable.get(key_type, 'Unknown format') - - @staticmethod - def _getSSHCOMKeyContent(data): - """ - Return the raw content of the SSH.com key (private or public) without - armor and headers. - """ - lines = data.strip().splitlines() - # Split in lines, ignoring the first and last armors. - lines = lines[1:-1] - - # Filter headers, first line without ':' and which is not a - # continuation is the first line of the headers - continuation = False - while True: - if not lines: - # End of content. - break - - line = lines.pop(0) - if continuation: - # We have a continued line. - # ignore it and check if this line still continues. - if not line.endswith('\\'): - continuation = False - continue - - if ':' in line: - # We have a header line - # Ignore it and check if this is a long header. - if line.endswith('\\'): - continuation = True - continue - # This is not a header and not a continuation, so it must be the - # first line form content. - # Put it back and stop filtering the content. - lines.insert(0, line) - break - - content = ''.join(lines) - return base64.decodestring(content) - - @classmethod - def _fromString_PUBLIC_SSHCOM(cls, data): - """ - Return a public key object corresponding to this SSH.com public key - string. The format of a SSH.com public key string is:: - ---- BEGIN SSH2 PUBLIC KEY ---- - Subject: KEY_SUBJECT_UTF8 - Comment: KEY_COMMENT_UTF8 \ - KEY_COMMEMENT_CONTINUATION - x-private-headder: VALUE_UTF8 - - ---- END SSH2 PUBLIC KEY ---- - - * SSH.com content is wrapped at 70. putty-gen wraps it at 64. - * Header-tag MUST NOT be more than 64 8-bit bytes and is - case-insensitive. - * The Header-value MUST NOT be more than 1024 8-bit bytes. - * Each line in the header MUST NOT be more than 72 8-bit bytes. - * A line is continued if the last character in the line is a '\'. - * The Header-tag MUST be encoded in US-ASCII. - * The Header-value MUST be encoded in UTF-8 - - Compliant implementations MUST ignore headers with unrecognized - header-tags. Implementations SHOULD preserve such unrecognized - headers when manipulating the key file. - - @type data: C{bytes} - @return: A {Crypto.PublicKey.pubkey.pubkey} object - @raises BadKeyError: if the blob type is unknown. - """ - if not data.strip().endswith('---- END SSH2 PUBLIC KEY ----'): - raise BadKeyError("Fail to find END tag for SSH.com key.") - - blob = cls._getSSHCOMKeyContent(data) - return cls._fromString_BLOB(blob) - - @classmethod - def _fromString_PRIVATE_SSHCOM(cls, data, passphrase): - """ - Return a private key object corresponding to this SSH.com private key - string. - - See: L{_fromString_PUBLIC_SSH2} for information about key format. - - Key content is in PKCS#8 RFC 5208 Base64 encoded, - wrapped at maximum 72. - - SSH.com and putty-gen wraps the key at 70. - - Blob format as documented in Putty/import.c: - * uint32 magic number - * uint32 total blob size - * string key-type - * string cipher-type (tells you if key is encrypted) - * string encrypted-blob - - Key types: - * RSA if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}} - * DSA dl-modp{sign{dsa-nist-sha1},dh{plain}} - - Encryption key: - * first 16 bytes are MD5(passphrase) - * next 16 bytes are MD5(passphrase || first 16 bytes) - * concatenate at 24 - - The payload for an RSA key: - * mpint e - * mpint d - * mpint n - * mpint u - * mpint p - * mpint q - - The payload for a DSA key: - * uint32 0 - * mpint p - * mpint g - * mpint q - * mpint y - * mpint x - - @type data: C{bytes} - @return: A {Crypto.PublicKey.pubkey.pubkey} object - @raises BadKeyError: if - * the blob type is unknown. - * a passphrase is provided for an unencrypted key - """ - blob = cls._getSSHCOMKeyContent(data) - magic_number = struct.unpack('>I', blob[:4])[0] - if magic_number != SSHCOM_MAGIC_NUMBER: - raise BadKeyError( - 'Bad magic number for SSH.com key "%s"' % ( - force_unicode(magic_number),)) - struct.unpack('>I', blob[4:8])[0] # Ignore value for total size. - type_signature, rest = common.getNS(blob[8:]) - - key_type = None - if type_signature.startswith('if-modn{sign{rsa'): - key_type = 'rsa' - elif type_signature.startswith('dl-modp{sign{dsa'): - key_type = 'dsa' - else: - raise BadKeyError( - 'Unknown SSH.com key type "%s"' % force_unicode(type_signature)) - - cipher_type, rest = common.getNS(rest) - encrypted_blob, _ = common.getNS(rest) - - encryption_key = None - if cipher_type.lower() == b'none': - if passphrase: - raise BadKeyError('SSH.com key not encrypted') - key_data = encrypted_blob - elif cipher_type.lower() == b'3des-cbc': - if not passphrase: - raise EncryptedKeyError( - 'Passphrase must be provided for an encrypted key.') - encryption_key = cls._getDES3EncryptionKey(passphrase) - decryptor = Cipher( - algorithms.TripleDES(encryption_key), - modes.CBC(b'\x00' * 8), - backend=default_backend() - ).decryptor() - key_data = decryptor.update(encrypted_blob) + decryptor.finalize() - else: - raise BadKeyError( - 'Encryption method not supported: "%s"' % ( - force_unicode(cipher_type[:30]))) - - try: - payload, _ = common.getNS(key_data) - if key_type == 'rsa': - e, d, n, u, p, q, rest = cls._unpackMPSSHCOM(payload, 6) - return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) - - if key_type == 'dsa': - # First 32bit is an uint with value 0. We just ignore it. - p, g, q, y, x, rest = cls._unpackMPSSHCOM(payload[4:], 5) - return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) - except struct.error: - if encryption_key: - raise EncryptedKeyError('Bad password or bad key format.') - else: - BadKeyError('Failed to parse payload.') - - @staticmethod - def _getDES3EncryptionKey(passphrase): - """ - Return the encryption key used in DES3 cypher. - """ - DES3_KEY_SIZE = 24 - pass_1 = md5(passphrase).digest() - pass_2 = md5(passphrase + pass_1).digest() - return (pass_1 + pass_2)[:DES3_KEY_SIZE] - - @staticmethod - def _unpackMPSSHCOM(data, count=1): - """ - Get SSHCOM mpint. - - 32-bit bit count N, followed by (N+7)/8 bytes of data. - - Similar to Twisted getMP method. - """ - c = 0 - mp = [] - for i in range(count): - length = struct.unpack('>I', data[c:c + 4])[0] - length = (length + 7) // 8 - mp.append(int_from_bytes(data[c + 4:c + 4 + length], 'big')) - c += length + 4 - return tuple(mp) + (data[c:],) - - @staticmethod - def _packMPSSHCOM(number): - """ - Return the wire representation of a MP number for SSH.com. - - Similar to Twisted MP method. - """ - if number == 0: - return '\000' * 4 - - wire_number = int_to_bytes(number) - - wire_length = (len(wire_number) * 8) - 7 - return struct.pack('>L', wire_length) + wire_number - - def _toString_SSHCOM(self, comment=None, passphrase=None): - """ - Return a public or private SSH.com string. - - See _fromString_PUBLIC_SSHCOM and _fromString_PRIVATE_SSHCOM for the - string formats. If extra is present, it represents a comment for a - public key, or a passphrase for a private key. - - @param extra: Comment for a public key or passphrase for a private key. - @type extra: C{bytes} - - @rtype: C{bytes} - """ - if self.isPublic(): - return self._toString_SSHCOM_public(comment) - else: - return self._toString_SSHCOM_private(passphrase) - - def _toString_SSHCOM_public(self, extra): - """ - Return the public SSH.com string. - """ - lines = ['---- BEGIN SSH2 PUBLIC KEY ----'] - if extra: - line = 'Comment: "%s"' % (extra.encode('utf-8'),) - lines.append('\\\n'.join(textwrap.wrap(line, 70))) - - base64Data = base64.b64encode(self.blob()) - lines.extend(textwrap.wrap(base64Data, 70)) - lines.append('---- END SSH2 PUBLIC KEY ----') - return '\n'.join(lines) - - def _toString_SSHCOM_private(self, extra): - """ - Return the private SSH.com string. - """ - # Now we are left with a private key. - # Both encrypted and unencrypted keys have the same armor. - lines = ['---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----'] - - type_signature = None - payload_blob = None - data = self.data() - type = self.type() - if type == 'RSA': - type_signature = ( - 'if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}}') - payload_blob = ( - self._packMPSSHCOM(data['e']) + - self._packMPSSHCOM(data['d']) + - self._packMPSSHCOM(data['n']) + - self._packMPSSHCOM(data['u']) + - self._packMPSSHCOM(data['p']) + - self._packMPSSHCOM(data['q']) - ) - elif type == 'DSA': - type_signature = 'dl-modp{sign{dsa-nist-sha1},dh{plain}}' - payload_blob = ( - struct.pack('>I', 0) + - self._packMPSSHCOM(data['p']) + - self._packMPSSHCOM(data['g']) + - self._packMPSSHCOM(data['q']) + - self._packMPSSHCOM(data['y']) + - self._packMPSSHCOM(data['x']) - ) - else: # pragma: no cover - raise BadKeyError('Unsupported key type "%s"' % force_unicode(type)) - - payload_blob = common.NS(payload_blob) - - if extra: - # We got a password, so encrypt it. - cipher_type = '3des-cbc' - padding = b'\x00' * (8 - (len(payload_blob) % 8)) - payload_blob = payload_blob + padding - encryption_key = self._getDES3EncryptionKey(extra) - - encryptor = Cipher( - algorithms.TripleDES(encryption_key), - modes.CBC(b'\x00' * 8), - backend=default_backend() - ).encryptor() - encrypted_blob = ( - encryptor.update(payload_blob) + encryptor.finalize()) - else: - cipher_type = 'none' - encrypted_blob = payload_blob - - # We first create the content without magic number and - # total size, then compute the total size, and update the - # final content. - blob = ( - common.NS(type_signature) - + common.NS(cipher_type) - + common.NS(encrypted_blob) - ) - total_size = 8 + len(blob) - blob = ( - struct.pack('>I', SSHCOM_MAGIC_NUMBER) - + struct.pack('>I', total_size) - + blob - ) - - # In the end, encode in base 64 and wrap it. - blob = base64.b64encode(blob) - lines.extend(textwrap.wrap(blob, 70)) - - lines.append('---- END SSH2 ENCRYPTED PRIVATE KEY ----') - return '\n'.join(lines).encode('ascii') - - @classmethod - def _fromString_PRIVATE_PUTTY(cls, data, passphrase): - """ - Read a private Putty key. - - Format is: - - PuTTY-User-Key-File-2: ssh-rsa - Encryption: aes256-cbc | none - Comment: SINGLE_LINE_COMMENT - Public-Lines: PUBLIC_LINES - < base64 public part always in plain > - Private-Lines: 8 - < base64 private part > - Private-MAC: 1398fbfc7ce307d9ee0e42851f183f88c728398f - - Pulic part RSA: - * string type (ssh-rsa) - * mpint e - * mpint n - Private part RSA: - * mpint d - * mpint q - * mpint p - * mpint u - - Pulic part DSA: - * string type (ssh-dss) - * mpint p - * mpint q - * mpint g - * mpint v` - Private part DSA: - * mpint x - - Public part ECDSA-SHA2-*: - * string 'ecdsa-sha2-[identifier]' - * string identifier - * mpint x - * mpint y - Private part ECDSA-SHA2-*: - * string q - * mpint privateValue - - Public part Ed25519: - * string type (ssh-ed25519) - * string a - Private part Ed25519: - * string k - - Private part is padded for encryption. - - Encryption key is composed of concatenating, up to block size: - * uint32 sequence, starting from 0 - * passphrase - - Lines are terminated by CRLF, although CR-only and LF-only are - tolerated on input. - - Only version 2 is supported. - Version 2 was introduced in PuTTY 0.52. - Version 1 was an in-development format used in 0.52 snapshot - """ - lines = data.strip().splitlines() - - key_type = lines[0][22:].strip().lower() - if key_type not in [ - b'ssh-rsa', - b'ssh-dss', - b'ssh-ed25519', - ] and key_type not in _curveTable: - raise BadKeyError( - 'Unsupported key type: "%s"' % force_unicode(key_type[:30])) - - encryption_type = lines[1][11:].strip().lower() - - if encryption_type == b'none': - if passphrase: - raise BadKeyError('PuTTY key not encrypted') - elif encryption_type != b'aes256-cbc': - raise BadKeyError( - 'Unsupported encryption type: "%s"' % force_unicode( - encryption_type[:30])) - - comment = lines[2][9:].strip() - - public_count = int(lines[3][14:].strip()) - base64_content = ''.join(lines[ - 4: - 4 + public_count - ]) - public_blob = base64.decodestring(base64_content) - public_type, public_payload = common.getNS(public_blob) - - if public_type.lower() != key_type: - raise BadKeyError( - 'Mismatch key type. Header has "%s", public has "%s"' % ( - force_unicode(key_type[:30]), - force_unicode(public_type[:30]))) - - # We skip 4 lines so far and the total public lines. - private_start_line = 4 + public_count - private_count = int(lines[private_start_line][15:].strip()) - base64_content = ''.join(lines[ - private_start_line + 1: - private_start_line + 1 + private_count - ]) - private_blob = base64.decodestring(base64_content) - - private_mac = lines[-1][12:].strip() - - hmac_key = PUTTY_HMAC_KEY - encryption_key = None - if encryption_type == b'aes256-cbc': - if not passphrase: - raise EncryptedKeyError( - 'Passphrase must be provided for an encrypted key.') - hmac_key += passphrase - encryption_key = cls._getPuttyAES256EncryptionKey(passphrase) - decryptor = Cipher( - algorithms.AES(encryption_key), - modes.CBC(b'\x00' * 16), - backend=default_backend() - ).decryptor() - private_blob = ( - decryptor.update(private_blob) + decryptor.finalize()) - - # I have no idea why these values are packed form HMAC as net strings. - hmac_data = ( - common.NS(key_type) + - common.NS(encryption_type) + - common.NS(comment) + - common.NS(public_blob) + - common.NS(private_blob) - ) - hmac_key = sha1(hmac_key).digest() - computed_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest() - if private_mac != computed_mac: - if encryption_key: - raise EncryptedKeyError('Bad password or HMAC mismatch.') - else: - raise BadKeyError( - 'HMAC mismatch: file declare "%s", actual is "%s"' % ( - force_unicode(private_mac), - force_unicode(computed_mac))) - - if key_type == b'ssh-rsa': - e, n, _ = common.getMP(public_payload, count=2) - d, q, p, u, _ = common.getMP(private_blob, count=4) - return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) - - if key_type == b'ssh-dss': - p, q, g, y, _ = common.getMP(public_payload, count=4) - x, _ = common.getMP(private_blob) - return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) - - if key_type == b'ssh-ed25519': - a, _ = common.getNS(public_payload) - k, _ = common.getNS(private_blob) - return cls._fromEd25519Components(a=a, k=k) - - if key_type in _curveTable: - curve = _curveTable[key_type] - curveName, q, _ = common.getNS(public_payload, 2) - if curveName != _secToNist[curve.name.encode('ascii')]: - raise BadKeyError( - 'ECDSA curve name "%s" does not match key type "%s"' % ( - force_unicode(curveName), - force_unicode(key_type))) - - privateValue, _ = common.getMP(private_blob) - return cls._fromECEncodedPoint( - encodedPoint=q, curve=key_type, privateValue=privateValue) - - @staticmethod - def _getPuttyAES256EncryptionKey(passphrase): - """ - Return the encryption key used in Putty AES 256 cipher. - """ - key_size = 32 - part_1 = sha1(b'\x00\x00\x00\x00' + passphrase).digest() - part_2 = sha1(b'\x00\x00\x00\x01' + passphrase).digest() - return (part_1 + part_2)[:key_size] - - def _toString_PUTTY(self, comment=None, passphrase=None): - """ - Return a public or private Putty string. - - See _fromString_PRIVATE_PUTTY for the private format. - See _fromString_PUBLIC_SSHCOM for the public format. - - Private key is exported in version 2 format. - - If extra is present, it represents a comment for a - public key, or a passphrase for a private key. - - @param extra: Comment for a public key or passphrase for a private key. - @type extra: C{bytes} - - @rtype: C{bytes} - """ - if self.isPublic(): - # Putty uses SSH.com as public format. - return self._toString_SSHCOM_public(comment) - else: - return self._toString_PUTTY_private(passphrase) - - def _toString_PUTTY_private(self, extra): - """ - Return the Putty private key representation. - - See fromString for Putty file format. - """ - aes_block_size = 16 - lines = [] - key_type = self.sshType() - comment = 'Exported by chevah-keycert.' - data = self.data() - - hmac_key = PUTTY_HMAC_KEY - if extra: - encryption_type = b'aes256-cbc' - hmac_key += extra - else: - encryption_type = 'none' - - if key_type == b'ssh-rsa': - public_blob = ( - common.NS(key_type) + - common.MP(data['e']) + - common.MP(data['n']) - ) - private_blob = ( - common.MP(data['d']) + - common.MP(data['q']) + - common.MP(data['p']) + - common.MP(data['u']) - ) - elif key_type == b'ssh-dss': - public_blob = ( - common.NS(key_type) + - common.MP(data['p']) + - common.MP(data['q']) + - common.MP(data['g']) + - common.MP(data['y']) - ) - private_blob = common.MP(data['x']) - - elif key_type == b'ssh-ed25519': - public_blob = ( - common.NS(key_type) + - common.NS(data['a']) - ) - private_blob = common.NS(data['k']) - - elif key_type in _curveTable: - - curve_name = _secToNist[self._keyObject.curve.name] - public_blob = ( - common.NS(key_type) + - common.NS(curve_name) + - common.NS(self._keyObject.public_key().public_numbers().encode_point()) - ) - private_blob = common.MP(data['privateValue']) - - else: # pragma: no cover - raise BadKeyError('Unsupported key type.') - - private_blob_plain = private_blob - private_blob_encrypted = private_blob - - if extra: - # Encryption is requested. - # Padding is required for encryption. - padding_size = -1 * ( - (len(private_blob) % aes_block_size) - aes_block_size) - private_blob_plain += b'\x00' * padding_size - encryption_key = self._getPuttyAES256EncryptionKey(extra) - encryptor = Cipher( - algorithms.AES(encryption_key), - modes.CBC(b'\x00' * aes_block_size), - backend=default_backend() - ).encryptor() - private_blob_encrypted = ( - encryptor.update(private_blob_plain) + encryptor.finalize()) - - public_lines = textwrap.wrap(base64.b64encode(public_blob), 64) - private_lines = textwrap.wrap( - base64.b64encode(private_blob_encrypted), 64) - - hmac_data = ( - common.NS(key_type) + - common.NS(encryption_type) + - common.NS(comment) + - common.NS(public_blob) + - common.NS(private_blob_plain) - ) - hmac_key = sha1(hmac_key).digest() - private_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest() - - lines.append('PuTTY-User-Key-File-2: %s' % key_type) - lines.append('Encryption: %s' % encryption_type) - lines.append('Comment: %s' % comment) - lines.append('Public-Lines: %s' % len(public_lines)) - lines.extend(public_lines) - lines.append('Private-Lines: %s' % len(private_lines)) - lines.extend(private_lines) - lines.append('Private-MAC: %s' % private_mac) - return '\r\n'.join(lines) - - @classmethod - def _fromString_PUBLIC_X509_CERTIFICATE(cls, data): - """ - Read the public key from X509 Certificates in PEM format. - """ - try: - cert = crypto.load_certificate(crypto.FILETYPE_PEM, data) - except crypto.Error as error: - raise BadKeyError( - 'Failed to load certificate. "%s"' % (force_unicode(error),)) - - return cls._fromOpenSSLPublic(cert.get_pubkey(), 'certificate') - - @classmethod - def _fromString_PUBLIC_X509(cls, data): - """ - Read the public key from X509 public key PEM format. - """ - try: - pkey = crypto.load_publickey(crypto.FILETYPE_PEM, data) - except crypto.Error as error: - raise BadKeyError( - 'Failed to load PKCS#1 public key. "%s"' % ( - force_unicode(error),)) - - return cls._fromOpenSSLPublic(pkey, 'X509 public PEM file') - - @classmethod - def _fromOpenSSLPublic(cls, pkey, source_type): - """ - Load the SSH from an OpenSSL Public Key object. - """ - return cls(pkey.to_cryptography_key()) - - @classmethod - def _fromString_PRIVATE_PKCS8(cls, data, passphrase=None): - """ - Read the private key from PKCS8 PEM format. - """ - return cls._load_PRIVATE_PKCS8(data, passphrase='') - - @classmethod - def _fromString_PRIVATE_ENCRYPTED_PKCS8(cls, data, passphrase=None): - """ - Read the encrypted private key from PKCS8 PEM format. - """ - if not passphrase: - raise EncryptedKeyError( - 'Passphrase must be provided for an encrypted key') - - return cls._load_PRIVATE_PKCS8(data, passphrase) - - @classmethod - def _load_PRIVATE_PKCS8(cls, data, passphrase): - """ - Shared code for loading a private PKCS8 key. - """ - try: - key = crypto.load_privatekey( - crypto.FILETYPE_PEM, data, passphrase=passphrase) - except crypto.Error as error: - raise BadKeyError( - 'Failed to load PKCS#8 PEM. "%s"' % (force_unicode(error),)) - - return cls(key.to_cryptography_key()) - - @classmethod - def _fromString_PUBLIC_PKCS1_RSA(cls, data): - """ - Read the public key from PKCS1 PEM format. - - This is also the OpenSSH public PEM. - - RSAPublicKey ::= SEQUENCE { - modulus INTEGER, -- n - publicExponent INTEGER -- e - } - - """ - lines = data.strip().splitlines() - data = base64.decodestring(b''.join(lines[1:-1])) - decodedKey = berDecoder.decode(data)[0] - if len(decodedKey) != 2: - raise BadKeyError('Invalid ASN.1 payload for PKCS1 PEM.') - - n = long(decodedKey[0]) - e = long(decodedKey[1]) - return cls._fromRSAComponents(n=n, e=e) - - -def generate_ssh_key_parser(subparsers, name, default_key_type='rsa'): - """ - Create an argparse sub-command with `name` attached to `subparsers`. - """ - generate_ssh_key = subparsers.add_parser( - name, - help='Create a SSH public and private key pair.', - ) - generate_ssh_key.add_argument( - '--key-file', - metavar='FILE', - help=( - 'Store the keys pair in FILE and FILE.pub. Default id_TYPE.'), - ) - generate_ssh_key.add_argument( - '--key-size', - type=int, metavar="SIZE", default=None, - help='Generate a SSH key of size SIZE', - ) - generate_ssh_key.add_argument( - '--key-type', - metavar="[rsa|dsa|ecdsa|ed25519]", default=default_key_type, - help='Generate a new SSH private and public key. Default %(default)s.', - ) - generate_ssh_key.add_argument( - '--key-comment', - metavar="COMMENT_TEXT", - help=( - 'Generate the public key using this comment. Default no comment.'), - ) - generate_ssh_key.add_argument( - '--key-format', - metavar="[openssh|openssh_v1|putty]", default='openssh_v1', - help='Generate a new SSH private and public key. Default %(default)s.', - ) - generate_ssh_key.add_argument( - '--key-password', - metavar="PLAIN-PASS", default=None, - help='Password used to store the SSH private key.', - ) - generate_ssh_key.add_argument( - '--key-skip', - action='store_true', default=False, - help='Do not create a new key if a key file already exists.', - ) - return generate_ssh_key - - -def generate_ssh_key(options, open_method=None): - """ - Generate a SSH RSA or DSA key and store it on disk. - - `options` is an argparse namespace. See `generate_ssh_key_subparser`. - - Return a tuple of (exit_code, operation_message, key). - - For success, exit_code is 0. - - `open_method` is a helper for dependency injection during tests. - """ - key = None - - if open_method is None: # pragma: no cover - open_method = open - - exit_code = 0 - message = '' - try: - key_size = options.key_size - key_type = options.key_type.lower() - key_format = options.key_format.lower() - - if not hasattr(options, 'key_file') or options.key_file is None: - options.key_file = u'id_%s' % (key_type) - - private_file = options.key_file - - public_file = u'%s%s' % ( - options.key_file, DEFAULT_PUBLIC_KEY_EXTENSION) - - skip = _skip_key_generation(options, private_file, public_file) - if skip: - return (0, u'Key already exists.', key) - - key = Key.generate(key_type=key_type, key_size=key_size) - - with open_method(_path(private_file), 'wb') as file_handler: - _store_SSHKey( - key, - private_file=file_handler, - key_format=key_format, - password=options.key_password, - ) - - key_comment = None - if hasattr(options, 'key_comment') and options.key_comment: - key_comment = options.key_comment - message_comment = u'having comment "%s"' % key_comment - if key_format != 'openssh': - key_comment = None - message_comment = ( - 'without comment as not supported by the output format') - else: - message_comment = u'without a comment' - - with open_method(_path(public_file), 'wb') as file_handler: - _store_SSHKey( - key, - public_file=file_handler, - comment=key_comment, - key_format=key_format, - ) - - message = ( - u'SSH key of type "%s" and length "%d" generated as ' - u'public key file "%s" and private key file "%s" %s.') % ( - key.sshType(), - key.size(), - public_file, - private_file, - message_comment, - ) - - exit_code = 0 - - except KeyCertException as error: - exit_code = 1 - message = error.message - except Exception as error: - exit_code = 1 - message = unicode(error) - - return (exit_code, message, key) - - -def _store_SSHKey( - key, - public_file=None, private_file=None, - comment=None, password=None, key_format='openssh_v1', - ): - """ - Store the public and private key into a file like object using - OpenSSH format. - """ - if public_file: - public_serialization = key.public().toString( - type=key_format) - if comment: - public_content = '%s %s' % ( - public_serialization, comment.encode('utf-8')) - else: - public_content = public_serialization - public_file.write(public_content) - - if private_file: - private_file.write(key.toString(type=key_format, passphrase=password)) - - -def _skip_key_generation(options, private_file, public_file): - """ - Return True when key generation can be skipped. - - Key generation can be skipped when private key already exists. Public - key is ignored. - - Raise KeyCertException if file exists. - """ - if os.path.exists(_path(private_file)): - if options.key_skip: - return True - else: - raise KeyCertException( - u'Private key already exists. %s' % private_file) - - if os.path.exists(_path(public_file)): - raise KeyCertException(u'Public key already exists. %s' % public_file) - return False diff --git a/chevah/keycert/ssl.py b/chevah/keycert/ssl.py deleted file mode 100644 index 49e4cf0..0000000 --- a/chevah/keycert/ssl.py +++ /dev/null @@ -1,417 +0,0 @@ -# Copyright (c) 2015 Adi Roiban. -# See LICENSE for details. -""" -SSL keys and certificates. -""" -from __future__ import unicode_literals -import os -from random import randint - -from OpenSSL import crypto - -from chevah.keycert import _path -from chevah.keycert.exceptions import KeyCertException - -_DEFAULT_SSL_KEY_CYPHER = b'aes-256-cbc' -_SUPPORTED_SIGN_ALGORITHMS = [b'md5', b'sha1', b'sha256', b'sha512'] - -# See https://www.openssl.org/docs/manmaster/man5/x509v3_config.html -_KEY_USAGE_STANDARD = { - 'digital-signature': b'digitalSignature', - 'non-repudiation': b'nonRepudiation', - 'key-encipherment': b'keyEncipherment', - 'data-encipherment': b'dataEncipherment', - 'key-agreement': b'keyAgreement', - 'key-cert-sign': b'keyCertSign', - 'crl-sign': b'cRLSign', - 'encipher-only': b'encipherOnly', - 'decipher-only': b'decipherOnly', - } -_KEY_USAGE_EXTENDED = { - 'server-authentication': b'serverAuth', - 'client-authentication': b'clientAuth', - 'code-signing': b'codeSigning', - 'email-protection': b'emailProtection', - } - - -def _generate_self_csr_parser(sub_command, default_key_size): - """ - Add share configuration options for CSR and self-signed generation. - """ - sub_command.add_argument( - '--common-name', - help='Common name associated with the certificate.', - required=True, - ) - - sub_command.add_argument( - '--key-size', - type=int, metavar="SIZE", default=default_key_size, - help='Size of the generate RSA private key. Default %(default)s', - ) - - sub_command.add_argument( - '--sign-algorithm', - default='sha256', - metavar='STRING', - help='Signature algorithm: sha1, sha256, sha512. Default: sha256.' - ) - - sub_command.add_argument( - '--key-usage', - default='', - help=( - 'Comma-separated key usage. ' - 'The following key usage extensions are supported: %s. ' - 'To mark usage as critical, prefix the values with `critical,`. ' - 'For example: "critical,key-agreement,digital-signature".' - ) % (', '.join( - _KEY_USAGE_STANDARD.keys() + _KEY_USAGE_EXTENDED.keys())), - ) - - sub_command.add_argument( - '--constraints', - default='', - help=( - 'Comma-separated basic constraints. ' - 'To mark constraints as critical, prefix the values with ' - '`critical,`. ' - 'For example: "critical,CA:TRUE,pathlen:0".' - ), - ) - - sub_command.add_argument( - '--email', - help='Email address.', - ) - sub_command.add_argument( - '--alternative-name', - help=( - 'Optional list of alternative names. ' - 'Use "DNS:your.domain.tld" for domain names. ' - 'Use "IP:1.2.3.4" for IP addresses. ' - 'Example: "DNS:top.com,DNS:www.top.com,IP:11.0.21.12".' - ) - ) - sub_command.add_argument( - '--organization', - help='Organization.', - ) - sub_command.add_argument( - '--organization-unit', - help='Organization unit.', - ) - sub_command.add_argument( - '--locality', - help='Full name of the locality.', - ) - sub_command.add_argument( - '--state', - help=( - 'Full name of the state/county/region/province.'), - ) - sub_command.add_argument( - '--country', - help=( - 'Two-letter country code.'), - ) - - -def generate_csr_parser(subparsers, name, default_key_size=2048): - """ - Create an argparse sub-command for generating CSR options with - `name` attached to `subparsers`. - """ - sub_command = subparsers.add_parser( - name, - help=( - 'Create an SSL private key and an associated certificate ' - 'signing request.'), - ) - - sub_command.add_argument( - '--key', - metavar="FILE", - default=None, - help=( - 'Sign the CSR using this private key. ' - 'Private key loaded as PEM PKCS#8 format. ' - ), - ) - sub_command.add_argument( - '--key-file', - metavar="FILE", - default='server.key', - help=( - 'Store the keys/CSR pair in FILE and FILE.csr. ' - 'Private key stored using PEM PKCS#8 format. ' - 'CSR file stored in PEM x509 format. ' - 'Default names: server.key and server.csr.'), - ) - - sub_command.add_argument( - '--key-password', - metavar="PASSPHRASE", - help=( - 'Password used to encrypt the generated key. ' - 'Default no encryption. Encrypted with %s.' % ( - _DEFAULT_SSL_KEY_CYPHER,)), - ) - _generate_self_csr_parser(sub_command, default_key_size) - - return sub_command - - -def generate_self_signed_parser(subparsers, name, default_key_size=2048): - """ - Create an argparse sub-command for generating self signed options with - `name` attached to `subparsers`. - """ - sub_command = subparsers.add_parser( - name, - help=( - 'Create an SSL private key ' - 'and an associated self-signed certificate.'), - ) - _generate_self_csr_parser(sub_command, default_key_size) - return sub_command - - -def generate_csr(options): - """ - Generate a new SSL key and the associated SSL cert signing. - - Returns a tuple of (csr_pem, key_pem) - Raise KeyCertException on failure. - """ - try: - return _generate_csr(options) - except crypto.Error as error: - try: - message = error[0][0][2].decode('utf-8', errors='replace') - except IndexError: # pragma: no cover - message = 'no error details.' - raise KeyCertException(message) - - -def _set_subject_and_extensions(target, options): - """ - Set the subject and option for `target` CRS or certificate. - """ - common_name = options.common_name - constraints = getattr(options, 'constraints', '') - key_usage = getattr(options, 'key_usage', '').lower() - email = getattr(options, 'email', '') - alternative_name = getattr(options, 'alternative_name', '') - country = getattr(options, 'country', '') - state = getattr(options, 'state', '') - locality = getattr(options, 'locality', '') - organization = getattr(options, 'organization', '') - organization_unit = getattr(options, 'organization_unit', '') - - # RFC 2459 defines it as optional, and pyopenssl set it to `0` anyway. - # But we got reports that Windows 2003 and Windows 2008 Servers - # can not parse CSR generated using this tool, so here we are. - target.set_version(2) - - subject = target.get_subject() - - subject.CN = common_name.encode('idna') - - if country: - if len(country) != 2: - raise KeyCertException('Invalid country code.') - - subject.C = country - - if state: - subject.ST = state - - if locality: - subject.L = locality - - if organization: - subject.O = organization - - if organization_unit: - subject.OU = organization_unit - - if email: - try: - address, domain = options.email.split('@', 1) - except ValueError: - raise KeyCertException('Invalid email address.') - - subject.emailAddress = u'%s@%s' % (address, domain.encode('idna')) - - critical_constraints = False - critical_usage = False - standard_usage = [] - extended_usage = [] - extensions = [] - - if constraints.lower().startswith('critical'): - critical_constraints = True - constraints = constraints[8:].strip(',').strip() - - if key_usage.startswith('critical'): - critical_usage = True - key_usage = key_usage[8:] - - for usage in key_usage.split(','): - usage = usage.strip() - if not usage: - continue - if usage in _KEY_USAGE_STANDARD: - standard_usage.append(_KEY_USAGE_STANDARD[usage]) - if usage in _KEY_USAGE_EXTENDED: - extended_usage.append(_KEY_USAGE_EXTENDED[usage]) - - if constraints: - extensions.append(crypto.X509Extension( - b'basicConstraints', - critical_constraints, - constraints.encode('ascii'), - )) - - if standard_usage: - extensions.append(crypto.X509Extension( - b'keyUsage', - critical_usage, - b','.join(standard_usage), - )) - - if extended_usage: - extensions.append(crypto.X509Extension( - b'extendedKeyUsage', - critical_usage, - b','.join(extended_usage), - )) - - # Alternate name is optional. - if alternative_name: - extensions.append(crypto.X509Extension( - b'subjectAltName', - False, - alternative_name.encode('idna'))) - target.add_extensions(extensions) - - -def _sign_cert_or_csr(target, key, options): - """ - Sign the certificate or CSR. - """ - sign_algorithm = getattr( - options, 'sign_algorithm', 'sha256').encode('ascii') - - if sign_algorithm not in _SUPPORTED_SIGN_ALGORITHMS: - raise KeyCertException( - 'Invalid signing algorithm. Supported values: %s.' % ( - ', '.join(_SUPPORTED_SIGN_ALGORITHMS))) - - target.set_pubkey(key) - target.sign(key, sign_algorithm) - - -def _generate_csr(options): - """ - Helper to catch all crypto errors and reduce indentation. - """ - key_size = getattr(options, 'key_size', 2048) - - if key_size < 512: - raise KeyCertException('Key size must be greater or equal to 512.') - - key_type = crypto.TYPE_RSA - - csr = crypto.X509Req() - - _set_subject_and_extensions(csr, options) - - key_pem = None - private_key = options.key - if private_key: - if os.path.exists(_path(private_key)): - with open(_path(private_key), 'rb') as stream: - private_key = stream.read() - - key_pem = private_key - key = crypto.load_privatekey(crypto.FILETYPE_PEM, private_key) - else: - # Generate new Key. - key = crypto.PKey() - key.generate_key(key_type, key_size) - - _sign_cert_or_csr(csr, key, options) - - csr_pem = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr) - - if not key_pem: - if options.key_password: - key_pem = crypto.dump_privatekey( - crypto.FILETYPE_PEM, key, - _DEFAULT_SSL_KEY_CYPHER, options.key_password.encode('utf-8')) - else: - key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) - - return { - 'csr_pem': csr_pem, - 'key_pem': key_pem, - 'csr': csr, - 'key': key, - } - - -def generate_ssl_self_signed_certificate(options): - """ - Generate a self signed SSL certificate. - - Returns a tuple of (certificate_pem, key_pem) - """ - key_size = getattr(options, 'key_size', 2048) - - serial = randint(0, 1000000000000) - - key = crypto.PKey() - key.generate_key(crypto.TYPE_RSA, key_size) - - cert = crypto.X509() - - _set_subject_and_extensions(cert, options) - - cert.set_serial_number(serial) - cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) - - cert.set_issuer(cert.get_subject()) - - _sign_cert_or_csr(cert, key, options) - - certificate_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) - key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) - return (certificate_pem, key_pem) - - -def generate_and_store_csr(options, encoding='utf-8'): - """ - Generate a key/csr and try to store it on disk. - - Raise KeyCertException when failing to create the key or csr. - """ - name, _ = os.path.splitext(options.key_file) - csr_name = u'%s.csr' % name - - if os.path.exists(_path(options.key_file, encoding)): - raise KeyCertException('Key file already exists.') - - result = generate_csr(options) - - try: - with open(_path(options.key_file, encoding), 'wb') as store_file: - store_file.write(result['key_pem']) - - with open(_path(csr_name, encoding), 'wb') as store_file: - store_file.write(result['csr_pem']) - except Exception as error: - raise KeyCertException(str(error).decode('utf-8', errors='replace')) diff --git a/chevah/keycert/tests/__init__.py b/chevah/keycert/tests/__init__.py deleted file mode 100644 index 1c5610b..0000000 --- a/chevah/keycert/tests/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from chevah.compat.testing import mk - - -def setup_package(): - """ - Called before running all tests. - """ - # Prepare the main testing filesystem. - mk.fs.setUpTemporaryFolder() - - -def teardown_package(): - """ - Called after all tests were run. - """ - # Remove main testing folder. - mk.fs.tearDownTemporaryFolder() - mk.fs.checkCleanTemporaryFolders() diff --git a/chevah/keycert/tests/helpers.py b/chevah/keycert/tests/helpers.py deleted file mode 100644 index 7ba5a75..0000000 --- a/chevah/keycert/tests/helpers.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2015 Adi Roiban. -# See LICENSE for details. -""" -Helpers for testing the project. -""" -from argparse import Namespace -from StringIO import StringIO -import sys - - -class CommandLineMixin(object): - """ - Helper to test command line tools. - """ - def parseArguments(self, args): - """ - Parse arguments and return options and captured stdout. - """ - stdout = StringIO() - stderr = StringIO() - prev_stdout = sys.stdout - prev_stderr = sys.stderr - try: - sys.stdout = stdout - sys.stderr = stderr - options = self.parser.parse_args(args) - return options - except SystemExit as error: # pragma: no cover - raise AssertionError( - 'Fail to parse %s\n-- stdout --\n%s\n-- stderr --\n%s' % ( - error.code, - stdout.getvalue(), - stderr.getvalue(), - )) - finally: - # We don't revert to sys.__stdout__ and the test runner might - # have injected its logger. - sys.stdout = prev_stdout - sys.stderr = prev_stderr - - def parseArgumentsFailure(self, args): - """ - Parse arguments and capture exit_code and stderr. - """ - stdout = StringIO() - stderr = StringIO() - prev_stdout = sys.stdout - prev_stderr = sys.stderr - try: - sys.stdout = stdout - sys.stderr = stderr - self.parser.parse_args(args) - raise AssertionError( # pragma: no cover - 'Failure not triggered when parsing the arguments.') - except SystemExit as error: - return error.code, stderr.getvalue() - finally: - # We don't revert to sys.__stdout__ and the test runner might - # have injected its logger. - sys.stdout = prev_stdout - sys.stderr = prev_stderr - - def assertNamespaceEqual(self, expected, actual): - """ - Check that namespaces are equal. - """ - namespace = Namespace(**expected) - self.assertEqual(namespace, actual) diff --git a/chevah/keycert/tests/keydata.py b/chevah/keycert/tests/keydata.py deleted file mode 100644 index cb2ca06..0000000 --- a/chevah/keycert/tests/keydata.py +++ /dev/null @@ -1,632 +0,0 @@ -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. -""" -Data used by test_keys as well as others. -""" -from __future__ import absolute_import, division, unicode_literals - -from base64 import decodestring as decodebytes - -RSAData = { - 'n': long('269413617238113438198661010376758399219880277968382122687862697' - '296942471209955603071120391975773283844560230371884389952067978' - '789684135947515341209478065209455427327369102356204259106807047' - '964139525310539133073743116175821417513079706301100600025815509' - '786721808719302671068052414466483676821987505720384645561708425' - '794379383191274856941628512616355437197560712892001107828247792' - '561858327085521991407807015047750218508971611590850575870321007' - '991909043252470730134547038841839367764074379439843108550888709' - '430958143271417044750314742880542002948053835745429446485015316' - '60749404403945254975473896534482849256068133525751'), - 'e': long(65537), - 'd': long('420335724286999695680502438485489819800002417295071059780489811' - '840828351636754206234982682752076205397047218449504537476523960' - '987613148307573487322720481066677105211155388802079519869249746' - '774085882219244493290663802569201213676433159425782937159766786' - '329742053214957933941260042101377175565683849732354700525628975' - '239000548651346620826136200952740446562751690924335365940810658' - '931238410612521441739702170503547025018016868116037053013935451' - '477930426013703886193016416453215950072147440344656137718959053' - '897268663969428680144841987624962928576808352739627262941675617' - '7724661940425316604626522633351193810751757014073'), - 'p': long('152689878451107675391723141129365667732639179427453246378763774' - '448531436802867910180261906924087589684175595016060014593521649' - '964959248408388984465569934780790357826811592229318702991401054' - '226302790395714901636384511513449977061729214247279176398290513' - '085108930550446985490864812445551198848562639933888780317'), - 'q': long('176444974592327996338888725079951900172097062203378367409936859' - '072670162290963119826394224277287608693818012745872307600855894' - '647300295516866118620024751601329775653542084052616260193174546' - '400544176890518564317596334518015173606460860373958663673307503' - '231977779632583864454001476729233959405710696795574874403'), - 'u': long('936018002388095842969518498561007090965136403384715613439364803' - '229386793506402222847415019772053080458257034241832795210460612' - '924445085372678524176842007912276654532773301546269997020970818' - '155956828553418266110329867222673040098885651348225673298948529' - '93885224775891490070400861134282266967852120152546563278') -} - -DSAData = { - 'g': long("10253261326864117157640690761723586967382334319435778695" - "29171533815411392477819921538350732400350395446211982054" - "96512489289702949127531056893725702005035043292195216541" - "11525058911428414042792836395195432445511200566318251789" - "10575695836669396181746841141924498545494149998282951407" - "18645344764026044855941864175"), - 'p': long("10292031726231756443208850082191198787792966516790381991" - "77502076899763751166291092085666022362525614129374702633" - "26262930887668422949051881895212412718444016917144560705" - "45675251775747156453237145919794089496168502517202869160" - "78674893099371444940800865897607102159386345313384716752" - "18590012064772045092956919481"), - 'q': long(1393384845225358996250882900535419012502712821577), - 'x': long(1220877188542930584999385210465204342686893855021), - 'y': long("14604423062661947579790240720337570315008549983452208015" - "39426429789435409684914513123700756086453120500041882809" - "10283610277194188071619191739512379408443695946763554493" - "86398594314468629823767964702559709430618263927529765769" - "10270265745700231533660131769648708944711006508965764877" - "684264272082256183140297951") -} - -ECDatanistp256 = { - 'x': long('762825130203920963171185031449647317742997734817505505433829043' - '45687059013883'), - 'y': long('815431978646028526322656647694416475343443758943143196810611371' - '59310646683104'), - 'privateValue': long('3463874347721034170096400845565569825355565567882605' - '9678074967909361042656500'), - 'curve': b'ecdsa-sha2-nistp256' -} - -ECDatanistp384 = { - 'privateValue': long('280814107134858470598753916394807521398239633534281633982576099083' - '35787109896602102090002196616273211495718603965098'), - 'x': long('10036914308591746758780165503819213553101287571902957054148542' - '504671046744460374996612408381962208627004841444205030'), - 'y': long('17337335659928075994560513699823544906448896792102247714689323' - '575406618073069185107088229463828921069465902299522926'), - 'curve': b'ecdsa-sha2-nistp384' -} - -ECDatanistp521 = { - 'x': long('12944742826257420846659527752683763193401384271391513286022917' - '29910013082920512632908350502247952686156279140016049549948975' - '670668730618745449113644014505462'), - 'y': long('10784108810271976186737587749436295782985563640368689081052886' - '16296815984553198866894145509329328086635278430266482551941240' - '591605833440825557820439734509311'), - 'privateValue': long('662751235215460886290293902658128847495347691199214706697089140769' - '672273950767961331442265530524063943548846724348048614239791498442' - '5997823106818915698960565'), - 'curve': b'ecdsa-sha2-nistp521' -} - -Ed25519Data = { - 'a': (b'\xf1\x16\xd1\x15J\x1e\x15\x0e\x19^\x19F\xb5\xf2D\r\xb2R\xa0\xae*k' - b'#\x13sE\xfd@\xd9W{\x8b'), - 'k': (b'7/%\xda\x8d\xd4\xa8\x9ax|a\xf0\x98\x01\xc6\xf4^mg\x05i17Li\r\x05U' - b'\xbb\xc9DX') -} - -privateECDSA_openssh521 = b"""-----BEGIN EC PRIVATE KEY----- -MIHcAgEBBEIAjn0lSVF6QweS4bjOGP9RHwqxUiTastSE0MVuLtFvkxygZqQ712oZ -ewMvqKkxthMQgxzSpGtRBcmkL7RqZ94+18qgBwYFK4EEACOhgYkDgYYABAFpX/6B -mxxglwD+VpEvw0hcyxVzLxNnMGzxZGF7xmNj8nlF7M+TQctdlR2Xv/J+AgIeVGmB -j2p84bkV9jBzrUNJEACsJjttZw8NbUrhxjkLT/3rMNtuwjE4vLja0P7DMTE0EV8X -f09ETdku/z/1tOSSrSvRwmUcM9nQUJtHHAZlr5Q0fw== ------END EC PRIVATE KEY-----""" - -# New format introduced in OpenSSH 6.5 -privateECDSA_openssh521_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS -1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBaV/+gZscYJcA/laRL8NIXMsVcy8T -ZzBs8WRhe8ZjY/J5RezPk0HLXZUdl7/yfgICHlRpgY9qfOG5FfYwc61DSRAArCY7bWcPDW -1K4cY5C0/96zDbbsIxOLy42tD+wzExNBFfF39PRE3ZLv8/9bTkkq0r0cJlHDPZ0FCbRxwG -Za+UNH8AAAEAeRISlnkSEpYAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ -AAAIUEAWlf/oGbHGCXAP5WkS/DSFzLFXMvE2cwbPFkYXvGY2PyeUXsz5NBy12VHZe/8n4C -Ah5UaYGPanzhuRX2MHOtQ0kQAKwmO21nDw1tSuHGOQtP/esw227CMTi8uNrQ/sMxMTQRXx -d/T0RN2S7/P/W05JKtK9HCZRwz2dBQm0ccBmWvlDR/AAAAQgCOfSVJUXpDB5LhuM4Y/1Ef -CrFSJNqy1ITQxW4u0W+THKBmpDvXahl7Ay+oqTG2ExCDHNKka1EFyaQvtGpn3j7XygAAAA -ABAg== ------END OPENSSH PRIVATE KEY----- -""" - -publicECDSA_openssh521 = ( - b"ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACF" - b"BAFpX/6BmxxglwD+VpEvw0hcyxVzLxNnMGzxZGF7xmNj8nlF7M+TQctdlR2Xv/J+AgIeVGmB" - b"j2p84bkV9jBzrUNJEACsJjttZw8NbUrhxjkLT/3rMNtuwjE4vLja0P7DMTE0EV8Xf09ETdku" - b"/z/1tOSSrSvRwmUcM9nQUJtHHAZlr5Q0fw== comment" -) - -privateECDSA_openssh384 = b"""-----BEGIN EC PRIVATE KEY----- -MIGkAgEBBDAtAi7I8j73WCX20qUM5hhHwHuFzYWYYILs2Sh8UZ+awNkARZ/Fu2LU -LLl5RtOQpbWgBwYFK4EEACKhZANiAATU17sA9P5FRwSknKcFsjjsk0+E3CeXPYX0 -Tk/M0HK3PpWQWgrO8JdRHP9eFE9O/23P8BumwFt7F/AvPlCzVd35VfraFT0o4cCW -G0RqpQ+np31aKmeJshkcYALEchnU+tQ= ------END EC PRIVATE KEY-----""" - -# New format introduced in OpenSSH 6.5 -privateECDSA_openssh384_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS -1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTU17sA9P5FRwSknKcFsjjsk0+E3CeX -PYX0Tk/M0HK3PpWQWgrO8JdRHP9eFE9O/23P8BumwFt7F/AvPlCzVd35VfraFT0o4cCWG0 -RqpQ+np31aKmeJshkcYALEchnU+tQAAADIiktpWIpLaVgAAAATZWNkc2Etc2hhMi1uaXN0 -cDM4NAAAAAhuaXN0cDM4NAAAAGEE1Ne7APT+RUcEpJynBbI47JNPhNwnlz2F9E5PzNBytz -6VkFoKzvCXURz/XhRPTv9tz/AbpsBbexfwLz5Qs1Xd+VX62hU9KOHAlhtEaqUPp6d9Wipn -ibIZHGACxHIZ1PrUAAAAMC0CLsjyPvdYJfbSpQzmGEfAe4XNhZhgguzZKHxRn5rA2QBFn8 -W7YtQsuXlG05CltQAAAAA= ------END OPENSSH PRIVATE KEY----- -""" - -publicECDSA_openssh384 = ( - b"ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABh" - b"BNTXuwD0/kVHBKScpwWyOOyTT4TcJ5c9hfROT8zQcrc+lZBaCs7wl1Ec/14UT07/bc/wG6bA" - b"W3sX8C8+ULNV3flV+toVPSjhwJYbRGqlD6enfVoqZ4myGRxgAsRyGdT61A== comment" -) - -publicECDSA_openssh = ( - b"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABB" - b"BKimX1DZ7+Qj0SpfePMbo1pb6yGkAb5l7duC1l855yD7tEfQfqk7bc7v46We1hLMyz6ObUBY" - b"gkN/34n42F4vpeA= comment" -) - -privateECDSA_openssh = b"""-----BEGIN EC PRIVATE KEY----- -MHcCAQEEIEyU1YOT2JxxofwbJXIjGftdNcJK55aQdNrhIt2xYQz0oAoGCCqGSM49 -AwEHoUQDQgAEqKZfUNnv5CPRKl948xujWlvrIaQBvmXt24LWXznnIPu0R9B+qTtt -zu/jpZ7WEszLPo5tQFiCQ3/fifjYXi+l4A== ------END EC PRIVATE KEY-----""" - -# New format introduced in OpenSSH 6.5 -privateECDSA_openssh_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS -1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSopl9Q2e/kI9EqX3jzG6NaW+shpAG+ -Ze3bgtZfOecg+7RH0H6pO23O7+OlntYSzMs+jm1AWIJDf9+J+NheL6XgAAAAmCKU4hcilO -IXAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKimX1DZ7+Qj0Spf -ePMbo1pb6yGkAb5l7duC1l855yD7tEfQfqk7bc7v46We1hLMyz6ObUBYgkN/34n42F4vpe -AAAAAgTJTVg5PYnHGh/BslciMZ+101wkrnlpB02uEi3bFhDPQAAAAA ------END OPENSSH PRIVATE KEY----- -""" - -publicEd25519_openssh = ( - b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPEW0RVKHhUOGV4ZRrXyRA2yUqCuKmsjE3NF" - b"/UDZV3uL comment" -) - -# OpenSSH has only ever supported the "new" (v1) format for Ed25519. -privateEd25519_openssh_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACDxFtEVSh4VDhleGUa18kQNslKgriprIxNzRf1A2Vd7iwAAAJA61eMLOtXj -CwAAAAtzc2gtZWQyNTUxOQAAACDxFtEVSh4VDhleGUa18kQNslKgriprIxNzRf1A2Vd7iw -AAAEA3LyXajdSomnh8YfCYAcb0Xm1nBWkxN0xpDQVVu8lEWPEW0RVKHhUOGV4ZRrXyRA2y -UqCuKmsjE3NF/UDZV3uLAAAAB2NvbW1lbnQBAgMEBQY= ------END OPENSSH PRIVATE KEY-----""" - -publicRSA_openssh = ( - b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDVaqx4I9bWG+wloVDEd2NQhEUBVUIUKirg" - b"0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHjVyqgYwBGTJAkMgUyP" - b"95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auIJNm/9NNN9b0b/h9qp" - b"KSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY6RKXCpCnd1bqcPUWz" - b"xiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvbw7KW4CC1ffdOgTtDc1" - b"foNfICZgptyti8ZseZj3 comment" -) - -privateRSA_openssh = b'''-----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEA1WqseCPW1hvsJaFQxHdjUIRFAVVCFCoq4NBg7tTpo61K+jkG -XoRVdV8ANr9vqio/gyY3wWkuW/3w89J91pjNOkB41cqoGMARkyQJDIFMj/ec7RMW -aqQE6Ul3w+RVZLN5aJ4sCOus6AQtIXcFp47vUzANpeW7PWriCTZv/TTTfW9G/4fa -qSknqv+t9YXmPhq4eh1KserAWvcw3x/CpOTvP5FJlkDVGXctN8Ne7J2mOkSlwqQp -3dW6nD1Fs8YsGGTVuj3fq3/NQqyn8RgLoFgVYgukKm5Dw+QEnzWjR45G7TOlZb28 -OyluAgtX33ToE7Q3NX6DXyAmYKbcrYvGbHmY9wIDAQABAoIBACFMCGaiKNW0+44P -chuFCQC58k438BxXS+NRf54jp+Q6mFUb6ot6mB682Lqx+YkSGGCs6MwLTglaQGq6 -L5n4syRghLnOaZWa+eL8H1FNJxXbKyet77RprL59EOuGR3BztACHlRU7N/nnFOeA -u2geG+bdu3NjuWfmsid/z88wm8KY/dkYNi82LvE9gXqf4QMtR9s0UWI53U/prKiL -2dbzhMQXuXGdBghCeE27xSr0w1jNVSvtvjNfBOp75gQkY/It1z0bbNWcY0MvkoiN -Pm7aGDfYDyVniR25RjReyc7Ei+2SWjMHD9+GCPmS6dvrOAg2yc3NCgFIWzk+esrG -gKnc1DkCgYEA2XAG2OK81HiRUJTUwRuJOGxGZFpRoJoHPUiPA1HMaxKOfRqxZedx -dTngMgV1jRhMr5OxSbFmX3hietEMyuZNQ7Oc9Gt95gyY3M8hYo7VLhLeBK7XJG6D -MaIVokQ9IqliJiK5su1UCp0Ig6cHDf8ZGI7Yqx3aSJwxaBGhZm3j2B0CgYEA+0QX -i6Q2vh43Haf2YWwExKrdeD4HjB4zAq4DFIeDeuWefQhnqPKqvxJwz3Kpp8cLHYjV -IP2cY8pHMFVOi8TP9H8WpJISdKEJwsRunIwz76Xl9+ArrU9cEaoahDdb/Xrqw818 -sMjkH1Rjtcev3/QJp/zHJfxc6ZHXksWYHlbTsSMCgYBRr+mSn5QLSoRlPpSzO5IQ -tXS4jMnvyQ4BMvovaBKhAyauz1FoFEwmmyikAjMIX+GncJgBNHleUo7Ezza8H0tV -rOvBU4TH4WGoStSi/0ANgB8SqVDAKhh1lAwGmxZQqEvsQc177/dLyXUCaMSYuIaI -GFpD5wIzlyJkk4MMRSp87QKBgGlmN8ZA3SHFBPOwuD5HlHx2/C3rPzk8lcNDAVHE -Qpfz6Bakxu7s1EkQUDgE7jvN19DMzDJpkAegG1qf/jHNHjp+cR4ZlBpOTwzfX1LV -0Rdu7NectlWd244hX7wkiLb8r6vw76QssNyfhrADEriL4t0PwO4jPUpQ/i+4KUZY -v7YnAoGAZhb5IDTQVCW8YTGsgvvvnDUefkpVAmiVDQqTvh6/4UD6kKdUcDHpePzg -Zrcid5rr3dXSMEbK4tdeQZvPtUg1Uaol3N7bNClIIdvWdPx+5S9T95wJcLnkoHam -rXp0IjScTxfLP+Cq5V6lJ94/pX8Ppoj1FdZfNxeS4NYFSRA7kvY= ------END RSA PRIVATE KEY-----''' - -# Some versions of OpenSSH generate these (slightly different keys): the PKCS#1 -# structure is wrapped in an extra ASN.1 SEQUENCE and there's an empty SEQUENCE -# following it. It is not any standard key format and was probably a bug in -# OpenSSH at some point. -privateRSA_openssh_alternate = b"""-----BEGIN RSA PRIVATE KEY----- -MIIEqTCCBKMCAQACggEBANVqrHgj1tYb7CWhUMR3Y1CERQFVQhQqKuDQYO7U6aOtSvo5Bl6EVXVf -ADa/b6oqP4MmN8FpLlv98PPSfdaYzTpAeNXKqBjAEZMkCQyBTI/3nO0TFmqkBOlJd8PkVWSzeWie -LAjrrOgELSF3BaeO71MwDaXluz1q4gk2b/00031vRv+H2qkpJ6r/rfWF5j4auHodSrHqwFr3MN8f -wqTk7z+RSZZA1Rl3LTfDXuydpjpEpcKkKd3Vupw9RbPGLBhk1bo936t/zUKsp/EYC6BYFWILpCpu -Q8PkBJ81o0eORu0zpWW9vDspbgILV9906BO0NzV+g18gJmCm3K2Lxmx5mPcCAwEAAQKCAQAhTAhm -oijVtPuOD3IbhQkAufJON/AcV0vjUX+eI6fkOphVG+qLepgevNi6sfmJEhhgrOjMC04JWkBqui+Z -+LMkYIS5zmmVmvni/B9RTScV2ysnre+0aay+fRDrhkdwc7QAh5UVOzf55xTngLtoHhvm3btzY7ln -5rInf8/PMJvCmP3ZGDYvNi7xPYF6n+EDLUfbNFFiOd1P6ayoi9nW84TEF7lxnQYIQnhNu8Uq9MNY -zVUr7b4zXwTqe+YEJGPyLdc9G2zVnGNDL5KIjT5u2hg32A8lZ4kduUY0XsnOxIvtklozBw/fhgj5 -kunb6zgINsnNzQoBSFs5PnrKxoCp3NQ5AoGBANlwBtjivNR4kVCU1MEbiThsRmRaUaCaBz1IjwNR -zGsSjn0asWXncXU54DIFdY0YTK+TsUmxZl94YnrRDMrmTUOznPRrfeYMmNzPIWKO1S4S3gSu1yRu -gzGiFaJEPSKpYiYiubLtVAqdCIOnBw3/GRiO2Ksd2kicMWgRoWZt49gdAoGBAPtEF4ukNr4eNx2n -9mFsBMSq3Xg+B4weMwKuAxSHg3rlnn0IZ6jyqr8ScM9yqafHCx2I1SD9nGPKRzBVTovEz/R/FqSS -EnShCcLEbpyMM++l5ffgK61PXBGqGoQ3W/166sPNfLDI5B9UY7XHr9/0Caf8xyX8XOmR15LFmB5W -07EjAoGAUa/pkp+UC0qEZT6UszuSELV0uIzJ78kOATL6L2gSoQMmrs9RaBRMJpsopAIzCF/hp3CY -ATR5XlKOxM82vB9LVazrwVOEx+FhqErUov9ADYAfEqlQwCoYdZQMBpsWUKhL7EHNe+/3S8l1AmjE -mLiGiBhaQ+cCM5ciZJODDEUqfO0CgYBpZjfGQN0hxQTzsLg+R5R8dvwt6z85PJXDQwFRxEKX8+gW -pMbu7NRJEFA4BO47zdfQzMwyaZAHoBtan/4xzR46fnEeGZQaTk8M319S1dEXbuzXnLZVnduOIV+8 -JIi2/K+r8O+kLLDcn4awAxK4i+LdD8DuIz1KUP4vuClGWL+2JwKBgQCFSxt6mxIQN54frV7a/saW -/t81a7k04haXkiYJvb1wIAOnNb0tG6DSB0cr1N6oqAcHG7gEIKcnQTxsOTnpQc7nFx3RTFy8PdIm -Jv5q1v1Icq5G+nvD0xlgRB2lE6eA9WMp1HpdBgcWXfaLPctkOuKEWk2MBi0tnRzrg0x4PXlUzjAA ------END RSA PRIVATE KEY-----""" - -# New format introduced in OpenSSH 6.5 -privateRSA_openssh_new = b'''-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn -NhAAAAAwEAAQAAAQEA1WqseCPW1hvsJaFQxHdjUIRFAVVCFCoq4NBg7tTpo61K+jkGXoRV -dV8ANr9vqio/gyY3wWkuW/3w89J91pjNOkB41cqoGMARkyQJDIFMj/ec7RMWaqQE6Ul3w+ -RVZLN5aJ4sCOus6AQtIXcFp47vUzANpeW7PWriCTZv/TTTfW9G/4faqSknqv+t9YXmPhq4 -eh1KserAWvcw3x/CpOTvP5FJlkDVGXctN8Ne7J2mOkSlwqQp3dW6nD1Fs8YsGGTVuj3fq3 -/NQqyn8RgLoFgVYgukKm5Dw+QEnzWjR45G7TOlZb28OyluAgtX33ToE7Q3NX6DXyAmYKbc -rYvGbHmY9wAAA7gXkBoMF5AaDAAAAAdzc2gtcnNhAAABAQDVaqx4I9bWG+wloVDEd2NQhE -UBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHjVyqgY -wBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auIJNm -/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY6 -RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvb -w7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3AAAAAwEAAQAAAQAhTAhmoijVtPuOD3Ib -hQkAufJON/AcV0vjUX+eI6fkOphVG+qLepgevNi6sfmJEhhgrOjMC04JWkBqui+Z+LMkYI -S5zmmVmvni/B9RTScV2ysnre+0aay+fRDrhkdwc7QAh5UVOzf55xTngLtoHhvm3btzY7ln -5rInf8/PMJvCmP3ZGDYvNi7xPYF6n+EDLUfbNFFiOd1P6ayoi9nW84TEF7lxnQYIQnhNu8 -Uq9MNYzVUr7b4zXwTqe+YEJGPyLdc9G2zVnGNDL5KIjT5u2hg32A8lZ4kduUY0XsnOxIvt -klozBw/fhgj5kunb6zgINsnNzQoBSFs5PnrKxoCp3NQ5AAAAgQCFSxt6mxIQN54frV7a/s -aW/t81a7k04haXkiYJvb1wIAOnNb0tG6DSB0cr1N6oqAcHG7gEIKcnQTxsOTnpQc7nFx3R -TFy8PdImJv5q1v1Icq5G+nvD0xlgRB2lE6eA9WMp1HpdBgcWXfaLPctkOuKEWk2MBi0tnR -zrg0x4PXlUzgAAAIEA2XAG2OK81HiRUJTUwRuJOGxGZFpRoJoHPUiPA1HMaxKOfRqxZedx -dTngMgV1jRhMr5OxSbFmX3hietEMyuZNQ7Oc9Gt95gyY3M8hYo7VLhLeBK7XJG6DMaIVok -Q9IqliJiK5su1UCp0Ig6cHDf8ZGI7Yqx3aSJwxaBGhZm3j2B0AAACBAPtEF4ukNr4eNx2n -9mFsBMSq3Xg+B4weMwKuAxSHg3rlnn0IZ6jyqr8ScM9yqafHCx2I1SD9nGPKRzBVTovEz/ -R/FqSSEnShCcLEbpyMM++l5ffgK61PXBGqGoQ3W/166sPNfLDI5B9UY7XHr9/0Caf8xyX8 -XOmR15LFmB5W07EjAAAAAAEC ------END OPENSSH PRIVATE KEY----- -''' - -# Encrypted with the passphrase 'encrypted' -privateRSA_openssh_encrypted = b"""-----BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,FFFFFFFFFFFFFFFF - -p2A1YsHLXkpMVcsEqhh/nCYb5AqL0uMzfEIqc8hpZ/Ub8PtLsypilMkqzYTnZIGS -ouyPjU/WgtR4VaDnutPWdgYaKdixSEmGhKghCtXFySZqCTJ4O8NCczsktYjUK3D4 -Jtl90zL6O81WBY6xP76PBQo9lrI/heAetATeyqutc18bwQIGU+gKk32qvfo15DfS -VYiY0Ds4D7F7fd9pz+f5+UbFUCgU+tfDvBrqodYrUgmH7jKoW/CRDCHHyeEIZDbF -mcMwdcKOyw1sRLaPdihRSVx3kOMvIotHKVTkIDMp+0RTNeXzQnp5U2qzsxzTcG/M -UyJN38XXkuvq5VMj2zmmjHzx34w3NK3ZxpZcoaFUqUBlNp2C8hkCLrAa/DWobKqN -5xA1ElrQvli9XXkT/RIuy4Gc10bbGEoJjuxNRibtSxxWd5Bd1E40ocOd4l1ebI8+ -w69XvMTnsmHvkBEADGF2zfRszKnMelg+W5NER1UDuNT03i+1cuhp+2AZg8z7niTO -M17XP3ScGVxrQAEYgtxPrPeIpFJvOx2j5Yt78U9Y2WlaAG6DrubbYv2RsMIibhOG -yk139vMdD8FwCey6yMkkhFAJwnBtC22MAWgjmC5c6AF3SRQSjjQXepPsJcLgpOjy -YwjhnL8w56x9kVDUNPw9A9Cqgxo2sty34ATnKrh4h59PsP83LOL6OC5WjbASgZRd -OIBD8RloQPISo+RUF7X0i4kdaHVNPlR0KyapR+3M5BwhQuvEO99IArDV2LNKGzfc -W4ssugm8iyAJlmwmb2yRXIDHXabInWY7XCdGk8J2qPFbDTvnPbiagJBimjVjgpWw -tV3sVlJYqmOqmCDP78J6he04l0vaHtiOWTDEmNCrK7oFMXIIp3XWjOZGPSOJFdPs -6Go3YB+EGWfOQxqkFM28gcqmYfVPF2sa1FbZLz0ffO11Ma/rliZxZu7WdrAXe/tc -BgIQ8etp2PwAK4jCwwVwjIO8FzqQGpS23Y9NY3rfi97ckgYXKESFtXPsMMA+drZd -ThbXvccfh4EPmaqQXKf4WghHiVJ+/yuY1kUIDEl/O0jRZWT7STgBim/Aha1m6qRs -zl1H7hkDbU4solb1GM5oPzbgGTzyBc+z0XxM9iFRM+fMzPB8+yYHTr4kPbVmKBjy -SCovjQQVsHE4YeUGTq6k/NF5cVIRKTW/RlHvzxsky1Zj31MC736jrxGw4KG7VSLZ -fP6F5jj+mXwS7m0v5to42JBZmRJdKUD88QaGE3ncyQ4yleW5bn9Lf9SuzQg1Dhao -3rSA1RuexsHlIAHvGxx/17X+pyygl8DJbt6TBfbLQk9wc707DJTfh5M/bnk9wwIX -l/Hsa1WtylAMW/2MzgiVy83MbYz4+Ss6GQ5W66okWji+NxrnrYEy6q+WgVQanp7X -D+D7oKykqE1Cdvvulvtfl5fh8wlAs8mrUnKPBBUru348u++2lfacLkxRXyT1ooqY -uSNE5nlwFt08N2Ou/bl7yq6QNRMYrRkn+UEfHWCNYDoGMHln2/i6Z1RapQzNarik -tJf7radBz5nBwBjP08YAEACNSQvpsUgdqiuYjLwX7efFXQva2RzqaQ== ------END RSA PRIVATE KEY-----""" - -# Encrypted with the passphrase 'encrypted', and using the new format -# introduced in OpenSSH 6.5 -privateRSA_openssh_encrypted_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABD0f9WAof -DTbmwztb8pdrSeAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDVaqx4I9bW -G+wloVDEd2NQhEUBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n -3WmM06QHjVyqgYwBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9T -MA2l5bs9auIJNm/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQN -UZdy03w17snaY6RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASf -NaNHjkbtM6Vlvbw7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3AAADwPQaac8s1xX3af -hQTQexj0vEAWDQsLYzDHN9G7W+UP5WHUu7igeu2GqAC/TOnjUXDP73I+EN3n7T3JFeDRfs -U1Z6Zqb0NKHSRVYwDIdIi8qVohFv85g6+xQ01OpaoOzz+vI34OUvCRHQGTgR6L9fQShZyC -McopYMYfbIse6KcqkfxX3KSdG1Pao6Njx/ShFRbgvmALpR/z0EaGCzHCDxpfUyAdnxm621 -Jzaf+LverWdN7sfrfMptaS9//9iJb70sL67K+YIB64qhDnA/w9UOQfXGQFL+AEtdM0BPv8 -thP1bs7T0yucBl+ZXdrDKVLZfaS3S/w85Jlgfu+a1DG73pOBOuag435iEJ9EnspjXiiydx -GrfSRk2C+/c4fBDZVGFscK5bfQuUUZyU1qOagekxX7WLHFKk9xajnud+nrAN070SeNwlX8 -FZ2CI4KGlQfDvVUpKanYn8Kkj3fZ+YBGyx4M+19clF65FKSM0x1Rrh5tAmNT/SNDbSc28m -ASxrBhztzxUFTrIn3tp+uqkJniFLmFsUtiAUmj8fNyE9blykU7dqq+CqpLA872nQ9bOHHA -JsS1oBYmQ0n6AJz8WrYMdcepqWVld6Q8QSD1zdrY/sAWUovuBA1s4oIEXZhpXSS4ZJiMfh -PVktKBwj5bmoG/mmwYLbo0JHntK8N3TGTzTGLq5TpSBBdVvWSWo7tnfEkrFObmhi1uJSrQ -3zfPVP6BguboxBv+oxhaUBK8UOANe6ZwM4vfiu+QN+sZqWymHIfAktz7eWzwlToe4cKpdG -Uv+e3/7Lo2dyMl3nke5HsSUrlsMGPREuGkBih8+o85ii6D+cuCiVtus3f5c78Cir80zLIr -Z0wWvEAjciEvml00DWaA+JIaOrWwvXySaOzFGpCqC9SQjao379bvn9P3b7kVZsy6zBfHqm -bNEJUOuhBZaY8Okz36chh1xqh4sz7m3nsZ3GYGcvM+3mvRY72QnqsQEG0Sp1XYIn2bHa29 -tqp7CG9X8J6dqMcPeoPRDWIX9gw7EPl/M0LP6xgewGJ9bgxwle6Mnr9kNITIswjAJqrLec -zx7dfixjAPc42ADqrw/tEdFQcSqxigcfJNKO1LbDBjh+Hk/cSBou2PoxbIcl0qfQfbGcqI -Dbpd695IEuiW9pYR22txNoIi+7cbMsuFHxQ/OqbrX/jCsprGNNJLAjgGsVEI1JnHWDH0db -3UbqbOHAeY3ufoYXNY1utVOIACpW3r9wBw3FjRi04d70VcKr16OXvOAHGN2G++Y+kMya84 -Hl/Kt/gA== ------END OPENSSH PRIVATE KEY----- -""" - -# Encrypted with the passphrase 'testxp'. NB: this key was generated by -# OpenSSH, so it doesn't use the same key data as the other keys here. -privateRSA_openssh_encrypted_aes = b"""-----BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: AES-128-CBC,0673309A6ACCAB4B77DEE1C1E536AC26 - -4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n -T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H -g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB -sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5 -9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV -gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW -0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE -vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS -hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk -2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf -qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk -4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY -EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n -8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0 -fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P -V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+ -0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5 -xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI -dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup -VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk -gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c -8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw -SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 -CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE -xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""" - -publicRSA_lsh = ( - b'{KDEwOnB1YmxpYy1rZXkoMTQ6cnNhLXBrY3MxLXNoYTEoMTpuMjU3OgDVaqx4I9bWG+wloVD' - b'Ed2NQhEUBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHj' - b'VyqgYwBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auI' - b'JNm/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY' - b'6RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvbw' - b'7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3KSgxOmUzOgEAASkpKQ==}' -) - -privateRSA_lsh = ( - b"(11:private-key(9:rsa-pkcs1(1:n257:\x00\xd5j\xacx#\xd6\xd6\x1b\xec%\xa1P" - b"\xc4wcP\x84E\x01UB\x14**\xe0\xd0`\xee\xd4\xe9\xa3\xadJ\xfa9\x06^\x84Uu_" - b"\x006\xbfo\xaa*?\x83&7\xc1i.[\xfd\xf0\xf3\xd2}\xd6\x98\xcd:@x\xd5\xca" - b"\xa8\x18\xc0\x11\x93$\t\x0c\x81L\x8f\xf7\x9c\xed\x13\x16j\xa4\x04\xe9Iw" - b"\xc3\xe4Ud\xb3yh\x9e,\x08\xeb\xac\xe8\x04-!w\x05\xa7\x8e\xefS0\r\xa5\xe5" - b"\xbb=j\xe2\t6o\xfd4\xd3}oF\xff\x87\xda\xa9)'\xaa\xff\xad\xf5\x85\xe6>" - b"\x1a\xb8z\x1dJ\xb1\xea\xc0Z\xf70\xdf\x1f\xc2\xa4\xe4\xef?\x91I\x96@\xd5" - b"\x19w-7\xc3^\xec\x9d\xa6:D\xa5\xc2\xa4)\xdd\xd5\xba\x9c=E\xb3\xc6,\x18d" - b"\xd5\xba=\xdf\xab\x7f\xcdB\xac\xa7\xf1\x18\x0b\xa0X\x15b\x0b\xa4*nC\xc3" - b"\xe4\x04\x9f5\xa3G\x8eF\xed3\xa5e\xbd\xbc;)n\x02\x0bW\xdft\xe8\x13\xb475" - b"~\x83_ &`\xa6\xdc\xad\x8b\xc6ly\x98\xf7)(1:e3:\x01\x00\x01)(1:d256:!L" - b"\x08f\xa2(\xd5\xb4\xfb\x8e\x0fr\x1b\x85\t\x00\xb9\xf2N7\xf0\x1cWK\xe3Q" - b"\x7f\x9e#\xa7\xe4:\x98U\x1b\xea\x8bz\x98\x1e\xbc\xd8\xba\xb1\xf9\x89\x12" - b"\x18`\xac\xe8\xcc\x0bN\tZ@j\xba/\x99\xf8\xb3$`\x84\xb9\xcei\x95\x9a\xf9" - b"\xe2\xfc\x1fQM'\x15\xdb+'\xad\xef\xb4i\xac\xbe}\x10\xeb\x86Gps\xb4\x00" - b"\x87\x95\x15;7\xf9\xe7\x14\xe7\x80\xbbh\x1e\x1b\xe6\xdd\xbbsc\xb9g\xe6" - b"\xb2'\x7f\xcf\xcf0\x9b\xc2\x98\xfd\xd9\x186/6.\xf1=\x81z\x9f\xe1\x03-G" - b"\xdb4Qb9\xddO\xe9\xac\xa8\x8b\xd9\xd6\xf3\x84\xc4\x17\xb9q\x9d\x06\x08Bx" - b"M\xbb\xc5*\xf4\xc3X\xcdU+\xed\xbe3_\x04\xea{\xe6\x04$c\xf2-\xd7=\x1bl" - b"\xd5\x9ccC/\x92\x88\x8d>n\xda\x187\xd8\x0f%g\x89\x1d\xb9F4^\xc9\xce\xc4" - b"\x8b\xed\x92Z3\x07\x0f\xdf\x86\x08\xf9\x92\xe9\xdb\xeb8\x086\xc9\xcd\xcd" - b"\n\x01H[9>z\xca\xc6\x80\xa9\xdc\xd49)(1:p129:\x00\xfbD\x17\x8b\xa46\xbe" - b"\x1e7\x1d\xa7\xf6al\x04\xc4\xaa\xddx>\x07\x8c\x1e3\x02\xae\x03\x14\x87" - b"\x83z\xe5\x9e}\x08g\xa8\xf2\xaa\xbf\x12p\xcfr\xa9\xa7\xc7\x0b\x1d\x88" - b"\xd5 \xfd\x9cc\xcaG0UN\x8b\xc4\xcf\xf4\x7f\x16\xa4\x92\x12t\xa1\t\xc2" - b"\xc4n\x9c\x8c3\xef\xa5\xe5\xf7\xe0+\xadO\\\x11\xaa\x1a\x847[\xfdz\xea" - b"\xc3\xcd|\xb0\xc8\xe4\x1fTc\xb5\xc7\xaf\xdf\xf4\t\xa7\xfc\xc7%\xfc\\\xe9" - b"\x91\xd7\x92\xc5\x98\x1eV\xd3\xb1#)(1:q129:\x00\xd9p\x06\xd8\xe2\xbc\xd4" - b"x\x91P\x94\xd4\xc1\x1b\x898lFdZQ\xa0\x9a\x07=H\x8f\x03Q\xcck\x12\x8e}" - b"\x1a\xb1e\xe7qu9\xe02\x05u\x8d\x18L\xaf\x93\xb1I\xb1f_xbz\xd1\x0c\xca" - b"\xe6MC\xb3\x9c\xf4k}\xe6\x0c\x98\xdc\xcf!b\x8e\xd5.\x12\xde\x04\xae\xd7$" - b"n\x831\xa2\x15\xa2D=\"\xa9b&\"\xb9\xb2\xedT\n\x9d\x08\x83\xa7\x07\r\xff" - b"\x19\x18\x8e\xd8\xab\x1d\xdaH\x9c1h\x11\xa1fm\xe3\xd8\x1d)(1:a128:if7" - b"\xc6@\xdd!\xc5\x04\xf3\xb0\xb8>G\x94|v\xfc-\xeb?9<\x95\xc3C\x01Q\xc4B" - b"\x97\xf3\xe8\x16\xa4\xc6\xee\xec\xd4I\x10P8\x04\xee;\xcd\xd7\xd0\xcc\xcc" - b"2i\x90\x07\xa0\x1bZ\x9f\xfe1\xcd\x1e:~q\x1e\x19\x94\x1aNO\x0c\xdf_R\xd5" - b"\xd1\x17n\xec\xd7\x9c\xb6U\x9d\xdb\x8e!_\xbc$\x88\xb6\xfc\xaf\xab\xf0" - b"\xef\xa4,\xb0\xdc\x9f\x86\xb0\x03\x12\xb8\x8b\xe2\xdd\x0f\xc0\xee#=JP" - b"\xfe/\xb8)FX\xbf\xb6')(1:b128:Q\xaf\xe9\x92\x9f\x94\x0bJ\x84e>\x94\xb3;" - b"\x92\x10\xb5t\xb8\x8c\xc9\xef\xc9\x0e\x012\xfa/h\x12\xa1\x03&\xae\xcfQh" - b"\x14L&\x9b(\xa4\x023\x08_\xe1\xa7p\x98\x014y^R\x8e\xc4\xcf6\xbc\x1fKU" - b"\xac\xeb\xc1S\x84\xc7\xe1a\xa8J\xd4\xa2\xff@\r\x80\x1f\x12\xa9P\xc0*\x18" - b"u\x94\x0c\x06\x9b\x16P\xa8K\xecA\xcd{\xef\xf7K\xc9u\x02h\xc4\x98\xb8\x86" - b"\x88\x18ZC\xe7\x023\x97\"d\x93\x83\x0cE*|\xed)(1:c128:f\x16\xf9 4\xd0T%" - b"\xbca1\xac\x82\xfb\xef\x9c5\x1e~JU\x02h\x95\r\n\x93\xbe\x1e\xbf\xe1@\xfa" - b"\x90\xa7Tp1\xe9x\xfc\xe0f\xb7\"w\x9a\xeb\xdd\xd5\xd20F\xca\xe2\xd7^A\x9b" - b"\xcf\xb5H5Q\xaa%\xdc\xde\xdb4)H!\xdb\xd6t\xfc~\xe5/S\xf7\x9c\tp\xb9\xe4" - b"\xa0v\xa6\xadzt\"4\x9cO\x17\xcb?\xe0\xaa\xe5^\xa5\'\xde?\xa5\x7f\x0f\xa6" - b"\x88\xf5\x15\xd6_7\x17\x92\xe0\xd6\x05I\x10;\x92\xf6)))" -) - -privateRSA_agentv3 = ( - b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x03\x01\x00\x01\x00\x00\x01\x00!L" - b"\x08f\xa2(\xd5\xb4\xfb\x8e\x0fr\x1b\x85\t\x00\xb9\xf2N7\xf0\x1cWK\xe3Q" - b"\x7f\x9e#\xa7\xe4:\x98U\x1b\xea\x8bz\x98\x1e\xbc\xd8\xba\xb1\xf9\x89\x12" - b"\x18`\xac\xe8\xcc\x0bN\tZ@j\xba/\x99\xf8\xb3$`\x84\xb9\xcei\x95\x9a\xf9" - b"\xe2\xfc\x1fQM'\x15\xdb+'\xad\xef\xb4i\xac\xbe}\x10\xeb\x86Gps\xb4\x00" - b"\x87\x95\x15;7\xf9\xe7\x14\xe7\x80\xbbh\x1e\x1b\xe6\xdd\xbbsc\xb9g\xe6" - b"\xb2'\x7f\xcf\xcf0\x9b\xc2\x98\xfd\xd9\x186/6.\xf1=\x81z\x9f\xe1\x03-G" - b"\xdb4Qb9\xddO\xe9\xac\xa8\x8b\xd9\xd6\xf3\x84\xc4\x17\xb9q\x9d\x06\x08Bx" - b"M\xbb\xc5*\xf4\xc3X\xcdU+\xed\xbe3_\x04\xea{\xe6\x04$c\xf2-\xd7=\x1bl" - b"\xd5\x9ccC/\x92\x88\x8d>n\xda\x187\xd8\x0f%g\x89\x1d\xb9F4^\xc9\xce\xc4" - b"\x8b\xed\x92Z3\x07\x0f\xdf\x86\x08\xf9\x92\xe9\xdb\xeb8\x086\xc9\xcd\xcd" - b"\n\x01H[9>z\xca\xc6\x80\xa9\xdc\xd49\x00\x00\x01\x01\x00\xd5j\xacx#\xd6" - b"\xd6\x1b\xec%\xa1P\xc4wcP\x84E\x01UB\x14**\xe0\xd0`\xee\xd4\xe9\xa3\xadJ" - b"\xfa9\x06^\x84Uu_\x006\xbfo\xaa*?\x83&7\xc1i.[\xfd\xf0\xf3\xd2}\xd6\x98" - b"\xcd:@x\xd5\xca\xa8\x18\xc0\x11\x93$\t\x0c\x81L\x8f\xf7\x9c\xed\x13\x16j" - b"\xa4\x04\xe9Iw\xc3\xe4Ud\xb3yh\x9e,\x08\xeb\xac\xe8\x04-!w\x05\xa7\x8e" - b"\xefS0\r\xa5\xe5\xbb=j\xe2\t6o\xfd4\xd3}oF\xff\x87\xda\xa9)'\xaa\xff\xad" - b"\xf5\x85\xe6>\x1a\xb8z\x1dJ\xb1\xea\xc0Z\xf70\xdf\x1f\xc2\xa4\xe4\xef?" - b"\x91I\x96@\xd5\x19w-7\xc3^\xec\x9d\xa6:D\xa5\xc2\xa4)\xdd\xd5\xba\x9c=E" - b"\xb3\xc6,\x18d\xd5\xba=\xdf\xab\x7f\xcdB\xac\xa7\xf1\x18\x0b\xa0X\x15b" - b"\x0b\xa4*nC\xc3\xe4\x04\x9f5\xa3G\x8eF\xed3\xa5e\xbd\xbc;)n\x02\x0bW\xdf" - b"t\xe8\x13\xb475~\x83_ &`\xa6\xdc\xad\x8b\xc6ly\x98\xf7\x00\x00\x00\x81" - b"\x00\x85K\x1bz\x9b\x12\x107\x9e\x1f\xad^\xda\xfe\xc6\x96\xfe\xdf5k\xb94" - b"\xe2\x16\x97\x92&\t\xbd\xbdp \x03\xa75\xbd-\x1b\xa0\xd2\x07G+\xd4\xde" - b"\xa8\xa8\x07\x07\x1b\xb8\x04 \xa7'A\x07\x8c\x1e3\x02\xae\x03" - b"\x14\x87\x83z\xe5\x9e}\x08g\xa8\xf2\xaa\xbf\x12p\xcfr\xa9\xa7\xc7\x0b" - b"\x1d\x88\xd5 \xfd\x9cc\xcaG0UN\x8b\xc4\xcf\xf4\x7f\x16\xa4\x92\x12t\xa1" - b"\t\xc2\xc4n\x9c\x8c3\xef\xa5\xe5\xf7\xe0+\xadO\\\x11\xaa\x1a\x847[\xfdz" - b"\xea\xc3\xcd|\xb0\xc8\xe4\x1fTc\xb5\xc7\xaf\xdf\xf4\t\xa7\xfc\xc7%\xfc\\" - b"\xe9\x91\xd7\x92\xc5\x98\x1eV\xd3\xb1#" -) - -publicDSA_openssh = b"""\ -ssh-dss AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9\ -LvFYmFFVMIuHFGlZpIL7sh3IMkqy+cssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+Gu48G\ -+yFuE8l0fVVUivos/MmYVJ66qT99htcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+Sx9a5AAAAFQD0\ -EYmTNaFJ8CS0+vFSF4nYcyEnSQAAAIEAkgLjxHJAE7qFWdTqf7EZngu7jAGmdB9k3YzMHe1ldMxEB\ -7zNw5aOnxjhoYLtiHeoEcOk2XOyvnE+VfhIWwWAdOiKRTEZlmizkvhGbq0DCe2EPMXirjqWACI5nD\ -ioQX1oEMonR8N3AEO5v9SfBqS2Q9R6OBr6lf04RvwpHZ0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQT\ -NEpklRZqeBGo1gotJggNmVaIQNIClGlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2G\ -gdgMQWC7S6WFIXePGGXqNQDdWxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8= \ -comment\ -""" - -privateDSA_openssh = b"""\ ------BEGIN DSA PRIVATE KEY----- -MIIBvAIBAAKBgQCSkDrFREVQ0CKQDRx/iQAMpaPS7xWJhRVTCLhxRpWaSC+7IdyD -JKsvnLLCDTP5Zxw935rAMi5VF2bbejw/S4GUWs7DzoKbbh/hruPBvshbhPJdH1VV -Ir6LPzJmFSeuqk/fYbXGSmra01mZ6VVu4BQAaKsPoXti2dIJHlNPksfWuQIVAPQR -iZM1oUnwJLT68VIXidhzISdJAoGBAJIC48RyQBO6hVnU6n+xGZ4Lu4wBpnQfZN2M -zB3tZXTMRAe8zcOWjp8Y4aGC7Yh3qBHDpNlzsr5xPlX4SFsFgHToikUxGZZos5L4 -Rm6tAwnthDzF4q46lgAiOZw4qEF9aBDKJ0fDdwBDub/UnwaktkPUejga+pX9OEb8 -KR2dFBrvAoGAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQNICl -GlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXeP -GGXqNQDdWxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8CFQDV2gbL -czUdxCus0pfEP1bddaXRLQ== ------END DSA PRIVATE KEY-----\ -""" - -privateDSA_openssh_new = b"""\ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsgAAAAdzc2gtZH -NzAAAAgQCSkDrFREVQ0CKQDRx/iQAMpaPS7xWJhRVTCLhxRpWaSC+7IdyDJKsvnLLCDTP5 -Zxw935rAMi5VF2bbejw/S4GUWs7DzoKbbh/hruPBvshbhPJdH1VVIr6LPzJmFSeuqk/fYb -XGSmra01mZ6VVu4BQAaKsPoXti2dIJHlNPksfWuQAAABUA9BGJkzWhSfAktPrxUheJ2HMh -J0kAAACBAJIC48RyQBO6hVnU6n+xGZ4Lu4wBpnQfZN2MzB3tZXTMRAe8zcOWjp8Y4aGC7Y -h3qBHDpNlzsr5xPlX4SFsFgHToikUxGZZos5L4Rm6tAwnthDzF4q46lgAiOZw4qEF9aBDK -J0fDdwBDub/UnwaktkPUejga+pX9OEb8KR2dFBrvAAAAgAIUacRjCFhMmhIfGJ44ms0EzR -KZJUWangRqNYKLSYIDZlWiEDSApRpS8got+fXnxFLkHGfUl8TOfT/oXnHPxlPxh2pFuWFh -OHT9hoHYDEFgu0ulhSF3jxhl6jUA3VsZV/LpoXp70KmtT5yqxUYQ6ycPGexo3R8X5bMQhJ -lz6CzfAAAB2MVcBjzFXAY8AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9Lv -FYmFFVMIuHFGlZpIL7sh3IMkqy+cssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+ -Gu48G+yFuE8l0fVVUivos/MmYVJ66qT99htcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+S -x9a5AAAAFQD0EYmTNaFJ8CS0+vFSF4nYcyEnSQAAAIEAkgLjxHJAE7qFWdTqf7EZngu7jA -GmdB9k3YzMHe1ldMxEB7zNw5aOnxjhoYLtiHeoEcOk2XOyvnE+VfhIWwWAdOiKRTEZlmiz -kvhGbq0DCe2EPMXirjqWACI5nDioQX1oEMonR8N3AEO5v9SfBqS2Q9R6OBr6lf04RvwpHZ -0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQNIClGlLyCi35 -9efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXePGGXqNQDdWxlX8u -mhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8AAAAVANXaBstzNR3EK6zSl8Q/Vt11 -pdEtAAAAAAE= ------END OPENSSH PRIVATE KEY----- -""" - -publicDSA_lsh = decodebytes(b"""\ -e0tERXdPbkIxWW14cFl5MXJaWGtvTXpwa2MyRW9NVHB3TVRJNU9nQ1NrRHJGUkVWUTBDS1FEUngv -aVFBTXBhUFM3eFdKaFJWVENMaHhScFdhU0MrN0lkeURKS3N2bkxMQ0RUUDVaeHc5MzVyQU1pNVZG -MmJiZWp3L1M0R1VXczdEem9LYmJoL2hydVBCdnNoYmhQSmRIMVZWSXI2TFB6Sm1GU2V1cWsvZlli -WEdTbXJhMDFtWjZWVnU0QlFBYUtzUG9YdGkyZElKSGxOUGtzZld1U2tvTVRweE1qRTZBUFFSaVpN -MW9VbndKTFQ2OFZJWGlkaHpJU2RKS1NneE9tY3hNams2QUpJQzQ4UnlRQk82aFZuVTZuK3hHWjRM -dTR3QnBuUWZaTjJNekIzdFpYVE1SQWU4emNPV2pwOFk0YUdDN1loM3FCSERwTmx6c3I1eFBsWDRT -RnNGZ0hUb2lrVXhHWlpvczVMNFJtNnRBd250aER6RjRxNDZsZ0FpT1p3NHFFRjlhQkRLSjBmRGR3 -QkR1Yi9Vbndha3RrUFVlamdhK3BYOU9FYjhLUjJkRkJydktTZ3hPbmt4TWpnNkFoUnB4R01JV0V5 -YUVoOFluamlhelFUTkVwa2xSWnFlQkdvMWdvdEpnZ05tVmFJUU5JQ2xHbEx5Q2kzNTllZkVVdVFj -WjlTWHhNNTlQK2hlY2MvR1UvR0hha1c1WVdFNGRQMkdnZGdNUVdDN1M2V0ZJWGVQR0dYcU5RRGRX -eGxYOHVtaGVudlFxYTFQbktyRlJoRHJKdzhaN0dqZEh4ZmxzeENFbVhQb0xOOHBLU2s9fQ== -""") - -privateDSA_lsh = decodebytes(b"""\ -KDExOnByaXZhdGUta2V5KDM6ZHNhKDE6cDEyOToAkpA6xURFUNAikA0cf4kADKWj0u8ViYUVUwi4 -cUaVmkgvuyHcgySrL5yywg0z+WccPd+awDIuVRdm23o8P0uBlFrOw86Cm24f4a7jwb7IW4TyXR9V -VSK+iz8yZhUnrqpP32G1xkpq2tNZmelVbuAUAGirD6F7YtnSCR5TT5LH1rkpKDE6cTIxOgD0EYmT -NaFJ8CS0+vFSF4nYcyEnSSkoMTpnMTI5OgCSAuPEckATuoVZ1Op/sRmeC7uMAaZ0H2TdjMwd7WV0 -zEQHvM3Dlo6fGOGhgu2Id6gRw6TZc7K+cT5V+EhbBYB06IpFMRmWaLOS+EZurQMJ7YQ8xeKuOpYA -IjmcOKhBfWgQyidHw3cAQ7m/1J8GpLZD1Ho4GvqV/ThG/CkdnRQa7ykoMTp5MTI4OgIUacRjCFhM -mhIfGJ44ms0EzRKZJUWangRqNYKLSYIDZlWiEDSApRpS8got+fXnxFLkHGfUl8TOfT/oXnHPxlPx -h2pFuWFhOHT9hoHYDEFgu0ulhSF3jxhl6jUA3VsZV/LpoXp70KmtT5yqxUYQ6ycPGexo3R8X5bMQ -hJlz6CzfKSgxOngyMToA1doGy3M1HcQrrNKXxD9W3XWl0S0pKSk= -""") - -privateDSA_agentv3 = decodebytes(b"""\ -AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9LvFYmFFVMIuHFGlZpIL7sh3IMkqy+c -ssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+Gu48G+yFuE8l0fVVUivos/MmYVJ66qT99h -tcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+Sx9a5AAAAFQD0EYmTNaFJ8CS0+vFSF4nYcyEnSQAA -AIEAkgLjxHJAE7qFWdTqf7EZngu7jAGmdB9k3YzMHe1ldMxEB7zNw5aOnxjhoYLtiHeoEcOk2XOy -vnE+VfhIWwWAdOiKRTEZlmizkvhGbq0DCe2EPMXirjqWACI5nDioQX1oEMonR8N3AEO5v9SfBqS2 -Q9R6OBr6lf04RvwpHZ0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQ -NIClGlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXePGGXqNQDd -WxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8AAAAVANXaBstzNR3EK6zSl8Q/Vt11 -pdEt -""") - -# Custom code - -privateRSA_fingerprint_md5 = '85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da' - -RSAData2 = { - 'n': long('106248668575524741116943830949539894737212779118943280948138' - '20729711061576321820845393835692814935201176341295575504152775' - '16685881326038852354459895734875625093273594925884531272867425' - '864910490065695876046999646807138717162833156501'), - 'e': long(35), - 'd': long('667848773903298372735075508825679338348194611604786337388297' - '30301040958479737159599618395783408164121679859572188879144827' - '13602371850869127033494910375212470664166001439410214474266799' - '85974425203903884190893469297150446322896587555'), - 'q': long('3395694744258061291019136154000709371890447462086362702627' - '9704149412726577280741108645721676968699696898960891593323'), - 'p': long('3128922844292337321766351031842562691837301298995834258844' - '4720539204069737532863831050930719431498338835415515173887'), - 'u': long('2777403202132551568802514199893235993376771442611051821485' - '0278129927603609294283482712900532542110958095343012272938') - } - -DSAData2 = { - 'g': long("10253261326864117157640690761723586967382334319435778695" - "29171533815411392477819921538350732400350395446211982054" - "96512489289702949127531056893725702005035043292195216541" - "11525058911428414042792836395195432445511200566318251789" - "10575695836669396181746841141924498545494149998282951407" - "18645344764026044855941864175"), - 'p': long("10292031726231756443208850082191198787792966516790381991" - "77502076899763751166291092085666022362525614129374702633" - "26262930887668422949051881895212412718444016917144560705" - "45675251775747156453237145919794089496168502517202869160" - "78674893099371444940800865897607102159386345313384716752" - "18590012064772045092956919481"), - 'q': long(1393384845225358996250882900535419012502712821577), - 'x': long(1220877188542930584999385210465204342686893855021), - 'y': long("14604423062661947579790240720337570315008549983452208015" - "39426429789435409684914513123700756086453120500041882809" - "10283610277194188071619191739512379408443695946763554493" - "86398594314468629823767964702559709430618263927529765769" - "10270265745700231533660131769648708944711006508965764877" - "684264272082256183140297951") - } \ No newline at end of file diff --git a/chevah/keycert/tests/ssh_common_test_inc.sh b/chevah/keycert/tests/ssh_common_test_inc.sh deleted file mode 100644 index 8ceed02..0000000 --- a/chevah/keycert/tests/ssh_common_test_inc.sh +++ /dev/null @@ -1,18 +0,0 @@ -# Files holding passwords. -# Non-empty passwors MUST be at least 5 characters long. -# (Limitation imposed by ssh-keygen for password-protected PCKS8 keys.) -# Non-empty passwords MUST start with a letter. -# (Limitation imposed by the script testing self-generated keys.) -# Complex passwords must be at least 10 characters long. -# (Limitation imposed by the script testing self-generated keys.) - -> pass_file_empty -echo 'chevah' > pass_file_simple -echo 'V^#~(?)%&\/+-1.,="*`!>|<:$;@N' > pass_file_complex -PASS_TYPES="empty simple complex" - -> comm_file_empty -echo 'chevah' > comm_file_simple -echo ' V^#~(?)%&\/+-1. ,="*`!>|<:$;@N' > comm_file_complex -echo 'âåæāăąǎǟȁȃȧȺαἀащѝѱҡҩժݐሀᠠァぁ妈媽✯➾♤♟⚅🂠⚇𓀀😀☠️👩🏿‍🦽🦺⛈🪐🥂🏁🏴‍☠️🐈‍⬛' > comm_file_unicode -COMM_TYPES="empty simple complex unicode" diff --git a/chevah/keycert/tests/ssh_gen_keys_tests.sh b/chevah/keycert/tests/ssh_gen_keys_tests.sh deleted file mode 100755 index e304cb2..0000000 --- a/chevah/keycert/tests/ssh_gen_keys_tests.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env bash -# -# Generate supported key types and test them with various SSH key tools. - -set -euo pipefail - -# Key types to generate and then test with puttygen, ssh-keygen, ssh-keygen-g3. -# Accepted parameters (one or more): ed25519, ecdsa, rsa, dsa. -# Generating large size RSA and DSA keys takes a lot of CPU time. -KEY_TYPES=$* -if [ -z "$KEY_TYPES" ]; then - # If no parameters given, test all key types. - KEY_TYPES="ed25519 ecdsa rsa dsa" -fi - -KEYCERT_CMD="../build-keycert/bin/python ../keycert-demo.py" -KEYCERT_FORMATS="openssh openssh_v1 putty" - -SUCCESS_FILE="gen_keys_tests_success" -ERROR_FILE="gen_keys_tests_error" - -# Empty the files holding test results, if present. -> $SUCCESS_FILE -> $ERROR_FILE - -# Common routines like setting password files. -source ../chevah/keycert/tests/ssh_common_test_inc.sh - -sort_tests_per_error(){ - local cmd_to_test=$* - local cmd_err_code - - set +e - $cmd_to_test - cmd_err_code=$? - set -e - - # Record last parameter. - if [ $cmd_err_code -eq 0 ]; then - echo "${@: -1}" >> $SUCCESS_FILE - else - echo "${@: -1}" >> $ERROR_FILE - fi -} - -puttygen_tests(){ - local priv_key=$1 - local pub_key=${1}.pub - - sort_tests_per_error puttygen -O fingerprint $pub_key - sort_tests_per_error puttygen -o /dev/null --old-passphrase pass_file_${2} -L $priv_key -} - -sshkeygen_tests(){ - local priv_key=$1 - local pub_key=${1}.pub - - sort_tests_per_error ssh-keygen -l -f $pub_key - if [ $2 = "empty" ]; then - sort_tests_per_error ssh-keygen -y -f $priv_key - else - sort_tests_per_error ssh-keygen -y -P "$(cat pass_file_${2})" -f $priv_key - fi -} - -# First parameter is the key type. -# Second (optional) parameter is the password. MUST start with a letter -keycert_gen_keys(){ - local key_size - local key_format - local key_type=$1 - local key_pass_type - local keycert_opts="ssh-gen-key --key-type $key_type" - - # Remove first parameter, the password should be now first, if existing. - shift - # Check if there is a password to be used. - if [[ "${1:0:1}" =~ [a-zA-Z] ]]; then - # First remaining parameter is the password, as it starts with a non-digit. - keycert_opts="$keycert_opts --key-password ${1} --key-comment ${1}" - # Check password type by password length. - if [ ${#1} -ge 10 ]; then - key_pass_type="complex" - else - key_pass_type="simple" - fi - shift - else - key_pass_type="empty" - fi - - for key_size in $*; do - for key_format in $KEYCERT_FORMATS; do - if [ $key_format = "openssh" -a $key_type = "ed25519" ]; then - # "Cannot serialize Ed25519 key to openssh format". - (>&2 echo "Not generating $key_type key with the $key_format format.") - continue - fi - final_keycert_opts="${keycert_opts} --key-size $key_size --key-format $key_format" - # An associated public key is also generated with same name + '.pub'. - key_file=${key_type}_${key_size}_${key_format}_${key_pass_type} - $KEYCERT_CMD ${final_keycert_opts} --key-file $key_file - # OpenSSH's tool will complain of unsafe permissions. - chmod 600 $key_file - case $key_format in - openssh*) - sshkeygen_tests $key_file $key_pass_type - ;; - putty) - puttygen_tests $key_file $key_pass_type - ;; - esac - rm $key_file ${key_file}.pub - done - done -} - - - -for pass_type in $PASS_TYPES; do - pass=$(cat pass_file_${pass_type}) - - for key in $KEY_TYPES; do - case $key in - "ed25519") - keycert_gen_keys ed25519 $pass 256 - ;; - "ecdsa") - keycert_gen_keys ecdsa $pass 256 384 521 - ;; - "rsa") - # An unusual prime size is also tested. - keycert_gen_keys rsa $pass 1024 2111 3072 4096 8192 - ;; - "dsa") - keycert_gen_keys dsa $pass 1024 2048 3072 4096 - ;; - esac - done - - rm pass_file_${pass_type} -done - -# FIXME:51: -# This doesn't support testing type of comments independently yet. -rm comm_file_* - -echo -ne "\nCombinations tested: " -cat $SUCCESS_FILE $ERROR_FILE | wc -l - -echo -ne "\nCombinations with no errors: " -cat $SUCCESS_FILE | wc -l -cat $SUCCESS_FILE -rm $SUCCESS_FILE - -echo -ne "\nCombinations with errors: " -cat $ERROR_FILE | wc -l -cat $ERROR_FILE - -if [ -s $ERROR_FILE ]; then - rm $ERROR_FILE - exit 13 -else - rm $ERROR_FILE -fi diff --git a/chevah/keycert/tests/ssh_load_keys_tests.sh b/chevah/keycert/tests/ssh_load_keys_tests.sh deleted file mode 100755 index a48d91b..0000000 --- a/chevah/keycert/tests/ssh_load_keys_tests.sh +++ /dev/null @@ -1,308 +0,0 @@ -#!/usr/bin/env bash -# -# Test loading keys generated with various SSH key generators. - -set -euo pipefail - -# Key types to generate with puttygen, ssh-keygen, ssh-keygen-g3. -# Accepted parameters (one or more): ed25519, ecdsa, rsa, dsa. -# Generating large size RSA and DSA keys takes a lot of CPU time. -KEY_TYPES=$* -if [ -z "$KEY_TYPES" ]; then - # If no parameters given, test all. - KEY_TYPES="ed25519 ecdsa dsa rsa" -fi - -KEYCERT_CMD="../build-keycert/bin/python ../keycert-demo.py" -KEYCERT_NO_ERRORS_FILE="load_keys_tests_errors_none" -KEYCERT_EXPECTED_ERRORS_FILE="load_keys_tests_errors_expected" -KEYCERT_UNEXPECTED_ERRORS_FILE="load_keys_tests_errors_unexpected" -KEYCERT_DEMOSCRIPT_ERRORS_FILE="load_keys_tests_errors_demoscript" - -# puttygen supports key type "rsa1", but it's not used here. -# private-sshcom doesn't work with ed25519 and ecdsa in puttygen 0.74. -PUTTY_PRIV_OUTPUTS="private private-openssh private-openssh-new private-sshcom" -PUTTY_PUB_OUTPUTS="public public-openssh" - -# The "default" option is more of a placeholder for not using an extra format. -OPENSSH_FORMATS="default RFC4716 PKCS8 PEM" - -TECTIA_FORMATS="secsh2 pkcs1 pkcs8 pkcs12 openssh2 openssh2-aes" -TECTIA_HASHES="sha1 sha224 sha256 sha384 sha512" - -# Empty the files holding test results, if present. -> $KEYCERT_NO_ERRORS_FILE -> $KEYCERT_EXPECTED_ERRORS_FILE -> $KEYCERT_UNEXPECTED_ERRORS_FILE -> $KEYCERT_DEMOSCRIPT_ERRORS_FILE - -# Common routines like setting password files. -source ../chevah/keycert/tests/ssh_common_test_inc.sh -# FIXME:50 -# Unicode comments are not supported. -COMM_TYPES="empty simple complex" -# FIXME:52 -# Comments starting with a blank are not supported. -COMM_TYPES="empty simple" - -# First parameter is the private or public key file. -# Second (optional) parameter is the password. -keycert_load_key(){ - local keycert_opts="ssh-load-key --file $1" - if [ "$#" = 2 ]; then - local keycert_opts="$keycert_opts --password $2" - fi - set +e - $KEYCERT_CMD $keycert_opts - local keycert_err_code=$? - set -e - if [ $keycert_err_code -eq 0 ]; then - echo $1 >> $KEYCERT_NO_ERRORS_FILE - elif [ $keycert_err_code -eq 1 ]; then - echo $1 >> $KEYCERT_EXPECTED_ERRORS_FILE - elif [ $keycert_err_code -eq 2 ]; then - echo $1 >> $KEYCERT_UNEXPECTED_ERRORS_FILE - elif [ $keycert_err_code -eq 3 ]; then - echo $1 >> $KEYCERT_DEMOSCRIPT_ERRORS_FILE - else - (>&2 echo "Unexpected error code: $keycert_err_code") - exit 42 - fi -} - -putty_keys_test(){ - local bit_lengths="$1" - local pass_type - local pass_file - local priv_key_file - local pub_key_file - local pub_output - - for bits in $bit_lengths; do - for pass_type in $PASS_TYPES; do - for comm_type in $COMM_TYPES; do - echo -n "Generating $KEY key of type $PUTTY_PRIV_OUTPUT and size $bits" - echo " with $pass_type password and $comm_type comment:" - priv_key_file="putty_${KEY}_${bits}_${PUTTY_PRIV_OUTPUT}_${pass_type}pass_${comm_type}comm" - pass_file="pass_file_${pass_type}" - comm_file="comm_file_${comm_type}" - puttygen --random-device /dev/random -C "$(cat $comm_file)" --new-passphrase $pass_file \ - -t $KEY -O $PUTTY_PRIV_OUTPUT -b $bits -o $priv_key_file - keycert_load_key $priv_key_file $(cat $pass_file) - # Extract/test public key in all supported public formats, but only when: - # 1) The private key is in Putty's own format. - # 2) The complex password is used. - if [ "$PUTTY_PRIV_OUTPUT" = "private" -a $pass_type = "complex" ]; then - for pub_output in $PUTTY_PUB_OUTPUTS; do - pub_key_file="putty_${KEY}_${bits}_${pub_output}_${pass_type}pass_${comm_type}comm" - puttygen --old-passphrase $pass_file -O $pub_output -o $pub_key_file $priv_key_file - keycert_load_key $pub_key_file - rm $pub_key_file - done - fi - rm $priv_key_file - done - done - done -} - -openssh_format_set(){ - if [ $format != "default" ]; then - OPENSSH_OPTS="$OPENSSH_OPTS -m $format" - fi -} - -openssh_keys_test(){ - local bit_lengths="$1" - local pass_type - local pass_file - local format - local priv_key_file - local pub_key_file - - for bits in $bit_lengths; do - for pass_type in $PASS_TYPES; do - pass_file="pass_file_${pass_type}" - for comm_type in $COMM_TYPES; do - comm_file="comm_file_${comm_type}" - for format in $OPENSSH_FORMATS; do - priv_key_file=openssh_${KEY}_${bits}_${format}_${pass_type}pass_${comm_type}comm - pub_key_file=$priv_key_file.pub - if [ $pass_type = "empty" ]; then - if [ $format = "PKCS8" ]; then - if [ $KEY = "ecdsa" -o $KEY = "rsa" -o $KEY = "dsa" ]; then - # Minimum 5 characters required for these combinations. - (>&2 echo "Not generating $format $KEY key with $pass_type password.") - continue - fi - fi - OPENSSH_OPTS="" - openssh_format_set - ssh-keygen -C "$(cat $comm_file)" -t $KEY -b $bits $OPENSSH_OPTS -f $priv_key_file -N "" - else - OPENSSH_OPTS="-N $(cat $pass_file)" - openssh_format_set - ssh-keygen -C "$(cat $comm_file)" -t $KEY -b $bits $OPENSSH_OPTS -f $priv_key_file - fi - keycert_load_key $priv_key_file $(cat $pass_file) - keycert_load_key $pub_key_file - rm $priv_key_file $pub_key_file - done - done - done - done -} - -tectia_keys_test(){ - local bit_lengths="$1" - local pass_type - local pass_file - local format - local fips_mode - local priv_key_file - local pub_key_file - local gen_opts - - for bits in $bit_lengths; do - # FIXME:53 - # Tectia tests are currently disabled. - break - for pass_type in $PASS_TYPES; do - pass_file="pass_file_${pass_type}" - for comm_type in $COMM_TYPES; do - comm_file="comm_file_${comm_type}" - for format in $TECTIA_FORMATS; do - for fips_mode in nofips fips; do - if [ $fips_mode = "fips" -a $KEY = "ed25519" ]; then - continue - elif [ $fips_mode = "fips" -a $pass_type = "empty" ]; then - continue - elif [ $fips_mode = "fips" -a "${format%openssh2*}" = "" ]; then - # "OpenSSH2 keys operations are forbidden when in FIPS mode." - continue - fi - for hash in $TECTIA_HASHES; do - gen_opts="-b $bits -t $KEY --key-format $format --key-hash $hash" - if [ $fips_mode = "fips" ]; then - gen_opts="$gen_opts --fips-mode" - fi - priv_key_file=tectia_${KEY}_${bits}_${format}_${hash}_${fips_mode}_${pass_type}_${comm_type} - pub_key_file=$priv_key_file.pub - if [ $pass_type = "empty" ]; then - ssh-keygen-g3 -c "$(cat $comm_file)" $gen_opts -P $(pwd)/$priv_key_file - else - ssh-keygen-g3 -c "$(cat $comm_file)" $gen_opts -p $(cat $pass_file) $(pwd)/$priv_key_file - fi - keycert_load_key $priv_key_file $(cat $pass_file) - keycert_load_key $pub_key_file - rm $priv_key_file $pub_key_file - done - done - done - done - done - done -} - - - -# Putty's puttygen tests. -for KEY in $KEY_TYPES; do - for PUTTY_PRIV_OUTPUT in $PUTTY_PRIV_OUTPUTS; do - if [ $KEY = "ed25519" -a $PUTTY_PRIV_OUTPUT = "private-openssh-new" ]; then - # No need to force new OpenSSH format for ED25519 keys. - continue - fi - if [ $PUTTY_PRIV_OUTPUT = "private-sshcom" ]; then - if [ $KEY = "ed25519" -o $KEY = "ecdsa" ]; then - # Not working in puttygen 0.74. - continue - fi - fi - # Test specific numbers of bits per key type. - case $KEY in - "ed25519") - putty_keys_test "256" - ;; - "ecdsa") - putty_keys_test "256 384 521" - ;; - "rsa") - putty_keys_test "512 2048 4096" - ;; - "dsa") - # An unusual prime size is also tested. - putty_keys_test "2111 3072 4096" - ;; - esac - done -done - -# OpenSSH's ssh-keygen tests. -for KEY in $KEY_TYPES; do - case $KEY in - "ed25519") - openssh_keys_test "256" - ;; - "ecdsa") - openssh_keys_test "256 384 521" - ;; - "rsa") - # An unusual prime size is also tested. - openssh_keys_test "1024 2111 3072 8192" - ;; - "dsa") - openssh_keys_test "1024" - ;; - esac -done - -# Tectia's ssh-keygen-g3 tests. -for KEY in $KEY_TYPES; do - case $KEY in - "ed25519") - tectia_keys_test "256" - ;; - "ecdsa") - tectia_keys_test "256 384 521" - ;; - "rsa") - tectia_keys_test "512 1024 2048 3072 4096 8192" - ;; - "dsa") - tectia_keys_test "1024 2048 3072 4096" - ;; - esac -done - -# Cleanup test files. -rm pass_file_* comm_file_* - -echo -ne "\nCombinations tested: " -cat $KEYCERT_NO_ERRORS_FILE $KEYCERT_EXPECTED_ERRORS_FILE $KEYCERT_UNEXPECTED_ERRORS_FILE | wc -l - -echo -ne "\nCombinations with no errors: " -cat $KEYCERT_NO_ERRORS_FILE | wc -l -cat $KEYCERT_NO_ERRORS_FILE -rm $KEYCERT_NO_ERRORS_FILE - -echo -ne "\nCombinations with demo script errors: " -cat $KEYCERT_DEMOSCRIPT_ERRORS_FILE | wc -l -cat $KEYCERT_DEMOSCRIPT_ERRORS_FILE -rm $KEYCERT_DEMOSCRIPT_ERRORS_FILE - -echo -ne "\nCombinations with expected errors: " -cat $KEYCERT_EXPECTED_ERRORS_FILE | wc -l -cat $KEYCERT_EXPECTED_ERRORS_FILE -rm $KEYCERT_EXPECTED_ERRORS_FILE - -echo -ne "\nCombinations with unexpected errors: " -cat $KEYCERT_UNEXPECTED_ERRORS_FILE | wc -l -cat $KEYCERT_UNEXPECTED_ERRORS_FILE - -if [ -s $KEYCERT_UNEXPECTED_ERRORS_FILE ]; then - rm $KEYCERT_UNEXPECTED_ERRORS_FILE - exit 13 -else - rm $KEYCERT_UNEXPECTED_ERRORS_FILE -fi diff --git a/chevah/keycert/tests/test_exceptions.py b/chevah/keycert/tests/test_exceptions.py deleted file mode 100644 index 7a22824..0000000 --- a/chevah/keycert/tests/test_exceptions.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2015 Adi Roiban. -# See LICENSE for details. -""" -Test for exceptions raise by this package. -""" -from chevah.compat.testing import mk, ChevahTestCase - -from chevah.keycert.exceptions import KeyCertException - - -class TestExceptions(ChevahTestCase): - """ - Test for exceptions - """ - - def test_KeyCertException(self): - """ - It provides a message. - """ - message = mk.string() - - error = KeyCertException(message) - - self.assertEqual(message, error.message) - - def test_KeyCertException_str(self): - """ - The message is the string serialization. - """ - message = mk.string() - - error = KeyCertException(message) - - self.assertEqual(message.encode('utf-8'), str(error)) diff --git a/chevah/keycert/tests/test_ssh.py b/chevah/keycert/tests/test_ssh.py deleted file mode 100644 index 47c3ed1..0000000 --- a/chevah/keycert/tests/test_ssh.py +++ /dev/null @@ -1,2898 +0,0 @@ -# Copyright (c) 2014 Adi Roiban. -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. -""" -Test for SSH keys management. -""" -from __future__ import absolute_import, division, unicode_literals - -from argparse import ArgumentParser -from StringIO import StringIO -import base64 -import textwrap - -from chevah.compat.testing import mk, ChevahTestCase -from nose.plugins.attrib import attr - -# Twisted test compatibility. -from chevah.keycert import ssh as keys, common, sexpy, _path -from chevah.keycert.exceptions import ( - BadKeyError, - KeyCertException, - EncryptedKeyError, - ) -from chevah.keycert.ssh import ( - Key, - generate_ssh_key, - generate_ssh_key_parser, - ) -from chevah.keycert.tests import keydata -from chevah.keycert.tests.helpers import CommandLineMixin - - -OPENSSH_RSA_PRIVATE = (b'''-----BEGIN RSA PRIVATE KEY----- -MIICWwIBAAKBgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKAPkPAWzlu5BRHcmA -u0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAfp1YxCR -9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLwIDAQAB -AoGACB5cQDvxmBdgYVpuy43DduabTmR71HFaNFl+nE5vwFxUqX0qFOQpG0E2Cv56 -zesPzT1JWBiqffSir4iSjH/lnskZnM9J1xfpnoJ5HTzcGHaBYVFEEXS6fOsyWT15 -oY7Kb6rRBTnWV0Ins/05Hhp38r/RR/O4poB+3NwQJDl/6gECQQDoAnRdC+5SyjrZ -1JQUWUkapiYHIhFq6kWtGm3kWJn0IxCBtFhGvqIWJwZIAjf6tTKMUk6bjG9p7Jpe -tXUsTiDBAkEAy5EDU2F42Xm6tvQzM8bAgq7d2/x2iHRuOkDUb1bK3YwByTihl9BL -qvdRhRxpl21EcqWpB/RzAFbGa+60G/iV7wJABSz415KKkII+admaLBIJ1XRbaNFT -viTXxRLP3MY1OQMHPT1+sqVSDFh2hWi3QvqD1CmJ42JwodZLY018/a4IgQJAOsCg -yBjyyznB9PnoKUJs34rex5ZHE70e7zs01Omk5Wp6PXxVzz40CKUW5yc7JpRH1BsR -/RTFeEyTOiWL4CLQCwJAf4BF9eVLxRQ9A4Mm9Ikt4lF8ii6na4nxdtEzP8p2LP9t -LqHYUobNanxB+7Msi4f3gYyuKdOGnWHqD2U4HcLdMQ== ------END RSA PRIVATE KEY-----''') - -# Converted from old format using OpenSSH without a password. -# $ ssh-keygen -e -p -f OPENSSH_RSA_PRIVATE.key -OPENSSH_V1_RSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn -NhAAAAAwEAAQAAAIEAuH1erUmpA0gemaL8oC7H9YgvT97SR3j0ygD5DwFs5buQUR3JgLtL -k45+xoa6cW441jMA1TdDsc8O7XiNDhCFZKZ5XMjOi+ZhpPl+i3OPpQH6dWMQkfaPfhVzFk -iT99o0cCPuC4VmMZ2FJXbwDuSw8/UHkdMiopsHI0U6Bev46S8AAAH4y/dH2sv3R9oAAAAH -c3NoLXJzYQAAAIEAuH1erUmpA0gemaL8oC7H9YgvT97SR3j0ygD5DwFs5buQUR3JgLtLk4 -5+xoa6cW441jMA1TdDsc8O7XiNDhCFZKZ5XMjOi+ZhpPl+i3OPpQH6dWMQkfaPfhVzFkiT -99o0cCPuC4VmMZ2FJXbwDuSw8/UHkdMiopsHI0U6Bev46S8AAAADAQABAAAAgAgeXEA78Z -gXYGFabsuNw3bmm05ke9RxWjRZfpxOb8BcVKl9KhTkKRtBNgr+es3rD809SVgYqn30oq+I -kox/5Z7JGZzPSdcX6Z6CeR083Bh2gWFRRBF0unzrMlk9eaGOym+q0QU51ldCJ7P9OR4ad/ -K/0UfzuKaAftzcECQ5f+oBAAAAQH+ARfXlS8UUPQODJvSJLeJRfIoup2uJ8XbRMz/Kdiz/ -bS6h2FKGzWp8QfuzLIuH94GMrinThp1h6g9lOB3C3TEAAABBAOgCdF0L7lLKOtnUlBRZSR -qmJgciEWrqRa0abeRYmfQjEIG0WEa+ohYnBkgCN/q1MoxSTpuMb2nsml61dSxOIMEAAABB -AMuRA1NheNl5urb0MzPGwIKu3dv8doh0bjpA1G9Wyt2MAck4oZfQS6r3UYUcaZdtRHKlqQ -f0cwBWxmvutBv4le8AAAAAAQID ------END OPENSSH PRIVATE KEY-----''') - -# Converted from old format using OpenSSH with `test` as password. -# $ ssh-keygen -e -p -f OPENSSH_RSA_PRIVATE.key -OPENSSH_V1_ENCRYPTED_RSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCO5u6Nze -CPk3e+vkL9MmvWAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakD -SB6ZovygLsf1iC9P3tJHePTKAPkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI -0OEIVkpnlcyM6L5mGk+X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz -9QeR0yKimwcjRToF6/jpLwAAAgDuID/fk0osaBUXQ+M32lA677YjC9BX5bSwKHNdbaH/eD -H5T4mNZNe8IvZXsYGsVXKT5yaRP/19A/5pVivnTn2n0dOZ0tbfnqrPLJnEdPPTlLVv+YaR -+TZYRYfydOXpZ44MsJAzmOmCWVIlDNratEt/zoiqhF2T3q4ODFEABfDQ3LixRx+Jk90icy -FrL7DuDLsTdjXLnmUSh7Ytzd9v8XrQ8ku98EvOzqCCneYguYt2zHrRVd+jWivJ7Pdv86lg -kksqxIlY7TV+wqcbYvLDuZF6iP3jWAGoQYSUJpqVwp0PLz53hzxwcLMEg+V93e9fYiQjsE -psoQ/y8ZGmBIGqkAj+BC9Y6DXFPmstv0yHlSoB/A4FwVerZiVu4G239LF8Wt6gfAU7Bu7j -yvWKic87GsONUvp8iKFntCFgeX4aa9bVsl4N9APzEBPsj2ni4E3+UYYovGBo8jlmxBAj3V -evUSgiQfOTIM8UkZfk6plXchJTmshIeL1SMyjdNF2ziVh72T1RCOs/905gXXvw+Bl+zdtJ -5sRcoQii4HcPjK0WUZaSM/5LsxSsqDt+nBVoaq7k24ITTjXdHIuiT1YnKFjErzD3bznosW -wNe7YoLXxnuszUFaBAWthJuOsE1JVAScqo7oClPc1CHX8qEZz5vihkEploAOGe0hj5Kjt6 -vLDBLhI7ag== ------END OPENSSH PRIVATE KEY-----''') - -OPENSSH_RSA_PUBLIC = ( - 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKA' - 'PkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAf' - 'p1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLw==' - ) - -PKCS1_RSA_PUBLIC = (b'''-----BEGIN RSA PUBLIC KEY----- -MIGJAoGBALh9Xq1JqQNIHpmi/KAux/WIL0/e0kd49MoA+Q8BbOW7kFEdyYC7S5OO -fsaGunFuONYzANU3Q7HPDu14jQ4QhWSmeVzIzovmYaT5fotzj6UB+nVjEJH2j34V -cxZIk/faNHAj7guFZjGdhSV28A7ksPP1B5HTIqKbByNFOgXr+OkvAgMBAAE= ------END RSA PUBLIC KEY-----''') - -OPENSSH_DSA_PRIVATE = (b'''-----BEGIN DSA PRIVATE KEY----- -MIIBugIBAAKBgQDOwkKGnmVZ9bRl7ZCn/wSELV0n5ELsqVZFOtBpHleEOitsvjEB -BbTKX0fZ83vaMVnJFVw3DQSbi192krvk909Y6h3HVO2MKBRd9t29fr26VvCZQOxR -4fzkPuL+Px4+ShqE171sOzsuEDt0Mkxf152QxrA2vPowkj7fmzRH5xgDTQIVAIYb -/ljSUclo6TiNwoiF+9byafFJAoGAXA+TAGCmF2ZeNZN04mgxeyT34IAw37NGmLLP -/byi86dKcdz5htqPiOWcNmFzrA7a0o+erE3B+miwEm2sVz+eVWfNOCJQalHUqRrk -1iV542FL0BCePiJa91Baw4pVS5hnSNko/Wsp0VnW3q5OK/tPs1pRy+3qWUwwrg5i -zhYkBfwCgYB/6sL9MO4ZwtFzwbOKNOoZxfORwNbzzHf+IpzyBTxxQJcYS6QgbtSi -2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3pZ7SnoRJ7vATQnMJpwZYno -8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk204gIUTk7aRv65 -oTedYsAyi80L8phYBN4= ------END DSA PRIVATE KEY-----''') - -# Converted from old format using OpenSSH without a password. -# $ ssh-keygen -e -p -f OPENSSH_DSA_PRIVATE.key -OPENSSH_V1_DSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH -NzAAAAgQDOwkKGnmVZ9bRl7ZCn/wSELV0n5ELsqVZFOtBpHleEOitsvjEBBbTKX0fZ83va -MVnJFVw3DQSbi192krvk909Y6h3HVO2MKBRd9t29fr26VvCZQOxR4fzkPuL+Px4+ShqE17 -1sOzsuEDt0Mkxf152QxrA2vPowkj7fmzRH5xgDTQAAABUAhhv+WNJRyWjpOI3CiIX71vJp -8UkAAACAXA+TAGCmF2ZeNZN04mgxeyT34IAw37NGmLLP/byi86dKcdz5htqPiOWcNmFzrA -7a0o+erE3B+miwEm2sVz+eVWfNOCJQalHUqRrk1iV542FL0BCePiJa91Baw4pVS5hnSNko -/Wsp0VnW3q5OK/tPs1pRy+3qWUwwrg5izhYkBfwAAACAf+rC/TDuGcLRc8GzijTqGcXzkc -DW88x3/iKc8gU8cUCXGEukIG7UotrVGNVnicZqv8ZDKFS4KZqSujTfjbgemrseeFO4eQd6 -We0p6ESe7wE0JzCacGWJ6PLmV4ceE5jqEsc8cto0yW+K7eEem28I2iPXGv9Yi2r6HENtfG -ZNtOIAAAHYZ8aTg2fGk4MAAAAHc3NoLWRzcwAAAIEAzsJChp5lWfW0Ze2Qp/8EhC1dJ+RC -7KlWRTrQaR5XhDorbL4xAQW0yl9H2fN72jFZyRVcNw0Em4tfdpK75PdPWOodx1TtjCgUXf -bdvX69ulbwmUDsUeH85D7i/j8ePkoahNe9bDs7LhA7dDJMX9edkMawNrz6MJI+35s0R+cY -A00AAAAVAIYb/ljSUclo6TiNwoiF+9byafFJAAAAgFwPkwBgphdmXjWTdOJoMXsk9+CAMN -+zRpiyz/28ovOnSnHc+Ybaj4jlnDZhc6wO2tKPnqxNwfposBJtrFc/nlVnzTgiUGpR1Kka -5NYleeNhS9AQnj4iWvdQWsOKVUuYZ0jZKP1rKdFZ1t6uTiv7T7NaUcvt6llMMK4OYs4WJA -X8AAAAgH/qwv0w7hnC0XPBs4o06hnF85HA1vPMd/4inPIFPHFAlxhLpCBu1KLa1RjVZ4nG -ar/GQyhUuCmakro03424Hpq7HnhTuHkHelntKehEnu8BNCcwmnBliejy5leHHhOY6hLHPH -LaNMlviu3hHptvCNoj1xr/WItq+hxDbXxmTbTiAAAAFE5O2kb+uaE3nWLAMovNC/KYWATe -AAAAAAECAw== ------END OPENSSH PRIVATE KEY-----''') - -# Converted from old format using OpenSSH with `test` as the password. -# $ ssh-keygen -e -p -f OPENSSH_DSA_PRIVATE.key -OPENSSH_V1_ENCRYPTED_DSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCR+DbQqo -2salfbIh0HztjEAAAAEAAAAAEAAAGxAAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/ -BIQtXSfkQuypVkU60GkeV4Q6K2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHc -dU7YwoFF323b1+vbpW8JlA7FHh/OQ+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCS -Pt+bNEfnGANNAAAAFQCGG/5Y0lHJaOk4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaD -F7JPfggDDfs0aYss/9vKLzp0px3PmG2o+I5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804 -IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLmGdI2Sj9aynRWdberk4r+0+zWlHL7epZTD -CuDmLOFiQF/AAAAIB/6sL9MO4ZwtFzwbOKNOoZxfORwNbzzHf+IpzyBTxxQJcYS6QgbtSi -2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3pZ7SnoRJ7vATQnMJpwZYno8uZXhx -4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk204gAAAeBVUr2hdw/PN3S0QUwq -Ny7fOtmBVyuhRDvlS7OTsCaOs4cPF3j9o8K56Fk2Fdj69G8g56/2NrRPHvGyCtoN4olKwZ -Cc/MsePe0R7vWumVgTt1kDk6/CcnAUnTtCL7GW7a1w+8ZDwBotCZgznDD9NlnhfH0g0MZ9 -eLP4UY181lYC6452fy8E2pV9qyYufRnRYe5Gu0zoRjEuyYDbNzDBCU4WZ4O7InJDiHuVVE -hocQSVu4WzfABuCageM2wCkbKeM0mRZw1jljhO8a/T45wLmoYQxnUYFeUkUuy4akn5/uJ2 -xvIn3zl6fCqiWAnwbRjZeBfQ7q+5E/jUrUklGyBeEMn2RNo9kYTEOItuj6j8bXYELsTyjH -tJ8DplDkNN3/FYG+D8JYyhuaGd4cSLtjXS95nuazHvwyb60CQxPwbmUcojqsrM65Yu7+dQ -wwYEpG5w9/IlKJ62JmEqhEVMI4HHyDLcocYlU6OoD1Ivy09dcIO8uRBYc9jFccj/1ej5oI -tn6RsW0HRlVx06tbp6RDHBfAdg5suu5pW9uv2tESbEqpMHt4FQgqKcSQwzYLvo/bfPuxs0 -HNOQMLNwRg8yYbCG+u2HU9YTlQdTgG/5h+eYsQLObPU+TjYgS5p6sUZCkTCnOz8= ------END OPENSSH PRIVATE KEY-----''') - - -OPENSSH_DSA_PUBLIC = ( - 'ssh-dss AAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/BIQtXSfkQuypVkU60GkeV4Q6K' - '2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU7YwoFF323b1+vbpW8JlA7FHh/O' - 'Q+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCSPt+bNEfnGANNAAAAFQCGG/5Y0lHJaOk' - '4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaDF7JPfggDDfs0aYss/9vKLzp0px3PmG2o+I' - '5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLm' - 'GdI2Sj9aynRWdberk4r+0+zWlHL7epZTDCuDmLOFiQF/AAAAIB/6sL9MO4ZwtFzwbOKNOoZxf' - 'ORwNbzzHf+IpzyBTxxQJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3p' - 'Z7SnoRJ7vATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk20' - '4g==' - ) - -# Same key as OPENSSH_RSA_PUBLIC, wrapped at 70 characters. -SSHCOM_RSA_PUBLIC = b"""---- BEGIN SSH2 PUBLIC KEY ---- -AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKAPkPAW -zlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAfp1 -YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLw== ----- END SSH2 PUBLIC KEY ----""" - -# Same key as OPENSSH_DSA_PUBLIC. -SSHCOM_DSA_PUBLIC = b"""---- BEGIN SSH2 PUBLIC KEY ---- -AAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/BIQtXSfkQuypVkU60GkeV4Q6K2y+MQ -EFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU7YwoFF323b1+vbpW8JlA7FHh/OQ+ -4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCSPt+bNEfnGANNAAAAFQCGG/5Y0lHJaO -k4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaDF7JPfggDDfs0aYss/9vKLzp0px3PmG -2o+I5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UF -rDilVLmGdI2Sj9aynRWdberk4r+0+zWlHL7epZTDCuDmLOFiQF/AAAAIB/6sL9MO4ZwtFz -wbOKNOoZxfORwNbzzHf+IpzyBTxxQJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB -6aux54U7h5B3pZ7SnoRJ7vATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca -/1iLavocQ218Zk204g== ----- END SSH2 PUBLIC KEY ----""" - -# Same as OPENSSH_RSA_PRIVATE -SSHCOM_RSA_PRIVATE_NO_PASSWORD = b"""---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- -P2/56wAAAi4AAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0e3JzYS -1wa2NzMXYyLW9hZXB9fQAAAARub25lAAAB3wAAAdsAAAARAQABAAAD+QgeXEA78ZgXYGFa -bsuNw3bmm05ke9RxWjRZfpxOb8BcVKl9KhTkKRtBNgr+es3rD809SVgYqn30oq+Ikox/5Z -7JGZzPSdcX6Z6CeR083Bh2gWFRRBF0unzrMlk9eaGOym+q0QU51ldCJ7P9OR4ad/K/0Ufz -uKaAftzcECQ5f+oBAAAD+bh9Xq1JqQNIHpmi/KAux/WIL0/e0kd49MoA+Q8BbOW7kFEdyY -C7S5OOfsaGunFuONYzANU3Q7HPDu14jQ4QhWSmeVzIzovmYaT5fotzj6UB+nVjEJH2j34V -cxZIk/faNHAj7guFZjGdhSV28A7ksPP1B5HTIqKbByNFOgXr+OkvAAAB+X+ARfXlS8UUPQ -ODJvSJLeJRfIoup2uJ8XbRMz/Kdiz/bS6h2FKGzWp8QfuzLIuH94GMrinThp1h6g9lOB3C -3TEAAAH5y5EDU2F42Xm6tvQzM8bAgq7d2/x2iHRuOkDUb1bK3YwByTihl9BLqvdRhRxpl2 -1EcqWpB/RzAFbGa+60G/iV7wAAAfnoAnRdC+5SyjrZ1JQUWUkapiYHIhFq6kWtGm3kWJn0 -IxCBtFhGvqIWJwZIAjf6tTKMUk6bjG9p7JpetXUsTiDB ----- END SSH2 ENCRYPTED PRIVATE KEY ----""" - -# Same as OPENSSH_RSA_PRIVATE and with 'chevah' password. -SSHCOM_RSA_PRIVATE_WITH_PASSWORD = ( - b"""---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- -P2/56wAAAjMAAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0e3JzYS -1wa2NzMXYyLW9hZXB9fQAAAAgzZGVzLWNiYwAAAeAqUfFcnQIi4HEOAvAoJp8nIsw3WZMc -MhWiSWenwY0tKZPxngo1s2p8QkIclw0Tu7twvtG2zABb4x/jfyqLPc5brvBdYiAXMg1xPS -xzJ7gmaYLbAJEeQxdzPqXmxJXvxSwElYhozCFHpTYm56PYBONUSbV2ORCA4eEn9VjFRxqX -Q/XQ433aF4ZlnCVl+tCJRxhfjDTw/p5jfVETVwqdm7XCM2rGYvHxqn5uUxOl+jUorDtPHu -aPZGuKND1rGWSve8p9RA662P/M6HNHMq5w5mEKKc6aOikSFWwFe3vKZ3nE1WtXEvE2bgBD -1rvYLBp9tFx4U3uQAMxvVQAeyYNeK9Qt11IMg7+seskBmVQNXo2h3Wbn8TRUxSscgQNfnm -BnNIQQbiaMEk1Em8K2I5L+DRrcOzSvkVBNguOaiLCuSbP4f4JkAvD743scRFrT3QgCdjqr -4FHJG/z/D7dEbeC3mJfXFrM7PgCGFx9L6/FqLC+piJmyEq8nggkg9P0o+oJ7/c/xGU7at9 -BsDKrM0FEXc8bFp39e8BNRbikCD61zfFp7B1s64y1mmqJkDYe2pH7FUA9mbC3vv6YM9tsY -fWGAGt8dHGIMM6MrzZYr8xJLwdmPDwAtFt2GR1Y8M0vnw6WtoL4= ----- END SSH2 ENCRYPTED PRIVATE KEY ----""") - -SSHCOM_DSA_PRIVATE_NO_PASSWORD = b"""---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- -P2/56wAAAgIAAAAmZGwtbW9kcHtzaWdue2RzYS1uaXN0LXNoYTF9LGRoe3BsYWlufX0AAA -AEbm9uZQAAAcQAAAHAAAAAAAAAA/nOwkKGnmVZ9bRl7ZCn/wSELV0n5ELsqVZFOtBpHleE -OitsvjEBBbTKX0fZ83vaMVnJFVw3DQSbi192krvk909Y6h3HVO2MKBRd9t29fr26VvCZQO -xR4fzkPuL+Px4+ShqE171sOzsuEDt0Mkxf152QxrA2vPowkj7fmzRH5xgDTQAAA/lcD5MA -YKYXZl41k3TiaDF7JPfggDDfs0aYss/9vKLzp0px3PmG2o+I5Zw2YXOsDtrSj56sTcH6aL -ASbaxXP55VZ804IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLmGdI2Sj9aynRWdberk4r -+0+zWlHL7epZTDCuDmLOFiQF/AAAAJmGG/5Y0lHJaOk4jcKIhfvW8mnxSQAAA/l/6sL9MO -4ZwtFzwbOKNOoZxfORwNbzzHf+IpzyBTxxQJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6 -NN+NuB6aux54U7h5B3pZ7SnoRJ7vATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbw -jaI9ca/1iLavocQ218Zk204gAAAJlOTtpG/rmhN51iwDKLzQvymFgE3g== ----- END SSH2 ENCRYPTED PRIVATE KEY ----""" - -# Same as OPENSSH_RSA_PRIVATE -# Make sure it has Windows newlines. -PUTTY_RSA_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-rsa\r -Encryption: none\r -Comment: imported-openssh-key\r -Public-Lines: 4\r -AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK\r -APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk\r -+X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj\r -RToF6/jpLw==\r -Private-Lines: 8\r -AAAAgAgeXEA78ZgXYGFabsuNw3bmm05ke9RxWjRZfpxOb8BcVKl9KhTkKRtBNgr+\r -es3rD809SVgYqn30oq+Ikox/5Z7JGZzPSdcX6Z6CeR083Bh2gWFRRBF0unzrMlk9\r -eaGOym+q0QU51ldCJ7P9OR4ad/K/0UfzuKaAftzcECQ5f+oBAAAAQQDoAnRdC+5S\r -yjrZ1JQUWUkapiYHIhFq6kWtGm3kWJn0IxCBtFhGvqIWJwZIAjf6tTKMUk6bjG9p\r -7JpetXUsTiDBAAAAQQDLkQNTYXjZebq29DMzxsCCrt3b/HaIdG46QNRvVsrdjAHJ\r -OKGX0Euq91GFHGmXbURypakH9HMAVsZr7rQb+JXvAAAAQH+ARfXlS8UUPQODJvSJ\r -LeJRfIoup2uJ8XbRMz/Kdiz/bS6h2FKGzWp8QfuzLIuH94GMrinThp1h6g9lOB3C\r -3TE=\r -Private-MAC: 7630b86be300c6302ce1390fb264487bb61e67ce""" - -# Same as OPENSSH_RSA_PRIVATE, with 'chevah' password. -PUTTY_RSA_PRIVATE_WITH_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-rsa\r -Encryption: aes256-cbc\r -Comment: imported-openssh-key\r -Public-Lines: 4\r -AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK\r -APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk\r -+X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj\r -RToF6/jpLw==\r -Private-Lines: 8\r -dqtZBETu8cK9VpOX/IB9iIehQE7r6ceVvzsDqrjwGnw64LkEoqlqobP7diV3/gpc\r -b1Vmf8EitczdQBUdWkVtSJVA7FYBUNQlBd4ghkDJm58goTVzdGxpoafpQ9nFNO72\r -iQFg1wfpJQn9fcR0vQL1s5uykCSeEy232rHeFO4tMssq4xrhLqK9vWaYilWJoBxM\r -jzmVdL04QJERTJXh7k3wsRWGO12r+PGnp/8upiHHfnjVZlzDw6Dw6WQ+EaqI99mm\r -Cgo4ZiBwubHtPZq+eeP8Db/m3lMaKQNKAyYe3VlKCUwkC8N4jZR8QQlaOjBfHfPR\r -vO+Znb71OYvwFHQbwA3K64M9KnWCdXZxdCrBvm2UuEcKBz7SDEXQV2UvtGueg0s0\r -EO5R1D0fXky8HGA6VciUGR6g2zclO6rNR+Ooc5ThsZQ9sKVrpcvYYC8WdZ5LB50B\r -J8IuFywygVI4PbRs98v9Dg==\r -Private-MAC: 3ffe2587759ff8f50c6acdcad44f62a67e88ef2b""" - -# This is the same key as OPENSSH_DSA_PRIVATE -PUTTY_DSA_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-dss\r -Encryption: none\r -Comment: imported-openssh-key\r -Public-Lines: 10\r -AAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/BIQtXSfkQuypVkU60GkeV4Q6\r -K2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU7YwoFF323b1+vbpW\r -8JlA7FHh/OQ+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCSPt+bNEfnGANN\r -AAAAFQCGG/5Y0lHJaOk4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaDF7JPfg\r -gDDfs0aYss/9vKLzp0px3PmG2o+I5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804\r -IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLmGdI2Sj9aynRWdberk4r+0+zWlHL\r -7epZTDCuDmLOFiQF/AAAAIB/6sL9MO4ZwtFzwbOKNOoZxfORwNbzzHf+IpzyBTxx\r -QJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3pZ7SnoRJ7v\r -ATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk20\r -4g==\r -Private-Lines: 1\r -AAAAFE5O2kb+uaE3nWLAMovNC/KYWATe\r -Private-MAC: 1b98c142780beaa5555ad5c23a0469e36f24b6f9""" - -PUTTY_ED25519_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-ed25519\r -Encryption: none\r -Comment: ed25519-key-20210106\r -Public-Lines: 2\r -AAAAC3NzaC1lZDI1NTE5AAAAIEjwKguKHPrqp3UEqSP7XTmOhBavcTxkHwnzQveQ\r -2MGG\r -Private-Lines: 1\r -AAAAINWl263e/oNph4x7jM94kE7BaSNcXD7G6bbWatylw61A\r -Private-MAC: ead2308fe2f6be87941f17e9d61ede28da2cde8a\r -""" - -# Password is: chevah -PUTTY_ED25519_PRIVATE_WITH_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-ed25519\r -Encryption: aes256-cbc\r -Comment: ed25519-key-20210106\r -Public-Lines: 2\r -AAAAC3NzaC1lZDI1NTE5AAAAIKY6CzyQPkESUswMjxdbK7XgpfAExYRc0ydzwzco\r -bmlL\r -Private-Lines: 1\r -jvO/yHUJlgjCCzEFlkYwDeSIYggO3Ry1/iP1lm49BU6GljU/miaUemDBHT9umt0o\r -Private-MAC: 6b753f6180f48d153a700c6734b46b2e52f1f7e9\r -""" - -PUTTY_ECDSA_SHA2_NISTP256_PRIVATE_NO_PASSWORD = ( -b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp256\r -Encryption: none\r -Comment: ecdsa-key-20210106\r -Public-Lines: 3\r -AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPA3+gjOpajd\r -9iRVm72ArvQfjVW+3bz9IMrPNMIANSmwTj+0NuFgXZGLaxT8BKslZLZvJX+XuUr/\r -Yvgn32oS7Iw=\r -Private-Lines: 1\r -AAAAIDe7fQUAaorrEkedXTSmXrCY4vabtFV7e4Z8xBSvty8Q\r -Private-MAC: a84b17c5dead6fed8f474406929312d45c096dfc\r -""") - -PUTTY_ECDSA_SHA2_NISTP384_PRIVATE_NO_PASSWORD = ( -b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp384\r -Encryption: none\r -Comment: ecdsa-key-20210106\r -Public-Lines: 3\r -AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEjK280ap/RD\r -R916Q00OI1LIHyRG1fcH6twBjmynTgl0uGlcb8bnbpGO1JOgbhBqqzVQHVckHzqT\r -fUif6rRRQuiUJEenXRmgjQ0uEcj21Rdomz7TJPz1k8tHmOZCHgJx6g==\r -Private-Lines: 2\r -AAAAMQCNcgWtnEeeTqFN383FBJdM90keHkJwproyLPgWQLlbZe+r8py0Pl7mUHvj\r -SGmXUVc=\r -Private-MAC: 1464df777d20427e2b99adb148ed4b8a1a839409\r -""") - -PUTTY_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD = ( -b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp521\r -Encryption: none\r -Comment: ecdsa-key-20210106\r -Public-Lines: 4\r -AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGtj24Kr7OY\r -21mtlHTFuH0NmrhI1mco0nND4FvDbNTTU/87t1ZDqbPEnRqmYBM6/dGPyOK82PH8\r -NmCrCjj0rmckNgC3+Jg/+ok1bJG7/WeTOObnIdDBJklxksIjMF6LG6hVngIibxgF\r -V3iBGD5eWUr40AK+6+wN7uKsaFHMBCg8lde5Mg==\r -Private-Lines: 2\r -AAAAQgE64XtEewBVYUz+sfojvHmsiwdT+2BBBw1IAcKuozuhsz8EkOEOBJGZqCBP\r -B9pAqlHsVHQJF/uVpFbJFUnjEokJ4w==\r -Private-MAC: e828d7207e0e73453005d606216ca36c64d1e304\r -""") - - -class DummyOpenContext(object): - """ - Helper for testing operations using open context manager. - - It keeps a record or all calls in self.calls. - """ - - def __init__(self): - self.calls = [] - self.last_stream = None - - def __call__(self, path, mode): - self.last_stream = StringIO() - self.calls.append( - {'path': path, 'mode': mode, 'stream': self.last_stream}) - return self - - def __enter__(self): - return self.last_stream - - def __exit__(self, exc_type, exc_value, tb): - return False - - -class TestHelpers(ChevahTestCase, CommandLineMixin): - """ - Unit tests for helper methods from this module. - """ - - def test_path(self): - """ - Will take an unicode and will return the os encoded path. - """ - result = _path(u'path-\N{sun}') - if self.os_name == 'windows': - self.assertEqual(u'path-\N{sun}', result) - else: - self.assertEqual(b'path-\xe2\x98\x89', result) - - -class TestKey(ChevahTestCase): - """ - Unit test for SSH key generation. - - The actual test creating real keys are located in functional. - """ - - def setUp(self): - super(TestKey, self).setUp() - self.rsaObj = keys.Key._fromRSAComponents( - n=keydata.RSAData['n'], - e=keydata.RSAData['e'], - d=keydata.RSAData['d'], - p=keydata.RSAData['p'], - q=keydata.RSAData['q'], - u=keydata.RSAData['u'], - )._keyObject - self.dsaObj = keys.Key._fromDSAComponents( - y=keydata.DSAData['y'], - p=keydata.DSAData['p'], - q=keydata.DSAData['q'], - g=keydata.DSAData['g'], - x=keydata.DSAData['x'], - )._keyObject - self.ecObj = keys.Key._fromECComponents( - x=keydata.ECDatanistp256['x'], - y=keydata.ECDatanistp256['y'], - privateValue=keydata.ECDatanistp256['privateValue'], - curve=keydata.ECDatanistp256['curve'] - )._keyObject - self.ecObj384 = keys.Key._fromECComponents( - x=keydata.ECDatanistp384['x'], - y=keydata.ECDatanistp384['y'], - privateValue=keydata.ECDatanistp384['privateValue'], - curve=keydata.ECDatanistp384['curve'] - )._keyObject - self.ecObj521 = keys.Key._fromECComponents( - x=keydata.ECDatanistp521['x'], - y=keydata.ECDatanistp521['y'], - privateValue=keydata.ECDatanistp521['privateValue'], - curve=keydata.ECDatanistp521['curve'] - )._keyObject - self.ed25519Obj = keys.Key._fromEd25519Components( - a=keydata.Ed25519Data['a'], - k=keydata.Ed25519Data['k'] - )._keyObject - self.rsaSignature = ( - b"\x00\x00\x00\x07ssh-rsa\x00\x00\x01\x00~Y\xa3\xd7\xfdW\xc6pu@" - b"\xd81\xa1S\xf3O\xdaE\xf4/\x1ex\x1d\xf1\x9a\xe1G3\xd9\xd6U\x1f" - b"\x8c\xd9\x1b\x8b\x90\x0e\x8a\xc1\x91\xd8\x0cd\xc9\x0c\xe7\xb2" - b"\xc9,'=\x15\x1cQg\xe7x\xb5j\xdbI\xc0\xde\xafb\xd7@\xcar\x0b" - b"\xce\xa3zM\x151q5\xde\xfa\x0c{wjKN\x88\xcbC\xe5\x89\xc3\xf9i" - b"\x96\x91\xdb\xca}\xdbR\x1a\x13T\xf9\x0cDJH\x0b\x06\xcfl\xf3" - b"\x13[\x82\xa2\x9d\x93\xfd\x8e\xce|\xfb^n\xd4\xed\xe2\xd1\x8a" - b"\xb7aY\x9bB\x8f\xa4\xc7\xbe7\xb5\x0b9j\xa4.\x87\x13\xf7\xf0" - b"\xda\xd7\xd2\xf9\x1f9p\xfd?\x18\x0f\xf2N\x9b\xcf/\x1e)\n>A\x19" - b"\xc2\xb5j\xf9UW\xd4\xae\x87B\xe6\x99t\xa2y\x90\x98\xa2\xaaf\xcb" - b"\x86\xe5k\xe3\xce\xe0u\x1c\xeb\x93\x1aN\x88\xc9\x93Y\xc3.V\xb1L" - b"44`C\xc7\xa66\xaf\xfa\x7f\x04Y\x92\xfa\xa4\x1a\x18%\x19\xd5 4^" - b"\xb9rY\xba \x01\xf9.\x89%H\xbe\x1c\x83A\x96" - ) - self.dsaSignature = ( - b'\x00\x00\x00\x07ssh-dss\x00\x00\x00(?\xc7\xeb\x86;\xd5TFA\xb4' - b'\xdf\x0c\xc4E@4,d\xbc\t\xd9\xae\xdd[\xed-\x82nQ\x8cf\x9b\xe8\xe1' - b'jrg\x84p<' - ) - self.oldSecureRandom = Key.secureRandom - Key.secureRandom = lambda me, x: '\xff' * x - - def tearDown(self): - Key.secureRandom = self.oldSecureRandom - del self.oldSecureRandom - super(TestKey, self).tearDown() - - def assertBadKey(self, content, message): - """ - Check the `content` raise a BadKeyError with `message`. - """ - with self.assertRaises(BadKeyError) as context: - Key.fromString(content) - - self.assertEqual(message, context.exception.message) - - def assertKeyIsTooShort(self, content): - """ - Check the key content is too short. - """ - self.assertBadKey(content, 'Key is too short.') - - def assertKeyParseError(self, content): - """ - Check that key content fail to parse. - """ - self.assertBadKey(content, 'Fail to parse key content.') - - def _getKeysForFingerprintTest(self): - """ - Return tuple with public RSA and DSA keys from the test data. - """ - rsa = keys.Key._fromRSAComponents( - n=keydata.RSAData['n'], - e=keydata.RSAData['e'], - d=keydata.RSAData['d'], - p=keydata.RSAData['p'], - q=keydata.RSAData['q'], - u=keydata.RSAData['u'], - )._keyObject - dsa = keys.Key._fromDSAComponents( - y=keydata.DSAData['y'], - p=keydata.DSAData['p'], - q=keydata.DSAData['q'], - g=keydata.DSAData['g'], - x=keydata.DSAData['x'], - )._keyObject - return (rsa, dsa) - - def _testPublicPrivateFromString(self, public, private, type, data): - self._testPublicFromString(public, type, data) - self._testPrivateFromString(private, type, data) - - def _testPublicFromString(self, public, type, data): - publicKey = keys.Key.fromString(public) - self.assertTrue(publicKey.isPublic()) - self.assertEqual(publicKey.type(), type) - for k, v in publicKey.data().items(): - self.assertEqual(data[k], v) - - def _testPrivateFromString(self, private, type, data): - privateKey = keys.Key.fromString(private) - self.assertFalse(privateKey.isPublic()) - self.assertEqual(privateKey.type(), type) - for k, v in data.items(): - self.assertEqual(privateKey.data()[k], v) - - def test_init(self): - """ - Test that the PublicKey object is initialized correctly. - """ - obj = keys.Key._fromRSAComponents(n=long(5), e=long(3))._keyObject - key = keys.Key(obj) - self.assertEqual(key._keyObject, obj) - - def test_equal(self): - """ - Test that Key objects are compared correctly. - """ - rsa1 = keys.Key(self.rsaObj) - rsa2 = keys.Key(self.rsaObj) - rsa3 = keys.Key( - keys.Key._fromRSAComponents(n=long(5), e=long(3))._keyObject) - dsa = keys.Key(self.dsaObj) - self.assertTrue(rsa1 == rsa2) - self.assertFalse(rsa1 == rsa3) - self.assertFalse(rsa1 == dsa) - self.assertFalse(rsa1 == object) - self.assertFalse(rsa1 == None) - - def test_notEqual(self): - """ - Test that Key objects are not-compared correctly. - """ - rsa1 = keys.Key(self.rsaObj) - rsa2 = keys.Key(self.rsaObj) - rsa3 = keys.Key( - keys.Key._fromRSAComponents(n=long(5), e=long(3))._keyObject) - dsa = keys.Key(self.dsaObj) - self.assertFalse(rsa1 != rsa2) - self.assertTrue(rsa1 != rsa3) - self.assertTrue(rsa1 != dsa) - self.assertTrue(rsa1 != object) - self.assertTrue(rsa1 != None) - - def test_type(self): - """ - Test that the type method returns the correct type for an object. - """ - self.assertEqual(keys.Key(self.rsaObj).type(), 'RSA') - self.assertEqual(keys.Key(self.rsaObj).sshType(), b'ssh-rsa') - self.assertEqual(keys.Key(self.dsaObj).type(), 'DSA') - self.assertEqual(keys.Key(self.dsaObj).sshType(), b'ssh-dss') - self.assertRaises(RuntimeError, keys.Key(None).type) - self.assertRaises(RuntimeError, keys.Key(None).sshType) - self.assertRaises(RuntimeError, keys.Key(self).type) - self.assertRaises(RuntimeError, keys.Key(self).sshType) - - def test_generate_no_key_type(self): - """ - An error is raised when generating a key with unknown type. - """ - with self.assertRaises(KeyCertException) as context: - Key.generate(key_type=None) - - self.assertEqual( - 'Unknown key type "not-specified".', context.exception.message) - - def test_generate_unknown_type(self): - """ - An error is raised when generating a key with unknown type. - """ - with self.assertRaises(KeyCertException) as context: - Key.generate(key_type='bad-type') - - self.assertEqual( - 'Unknown key type "bad-type".', context.exception.message) - - @attr('slow') - def test_generate_rsa(self): - """ - Check generation of an RSA key with a case insensitive type name. - """ - key = Key.generate(key_type='rSA', key_size=1024) - - self.assertEqual('RSA', key.type()) - self.assertEqual(1024, key.size()) - - @attr('slow') - def test_generate_dsa(self): - """ - Check generation of a DSA key with a case insensitive type name. - """ - key = Key.generate(key_type='dSA', key_size=1024) - - self.assertEqual('DSA', key.type()) - self.assertEqual(1024, key.size()) - - def test_generate_failed(self): - """ - A ServerError is raised when it fails to generate the key. - """ - with self.assertRaises(KeyCertException) as context: - Key.generate(key_type='dSa', key_size=2048) - - self.assertEqual( - u'Wrong key size "2048". Number of bits in p must be a multiple ' - 'of 64 between 512 and 1024, not 2048 bits.', - context.exception.message) - - def test_guessStringType(self): - """ - Test that the _guessStringType method guesses string types - correctly. - - Imported from Twisted. - """ - self.assertEqual( - keys.Key._guessStringType(keydata.publicRSA_openssh), - 'public_openssh') - self.assertEqual( - keys.Key._guessStringType(keydata.publicDSA_openssh), - 'public_openssh') - self.assertEqual( - keys.Key._guessStringType( - keydata.privateRSA_openssh), - 'private_openssh') - self.assertEqual( - keys.Key._guessStringType( - keydata.privateDSA_openssh), - 'private_openssh') - self.assertEqual( - keys.Key._guessStringType(keydata.publicRSA_lsh), - 'public_lsh') - self.assertEqual( - keys.Key._guessStringType(keydata.publicDSA_lsh), - 'public_lsh') - self.assertEqual( - keys.Key._guessStringType(keydata.privateRSA_lsh), - 'private_lsh') - self.assertEqual( - keys.Key._guessStringType(keydata.privateDSA_lsh), - 'private_lsh') - self.assertEqual( - keys.Key._guessStringType( - keydata.privateRSA_agentv3), - 'agentv3') - self.assertEqual( - keys.Key._guessStringType( - keydata.privateDSA_agentv3), - 'agentv3') - self.assertEqual( - keys.Key._guessStringType( - '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x01'), - 'blob') - self.assertEqual( - keys.Key._guessStringType( - '\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x01'), - 'blob') - self.assertEqual( - keys.Key._guessStringType('not a key'), - None) - - def test_guessStringType_unknown(self): - """ - None is returned when could not detect key type. - """ - content = mk.ascii() - - result = Key._guessStringType(content) - - self.assertIsNone(result) - - def test_guessStringType_X509_PEM_certificate(self): - """ - PEM certificates are recognized as public keys. - """ - content = ( - '-----BEGIN CERTIFICATE-----\n' - 'CONTENT\n' - '-----END CERTIFICATE-----\n' - ) - - result = Key._guessStringType(content) - - self.assertEqual('public_x509_certificate', result) - - def test_guessStringType_X509_PUBLIC(self): - """ - x509 public PEM are recognized as public keys. - """ - content = ( - '-----BEGIN PUBLIC KEY-----\n' - 'CONTENT\n' - '-----END PUBLIC KEY-----\n' - ) - - result = Key._guessStringType(content) - - self.assertEqual('public_x509', result) - - def test_guessStringType_PKCS8_PRIVATE(self): - """ - PKS#8 private PEM are recognized as private keys. - """ - content = ( - '-----BEGIN PRIVATE KEY-----\n' - 'CONTENT\n' - '-----END PRIVATE KEY-----\n' - ) - - result = Key._guessStringType(content) - - self.assertEqual('private_pkcs8', result) - - def test_guessStringType_PKCS8_PRIVATE_ENCRYPTED(self): - """ - PKS#8 encrypted private PEM are recognized as private keys. - """ - content = ( - '-----BEGIN ENCRYPTED PRIVATE KEY-----\n' - 'CONTENT\n' - '-----END ENCRYPTED PRIVATE KEY-----\n' - ) - - result = Key._guessStringType(content) - - self.assertEqual('private_encrypted_pkcs8', result) - - def test_guessStringType_private_OpenSSH_RSA(self): - """ - Can recognize an OpenSSH RSA private key. - """ - result = Key._guessStringType(OPENSSH_RSA_PRIVATE) - - self.assertEqual('private_openssh', result) - - def test_guessStringType_private_OpenSSH_DSA(self): - """ - Can recognize an OpenSSH DSA private key. - """ - result = Key._guessStringType(OPENSSH_DSA_PRIVATE) - - self.assertEqual('private_openssh', result) - - def test_guessStringType_private_OpenSSH_ECDSA(self): - """ - Can recognize an OpenSSH ECDSA private key. - """ - result = Key._guessStringType(keydata.privateECDSA_256_openssh) - - self.assertEqual('private_openssh', result) - - def test_guessStringType_public_OpenSSH(self): - """ - Can recognize an OpenSSH public key. - """ - result = Key._guessStringType(OPENSSH_RSA_PUBLIC) - - self.assertEqual('public_openssh', result) - - def test_guessStringType_public_PKCS1(self): - """ - Can recognize an PKCS1 PEM public key. - """ - result = Key._guessStringType(PKCS1_RSA_PUBLIC) - - self.assertEqual('public_pkcs1_rsa', result) - - def test_guessStringType_public_OpenSSH_ECDSA(self): - """ - Can recognize an OpenSSH public key. - """ - result = Key._guessStringType(keydata.publicECDSA_256_openssh) - - self.assertEqual('public_openssh', result) - - result = Key._guessStringType(keydata.publicECDSA_384_openssh) - - self.assertEqual('public_openssh', result) - - result = Key._guessStringType(keydata.publicECDSA_521_openssh) - - self.assertEqual('public_openssh', result) - - def test_guessStringType_private_SSHCOM(self): - """ - Can recognize an SSH.com private key. - """ - result = Key._guessStringType(SSHCOM_RSA_PRIVATE_NO_PASSWORD) - - self.assertEqual('private_sshcom', result) - - def test_guessStringType_public_SSHCOM(self): - """ - Can recognize an SSH.com public key. - """ - result = Key._guessStringType(SSHCOM_RSA_PUBLIC) - - self.assertEqual('public_sshcom', result) - - def test_guessStringType_putty(self): - """ - Can recognize a Putty private key. - """ - result = Key._guessStringType(PUTTY_RSA_PRIVATE_NO_PASSWORD) - - self.assertEqual('private_putty', result) - - def test_getKeyFormat_unknown(self): - """ - Inform using a human readable text that format is not known. - """ - result = Key.getKeyFormat('no-such-format') - - self.assertEqual('Unknown format', result) - - def test_getKeyFormat_known(self): - """ - Return the human readable description of key format. - """ - - result = Key.getKeyFormat(SSHCOM_RSA_PUBLIC) - - self.assertEqual('SSH.com Public', result) - - def test_public_get(self): - """ - Return an instance of same class but with only public elements for - the private key. - """ - sut = Key.fromString(OPENSSH_RSA_PRIVATE) - - result = sut.public() - - self.assertFalse(sut.isPublic()) - self.assertIsInstance(Key, result) - self.assertTrue(result.isPublic()) - self.assertEqual(result.data()['e'], sut.data()['e']) - self.assertEqual(result.data()['n'], sut.data()['n']) - - def test_fromFile(self): - """ - Test that fromFile works correctly. - """ - self.test_segments = mk.fs.createFileInTemp( - content=keydata.privateRSA_openssh) - key_path = mk.fs.getRealPathFromSegments(self.test_segments) - - self.assertEqual( - keys.Key.fromFile(key_path), - keys.Key.fromString(keydata.privateRSA_openssh)) - - self.assertRaises( - keys.BadKeyError, keys.Key.fromFile, key_path, 'bad_type') - - def test_fromString_type_unkwown(self): - """ - An exceptions is raised when reading a key for which type could not - be detected. Exception only contains the beginning of the content. - """ - content = mk.ascii() * 100 - - self.assertBadKey( - content, 'Cannot guess the type for "%s"' % content[:80]) - - def test_fromString_struct_errors(self): - """ - Errors caused by parsing the content are raises as BadKeyError. - """ - content = OPENSSH_DSA_PUBLIC[:32] - - self.assertKeyParseError(content) - - def test_fromString_errors(self): - """ - keys.Key.fromString should raise BadKeyError when the key is invalid. - """ - self.assertRaises(keys.BadKeyError, keys.Key.fromString, '') - # no key data with a bad key type - self.assertRaises( - keys.BadKeyError, keys.Key.fromString, '', 'bad_type') - # trying to decrypt a key which doesn't support encryption - self.assertRaises( - keys.BadKeyError, - keys.Key.fromString, - keydata.publicRSA_lsh, passphrase='unencrypted') - # trying t fo decrypt a key with the wrong passphrase - self.assertRaises( - keys.EncryptedKeyError, - keys.Key.fromString, - keys.Key(self.rsaObj).toString('openssh', 'encrypted')) - # key with no key data - self.assertRaises( - keys.BadKeyError, - keys.Key.fromString, - '-----BEGIN RSA KEY-----\nwA==\n') - # key with invalid DEK Info - self.assertRaises( - keys.BadKeyError, - keys.Key.fromString, - """-----BEGIN ENCRYPTED RSA KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: weird type - -4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n -T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H -g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB -sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5 -9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV -gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW -0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE -vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS -hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk -2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf -qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk -4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY -EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n -8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0 -fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P -V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+ -0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5 -xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI -dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup -VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk -gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c -8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw -SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 -CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE -xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase='encrypted') - # key with invalid encryption type - self.assertRaises( - keys.BadKeyError, keys.Key.fromString, - """-----BEGIN ENCRYPTED RSA KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: FOO-123-BAR,01234567 - -4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n -T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H -g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB -sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5 -9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV -gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW -0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE -vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS -hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk -2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf -qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk -4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY -EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n -8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0 -fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P -V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+ -0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5 -xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI -dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup -VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk -gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c -8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw -SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 -CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE -xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase='encrypted') - # key with bad IV (AES) - self.assertRaises( - keys.BadKeyError, keys.Key.fromString, - """-----BEGIN ENCRYPTED RSA KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: AES-128-CBC,01234 - -4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n -T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H -g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB -sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5 -9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV -gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW -0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE -vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS -hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk -2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf -qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk -4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY -EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n -8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0 -fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P -V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+ -0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5 -xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI -dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup -VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk -gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c -8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw -SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 -CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE -xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase='encrypted') - # key with bad IV (DES3) - self.assertRaises( - keys.BadKeyError, keys.Key.fromString, - """-----BEGIN ENCRYPTED RSA KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,01234 - -4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n -T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H -g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB -sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5 -9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV -gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW -0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE -vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS -hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk -2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf -qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk -4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY -EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n -8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0 -fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P -V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+ -0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5 -xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI -dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup -VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk -gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c -8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw -SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 -CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE -xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase='encrypted') - - def test_toStringErrors(self): - """ - Test that toString raises errors appropriately. - """ - self.assertRaises( - keys.BadKeyError, keys.Key(self.rsaObj).toString, 'bad_type') - - def test_fromString_BLOB_blob_type_non_ascii(self): - """ - Raise with printable information for the bad type, - even if blob type has non-ascii data. - """ - badBlob = common.NS('ssh-\xbd\xbd\xbd') - self.assertBadKey( - badBlob, - 'Cannot guess the type for "' - r'\x00\x00\x00' + '\n' + r'ssh-\xc2\xbd\xc2\xbd\xc2\xbd"' - ) - - def test_fromString_PRIVATE_BLOB(self): - """ - Test that a private key is correctly generated from a private key blob. - """ - rsaBlob = (common.NS('ssh-rsa') + common.MP(2) + common.MP(3) + - common.MP(4) + common.MP(5) + common.MP(6) + common.MP(7)) - rsaKey = keys.Key._fromString_PRIVATE_BLOB(rsaBlob) - dsaBlob = (common.NS('ssh-dss') + common.MP(2) + common.MP(3) + - common.MP(4) + common.MP(5) + common.MP(6)) - dsaKey = keys.Key._fromString_PRIVATE_BLOB(dsaBlob) - badBlob = common.NS('ssh-bad') - self.assertFalse(rsaKey.isPublic()) - self.assertEqual( - rsaKey.data(), - {'n': 2L, 'e': 3L, 'd': 4L, 'u': 5L, 'p': 6L, 'q': 7L}) - self.assertFalse(dsaKey.isPublic()) - self.assertEqual( - dsaKey.data(), {'p': 2L, 'q': 3L, 'g': 4L, 'y': 5L, 'x': 6L}) - self.assertRaises( - keys.BadKeyError, keys.Key._fromString_PRIVATE_BLOB, badBlob) - - def test_blobRSA(self): - """ - Return the over-the-wire SSH format of the RSA public key. - """ - self.assertEqual( - keys.Key(self.rsaObj).blob(), - common.NS(b'ssh-rsa') + - common.MP(self.rsaObj.private_numbers().public_numbers.e) + - common.MP(self.rsaObj.private_numbers().public_numbers.n) - ) - - def test_blobDSA(self): - """ - Return the over-the-wire SSH format of the DSA public key. - """ - publicNumbers = self.dsaObj.private_numbers().public_numbers - - self.assertEqual( - keys.Key(self.dsaObj).blob(), - common.NS(b'ssh-dss') + - common.MP(publicNumbers.parameter_numbers.p) + - common.MP(publicNumbers.parameter_numbers.q) + - common.MP(publicNumbers.parameter_numbers.g) + - common.MP(publicNumbers.y) - ) - - def test_blobEC(self): - """ - Return the over-the-wire SSH format of the EC public key. - """ - from cryptography import utils - - byteLength = (self.ecObj.curve.key_size + 7) // 8 - self.assertEqual( - keys.Key(self.ecObj).blob(), - common.NS(keydata.ECDatanistp256['curve']) + - common.NS(keydata.ECDatanistp256['curve'][-8:]) + - common.NS(b'\x04' + - utils.int_to_bytes( - self.ecObj.private_numbers().public_numbers.x, byteLength) + - utils.int_to_bytes( - self.ecObj.private_numbers().public_numbers.y, byteLength)) - ) - - def test_blobEd25519(self): - """ - Return the over-the-wire SSH format of the Ed25519 public key. - """ - from cryptography.hazmat.primitives import serialization - - publicBytes = self.ed25519Obj.public_key().public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw - ) - - self.assertEqual( - keys.Key(self.ed25519Obj).blob(), - common.NS(b'ssh-ed25519') + - common.NS(publicBytes) - ) - - def test_blobNoKey(self): - """ - C{RuntimeError} is raised when the blob is requested for a Key - which is not wrapping anything. - """ - badKey = keys.Key(None) - - self.assertRaises(RuntimeError, badKey.blob) - - def test_privateBlobRSA(self): - """ - L{keys.Key.privateBlob} returns the SSH protocol-level format of an - RSA private key. - """ - numbers = self.rsaObj.private_numbers() - self.assertEqual( - keys.Key(self.rsaObj).privateBlob(), - common.NS(b'ssh-rsa') + - common.MP(numbers.public_numbers.n) + - common.MP(numbers.public_numbers.e) + - common.MP(numbers.d) + - common.MP(numbers.iqmp) + - common.MP(numbers.p) + - common.MP(numbers.q) - ) - - def test_privateBlobDSA(self): - """ - L{keys.Key.privateBlob} returns the SSH protocol-level format of a DSA - private key. - """ - publicNumbers = self.dsaObj.private_numbers().public_numbers - - self.assertEqual( - keys.Key(self.dsaObj).privateBlob(), - common.NS(b'ssh-dss') + - common.MP(publicNumbers.parameter_numbers.p) + - common.MP(publicNumbers.parameter_numbers.q) + - common.MP(publicNumbers.parameter_numbers.g) + - common.MP(publicNumbers.y) + - common.MP(self.dsaObj.private_numbers().x) - ) - - def test_privateBlobEC(self): - """ - L{keys.Key.privateBlob} returns the SSH ptotocol-level format of EC - private key. - """ - from cryptography.hazmat.primitives import serialization - self.assertEqual( - keys.Key(self.ecObj).privateBlob(), - common.NS(keydata.ECDatanistp256['curve']) + - common.NS(keydata.ECDatanistp256['curve'][-8:]) + - common.NS( - self.ecObj.public_key().public_bytes( - serialization.Encoding.X962, - serialization.PublicFormat.UncompressedPoint)) + - common.MP(self.ecObj.private_numbers().private_value) - ) - - def test_privateBlobEd25519(self): - """ - L{keys.Key.privateBlob} returns the SSH protocol-level format of an - Ed25519 private key. - """ - from cryptography.hazmat.primitives import serialization - publicBytes = self.ed25519Obj.public_key().public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw - ) - privateBytes = self.ed25519Obj.private_bytes( - serialization.Encoding.Raw, - serialization.PrivateFormat.Raw, - serialization.NoEncryption() - ) - - self.assertEqual( - keys.Key(self.ed25519Obj).privateBlob(), - common.NS(b'ssh-ed25519') + - common.NS(publicBytes) + - common.NS(privateBytes + publicBytes) - ) - - def test_privateBlobNoKeyObject(self): - """ - Raises L{RuntimeError} if the underlying key object does not exists. - """ - badKey = keys.Key(None) - - self.assertRaises(RuntimeError, badKey.privateBlob) - - def test_fromString_PUBLIC_OPENSSH_RSA(self): - """ - Can load public RSA OpenSSH key. - """ - sut = Key.fromString(OPENSSH_RSA_PUBLIC) - - self.checkParsedRSAPublic1024(sut) - - def test_fromString_PUBLIC_PKC1_RSA(self): - """ - Can load public RSA PKC1 key. - """ - sut = Key.fromString(PKCS1_RSA_PUBLIC) - - self.checkParsedRSAPublic1024(sut) - - def test_fromString_PUBLIC_OPENSSH_RSA_too_short(self): - """ - An exception is raised when public RSA OpenSSH key is bad formatted. - """ - self.assertKeyIsTooShort('ssh-rsa') - - def test_fromString_PUBLIC_OPENSSH_invalid_payload(self): - """ - Raise an exception when key blob has a bad format. - """ - self.assertKeyParseError('ssh-rsa AAAAB3NzaC1yc2EA') - - def test_fromString_PUBLIC_OPENSSH_DSA(self): - """ - Can load a public OpenSSH in DSA format. - """ - sut = Key.fromString(OPENSSH_DSA_PUBLIC) - - self.checkParsedDSAPublic1024(sut) - - def test_fromString_OpenSSH(self): - """ - Test that keys are correctly generated from OpenSSH strings. - """ - self._testPublicPrivateFromString( - keydata.publicRSA_openssh, - keydata.privateRSA_openssh, 'RSA', keydata.RSAData) - - self.assertEqual( - keys.Key.fromString( - keydata.privateRSA_openssh_encrypted, - passphrase='encrypted'), - keys.Key.fromString(keydata.privateRSA_openssh)) - - self.assertEqual( - keys.Key.fromString( - keydata.privateRSA_openssh_alternate), - keys.Key.fromString(keydata.privateRSA_openssh)) - - self._testPublicPrivateFromString( - keydata.publicDSA_openssh, - keydata.privateDSA_openssh, 'DSA', keydata.DSAData) - - def test_fromString_OpenSSH_private_missing_password(self): - """ - It fails to load an ecrypted key when password is not provided. - """ - with self.assertRaises(EncryptedKeyError) as context: - keys.Key.fromString(keydata.privateRSA_openssh_encrypted) - - self.assertEqual( - 'Passphrase must be provided for an encrypted key', - context.exception.message, - ) - - def test_fromString_PRIVATE_OPENSSH_with_whitespace(self): - """ - If key strings have trailing whitespace, it should be ignored. - """ - # from Twisted bug #3391, since our test key data doesn't have - # an issue with appended newlines - privateDSAData = """-----BEGIN DSA PRIVATE KEY----- -MIIBuwIBAAKBgQDylESNuc61jq2yatCzZbenlr9llG+p9LhIpOLUbXhhHcwC6hrh -EZIdCKqTO0USLrGoP5uS9UHAUoeN62Z0KXXWTwOWGEQn/syyPzNJtnBorHpNUT9D -Qzwl1yUa53NNgEctpo4NoEFOx8PuU6iFLyvgHCjNn2MsuGuzkZm7sI9ZpQIVAJiR -9dPc08KLdpJyRxz8T74b4FQRAoGAGBc4Z5Y6R/HZi7AYM/iNOM8su6hrk8ypkBwR -a3Dbhzk97fuV3SF1SDrcQu4zF7c4CtH609N5nfZs2SUjLLGPWln83Ysb8qhh55Em -AcHXuROrHS/sDsnqu8FQp86MaudrqMExCOYyVPE7jaBWW+/JWFbKCxmgOCSdViUJ -esJpBFsCgYEA7+jtVvSt9yrwsS/YU1QGP5wRAiDYB+T5cK4HytzAqJKRdC5qS4zf -C7R0eKcDHHLMYO39aPnCwXjscisnInEhYGNblTDyPyiyNxAOXuC8x7luTmwzMbNJ -/ow0IqSj0VF72VJN9uSoPpFd4lLT0zN8v42RWja0M8ohWNf+YNJluPgCFE0PT4Vm -SUrCyZXsNh6VXwjs3gKQ ------END DSA PRIVATE KEY-----""" - self.assertEqual(keys.Key.fromString(privateDSAData), - keys.Key.fromString(privateDSAData + '\n')) - - def test_fromString_PRIVATE_OPENSSH_newer(self): - """ - Newer versions of OpenSSH generate encrypted keys which have a longer - IV than the older versions. These newer keys are also loaded. - """ - key = keys.Key.fromString(keydata.privateRSA_openssh_encrypted_aes, - passphrase='testxp') - self.assertEqual(key.type(), 'RSA') - key2 = keys.Key.fromString( - keydata.privateRSA_openssh_encrypted_aes + '\n', - passphrase='testxp') - self.assertEqual(key, key2) - - def test_fromString_PRIVATE_OPENSSH_not_encrypted_with_passphrase(self): - """ - When loading a unencrypted OpenSSH private key with passhphrase - will raise BadKeyError. - """ - - with self.assertRaises(BadKeyError) as context: - Key.fromString(OPENSSH_RSA_PRIVATE, passphrase='pass') - - self.assertEqual( - 'OpenSSH key not encrypted', - context.exception.message) - - def test_toString_OPENSSH(self): - """ - Test that the Key object generates OpenSSH keys correctly. - """ - key = keys.Key.fromString(keydata.privateRSA_lsh) - - self.assertEqual(key.toString('openssh'), keydata.privateRSA_openssh) - self.assertEqual( - key.toString('openssh', 'encrypted'), - keydata.privateRSA_openssh_encrypted) - self.assertEqual( - key.public().toString('openssh'), - keydata.publicRSA_openssh[:-8]) - self.assertEqual( - key.public().toString('openssh', 'comment'), - keydata.publicRSA_openssh) - - key = keys.Key.fromString(keydata.privateDSA_lsh) - - self.assertEqual(key.toString('openssh'), keydata.privateDSA_openssh) - self.assertEqual( - key.public().toString('openssh', 'comment'), - keydata.publicDSA_openssh) - self.assertEqual( - key.public().toString('openssh'), keydata.publicDSA_openssh[:-8]) - - def addSSHCOMKeyHeaders(self, source, headers): - """ - Add headers to a SSH.com key. - - Long headers are wrapped at 70 characters. - """ - lines = source.splitlines() - for key, value in headers.items(): - line = '%s: %s' % (key, value.encode('utf-8')) - header = '\\\n'.join(textwrap.wrap(line, 70)) - lines.insert(1, header) - return '\n'.join(lines) - - def checkParsedDSAPublic1024(self, sut): - """ - Check the default public DSA key of size 1024. - - This is a shared test for parsing DSA key from various formats. - """ - self.assertEqual(1024, sut.size()) - self.assertEqual('DSA', sut.type()) - self.assertTrue(sut.isPublic()) - self.checkParsedDSAPublic1024Data(sut) - - def checkParsedDSAPublic1024Data(self, sut): - """ - Check the public part values for the default DSA key of size 1024. - """ - data = sut.data() - self.assertEqual(long( - '89826398702575694025672739759021185748719093895775418981133245507' - '56542191015877768589699407493932539140865803919573940821357868468' - '55675657634384222748339103943127442354510383477300256462657784441' - '71019786268219332779725063911288445634960873466719023048095246499' - '763675183656402590703132265805882271082319033570L'), - data['y']) - self.assertEqual(long( - '14519098631088118929874535941241101897542246758347965800832728196' - '81139199597265476885338795620826004398884602230901691384070382776' - '92982149652731866793940314712388781003443391479314606037340161379' - '86631331044475413634865132557582890274917465191550388575486379853' - '0603422003777150811982254140040687593424378397517L'), - data['p']) - self.assertEqual( - long('765629040155792319453907037659138573169171493193L'), - data['q']) - self.assertEqual(long( - '64647318098084998690447943642968245369499209364165550549740815561' - '71156388976417089337555666453157891497405105710031098879473402131' - '15408225147127626829407642540707192214402604495716677723330515779' - '34611656548484464881147166978432509157365635746874869548636130785' - '946819310836368885242376237240564866586977240572L'), - data['g']) - - def checkParsedDSAPrivate1024(self, sut): - """ - Check the default private DSA key of size 1024. - """ - self.assertEqual(1024, sut.size()) - self.assertEqual('DSA', sut.type()) - self.assertFalse(sut.isPublic()) - data = sut.data() - self.checkParsedDSAPublic1024Data(sut) - self.assertEqual(long( - '447059752886431435417087644871194130561824720094L'), - data['x']) - - def checkParsedRSAPublic1024(self, sut): - """ - Check the default public RSA key of size 1024. - """ - self.assertEqual(1024, sut.size()) - self.assertEqual('RSA', sut.type()) - self.assertTrue(sut.isPublic()) - self.checkParsedRSAPublic1024Data(sut) - - def checkParsedRSAPublic1024Data(self, sut): - """ - Check data for public RSA key of size 1024. - """ - data = sut.data() - self.assertEqual(65537L, data['e']) - self.assertEqual(long( - '12955309129371696361961156024018278506192853914566590418922947244' - '33008028380639675460754206681134187533029942882729688747039044313' - '67411245192523108247958392655021595783971049572916657240822239036' - '02442387266290082476044614892594356080524766995335587624348179950' - '6405887692619349988915280409504938876523941259567L'), - data['n']) - - def checkParsedRSAPrivate1024(self, sut): - """ - Check the default private RSA key of size 1024. - """ - self.assertEqual(1024, sut.size()) - self.assertEqual('RSA', sut.type()) - self.assertFalse(sut.isPublic()) - data = sut.data() - self.assertEqual(65537L, data['e']) - self.checkParsedRSAPublic1024Data(sut) - self.assertEqual(long( - '57010713839675255669157840568333483699071044890077432241594488384' - '64981848192265169337649163172545274951948296799964023904757013291' - '17313931268194522463817291948793747715146018146051093951466872189' - '64147610108577761761364098616952641696814228146724216997423652825' - '24517268536277980834876649127946895862158846465L'), - data['d']) - self.assertEqual(long( - '10661640454627350493191065484215149934251067848734449698668476614' - '18981319570111200535213963399376281314470995958266981264747210946' - '6364885923117389812635119L'), - data['p']) - self.assertEqual(long( - '12151328104249520956550929707892880056509323657595783040548358917' - '98785549316902458371621691657702435263762556929800891556172971312' - '6473919204485168003686593L'), - data['q']) - self.assertEqual(long( - '66777727502990278851698381429390065987141247478987840061938912337' - '88877413103516203638312270220327073357315389300205491590285175084' - '040066037688353071226161L'), - data['u']) - - def test_fromString_PUBLIC_SSHCOM_RSA_no_headers(self): - """ - Can load a public RSA SSH.com key which has no headers. - """ - sut = Key.fromString(SSHCOM_RSA_PUBLIC) - - self.checkParsedRSAPublic1024(sut) - - def test_fromString_PUBLIC_SSHCOM_RSA_public_headers(self): - """ - Can import a public RSA SSH.com key with headers. - """ - key_content = self.addSSHCOMKeyHeaders( - source=SSHCOM_RSA_PUBLIC, - headers={ - 'Comment': '"short comment"', - 'Subject': 'Very very long subject' * 10, - 'x-private': mk.string(), - }, - ) - sut = Key.fromString(key_content) - - self.assertEqual(1024, sut.size()) - self.assertEqual('RSA', sut.type()) - self.assertTrue(sut.isPublic()) - data = sut.data() - self.assertEqual(65537L, data['e']) - - def test_fromString_PUBLIC_SSHCOM_DSA(self): - """ - Can load a public SSH.com in DSA format. - """ - sut = Key.fromString(SSHCOM_DSA_PUBLIC) - - self.checkParsedDSAPublic1024(sut) - - def test_fromString_PUBLIC_SSHCOM_no_end_tag(self): - """ - Raise an exception when there is no END tag. - """ - content = '---- BEGIN SSH2 PUBLIC KEY ----' - - self.assertBadKey(content, 'Fail to find END tag for SSH.com key.') - - content = '---- BEGIN SSH2 PUBLIC KEY ----\nnext line' - - self.assertBadKey(content, 'Fail to find END tag for SSH.com key.') - - def test_fromString_PUBLIC_SSHCOM_RSA_invalid_payload(self): - """ - Raise an exception when key has a bad format. - """ - content = """---- BEGIN SSH2 PUBLIC KEY ---- -AAAAB3NzaC1yc2EA ----- END SSH2 PUBLIC KEY ----""" - - self.assertKeyParseError(content) - - def test_toString_SSHCOM_RSA_public_no_headers(self): - """ - Can export a public RSA SSH.com key with headers. - """ - sut = Key.fromString(OPENSSH_RSA_PUBLIC) - - result = sut.toString(type='sshcom') - - self.assertEqual(SSHCOM_RSA_PUBLIC, result) - - def test_toString_SSHCOM_RSA_public_with_comment(self): - """ - Can export a public RSA SSH.com key with headers. - """ - sut = Key.fromString(OPENSSH_RSA_PUBLIC) - comment = mk.string() * 20 - - result = sut.toString(type='sshcom', extra=comment) - - expected = self.addSSHCOMKeyHeaders( - source=SSHCOM_RSA_PUBLIC, - headers={'Comment': '"%s"' % comment}, - ) - self.assertEqual(expected, result) - - def test_toString_SSHCOM_DSA_public(self): - """ - Can export a public DSA SSH.com key. - """ - sut = Key.fromString(OPENSSH_DSA_PUBLIC) - - result = sut.toString(type='sshcom') - - self.assertEqual(SSHCOM_DSA_PUBLIC, result) - - def test_fromString_PRIVATE_OPENSSH_RSA(self): - """ - Can load a private OpenSSH RSA key. - """ - sut = Key.fromString(OPENSSH_RSA_PRIVATE) - - self.checkParsedRSAPrivate1024(sut) - - def test_fromString_PRIVATE_OPENSSH_v1_RSA(self): - """ - Can load a private OpenSSH v1 RSA key. - """ - sut = Key.fromString(OPENSSH_V1_RSA_PRIVATE) - - self.checkParsedRSAPrivate1024(sut) - - def test_fromString_PRIVATE_OPENSSH_DSA(self): - """ - Can load a private OpenSSH DSA key. - """ - sut = Key.fromString(OPENSSH_DSA_PRIVATE) - - self.checkParsedDSAPrivate1024(sut) - - def test_fromString_PRIVATE_OPENSSH_v1_DSA(self): - """ - Can load a private OpenSSH V1 DSA key. - """ - sut = Key.fromString(OPENSSH_V1_DSA_PRIVATE) - - self.checkParsedDSAPrivate1024(sut) - - def test_fromString_PRIVATE_OPENSSH_ECDSA(self): - """ - Can not load a private OPENSSH ECDSA. - """ - self.assertBadKey( - keydata.privateECDSA_256_openssh, - 'Key type \'EC\' not supported.' - ) - - def test_fromString_PRIVATE_OPENSSH_short(self): - """ - Raise an error when private OpenSSH key is too short. - """ - content = '-----BEGIN RSA PRIVATE KEY-----' - - self.assertKeyIsTooShort(content) - - content = '-----BEGIN RSA PRIVATE KEY-----\nAnother Line' - - self.assertBadKey( - content, - 'Failed to decode key (Bad Passphrase?): ' - 'Short octet stream on tag decoding') - - def test_fromString_PRIVATE_OPENSSH_bad_encoding(self): - """ - Raise an error when private OpenSSH key data can not be decoded. - """ - content = '-----BEGIN RSA PRIVATE KEY-----\nAnother Line\nLast' - - self.assertKeyParseError(content) - - def test_fromString_PRIVATE_SSHCOM_unencrypted_with_passphrase(self): - """ - When loading a unencrypted SSH.com private key with passhphrase - will raise BadKeyError. - """ - - with self.assertRaises(BadKeyError) as context: - Key.fromString(SSHCOM_RSA_PRIVATE_NO_PASSWORD, passphrase='pass') - - self.assertEqual( - 'SSH.com key not encrypted', - context.exception.message) - - def test_fromString_PRIVATE_SSHCOM_RSA_no_headers_no_password(self): - """ - Can load a private SSH.com key which has no headers and no password. - """ - sut = Key.fromString(SSHCOM_RSA_PRIVATE_NO_PASSWORD) - - self.checkParsedRSAPrivate1024(sut) - - def test_fromString_PRIVATE_SSHCOM_RSA_encrypted(self): - """ - Can load a private SSH.com key encrypted with password`. - """ - sut = Key.fromString( - SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase='chevah') - - self.checkParsedRSAPrivate1024(sut) - - def test_fromString_PRIVATE_SSHCOM_DSA_no_password(self): - """ - Can load a private SSH.com in DSA format. - """ - sut = Key.fromString(SSHCOM_DSA_PRIVATE_NO_PASSWORD) - - self.checkParsedDSAPrivate1024(sut) - - def test_fromString_PRIVATE_SSHCOM_short(self): - """ - Raise an exception when private key is too short. - """ - content = '---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----' - - self.assertKeyParseError(content) - - content = '---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\nnext line' - - self.assertKeyParseError(content) - - def test_fromString_PRIVATE_SSHCOM_RSA_encrypted_no_password(self): - """ - An exceptions is raised whey trying to load a private SSH.com key - which is encrypted, but without providing a password. - """ - with self.assertRaises(EncryptedKeyError) as context: - Key.fromString(SSHCOM_RSA_PRIVATE_WITH_PASSWORD) - - self.assertEqual( - 'Passphrase must be provided for an encrypted key.', - context.exception.message) - - def test_fromString_PRIVATE_SSHCOM_RSA_with_wrong_password(self): - """ - An exceptions is raised whey trying to load a private SSH.com key - which is encrypted, but providing a wrong password. - """ - with self.assertRaises(EncryptedKeyError) as context: - Key.fromString(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase='on') - - self.assertEqual( - 'Bad password or bad key format.', - context.exception.message) - - def test_fromString_PRIVATE_OPENSSH_bad_magic(self): - """ - Exception is raised when key data does not start with the key marker. - """ - content = """---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- -B2/56wAAAi4AAAA3 ----- END SSH2 ENCRYPTED PRIVATE KEY ----""" - - self.assertBadKey( - content, 'Bad magic number for SSH.com key "124778987"') - - def test_fromString_PRIVATE_OPENSSH_bad_key_type(self): - """ - Exception is raised when key has an unknown type. - """ - content = """---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- -P2/56wAAAi4AAAA3aWYtbW9kbntzaW== ----- END SSH2 ENCRYPTED PRIVATE KEY ----""" - - self.assertBadKey(content, 'Unknown SSH.com key type "if-modn{si"') - - def test_fromString_PRIVATE_OPENSSH_bad_structure(self): - """ - Exception is raised when key has no valid parts, ie too short. - """ - content = """---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- -P2/56wAAAi4AAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0e3JzYS -1wa2NzMXYyLW9hZXB9fQAAAARub25l ----- END SSH2 ENCRYPTED PRIVATE KEY ----""" - - self.assertKeyParseError(content) - - def test_fromString_X509_PEM_invalid_format(self): - """ - It fails to load invalid formated X509 PEM certificate. - """ - data = """-----BEGIN CERTIFICATE----- -MIIBNDCB66ADAgECAgEBMAoGCCqGSM49BAMCMDQxCzAJBgNVBAYTAkdCMQ8wDQYD -8J4wCgYIKoZIzj0EAwIDOAAwNQIZANYXcrq622yfNJSyjlzDvk3w59IaOlljqwIY -Gt7MBDMYYr8yfcZS94pZEUfhebR3CYAZ ------END CERTIFICATE----- -""" - with self.assertRaises(BadKeyError) as context: - Key.fromString(data) - - self.assertStartsWith( - "Failed to load certificate. [('asn1 encoding routines'", - context.exception.message, - ) - - def test_fromString_X509_PEM_EC(self): - """ - EC public key from an X509 PEM certificate are not supported. - """ - data = """-----BEGIN CERTIFICATE----- -MIIBNDCB66ADAgECAgEBMAoGCCqGSM49BAMCMDQxCzAJBgNVBAYTAkdCMQ8wDQYD -VQQKEwZDaGV2YWgxFDASBgNVBAMTC3Rlc3QtZWMtc3NoMB4XDTE5MDYxOTEyNTQw -MFoXDTIwMDYxOTEyNTQwMFowNDELMAkGA1UEBhMCR0IxDzANBgNVBAoTBkNoZXZh -aDEUMBIGA1UEAxMLdGVzdC1lYy1zc2gwSTATBgcqhkjOPQIBBggqhkjOPQMBAQMy -AARzpUpSPLojoyouYH7HhSFV661wUKrRVqLyJlBb1cWU8f4wLZsGkXymZpAPClwu -8J4wCgYIKoZIzj0EAwIDOAAwNQIZANYXcrq622yfNJSyjlzDvk3w59IaOlljqwIY -Gt7MBDMYYr8yfcZS94pZEUfhebR3CYAZ ------END CERTIFICATE----- -""" - with self.assertRaises(BadKeyError) as context: - Key.fromString(data) - - self.assertEqual( - 'Unsupported key found in the certificate.', - context.exception.message, - ) - - def test_fromString_PKCS1_PUBLIC_EC(self): - """ - It can extract RSA public key from an PKCS1 public EC PEM file. - """ - # This is the same as the X509 RSA cert. - # $ openssl x509 -in bla.cert -pubkey -noout - data = """-----BEGIN PUBLIC KEY----- -MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEc6VKUjy6I6MqLmB+x4UhVeutcFCq -0Vai8iZQW9XFlPH+MC2bBpF8pmaQDwpcLvCe ------END PUBLIC KEY----- -""" - with self.assertRaises(BadKeyError) as context: - Key.fromString(data) - - self.assertEqual( - 'Unsupported key found in the X509 public PEM file.', - context.exception.message, - ) - - def test_fromString_X509_PEM_RSA(self): - """ - It can extract RSA public key from an X509 PEM certificate - """ - data = """-----BEGIN CERTIFICATE----- -MIICaDCCAdGgAwIBAgIBDjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJHQjEP -MA0GA1UEChMGQ2hldmFoMRIwEAYDVQQLEwlDaGV2YWggQ0ExEjAQBgNVBAMTCUNo -ZXZhaCBDQTAeFw0xNjA2MTUxNDM4MDBaFw0zNjA2MTUxNDM4MDBaMEgxCzAJBgNV -BAYTAkdCMQ8wDQYDVQQKEwZDaGV2YWgxFDASBgNVBAsTC0NoZXZhaCBUZXN0MRIw -EAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM6h -lRh3woxhut7nNkjBH5Xp07b5wJhVLjoEdtFuq3uBzOSghaEpapeL0/M4Rpw9ANjy -ulGy7rwJI9Me95aG53BrjMbBKk1qaHuNXa3PJjcgVmPelwPcbzk5Wl4E57dLN+eh -4Rf/Qyi9HBdtrDf19OzBmBs7W7pO9LPo5/usHlyVAgMBAAGjZDBiMBMGA1UdJQQM -MAoGCCsGAQUFBwMBMDgGA1UdHwQxMC8wLaAroCmGJ2h0dHA6Ly9sb2NhbGhvc3Q6 -ODA4MC9zb21lLWNoaWxkL2NhLmNybDARBglghkgBhvhCAQEEBAMCBkAwDQYJKoZI -hvcNAQEFBQADgYEAM8Ro0XZeIrR7+fi4pGMdqTAdNFNd2O86YgzpvGpUIbhmJnty -1k0aF2QNot4M6i6OhVQEwL4Ph/l6pbOnusv238nuzHyDHFWNPy1wV02hjacXF9EW -JZQaMjV9XxNTFOlNUTWswff3uE677wSVDPSuNkxo2FLRcGfPUxAQGsgL5Ts= ------END CERTIFICATE----- -""" - - sut = Key.fromString(data) - - self.assertTrue(sut.isPublic()) - self.assertEqual('RSA', sut.type()) - self.assertEqual(1024, sut.size()) - - components = sut.data() - self.assertEqual(65537L, components['e']) - n = long( - '14510135000543456324610075074919561379239940215773254633039625814' - '50191438083097108908667737243399472490927083264564327600896049375' - '92092816317169486450111458914839337717035721053431064458247582292' - '33425907841901335798792724220900289242783534069221630733833594745' - '1002424312049140771718167143894887320401855011989L' - ) - self.assertEqual(n, components['n']) - - def test_fromString_PKCS1_PUBLIC_PEM_invalid_format(self): - """ - It fails to load invalid formated PKCS1 public PEM file. - """ - data = """-----BEGIN PUBLIC KEY----- -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOoZUYd8KMYbre5zZIwR+V6dO2 -O1u6TvSz6Of7rB5clQIDAQAB ------END PUBLIC KEY----- -""" - with self.assertRaises(BadKeyError) as context: - Key.fromString(data) - - self.assertStartsWith( - "Failed to load PKCS#1 public key. [('asn1 encoding routines'", - context.exception.message, - ) - - def test_fromString_PKCS1_PUBLIC_RSA(self): - """ - It can extract RSA public key from an PKCS1 public RSA PEM file. - """ - # This is the same as the X509 RSA cert. - # $ openssl x509 -in bla.cert -pubkey -noout - data = """-----BEGIN PUBLIC KEY----- -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOoZUYd8KMYbre5zZIwR+V6dO2 -+cCYVS46BHbRbqt7gczkoIWhKWqXi9PzOEacPQDY8rpRsu68CSPTHveWhudwa4zG -wSpNamh7jV2tzyY3IFZj3pcD3G85OVpeBOe3SzfnoeEX/0MovRwXbaw39fTswZgb -O1u6TvSz6Of7rB5clQIDAQAB ------END PUBLIC KEY----- -""" - - sut = Key.fromString(data) - - self.assertTrue(sut.isPublic()) - self.assertEqual('RSA', sut.type()) - self.assertEqual(1024, sut.size()) - - components = sut.data() - self.assertEqual(65537L, components['e']) - n = long( - '14510135000543456324610075074919561379239940215773254633039625814' - '50191438083097108908667737243399472490927083264564327600896049375' - '92092816317169486450111458914839337717035721053431064458247582292' - '33425907841901335798792724220900289242783534069221630733833594745' - '1002424312049140771718167143894887320401855011989L' - ) - self.assertEqual(n, components['n']) - - def test_fromString_X509_PEM_DSA(self): - """ - It can extract DSA public key from an X509 PEM certificate - """ - data = """-----BEGIN CERTIFICATE----- -MIICsDCCAm6gAwIBAgIBATALBglghkgBZQMEAwIwPTELMAkGA1UEBhMCR0IxDzAN -BgNVBAoTBkNoZXZhaDEdMBsGA1UEAxMUdGVzdC1zc2gtY29udmVyc3Rpb24wHhcN -MTkwNjE5MTIzNjAwWhcNMjAwNjE5MTIzNjAwWjA9MQswCQYDVQQGEwJHQjEPMA0G -A1UEChMGQ2hldmFoMR0wGwYDVQQDExR0ZXN0LXNzaC1jb252ZXJzdGlvbjCCAbcw -ggEsBgcqhkjOOAQBMIIBHwKBgQD/HJmstkyONrDh2iSafsRqxAzRG4dIUa70PdsE -gfMYBx95Nk1vhwGFyEQyCy305b2mgLG9+nkFkaLiD5UnoBbmO1NCggXlSNoe3ezq -akr80gV6dCwbM4T7B7lc3S0Eh5OJ2F5DKewzT65QyRrnkfECFlvjJqpeywhfucvg -nadoCwIVAIA92hGRUbX41P8zCqRBAMiEChlzAoGBALg27DhLThHhJHWdFX2gZYTm -NMjv/Z7mHCAda8/uqNXjAz97jI9w6KCSYIC7qiyl0lwGuW7kGqNCtnsZyxKWQzTy -HoONu9gfAmAxZbI3TuE49fYZJ/0m0mXyPpCg0VIeFJVcS6lA2W51UD1JrvCrUb1M -1SgNW+V/VHw6M54e+v1SA4GEAAKBgC/cCWpZpebhiEThZLd+eodR9vCntB8sIzrA -0JRCmi4t8vBOxLNAZQE7WdPWXZJA7d43+6B4//DZH+GOt6EoxLyPxcqM+GHqa99i -EwIuTKCIG6ucDtvzMSgwvYVFugfYaoJvu0Okc+6elNywpk9t3HLH5p2QbpPXPYgO -SH6qmzKdMAsGCWCGSAFlAwQDAgMvADAsAhR2vu0VK+loePjKDZcalym8vjgwkwIU -HNkVqo/9uKhSFkhbG6uKWUnOky0= ------END CERTIFICATE----- -""" - - sut = Key.fromString(data) - - self.assertTrue(sut.isPublic()) - self.assertEqual('DSA', sut.type()) - self.assertEqual(1024, sut.size()) - - components = sut.data() - y = long( - '33608096932577498834618892325416552088960771123656082234885710486' - '75507586904443594643612585160476637613084634099891307779753871384' - '19072984388914093315900417736990449366567905225558889080164633948' - '75642330307431599331123161679260711587324602448450132263105327567' - '324900691359269978674482129301723462636106625693' - ) - p = long( - '17914554197956231476032656039682646299975055883332311875135017227' - '52180243454588892360869849018970437236700881503241838175380166833' - '56570852141623851276212449051705325396966909384918507908491159872' - '81118556760058432354600693107636249903432532125207156471720334839' - '5401646777661899361981163845950810903143363602443' - ) - g = long( - '12935985053463672691492638315705405640647316377002915690069266627' - '73032720642846501430445126372712764104983906841935717997673558164' - '74657088881395785073303554687569602926262408886111665706815822813' - '14448994749901282518897434324098506093655990924057550618491224583' - '7106339202519842112263186663472095769544164572498' - ) - self.assertEqual(y, components['y']) - self.assertEqual(p, components['p']) - self.assertEqual(g, components['g']) - self.assertEqual( - 732130160578857514768194964362219084190055012723L, components['q']) - - def test_fromString_PCKS1_PUBLIC_DSA(self): - """ - It can extract RSA public key from an PKCS1 public DSA PEM file. - """ - # This is the same as the X509 DSA cert. - # $ openssl x509 -in bla.cert -pubkey -noout - data = """-----BEGIN PUBLIC KEY----- -MIIBtzCCASwGByqGSM44BAEwggEfAoGBAP8cmay2TI42sOHaJJp+xGrEDNEbh0hR -rvQ92wSB8xgHH3k2TW+HAYXIRDILLfTlvaaAsb36eQWRouIPlSegFuY7U0KCBeVI -2h7d7OpqSvzSBXp0LBszhPsHuVzdLQSHk4nYXkMp7DNPrlDJGueR8QIWW+Mmql7L -CF+5y+Cdp2gLAhUAgD3aEZFRtfjU/zMKpEEAyIQKGXMCgYEAuDbsOEtOEeEkdZ0V -faBlhOY0yO/9nuYcIB1rz+6o1eMDP3uMj3DooJJggLuqLKXSXAa5buQao0K2exnL -EpZDNPIeg4272B8CYDFlsjdO4Tj19hkn/SbSZfI+kKDRUh4UlVxLqUDZbnVQPUmu -8KtRvUzVKA1b5X9UfDoznh76/VIDgYQAAoGAL9wJalml5uGIROFkt356h1H28Ke0 -HywjOsDQlEKaLi3y8E7Es0BlATtZ09ZdkkDt3jf7oHj/8Nkf4Y63oSjEvI/Fyoz4 -Yepr32ITAi5MoIgbq5wO2/MxKDC9hUW6B9hqgm+7Q6Rz7p6U3LCmT23ccsfmnZBu -k9c9iA5IfqqbMp0= ------END PUBLIC KEY----- -""" - - sut = Key.fromString(data) - - self.assertTrue(sut.isPublic()) - self.assertEqual('DSA', sut.type()) - self.assertEqual(1024, sut.size()) - - components = sut.data() - y = long( - '33608096932577498834618892325416552088960771123656082234885710486' - '75507586904443594643612585160476637613084634099891307779753871384' - '19072984388914093315900417736990449366567905225558889080164633948' - '75642330307431599331123161679260711587324602448450132263105327567' - '324900691359269978674482129301723462636106625693' - ) - p = long( - '17914554197956231476032656039682646299975055883332311875135017227' - '52180243454588892360869849018970437236700881503241838175380166833' - '56570852141623851276212449051705325396966909384918507908491159872' - '81118556760058432354600693107636249903432532125207156471720334839' - '5401646777661899361981163845950810903143363602443' - ) - g = long( - '12935985053463672691492638315705405640647316377002915690069266627' - '73032720642846501430445126372712764104983906841935717997673558164' - '74657088881395785073303554687569602926262408886111665706815822813' - '14448994749901282518897434324098506093655990924057550618491224583' - '7106339202519842112263186663472095769544164572498' - ) - self.assertEqual(y, components['y']) - self.assertEqual(p, components['p']) - self.assertEqual(g, components['g']) - self.assertEqual( - 732130160578857514768194964362219084190055012723L, components['q']) - - def test_fromString_PRIVATE_PKCS8_invalid_format(self): - """ - It fails to load invalid formated PKCS8 PEM file. - """ - data = """-----BEGIN PRIVATE KEY----- -MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAM6hlRh3woxhut7n -r3fAiJ9U0aDLrcUh ------END PRIVATE KEY----- -""" - with self.assertRaises(BadKeyError) as context: - Key.fromString(data) - - self.assertStartsWith( - "Failed to load PKCS#8 PEM. [('asn1 encoding routines'", - context.exception.message, - ) - - def test_fromString_PRIVATE_PKCS8_RSA(self): - """ - It can extract RSA key from an PKCS8 private RSA PEM file, - without encryption. - """ - # openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in pkcs1.key - data = """-----BEGIN PRIVATE KEY----- -MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALh9Xq1JqQNIHpmi -/KAux/WIL0/e0kd49MoA+Q8BbOW7kFEdyYC7S5OOfsaGunFuONYzANU3Q7HPDu14 -jQ4QhWSmeVzIzovmYaT5fotzj6UB+nVjEJH2j34VcxZIk/faNHAj7guFZjGdhSV2 -8A7ksPP1B5HTIqKbByNFOgXr+OkvAgMBAAECgYAIHlxAO/GYF2BhWm7LjcN25ptO -ZHvUcVo0WX6cTm/AXFSpfSoU5CkbQTYK/nrN6w/NPUlYGKp99KKviJKMf+WeyRmc -z0nXF+megnkdPNwYdoFhUUQRdLp86zJZPXmhjspvqtEFOdZXQiez/TkeGnfyv9FH -87imgH7c3BAkOX/qAQJBAOgCdF0L7lLKOtnUlBRZSRqmJgciEWrqRa0abeRYmfQj -EIG0WEa+ohYnBkgCN/q1MoxSTpuMb2nsml61dSxOIMECQQDLkQNTYXjZebq29DMz -xsCCrt3b/HaIdG46QNRvVsrdjAHJOKGX0Euq91GFHGmXbURypakH9HMAVsZr7rQb -+JXvAkAFLPjXkoqQgj5p2ZosEgnVdFto0VO+JNfFEs/cxjU5Awc9PX6ypVIMWHaF -aLdC+oPUKYnjYnCh1ktjTXz9rgiBAkA6wKDIGPLLOcH0+egpQmzfit7HlkcTvR7v -OzTU6aTlano9fFXPPjQIpRbnJzsmlEfUGxH9FMV4TJM6JYvgItALAkB/gEX15UvF -FD0Dgyb0iS3iUXyKLqdrifF20TM/ynYs/20uodhShs1qfEH7syyLh/eBjK4p04ad -YeoPZTgdwt0x ------END PRIVATE KEY----- -""" - sut = Key.fromString(data) - - self.checkParsedRSAPrivate1024(sut) - - def test_fromString_PRIVATE_PKCS8_RSA_ENCRYPTED(self): - """ - It can extract RSA key from an PKCS8 private RSA PEM file, - with encryption. - """ - # openssl pkcs8 -topk8 -inform PEM -outform PEM -in pkcs1.key - data = """-----BEGIN ENCRYPTED PRIVATE KEY----- -MIIC3TBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQICxbcEPe+vjECAggA -MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAhBDpmQH4bpzIQSQqpw+GjBIIC -gMKX1CcvdGi6ZFxbhp9ycCnXU04bCsQrijAyYmndInf+EWSSTWpIzM86K6huOjdG -fKsTrmWb0bUM7LTu50GzNHwwGJgVMrUrL7rZQcTkht1D3mdLXWpanaCWyn2IYW8s -jXuzftEUn4AVHVzMeU95wlorgH33QlcAIDt/ZIDzeCfygsu3yJQW44kzWvp3/Eoy -tjBL+K6u7IRoHj67knh6YJ6cQxusK9cAFEpS8RfRLJpryAZyUfvwJteVK0LXQgcS -b8WsIwC+iv8E2QKExFmh4aoUsSsfOrdAb/H2iKTNU/qChCkeeYtzPFVLNmXYL1zG -9G80EGEKmaMgPTIt+oXx2cmY4W21jRGEQ/5KAUcLAWNR+3fEcDVdgfKxlCWQGSad -fQdemXnYhXW1emyb6RvWl0ml7f3ZzVFdeWgShLwx9ZVYdMT/ed4aCucK++XaXl55 -dK37TVTeVe6dzyhOADj8lNZ695Xt7+QO+O/hd+9K54xrjmt9TUKxFBbmS3Oqz9rI -T/0h4ym65OOio0CCePzj0vNrCvAD5rBo63B9Kjqxwnyzh2XmIBhUxcCzBEzm1pbS -FM6UHBQ3Jj595U0LGgParXRXxmt1A0i28Q9JhOQp5R1lxD+/q4q3eq/kV05bACyD -IdZR03u3euOWDtw0+Q6+DXvq53m1X1d9A4Dl14spNZoAdGnDLawrvdbWPvSeeXqR -5O9OYI0dake/SYROPlDvc2MgehllwSVU1IXdsrP3xChP2V4YupESRDcFcX+/zlph -HZ6BMxEKcYuIT9PKwhhp+FrwNo6J8mylpQLnCJ3hvXlhEPmyalg4rwVoeTHXRK6Y -TbW5RErmC8ifa/J4NdCv7MY= ------END ENCRYPTED PRIVATE KEY----- -""" - sut = Key.fromString(data, passphrase='password') - - self.checkParsedRSAPrivate1024(sut) - - def test_fromString_PRIVATE_PKCS8_ENCRYPTED_no_pass(self): - """ - It fails to extract RSA key from an PKCS8 private RSA PEM file, - if no password is provided and file is encrypted. - """ - # openssl pkcs8 -topk8 -inform PEM -outform PEM -in pkcs1.key - data = """-----BEGIN ENCRYPTED PRIVATE KEY----- -MIIC3TBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQICxbcEPe+vjECAggA -MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAhBDpmQH4bpzIQSQqpw+GjBIIC -gMKX1CcvdGi6ZFxbhp9ycCnXU04bCsQrijAyYmndInf+EWSSTWpIzM86K6huOjdG -fKsTrmWb0bUM7LTu50GzNHwwGJgVMrUrL7rZQcTkht1D3mdLXWpanaCWyn2IYW8s -jXuzftEUn4AVHVzMeU95wlorgH33QlcAIDt/ZIDzeCfygsu3yJQW44kzWvp3/Eoy -tjBL+K6u7IRoHj67knh6YJ6cQxusK9cAFEpS8RfRLJpryAZyUfvwJteVK0LXQgcS -b8WsIwC+iv8E2QKExFmh4aoUsSsfOrdAb/H2iKTNU/qChCkeeYtzPFVLNmXYL1zG -9G80EGEKmaMgPTIt+oXx2cmY4W21jRGEQ/5KAUcLAWNR+3fEcDVdgfKxlCWQGSad -fQdemXnYhXW1emyb6RvWl0ml7f3ZzVFdeWgShLwx9ZVYdMT/ed4aCucK++XaXl55 -dK37TVTeVe6dzyhOADj8lNZ695Xt7+QO+O/hd+9K54xrjmt9TUKxFBbmS3Oqz9rI -T/0h4ym65OOio0CCePzj0vNrCvAD5rBo63B9Kjqxwnyzh2XmIBhUxcCzBEzm1pbS -FM6UHBQ3Jj595U0LGgParXRXxmt1A0i28Q9JhOQp5R1lxD+/q4q3eq/kV05bACyD -IdZR03u3euOWDtw0+Q6+DXvq53m1X1d9A4Dl14spNZoAdGnDLawrvdbWPvSeeXqR -5O9OYI0dake/SYROPlDvc2MgehllwSVU1IXdsrP3xChP2V4YupESRDcFcX+/zlph -HZ6BMxEKcYuIT9PKwhhp+FrwNo6J8mylpQLnCJ3hvXlhEPmyalg4rwVoeTHXRK6Y -TbW5RErmC8ifa/J4NdCv7MY= ------END ENCRYPTED PRIVATE KEY----- -""" - with self.assertRaises(EncryptedKeyError) as context: - Key.fromString(data) - - self.assertEqual( - 'Passphrase must be provided for an encrypted key', - context.exception.message, - ) - - def test_fromString_PRIVATE_PKCS8_DSA(self): - """ - It can extract DSA key from an PKCS8 private RSA PEM file, - without encryption. - """ - # Obtain from a P12 - # openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in pkcs1.key - data = """-----BEGIN PRIVATE KEY----- -MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAM7CQoaeZVn1tGXtkKf/BIQtXSfk -QuypVkU60GkeV4Q6K2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU -7YwoFF323b1+vbpW8JlA7FHh/OQ+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8 -+jCSPt+bNEfnGANNAhUAhhv+WNJRyWjpOI3CiIX71vJp8UkCgYBcD5MAYKYXZl41 -k3TiaDF7JPfggDDfs0aYss/9vKLzp0px3PmG2o+I5Zw2YXOsDtrSj56sTcH6aLAS -baxXP55VZ804IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLmGdI2Sj9aynRWdbe -rk4r+0+zWlHL7epZTDCuDmLOFiQF/AQWAhROTtpG/rmhN51iwDKLzQvymFgE3g== ------END PRIVATE KEY----- -""" - sut = Key.fromString(data) - - self.checkParsedDSAPrivate1024(sut) - - def test_fromString_PRIVATE_PKCS8_EC(self): - """ - It fails to extract the EC key from an PKCS8 private EC PEM file, - """ - # openssl ecparam -name prime256v1 -genkey -noout -out private.ec.key - # openssl pkcs8 -topk8 -in private.ec.key -nocrypt - data = """-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrNfvVhrhJeyufkeZ -4oQ6i/kUFKudRU+xZ69FaAsw3MehRANCAASpL4fmdxdxbt317O8gV4Op5fVYwDnQ -7C/wsAsbx6monIz1qc1jje9RgggJL5pZ5HfbDInclQfV5T9rz6kWFEZS ------END PRIVATE KEY----- -""" - with self.assertRaises(BadKeyError) as context: - Key.fromString(data) - - self.assertEqual( - 'Unsupported key found in the PKCS#8 private PEM file.', - context.exception.message, - ) - - def test_toString_SSHCOM_RSA_private_without_encryption(self): - """ - Can export a private RSA SSH.com without without encryption. - """ - sut = Key.fromString(OPENSSH_RSA_PRIVATE) - - result = sut.toString(type='sshcom') - - # Check that it looks like SSH.com private key. - self.assertEqual(SSHCOM_RSA_PRIVATE_NO_PASSWORD, result) - # Load the serialized key and see that we get the same key. - reloaded = Key.fromString(result) - self.assertEqual(sut, reloaded) - - def test_toString_SSHCOM_RSA_private_encrypted(self): - """ - Can export an encrypted private RSA SSH.com. - """ - sut = Key.fromString(OPENSSH_RSA_PRIVATE) - - result = sut.toString(type='sshcom', extra='chevah') - - # Check that it looks like SSH.com private key. - self.assertEqual(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, result) - # Load the serialized key and see that we get the same key. - reloaded = Key.fromString(result, passphrase='chevah') - self.assertEqual(sut, reloaded) - - def test_toString_SSHCOM_DSA_private(self): - """ - Can export a private DSA SSH.com key. - """ - sut = Key.fromString(OPENSSH_DSA_PRIVATE) - - result = sut.toString(type='sshcom') - - self.assertEqual(SSHCOM_DSA_PRIVATE_NO_PASSWORD, result) - # Load the serialized key and see that we get the same key. - reloaded = Key.fromString(result) - self.assertEqual(sut, reloaded) - - def test_fromString_PRIVATE_PUTTY_RSA_no_password(self): - """ - It can read private RSA keys in Putty format which are not - encrypted. - """ - sut = Key.fromString(PUTTY_RSA_PRIVATE_NO_PASSWORD) - - self.checkParsedRSAPrivate1024(sut) - - def test_fromString_PRIVATE_PUTTY_not_encrypted_with_passphrase(self): - """ - When loading a unencrypted PuTTY private key with passhphrase - will raise BadKeyError. - """ - with self.assertRaises(BadKeyError) as context: - Key.fromString(PUTTY_RSA_PRIVATE_NO_PASSWORD, passphrase='pass') - - self.assertEqual( - 'PuTTY key not encrypted', - context.exception.message) - - def test_fromString_PRIVATE_PUTTY_RSA_with_password(self): - """ - It can read private RSA keys in Putty format which are encrypted. - """ - sut = Key.fromString( - PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase='chevah') - - self.checkParsedRSAPrivate1024(sut) - - def test_fromString_PRIVATE_PUTTY_short(self): - """ - An exception is raised when key is too short. - """ - content = 'PuTTY-User-Key-File-2: ssh-rsa' - - self.assertKeyIsTooShort(content) - - content = ( - 'PuTTY-User-Key-File-2: ssh-rsa\n' - 'Encryption: aes256-cbc\n' - ) - - self.assertKeyIsTooShort(content) - - content = ( - 'PuTTY-User-Key-File-2: ssh-rsa\n' - 'Encryption: aes256-cbc\n' - 'Comment: bla\n' - ) - - self.assertKeyIsTooShort(content) - - def test_fromString_PRIVATE_PUTTY_RSA_bad_password(self): - """ - An exception is raised when password is not valid. - """ - with self.assertRaises(EncryptedKeyError) as context: - Key.fromString( - PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase='bad-pass') - - self.assertEqual( - 'Bad password or HMAC mismatch.', context.exception.message) - - def test_fromString_PRIVATE_PUTTY_RSA_missing_password(self): - """ - An exception is raised when key is encrypted but no password was - provided. - """ - with self.assertRaises(EncryptedKeyError) as context: - Key.fromString(PUTTY_RSA_PRIVATE_WITH_PASSWORD) - - self.assertEqual( - 'Passphrase must be provided for an encrypted key.', - context.exception.message) - - def test_fromString_PRIVATE_PUTTY_unsupported_type(self): - """ - An exception is raised when key contain a type which is not supported. - """ - content = """PuTTY-User-Key-File-2: ssh-bad -IGNORED -""" - self.assertBadKey( - content, 'Unsupported key type: ssh-bad') - - def test_fromString_PRIVATE_PUTTY_unsupported_encryption(self): - """ - An exception is raised when key contain an encryption method - which is not supported. - """ - content = """PuTTY-User-Key-File-2: ssh-dss -Encryption: aes126-cbc -IGNORED -""" - self.assertBadKey( - content, 'Unsupported encryption type: aes126-cbc') - - def test_fromString_PRIVATE_PUTTY_type_mismatch(self): - """ - An exception is raised when key header advertise one key type while - the public key another. - """ - content = """PuTTY-User-Key-File-2: ssh-rsa -Encryption: aes256-cbc -Comment: imported-openssh-key -Public-Lines: 4 -AAAAB3NzaC1kc3MAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK -APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk -+X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj -RToF6/jpLw== -IGNORED -""" - self.assertBadKey( - content, - ( - 'Mismatch key type. Header has "ssh-rsa",' - ' public has "ssh-dss"'), - ) - - def test_fromString_PRIVATE_PUTTY_hmac_mismatch(self): - """ - An exception is raised when key HMAC differs from the one - advertise by the key file. - """ - content = PUTTY_RSA_PRIVATE_NO_PASSWORD[:-1] - content += 'a' - - self.assertBadKey( - content, - 'HMAC mismatch: file declare ' - '"7630b86be300c6302ce1390fb264487bb61e67ca", actual is ' - '"7630b86be300c6302ce1390fb264487bb61e67ce"', - ) - - def test_fromString_PRIVATE_OpenSSH_DSA_no_password(self): - """ - It can read private DSA keys in OpenSSH format. - """ - sut = Key.fromString(OPENSSH_DSA_PRIVATE) - - self.checkParsedDSAPrivate1024(sut) - - def test_fromString_PRIVATE_PUTTY_DSA_no_password(self): - """ - It can read private DSA keys in Putty format which are not - encrypted. - """ - sut = Key.fromString(PUTTY_DSA_PRIVATE_NO_PASSWORD) - - self.checkParsedDSAPrivate1024(sut) - - def test_toString_PUTTY_RSA_plain(self): - """ - Can export to private RSA Putty without encryption. - """ - sut = Key.fromString(OPENSSH_RSA_PRIVATE) - - result = sut.toString(type='putty') - - # We can not check the exact text as comment is hardcoded in - # Twisted. - # Load the serialized key and see that we get the same key. - reloaded = Key.fromString(result) - self.assertEqual(sut, reloaded) - - def test_toString_PUTTY_RSA_encrypted(self): - """ - Can export to encrypted private RSA Putty key. - """ - sut = Key.fromString(OPENSSH_RSA_PRIVATE) - - result = sut.toString(type='putty', extra='write-pass') - - # We can not check the exact text as comment is hardcoded in - # Twisted. - # Load the serialized key and see that we get the same key. - reloaded = Key.fromString(result, passphrase='write-pass') - self.assertEqual(sut, reloaded) - - def test_toString_PUTTY_DSA_plain(self): - """ - Can export to private DSA Putty key without encryption. - """ - sut = Key.fromString(OPENSSH_DSA_PRIVATE) - - result = sut.toString(type='putty') - - # We can not check the exact text as comment is hardcoded in - # Twisted. - # Load the serialized key and see that we get the same key. - reloaded = Key.fromString(result) - self.assertEqual(sut, reloaded) - - def test_toString_PUTTY_public(self): - """ - Can export to public RSA Putty. - """ - sut = Key.fromString(OPENSSH_RSA_PRIVATE).public() - - result = sut.toString(type='putty') - - reloaded = Key.fromString(result) - self.assertEqual(sut, reloaded) - - def test_fromString_LSH(self): - """ - Test that keys are correctly generated from LSH strings. - """ - self._testPublicPrivateFromString( - keydata.publicRSA_lsh, - keydata.privateRSA_lsh, 'RSA', keydata.RSAData) - self._testPublicPrivateFromString( - keydata.publicDSA_lsh, - keydata.privateDSA_lsh, 'DSA', keydata.DSAData) - - sexp = sexpy.pack([['public-key', ['bad-key', ['p', '2']]]]) - self.assertRaises( - keys.BadKeyError, - keys.Key.fromString, - data='{' + base64.encodestring(sexp) + '}') - - sexp = sexpy.pack([['private-key', ['bad-key', ['p', '2']]]]) - self.assertRaises( - keys.BadKeyError, keys.Key.fromString, sexp) - - def test_toString_LSH(self): - """ - Test that the Key object generates LSH keys correctly. - """ - key = keys.Key.fromString(keydata.privateRSA_openssh) - self.assertEqual(key.toString('lsh'), keydata.privateRSA_lsh) - self.assertEqual( - key.public().toString('lsh'), keydata.publicRSA_lsh) - key = keys.Key.fromString(keydata.privateDSA_openssh) - self.assertEqual(key.toString('lsh'), keydata.privateDSA_lsh) - self.assertEqual( - key.public().toString('lsh'), keydata.publicDSA_lsh) - - def test_toString_AGENTV3(self): - """ - Test that the Key object generates Agent v3 keys correctly. - """ - key = keys.Key.fromString(keydata.privateRSA_openssh) - self.assertEqual(key.toString('agentv3'), keydata.privateRSA_agentv3) - key = keys.Key.fromString(keydata.privateDSA_openssh) - self.assertEqual(key.toString('agentv3'), keydata.privateDSA_agentv3) - - def test_fromString_AGENTV3(self): - """ - Test that keys are correctly generated from Agent v3 strings. - """ - self._testPrivateFromString( - keydata.privateRSA_agentv3, 'RSA', keydata.RSAData) - self._testPrivateFromString( - keydata.privateDSA_agentv3, 'DSA', keydata.DSAData) - self.assertRaises( - keys.BadKeyError, - keys.Key.fromString, - '\x00\x00\x00\x07ssh-foo' + '\x00\x00\x00\x01\x01' * 5) - - def test_fingerprint(self): - """ - Will return the md5 fingerprint with colons separator. - """ - key = keys.Key.fromString(keydata.privateRSA_openssh) - - result = key.fingerprint() - self.assertEqual(keydata.privateRSA_fingerprint_md5, result) - - def test_fingerprintdefault(self): - """ - Test that the fingerprint method returns fingerprint in - L{FingerprintFormats.MD5-HEX} format by default. - """ - rsaObj, dsaObj = self._getKeysForFingerprintTest() - - self.assertEqual( - keys.Key(rsaObj).fingerprint(), - '85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da') - self.assertEqual( - keys.Key(dsaObj).fingerprint(), - '63:15:b3:0e:e6:4f:50:de:91:48:3d:01:6b:b3:13:c1') - - def test_fingerprint_md5_hex(self): - """ - fingerprint method generates key fingerprint in - L{FingerprintFormats.MD5-HEX} format if explicitly specified. - """ - rsaObj, dsaObj = self._getKeysForFingerprintTest() - - self.assertEqual( - keys.Key(rsaObj).fingerprint( - keys.FingerprintFormats.MD5_HEX), - '85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da') - self.assertEqual( - keys.Key(dsaObj).fingerprint( - keys.FingerprintFormats.MD5_HEX), - '63:15:b3:0e:e6:4f:50:de:91:48:3d:01:6b:b3:13:c1') - - def test_fingerprintsha256(self): - """ - fingerprint method generates key fingerprint in - L{FingerprintFormats.SHA256-BASE64} format if explicitly specified. - """ - rsaObj, dsaObj = self._getKeysForFingerprintTest() - - self.assertEqual( - keys.Key(rsaObj).fingerprint( - keys.FingerprintFormats.SHA256_BASE64), - 'FBTCOoknq0mHy+kpfnY9tDdcAJuWtCpuQMaV3EsvbUI=') - self.assertEqual( - keys.Key(dsaObj).fingerprint( - keys.FingerprintFormats.SHA256_BASE64), - 'Wz5o2YbKyxOEcJn1au/UaALSVruUzfz0vaLI1xiIGyY=') - - def test_fingerprintsha1(self): - """ - fingerprint method generates key fingerprint in - L{FingerprintFormats.SHA1-BASE64} format if explicitly specified. - """ - rsaObj, dsaObj = self._getKeysForFingerprintTest() - - self.assertEqual( - keys.Key(rsaObj).fingerprint( - keys.FingerprintFormats.SHA1_BASE64), - 'tuUFlgv3kknie9WYExgS7OQj54k=') - self.assertEqual( - keys.Key(dsaObj).fingerprint( - keys.FingerprintFormats.SHA1_BASE64), - '9CCuTybG5aORtuW4jrFcp0PbK4U=') - - def test_fingerprintBadFormat(self): - """ - A C{BadFingerPrintFormat} error is raised when unsupported - formats are requested. - """ - rsaObj = self._getKeysForFingerprintTest()[0] - - with self.assertRaises(keys.BadFingerPrintFormat) as em: - keys.Key(rsaObj).fingerprint('sha256-base') - self.assertEqual( - 'Unsupported fingerprint format: sha256-base', - em.exception.args[0]) - - def test_sign(self): - """ - Test that the Key object generates correct signatures. - """ - key = keys.Key.fromString(keydata.privateRSA_openssh) - self.assertEqual(key.sign(''), self.rsaSignature) - key = keys.Key.fromString(keydata.privateDSA_openssh) - self.assertEqual(key.sign(''), self.dsaSignature) - - def test_verify(self): - """ - Test that the Key object correctly verifies signatures. - """ - key = keys.Key.fromString(keydata.publicRSA_openssh) - self.assertTrue(key.verify(self.rsaSignature, b'')) - self.assertFalse(key.verify(self.rsaSignature, b'a')) - self.assertFalse(key.verify(self.dsaSignature, b'')) - key = keys.Key.fromString(keydata.publicDSA_openssh) - self.assertTrue(key.verify(self.dsaSignature, b'')) - self.assertFalse(key.verify(self.dsaSignature, b'a')) - self.assertFalse(key.verify(self.rsaSignature, b'')) - - def test_verifyDSANoPrefix(self): - """ - Some commercial SSH servers send DSA keys as 2 20-byte numbers; - they are still verified as valid keys. - """ - key = keys.Key.fromString(keydata.publicDSA_openssh) - self.assertTrue(key.verify(self.dsaSignature[-40:], b'')) - - def test_repr(self): - """ - Test the pretty representation of Key. - """ - self.assertEqual( - repr(keys.Key(self.rsaObj)), - b"""\ -""") - - -class Test_generate_ssh_key_parser(ChevahTestCase, CommandLineMixin): - """ - Unit tests for generate_ssh_key_parser. - """ - - def setUp(self): - super(Test_generate_ssh_key_parser, self).setUp() - self.parser = ArgumentParser(prog='test-command') - self.subparser = self.parser.add_subparsers( - help='Available sub-commands', dest='sub_command') - - def test_default(self): - """ - It only need a subparser and sub-command name. - """ - generate_ssh_key_parser(self.subparser, 'key-gen') - - options = self.parseArguments(['key-gen']) - - self.assertNamespaceEqual({ - 'sub_command': 'key-gen', - 'key_comment': None, - 'key_file': None, - 'key_size': 2048, - 'key_type': 'rsa', - 'key_format': 'openssh_v1', - 'key_skip': False, - }, options) - - def test_value(self): - """ - Options are parsed from the command line. - """ - generate_ssh_key_parser(self.subparser, 'key-gen') - - options = self.parseArguments([ - 'key-gen', - '--key-comment', 'some comment', - '--key-file=id_dsa', - '--key-size', '1024', - '--key-type', 'dsa', - '--key-skip', - ]) - - self.assertNamespaceEqual({ - 'sub_command': 'key-gen', - 'key_comment': 'some comment', - 'key_file': 'id_dsa', - 'key_size': 1024, - 'key_type': 'dsa', - 'key_format': 'openssh_v1', - 'key_skip': True, - }, options) - - def test_default_overwrite(self): - """ - You can change default values. - """ - generate_ssh_key_parser( - self.subparser, 'key-gen', - default_key_size=1024, - default_key_type='dsa', - ) - - options = self.parseArguments(['key-gen']) - - self.assertNamespaceEqual({ - 'sub_command': 'key-gen', - 'key_comment': None, - 'key_file': None, - 'key_size': 1024, - 'key_type': 'dsa', - 'key_format': 'openssh_v1', - 'key_skip': False, - }, options) - - -class Testgenerate_ssh_key(ChevahTestCase, CommandLineMixin): - """ - Tests for generate_ssh_key. - """ - - def setUp(self): - super(Testgenerate_ssh_key, self).setUp() - self.parser = ArgumentParser(prog='test-command') - self.sub_command_name = 'gen-ssh-key' - subparser = self.parser.add_subparsers( - help='Available sub-commands', dest='sub_command') - generate_ssh_key_parser(subparser, self.sub_command_name) - - def assertPathEqual(self, expected, actual): - """ - Check that pats are equal. - """ - if self.os_family == 'posix': - expected = expected.encode('utf-8') - self.assertEqual(expected, actual) - - def test_generate_ssh_key_custom_values(self): - """ - When custom values are provided, the key is generated using those - values. - """ - file_name = mk.ascii().decode('ascii') - file_name_pub = file_name + '.pub' - options = self.parseArguments([ - self.sub_command_name, - u'--key-size=512', - u'--key-type=DSA', - u'--key-file=' + file_name, - u'--key-comment=this is a comment', - ]) - open_method = DummyOpenContext() - - exit_code, message, key = generate_ssh_key( - options, open_method=open_method) - - self.assertEqual('DSA', key.type()) - self.assertEqual(512, key.size()) - - # First it writes the private key. - first_file = open_method.calls.pop(0) - - self.assertPathEqual( - _path(file_name), first_file['path']) - self.assertEqual('wb', first_file['mode']) - self.assertEqual( - key.toString('openssh'), first_file['stream'].getvalue()) - - # Second it writes the public key. - second_file = open_method.calls.pop(0) - self.assertPathEqual( - _path(file_name_pub.decode('ascii')), second_file['path']) - self.assertEqual('wb', second_file['mode']) - self.assertEqual( - key.public().toString('openssh', 'this is a comment'), - second_file['stream'].getvalue()) - - self.assertEqual( - u'SSH key of type "dsa" and length "512" generated as public ' - u'key file "%s" and private key file "%s" ' - u'having comment "this is a comment".' % ( - file_name_pub, file_name), - message, - ) - self.assertEqual(0, exit_code) - - def test_generate_ssh_key_default_values(self): - """ - When no path and no comment are provided, it will use default - values. - """ - options = self.parseArguments([ - self.sub_command_name, - '--key-size=1024', - '--key-type=RSA', - ]) - open_method = DummyOpenContext() - - exit_code, message, key = generate_ssh_key( - options, open_method=open_method) - - self.assertEqual('RSA', key.type()) - self.assertEqual(1024, key.size()) - - # First it writes the private key. - first_file = open_method.calls.pop(0) - self.assertPathEqual(_path(u'id_rsa'), first_file['path']) - self.assertEqual('wb', first_file['mode']) - self.assertEqual( - key.toString('openssh'), first_file['stream'].getvalue()) - - # Second it writes the public key. - second_file = open_method.calls.pop(0) - self.assertPathEqual(u'id_rsa.pub', second_file['path']) - self.assertEqual('wb', second_file['mode']) - self.assertEqual( - key.public().toString('openssh'), second_file['stream'].getvalue()) - - # Message informs what default values were used. - self.assertEqual( - u'SSH key of type "rsa" and length "1024" generated as public ' - u'key file "id_rsa.pub" and private key file "id_rsa" without ' - u'a comment.', - message, - ) - - def test_generate_ssh_key_private_exist_no_migration(self): - """ - When no migration is done it will not generate the key, - if private file already exists and exit with error. - """ - self.test_segments = mk.fs.createFileInTemp() - path = mk.fs.getRealPathFromSegments(self.test_segments) - options = self.parseArguments([ - self.sub_command_name, - '--key-type=RSA', - '--key-size=2048', - '--key-file', path, - ]) - open_method = DummyOpenContext() - - exit_code, message, key = generate_ssh_key( - options, open_method=open_method) - - self.assertEqual(1, exit_code) - self.assertEqual(u'Private key already exists. %s' % path, message) - # Open is not called. - self.assertIsEmpty(open_method.calls) - - def test_generate_ssh_key_private_exist_skip(self): - """ - On skip, will not generate the key if private file already - exists and exit without error. - """ - self.test_segments = mk.fs.createFileInTemp() - path = mk.fs.getRealPathFromSegments(self.test_segments) - options = self.parseArguments([ - self.sub_command_name, - '--key-skip', - '--key-type=RSA', - '--key-size=2048', - '--key-file', path, - ]) - open_method = DummyOpenContext() - - exit_code, message, key = generate_ssh_key( - options, open_method=open_method) - - self.assertEqual(0, exit_code) - self.assertEqual(u'Key already exists.', message) - # Open is not called. - self.assertIsEmpty(open_method.calls) - - def test_generate_ssh_key_public_exist(self): - """ - Will not generate the key, if public file already exists. - """ - self.test_segments = mk.fs.createFileInTemp(suffix='.pub') - path = mk.fs.getRealPathFromSegments(self.test_segments) - options = self.parseArguments([ - self.sub_command_name, - '--key-type=RSA', - '--key-size=2048', - # path is for public key, but we pass the private path. - '--key-file', path[:-4], - ]) - open_method = DummyOpenContext() - - exit_code, message, key = generate_ssh_key( - options, open_method=open_method) - - self.assertEqual(1, exit_code) - self.assertEqual(u'Public key already exists. %s' % path, message) - # Open is not called. - self.assertIsEmpty(open_method.calls) - - def test_generate_ssh_key_fail_to_write(self): - """ - Will return an error when failing to write the key. - """ - options = self.parseArguments([ - self.sub_command_name, - '--key-type=RSA', - '--key-size=1024', - '--key-file', 'no-such-parent/ssh.key', - ]) - - exit_code, message, key = generate_ssh_key(options) - - self.assertEqual(1, exit_code) - self.assertEqual( - "[Errno 2] No such file or directory: 'no-such-parent/ssh.key'", - message) diff --git a/chevah/keycert/tests/test_ssl.py b/chevah/keycert/tests/test_ssl.py deleted file mode 100644 index ea3e416..0000000 --- a/chevah/keycert/tests/test_ssl.py +++ /dev/null @@ -1,567 +0,0 @@ -# Copyright (c) 2015 Adi Roiban. -# See LICENSE for details. -""" -Test for SSL keys/cert management. -""" -from __future__ import unicode_literals -from argparse import ArgumentParser - -from bunch import Bunch -from chevah.compat.testing import mk, ChevahTestCase -from OpenSSL import crypto - -from chevah.keycert.exceptions import KeyCertException -from chevah.keycert.ssl import ( - generate_and_store_csr, - generate_csr, - generate_csr_parser, - generate_self_signed_parser, - generate_ssl_self_signed_certificate, - ) -from chevah.keycert.tests.helpers import CommandLineMixin - -RSA_PRIVATE = b"""-----BEGIN RSA PRIVATE KEY----- -MIICWwIBAAKBgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKAPkPAWzlu5BRHcmA -u0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAfp1YxCR -9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLwIDAQAB -AoGACB5cQDvxmBdgYVpuy43DduabTmR71HFaNFl+nE5vwFxUqX0qFOQpG0E2Cv56 -zesPzT1JWBiqffSir4iSjH/lnskZnM9J1xfpnoJ5HTzcGHaBYVFEEXS6fOsyWT15 -oY7Kb6rRBTnWV0Ins/05Hhp38r/RR/O4poB+3NwQJDl/6gECQQDoAnRdC+5SyjrZ -1JQUWUkapiYHIhFq6kWtGm3kWJn0IxCBtFhGvqIWJwZIAjf6tTKMUk6bjG9p7Jpe -tXUsTiDBAkEAy5EDU2F42Xm6tvQzM8bAgq7d2/x2iHRuOkDUb1bK3YwByTihl9BL -qvdRhRxpl21EcqWpB/RzAFbGa+60G/iV7wJABSz415KKkII+admaLBIJ1XRbaNFT -viTXxRLP3MY1OQMHPT1+sqVSDFh2hWi3QvqD1CmJ42JwodZLY018/a4IgQJAOsCg -yBjyyznB9PnoKUJs34rex5ZHE70e7zs01Omk5Wp6PXxVzz40CKUW5yc7JpRH1BsR -/RTFeEyTOiWL4CLQCwJAf4BF9eVLxRQ9A4Mm9Ikt4lF8ii6na4nxdtEzP8p2LP9t -LqHYUobNanxB+7Msi4f3gYyuKdOGnWHqD2U4HcLdMQ== ------END RSA PRIVATE KEY----- -""" - - -class CommandLineTestBase(ChevahTestCase, CommandLineMixin): - """ - Share code for testing methods which read SSL command line input. - """ - - def setUp(self): - super(CommandLineTestBase, self).setUp() - self.parser = ArgumentParser(prog='test-command') - subparser = self.parser.add_subparsers( - help='Available sub-commands', dest='sub_command') - self.command_name = 'gen-csr' - generate_csr_parser(subparser, self.command_name) - generate_self_signed_parser(subparser, 'self-gen') - - -class Test_generate_ssl_self_signed_certificate(CommandLineTestBase): - """ - Unit tests for generate_ssl_self_signed_certificate. - """ - - def test_generate(self): - """ - Will generate the key and self signed certificate for current - hostname. - """ - options = self.parseArguments([ - 'self-gen', - '--common-name', 'domain.com', - '--key-size=1024', - '--alternative-name=DNS:ex.com,IP:1.2.3.4', - '--constraints=critical,CA:TRUE', - '--key-usage=server-authentication,crl-sign', - '--sign-algorithm=sha512', - '--email=dev@chevah.com', - '--state=MS', - '--locality=Cluj', - '--organization=Chevah Team', - '--organization-unit=DevTeam', - '--country=UN', - ]) - - cert_pem, key_pem = generate_ssl_self_signed_certificate(options) - - key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem) - cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem) - self.assertEqual(1024, key.bits()) - self.assertEqual(crypto.TYPE_RSA, key.type()) - self.assertEqual(u'domain.com', cert.get_subject().CN) - - self.assertEqual(u'dev@chevah.com', cert.get_subject().emailAddress) - - self.assertEqual(u'MS', cert.get_subject().ST) - self.assertEqual(u'Cluj', cert.get_subject().L) - self.assertEqual(u'Chevah Team', cert.get_subject().O) - self.assertEqual(u'DevTeam', cert.get_subject().OU) - self.assertEqual(u'UN', cert.get_subject().C) - - self.assertNotEqual(0, cert.get_serial_number()) - issuer = cert.get_issuer() - self.assertEqual(cert.subject_name_hash(), issuer.hash()) - - constraints = cert.get_extension(0) - self.assertEqual(b'basicConstraints', constraints.get_short_name()) - self.assertTrue(constraints.get_critical()) - self.assertEqual(b'0\x03\x01\x01\xff', constraints.get_data()) - - key_usage = cert.get_extension(1) - self.assertEqual(b'keyUsage', key_usage.get_short_name()) - self.assertFalse(key_usage.get_critical()) - - extended_usage = cert.get_extension(2) - self.assertEqual(b'extendedKeyUsage', extended_usage.get_short_name()) - self.assertFalse(extended_usage.get_critical()) - - alt_name = cert.get_extension(3) - self.assertEqual(b'subjectAltName', alt_name.get_short_name()) - self.assertFalse(alt_name.get_critical()) - self.assertEqual( - b'0\x0e\x82\x06ex.com\x87\x04\x01\x02\x03\x04', - alt_name.get_data()) - - def test_generate_basic_options(self): - """ - Can generate using just common name as the options. - """ - options = Bunch(common_name='test') - - cert_pem, key_pem = generate_ssl_self_signed_certificate(options) - - key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem) - cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem) - self.assertEqual(2048, key.bits()) - self.assertEqual(crypto.TYPE_RSA, key.type()) - self.assertEqual(u'test', cert.get_subject().CN) - self.assertIsNone(cert.get_subject().C) - self.assertNotEqual(0, cert.get_serial_number()) - issuer = cert.get_issuer() - self.assertEqual(cert.subject_name_hash(), issuer.hash()) - # No extensions are set. - self.assertEqual(0, cert.get_extension_count()) - - -class Test_generate_csr_parser( - ChevahTestCase, CommandLineMixin): - """ - Unit tests for generate_csr_parser. - """ - - def setUp(self): - super(Test_generate_csr_parser, self).setUp() - self.parser = ArgumentParser(prog='test-command') - self.subparser = self.parser.add_subparsers( - help='Available sub-commands', dest='sub_command') - - def test_common_name_required(self): - """ - It can not be called without at least the common-name argument - """ - generate_csr_parser(self.subparser, 'key-gen') - - code, error = self.parseArgumentsFailure(['key-gen']) - - self.assertStartsWith('usage: test-command key-gen [-h]', error) - self.assertEndsWith( - '\ntest-command key-gen: ' - 'error: argument --common-name is required\n', - error) - - def test_default(self): - """ - It can be initialized with only a subparser and sub-command name. - """ - generate_csr_parser(self.subparser, 'key-gen') - - options = self.parseArguments([ - 'key-gen', - '--common-name', 'domain.com', - ]) - - self.assertNamespaceEqual({ - 'sub_command': 'key-gen', - 'key': None, - 'key_file': 'server.key', - 'key_size': 2048, - 'key_password': None, - 'common_name': 'domain.com', - 'alternative_name': None, - 'email': None, - 'organization': None, - 'organization_unit': None, - 'locality': None, - 'state': None, - 'country': None, - 'constraints': '', - 'key_usage': '', - 'sign_algorithm': 'sha256', - }, options) - - def test_value(self): - """ - Options are parsed form command line. - """ - generate_csr_parser(self.subparser, 'key-gen') - - options = self.parseArguments([ - 'key-gen', - '--common-name', 'sub.domain.com', - '--key-file=my_server.pem', - '--key-size', '1024', - '--key-password', u'valu\u20ac', - '--alternative-name', 'DNS:www.domain.com,IP:127.0.0.1', - '--email', 'admin@domain.com', - '--organization', 'OU Name', - '--organization-unit=OU Unit', - '--locality=somewhere', - '--state=without', - '--country=GB', - '--constraints=critical,CA:FALSE', - '--key-usage=crl-sign', - '--sign-algorithm=sha1', - ]) - - self.assertNamespaceEqual({ - 'sub_command': 'key-gen', - 'key': None, - 'key_file': 'my_server.pem', - 'key_size': 1024, - 'key_password': u'valu\u20ac', - 'common_name': 'sub.domain.com', - 'alternative_name': 'DNS:www.domain.com,IP:127.0.0.1', - 'email': 'admin@domain.com', - 'organization': 'OU Name', - 'organization_unit': 'OU Unit', - 'locality': 'somewhere', - 'state': 'without', - 'country': 'GB', - 'constraints': 'critical,CA:FALSE', - 'key_usage': 'crl-sign', - 'sign_algorithm': 'sha1', - - }, options) - - def test_default_overwrite(self): - """ - You can change default values. - """ - generate_csr_parser( - self.subparser, 'key-gen', - default_key_size=1024, - ) - - options = self.parseArguments([ - 'key-gen', - '--common-name', 'domain.com', - ]) - - self.assertNamespaceEqual({ - 'sub_command': 'key-gen', - 'key': None, - 'key_file': 'server.key', - 'key_size': 1024, - 'key_password': None, - 'common_name': 'domain.com', - 'alternative_name': None, - 'email': None, - 'organization': None, - 'organization_unit': None, - 'locality': None, - 'state': None, - 'country': None, - 'constraints': '', - 'key_usage': '', - 'sign_algorithm': 'sha256', - }, options) - - -class Test_generate_csr(CommandLineTestBase): - """ - Unit tests for generate_csr. - """ - - def test_bad_size(self): - """ - Raise an exception when failing to generate the key. - """ - options = self.parseArguments([ - self.command_name, - '--common-name=domain.com', - '--key-size=12', - ]) - - with self.assertRaises(KeyCertException) as context: - generate_csr(options) - - self.assertEqual( - 'Key size must be greater or equal to 512.', - context.exception.message) - - def test_bad_country_long(self): - """ - Raise an exception when country code is not correct. - """ - options = self.parseArguments([ - self.command_name, - '--common-name=domain.com', - '--country=USA', - ]) - - with self.assertRaises(KeyCertException) as context: - generate_csr(options) - - self.assertEqual('Invalid country code.', context.exception.message) - - def test_bad_country_short(self): - """ - Raise an exception when country code is not correct. - """ - options = self.parseArguments([ - self.command_name, - '--common-name=domain.com', - '--country=A', - ]) - - with self.assertRaises(KeyCertException) as context: - generate_csr(options) - - self.assertEqual('Invalid country code.', context.exception.message) - - def test_sign_algorithm_invalid(self): - """ - Raise an exception when the signing algorithm is not correct. - """ - options = self.parseArguments([ - self.command_name, - '--common-name=domain.com', - '--sign-algorithm=unknown', - ]) - - with self.assertRaises(KeyCertException) as context: - generate_csr(options) - - self.assertEqual( - 'Invalid signing algorithm. ' - 'Supported values: md5, sha1, sha256, sha512.', - context.exception.message) - - def test_email_invalid(self): - """ - Raise an exception when the email adress is not correct. - """ - options = self.parseArguments([ - self.command_name, - '--common-name=domain.com', - '--email=invalid', - ]) - - with self.assertRaises(KeyCertException) as context: - generate_csr(options) - - self.assertEqual('Invalid email address.', context.exception.message) - - def test_default_gen(self): - """ - By default it will serialized the key without password and generate - the csr without alternative name and just the common name. - """ - options = self.parseArguments([ - self.command_name, - '--common-name=domain.com', - ]) - - result = generate_csr(options) - - # OpenSSL.crypto.PKey has no equality so we need to compare the - # serialization. - self.assertEqual(2048L, result['key'].bits()) - self.assertEqual(crypto.TYPE_RSA, result['key'].type()) - key = crypto.dump_privatekey(crypto.FILETYPE_PEM, result['key']) - self.assertEqual(key, result['key_pem']) - # For CSR we can not get extensions so we only check the subject. - csr = crypto.dump_certificate_request( - crypto.FILETYPE_PEM, result['csr']) - self.assertEqual(csr, result['csr_pem']) - subject = result['csr'].get_subject() - self.assertEqual(u'domain.com', subject.commonName) - self.assertIsNone(subject.emailAddress) - self.assertIsNone(subject.organizationName) - self.assertIsNone(subject.organizationalUnitName) - self.assertIsNone(subject.localityName) - self.assertIsNone(subject.stateOrProvinceName) - self.assertIsNone(subject.countryName) - self.assertEqual(2, result['csr'].get_version()) - - def test_gen_unicode(self): - """ - Domains are encoded using IDNA and names using Unicode. - """ - options = self.parseArguments([ - self.command_name, - u'--common-name=domain-\u20acuro.com', - u'--key-size=512', - u'--alternative-name=DNS:www.domain-\u20acuro.com,IP:127.0.0.1', - u'--email=name@domain-\u20acuro.com', - u'--organization=OU Nam\u20acuro', - u'--organization-unit=OU Unit\u20acuro', - u'--locality=Som\u20acwhere', - u'--state=Stat\u20ac', - u'--country=GB', - ]) - - result = generate_csr(options) - - csr = crypto.dump_certificate_request( - crypto.FILETYPE_PEM, result['csr']) - self.assertEqual(csr, result['csr_pem']) - subject = result['csr'].get_subject() - self.assertEqual(u'xn--domain-uro-x77e.com', subject.commonName) - self.assertEqual( - u'name@xn--domain-uro-x77e.com', subject.emailAddress) - self.assertEqual(u'OU Nam\u20acuro', subject.organizationName) - self.assertEqual(u'OU Unit\u20acuro', subject.organizationalUnitName) - self.assertEqual(u'Som\u20acwhere', subject.localityName) - self.assertEqual(u'Stat\u20ac', subject.stateOrProvinceName) - self.assertEqual(u'GB', subject.countryName) - - def test_encrypted_key(self): - """ - When asked it will serialize the key with a password. - """ - options = self.parseArguments([ - self.command_name, - u'--common-name=domain.com', - u'--key-size=512', - u'--key-password=\u20acuro', - ]) - - result = generate_csr(options) - - # We decrypt the key and compare the unencrypted serialization. - key = crypto.load_privatekey( - crypto.FILETYPE_PEM, - result['key_pem'], - u'\u20acuro'.encode('utf-8')) - self.assertEqual( - crypto.dump_privatekey(crypto.FILETYPE_PEM, key), - crypto.dump_privatekey(crypto.FILETYPE_PEM, result['key']), - ) - - def test_existing_key_string(self): - """ - It can generate a CSR from an existing private key as text. - """ - key_pem = RSA_PRIVATE - - options = self.parseArguments([ - self.command_name, - '--common-name=domain.com', - ]) - options.key = key_pem - - result = generate_csr(options) - - # OpenSSL.crypto.PKey has no equality so we need to compare the - # serialization. - self.assertEqual(1024L, result['key'].bits()) - self.assertEqual(crypto.TYPE_RSA, result['key'].type()) - self.assertEqual(key_pem, result['key_pem']) - # For CSR we can not get extensions so we only check the subject. - csr = crypto.dump_certificate_request( - crypto.FILETYPE_PEM, result['csr']) - self.assertEqual(csr, result['csr_pem']) - subject = result['csr'].get_subject() - self.assertEqual(u'domain.com', subject.commonName) - - def test_existing_key_path(self): - """ - It can generate a CSR from an existing private key file. - """ - key_pem = RSA_PRIVATE - key_path, _ = self.tempFile(content=key_pem) - - options = self.parseArguments([ - self.command_name, - '--key', key_path, - '--common-name=domain.com', - ]) - - result = generate_csr(options) - - # OpenSSL.crypto.PKey has no equality so we need to compare the - # serialization. - self.assertEqual(1024L, result['key'].bits()) - self.assertEqual(crypto.TYPE_RSA, result['key'].type()) - self.assertEqual(key_pem, result['key_pem']) - # For CSR we can not get extensions so we only check the subject. - csr = crypto.dump_certificate_request( - crypto.FILETYPE_PEM, result['csr']) - self.assertEqual(csr, result['csr_pem']) - subject = result['csr'].get_subject() - self.assertEqual(u'domain.com', subject.commonName) - - -class Test_generate_and_store_csr(CommandLineTestBase): - """ - Unit tests for generate_and_store_csr. - """ - - def test_key_exists(self): - """ - Raise an exception when server key already exists. - """ - self.test_segments = mk.fs.createFileInTemp() - path = mk.fs.getRealPathFromSegments(self.test_segments) - options = self.parseArguments([ - self.command_name, - '--common-name=domain.com', - '--key-file', path, - ]) - - with self.assertRaises(KeyCertException) as context: - generate_and_store_csr(options) - - self.assertEqual('Key file already exists.', context.exception.message) - - def test_key_and_csr(self): - """ - Will write the key an csr on local filesystem. - """ - key_path, self.test_segments = mk.fs.makePathInTemp() - csr_segments = self.test_segments[:] - csr_segments[-1] = u'%s.csr' % csr_segments[-1] - self.addCleanup(mk.fs.deleteFile, csr_segments) - - options = self.parseArguments([ - self.command_name, - '--common-name=domain.com', - '--key-file', key_path, - '--key-size=512', - ]) - - generate_and_store_csr(options) - - key_content = mk.fs.getFileContent(self.test_segments) - key = crypto.load_privatekey( - crypto.FILETYPE_PEM, key_content) - self.assertEqual(512, key.bits()) - csr_content = mk.fs.getFileContent(csr_segments) - csr = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_content) - self.assertEqual(u'domain.com', csr.get_subject().CN) - - def test_store_error(self): - """ - Raise an exception when failing to write the file. - """ - options = self.parseArguments([ - self.command_name, - '--common-name=domain.com', - '--key-file', 'no-such/parent/key.file', - '--key-size=512', - ]) - - with self.assertRaises(KeyCertException) as context: - generate_and_store_csr(options) - - self.assertStartsWith( - "[Errno 2] No such file or directory: ", - context.exception.message) diff --git a/pavement.py b/pavement.py index cacd888..9365663 100644 --- a/pavement.py +++ b/pavement.py @@ -24,7 +24,7 @@ def deps(): """ pip = load_entry_point('pip', 'console_scripts', 'pip') pip(args=[ - 'install', + 'install', '-U', '--extra-index-url', EXTRA_PYPI_INDEX, '-e', '.[dev]', ]) @@ -60,7 +60,7 @@ def test_interop_load_dsa(args): exit_code = 1 with pushd('build'): exit_code = call( - "../chevah/keycert/tests/ssh_load_keys_tests.sh dsa", shell=True) + "../src/chevah_keycert/tests/ssh_load_keys_tests.sh dsa", shell=True) sys.exit(exit_code) @@ -78,7 +78,7 @@ def test_interop_load_rsa(args): exit_code = 1 with pushd('build'): exit_code = call( - "../chevah/keycert/tests/ssh_load_keys_tests.sh rsa", shell=True) + "../src/chevah_keycert/tests/ssh_load_keys_tests.sh rsa", shell=True) sys.exit(exit_code) @@ -96,7 +96,7 @@ def test_interop_load_eced(args): exit_code = 1 with pushd('build'): exit_code = call( - "../chevah/keycert/tests/ssh_load_keys_tests.sh ecdsa ed25519", shell=True) + "../src/chevah_keycert/tests/ssh_load_keys_tests.sh ecdsa ed25519", shell=True) sys.exit(exit_code) @@ -114,7 +114,7 @@ def test_interop_generate(args): exit_code = 1 with pushd('build'): exit_code = call( - "../chevah/keycert/tests/ssh_gen_keys_tests.sh", shell=True) + "../stc/chevah_keycert/tests/ssh_gen_keys_tests.sh", shell=True) sys.exit(exit_code) @@ -129,7 +129,7 @@ def lint(): sys.argv = [ re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])] + [ - 'chevah', + 'src/chevah_keycert', ] try: diff --git a/setup.py b/setup.py index 315c2c5..1210cdc 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand -VERSION = '2.1.2' +VERSION = '3.0.0' class NoseTestCommand(TestCommand): @@ -99,15 +99,14 @@ def run_tests(self): keywords='twisted ssh ssl tls pki ca', - namespace_packages=['chevah'], - + package_dir={'.': 'src'}, packages=find_packages(exclude=['contrib', 'docs', 'tests*']), install_requires=[ 'pyopenssl >=0.13', 'pyasn1 >=0.1.7', 'cryptography >= 3.2', - 'chevah-compat >=0.58.2', + 'chevah-compat >= 1.0.1', 'scandir >= 1.7', 'constantly >=15.1.0', ], From 85515448a40d37c4711b66fba42d1e89eb7f963d Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Mon, 20 Mar 2023 23:54:07 +0000 Subject: [PATCH 02/41] Move to src and in non-namespace. --- chevah/__init__.py | 7 - setup.cfg | 49 +- setup.py | 63 +- src/chevah_keycert/__init__.py | 13 + src/chevah_keycert/common.py | 179 + src/chevah_keycert/exceptions.py | 41 + src/chevah_keycert/sexpy.py | 48 + src/chevah_keycert/ssh.py | 2824 ++++++++++++++++ src/chevah_keycert/ssl.py | 418 +++ src/chevah_keycert/tests/__init__.py | 19 + src/chevah_keycert/tests/helpers.py | 69 + src/chevah_keycert/tests/keydata.py | 632 ++++ .../tests/ssh_common_test_inc.sh | 18 + .../tests/ssh_gen_keys_tests.sh | 165 + .../tests/ssh_load_keys_tests.sh | 308 ++ src/chevah_keycert/tests/test_exceptions.py | 35 + src/chevah_keycert/tests/test_ssh.py | 2898 +++++++++++++++++ src/chevah_keycert/tests/test_ssl.py | 568 ++++ 18 files changed, 8284 insertions(+), 70 deletions(-) delete mode 100644 chevah/__init__.py create mode 100644 src/chevah_keycert/__init__.py create mode 100644 src/chevah_keycert/common.py create mode 100644 src/chevah_keycert/exceptions.py create mode 100644 src/chevah_keycert/sexpy.py create mode 100644 src/chevah_keycert/ssh.py create mode 100644 src/chevah_keycert/ssl.py create mode 100644 src/chevah_keycert/tests/__init__.py create mode 100644 src/chevah_keycert/tests/helpers.py create mode 100644 src/chevah_keycert/tests/keydata.py create mode 100644 src/chevah_keycert/tests/ssh_common_test_inc.sh create mode 100755 src/chevah_keycert/tests/ssh_gen_keys_tests.sh create mode 100755 src/chevah_keycert/tests/ssh_load_keys_tests.sh create mode 100644 src/chevah_keycert/tests/test_exceptions.py create mode 100644 src/chevah_keycert/tests/test_ssh.py create mode 100644 src/chevah_keycert/tests/test_ssl.py diff --git a/chevah/__init__.py b/chevah/__init__.py deleted file mode 100644 index 2e2033b..0000000 --- a/chevah/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# this is a namespace package -try: - import pkg_resources - pkg_resources.declare_namespace(__name__) -except ImportError: - import pkgutil - __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/setup.cfg b/setup.cfg index 5e40900..69b631c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,49 @@ -[wheel] +[metadata] +name = chevah-keycert +version = 3.0.0 +maintainer = Adi Roiban +maintainer_email = adi.roiban@proatria.com +license = MIT +platforms = any +description = SSH Keys and SSL keys and certificates management. +long_description = file: README.rst +url = 'https://github.com/chevah/chevah-keycert' + + +[options] +install_requires = + pyopenssl >=0.13 + pyasn1 >=0.1.7 + cryptography >= 3.2 + chevah-compat >= 1 + scandir >= 1.7 + constantly >=15.1.0 +packages = find: +package_dir = =src + +[options.packages.find] +where = src + + +[options.extras_require] +; These are the deps required to develop. +; Try to pin them as much as possible. +dev = + zope.interface + future + + pocketlint ==1.4.4.c10 + pyflakes >= 1.5.0 + pycodestyle ==2.3.1 + + nose + mock + bunch + coverage==4.5.4 + codecov + unidecode + distro + + +[bdist_wheel] universal = 1 diff --git a/setup.py b/setup.py index 1210cdc..dca8b3c 100644 --- a/setup.py +++ b/setup.py @@ -6,8 +6,6 @@ from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand -VERSION = '3.0.0' - class NoseTestCommand(TestCommand): @@ -70,66 +68,7 @@ def run_tests(self): here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() - setup( - name='chevah-keycert', - - version=VERSION, - - description='SSH Keys and SSL keys and certificates management.', - long_description=long_description, - - url='https://github.com/chevah/chevah-keycert', - - author='Adi Roiban', - author_email='adiroiban@gmail.com', - - license='MIT', - - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Build Tools', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - ], - - keywords='twisted ssh ssl tls pki ca', - - package_dir={'.': 'src'}, - packages=find_packages(exclude=['contrib', 'docs', 'tests*']), - - install_requires=[ - 'pyopenssl >=0.13', - 'pyasn1 >=0.1.7', - 'cryptography >= 3.2', - 'chevah-compat >= 1.0.1', - 'scandir >= 1.7', - 'constantly >=15.1.0', - ], - - extras_require={ - 'dev': [ - 'zope.interface', - 'future', - - 'pocketlint ==1.4.4.c10', - 'pyflakes >= 1.5.0', - 'pycodestyle ==2.3.1', - - 'nose', - 'remote_pdb', - 'mock', - 'bunch', - 'coverage==4.5.4', - 'codecov', - 'unidecode', - 'ld', - ], - }, cmdclass={'test': NoseTestCommand}, - test_suite='chevah.keycert', + test_suite='chevah_keycert', ) diff --git a/src/chevah_keycert/__init__.py b/src/chevah_keycert/__init__.py new file mode 100644 index 0000000..2961557 --- /dev/null +++ b/src/chevah_keycert/__init__.py @@ -0,0 +1,13 @@ +""" +SSL and SSH key management. +""" +from __future__ import absolute_import +import sys + + +def _path(path, encoding='utf-8'): + if sys.platform.startswith('win'): + # On Windows and OSX we always use unicode. + return path # pragma: no cover + + return path.encode(encoding) diff --git a/src/chevah_keycert/common.py b/src/chevah_keycert/common.py new file mode 100644 index 0000000..c7b52b4 --- /dev/null +++ b/src/chevah_keycert/common.py @@ -0,0 +1,179 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Common functions for the all classes from this package. + +Forked from twisted.conch.ssh.common +""" + +from __future__ import absolute_import, division +import struct + +from cryptography.utils import int_from_bytes, int_to_bytes +import six +from six.moves import range + + +def iterbytes(originalBytes): + return originalBytes + + +def native_string(s): + return s + + + +def NS(t): + """ + net string + """ + if isinstance(t, six.text_type): + t = t.encode("utf-8") + return struct.pack('!L', len(t)) + t + + + +def getNS(s, count=1): + """ + get net string + """ + ns = [] + c = 0 + for i in range(count): + l, = struct.unpack('!L', s[c:c + 4]) + ns.append(s[c + 4:4 + l + c]) + c += 4 + l + return tuple(ns) + (s[c:],) + + + +def MP(number): + if number == 0: + return b'\000' * 4 + assert number > 0 + bn = int_to_bytes(number) + if ord(bn[0:1]) & 128: + bn = b'\000' + bn + return struct.pack('>L', len(bn)) + bn + + + +def getMP(data, count=1): + """ + Get multiple precision integer out of the string. A multiple precision + integer is stored as a 4-byte length followed by length bytes of the + integer. If count is specified, get count integers out of the string. + The return value is a tuple of count integers followed by the rest of + the data. + """ + mp = [] + c = 0 + for i in range(count): + length, = struct.unpack('>L', data[c:c + 4]) + mp.append(int_from_bytes(data[c + 4:c + 4 + length], 'big')) + c += 4 + length + return tuple(mp) + (data[c:],) + + + +def ffs(c, s): + """ + first from second + goes through the first list, looking for items in the second, returns the first one + """ + for i in c: + if i in s: + return i + + + +def force_unicode(value): + """ + Decode the `value` to unicode. + + It will try to extract the message from an exception. + + In case there are encoding errors when converting the invalid characters + are replaced. + """ + import errno + + def str_or_repr(value): + + if isinstance(value, six.text_type): + return value + + try: + return six.text_type(value, encoding='utf-8') + except Exception: + """ + Not UTF-8 encoded value. + """ + + try: + return six.text_type(value, encoding='windows-1252') + except Exception: + """ + Not Windows encoded value. + """ + + try: + return six.text_type(str(value), encoding='utf-8', errors='replace') + except (UnicodeDecodeError, UnicodeEncodeError): + """ + Not UTF-8 encoded value. + """ + + try: + return six.text_type( + str(value), encoding='windows-1252', errors='replace') + except (UnicodeDecodeError, UnicodeEncodeError): + pass + + # No luck with str, try repr() + return six.text_type(repr(value), encoding='windows-1252', errors='replace') + + if value is None: + return u'None' + + if isinstance(value, six.text_type): + return value + + if isinstance(value, EnvironmentError) and value.errno: + # IOError, OSError, WindowsError. + code = value.errno + message = value.strerror + # Convert to Unix message to help with testing. + if code == errno.ENOENT: + # On Windows it is: + # The system cannot find the file specified. + message = b'No such file or directory' + if code == errno.EEXIST: + # On Windows it is: + # Cannot create a file when that file already exists + message = b'File exists' + if code == errno.EBADF: + # On AIX: Bad file number + message = b'Bad file descriptor' + + if code and message: + if value.filename: + return "[Errno %s] %s: '%s'" % ( + code, + str_or_repr(message), + str_or_repr(value.filename), + ) + return '[Errno %s] %s.' % (code, str_or_repr(message)) + + if isinstance(value, Exception): + try: + details = str(value) + except (UnicodeDecodeError, UnicodeEncodeError): + details = getattr(value, 'message', '') + result = str_or_repr(details) + if result: + return result + return str_or_repr(repr(value)) + + return str_or_repr(value) diff --git a/src/chevah_keycert/exceptions.py b/src/chevah_keycert/exceptions.py new file mode 100644 index 0000000..9b78279 --- /dev/null +++ b/src/chevah_keycert/exceptions.py @@ -0,0 +1,41 @@ +# Copyright (c) 2014 Adi Roiban. +# See LICENSE for details. +""" +Public exceptions raised by this package. +""" + + +class KeyCertException(Exception): + """ + Generic exception raised by the package. + + Code calling the public API should handle only this exception. + The other exceptions are just for fine tunning. + """ + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message.encode('utf-8') + + +class BadKeyError(KeyCertException): + """ + Raised when a key isn't what we expected from it. + + XXX: we really need to check for bad keys + """ + + +class BadSignatureAlgorithmError(KeyCertException): + """ + Raised when a public key signature algorithm name isn't defined for this + public key format. + """ + + +class EncryptedKeyError(BadKeyError): + """ + Raised when an encrypted key is presented to fromString/fromFile without + a password. + """ diff --git a/src/chevah_keycert/sexpy.py b/src/chevah_keycert/sexpy.py new file mode 100644 index 0000000..e2eb99b --- /dev/null +++ b/src/chevah_keycert/sexpy.py @@ -0,0 +1,48 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. +""" +S-expression read / write. + +Forked from twisted.conch.ssh.sexpy +""" + + +def parse(s): + s = s.strip() + expr = [] + while s: + if s[0] == '(': + newSexp = [] + if expr: + expr[-1].append(newSexp) + expr.append(newSexp) + s = s[1:] + continue + if s[0] == ')': + aList = expr.pop() + s = s[1:] + if not expr: + assert not s + return aList + continue + i = 0 + while s[i].isdigit(): + i += 1 + assert i + length = int(s[:i]) + data = s[i + 1:i + 1 + length] + expr[-1].append(data) + s = s[i + 1 + length:] + assert 0, "this should not happen" # pragma: no cover + + +def pack(sexp): + s = "" + for o in sexp: + if type(o) in (type(()), type([])): + s += '(' + s += pack(o) + s += ')' + else: + s += '%i:%s' % (len(o), o) + return s diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py new file mode 100644 index 0000000..597280d --- /dev/null +++ b/src/chevah_keycert/ssh.py @@ -0,0 +1,2824 @@ +# Copyright (c) 2014 Adi Roiban. +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Handling of RSA, DSA, ECDSA, and Ed25519 keys. +""" + +from __future__ import absolute_import, division, unicode_literals + +import binascii +import itertools + +from hashlib import md5, sha1, sha256 +import base64 +import hmac +import unicodedata +import struct +import textwrap + +import bcrypt +from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ( + dsa, ec, ed25519, padding, rsa) +from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key, load_ssh_public_key) +from cryptography import utils +from six.moves import map +import six +from six.moves import range + +try: + + from cryptography.hazmat.primitives.asymmetric.utils import ( + encode_dss_signature, decode_dss_signature) +except ImportError: + from cryptography.hazmat.primitives.asymmetric.utils import ( + encode_rfc6979_signature as encode_dss_signature, + decode_rfc6979_signature as decode_dss_signature) +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +from pyasn1.error import PyAsn1Error +from pyasn1.type import univ +from pyasn1.codec.ber import decoder as berDecoder +from pyasn1.codec.ber import encoder as berEncoder + +import os +import os.path +from os import urandom +from base64 import encodestring as encodebytes +from base64 import decodestring as decodebytes +from cryptography.utils import int_from_bytes, int_to_bytes +from OpenSSL import crypto + +from chevah_keycert import common, sexpy, _path +from chevah_keycert.common import ( + force_unicode, + iterbytes, + ) +from chevah_keycert.exceptions import ( + BadKeyError, + BadSignatureAlgorithmError, + EncryptedKeyError, + KeyCertException, + ) +from constantly import NamedConstant, Names + +DEFAULT_PUBLIC_KEY_EXTENSION = u'.pub' +DEFAULT_KEY_SIZE = 2048 +DEFAULT_KEY_TYPE = 'rsa' +SSHCOM_MAGIC_NUMBER = int('3f6ff9eb', base=16) +PUTTY_HMAC_KEY = 'putty-private-key-file-mac-key' +ID_SHA1 = b'\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' + +# Curve lookup table +_curveTable = { + b'ecdsa-sha2-nistp256': ec.SECP256R1(), + b'ecdsa-sha2-nistp384': ec.SECP384R1(), + b'ecdsa-sha2-nistp521': ec.SECP521R1(), +} + +_secToNist = { + b'secp256r1' : b'nistp256', + b'secp384r1' : b'nistp384', + b'secp521r1' : b'nistp521', +} + + +_ecSizeTable = { + 256: ec.SECP256R1(), + 384: ec.SECP384R1(), + 521: ec.SECP521R1(), +} + +class BadFingerPrintFormat(Exception): + """ + Raises when unsupported fingerprint formats are presented to fingerprint. + """ + + + +class FingerprintFormats(Names): + """ + Constants representing the supported formats of key fingerprints. + + @cvar MD5_HEX: Named constant representing fingerprint format generated + using md5[RFC1321] algorithm in hexadecimal encoding. + @type MD5_HEX: L{twisted.python.constants.NamedConstant} + + @cvar SHA256_BASE64: Named constant representing fingerprint format + generated using sha256[RFC4634] algorithm in base64 encoding + @type SHA256_BASE64: L{NamedConstant} + @cvar SHA1_BASE64: Named constant representing fingerprint format + generated using sha1[RFC3174] algorithm in base64 encoding + @type SHA1_BASE64: L{NamedConstant} + """ + + MD5_HEX = NamedConstant() + SHA256_BASE64 = NamedConstant() + SHA1_BASE64 = NamedConstant() + + +class PassphraseNormalizationError(Exception): + """ + Raised when a passphrase contains Unicode characters that cannot be + normalized using the available Unicode character database. + """ + + +def _normalizePassphrase(passphrase): + """ + Normalize a passphrase, which may be Unicode. + + If the passphrase is Unicode, this follows the requirements of U{NIST + 800-63B, section + 5.1.1.2} + for Unicode characters in memorized secrets: it applies the + Normalization Process for Stabilized Strings using NFKC normalization. + The passphrase is then encoded using UTF-8. + + @type passphrase: L{bytes} or L{unicode} or L{None} + @param passphrase: The passphrase to normalize. + + @return: The normalized passphrase, if any. + @rtype: L{bytes} or L{None} + @raises PassphraseNormalizationError: if the passphrase is Unicode and + cannot be normalized using the available Unicode character database. + """ + if isinstance(passphrase, six.text_type): + # The Normalization Process for Stabilized Strings requires aborting + # with an error if the string contains any unassigned code point. + if any(unicodedata.category(c) == "Cn" for c in passphrase): + # Perhaps not very helpful, but we don't want to leak any other + # information about the passphrase. + raise PassphraseNormalizationError() + return unicodedata.normalize("NFKC", passphrase).encode("UTF-8") + else: + return passphrase + + +class Key(object): + """ + An object representing a key. A key can be either a public or + private key. A public key can verify a signature; a private key can + create or verify a signature. To generate a string that can be stored + on disk, use the toString method. If you have a private key, but want + the string representation of the public key, use Key.public().toString(). + + SSH Transport local-peer Key: (PrivateKey) + * fromCryptograpyObject / __init__ - for local peer private key + * blob() - return public blob - for handshake / sign payload + * sign - for local peer private key + + SSH Transport remote-peer key /PublicKey): + * fromPublicBlob() / __init__ - for remote peer public key + * verify - for remote peer + + SSH Key: + * fromString / __init__ + * toString + * getFormat - human readable representation of internal guessed type + * getCryptographyObject + + generate_ssh_key(type, size) -> external helper + """ + + @classmethod + def fromFile(cls, filename, type=None, passphrase=None, encoding='utf-8'): + """ + Load a key from a file. + + @param filename: The path to load key data from. + + @type type: L{str} or L{None} + @param type: A string describing the format the key data is in, or + L{None} to attempt detection of the type. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase the key is encrypted with, or L{None} + if there is no encryption. + + @rtype: L{Key} + @return: The loaded key. + """ + with open(_path(filename, encoding), 'rb') as file: + return cls.fromString(file.read(), type, passphrase) + + @classmethod + def fromString(cls, data, type=None, passphrase=None): + """ + Return a Key object corresponding to the string data. + type is optionally the type of string, matching a _fromString_* + method. Otherwise, the _guessStringType() classmethod will be used + to guess a type. If the key is encrypted, passphrase is used as + the decryption key. + + @type data: L{bytes} + @param data: The key data. + + @type type: L{str} or L{None} + @param type: A string describing the format the key data is in, or + L{None} to attempt detection of the type. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase the key is encrypted with, or L{None} + if there is no encryption. + + @rtype: L{Key} + @return: The loaded key. + """ + if isinstance(data, six.text_type): + data = data.encode("utf-8") + passphrase = _normalizePassphrase(passphrase) + if type is None: + type = cls._guessStringType(data) + if type is None: + raise BadKeyError( + 'Cannot guess the type for "%s"' % force_unicode(data[:80])) + + try: + method = getattr(cls, '_fromString_%s' % type.upper(), None) + if method is None: + raise BadKeyError( + 'no _fromString method for "%s"' % force_unicode(type[:30])) + if method.__code__.co_argcount == 2: # no passphrase + if passphrase: + raise BadKeyError('key not encrypted') + return method(data) + else: + return method(data, passphrase) + except (IndexError): + # Most probably some parts are missing from the key, so + # we consider it too short. + raise BadKeyError('Key is too short.') + except (struct.error, binascii.Error, TypeError): + raise BadKeyError('Fail to parse key content.') + + @classmethod + def _fromString_BLOB(cls, blob): + """ + Return a public key object corresponding to this public key blob. + + The format of a RSA public key blob is:: + string 'ssh-rsa' + integer e + integer n + + The format of a DSA public key blob is:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + + The format of ECDSA-SHA2-* public key blob is:: + string 'ecdsa-sha2-[identifier]' + integer x + integer y + + identifier is the standard NIST curve name. + + The format of an Ed25519 public key blob is:: + string 'ssh-ed25519' + string a + + @type blob: L{bytes} + @param blob: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if the key type (the first string) is unknown. + """ + keyType, rest = common.getNS(blob) + if keyType == b'ssh-rsa': + e, n, rest = common.getMP(rest, 2) + return cls._fromRSAComponents(n, e) + elif keyType == b'ssh-dss': + p, q, g, y, rest = common.getMP(rest, 4) + return cls._fromDSAComponents( y, p, q, g) + elif keyType in _curveTable: + return cls._fromECEncodedPoint( + encodedPoint=common.getNS(rest, 2)[1], + curve=keyType, + ) + elif keyType == b'ssh-ed25519': + a, rest = common.getNS(rest) + return cls._fromEd25519Components(a) + else: + raise BadKeyError('unknown blob type: "{}"'.format( + force_unicode(keyType[:30]))) + + @classmethod + def _fromString_PRIVATE_BLOB(cls, blob): + """ + Return a private key object corresponding to this private key blob. + The blob formats are as follows: + + RSA keys:: + string 'ssh-rsa' + integer n + integer e + integer d + integer u + integer p + integer q + + DSA keys:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + integer x + + EC keys:: + string 'ecdsa-sha2-[identifier]' + string identifier + string q + integer privateValue + + identifier is the standard NIST curve name. + + Ed25519 keys:: + string 'ssh-ed25519' + string a + string k || a + + + @type blob: L{bytes} + @param blob: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if + * the key type (the first string) is unknown + * the curve name of an ECDSA key does not match the key type + """ + keyType, rest = common.getNS(blob) + + if keyType == b'ssh-rsa': + n, e, d, u, p, q, rest = common.getMP(rest, 6) + return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q) + elif keyType == b'ssh-dss': + p, q, g, y, x, rest = common.getMP(rest, 5) + return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) + elif keyType in _curveTable: + curve = _curveTable[keyType] + curveName, q, rest = common.getNS(rest, 2) + if curveName != _secToNist[curve.name.encode('ascii')]: + raise BadKeyError( + 'ECDSA curve name "%s" does not match key type "%s"' % ( + force_unicode(curveName), force_unicode(keyType))) + privateValue, rest = common.getMP(rest) + return cls._fromECEncodedPoint( + encodedPoint=q, curve=keyType, privateValue=privateValue) + elif keyType == b'ssh-ed25519': + # OpenSSH's format repeats the public key bytes for some reason. + # We're only interested in the private key here anyway. + a, combined, rest = common.getNS(rest, 2) + k = combined[:32] + return cls._fromEd25519Components(a, k=k) + else: + raise BadKeyError( + 'Unknown blob type: "%s"' % force_unicode(keyType[:30])) + + + @classmethod + def _fromString_PUBLIC_OPENSSH(cls, data): + """ + Return a public key object corresponding to this OpenSSH public key + string. The format of an OpenSSH public key string is:: + + + @type data: L{bytes} + @param data: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if the blob type is unknown. + """ + # ECDSA keys don't need base64 decoding which is required + # for RSA or DSA key. + if data.startswith(b'ecdsa-sha2'): + return cls(load_ssh_public_key(data, default_backend())) + blob = decodebytes(data.split()[1]) + return cls._fromString_BLOB(blob) + + + @classmethod + def _fromString_PRIVATE_OPENSSH_V1(cls, data, passphrase): + """ + Return a private key object corresponding to this OpenSSH private key + string, in the "openssh-key-v1" format introduced in OpenSSH 6.5. + + The format of an openssh-key-v1 private key string is:: + -----BEGIN OPENSSH PRIVATE KEY----- + + -----END OPENSSH PRIVATE KEY----- + + The SSH protocol string is as described in + U{PROTOCOL.key}. + + @type data: L{bytes} + @param data: The key data. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase the key is encrypted with, or L{None} + if it is not encrypted. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if + * a passphrase is provided for an unencrypted key + * the SSH protocol encoding is incorrect + @raises EncryptedKeyError: if + * a passphrase is not provided for an encrypted key + """ + lines = data.strip().splitlines() + keyList = decodebytes(b''.join(lines[1:-1])) + if not keyList.startswith(b'openssh-key-v1\0'): + raise BadKeyError('unknown OpenSSH private key format') + keyList = keyList[len(b'openssh-key-v1\0'):] + cipher, kdf, kdfOptions, rest = common.getNS(keyList, 3) + n = struct.unpack('!L', rest[:4])[0] + if n != 1: + raise BadKeyError('only OpenSSH private key files containing ' + 'a single key are supported') + # Ignore public key + _, encPrivKeyList, _ = common.getNS(rest[4:], 2) + if cipher != b'none': + if not passphrase: + raise EncryptedKeyError('Passphrase must be provided ' + 'for an encrypted key') + # Determine cipher + if cipher in (b'aes128-ctr', b'aes192-ctr', b'aes256-ctr'): + algorithmClass = algorithms.AES + blockSize = 16 + keySize = int(cipher[3:6]) // 8 + ivSize = blockSize + else: + raise BadKeyError('unknown encryption type "%s"' % ( + force_unicode(cipher),)) + if kdf == b'bcrypt': + salt, rest = common.getNS(kdfOptions) + rounds = struct.unpack('!L', rest[:4])[0] + decKey = bcrypt.kdf( + passphrase, salt, keySize + ivSize, rounds, + # We can only use the number of rounds that OpenSSH used. + ignore_few_rounds=True) + else: + raise BadKeyError( + 'unknown KDF type "%s"' % (force_unicode(kdf),)) + if (len(encPrivKeyList) % blockSize) != 0: + raise BadKeyError('bad padding') + decryptor = Cipher( + algorithmClass(decKey[:keySize]), + modes.CTR(decKey[keySize:keySize + ivSize]), + backend=default_backend() + ).decryptor() + privKeyList = ( + decryptor.update(encPrivKeyList) + decryptor.finalize()) + else: + if kdf != b'none': + raise BadKeyError('private key specifies KDF "%s" but no ' + 'cipher' % (force_unicode(kdf),)) + privKeyList = encPrivKeyList + check1 = struct.unpack('!L', privKeyList[:4])[0] + check2 = struct.unpack('!L', privKeyList[4:8])[0] + if check1 != check2: + raise BadKeyError( + 'Private key sanity check failed. Maybe invalid passphrase.') + return cls._fromString_PRIVATE_BLOB(privKeyList[8:]) + + + @classmethod + def _fromString_PRIVATE_OPENSSH(cls, data, passphrase): + """ + Return a private key object corresponding to this OpenSSH private key + string, in the old PEM-based format. + + The format of a PEM-based OpenSSH private key string is:: + -----BEGIN PRIVATE KEY----- + [Proc-Type: 4,ENCRYPTED + DEK-Info: DES-EDE3-CBC,] + + ------END PRIVATE KEY------ + + The ASN.1 structure of a RSA key is:: + (0, n, e, d, p, q) + + The ASN.1 structure of a DSA key is:: + (0, p, q, g, y, x) + + The ASN.1 structure of a ECDSA key is:: + (ECParameters, OID, NULL) + + @type data: L{bytes} + @param data: The key data. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase the key is encrypted with, or L{None} + if it is not encrypted. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if + * a passphrase is provided for an unencrypted key + * the ASN.1 encoding is incorrect + @raises EncryptedKeyError: if + * a passphrase is not provided for an encrypted key + """ + lines = data.strip().splitlines() + kind = lines[0][11:-17] + if lines[1].startswith(b'Proc-Type: 4,ENCRYPTED'): + if not passphrase: + raise EncryptedKeyError('Passphrase must be provided ' + 'for an encrypted key') + + # Determine cipher and initialization vector + try: + _, cipherIVInfo = lines[2].split(b' ', 1) + cipher, ivdata = cipherIVInfo.rstrip().split(b',', 1) + except ValueError: + raise BadKeyError( + 'invalid DEK-info "%s"' % (force_unicode(lines[2]),)) + + if cipher in (b'AES-128-CBC', b'AES-256-CBC'): + algorithmClass = algorithms.AES + keySize = int(cipher.split(b'-')[1]) // 8 + if len(ivdata) != 32: + raise BadKeyError('AES encrypted key with a bad IV') + elif cipher == b'DES-EDE3-CBC': + algorithmClass = algorithms.TripleDES + keySize = 24 + if len(ivdata) != 16: + raise BadKeyError('DES encrypted key with a bad IV') + else: + raise BadKeyError( + 'unknown encryption type "%s"' % (force_unicode(cipher),)) + + # Extract keyData for decoding + iv = bytes(bytearray([int(ivdata[i:i + 2], 16) + for i in range(0, len(ivdata), 2)])) + ba = md5(passphrase + iv[:8]).digest() + bb = md5(ba + passphrase + iv[:8]).digest() + decKey = (ba + bb)[:keySize] + b64Data = decodebytes(b''.join(lines[3:-1])) + + decryptor = Cipher( + algorithmClass(decKey), + modes.CBC(iv), + backend=default_backend() + ).decryptor() + keyData = decryptor.update(b64Data) + decryptor.finalize() + + removeLen = ord(keyData[-1:]) + keyData = keyData[:-removeLen] + else: + b64Data = b''.join(lines[1:-1]) + keyData = decodebytes(b64Data) + + try: + decodedKey = berDecoder.decode(keyData)[0] + except PyAsn1Error as e: + raise BadKeyError( + 'Failed to decode key (Bad Passphrase?): %s' % ( + force_unicode(e),)) + + if kind == b'EC': + return cls( + load_pem_private_key(data, passphrase, default_backend())) + + if kind == b'RSA': + if len(decodedKey) == 2: # Alternate RSA key + decodedKey = decodedKey[0] + if len(decodedKey) < 6: + raise BadKeyError('RSA key failed to decode properly') + + n, e, d, p, q, dmp1, dmq1, iqmp = [ + int(value) for value in decodedKey[1:9] + ] + return cls( + rsa.RSAPrivateNumbers( + p=p, + q=q, + d=d, + dmp1=dmp1, + dmq1=dmq1, + iqmp=iqmp, + public_numbers=rsa.RSAPublicNumbers(e=e, n=n), + ).private_key(default_backend()) + ) + elif kind == b'DSA': + p, q, g, y, x = [int(value) for value in decodedKey[1: 6]] + if len(decodedKey) < 6: + raise BadKeyError('DSA key failed to decode properly') + return cls._fromDSAComponents(y, p, q, g, x) + else: + raise BadKeyError('unknown key type "%s"' % (force_unicode(kind),)) + + + @classmethod + def _fromString_PUBLIC_LSH(cls, data): + """ + Return a public key corresponding to this LSH public key string. + The LSH public key string format is:: + , ()+))> + + The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e. + The names for a DSA (key type 'dsa') key are: y, g, p, q. + + @type data: L{bytes} + @param data: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if the key type is unknown + """ + sexp = sexpy.parse(decodebytes(data[1:-1])) + assert sexp[0] == b'public-key' + kd = {} + for name, data in sexp[1][1:]: + kd[name] = common.getMP(common.NS(data))[0] + if sexp[1][0] == b'dsa': + return cls._fromDSAComponents( + y=kd[b'y'], g=kd[b'g'], p=kd[b'p'], q=kd[b'q']) + + elif sexp[1][0] == b'rsa-pkcs1-sha1': + return cls._fromRSAComponents(n=kd[b'n'], e=kd[b'e']) + else: + raise BadKeyError('unknown lsh key type "%s"' % ( + force_unicode(sexp[1][0][:30]),)) + + @classmethod + def _fromString_PRIVATE_LSH(cls, data): + """ + Return a private key corresponding to this LSH private key string. + The LSH private key string format is:: + , (, )+))> + + The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e, d, p, q. + The names for a DSA (key type 'dsa') key are: y, g, p, q, x. + + @type data: L{bytes} + @param data: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if the key type is unknown + """ + sexp = sexpy.parse(data) + assert sexp[0] == b'private-key' + kd = {} + for name, data in sexp[1][1:]: + kd[name] = common.getMP(common.NS(data))[0] + if sexp[1][0] == b'dsa': + assert len(kd) == 5, len(kd) + return cls._fromDSAComponents( + y=kd[b'y'], g=kd[b'g'], p=kd[b'p'], q=kd[b'q'], x=kd[b'x']) + elif sexp[1][0] == b'rsa-pkcs1': + assert len(kd) == 8, len(kd) + if kd[b'p'] > kd[b'q']: # Make p smaller than q + kd[b'p'], kd[b'q'] = kd[b'q'], kd[b'p'] + return cls._fromRSAComponents( + n=kd[b'n'], e=kd[b'e'], d=kd[b'd'], p=kd[b'p'], q=kd[b'q']) + + else: + raise BadKeyError( + 'unknown lsh key type "%s"' % (force_unicode(sexp[1][0][:30]),)) + + @classmethod + def _fromString_AGENTV3(cls, data): + """ + Return a private key object corresponsing to the Secure Shell Key + Agent v3 format. + + The SSH Key Agent v3 format for a RSA key is:: + string 'ssh-rsa' + integer e + integer d + integer n + integer u + integer p + integer q + + The SSH Key Agent v3 format for a DSA key is:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + integer x + + @type data: L{bytes} + @param data: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if the key type (the first string) is unknown + """ + keyType, data = common.getNS(data) + if keyType == b'ssh-dss': + p, data = common.getMP(data) + q, data = common.getMP(data) + g, data = common.getMP(data) + y, data = common.getMP(data) + x, data = common.getMP(data) + return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) + elif keyType == b'ssh-rsa': + e, data = common.getMP(data) + d, data = common.getMP(data) + n, data = common.getMP(data) + u, data = common.getMP(data) + p, data = common.getMP(data) + q, data = common.getMP(data) + return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) + else: # pragma: no cover + raise BadKeyError( + 'unknown key type "%s"' % (force_unicode(keyType[:30]),)) + + @classmethod + def _guessStringType(cls, data): + """ + Guess the type of key in data. The types map to _fromString_* + methods. + + @type data: L{bytes} + @param data: The key data. + """ + if data.startswith(b'ssh-') or data.startswith(b'ecdsa-sha2-'): + return 'public_openssh' + elif data.startswith(b'---- BEGIN SSH2 PUBLIC KEY ----'): + return 'public_sshcom' + elif data.startswith(b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----'): + return 'private_sshcom' + elif data.startswith(b'-----BEGIN RSA PUBLIC'): + return 'public_pkcs1_rsa' + elif ( + data.startswith(b'-----BEGIN RSA PRIVATE') or + data.startswith(b'-----BEGIN DSA PRIVATE') or + data.startswith(b'-----BEGIN EC PRIVATE') + ): + # This is also private PKCS#1 format. + return 'private_openssh' + elif data.startswith(b'-----BEGIN OPENSSH PRIVATE KEY-----'): + return 'private_openssh_v1' + + elif data.startswith(b'-----BEGIN CERTIFICATE-----'): + return 'public_x509_certificate' + + elif data.startswith(b'-----BEGIN PUBLIC KEY-----'): + # Public Key in X.509 format it's as follows + return 'public_x509' + + elif data.startswith(b'-----BEGIN PRIVATE KEY-----'): + return 'private_pkcs8' + elif data.startswith(b'-----BEGIN ENCRYPTED PRIVATE KEY-----'): + return 'private_encrypted_pkcs8' + elif data.startswith(b'PuTTY-User-Key-File-2'): + return 'private_putty' + elif data.startswith(b'{'): + return 'public_lsh' + elif data.startswith(b'('): + return 'private_lsh' + elif (data.startswith(b'\x00\x00\x00\x07ssh-') or + data.startswith(b'\x00\x00\x00\x13ecdsa-') or + data.startswith(b'\x00\x00\x00\x0bssh-ed25519')): + ignored, rest = common.getNS(data) + count = 0 + while rest: + count += 1 + ignored, rest = common.getMP(rest) + if count > 4: + return 'agentv3' + else: + return 'blob' + + @classmethod + def _fromRSAComponents(cls, n, e, d=None, p=None, q=None, u=None): + """ + Build a key from RSA numerical components. + + @type n: L{int} + @param n: The 'n' RSA variable. + + @type e: L{int} + @param e: The 'e' RSA variable. + + @type d: L{int} or L{None} + @param d: The 'd' RSA variable (optional for a public key). + + @type p: L{int} or L{None} + @param p: The 'p' RSA variable (optional for a public key). + + @type q: L{int} or L{None} + @param q: The 'q' RSA variable (optional for a public key). + + @type u: L{int} or L{None} + @param u: The 'u' RSA variable. Ignored, as its value is determined by + p and q. + + @rtype: L{Key} + @return: An RSA key constructed from the values as given. + """ + publicNumbers = rsa.RSAPublicNumbers(e=e, n=n) + if d is None: + # We have public components. + keyObject = publicNumbers.public_key(default_backend()) + else: + privateNumbers = rsa.RSAPrivateNumbers( + p=p, + q=q, + d=d, + dmp1=rsa.rsa_crt_dmp1(d, p), + dmq1=rsa.rsa_crt_dmq1(d, q), + iqmp=rsa.rsa_crt_iqmp(p, q), + public_numbers=publicNumbers, + ) + keyObject = privateNumbers.private_key(default_backend()) + + return cls(keyObject) + + @classmethod + def _fromDSAComponents(cls, y, p, q, g, x=None): + """ + Build a key from DSA numerical components. + + @type y: L{int} + @param y: The 'y' DSA variable. + + @type p: L{int} + @param p: The 'p' DSA variable. + + @type q: L{int} + @param q: The 'q' DSA variable. + + @type g: L{int} + @param g: The 'g' DSA variable. + + @type x: L{int} or L{None} + @param x: The 'x' DSA variable (optional for a public key) + + @rtype: L{Key} + @return: A DSA key constructed from the values as given. + """ + publicNumbers = dsa.DSAPublicNumbers( + y=y, parameter_numbers=dsa.DSAParameterNumbers(p=p, q=q, g=g)) + + if x is None: + try: + # We have public components. + keyObject = publicNumbers.public_key(default_backend()) + return cls(keyObject) + except ValueError as error: + raise BadKeyError( + 'Unsupported DSA public key: "%s"' % (force_unicode(error),)) + + try: + privateNumbers = dsa.DSAPrivateNumbers( + x=x, public_numbers=publicNumbers) + keyObject = privateNumbers.private_key(default_backend()) + except ValueError as error: + raise BadKeyError( + 'Unsupported DSA private key: "%s"' % (force_unicode(error),)) + + return cls(keyObject) + + @classmethod + def _fromECComponents(cls, x, y, curve, privateValue=None): + """ + Build a key from EC components. + + @param x: The affine x component of the public point used for verifying. + @type x: L{int} + + @param y: The affine y component of the public point used for verifying. + @type y: L{int} + + @param curve: NIST name of elliptic curve. + @type curve: L{bytes} + + @param privateValue: The private value. + @type privateValue: L{int} + """ + + publicNumbers = ec.EllipticCurvePublicNumbers( + x=x, y=y, curve=_curveTable[curve]) + if privateValue is None: + # We have public components. + keyObject = publicNumbers.public_key(default_backend()) + else: + privateNumbers = ec.EllipticCurvePrivateNumbers( + private_value=privateValue, public_numbers=publicNumbers) + keyObject = privateNumbers.private_key(default_backend()) + + return cls(keyObject) + + @classmethod + def _fromECEncodedPoint(cls, encodedPoint, curve, privateValue=None): + """ + Build a key from an EC encoded point. + + @param encodedPoint: The public point encoded as in SEC 1 v2.0 + section 2.3.3. + @type encodedPoint: L{bytes} + + @param curve: NIST name of elliptic curve. + @type curve: L{bytes} + + @param privateValue: The private value. + @type privateValue: L{int} + """ + + if privateValue is None: + # We have public components. + keyObject = ec.EllipticCurvePublicKey.from_encoded_point( + _curveTable[curve], encodedPoint + ) + else: + keyObject = ec.derive_private_key( + privateValue, _curveTable[curve], default_backend() + ) + + return cls(keyObject) + + @classmethod + def _fromEd25519Components(cls, a, k=None): + """Build a key from Ed25519 components. + + @param a: The Ed25519 public key, as defined in RFC 8032 section + 5.1.5. + @type a: L{bytes} + + @param k: The Ed25519 private key, as defined in RFC 8032 section + 5.1.5. + @type k: L{bytes} + """ + + try: + if k is None: + keyObject = ed25519.Ed25519PublicKey.from_public_bytes(a) + else: + keyObject = ed25519.Ed25519PrivateKey.from_private_bytes(k) + + return cls(keyObject) + except UnsupportedAlgorithm: + raise BadKeyError('Ed25519 keys are not supported.') + + def __init__(self, keyObject): + """ + Initialize with a private or public + C{cryptography.hazmat.primitives.asymmetric} key. + + @param keyObject: Low level key. + @type keyObject: C{cryptography.hazmat.primitives.asymmetric} key. + """ + self._keyObject = keyObject + + def __eq__(self, other): + """ + Return True if other represents an object with the same key. + """ + if type(self) == type(other): + return self.type() == other.type() and self.data() == other.data() + else: + return NotImplemented + + def __ne__(self, other): + """ + Return True if other represents anything other than this key. + """ + result = self.__eq__(other) + if result == NotImplemented: + return result + return not result + + def __repr__(self): + """ + Return a pretty representation of this object. + """ + if self.type() == 'EC': + data = self.data() + name = data['curve'].decode('utf-8') + + if self.isPublic(): + out = '\n" + else: + lines = [ + '<%s %s (%s bits)' % ( + self.type(), + self.isPublic() and 'Public Key' or 'Private Key', + self.size())] + for k, v in sorted(self.data().items()): + lines.append('attr %s:' % (k,)) + by = v if self.type() == 'Ed25519' else common.MP(v)[4:] + while by: + m = by[:15] + by = by[15:] + o = '' + for c in iterbytes(m): + o = o + '%02x:' % (ord(c),) + if len(m) < 15: + o = o[:-1] + lines.append('\t' + o) + lines[-1] = lines[-1] + '>' + return '\n'.join(lines) + + def isPublic(self): + """ + Check if this instance is a public key. + + @return: C{True} if this is a public key. + """ + return isinstance( + self._keyObject, + (rsa.RSAPublicKey, dsa.DSAPublicKey, ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey)) + + def public(self): + """ + Returns a version of this key containing only the public key data. + If this is a public key, this may or may not be the same object + as self. + + @rtype: L{Key} + @return: A public key. + """ + if self.isPublic(): + return self + else: + return Key(self._keyObject.public_key()) + + def fingerprint(self, format=FingerprintFormats.MD5_HEX): + """ + The fingerprint of a public key consists of the output of the + message-digest algorithm in the specified format. + Supported formats include L{FingerprintFormats.MD5_HEX}, + L{FingerprintFormats.SHA256_BASE64} and + L{FingerprintFormats.SHA1_BASE64} + + The input to the algorithm is the public key data as specified by [RFC4253]. + + The output of sha256[RFC4634] and sha1[RFC3174] algorithms are + presented to the user in the form of base64 encoded sha256 and sha1 + hashes, respectively. + Examples: + C{US5jTUa0kgX5ZxdqaGF0yGRu8EgKXHNmoT8jHKo1StM=} + C{9CCuTybG5aORtuW4jrFcp0PbK4U=} + + The output of the MD5[RFC1321](default) algorithm is presented to the user as + a sequence of 16 octets printed as hexadecimal with lowercase letters + and separated by colons. + Example: C{c1:b1:30:29:d7:b8:de:6c:97:77:10:d7:46:41:63:87} + + @param format: Format for fingerprint generation. Consists + hash function and representation format. + Default is L{FingerprintFormats.MD5_HEX} + + @since: 8.2 + + @return: the user presentation of this L{Key}'s fingerprint, as a + string. + + @rtype: L{str} + """ + if format is FingerprintFormats.SHA256_BASE64: + return base64.b64encode( + sha256(self.blob()).digest()).decode('ascii') + elif format is FingerprintFormats.SHA1_BASE64: + return base64.b64encode( + sha1(self.blob()).digest()).decode('ascii') + elif format is FingerprintFormats.MD5_HEX: + return ':'.join([binascii.hexlify(x) + for x in iterbytes(md5(self.blob()).digest())]) + else: + raise BadFingerPrintFormat( + 'Unsupported fingerprint format: %s' % (format,)) + + def type(self): + """ + Return the type of the object we wrap. Currently this can only be + 'RSA', 'DSA', 'EC', or 'Ed25519'. + + @rtype: L{str} + @raises RuntimeError: If the object type is unknown. + """ + if isinstance( + self._keyObject, (rsa.RSAPublicKey, rsa.RSAPrivateKey)): + return 'RSA' + elif isinstance( + self._keyObject, (dsa.DSAPublicKey, dsa.DSAPrivateKey)): + return 'DSA' + elif isinstance( + self._keyObject, + (ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey)): + return 'EC' + elif isinstance( + self._keyObject, + (ed25519.Ed25519PublicKey, ed25519.Ed25519PrivateKey)): + return 'Ed25519' + else: + raise RuntimeError( + 'unknown type of object: %r' % (self._keyObject,)) + + def sshType(self): + """ + Get the type of the object we wrap as defined in the SSH protocol, + defined in RFC 4253, Section 6.6. Currently this can only be b'ssh-rsa', + b'ssh-dss' or b'ecdsa-sha2-[identifier]'. + + identifier is the standard NIST curve name + + @return: The key type format. + @rtype: L{bytes} + """ + if self.type() == 'EC': + return ( + b'ecdsa-sha2-' + + _secToNist[self._keyObject.curve.name.encode('ascii')]) + else: + return { + 'RSA': b'ssh-rsa', + 'DSA': b'ssh-dss', + 'Ed25519': b'ssh-ed25519', + }[self.type()] + + def supportedSignatureAlgorithms(self): + """ + Get the public key signature algorithms supported by this key. + @return: A list of supported public key signature algorithm names. + @rtype: L{list} of L{bytes} + """ + if self.type() == "RSA": + return [b"rsa-sha2-512", b"rsa-sha2-256", b"ssh-rsa"] + else: + return [self.sshType()] + + def _getHashAlgorithm(self, signatureType): + """ + Return a hash algorithm for this key type given an SSH signature + algorithm name, or L{None} if no such hash algorithm is defined for + this key type. + """ + if self.type() == "EC": + # Hash algorithm depends on key size + if signatureType == self.sshType(): + keySize = self.size() + if keySize <= 256: + return hashes.SHA256() + elif keySize <= 384: + return hashes.SHA384() + else: + return hashes.SHA512() + else: + return None + else: + return { + ("RSA", b"ssh-rsa"): hashes.SHA1(), + ("RSA", b"rsa-sha2-256"): hashes.SHA256(), + ("RSA", b"rsa-sha2-512"): hashes.SHA512(), + ("DSA", b"ssh-dss"): hashes.SHA1(), + ("Ed25519", b"ssh-ed25519"): hashes.SHA512(), + }.get((self.type(), signatureType)) + + def size(self): + """ + Return the size of the object we wrap. + + @return: The size of the key. + @rtype: L{int} + """ + if self._keyObject is None: + return 0 + elif self.type() == 'EC': + return self._keyObject.curve.key_size + elif self.type() == 'Ed25519': + return 256 + return self._keyObject.key_size + + def data(self): + """ + Return the values of the public key as a dictionary. + + @rtype: L{dict} + """ + if isinstance(self._keyObject, rsa.RSAPublicKey): + numbers = self._keyObject.public_numbers() + return { + "n": numbers.n, + "e": numbers.e, + } + elif isinstance(self._keyObject, rsa.RSAPrivateKey): + numbers = self._keyObject.private_numbers() + return { + "n": numbers.public_numbers.n, + "e": numbers.public_numbers.e, + "d": numbers.d, + "p": numbers.p, + "q": numbers.q, + # Use a trick: iqmp is q^-1 % p, u is p^-1 % q + "u": rsa.rsa_crt_iqmp(numbers.q, numbers.p), + } + elif isinstance(self._keyObject, dsa.DSAPublicKey): + numbers = self._keyObject.public_numbers() + return { + "y": numbers.y, + "g": numbers.parameter_numbers.g, + "p": numbers.parameter_numbers.p, + "q": numbers.parameter_numbers.q, + } + elif isinstance(self._keyObject, dsa.DSAPrivateKey): + numbers = self._keyObject.private_numbers() + return { + "x": numbers.x, + "y": numbers.public_numbers.y, + "g": numbers.public_numbers.parameter_numbers.g, + "p": numbers.public_numbers.parameter_numbers.p, + "q": numbers.public_numbers.parameter_numbers.q, + } + elif isinstance(self._keyObject, ec.EllipticCurvePublicKey): + numbers = self._keyObject.public_numbers() + return { + "x": numbers.x, + "y": numbers.y, + "curve": self.sshType(), + } + elif isinstance(self._keyObject, ec.EllipticCurvePrivateKey): + numbers = self._keyObject.private_numbers() + return { + "x": numbers.public_numbers.x, + "y": numbers.public_numbers.y, + "privateValue": numbers.private_value, + "curve": self.sshType(), + } + elif isinstance(self._keyObject, ed25519.Ed25519PublicKey): + return { + "a": self._keyObject.public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw + ), + } + elif isinstance(self._keyObject, ed25519.Ed25519PrivateKey): + return { + "a": self._keyObject.public_key().public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw + ), + "k": self._keyObject.private_bytes( + serialization.Encoding.Raw, + serialization.PrivateFormat.Raw, + serialization.NoEncryption() + ), + } + + else: + raise RuntimeError("Unexpected key type: %s" % (self._keyObject,)) + + def blob(self): + """ + Return the public key blob for this key. The blob is the + over-the-wire format for public keys. + + SECSH-TRANS RFC 4253 Section 6.6. + + RSA keys:: + string 'ssh-rsa' + integer e + integer n + + DSA keys:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + + EC keys:: + string 'ecdsa-sha2-[identifier]' + integer x + integer y + + identifier is the standard NIST curve name + + Ed25519 keys:: + string 'ssh-ed25519' + string a + + @rtype: L{bytes} + """ + type = self.type() + data = self.data() + if type == 'RSA': + return (common.NS(b'ssh-rsa') + common.MP(data['e']) + + common.MP(data['n'])) + elif type == 'DSA': + return (common.NS(b'ssh-dss') + common.MP(data['p']) + + common.MP(data['q']) + common.MP(data['g']) + + common.MP(data['y'])) + elif type == 'EC': + byteLength = (self._keyObject.curve.key_size + 7) // 8 + return ( + common.NS(data['curve']) + common.NS(data["curve"][-8:]) + + common.NS( + b'\x04' + utils.int_to_bytes(data['x'], byteLength) + + utils.int_to_bytes(data['y'], byteLength))) + elif type == 'Ed25519': + return common.NS(b'ssh-ed25519') + common.NS(data['a']) + else: + raise BadKeyError('unknown key type: "%s"' % (force_unicode(type,))) + + + def privateBlob(self): + """ + Return the private key blob for this key. The blob is the + over-the-wire format for private keys: + + Specification in OpenSSH PROTOCOL.agent + + RSA keys:: + string 'ssh-rsa' + integer n + integer e + integer d + integer u + integer p + integer q + + DSA keys:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + integer x + + EC keys:: + string 'ecdsa-sha2-[identifier]' + integer x + integer y + integer privateValue + + identifier is the NIST standard curve name. + + Ed25519 keys: + string 'ssh-ed25519' + string a + string k || a + """ + type = self.type() + data = self.data() + if type == 'RSA': + iqmp = rsa.rsa_crt_iqmp(data['p'], data['q']) + return (common.NS(b'ssh-rsa') + common.MP(data['n']) + + common.MP(data['e']) + common.MP(data['d']) + + common.MP(iqmp) + common.MP(data['p']) + + common.MP(data['q'])) + elif type == 'DSA': + return (common.NS(b'ssh-dss') + common.MP(data['p']) + + common.MP(data['q']) + common.MP(data['g']) + + common.MP(data['y']) + common.MP(data['x'])) + elif type == 'EC': + encPub = self._keyObject.public_key().public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint + ) + return (common.NS(data['curve']) + common.NS(data['curve'][-8:]) + + common.NS(encPub) + common.MP(data['privateValue'])) + elif type == 'Ed25519': + return (common.NS(b'ssh-ed25519') + common.NS(data['a']) + + common.NS(data['k'] + data['a'])) + else: + raise BadKeyError('unknown key type: "%s"' % (force_unicode(type,))) + + def toString(self, type, extra=None, comment=None, + passphrase=None): + """ + Create a string representation of this key. If the key is a private + key and you want the representation of its public key, use + C{key.public().toString()}. type maps to a _toString_* method. + + @param type: The type of string to emit. Currently supported values + are C{'OPENSSH'}, C{'LSH'}, and C{'AGENTV3'}. + @type type: L{str} + + @param extra: Any extra data supported by the selected format which + is not part of the key itself. For public OpenSSH keys, this is + a comment. For private OpenSSH keys, this is a passphrase to + encrypt with. (Deprecated since Twisted 20.3.0; use C{comment} + or C{passphrase} as appropriate instead.) + @type extra: L{bytes} or L{unicode} or L{None} + + @param comment: A comment to include with the key. Only supported + for OpenSSH keys. + + Present since Twisted 20.3.0. + + @type comment: L{bytes} or L{unicode} or L{None} + + @param passphrase: A passphrase to encrypt the key with. Only + supported for private OpenSSH keys. + + Present since Twisted 20.3.0. + + @type passphrase: L{bytes} or L{unicode} or L{None} + + @rtype: L{bytes} + """ + if extra is not None: + if self.isPublic(): + comment = extra + else: + passphrase = extra + if isinstance(comment, six.text_type): + comment = comment.encode("utf-8") + if isinstance(passphrase, six.text_type): + passphrase = passphrase.encode("utf-8") + method = getattr(self, '_toString_%s' % (type.upper(),), None) + if method is None: + raise BadKeyError( + 'unknown key type: "%s"' % (force_unicode(type[:30]),)) + + return method(comment=comment, passphrase=passphrase) + + def _toPublicOpenSSH(self, comment=None): + """ + Return a public OpenSSH key string. + + See _fromString_PUBLIC_OPENSSH for the string format. + + @type comment: L{bytes} or L{None} + @param comment: A comment to include with the key, or L{None} to + omit the comment. + """ + if self.type() == 'EC': + if not comment: + comment = b'' + return (self._keyObject.public_bytes( + serialization.Encoding.OpenSSH, + serialization.PublicFormat.OpenSSH + ) + b' ' + comment).strip() + + b64Data = encodebytes(self.blob()).replace(b'\n', b'') + if not comment: + comment = b'' + return (self.sshType() + b' ' + b64Data + b' ' + comment).strip() + + def _toString_OPENSSH_V1(self, comment=None, passphrase=None): + """ + Return a private OpenSSH key string, in the "openssh-key-v1" format + introduced in OpenSSH 6.5. + + See _fromPrivateOpenSSH_v1 for the string format. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase to encrypt the key with, or L{None} + if it is not encrypted. + """ + if self.isPublic(): + return self._toPublicOpenSSH(comment=comment) + + if passphrase: + # For now we just hardcode the cipher to the one used by + # OpenSSH. We could make this configurable later if it's + # needed. + cipher = algorithms.AES + cipherName = b'aes256-ctr' + kdfName = b'bcrypt' + blockSize = cipher.block_size // 8 + keySize = 32 + ivSize = blockSize + salt = self.secureRandom(ivSize) + rounds = 100 + kdfOptions = common.NS(salt) + struct.pack('!L', rounds) + else: + cipherName = b'none' + kdfName = b'none' + blockSize = 8 + kdfOptions = b'' + check = self.secureRandom(4) + privKeyList = ( + check + check + self.privateBlob() + common.NS(comment or b'')) + padByte = 0 + while len(privKeyList) % blockSize: + padByte += 1 + privKeyList += chr(padByte & 0xFF) + if passphrase: + encKey = bcrypt.kdf(passphrase, salt, keySize + ivSize, 100) + encryptor = Cipher( + cipher(encKey[:keySize]), + modes.CTR(encKey[keySize:keySize + ivSize]), + backend=default_backend() + ).encryptor() + encPrivKeyList = ( + encryptor.update(privKeyList) + encryptor.finalize()) + else: + encPrivKeyList = privKeyList + blob = ( + b'openssh-key-v1\0' + + common.NS(cipherName) + + common.NS(kdfName) + common.NS(kdfOptions) + + struct.pack('!L', 1) + + common.NS(self.blob()) + + common.NS(encPrivKeyList)) + b64Data = encodebytes(blob).replace(b'\n', b'') + lines = ( + [b'-----BEGIN OPENSSH PRIVATE KEY-----'] + + [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)] + + [b'-----END OPENSSH PRIVATE KEY-----']) + return b'\n'.join(lines) + b'\n' + + def _toString_OPENSSH(self, comment=None, passphrase=None): + """ + Return a private OpenSSH key string, in the old PEM-based format. + + See _fromPrivateOpenSSH_PEM for the string format. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase to encrypt the key with, or L{None} + if it is not encrypted. + """ + if self.isPublic(): + return self._toPublicOpenSSH(comment=comment) + + if self.type() == 'EC': + # EC keys has complex ASN.1 structure hence we do this this way. + if not passphrase: + # unencrypted private key + encryptor = serialization.NoEncryption() + else: + encryptor = serialization.BestAvailableEncryption(passphrase) + + return self._keyObject.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + encryptor) + elif self.type() == 'Ed25519': + raise BadKeyError( + 'Cannot serialize Ed25519 key to openssh format; ' + 'use openssh_v1 instead.' + ) + + data = self.data() + lines = [b''.join((b'-----BEGIN ', self.type().encode('ascii'), + b' PRIVATE KEY-----'))] + if self.type() == 'RSA': + p, q = data['p'], data['q'] + iqmp = rsa.rsa_crt_iqmp(p, q) + objData = (0, data['n'], data['e'], data['d'], p, q, + data['d'] % (p - 1), data['d'] % (q - 1), + iqmp) + else: + objData = (0, data['p'], data['q'], data['g'], data['y'], + data['x']) + asn1Sequence = univ.Sequence() + for index, value in zip(itertools.count(), objData): + asn1Sequence.setComponentByPosition(index, univ.Integer(value)) + asn1Data = berEncoder.encode(asn1Sequence) + if passphrase: + iv = self.secureRandom(8) + hexiv = ''.join(['%02X' % (ord(x),) for x in iterbytes(iv)]) + hexiv = hexiv.encode('ascii') + lines.append(b'Proc-Type: 4,ENCRYPTED') + lines.append(b'DEK-Info: DES-EDE3-CBC,' + hexiv + b'\n') + ba = md5(passphrase + iv).digest() + bb = md5(ba + passphrase + iv).digest() + encKey = (ba + bb)[:24] + padLen = 8 - (len(asn1Data) % 8) + asn1Data += chr(padLen) * padLen + + encryptor = Cipher( + algorithms.TripleDES(encKey), + modes.CBC(iv), + backend=default_backend() + ).encryptor() + + asn1Data = encryptor.update(asn1Data) + encryptor.finalize() + + b64Data = encodebytes(asn1Data).replace(b'\n', b'') + lines += [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)] + lines.append(b''.join((b'-----END ', self.type().encode('ascii'), + b' PRIVATE KEY-----'))) + return b'\n'.join(lines) + + def _toString_LSH(self, **kwargs): + """ + Return a public or private LSH key. See _fromString_PUBLIC_LSH and + _fromString_PRIVATE_LSH for the key formats. + + @rtype: L{bytes} + """ + data = self.data() + type = self.type() + if self.isPublic(): + if type == 'RSA': + keyData = sexpy.pack([[b'public-key', + [b'rsa-pkcs1-sha1', + [b'n', common.MP(data['n'])[4:]], + [b'e', common.MP(data['e'])[4:]]]]]) + elif type == 'DSA': + keyData = sexpy.pack([[b'public-key', + [b'dsa', + [b'p', common.MP(data['p'])[4:]], + [b'q', common.MP(data['q'])[4:]], + [b'g', common.MP(data['g'])[4:]], + [b'y', common.MP(data['y'])[4:]]]]]) + else: + raise BadKeyError( + 'unknown key type "%s"' % (force_unicode(type,))) + return (b'{' + encodebytes(keyData).replace(b'\n', b'') + + b'}') + else: + if type == 'RSA': + p, q = data['p'], data['q'] + iqmp = rsa.rsa_crt_iqmp(p, q) + return sexpy.pack([[b'private-key', + [b'rsa-pkcs1', + [b'n', common.MP(data['n'])[4:]], + [b'e', common.MP(data['e'])[4:]], + [b'd', common.MP(data['d'])[4:]], + [b'p', common.MP(q)[4:]], + [b'q', common.MP(p)[4:]], + [b'a', common.MP( + data['d'] % (q - 1))[4:]], + [b'b', common.MP( + data['d'] % (p - 1))[4:]], + [b'c', common.MP(iqmp)[4:]]]]]) + elif type == 'DSA': + return sexpy.pack([[b'private-key', + [b'dsa', + [b'p', common.MP(data['p'])[4:]], + [b'q', common.MP(data['q'])[4:]], + [b'g', common.MP(data['g'])[4:]], + [b'y', common.MP(data['y'])[4:]], + [b'x', common.MP(data['x'])[4:]]]]]) + else: + raise BadKeyError( + 'unknown key type "%s"' % (force_unicode(type,))) + + def _toString_AGENTV3(self, **kwargs): + """ + Return a private Secure Shell Agent v3 key. See + _fromString_AGENTV3 for the key format. + + @rtype: L{bytes} + """ + data = self.data() + if not self.isPublic(): + if self.type() == 'RSA': + values = (data['e'], data['d'], data['n'], data['u'], + data['p'], data['q']) + elif self.type() == 'DSA': + values = (data['p'], data['q'], data['g'], data['y'], + data['x']) + return common.NS(self.sshType()) + b''.join(map(common.MP, values)) + + def sign(self, data, signatureType=None): + """ + Sign some data with this private key. + + SECSH-TRANS RFC 4253 Section 6.6. + + @type data: L{bytes} + @param data: The data to sign. + + @rtype: L{bytes} + @return: A signature for the given data. + """ + if self.isPublic(): + raise KeyCertException('A private key is require to sign data.') + + keyType = self.type() + + if signatureType is None: + # Use the SSH public key type name by default, since for all + # current key types this can also be used as a public key + # algorithm name. (This exists for compatibility; new code + # should explicitly specify a public key algorithm name.) + signatureType = self.sshType() + + hashAlgorithm = self._getHashAlgorithm(signatureType) + if hashAlgorithm is None: + raise BadSignatureAlgorithmError( + "public key signature algorithm {} is not " + "defined for {} keys".format(signatureType, keyType) + ) + + if keyType == 'RSA': + sig = self._keyObject.sign(data, padding.PKCS1v15(), hashAlgorithm) + ret = common.NS(sig) + + elif keyType == 'DSA': + sig = self._keyObject.sign(data, hashAlgorithm) + (r, s) = decode_dss_signature(sig) + # SSH insists that the DSS signature blob be two 160-bit integers + # concatenated together. The sig[0], [1] numbers from obj.sign + # are just numbers, and could be any length from 0 to 160 bits. + # Make sure they are padded out to 160 bits (20 bytes each) + ret = common.NS(int_to_bytes(r, 20) + int_to_bytes(s, 20)) + + elif keyType == 'EC': # Pragma: no branch + signature = self._keyObject.sign(data, ec.ECDSA(hashAlgorithm)) + (r, s) = decode_dss_signature(signature) + + rb = int_to_bytes(r) + sb = int_to_bytes(s) + + # Int_to_bytes returns rb[0] as a str in python2 + # and an as int in python3 + if type(rb[0]) is str: + rcomp = ord(rb[0]) + else: + rcomp = rb[0] + + # If the MSB is set, prepend a null byte for correct formatting. + if rcomp & 0x80: + rb = b"\x00" + rb + + if type(sb[0]) is str: + scomp = ord(sb[0]) + else: + scomp = sb[0] + + if scomp & 0x80: + sb = b"\x00" + sb + + ret = common.NS(common.NS(rb) + common.NS(sb)) + + elif keyType == 'Ed25519': + ret = common.NS(self._keyObject.sign(data)) + + return common.NS(signatureType) + ret + + def verify(self, signature, data): + """ + Verify a signature using this key. + + @type signature: L{bytes} + @param signature: The signature to verify. + + @type data: L{bytes} + @param data: The signed data. + + @rtype: L{bool} + @return: C{True} if the signature is valid. + """ + if len(signature) == 40: + # DSA key with no padding + signatureType, signature = b'ssh-dss', common.NS(signature) + else: + signatureType, signature = common.getNS(signature) + + hashAlgorithm = self._getHashAlgorithm(signatureType) + if hashAlgorithm is None: + return False + + keyType = self.type() + if keyType == 'RSA': + k = self._keyObject + if not self.isPublic(): + k = k.public_key() + args = ( + common.getNS(signature)[0], + data, + padding.PKCS1v15(), + hashAlgorithm, + ) + elif keyType == 'DSA': + concatenatedSignature = common.getNS(signature)[0] + r = int_from_bytes(concatenatedSignature[:20], 'big') + s = int_from_bytes(concatenatedSignature[20:], 'big') + signature = encode_dss_signature(r, s) + k = self._keyObject + if not self.isPublic(): + k = k.public_key() + args = (signature, data, hashAlgorithm) + + elif keyType == 'EC': # Pragma: no branch + concatenatedSignature = common.getNS(signature)[0] + rstr, sstr, rest = common.getNS(concatenatedSignature, 2) + r = int_from_bytes(rstr, 'big') + s = int_from_bytes(sstr, 'big') + signature = encode_dss_signature(r, s) + + k = self._keyObject + if not self.isPublic(): + k = k.public_key() + + args = (signature, data, ec.ECDSA(hashAlgorithm)) + + elif keyType == 'Ed25519': + k = self._keyObject + if not self.isPublic(): + k = k.public_key() + args = (common.getNS(signature)[0], data) + + try: + k.verify(*args) + except InvalidSignature: + return False + else: + return True + + @staticmethod + def secureRandom(n): # pragma: no cover + return urandom(n) + + @classmethod + def generate(cls, key_type=DEFAULT_KEY_TYPE, key_size=None): + """ + Return a new private key. + + When `key_size` is None, the default value is used. + + `key_size` is ignored for ed25519. + """ + if not key_type: + key_type = 'not-specified' + key_type = key_type.lower() + + if not key_size: + if key_type == 'ecdsa': + key_size = 384 + else: + key_size = DEFAULT_KEY_SIZE + + key = None + try: + if key_type == u'rsa': + key = rsa.generate_private_key( + public_exponent=65537, + key_size=key_size, + ) + elif key_type == u'dsa': + key = dsa.generate_private_key(key_size=key_size) + elif key_type == 'ecdsa': + try: + curve = _ecSizeTable[key_size] + except KeyError: + raise KeyCertException( + 'Wrong key size "%s". Supported: %s.' % ( + key_size, + ', '.join([str(s) for s in _ecSizeTable.keys()]))) + key = ec.generate_private_key(curve) + elif key_type == 'ed25519': + key = ed25519.Ed25519PrivateKey.generate() + else: + raise KeyCertException('Unknown key type "%s".' % (key_type)) + + except ValueError as error: + raise KeyCertException( + u'Wrong key size "%d". %s' % (key_size, error)) + + return cls(key) + + + @classmethod + def getKeyFormat(cls, data): + """ + Return a type of key. + """ + key_type = cls._guessStringType(data) + human_readable = { + 'public_openssh': 'OpenSSH Public', + 'private_openssh': 'OpenSSH Private old format', + 'private_openssh_v1': 'OpenSSH Private new format', + 'public_sshcom': 'SSH.com Public', + 'private_sshcom': 'SSH.com Private', + 'private_putty': 'PuTTY Private', + 'public_lsh': 'LSH Public', + 'private_lsh': 'LSH Private', + 'public_x509_certificate': 'X509 Certificate', + 'public_x509': 'X509 Public', + 'public_pkcs1_rsa': 'PKCS#1 RSA Public', + 'private_pkcs8': 'PKCS#8 Private', + 'private_encrypted_pkcs8': 'PKCS#8 Encrypted Private', + } + + return human_readable.get(key_type, 'Unknown format') + + @staticmethod + def _getSSHCOMKeyContent(data): + """ + Return the raw content of the SSH.com key (private or public) without + armor and headers. + """ + lines = data.strip().splitlines() + # Split in lines, ignoring the first and last armors. + lines = lines[1:-1] + + # Filter headers, first line without ':' and which is not a + # continuation is the first line of the headers + continuation = False + while True: + if not lines: + # End of content. + break + + line = lines.pop(0) + if continuation: + # We have a continued line. + # ignore it and check if this line still continues. + if not line.endswith('\\'): + continuation = False + continue + + if ':' in line: + # We have a header line + # Ignore it and check if this is a long header. + if line.endswith('\\'): + continuation = True + continue + # This is not a header and not a continuation, so it must be the + # first line form content. + # Put it back and stop filtering the content. + lines.insert(0, line) + break + + content = ''.join(lines) + return base64.decodestring(content) + + @classmethod + def _fromString_PUBLIC_SSHCOM(cls, data): + """ + Return a public key object corresponding to this SSH.com public key + string. The format of a SSH.com public key string is:: + ---- BEGIN SSH2 PUBLIC KEY ---- + Subject: KEY_SUBJECT_UTF8 + Comment: KEY_COMMENT_UTF8 \ + KEY_COMMEMENT_CONTINUATION + x-private-headder: VALUE_UTF8 + + ---- END SSH2 PUBLIC KEY ---- + + * SSH.com content is wrapped at 70. putty-gen wraps it at 64. + * Header-tag MUST NOT be more than 64 8-bit bytes and is + case-insensitive. + * The Header-value MUST NOT be more than 1024 8-bit bytes. + * Each line in the header MUST NOT be more than 72 8-bit bytes. + * A line is continued if the last character in the line is a '\'. + * The Header-tag MUST be encoded in US-ASCII. + * The Header-value MUST be encoded in UTF-8 + + Compliant implementations MUST ignore headers with unrecognized + header-tags. Implementations SHOULD preserve such unrecognized + headers when manipulating the key file. + + @type data: C{bytes} + @return: A {Crypto.PublicKey.pubkey.pubkey} object + @raises BadKeyError: if the blob type is unknown. + """ + if not data.strip().endswith('---- END SSH2 PUBLIC KEY ----'): + raise BadKeyError("Fail to find END tag for SSH.com key.") + + blob = cls._getSSHCOMKeyContent(data) + return cls._fromString_BLOB(blob) + + @classmethod + def _fromString_PRIVATE_SSHCOM(cls, data, passphrase): + """ + Return a private key object corresponding to this SSH.com private key + string. + + See: L{_fromString_PUBLIC_SSH2} for information about key format. + + Key content is in PKCS#8 RFC 5208 Base64 encoded, + wrapped at maximum 72. + + SSH.com and putty-gen wraps the key at 70. + + Blob format as documented in Putty/import.c: + * uint32 magic number + * uint32 total blob size + * string key-type + * string cipher-type (tells you if key is encrypted) + * string encrypted-blob + + Key types: + * RSA if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}} + * DSA dl-modp{sign{dsa-nist-sha1},dh{plain}} + + Encryption key: + * first 16 bytes are MD5(passphrase) + * next 16 bytes are MD5(passphrase || first 16 bytes) + * concatenate at 24 + + The payload for an RSA key: + * mpint e + * mpint d + * mpint n + * mpint u + * mpint p + * mpint q + + The payload for a DSA key: + * uint32 0 + * mpint p + * mpint g + * mpint q + * mpint y + * mpint x + + @type data: C{bytes} + @return: A {Crypto.PublicKey.pubkey.pubkey} object + @raises BadKeyError: if + * the blob type is unknown. + * a passphrase is provided for an unencrypted key + """ + blob = cls._getSSHCOMKeyContent(data) + magic_number = struct.unpack('>I', blob[:4])[0] + if magic_number != SSHCOM_MAGIC_NUMBER: + raise BadKeyError( + 'Bad magic number for SSH.com key "%s"' % ( + force_unicode(magic_number),)) + struct.unpack('>I', blob[4:8])[0] # Ignore value for total size. + type_signature, rest = common.getNS(blob[8:]) + + key_type = None + if type_signature.startswith('if-modn{sign{rsa'): + key_type = 'rsa' + elif type_signature.startswith('dl-modp{sign{dsa'): + key_type = 'dsa' + else: + raise BadKeyError( + 'Unknown SSH.com key type "%s"' % force_unicode(type_signature)) + + cipher_type, rest = common.getNS(rest) + encrypted_blob, _ = common.getNS(rest) + + encryption_key = None + if cipher_type.lower() == b'none': + if passphrase: + raise BadKeyError('SSH.com key not encrypted') + key_data = encrypted_blob + elif cipher_type.lower() == b'3des-cbc': + if not passphrase: + raise EncryptedKeyError( + 'Passphrase must be provided for an encrypted key.') + encryption_key = cls._getDES3EncryptionKey(passphrase) + decryptor = Cipher( + algorithms.TripleDES(encryption_key), + modes.CBC(b'\x00' * 8), + backend=default_backend() + ).decryptor() + key_data = decryptor.update(encrypted_blob) + decryptor.finalize() + else: + raise BadKeyError( + 'Encryption method not supported: "%s"' % ( + force_unicode(cipher_type[:30]))) + + try: + payload, _ = common.getNS(key_data) + if key_type == 'rsa': + e, d, n, u, p, q, rest = cls._unpackMPSSHCOM(payload, 6) + return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) + + if key_type == 'dsa': + # First 32bit is an uint with value 0. We just ignore it. + p, g, q, y, x, rest = cls._unpackMPSSHCOM(payload[4:], 5) + return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) + except struct.error: + if encryption_key: + raise EncryptedKeyError('Bad password or bad key format.') + else: + BadKeyError('Failed to parse payload.') + + @staticmethod + def _getDES3EncryptionKey(passphrase): + """ + Return the encryption key used in DES3 cypher. + """ + DES3_KEY_SIZE = 24 + pass_1 = md5(passphrase).digest() + pass_2 = md5(passphrase + pass_1).digest() + return (pass_1 + pass_2)[:DES3_KEY_SIZE] + + @staticmethod + def _unpackMPSSHCOM(data, count=1): + """ + Get SSHCOM mpint. + + 32-bit bit count N, followed by (N+7)/8 bytes of data. + + Similar to Twisted getMP method. + """ + c = 0 + mp = [] + for i in range(count): + length = struct.unpack('>I', data[c:c + 4])[0] + length = (length + 7) // 8 + mp.append(int_from_bytes(data[c + 4:c + 4 + length], 'big')) + c += length + 4 + return tuple(mp) + (data[c:],) + + @staticmethod + def _packMPSSHCOM(number): + """ + Return the wire representation of a MP number for SSH.com. + + Similar to Twisted MP method. + """ + if number == 0: + return '\000' * 4 + + wire_number = int_to_bytes(number) + + wire_length = (len(wire_number) * 8) - 7 + return struct.pack('>L', wire_length) + wire_number + + def _toString_SSHCOM(self, comment=None, passphrase=None): + """ + Return a public or private SSH.com string. + + See _fromString_PUBLIC_SSHCOM and _fromString_PRIVATE_SSHCOM for the + string formats. If extra is present, it represents a comment for a + public key, or a passphrase for a private key. + + @param extra: Comment for a public key or passphrase for a private key. + @type extra: C{bytes} + + @rtype: C{bytes} + """ + if self.isPublic(): + return self._toString_SSHCOM_public(comment) + else: + return self._toString_SSHCOM_private(passphrase) + + def _toString_SSHCOM_public(self, extra): + """ + Return the public SSH.com string. + """ + lines = ['---- BEGIN SSH2 PUBLIC KEY ----'] + if extra: + line = 'Comment: "%s"' % (extra.encode('utf-8'),) + lines.append('\\\n'.join(textwrap.wrap(line, 70))) + + base64Data = base64.b64encode(self.blob()) + lines.extend(textwrap.wrap(base64Data, 70)) + lines.append('---- END SSH2 PUBLIC KEY ----') + return '\n'.join(lines) + + def _toString_SSHCOM_private(self, extra): + """ + Return the private SSH.com string. + """ + # Now we are left with a private key. + # Both encrypted and unencrypted keys have the same armor. + lines = ['---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----'] + + type_signature = None + payload_blob = None + data = self.data() + type = self.type() + if type == 'RSA': + type_signature = ( + 'if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}}') + payload_blob = ( + self._packMPSSHCOM(data['e']) + + self._packMPSSHCOM(data['d']) + + self._packMPSSHCOM(data['n']) + + self._packMPSSHCOM(data['u']) + + self._packMPSSHCOM(data['p']) + + self._packMPSSHCOM(data['q']) + ) + elif type == 'DSA': + type_signature = 'dl-modp{sign{dsa-nist-sha1},dh{plain}}' + payload_blob = ( + struct.pack('>I', 0) + + self._packMPSSHCOM(data['p']) + + self._packMPSSHCOM(data['g']) + + self._packMPSSHCOM(data['q']) + + self._packMPSSHCOM(data['y']) + + self._packMPSSHCOM(data['x']) + ) + else: # pragma: no cover + raise BadKeyError('Unsupported key type "%s"' % force_unicode(type)) + + payload_blob = common.NS(payload_blob) + + if extra: + # We got a password, so encrypt it. + cipher_type = '3des-cbc' + padding = b'\x00' * (8 - (len(payload_blob) % 8)) + payload_blob = payload_blob + padding + encryption_key = self._getDES3EncryptionKey(extra) + + encryptor = Cipher( + algorithms.TripleDES(encryption_key), + modes.CBC(b'\x00' * 8), + backend=default_backend() + ).encryptor() + encrypted_blob = ( + encryptor.update(payload_blob) + encryptor.finalize()) + else: + cipher_type = 'none' + encrypted_blob = payload_blob + + # We first create the content without magic number and + # total size, then compute the total size, and update the + # final content. + blob = ( + common.NS(type_signature) + + common.NS(cipher_type) + + common.NS(encrypted_blob) + ) + total_size = 8 + len(blob) + blob = ( + struct.pack('>I', SSHCOM_MAGIC_NUMBER) + + struct.pack('>I', total_size) + + blob + ) + + # In the end, encode in base 64 and wrap it. + blob = base64.b64encode(blob) + lines.extend(textwrap.wrap(blob, 70)) + + lines.append('---- END SSH2 ENCRYPTED PRIVATE KEY ----') + return '\n'.join(lines).encode('ascii') + + @classmethod + def _fromString_PRIVATE_PUTTY(cls, data, passphrase): + """ + Read a private Putty key. + + Format is: + + PuTTY-User-Key-File-2: ssh-rsa + Encryption: aes256-cbc | none + Comment: SINGLE_LINE_COMMENT + Public-Lines: PUBLIC_LINES + < base64 public part always in plain > + Private-Lines: 8 + < base64 private part > + Private-MAC: 1398fbfc7ce307d9ee0e42851f183f88c728398f + + Pulic part RSA: + * string type (ssh-rsa) + * mpint e + * mpint n + Private part RSA: + * mpint d + * mpint q + * mpint p + * mpint u + + Pulic part DSA: + * string type (ssh-dss) + * mpint p + * mpint q + * mpint g + * mpint v` + Private part DSA: + * mpint x + + Public part ECDSA-SHA2-*: + * string 'ecdsa-sha2-[identifier]' + * string identifier + * mpint x + * mpint y + Private part ECDSA-SHA2-*: + * string q + * mpint privateValue + + Public part Ed25519: + * string type (ssh-ed25519) + * string a + Private part Ed25519: + * string k + + Private part is padded for encryption. + + Encryption key is composed of concatenating, up to block size: + * uint32 sequence, starting from 0 + * passphrase + + Lines are terminated by CRLF, although CR-only and LF-only are + tolerated on input. + + Only version 2 is supported. + Version 2 was introduced in PuTTY 0.52. + Version 1 was an in-development format used in 0.52 snapshot + """ + lines = data.strip().splitlines() + + key_type = lines[0][22:].strip().lower() + if key_type not in [ + b'ssh-rsa', + b'ssh-dss', + b'ssh-ed25519', + ] and key_type not in _curveTable: + raise BadKeyError( + 'Unsupported key type: "%s"' % force_unicode(key_type[:30])) + + encryption_type = lines[1][11:].strip().lower() + + if encryption_type == b'none': + if passphrase: + raise BadKeyError('PuTTY key not encrypted') + elif encryption_type != b'aes256-cbc': + raise BadKeyError( + 'Unsupported encryption type: "%s"' % force_unicode( + encryption_type[:30])) + + comment = lines[2][9:].strip() + + public_count = int(lines[3][14:].strip()) + base64_content = ''.join(lines[ + 4: + 4 + public_count + ]) + public_blob = base64.decodestring(base64_content) + public_type, public_payload = common.getNS(public_blob) + + if public_type.lower() != key_type: + raise BadKeyError( + 'Mismatch key type. Header has "%s", public has "%s"' % ( + force_unicode(key_type[:30]), + force_unicode(public_type[:30]))) + + # We skip 4 lines so far and the total public lines. + private_start_line = 4 + public_count + private_count = int(lines[private_start_line][15:].strip()) + base64_content = ''.join(lines[ + private_start_line + 1: + private_start_line + 1 + private_count + ]) + private_blob = base64.decodestring(base64_content) + + private_mac = lines[-1][12:].strip() + + hmac_key = PUTTY_HMAC_KEY + encryption_key = None + if encryption_type == b'aes256-cbc': + if not passphrase: + raise EncryptedKeyError( + 'Passphrase must be provided for an encrypted key.') + hmac_key += passphrase + encryption_key = cls._getPuttyAES256EncryptionKey(passphrase) + decryptor = Cipher( + algorithms.AES(encryption_key), + modes.CBC(b'\x00' * 16), + backend=default_backend() + ).decryptor() + private_blob = ( + decryptor.update(private_blob) + decryptor.finalize()) + + # I have no idea why these values are packed form HMAC as net strings. + hmac_data = ( + common.NS(key_type) + + common.NS(encryption_type) + + common.NS(comment) + + common.NS(public_blob) + + common.NS(private_blob) + ) + hmac_key = sha1(hmac_key).digest() + computed_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest() + if private_mac != computed_mac: + if encryption_key: + raise EncryptedKeyError('Bad password or HMAC mismatch.') + else: + raise BadKeyError( + 'HMAC mismatch: file declare "%s", actual is "%s"' % ( + force_unicode(private_mac), + force_unicode(computed_mac))) + + if key_type == b'ssh-rsa': + e, n, _ = common.getMP(public_payload, count=2) + d, q, p, u, _ = common.getMP(private_blob, count=4) + return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) + + if key_type == b'ssh-dss': + p, q, g, y, _ = common.getMP(public_payload, count=4) + x, _ = common.getMP(private_blob) + return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) + + if key_type == b'ssh-ed25519': + a, _ = common.getNS(public_payload) + k, _ = common.getNS(private_blob) + return cls._fromEd25519Components(a=a, k=k) + + if key_type in _curveTable: + curve = _curveTable[key_type] + curveName, q, _ = common.getNS(public_payload, 2) + if curveName != _secToNist[curve.name.encode('ascii')]: + raise BadKeyError( + 'ECDSA curve name "%s" does not match key type "%s"' % ( + force_unicode(curveName), + force_unicode(key_type))) + + privateValue, _ = common.getMP(private_blob) + return cls._fromECEncodedPoint( + encodedPoint=q, curve=key_type, privateValue=privateValue) + + @staticmethod + def _getPuttyAES256EncryptionKey(passphrase): + """ + Return the encryption key used in Putty AES 256 cipher. + """ + key_size = 32 + part_1 = sha1(b'\x00\x00\x00\x00' + passphrase).digest() + part_2 = sha1(b'\x00\x00\x00\x01' + passphrase).digest() + return (part_1 + part_2)[:key_size] + + def _toString_PUTTY(self, comment=None, passphrase=None): + """ + Return a public or private Putty string. + + See _fromString_PRIVATE_PUTTY for the private format. + See _fromString_PUBLIC_SSHCOM for the public format. + + Private key is exported in version 2 format. + + If extra is present, it represents a comment for a + public key, or a passphrase for a private key. + + @param extra: Comment for a public key or passphrase for a private key. + @type extra: C{bytes} + + @rtype: C{bytes} + """ + if self.isPublic(): + # Putty uses SSH.com as public format. + return self._toString_SSHCOM_public(comment) + else: + return self._toString_PUTTY_private(passphrase) + + def _toString_PUTTY_private(self, extra): + """ + Return the Putty private key representation. + + See fromString for Putty file format. + """ + aes_block_size = 16 + lines = [] + key_type = self.sshType() + comment = 'Exported by chevah-keycert.' + data = self.data() + + hmac_key = PUTTY_HMAC_KEY + if extra: + encryption_type = b'aes256-cbc' + hmac_key += extra + else: + encryption_type = 'none' + + if key_type == b'ssh-rsa': + public_blob = ( + common.NS(key_type) + + common.MP(data['e']) + + common.MP(data['n']) + ) + private_blob = ( + common.MP(data['d']) + + common.MP(data['q']) + + common.MP(data['p']) + + common.MP(data['u']) + ) + elif key_type == b'ssh-dss': + public_blob = ( + common.NS(key_type) + + common.MP(data['p']) + + common.MP(data['q']) + + common.MP(data['g']) + + common.MP(data['y']) + ) + private_blob = common.MP(data['x']) + + elif key_type == b'ssh-ed25519': + public_blob = ( + common.NS(key_type) + + common.NS(data['a']) + ) + private_blob = common.NS(data['k']) + + elif key_type in _curveTable: + + curve_name = _secToNist[self._keyObject.curve.name] + public_blob = ( + common.NS(key_type) + + common.NS(curve_name) + + common.NS(self._keyObject.public_key().public_numbers().encode_point()) + ) + private_blob = common.MP(data['privateValue']) + + else: # pragma: no cover + raise BadKeyError('Unsupported key type.') + + private_blob_plain = private_blob + private_blob_encrypted = private_blob + + if extra: + # Encryption is requested. + # Padding is required for encryption. + padding_size = -1 * ( + (len(private_blob) % aes_block_size) - aes_block_size) + private_blob_plain += b'\x00' * padding_size + encryption_key = self._getPuttyAES256EncryptionKey(extra) + encryptor = Cipher( + algorithms.AES(encryption_key), + modes.CBC(b'\x00' * aes_block_size), + backend=default_backend() + ).encryptor() + private_blob_encrypted = ( + encryptor.update(private_blob_plain) + encryptor.finalize()) + + public_lines = textwrap.wrap(base64.b64encode(public_blob), 64) + private_lines = textwrap.wrap( + base64.b64encode(private_blob_encrypted), 64) + + hmac_data = ( + common.NS(key_type) + + common.NS(encryption_type) + + common.NS(comment) + + common.NS(public_blob) + + common.NS(private_blob_plain) + ) + hmac_key = sha1(hmac_key).digest() + private_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest() + + lines.append('PuTTY-User-Key-File-2: %s' % key_type) + lines.append('Encryption: %s' % encryption_type) + lines.append('Comment: %s' % comment) + lines.append('Public-Lines: %s' % len(public_lines)) + lines.extend(public_lines) + lines.append('Private-Lines: %s' % len(private_lines)) + lines.extend(private_lines) + lines.append('Private-MAC: %s' % private_mac) + return '\r\n'.join(lines) + + @classmethod + def _fromString_PUBLIC_X509_CERTIFICATE(cls, data): + """ + Read the public key from X509 Certificates in PEM format. + """ + try: + cert = crypto.load_certificate(crypto.FILETYPE_PEM, data) + except crypto.Error as error: + raise BadKeyError( + 'Failed to load certificate. "%s"' % (force_unicode(error),)) + + return cls._fromOpenSSLPublic(cert.get_pubkey(), 'certificate') + + @classmethod + def _fromString_PUBLIC_X509(cls, data): + """ + Read the public key from X509 public key PEM format. + """ + try: + pkey = crypto.load_publickey(crypto.FILETYPE_PEM, data) + except crypto.Error as error: + raise BadKeyError( + 'Failed to load PKCS#1 public key. "%s"' % ( + force_unicode(error),)) + + return cls._fromOpenSSLPublic(pkey, 'X509 public PEM file') + + @classmethod + def _fromOpenSSLPublic(cls, pkey, source_type): + """ + Load the SSH from an OpenSSL Public Key object. + """ + return cls(pkey.to_cryptography_key()) + + @classmethod + def _fromString_PRIVATE_PKCS8(cls, data, passphrase=None): + """ + Read the private key from PKCS8 PEM format. + """ + return cls._load_PRIVATE_PKCS8(data, passphrase='') + + @classmethod + def _fromString_PRIVATE_ENCRYPTED_PKCS8(cls, data, passphrase=None): + """ + Read the encrypted private key from PKCS8 PEM format. + """ + if not passphrase: + raise EncryptedKeyError( + 'Passphrase must be provided for an encrypted key') + + return cls._load_PRIVATE_PKCS8(data, passphrase) + + @classmethod + def _load_PRIVATE_PKCS8(cls, data, passphrase): + """ + Shared code for loading a private PKCS8 key. + """ + try: + key = crypto.load_privatekey( + crypto.FILETYPE_PEM, data, passphrase=passphrase) + except crypto.Error as error: + raise BadKeyError( + 'Failed to load PKCS#8 PEM. "%s"' % (force_unicode(error),)) + + return cls(key.to_cryptography_key()) + + @classmethod + def _fromString_PUBLIC_PKCS1_RSA(cls, data): + """ + Read the public key from PKCS1 PEM format. + + This is also the OpenSSH public PEM. + + RSAPublicKey ::= SEQUENCE { + modulus INTEGER, -- n + publicExponent INTEGER -- e + } + + """ + lines = data.strip().splitlines() + data = base64.decodestring(b''.join(lines[1:-1])) + decodedKey = berDecoder.decode(data)[0] + if len(decodedKey) != 2: + raise BadKeyError('Invalid ASN.1 payload for PKCS1 PEM.') + + n = int(decodedKey[0]) + e = int(decodedKey[1]) + return cls._fromRSAComponents(n=n, e=e) + + +def generate_ssh_key_parser(subparsers, name, default_key_type='rsa'): + """ + Create an argparse sub-command with `name` attached to `subparsers`. + """ + generate_ssh_key = subparsers.add_parser( + name, + help='Create a SSH public and private key pair.', + ) + generate_ssh_key.add_argument( + '--key-file', + metavar='FILE', + help=( + 'Store the keys pair in FILE and FILE.pub. Default id_TYPE.'), + ) + generate_ssh_key.add_argument( + '--key-size', + type=int, metavar="SIZE", default=None, + help='Generate a SSH key of size SIZE', + ) + generate_ssh_key.add_argument( + '--key-type', + metavar="[rsa|dsa|ecdsa|ed25519]", default=default_key_type, + help='Generate a new SSH private and public key. Default %(default)s.', + ) + generate_ssh_key.add_argument( + '--key-comment', + metavar="COMMENT_TEXT", + help=( + 'Generate the public key using this comment. Default no comment.'), + ) + generate_ssh_key.add_argument( + '--key-format', + metavar="[openssh|openssh_v1|putty]", default='openssh_v1', + help='Generate a new SSH private and public key. Default %(default)s.', + ) + generate_ssh_key.add_argument( + '--key-password', + metavar="PLAIN-PASS", default=None, + help='Password used to store the SSH private key.', + ) + generate_ssh_key.add_argument( + '--key-skip', + action='store_true', default=False, + help='Do not create a new key if a key file already exists.', + ) + return generate_ssh_key + + +def generate_ssh_key(options, open_method=None): + """ + Generate a SSH RSA or DSA key and store it on disk. + + `options` is an argparse namespace. See `generate_ssh_key_subparser`. + + Return a tuple of (exit_code, operation_message, key). + + For success, exit_code is 0. + + `open_method` is a helper for dependency injection during tests. + """ + key = None + + if open_method is None: # pragma: no cover + open_method = open + + exit_code = 0 + message = '' + try: + key_size = options.key_size + key_type = options.key_type.lower() + key_format = options.key_format.lower() + + if not hasattr(options, 'key_file') or options.key_file is None: + options.key_file = u'id_%s' % (key_type) + + private_file = options.key_file + + public_file = u'%s%s' % ( + options.key_file, DEFAULT_PUBLIC_KEY_EXTENSION) + + skip = _skip_key_generation(options, private_file, public_file) + if skip: + return (0, u'Key already exists.', key) + + key = Key.generate(key_type=key_type, key_size=key_size) + + with open_method(_path(private_file), 'wb') as file_handler: + _store_SSHKey( + key, + private_file=file_handler, + key_format=key_format, + password=options.key_password, + ) + + key_comment = None + if hasattr(options, 'key_comment') and options.key_comment: + key_comment = options.key_comment + message_comment = u'having comment "%s"' % key_comment + if key_format != 'openssh': + key_comment = None + message_comment = ( + 'without comment as not supported by the output format') + else: + message_comment = u'without a comment' + + with open_method(_path(public_file), 'wb') as file_handler: + _store_SSHKey( + key, + public_file=file_handler, + comment=key_comment, + key_format=key_format, + ) + + message = ( + u'SSH key of type "%s" and length "%d" generated as ' + u'public key file "%s" and private key file "%s" %s.') % ( + key.sshType(), + key.size(), + public_file, + private_file, + message_comment, + ) + + exit_code = 0 + + except KeyCertException as error: + exit_code = 1 + message = error.message + except Exception as error: + exit_code = 1 + message = six.text_type(error) + + return (exit_code, message, key) + + +def _store_SSHKey( + key, + public_file=None, private_file=None, + comment=None, password=None, key_format='openssh_v1', + ): + """ + Store the public and private key into a file like object using + OpenSSH format. + """ + if public_file: + public_serialization = key.public().toString( + type=key_format) + if comment: + public_content = '%s %s' % ( + public_serialization, comment.encode('utf-8')) + else: + public_content = public_serialization + public_file.write(public_content) + + if private_file: + private_file.write(key.toString(type=key_format, passphrase=password)) + + +def _skip_key_generation(options, private_file, public_file): + """ + Return True when key generation can be skipped. + + Key generation can be skipped when private key already exists. Public + key is ignored. + + Raise KeyCertException if file exists. + """ + if os.path.exists(_path(private_file)): + if options.key_skip: + return True + else: + raise KeyCertException( + u'Private key already exists. %s' % private_file) + + if os.path.exists(_path(public_file)): + raise KeyCertException(u'Public key already exists. %s' % public_file) + return False diff --git a/src/chevah_keycert/ssl.py b/src/chevah_keycert/ssl.py new file mode 100644 index 0000000..d5a4c52 --- /dev/null +++ b/src/chevah_keycert/ssl.py @@ -0,0 +1,418 @@ +# Copyright (c) 2015 Adi Roiban. +# See LICENSE for details. +""" +SSL keys and certificates. +""" +from __future__ import unicode_literals +from __future__ import absolute_import +import os +from random import randint + +from OpenSSL import crypto + +from chevah_keycert import _path +from chevah_keycert.exceptions import KeyCertException + +_DEFAULT_SSL_KEY_CYPHER = b'aes-256-cbc' +_SUPPORTED_SIGN_ALGORITHMS = [b'md5', b'sha1', b'sha256', b'sha512'] + +# See https://www.openssl.org/docs/manmaster/man5/x509v3_config.html +_KEY_USAGE_STANDARD = { + 'digital-signature': b'digitalSignature', + 'non-repudiation': b'nonRepudiation', + 'key-encipherment': b'keyEncipherment', + 'data-encipherment': b'dataEncipherment', + 'key-agreement': b'keyAgreement', + 'key-cert-sign': b'keyCertSign', + 'crl-sign': b'cRLSign', + 'encipher-only': b'encipherOnly', + 'decipher-only': b'decipherOnly', + } +_KEY_USAGE_EXTENDED = { + 'server-authentication': b'serverAuth', + 'client-authentication': b'clientAuth', + 'code-signing': b'codeSigning', + 'email-protection': b'emailProtection', + } + + +def _generate_self_csr_parser(sub_command, default_key_size): + """ + Add share configuration options for CSR and self-signed generation. + """ + sub_command.add_argument( + '--common-name', + help='Common name associated with the certificate.', + required=True, + ) + + sub_command.add_argument( + '--key-size', + type=int, metavar="SIZE", default=default_key_size, + help='Size of the generate RSA private key. Default %(default)s', + ) + + sub_command.add_argument( + '--sign-algorithm', + default='sha256', + metavar='STRING', + help='Signature algorithm: sha1, sha256, sha512. Default: sha256.' + ) + + sub_command.add_argument( + '--key-usage', + default='', + help=( + 'Comma-separated key usage. ' + 'The following key usage extensions are supported: %s. ' + 'To mark usage as critical, prefix the values with `critical,`. ' + 'For example: "critical,key-agreement,digital-signature".' + ) % (', '.join( + list(_KEY_USAGE_STANDARD.keys()) + list(_KEY_USAGE_EXTENDED.keys()))), + ) + + sub_command.add_argument( + '--constraints', + default='', + help=( + 'Comma-separated basic constraints. ' + 'To mark constraints as critical, prefix the values with ' + '`critical,`. ' + 'For example: "critical,CA:TRUE,pathlen:0".' + ), + ) + + sub_command.add_argument( + '--email', + help='Email address.', + ) + sub_command.add_argument( + '--alternative-name', + help=( + 'Optional list of alternative names. ' + 'Use "DNS:your.domain.tld" for domain names. ' + 'Use "IP:1.2.3.4" for IP addresses. ' + 'Example: "DNS:top.com,DNS:www.top.com,IP:11.0.21.12".' + ) + ) + sub_command.add_argument( + '--organization', + help='Organization.', + ) + sub_command.add_argument( + '--organization-unit', + help='Organization unit.', + ) + sub_command.add_argument( + '--locality', + help='Full name of the locality.', + ) + sub_command.add_argument( + '--state', + help=( + 'Full name of the state/county/region/province.'), + ) + sub_command.add_argument( + '--country', + help=( + 'Two-letter country code.'), + ) + + +def generate_csr_parser(subparsers, name, default_key_size=2048): + """ + Create an argparse sub-command for generating CSR options with + `name` attached to `subparsers`. + """ + sub_command = subparsers.add_parser( + name, + help=( + 'Create an SSL private key and an associated certificate ' + 'signing request.'), + ) + + sub_command.add_argument( + '--key', + metavar="FILE", + default=None, + help=( + 'Sign the CSR using this private key. ' + 'Private key loaded as PEM PKCS#8 format. ' + ), + ) + sub_command.add_argument( + '--key-file', + metavar="FILE", + default='server.key', + help=( + 'Store the keys/CSR pair in FILE and FILE.csr. ' + 'Private key stored using PEM PKCS#8 format. ' + 'CSR file stored in PEM x509 format. ' + 'Default names: server.key and server.csr.'), + ) + + sub_command.add_argument( + '--key-password', + metavar="PASSPHRASE", + help=( + 'Password used to encrypt the generated key. ' + 'Default no encryption. Encrypted with %s.' % ( + _DEFAULT_SSL_KEY_CYPHER,)), + ) + _generate_self_csr_parser(sub_command, default_key_size) + + return sub_command + + +def generate_self_signed_parser(subparsers, name, default_key_size=2048): + """ + Create an argparse sub-command for generating self signed options with + `name` attached to `subparsers`. + """ + sub_command = subparsers.add_parser( + name, + help=( + 'Create an SSL private key ' + 'and an associated self-signed certificate.'), + ) + _generate_self_csr_parser(sub_command, default_key_size) + return sub_command + + +def generate_csr(options): + """ + Generate a new SSL key and the associated SSL cert signing. + + Returns a tuple of (csr_pem, key_pem) + Raise KeyCertException on failure. + """ + try: + return _generate_csr(options) + except crypto.Error as error: + try: + message = error[0][0][2].decode('utf-8', errors='replace') + except IndexError: # pragma: no cover + message = 'no error details.' + raise KeyCertException(message) + + +def _set_subject_and_extensions(target, options): + """ + Set the subject and option for `target` CRS or certificate. + """ + common_name = options.common_name + constraints = getattr(options, 'constraints', '') + key_usage = getattr(options, 'key_usage', '').lower() + email = getattr(options, 'email', '') + alternative_name = getattr(options, 'alternative_name', '') + country = getattr(options, 'country', '') + state = getattr(options, 'state', '') + locality = getattr(options, 'locality', '') + organization = getattr(options, 'organization', '') + organization_unit = getattr(options, 'organization_unit', '') + + # RFC 2459 defines it as optional, and pyopenssl set it to `0` anyway. + # But we got reports that Windows 2003 and Windows 2008 Servers + # can not parse CSR generated using this tool, so here we are. + target.set_version(2) + + subject = target.get_subject() + + subject.CN = common_name.encode('idna') + + if country: + if len(country) != 2: + raise KeyCertException('Invalid country code.') + + subject.C = country + + if state: + subject.ST = state + + if locality: + subject.L = locality + + if organization: + subject.O = organization + + if organization_unit: + subject.OU = organization_unit + + if email: + try: + address, domain = options.email.split('@', 1) + except ValueError: + raise KeyCertException('Invalid email address.') + + subject.emailAddress = u'%s@%s' % (address, domain.encode('idna')) + + critical_constraints = False + critical_usage = False + standard_usage = [] + extended_usage = [] + extensions = [] + + if constraints.lower().startswith('critical'): + critical_constraints = True + constraints = constraints[8:].strip(',').strip() + + if key_usage.startswith('critical'): + critical_usage = True + key_usage = key_usage[8:] + + for usage in key_usage.split(','): + usage = usage.strip() + if not usage: + continue + if usage in _KEY_USAGE_STANDARD: + standard_usage.append(_KEY_USAGE_STANDARD[usage]) + if usage in _KEY_USAGE_EXTENDED: + extended_usage.append(_KEY_USAGE_EXTENDED[usage]) + + if constraints: + extensions.append(crypto.X509Extension( + b'basicConstraints', + critical_constraints, + constraints.encode('ascii'), + )) + + if standard_usage: + extensions.append(crypto.X509Extension( + b'keyUsage', + critical_usage, + b','.join(standard_usage), + )) + + if extended_usage: + extensions.append(crypto.X509Extension( + b'extendedKeyUsage', + critical_usage, + b','.join(extended_usage), + )) + + # Alternate name is optional. + if alternative_name: + extensions.append(crypto.X509Extension( + b'subjectAltName', + False, + alternative_name.encode('idna'))) + target.add_extensions(extensions) + + +def _sign_cert_or_csr(target, key, options): + """ + Sign the certificate or CSR. + """ + sign_algorithm = getattr( + options, 'sign_algorithm', 'sha256').encode('ascii') + + if sign_algorithm not in _SUPPORTED_SIGN_ALGORITHMS: + raise KeyCertException( + 'Invalid signing algorithm. Supported values: %s.' % ( + ', '.join(_SUPPORTED_SIGN_ALGORITHMS))) + + target.set_pubkey(key) + target.sign(key, sign_algorithm) + + +def _generate_csr(options): + """ + Helper to catch all crypto errors and reduce indentation. + """ + key_size = getattr(options, 'key_size', 2048) + + if key_size < 512: + raise KeyCertException('Key size must be greater or equal to 512.') + + key_type = crypto.TYPE_RSA + + csr = crypto.X509Req() + + _set_subject_and_extensions(csr, options) + + key_pem = None + private_key = options.key + if private_key: + if os.path.exists(_path(private_key)): + with open(_path(private_key), 'rb') as stream: + private_key = stream.read() + + key_pem = private_key + key = crypto.load_privatekey(crypto.FILETYPE_PEM, private_key) + else: + # Generate new Key. + key = crypto.PKey() + key.generate_key(key_type, key_size) + + _sign_cert_or_csr(csr, key, options) + + csr_pem = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr) + + if not key_pem: + if options.key_password: + key_pem = crypto.dump_privatekey( + crypto.FILETYPE_PEM, key, + _DEFAULT_SSL_KEY_CYPHER, options.key_password.encode('utf-8')) + else: + key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) + + return { + 'csr_pem': csr_pem, + 'key_pem': key_pem, + 'csr': csr, + 'key': key, + } + + +def generate_ssl_self_signed_certificate(options): + """ + Generate a self signed SSL certificate. + + Returns a tuple of (certificate_pem, key_pem) + """ + key_size = getattr(options, 'key_size', 2048) + + serial = randint(0, 1000000000000) + + key = crypto.PKey() + key.generate_key(crypto.TYPE_RSA, key_size) + + cert = crypto.X509() + + _set_subject_and_extensions(cert, options) + + cert.set_serial_number(serial) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) + + cert.set_issuer(cert.get_subject()) + + _sign_cert_or_csr(cert, key, options) + + certificate_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) + key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) + return (certificate_pem, key_pem) + + +def generate_and_store_csr(options, encoding='utf-8'): + """ + Generate a key/csr and try to store it on disk. + + Raise KeyCertException when failing to create the key or csr. + """ + name, _ = os.path.splitext(options.key_file) + csr_name = u'%s.csr' % name + + if os.path.exists(_path(options.key_file, encoding)): + raise KeyCertException('Key file already exists.') + + result = generate_csr(options) + + try: + with open(_path(options.key_file, encoding), 'wb') as store_file: + store_file.write(result['key_pem']) + + with open(_path(csr_name, encoding), 'wb') as store_file: + store_file.write(result['csr_pem']) + except Exception as error: + raise KeyCertException(str(error).decode('utf-8', errors='replace')) diff --git a/src/chevah_keycert/tests/__init__.py b/src/chevah_keycert/tests/__init__.py new file mode 100644 index 0000000..7f87b8f --- /dev/null +++ b/src/chevah_keycert/tests/__init__.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import +from chevah_compat.testing import mk + + +def setup_package(): + """ + Called before running all tests. + """ + # Prepare the main testing filesystem. + mk.fs.setUpTemporaryFolder() + + +def teardown_package(): + """ + Called after all tests were run. + """ + # Remove main testing folder. + mk.fs.tearDownTemporaryFolder() + mk.fs.checkCleanTemporaryFolders() diff --git a/src/chevah_keycert/tests/helpers.py b/src/chevah_keycert/tests/helpers.py new file mode 100644 index 0000000..5d2fb38 --- /dev/null +++ b/src/chevah_keycert/tests/helpers.py @@ -0,0 +1,69 @@ +# Copyright (c) 2015 Adi Roiban. +# See LICENSE for details. +""" +Helpers for testing the project. +""" +from __future__ import absolute_import +from argparse import Namespace +from io import StringIO +import sys + + +class CommandLineMixin(object): + """ + Helper to test command line tools. + """ + def parseArguments(self, args): + """ + Parse arguments and return options and captured stdout. + """ + stdout = StringIO() + stderr = StringIO() + prev_stdout = sys.stdout + prev_stderr = sys.stderr + try: + sys.stdout = stdout + sys.stderr = stderr + options = self.parser.parse_args(args) + return options + except SystemExit as error: # pragma: no cover + raise AssertionError( + 'Fail to parse %s\n-- stdout --\n%s\n-- stderr --\n%s' % ( + error.code, + stdout.getvalue(), + stderr.getvalue(), + )) + finally: + # We don't revert to sys.__stdout__ and the test runner might + # have injected its logger. + sys.stdout = prev_stdout + sys.stderr = prev_stderr + + def parseArgumentsFailure(self, args): + """ + Parse arguments and capture exit_code and stderr. + """ + stdout = StringIO() + stderr = StringIO() + prev_stdout = sys.stdout + prev_stderr = sys.stderr + try: + sys.stdout = stdout + sys.stderr = stderr + self.parser.parse_args(args) + raise AssertionError( # pragma: no cover + 'Failure not triggered when parsing the arguments.') + except SystemExit as error: + return error.code, stderr.getvalue() + finally: + # We don't revert to sys.__stdout__ and the test runner might + # have injected its logger. + sys.stdout = prev_stdout + sys.stderr = prev_stderr + + def assertNamespaceEqual(self, expected, actual): + """ + Check that namespaces are equal. + """ + namespace = Namespace(**expected) + self.assertEqual(namespace, actual) diff --git a/src/chevah_keycert/tests/keydata.py b/src/chevah_keycert/tests/keydata.py new file mode 100644 index 0000000..dc44a66 --- /dev/null +++ b/src/chevah_keycert/tests/keydata.py @@ -0,0 +1,632 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. +""" +Data used by test_keys as well as others. +""" +from __future__ import absolute_import, division, unicode_literals + +from base64 import decodestring as decodebytes + +RSAData = { + 'n': int('269413617238113438198661010376758399219880277968382122687862697' + '296942471209955603071120391975773283844560230371884389952067978' + '789684135947515341209478065209455427327369102356204259106807047' + '964139525310539133073743116175821417513079706301100600025815509' + '786721808719302671068052414466483676821987505720384645561708425' + '794379383191274856941628512616355437197560712892001107828247792' + '561858327085521991407807015047750218508971611590850575870321007' + '991909043252470730134547038841839367764074379439843108550888709' + '430958143271417044750314742880542002948053835745429446485015316' + '60749404403945254975473896534482849256068133525751'), + 'e': int(65537), + 'd': int('420335724286999695680502438485489819800002417295071059780489811' + '840828351636754206234982682752076205397047218449504537476523960' + '987613148307573487322720481066677105211155388802079519869249746' + '774085882219244493290663802569201213676433159425782937159766786' + '329742053214957933941260042101377175565683849732354700525628975' + '239000548651346620826136200952740446562751690924335365940810658' + '931238410612521441739702170503547025018016868116037053013935451' + '477930426013703886193016416453215950072147440344656137718959053' + '897268663969428680144841987624962928576808352739627262941675617' + '7724661940425316604626522633351193810751757014073'), + 'p': int('152689878451107675391723141129365667732639179427453246378763774' + '448531436802867910180261906924087589684175595016060014593521649' + '964959248408388984465569934780790357826811592229318702991401054' + '226302790395714901636384511513449977061729214247279176398290513' + '085108930550446985490864812445551198848562639933888780317'), + 'q': int('176444974592327996338888725079951900172097062203378367409936859' + '072670162290963119826394224277287608693818012745872307600855894' + '647300295516866118620024751601329775653542084052616260193174546' + '400544176890518564317596334518015173606460860373958663673307503' + '231977779632583864454001476729233959405710696795574874403'), + 'u': int('936018002388095842969518498561007090965136403384715613439364803' + '229386793506402222847415019772053080458257034241832795210460612' + '924445085372678524176842007912276654532773301546269997020970818' + '155956828553418266110329867222673040098885651348225673298948529' + '93885224775891490070400861134282266967852120152546563278') +} + +DSAData = { + 'g': int("10253261326864117157640690761723586967382334319435778695" + "29171533815411392477819921538350732400350395446211982054" + "96512489289702949127531056893725702005035043292195216541" + "11525058911428414042792836395195432445511200566318251789" + "10575695836669396181746841141924498545494149998282951407" + "18645344764026044855941864175"), + 'p': int("10292031726231756443208850082191198787792966516790381991" + "77502076899763751166291092085666022362525614129374702633" + "26262930887668422949051881895212412718444016917144560705" + "45675251775747156453237145919794089496168502517202869160" + "78674893099371444940800865897607102159386345313384716752" + "18590012064772045092956919481"), + 'q': int(1393384845225358996250882900535419012502712821577), + 'x': int(1220877188542930584999385210465204342686893855021), + 'y': int("14604423062661947579790240720337570315008549983452208015" + "39426429789435409684914513123700756086453120500041882809" + "10283610277194188071619191739512379408443695946763554493" + "86398594314468629823767964702559709430618263927529765769" + "10270265745700231533660131769648708944711006508965764877" + "684264272082256183140297951") +} + +ECDatanistp256 = { + 'x': int('762825130203920963171185031449647317742997734817505505433829043' + '45687059013883'), + 'y': int('815431978646028526322656647694416475343443758943143196810611371' + '59310646683104'), + 'privateValue': int('3463874347721034170096400845565569825355565567882605' + '9678074967909361042656500'), + 'curve': b'ecdsa-sha2-nistp256' +} + +ECDatanistp384 = { + 'privateValue': int('280814107134858470598753916394807521398239633534281633982576099083' + '35787109896602102090002196616273211495718603965098'), + 'x': int('10036914308591746758780165503819213553101287571902957054148542' + '504671046744460374996612408381962208627004841444205030'), + 'y': int('17337335659928075994560513699823544906448896792102247714689323' + '575406618073069185107088229463828921069465902299522926'), + 'curve': b'ecdsa-sha2-nistp384' +} + +ECDatanistp521 = { + 'x': int('12944742826257420846659527752683763193401384271391513286022917' + '29910013082920512632908350502247952686156279140016049549948975' + '670668730618745449113644014505462'), + 'y': int('10784108810271976186737587749436295782985563640368689081052886' + '16296815984553198866894145509329328086635278430266482551941240' + '591605833440825557820439734509311'), + 'privateValue': int('662751235215460886290293902658128847495347691199214706697089140769' + '672273950767961331442265530524063943548846724348048614239791498442' + '5997823106818915698960565'), + 'curve': b'ecdsa-sha2-nistp521' +} + +Ed25519Data = { + 'a': (b'\xf1\x16\xd1\x15J\x1e\x15\x0e\x19^\x19F\xb5\xf2D\r\xb2R\xa0\xae*k' + b'#\x13sE\xfd@\xd9W{\x8b'), + 'k': (b'7/%\xda\x8d\xd4\xa8\x9ax|a\xf0\x98\x01\xc6\xf4^mg\x05i17Li\r\x05U' + b'\xbb\xc9DX') +} + +privateECDSA_openssh521 = b"""-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIAjn0lSVF6QweS4bjOGP9RHwqxUiTastSE0MVuLtFvkxygZqQ712oZ +ewMvqKkxthMQgxzSpGtRBcmkL7RqZ94+18qgBwYFK4EEACOhgYkDgYYABAFpX/6B +mxxglwD+VpEvw0hcyxVzLxNnMGzxZGF7xmNj8nlF7M+TQctdlR2Xv/J+AgIeVGmB +j2p84bkV9jBzrUNJEACsJjttZw8NbUrhxjkLT/3rMNtuwjE4vLja0P7DMTE0EV8X +f09ETdku/z/1tOSSrSvRwmUcM9nQUJtHHAZlr5Q0fw== +-----END EC PRIVATE KEY-----""" + +# New format introduced in OpenSSH 6.5 +privateECDSA_openssh521_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS +1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBaV/+gZscYJcA/laRL8NIXMsVcy8T +ZzBs8WRhe8ZjY/J5RezPk0HLXZUdl7/yfgICHlRpgY9qfOG5FfYwc61DSRAArCY7bWcPDW +1K4cY5C0/96zDbbsIxOLy42tD+wzExNBFfF39PRE3ZLv8/9bTkkq0r0cJlHDPZ0FCbRxwG +Za+UNH8AAAEAeRISlnkSEpYAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ +AAAIUEAWlf/oGbHGCXAP5WkS/DSFzLFXMvE2cwbPFkYXvGY2PyeUXsz5NBy12VHZe/8n4C +Ah5UaYGPanzhuRX2MHOtQ0kQAKwmO21nDw1tSuHGOQtP/esw227CMTi8uNrQ/sMxMTQRXx +d/T0RN2S7/P/W05JKtK9HCZRwz2dBQm0ccBmWvlDR/AAAAQgCOfSVJUXpDB5LhuM4Y/1Ef +CrFSJNqy1ITQxW4u0W+THKBmpDvXahl7Ay+oqTG2ExCDHNKka1EFyaQvtGpn3j7XygAAAA +ABAg== +-----END OPENSSH PRIVATE KEY----- +""" + +publicECDSA_openssh521 = ( + b"ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACF" + b"BAFpX/6BmxxglwD+VpEvw0hcyxVzLxNnMGzxZGF7xmNj8nlF7M+TQctdlR2Xv/J+AgIeVGmB" + b"j2p84bkV9jBzrUNJEACsJjttZw8NbUrhxjkLT/3rMNtuwjE4vLja0P7DMTE0EV8Xf09ETdku" + b"/z/1tOSSrSvRwmUcM9nQUJtHHAZlr5Q0fw== comment" +) + +privateECDSA_openssh384 = b"""-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAtAi7I8j73WCX20qUM5hhHwHuFzYWYYILs2Sh8UZ+awNkARZ/Fu2LU +LLl5RtOQpbWgBwYFK4EEACKhZANiAATU17sA9P5FRwSknKcFsjjsk0+E3CeXPYX0 +Tk/M0HK3PpWQWgrO8JdRHP9eFE9O/23P8BumwFt7F/AvPlCzVd35VfraFT0o4cCW +G0RqpQ+np31aKmeJshkcYALEchnU+tQ= +-----END EC PRIVATE KEY-----""" + +# New format introduced in OpenSSH 6.5 +privateECDSA_openssh384_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS +1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQTU17sA9P5FRwSknKcFsjjsk0+E3CeX +PYX0Tk/M0HK3PpWQWgrO8JdRHP9eFE9O/23P8BumwFt7F/AvPlCzVd35VfraFT0o4cCWG0 +RqpQ+np31aKmeJshkcYALEchnU+tQAAADIiktpWIpLaVgAAAATZWNkc2Etc2hhMi1uaXN0 +cDM4NAAAAAhuaXN0cDM4NAAAAGEE1Ne7APT+RUcEpJynBbI47JNPhNwnlz2F9E5PzNBytz +6VkFoKzvCXURz/XhRPTv9tz/AbpsBbexfwLz5Qs1Xd+VX62hU9KOHAlhtEaqUPp6d9Wipn +ibIZHGACxHIZ1PrUAAAAMC0CLsjyPvdYJfbSpQzmGEfAe4XNhZhgguzZKHxRn5rA2QBFn8 +W7YtQsuXlG05CltQAAAAA= +-----END OPENSSH PRIVATE KEY----- +""" + +publicECDSA_openssh384 = ( + b"ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABh" + b"BNTXuwD0/kVHBKScpwWyOOyTT4TcJ5c9hfROT8zQcrc+lZBaCs7wl1Ec/14UT07/bc/wG6bA" + b"W3sX8C8+ULNV3flV+toVPSjhwJYbRGqlD6enfVoqZ4myGRxgAsRyGdT61A== comment" +) + +publicECDSA_openssh = ( + b"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABB" + b"BKimX1DZ7+Qj0SpfePMbo1pb6yGkAb5l7duC1l855yD7tEfQfqk7bc7v46We1hLMyz6ObUBY" + b"gkN/34n42F4vpeA= comment" +) + +privateECDSA_openssh = b"""-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEyU1YOT2JxxofwbJXIjGftdNcJK55aQdNrhIt2xYQz0oAoGCCqGSM49 +AwEHoUQDQgAEqKZfUNnv5CPRKl948xujWlvrIaQBvmXt24LWXznnIPu0R9B+qTtt +zu/jpZ7WEszLPo5tQFiCQ3/fifjYXi+l4A== +-----END EC PRIVATE KEY-----""" + +# New format introduced in OpenSSH 6.5 +privateECDSA_openssh_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSopl9Q2e/kI9EqX3jzG6NaW+shpAG+ +Ze3bgtZfOecg+7RH0H6pO23O7+OlntYSzMs+jm1AWIJDf9+J+NheL6XgAAAAmCKU4hcilO +IXAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKimX1DZ7+Qj0Spf +ePMbo1pb6yGkAb5l7duC1l855yD7tEfQfqk7bc7v46We1hLMyz6ObUBYgkN/34n42F4vpe +AAAAAgTJTVg5PYnHGh/BslciMZ+101wkrnlpB02uEi3bFhDPQAAAAA +-----END OPENSSH PRIVATE KEY----- +""" + +publicEd25519_openssh = ( + b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPEW0RVKHhUOGV4ZRrXyRA2yUqCuKmsjE3NF" + b"/UDZV3uL comment" +) + +# OpenSSH has only ever supported the "new" (v1) format for Ed25519. +privateEd25519_openssh_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDxFtEVSh4VDhleGUa18kQNslKgriprIxNzRf1A2Vd7iwAAAJA61eMLOtXj +CwAAAAtzc2gtZWQyNTUxOQAAACDxFtEVSh4VDhleGUa18kQNslKgriprIxNzRf1A2Vd7iw +AAAEA3LyXajdSomnh8YfCYAcb0Xm1nBWkxN0xpDQVVu8lEWPEW0RVKHhUOGV4ZRrXyRA2y +UqCuKmsjE3NF/UDZV3uLAAAAB2NvbW1lbnQBAgMEBQY= +-----END OPENSSH PRIVATE KEY-----""" + +publicRSA_openssh = ( + b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDVaqx4I9bWG+wloVDEd2NQhEUBVUIUKirg" + b"0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHjVyqgYwBGTJAkMgUyP" + b"95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auIJNm/9NNN9b0b/h9qp" + b"KSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY6RKXCpCnd1bqcPUWz" + b"xiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvbw7KW4CC1ffdOgTtDc1" + b"foNfICZgptyti8ZseZj3 comment" +) + +privateRSA_openssh = b'''-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA1WqseCPW1hvsJaFQxHdjUIRFAVVCFCoq4NBg7tTpo61K+jkG +XoRVdV8ANr9vqio/gyY3wWkuW/3w89J91pjNOkB41cqoGMARkyQJDIFMj/ec7RMW +aqQE6Ul3w+RVZLN5aJ4sCOus6AQtIXcFp47vUzANpeW7PWriCTZv/TTTfW9G/4fa +qSknqv+t9YXmPhq4eh1KserAWvcw3x/CpOTvP5FJlkDVGXctN8Ne7J2mOkSlwqQp +3dW6nD1Fs8YsGGTVuj3fq3/NQqyn8RgLoFgVYgukKm5Dw+QEnzWjR45G7TOlZb28 +OyluAgtX33ToE7Q3NX6DXyAmYKbcrYvGbHmY9wIDAQABAoIBACFMCGaiKNW0+44P +chuFCQC58k438BxXS+NRf54jp+Q6mFUb6ot6mB682Lqx+YkSGGCs6MwLTglaQGq6 +L5n4syRghLnOaZWa+eL8H1FNJxXbKyet77RprL59EOuGR3BztACHlRU7N/nnFOeA +u2geG+bdu3NjuWfmsid/z88wm8KY/dkYNi82LvE9gXqf4QMtR9s0UWI53U/prKiL +2dbzhMQXuXGdBghCeE27xSr0w1jNVSvtvjNfBOp75gQkY/It1z0bbNWcY0MvkoiN +Pm7aGDfYDyVniR25RjReyc7Ei+2SWjMHD9+GCPmS6dvrOAg2yc3NCgFIWzk+esrG +gKnc1DkCgYEA2XAG2OK81HiRUJTUwRuJOGxGZFpRoJoHPUiPA1HMaxKOfRqxZedx +dTngMgV1jRhMr5OxSbFmX3hietEMyuZNQ7Oc9Gt95gyY3M8hYo7VLhLeBK7XJG6D +MaIVokQ9IqliJiK5su1UCp0Ig6cHDf8ZGI7Yqx3aSJwxaBGhZm3j2B0CgYEA+0QX +i6Q2vh43Haf2YWwExKrdeD4HjB4zAq4DFIeDeuWefQhnqPKqvxJwz3Kpp8cLHYjV +IP2cY8pHMFVOi8TP9H8WpJISdKEJwsRunIwz76Xl9+ArrU9cEaoahDdb/Xrqw818 +sMjkH1Rjtcev3/QJp/zHJfxc6ZHXksWYHlbTsSMCgYBRr+mSn5QLSoRlPpSzO5IQ +tXS4jMnvyQ4BMvovaBKhAyauz1FoFEwmmyikAjMIX+GncJgBNHleUo7Ezza8H0tV +rOvBU4TH4WGoStSi/0ANgB8SqVDAKhh1lAwGmxZQqEvsQc177/dLyXUCaMSYuIaI +GFpD5wIzlyJkk4MMRSp87QKBgGlmN8ZA3SHFBPOwuD5HlHx2/C3rPzk8lcNDAVHE +Qpfz6Bakxu7s1EkQUDgE7jvN19DMzDJpkAegG1qf/jHNHjp+cR4ZlBpOTwzfX1LV +0Rdu7NectlWd244hX7wkiLb8r6vw76QssNyfhrADEriL4t0PwO4jPUpQ/i+4KUZY +v7YnAoGAZhb5IDTQVCW8YTGsgvvvnDUefkpVAmiVDQqTvh6/4UD6kKdUcDHpePzg +Zrcid5rr3dXSMEbK4tdeQZvPtUg1Uaol3N7bNClIIdvWdPx+5S9T95wJcLnkoHam +rXp0IjScTxfLP+Cq5V6lJ94/pX8Ppoj1FdZfNxeS4NYFSRA7kvY= +-----END RSA PRIVATE KEY-----''' + +# Some versions of OpenSSH generate these (slightly different keys): the PKCS#1 +# structure is wrapped in an extra ASN.1 SEQUENCE and there's an empty SEQUENCE +# following it. It is not any standard key format and was probably a bug in +# OpenSSH at some point. +privateRSA_openssh_alternate = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEqTCCBKMCAQACggEBANVqrHgj1tYb7CWhUMR3Y1CERQFVQhQqKuDQYO7U6aOtSvo5Bl6EVXVf +ADa/b6oqP4MmN8FpLlv98PPSfdaYzTpAeNXKqBjAEZMkCQyBTI/3nO0TFmqkBOlJd8PkVWSzeWie +LAjrrOgELSF3BaeO71MwDaXluz1q4gk2b/00031vRv+H2qkpJ6r/rfWF5j4auHodSrHqwFr3MN8f +wqTk7z+RSZZA1Rl3LTfDXuydpjpEpcKkKd3Vupw9RbPGLBhk1bo936t/zUKsp/EYC6BYFWILpCpu +Q8PkBJ81o0eORu0zpWW9vDspbgILV9906BO0NzV+g18gJmCm3K2Lxmx5mPcCAwEAAQKCAQAhTAhm +oijVtPuOD3IbhQkAufJON/AcV0vjUX+eI6fkOphVG+qLepgevNi6sfmJEhhgrOjMC04JWkBqui+Z ++LMkYIS5zmmVmvni/B9RTScV2ysnre+0aay+fRDrhkdwc7QAh5UVOzf55xTngLtoHhvm3btzY7ln +5rInf8/PMJvCmP3ZGDYvNi7xPYF6n+EDLUfbNFFiOd1P6ayoi9nW84TEF7lxnQYIQnhNu8Uq9MNY +zVUr7b4zXwTqe+YEJGPyLdc9G2zVnGNDL5KIjT5u2hg32A8lZ4kduUY0XsnOxIvtklozBw/fhgj5 +kunb6zgINsnNzQoBSFs5PnrKxoCp3NQ5AoGBANlwBtjivNR4kVCU1MEbiThsRmRaUaCaBz1IjwNR +zGsSjn0asWXncXU54DIFdY0YTK+TsUmxZl94YnrRDMrmTUOznPRrfeYMmNzPIWKO1S4S3gSu1yRu +gzGiFaJEPSKpYiYiubLtVAqdCIOnBw3/GRiO2Ksd2kicMWgRoWZt49gdAoGBAPtEF4ukNr4eNx2n +9mFsBMSq3Xg+B4weMwKuAxSHg3rlnn0IZ6jyqr8ScM9yqafHCx2I1SD9nGPKRzBVTovEz/R/FqSS +EnShCcLEbpyMM++l5ffgK61PXBGqGoQ3W/166sPNfLDI5B9UY7XHr9/0Caf8xyX8XOmR15LFmB5W +07EjAoGAUa/pkp+UC0qEZT6UszuSELV0uIzJ78kOATL6L2gSoQMmrs9RaBRMJpsopAIzCF/hp3CY +ATR5XlKOxM82vB9LVazrwVOEx+FhqErUov9ADYAfEqlQwCoYdZQMBpsWUKhL7EHNe+/3S8l1AmjE +mLiGiBhaQ+cCM5ciZJODDEUqfO0CgYBpZjfGQN0hxQTzsLg+R5R8dvwt6z85PJXDQwFRxEKX8+gW +pMbu7NRJEFA4BO47zdfQzMwyaZAHoBtan/4xzR46fnEeGZQaTk8M319S1dEXbuzXnLZVnduOIV+8 +JIi2/K+r8O+kLLDcn4awAxK4i+LdD8DuIz1KUP4vuClGWL+2JwKBgQCFSxt6mxIQN54frV7a/saW +/t81a7k04haXkiYJvb1wIAOnNb0tG6DSB0cr1N6oqAcHG7gEIKcnQTxsOTnpQc7nFx3RTFy8PdIm +Jv5q1v1Icq5G+nvD0xlgRB2lE6eA9WMp1HpdBgcWXfaLPctkOuKEWk2MBi0tnRzrg0x4PXlUzjAA +-----END RSA PRIVATE KEY-----""" + +# New format introduced in OpenSSH 6.5 +privateRSA_openssh_new = b'''-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEA1WqseCPW1hvsJaFQxHdjUIRFAVVCFCoq4NBg7tTpo61K+jkGXoRV +dV8ANr9vqio/gyY3wWkuW/3w89J91pjNOkB41cqoGMARkyQJDIFMj/ec7RMWaqQE6Ul3w+ +RVZLN5aJ4sCOus6AQtIXcFp47vUzANpeW7PWriCTZv/TTTfW9G/4faqSknqv+t9YXmPhq4 +eh1KserAWvcw3x/CpOTvP5FJlkDVGXctN8Ne7J2mOkSlwqQp3dW6nD1Fs8YsGGTVuj3fq3 +/NQqyn8RgLoFgVYgukKm5Dw+QEnzWjR45G7TOlZb28OyluAgtX33ToE7Q3NX6DXyAmYKbc +rYvGbHmY9wAAA7gXkBoMF5AaDAAAAAdzc2gtcnNhAAABAQDVaqx4I9bWG+wloVDEd2NQhE +UBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHjVyqgY +wBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auIJNm +/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY6 +RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvb +w7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3AAAAAwEAAQAAAQAhTAhmoijVtPuOD3Ib +hQkAufJON/AcV0vjUX+eI6fkOphVG+qLepgevNi6sfmJEhhgrOjMC04JWkBqui+Z+LMkYI +S5zmmVmvni/B9RTScV2ysnre+0aay+fRDrhkdwc7QAh5UVOzf55xTngLtoHhvm3btzY7ln +5rInf8/PMJvCmP3ZGDYvNi7xPYF6n+EDLUfbNFFiOd1P6ayoi9nW84TEF7lxnQYIQnhNu8 +Uq9MNYzVUr7b4zXwTqe+YEJGPyLdc9G2zVnGNDL5KIjT5u2hg32A8lZ4kduUY0XsnOxIvt +klozBw/fhgj5kunb6zgINsnNzQoBSFs5PnrKxoCp3NQ5AAAAgQCFSxt6mxIQN54frV7a/s +aW/t81a7k04haXkiYJvb1wIAOnNb0tG6DSB0cr1N6oqAcHG7gEIKcnQTxsOTnpQc7nFx3R +TFy8PdImJv5q1v1Icq5G+nvD0xlgRB2lE6eA9WMp1HpdBgcWXfaLPctkOuKEWk2MBi0tnR +zrg0x4PXlUzgAAAIEA2XAG2OK81HiRUJTUwRuJOGxGZFpRoJoHPUiPA1HMaxKOfRqxZedx +dTngMgV1jRhMr5OxSbFmX3hietEMyuZNQ7Oc9Gt95gyY3M8hYo7VLhLeBK7XJG6DMaIVok +Q9IqliJiK5su1UCp0Ig6cHDf8ZGI7Yqx3aSJwxaBGhZm3j2B0AAACBAPtEF4ukNr4eNx2n +9mFsBMSq3Xg+B4weMwKuAxSHg3rlnn0IZ6jyqr8ScM9yqafHCx2I1SD9nGPKRzBVTovEz/ +R/FqSSEnShCcLEbpyMM++l5ffgK61PXBGqGoQ3W/166sPNfLDI5B9UY7XHr9/0Caf8xyX8 +XOmR15LFmB5W07EjAAAAAAEC +-----END OPENSSH PRIVATE KEY----- +''' + +# Encrypted with the passphrase 'encrypted' +privateRSA_openssh_encrypted = b"""-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,FFFFFFFFFFFFFFFF + +p2A1YsHLXkpMVcsEqhh/nCYb5AqL0uMzfEIqc8hpZ/Ub8PtLsypilMkqzYTnZIGS +ouyPjU/WgtR4VaDnutPWdgYaKdixSEmGhKghCtXFySZqCTJ4O8NCczsktYjUK3D4 +Jtl90zL6O81WBY6xP76PBQo9lrI/heAetATeyqutc18bwQIGU+gKk32qvfo15DfS +VYiY0Ds4D7F7fd9pz+f5+UbFUCgU+tfDvBrqodYrUgmH7jKoW/CRDCHHyeEIZDbF +mcMwdcKOyw1sRLaPdihRSVx3kOMvIotHKVTkIDMp+0RTNeXzQnp5U2qzsxzTcG/M +UyJN38XXkuvq5VMj2zmmjHzx34w3NK3ZxpZcoaFUqUBlNp2C8hkCLrAa/DWobKqN +5xA1ElrQvli9XXkT/RIuy4Gc10bbGEoJjuxNRibtSxxWd5Bd1E40ocOd4l1ebI8+ +w69XvMTnsmHvkBEADGF2zfRszKnMelg+W5NER1UDuNT03i+1cuhp+2AZg8z7niTO +M17XP3ScGVxrQAEYgtxPrPeIpFJvOx2j5Yt78U9Y2WlaAG6DrubbYv2RsMIibhOG +yk139vMdD8FwCey6yMkkhFAJwnBtC22MAWgjmC5c6AF3SRQSjjQXepPsJcLgpOjy +YwjhnL8w56x9kVDUNPw9A9Cqgxo2sty34ATnKrh4h59PsP83LOL6OC5WjbASgZRd +OIBD8RloQPISo+RUF7X0i4kdaHVNPlR0KyapR+3M5BwhQuvEO99IArDV2LNKGzfc +W4ssugm8iyAJlmwmb2yRXIDHXabInWY7XCdGk8J2qPFbDTvnPbiagJBimjVjgpWw +tV3sVlJYqmOqmCDP78J6he04l0vaHtiOWTDEmNCrK7oFMXIIp3XWjOZGPSOJFdPs +6Go3YB+EGWfOQxqkFM28gcqmYfVPF2sa1FbZLz0ffO11Ma/rliZxZu7WdrAXe/tc +BgIQ8etp2PwAK4jCwwVwjIO8FzqQGpS23Y9NY3rfi97ckgYXKESFtXPsMMA+drZd +ThbXvccfh4EPmaqQXKf4WghHiVJ+/yuY1kUIDEl/O0jRZWT7STgBim/Aha1m6qRs +zl1H7hkDbU4solb1GM5oPzbgGTzyBc+z0XxM9iFRM+fMzPB8+yYHTr4kPbVmKBjy +SCovjQQVsHE4YeUGTq6k/NF5cVIRKTW/RlHvzxsky1Zj31MC736jrxGw4KG7VSLZ +fP6F5jj+mXwS7m0v5to42JBZmRJdKUD88QaGE3ncyQ4yleW5bn9Lf9SuzQg1Dhao +3rSA1RuexsHlIAHvGxx/17X+pyygl8DJbt6TBfbLQk9wc707DJTfh5M/bnk9wwIX +l/Hsa1WtylAMW/2MzgiVy83MbYz4+Ss6GQ5W66okWji+NxrnrYEy6q+WgVQanp7X +D+D7oKykqE1Cdvvulvtfl5fh8wlAs8mrUnKPBBUru348u++2lfacLkxRXyT1ooqY +uSNE5nlwFt08N2Ou/bl7yq6QNRMYrRkn+UEfHWCNYDoGMHln2/i6Z1RapQzNarik +tJf7radBz5nBwBjP08YAEACNSQvpsUgdqiuYjLwX7efFXQva2RzqaQ== +-----END RSA PRIVATE KEY-----""" + +# Encrypted with the passphrase 'encrypted', and using the new format +# introduced in OpenSSH 6.5 +privateRSA_openssh_encrypted_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABD0f9WAof +DTbmwztb8pdrSeAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQDVaqx4I9bW +G+wloVDEd2NQhEUBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n +3WmM06QHjVyqgYwBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9T +MA2l5bs9auIJNm/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQN +UZdy03w17snaY6RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASf +NaNHjkbtM6Vlvbw7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3AAADwPQaac8s1xX3af +hQTQexj0vEAWDQsLYzDHN9G7W+UP5WHUu7igeu2GqAC/TOnjUXDP73I+EN3n7T3JFeDRfs +U1Z6Zqb0NKHSRVYwDIdIi8qVohFv85g6+xQ01OpaoOzz+vI34OUvCRHQGTgR6L9fQShZyC +McopYMYfbIse6KcqkfxX3KSdG1Pao6Njx/ShFRbgvmALpR/z0EaGCzHCDxpfUyAdnxm621 +Jzaf+LverWdN7sfrfMptaS9//9iJb70sL67K+YIB64qhDnA/w9UOQfXGQFL+AEtdM0BPv8 +thP1bs7T0yucBl+ZXdrDKVLZfaS3S/w85Jlgfu+a1DG73pOBOuag435iEJ9EnspjXiiydx +GrfSRk2C+/c4fBDZVGFscK5bfQuUUZyU1qOagekxX7WLHFKk9xajnud+nrAN070SeNwlX8 +FZ2CI4KGlQfDvVUpKanYn8Kkj3fZ+YBGyx4M+19clF65FKSM0x1Rrh5tAmNT/SNDbSc28m +ASxrBhztzxUFTrIn3tp+uqkJniFLmFsUtiAUmj8fNyE9blykU7dqq+CqpLA872nQ9bOHHA +JsS1oBYmQ0n6AJz8WrYMdcepqWVld6Q8QSD1zdrY/sAWUovuBA1s4oIEXZhpXSS4ZJiMfh +PVktKBwj5bmoG/mmwYLbo0JHntK8N3TGTzTGLq5TpSBBdVvWSWo7tnfEkrFObmhi1uJSrQ +3zfPVP6BguboxBv+oxhaUBK8UOANe6ZwM4vfiu+QN+sZqWymHIfAktz7eWzwlToe4cKpdG +Uv+e3/7Lo2dyMl3nke5HsSUrlsMGPREuGkBih8+o85ii6D+cuCiVtus3f5c78Cir80zLIr +Z0wWvEAjciEvml00DWaA+JIaOrWwvXySaOzFGpCqC9SQjao379bvn9P3b7kVZsy6zBfHqm +bNEJUOuhBZaY8Okz36chh1xqh4sz7m3nsZ3GYGcvM+3mvRY72QnqsQEG0Sp1XYIn2bHa29 +tqp7CG9X8J6dqMcPeoPRDWIX9gw7EPl/M0LP6xgewGJ9bgxwle6Mnr9kNITIswjAJqrLec +zx7dfixjAPc42ADqrw/tEdFQcSqxigcfJNKO1LbDBjh+Hk/cSBou2PoxbIcl0qfQfbGcqI +Dbpd695IEuiW9pYR22txNoIi+7cbMsuFHxQ/OqbrX/jCsprGNNJLAjgGsVEI1JnHWDH0db +3UbqbOHAeY3ufoYXNY1utVOIACpW3r9wBw3FjRi04d70VcKr16OXvOAHGN2G++Y+kMya84 +Hl/Kt/gA== +-----END OPENSSH PRIVATE KEY----- +""" + +# Encrypted with the passphrase 'testxp'. NB: this key was generated by +# OpenSSH, so it doesn't use the same key data as the other keys here. +privateRSA_openssh_encrypted_aes = b"""-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,0673309A6ACCAB4B77DEE1C1E536AC26 + +4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n +T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H +g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB +sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5 +9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV +gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW +0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE +vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS +hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk +2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf +qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk +4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY +EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n +8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0 +fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P +V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+ +0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5 +xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI +dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup +VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk +gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c +8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw +SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 +CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE +xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P +-----END RSA PRIVATE KEY-----""" + +publicRSA_lsh = ( + b'{KDEwOnB1YmxpYy1rZXkoMTQ6cnNhLXBrY3MxLXNoYTEoMTpuMjU3OgDVaqx4I9bWG+wloVD' + b'Ed2NQhEUBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHj' + b'VyqgYwBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auI' + b'JNm/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY' + b'6RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvbw' + b'7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3KSgxOmUzOgEAASkpKQ==}' +) + +privateRSA_lsh = ( + b"(11:private-key(9:rsa-pkcs1(1:n257:\x00\xd5j\xacx#\xd6\xd6\x1b\xec%\xa1P" + b"\xc4wcP\x84E\x01UB\x14**\xe0\xd0`\xee\xd4\xe9\xa3\xadJ\xfa9\x06^\x84Uu_" + b"\x006\xbfo\xaa*?\x83&7\xc1i.[\xfd\xf0\xf3\xd2}\xd6\x98\xcd:@x\xd5\xca" + b"\xa8\x18\xc0\x11\x93$\t\x0c\x81L\x8f\xf7\x9c\xed\x13\x16j\xa4\x04\xe9Iw" + b"\xc3\xe4Ud\xb3yh\x9e,\x08\xeb\xac\xe8\x04-!w\x05\xa7\x8e\xefS0\r\xa5\xe5" + b"\xbb=j\xe2\t6o\xfd4\xd3}oF\xff\x87\xda\xa9)'\xaa\xff\xad\xf5\x85\xe6>" + b"\x1a\xb8z\x1dJ\xb1\xea\xc0Z\xf70\xdf\x1f\xc2\xa4\xe4\xef?\x91I\x96@\xd5" + b"\x19w-7\xc3^\xec\x9d\xa6:D\xa5\xc2\xa4)\xdd\xd5\xba\x9c=E\xb3\xc6,\x18d" + b"\xd5\xba=\xdf\xab\x7f\xcdB\xac\xa7\xf1\x18\x0b\xa0X\x15b\x0b\xa4*nC\xc3" + b"\xe4\x04\x9f5\xa3G\x8eF\xed3\xa5e\xbd\xbc;)n\x02\x0bW\xdft\xe8\x13\xb475" + b"~\x83_ &`\xa6\xdc\xad\x8b\xc6ly\x98\xf7)(1:e3:\x01\x00\x01)(1:d256:!L" + b"\x08f\xa2(\xd5\xb4\xfb\x8e\x0fr\x1b\x85\t\x00\xb9\xf2N7\xf0\x1cWK\xe3Q" + b"\x7f\x9e#\xa7\xe4:\x98U\x1b\xea\x8bz\x98\x1e\xbc\xd8\xba\xb1\xf9\x89\x12" + b"\x18`\xac\xe8\xcc\x0bN\tZ@j\xba/\x99\xf8\xb3$`\x84\xb9\xcei\x95\x9a\xf9" + b"\xe2\xfc\x1fQM'\x15\xdb+'\xad\xef\xb4i\xac\xbe}\x10\xeb\x86Gps\xb4\x00" + b"\x87\x95\x15;7\xf9\xe7\x14\xe7\x80\xbbh\x1e\x1b\xe6\xdd\xbbsc\xb9g\xe6" + b"\xb2'\x7f\xcf\xcf0\x9b\xc2\x98\xfd\xd9\x186/6.\xf1=\x81z\x9f\xe1\x03-G" + b"\xdb4Qb9\xddO\xe9\xac\xa8\x8b\xd9\xd6\xf3\x84\xc4\x17\xb9q\x9d\x06\x08Bx" + b"M\xbb\xc5*\xf4\xc3X\xcdU+\xed\xbe3_\x04\xea{\xe6\x04$c\xf2-\xd7=\x1bl" + b"\xd5\x9ccC/\x92\x88\x8d>n\xda\x187\xd8\x0f%g\x89\x1d\xb9F4^\xc9\xce\xc4" + b"\x8b\xed\x92Z3\x07\x0f\xdf\x86\x08\xf9\x92\xe9\xdb\xeb8\x086\xc9\xcd\xcd" + b"\n\x01H[9>z\xca\xc6\x80\xa9\xdc\xd49)(1:p129:\x00\xfbD\x17\x8b\xa46\xbe" + b"\x1e7\x1d\xa7\xf6al\x04\xc4\xaa\xddx>\x07\x8c\x1e3\x02\xae\x03\x14\x87" + b"\x83z\xe5\x9e}\x08g\xa8\xf2\xaa\xbf\x12p\xcfr\xa9\xa7\xc7\x0b\x1d\x88" + b"\xd5 \xfd\x9cc\xcaG0UN\x8b\xc4\xcf\xf4\x7f\x16\xa4\x92\x12t\xa1\t\xc2" + b"\xc4n\x9c\x8c3\xef\xa5\xe5\xf7\xe0+\xadO\\\x11\xaa\x1a\x847[\xfdz\xea" + b"\xc3\xcd|\xb0\xc8\xe4\x1fTc\xb5\xc7\xaf\xdf\xf4\t\xa7\xfc\xc7%\xfc\\\xe9" + b"\x91\xd7\x92\xc5\x98\x1eV\xd3\xb1#)(1:q129:\x00\xd9p\x06\xd8\xe2\xbc\xd4" + b"x\x91P\x94\xd4\xc1\x1b\x898lFdZQ\xa0\x9a\x07=H\x8f\x03Q\xcck\x12\x8e}" + b"\x1a\xb1e\xe7qu9\xe02\x05u\x8d\x18L\xaf\x93\xb1I\xb1f_xbz\xd1\x0c\xca" + b"\xe6MC\xb3\x9c\xf4k}\xe6\x0c\x98\xdc\xcf!b\x8e\xd5.\x12\xde\x04\xae\xd7$" + b"n\x831\xa2\x15\xa2D=\"\xa9b&\"\xb9\xb2\xedT\n\x9d\x08\x83\xa7\x07\r\xff" + b"\x19\x18\x8e\xd8\xab\x1d\xdaH\x9c1h\x11\xa1fm\xe3\xd8\x1d)(1:a128:if7" + b"\xc6@\xdd!\xc5\x04\xf3\xb0\xb8>G\x94|v\xfc-\xeb?9<\x95\xc3C\x01Q\xc4B" + b"\x97\xf3\xe8\x16\xa4\xc6\xee\xec\xd4I\x10P8\x04\xee;\xcd\xd7\xd0\xcc\xcc" + b"2i\x90\x07\xa0\x1bZ\x9f\xfe1\xcd\x1e:~q\x1e\x19\x94\x1aNO\x0c\xdf_R\xd5" + b"\xd1\x17n\xec\xd7\x9c\xb6U\x9d\xdb\x8e!_\xbc$\x88\xb6\xfc\xaf\xab\xf0" + b"\xef\xa4,\xb0\xdc\x9f\x86\xb0\x03\x12\xb8\x8b\xe2\xdd\x0f\xc0\xee#=JP" + b"\xfe/\xb8)FX\xbf\xb6')(1:b128:Q\xaf\xe9\x92\x9f\x94\x0bJ\x84e>\x94\xb3;" + b"\x92\x10\xb5t\xb8\x8c\xc9\xef\xc9\x0e\x012\xfa/h\x12\xa1\x03&\xae\xcfQh" + b"\x14L&\x9b(\xa4\x023\x08_\xe1\xa7p\x98\x014y^R\x8e\xc4\xcf6\xbc\x1fKU" + b"\xac\xeb\xc1S\x84\xc7\xe1a\xa8J\xd4\xa2\xff@\r\x80\x1f\x12\xa9P\xc0*\x18" + b"u\x94\x0c\x06\x9b\x16P\xa8K\xecA\xcd{\xef\xf7K\xc9u\x02h\xc4\x98\xb8\x86" + b"\x88\x18ZC\xe7\x023\x97\"d\x93\x83\x0cE*|\xed)(1:c128:f\x16\xf9 4\xd0T%" + b"\xbca1\xac\x82\xfb\xef\x9c5\x1e~JU\x02h\x95\r\n\x93\xbe\x1e\xbf\xe1@\xfa" + b"\x90\xa7Tp1\xe9x\xfc\xe0f\xb7\"w\x9a\xeb\xdd\xd5\xd20F\xca\xe2\xd7^A\x9b" + b"\xcf\xb5H5Q\xaa%\xdc\xde\xdb4)H!\xdb\xd6t\xfc~\xe5/S\xf7\x9c\tp\xb9\xe4" + b"\xa0v\xa6\xadzt\"4\x9cO\x17\xcb?\xe0\xaa\xe5^\xa5\'\xde?\xa5\x7f\x0f\xa6" + b"\x88\xf5\x15\xd6_7\x17\x92\xe0\xd6\x05I\x10;\x92\xf6)))" +) + +privateRSA_agentv3 = ( + b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x03\x01\x00\x01\x00\x00\x01\x00!L" + b"\x08f\xa2(\xd5\xb4\xfb\x8e\x0fr\x1b\x85\t\x00\xb9\xf2N7\xf0\x1cWK\xe3Q" + b"\x7f\x9e#\xa7\xe4:\x98U\x1b\xea\x8bz\x98\x1e\xbc\xd8\xba\xb1\xf9\x89\x12" + b"\x18`\xac\xe8\xcc\x0bN\tZ@j\xba/\x99\xf8\xb3$`\x84\xb9\xcei\x95\x9a\xf9" + b"\xe2\xfc\x1fQM'\x15\xdb+'\xad\xef\xb4i\xac\xbe}\x10\xeb\x86Gps\xb4\x00" + b"\x87\x95\x15;7\xf9\xe7\x14\xe7\x80\xbbh\x1e\x1b\xe6\xdd\xbbsc\xb9g\xe6" + b"\xb2'\x7f\xcf\xcf0\x9b\xc2\x98\xfd\xd9\x186/6.\xf1=\x81z\x9f\xe1\x03-G" + b"\xdb4Qb9\xddO\xe9\xac\xa8\x8b\xd9\xd6\xf3\x84\xc4\x17\xb9q\x9d\x06\x08Bx" + b"M\xbb\xc5*\xf4\xc3X\xcdU+\xed\xbe3_\x04\xea{\xe6\x04$c\xf2-\xd7=\x1bl" + b"\xd5\x9ccC/\x92\x88\x8d>n\xda\x187\xd8\x0f%g\x89\x1d\xb9F4^\xc9\xce\xc4" + b"\x8b\xed\x92Z3\x07\x0f\xdf\x86\x08\xf9\x92\xe9\xdb\xeb8\x086\xc9\xcd\xcd" + b"\n\x01H[9>z\xca\xc6\x80\xa9\xdc\xd49\x00\x00\x01\x01\x00\xd5j\xacx#\xd6" + b"\xd6\x1b\xec%\xa1P\xc4wcP\x84E\x01UB\x14**\xe0\xd0`\xee\xd4\xe9\xa3\xadJ" + b"\xfa9\x06^\x84Uu_\x006\xbfo\xaa*?\x83&7\xc1i.[\xfd\xf0\xf3\xd2}\xd6\x98" + b"\xcd:@x\xd5\xca\xa8\x18\xc0\x11\x93$\t\x0c\x81L\x8f\xf7\x9c\xed\x13\x16j" + b"\xa4\x04\xe9Iw\xc3\xe4Ud\xb3yh\x9e,\x08\xeb\xac\xe8\x04-!w\x05\xa7\x8e" + b"\xefS0\r\xa5\xe5\xbb=j\xe2\t6o\xfd4\xd3}oF\xff\x87\xda\xa9)'\xaa\xff\xad" + b"\xf5\x85\xe6>\x1a\xb8z\x1dJ\xb1\xea\xc0Z\xf70\xdf\x1f\xc2\xa4\xe4\xef?" + b"\x91I\x96@\xd5\x19w-7\xc3^\xec\x9d\xa6:D\xa5\xc2\xa4)\xdd\xd5\xba\x9c=E" + b"\xb3\xc6,\x18d\xd5\xba=\xdf\xab\x7f\xcdB\xac\xa7\xf1\x18\x0b\xa0X\x15b" + b"\x0b\xa4*nC\xc3\xe4\x04\x9f5\xa3G\x8eF\xed3\xa5e\xbd\xbc;)n\x02\x0bW\xdf" + b"t\xe8\x13\xb475~\x83_ &`\xa6\xdc\xad\x8b\xc6ly\x98\xf7\x00\x00\x00\x81" + b"\x00\x85K\x1bz\x9b\x12\x107\x9e\x1f\xad^\xda\xfe\xc6\x96\xfe\xdf5k\xb94" + b"\xe2\x16\x97\x92&\t\xbd\xbdp \x03\xa75\xbd-\x1b\xa0\xd2\x07G+\xd4\xde" + b"\xa8\xa8\x07\x07\x1b\xb8\x04 \xa7'A\x07\x8c\x1e3\x02\xae\x03" + b"\x14\x87\x83z\xe5\x9e}\x08g\xa8\xf2\xaa\xbf\x12p\xcfr\xa9\xa7\xc7\x0b" + b"\x1d\x88\xd5 \xfd\x9cc\xcaG0UN\x8b\xc4\xcf\xf4\x7f\x16\xa4\x92\x12t\xa1" + b"\t\xc2\xc4n\x9c\x8c3\xef\xa5\xe5\xf7\xe0+\xadO\\\x11\xaa\x1a\x847[\xfdz" + b"\xea\xc3\xcd|\xb0\xc8\xe4\x1fTc\xb5\xc7\xaf\xdf\xf4\t\xa7\xfc\xc7%\xfc\\" + b"\xe9\x91\xd7\x92\xc5\x98\x1eV\xd3\xb1#" +) + +publicDSA_openssh = b"""\ +ssh-dss AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9\ +LvFYmFFVMIuHFGlZpIL7sh3IMkqy+cssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+Gu48G\ ++yFuE8l0fVVUivos/MmYVJ66qT99htcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+Sx9a5AAAAFQD0\ +EYmTNaFJ8CS0+vFSF4nYcyEnSQAAAIEAkgLjxHJAE7qFWdTqf7EZngu7jAGmdB9k3YzMHe1ldMxEB\ +7zNw5aOnxjhoYLtiHeoEcOk2XOyvnE+VfhIWwWAdOiKRTEZlmizkvhGbq0DCe2EPMXirjqWACI5nD\ +ioQX1oEMonR8N3AEO5v9SfBqS2Q9R6OBr6lf04RvwpHZ0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQT\ +NEpklRZqeBGo1gotJggNmVaIQNIClGlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2G\ +gdgMQWC7S6WFIXePGGXqNQDdWxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8= \ +comment\ +""" + +privateDSA_openssh = b"""\ +-----BEGIN DSA PRIVATE KEY----- +MIIBvAIBAAKBgQCSkDrFREVQ0CKQDRx/iQAMpaPS7xWJhRVTCLhxRpWaSC+7IdyD +JKsvnLLCDTP5Zxw935rAMi5VF2bbejw/S4GUWs7DzoKbbh/hruPBvshbhPJdH1VV +Ir6LPzJmFSeuqk/fYbXGSmra01mZ6VVu4BQAaKsPoXti2dIJHlNPksfWuQIVAPQR +iZM1oUnwJLT68VIXidhzISdJAoGBAJIC48RyQBO6hVnU6n+xGZ4Lu4wBpnQfZN2M +zB3tZXTMRAe8zcOWjp8Y4aGC7Yh3qBHDpNlzsr5xPlX4SFsFgHToikUxGZZos5L4 +Rm6tAwnthDzF4q46lgAiOZw4qEF9aBDKJ0fDdwBDub/UnwaktkPUejga+pX9OEb8 +KR2dFBrvAoGAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQNICl +GlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXeP +GGXqNQDdWxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8CFQDV2gbL +czUdxCus0pfEP1bddaXRLQ== +-----END DSA PRIVATE KEY-----\ +""" + +privateDSA_openssh_new = b"""\ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsgAAAAdzc2gtZH +NzAAAAgQCSkDrFREVQ0CKQDRx/iQAMpaPS7xWJhRVTCLhxRpWaSC+7IdyDJKsvnLLCDTP5 +Zxw935rAMi5VF2bbejw/S4GUWs7DzoKbbh/hruPBvshbhPJdH1VVIr6LPzJmFSeuqk/fYb +XGSmra01mZ6VVu4BQAaKsPoXti2dIJHlNPksfWuQAAABUA9BGJkzWhSfAktPrxUheJ2HMh +J0kAAACBAJIC48RyQBO6hVnU6n+xGZ4Lu4wBpnQfZN2MzB3tZXTMRAe8zcOWjp8Y4aGC7Y +h3qBHDpNlzsr5xPlX4SFsFgHToikUxGZZos5L4Rm6tAwnthDzF4q46lgAiOZw4qEF9aBDK +J0fDdwBDub/UnwaktkPUejga+pX9OEb8KR2dFBrvAAAAgAIUacRjCFhMmhIfGJ44ms0EzR +KZJUWangRqNYKLSYIDZlWiEDSApRpS8got+fXnxFLkHGfUl8TOfT/oXnHPxlPxh2pFuWFh +OHT9hoHYDEFgu0ulhSF3jxhl6jUA3VsZV/LpoXp70KmtT5yqxUYQ6ycPGexo3R8X5bMQhJ +lz6CzfAAAB2MVcBjzFXAY8AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9Lv +FYmFFVMIuHFGlZpIL7sh3IMkqy+cssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+ +Gu48G+yFuE8l0fVVUivos/MmYVJ66qT99htcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+S +x9a5AAAAFQD0EYmTNaFJ8CS0+vFSF4nYcyEnSQAAAIEAkgLjxHJAE7qFWdTqf7EZngu7jA +GmdB9k3YzMHe1ldMxEB7zNw5aOnxjhoYLtiHeoEcOk2XOyvnE+VfhIWwWAdOiKRTEZlmiz +kvhGbq0DCe2EPMXirjqWACI5nDioQX1oEMonR8N3AEO5v9SfBqS2Q9R6OBr6lf04RvwpHZ +0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQNIClGlLyCi35 +9efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXePGGXqNQDdWxlX8u +mhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8AAAAVANXaBstzNR3EK6zSl8Q/Vt11 +pdEtAAAAAAE= +-----END OPENSSH PRIVATE KEY----- +""" + +publicDSA_lsh = decodebytes(b"""\ +e0tERXdPbkIxWW14cFl5MXJaWGtvTXpwa2MyRW9NVHB3TVRJNU9nQ1NrRHJGUkVWUTBDS1FEUngv +aVFBTXBhUFM3eFdKaFJWVENMaHhScFdhU0MrN0lkeURKS3N2bkxMQ0RUUDVaeHc5MzVyQU1pNVZG +MmJiZWp3L1M0R1VXczdEem9LYmJoL2hydVBCdnNoYmhQSmRIMVZWSXI2TFB6Sm1GU2V1cWsvZlli +WEdTbXJhMDFtWjZWVnU0QlFBYUtzUG9YdGkyZElKSGxOUGtzZld1U2tvTVRweE1qRTZBUFFSaVpN +MW9VbndKTFQ2OFZJWGlkaHpJU2RKS1NneE9tY3hNams2QUpJQzQ4UnlRQk82aFZuVTZuK3hHWjRM +dTR3QnBuUWZaTjJNekIzdFpYVE1SQWU4emNPV2pwOFk0YUdDN1loM3FCSERwTmx6c3I1eFBsWDRT +RnNGZ0hUb2lrVXhHWlpvczVMNFJtNnRBd250aER6RjRxNDZsZ0FpT1p3NHFFRjlhQkRLSjBmRGR3 +QkR1Yi9Vbndha3RrUFVlamdhK3BYOU9FYjhLUjJkRkJydktTZ3hPbmt4TWpnNkFoUnB4R01JV0V5 +YUVoOFluamlhelFUTkVwa2xSWnFlQkdvMWdvdEpnZ05tVmFJUU5JQ2xHbEx5Q2kzNTllZkVVdVFj +WjlTWHhNNTlQK2hlY2MvR1UvR0hha1c1WVdFNGRQMkdnZGdNUVdDN1M2V0ZJWGVQR0dYcU5RRGRX +eGxYOHVtaGVudlFxYTFQbktyRlJoRHJKdzhaN0dqZEh4ZmxzeENFbVhQb0xOOHBLU2s9fQ== +""") + +privateDSA_lsh = decodebytes(b"""\ +KDExOnByaXZhdGUta2V5KDM6ZHNhKDE6cDEyOToAkpA6xURFUNAikA0cf4kADKWj0u8ViYUVUwi4 +cUaVmkgvuyHcgySrL5yywg0z+WccPd+awDIuVRdm23o8P0uBlFrOw86Cm24f4a7jwb7IW4TyXR9V +VSK+iz8yZhUnrqpP32G1xkpq2tNZmelVbuAUAGirD6F7YtnSCR5TT5LH1rkpKDE6cTIxOgD0EYmT +NaFJ8CS0+vFSF4nYcyEnSSkoMTpnMTI5OgCSAuPEckATuoVZ1Op/sRmeC7uMAaZ0H2TdjMwd7WV0 +zEQHvM3Dlo6fGOGhgu2Id6gRw6TZc7K+cT5V+EhbBYB06IpFMRmWaLOS+EZurQMJ7YQ8xeKuOpYA +IjmcOKhBfWgQyidHw3cAQ7m/1J8GpLZD1Ho4GvqV/ThG/CkdnRQa7ykoMTp5MTI4OgIUacRjCFhM +mhIfGJ44ms0EzRKZJUWangRqNYKLSYIDZlWiEDSApRpS8got+fXnxFLkHGfUl8TOfT/oXnHPxlPx +h2pFuWFhOHT9hoHYDEFgu0ulhSF3jxhl6jUA3VsZV/LpoXp70KmtT5yqxUYQ6ycPGexo3R8X5bMQ +hJlz6CzfKSgxOngyMToA1doGy3M1HcQrrNKXxD9W3XWl0S0pKSk= +""") + +privateDSA_agentv3 = decodebytes(b"""\ +AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9LvFYmFFVMIuHFGlZpIL7sh3IMkqy+c +ssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+Gu48G+yFuE8l0fVVUivos/MmYVJ66qT99h +tcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+Sx9a5AAAAFQD0EYmTNaFJ8CS0+vFSF4nYcyEnSQAA +AIEAkgLjxHJAE7qFWdTqf7EZngu7jAGmdB9k3YzMHe1ldMxEB7zNw5aOnxjhoYLtiHeoEcOk2XOy +vnE+VfhIWwWAdOiKRTEZlmizkvhGbq0DCe2EPMXirjqWACI5nDioQX1oEMonR8N3AEO5v9SfBqS2 +Q9R6OBr6lf04RvwpHZ0UGu8AAACAAhRpxGMIWEyaEh8YnjiazQTNEpklRZqeBGo1gotJggNmVaIQ +NIClGlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXePGGXqNQDd +WxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8AAAAVANXaBstzNR3EK6zSl8Q/Vt11 +pdEt +""") + +# Custom code + +privateRSA_fingerprint_md5 = '85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da' + +RSAData2 = { + 'n': int('106248668575524741116943830949539894737212779118943280948138' + '20729711061576321820845393835692814935201176341295575504152775' + '16685881326038852354459895734875625093273594925884531272867425' + '864910490065695876046999646807138717162833156501'), + 'e': int(35), + 'd': int('667848773903298372735075508825679338348194611604786337388297' + '30301040958479737159599618395783408164121679859572188879144827' + '13602371850869127033494910375212470664166001439410214474266799' + '85974425203903884190893469297150446322896587555'), + 'q': int('3395694744258061291019136154000709371890447462086362702627' + '9704149412726577280741108645721676968699696898960891593323'), + 'p': int('3128922844292337321766351031842562691837301298995834258844' + '4720539204069737532863831050930719431498338835415515173887'), + 'u': int('2777403202132551568802514199893235993376771442611051821485' + '0278129927603609294283482712900532542110958095343012272938') + } + +DSAData2 = { + 'g': int("10253261326864117157640690761723586967382334319435778695" + "29171533815411392477819921538350732400350395446211982054" + "96512489289702949127531056893725702005035043292195216541" + "11525058911428414042792836395195432445511200566318251789" + "10575695836669396181746841141924498545494149998282951407" + "18645344764026044855941864175"), + 'p': int("10292031726231756443208850082191198787792966516790381991" + "77502076899763751166291092085666022362525614129374702633" + "26262930887668422949051881895212412718444016917144560705" + "45675251775747156453237145919794089496168502517202869160" + "78674893099371444940800865897607102159386345313384716752" + "18590012064772045092956919481"), + 'q': int(1393384845225358996250882900535419012502712821577), + 'x': int(1220877188542930584999385210465204342686893855021), + 'y': int("14604423062661947579790240720337570315008549983452208015" + "39426429789435409684914513123700756086453120500041882809" + "10283610277194188071619191739512379408443695946763554493" + "86398594314468629823767964702559709430618263927529765769" + "10270265745700231533660131769648708944711006508965764877" + "684264272082256183140297951") + } \ No newline at end of file diff --git a/src/chevah_keycert/tests/ssh_common_test_inc.sh b/src/chevah_keycert/tests/ssh_common_test_inc.sh new file mode 100644 index 0000000..8ceed02 --- /dev/null +++ b/src/chevah_keycert/tests/ssh_common_test_inc.sh @@ -0,0 +1,18 @@ +# Files holding passwords. +# Non-empty passwors MUST be at least 5 characters long. +# (Limitation imposed by ssh-keygen for password-protected PCKS8 keys.) +# Non-empty passwords MUST start with a letter. +# (Limitation imposed by the script testing self-generated keys.) +# Complex passwords must be at least 10 characters long. +# (Limitation imposed by the script testing self-generated keys.) + +> pass_file_empty +echo 'chevah' > pass_file_simple +echo 'V^#~(?)%&\/+-1.,="*`!>|<:$;@N' > pass_file_complex +PASS_TYPES="empty simple complex" + +> comm_file_empty +echo 'chevah' > comm_file_simple +echo ' V^#~(?)%&\/+-1. ,="*`!>|<:$;@N' > comm_file_complex +echo 'âåæāăąǎǟȁȃȧȺαἀащѝѱҡҩժݐሀᠠァぁ妈媽✯➾♤♟⚅🂠⚇𓀀😀☠️👩🏿‍🦽🦺⛈🪐🥂🏁🏴‍☠️🐈‍⬛' > comm_file_unicode +COMM_TYPES="empty simple complex unicode" diff --git a/src/chevah_keycert/tests/ssh_gen_keys_tests.sh b/src/chevah_keycert/tests/ssh_gen_keys_tests.sh new file mode 100755 index 0000000..e304cb2 --- /dev/null +++ b/src/chevah_keycert/tests/ssh_gen_keys_tests.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# +# Generate supported key types and test them with various SSH key tools. + +set -euo pipefail + +# Key types to generate and then test with puttygen, ssh-keygen, ssh-keygen-g3. +# Accepted parameters (one or more): ed25519, ecdsa, rsa, dsa. +# Generating large size RSA and DSA keys takes a lot of CPU time. +KEY_TYPES=$* +if [ -z "$KEY_TYPES" ]; then + # If no parameters given, test all key types. + KEY_TYPES="ed25519 ecdsa rsa dsa" +fi + +KEYCERT_CMD="../build-keycert/bin/python ../keycert-demo.py" +KEYCERT_FORMATS="openssh openssh_v1 putty" + +SUCCESS_FILE="gen_keys_tests_success" +ERROR_FILE="gen_keys_tests_error" + +# Empty the files holding test results, if present. +> $SUCCESS_FILE +> $ERROR_FILE + +# Common routines like setting password files. +source ../chevah/keycert/tests/ssh_common_test_inc.sh + +sort_tests_per_error(){ + local cmd_to_test=$* + local cmd_err_code + + set +e + $cmd_to_test + cmd_err_code=$? + set -e + + # Record last parameter. + if [ $cmd_err_code -eq 0 ]; then + echo "${@: -1}" >> $SUCCESS_FILE + else + echo "${@: -1}" >> $ERROR_FILE + fi +} + +puttygen_tests(){ + local priv_key=$1 + local pub_key=${1}.pub + + sort_tests_per_error puttygen -O fingerprint $pub_key + sort_tests_per_error puttygen -o /dev/null --old-passphrase pass_file_${2} -L $priv_key +} + +sshkeygen_tests(){ + local priv_key=$1 + local pub_key=${1}.pub + + sort_tests_per_error ssh-keygen -l -f $pub_key + if [ $2 = "empty" ]; then + sort_tests_per_error ssh-keygen -y -f $priv_key + else + sort_tests_per_error ssh-keygen -y -P "$(cat pass_file_${2})" -f $priv_key + fi +} + +# First parameter is the key type. +# Second (optional) parameter is the password. MUST start with a letter +keycert_gen_keys(){ + local key_size + local key_format + local key_type=$1 + local key_pass_type + local keycert_opts="ssh-gen-key --key-type $key_type" + + # Remove first parameter, the password should be now first, if existing. + shift + # Check if there is a password to be used. + if [[ "${1:0:1}" =~ [a-zA-Z] ]]; then + # First remaining parameter is the password, as it starts with a non-digit. + keycert_opts="$keycert_opts --key-password ${1} --key-comment ${1}" + # Check password type by password length. + if [ ${#1} -ge 10 ]; then + key_pass_type="complex" + else + key_pass_type="simple" + fi + shift + else + key_pass_type="empty" + fi + + for key_size in $*; do + for key_format in $KEYCERT_FORMATS; do + if [ $key_format = "openssh" -a $key_type = "ed25519" ]; then + # "Cannot serialize Ed25519 key to openssh format". + (>&2 echo "Not generating $key_type key with the $key_format format.") + continue + fi + final_keycert_opts="${keycert_opts} --key-size $key_size --key-format $key_format" + # An associated public key is also generated with same name + '.pub'. + key_file=${key_type}_${key_size}_${key_format}_${key_pass_type} + $KEYCERT_CMD ${final_keycert_opts} --key-file $key_file + # OpenSSH's tool will complain of unsafe permissions. + chmod 600 $key_file + case $key_format in + openssh*) + sshkeygen_tests $key_file $key_pass_type + ;; + putty) + puttygen_tests $key_file $key_pass_type + ;; + esac + rm $key_file ${key_file}.pub + done + done +} + + + +for pass_type in $PASS_TYPES; do + pass=$(cat pass_file_${pass_type}) + + for key in $KEY_TYPES; do + case $key in + "ed25519") + keycert_gen_keys ed25519 $pass 256 + ;; + "ecdsa") + keycert_gen_keys ecdsa $pass 256 384 521 + ;; + "rsa") + # An unusual prime size is also tested. + keycert_gen_keys rsa $pass 1024 2111 3072 4096 8192 + ;; + "dsa") + keycert_gen_keys dsa $pass 1024 2048 3072 4096 + ;; + esac + done + + rm pass_file_${pass_type} +done + +# FIXME:51: +# This doesn't support testing type of comments independently yet. +rm comm_file_* + +echo -ne "\nCombinations tested: " +cat $SUCCESS_FILE $ERROR_FILE | wc -l + +echo -ne "\nCombinations with no errors: " +cat $SUCCESS_FILE | wc -l +cat $SUCCESS_FILE +rm $SUCCESS_FILE + +echo -ne "\nCombinations with errors: " +cat $ERROR_FILE | wc -l +cat $ERROR_FILE + +if [ -s $ERROR_FILE ]; then + rm $ERROR_FILE + exit 13 +else + rm $ERROR_FILE +fi diff --git a/src/chevah_keycert/tests/ssh_load_keys_tests.sh b/src/chevah_keycert/tests/ssh_load_keys_tests.sh new file mode 100755 index 0000000..a48d91b --- /dev/null +++ b/src/chevah_keycert/tests/ssh_load_keys_tests.sh @@ -0,0 +1,308 @@ +#!/usr/bin/env bash +# +# Test loading keys generated with various SSH key generators. + +set -euo pipefail + +# Key types to generate with puttygen, ssh-keygen, ssh-keygen-g3. +# Accepted parameters (one or more): ed25519, ecdsa, rsa, dsa. +# Generating large size RSA and DSA keys takes a lot of CPU time. +KEY_TYPES=$* +if [ -z "$KEY_TYPES" ]; then + # If no parameters given, test all. + KEY_TYPES="ed25519 ecdsa dsa rsa" +fi + +KEYCERT_CMD="../build-keycert/bin/python ../keycert-demo.py" +KEYCERT_NO_ERRORS_FILE="load_keys_tests_errors_none" +KEYCERT_EXPECTED_ERRORS_FILE="load_keys_tests_errors_expected" +KEYCERT_UNEXPECTED_ERRORS_FILE="load_keys_tests_errors_unexpected" +KEYCERT_DEMOSCRIPT_ERRORS_FILE="load_keys_tests_errors_demoscript" + +# puttygen supports key type "rsa1", but it's not used here. +# private-sshcom doesn't work with ed25519 and ecdsa in puttygen 0.74. +PUTTY_PRIV_OUTPUTS="private private-openssh private-openssh-new private-sshcom" +PUTTY_PUB_OUTPUTS="public public-openssh" + +# The "default" option is more of a placeholder for not using an extra format. +OPENSSH_FORMATS="default RFC4716 PKCS8 PEM" + +TECTIA_FORMATS="secsh2 pkcs1 pkcs8 pkcs12 openssh2 openssh2-aes" +TECTIA_HASHES="sha1 sha224 sha256 sha384 sha512" + +# Empty the files holding test results, if present. +> $KEYCERT_NO_ERRORS_FILE +> $KEYCERT_EXPECTED_ERRORS_FILE +> $KEYCERT_UNEXPECTED_ERRORS_FILE +> $KEYCERT_DEMOSCRIPT_ERRORS_FILE + +# Common routines like setting password files. +source ../chevah/keycert/tests/ssh_common_test_inc.sh +# FIXME:50 +# Unicode comments are not supported. +COMM_TYPES="empty simple complex" +# FIXME:52 +# Comments starting with a blank are not supported. +COMM_TYPES="empty simple" + +# First parameter is the private or public key file. +# Second (optional) parameter is the password. +keycert_load_key(){ + local keycert_opts="ssh-load-key --file $1" + if [ "$#" = 2 ]; then + local keycert_opts="$keycert_opts --password $2" + fi + set +e + $KEYCERT_CMD $keycert_opts + local keycert_err_code=$? + set -e + if [ $keycert_err_code -eq 0 ]; then + echo $1 >> $KEYCERT_NO_ERRORS_FILE + elif [ $keycert_err_code -eq 1 ]; then + echo $1 >> $KEYCERT_EXPECTED_ERRORS_FILE + elif [ $keycert_err_code -eq 2 ]; then + echo $1 >> $KEYCERT_UNEXPECTED_ERRORS_FILE + elif [ $keycert_err_code -eq 3 ]; then + echo $1 >> $KEYCERT_DEMOSCRIPT_ERRORS_FILE + else + (>&2 echo "Unexpected error code: $keycert_err_code") + exit 42 + fi +} + +putty_keys_test(){ + local bit_lengths="$1" + local pass_type + local pass_file + local priv_key_file + local pub_key_file + local pub_output + + for bits in $bit_lengths; do + for pass_type in $PASS_TYPES; do + for comm_type in $COMM_TYPES; do + echo -n "Generating $KEY key of type $PUTTY_PRIV_OUTPUT and size $bits" + echo " with $pass_type password and $comm_type comment:" + priv_key_file="putty_${KEY}_${bits}_${PUTTY_PRIV_OUTPUT}_${pass_type}pass_${comm_type}comm" + pass_file="pass_file_${pass_type}" + comm_file="comm_file_${comm_type}" + puttygen --random-device /dev/random -C "$(cat $comm_file)" --new-passphrase $pass_file \ + -t $KEY -O $PUTTY_PRIV_OUTPUT -b $bits -o $priv_key_file + keycert_load_key $priv_key_file $(cat $pass_file) + # Extract/test public key in all supported public formats, but only when: + # 1) The private key is in Putty's own format. + # 2) The complex password is used. + if [ "$PUTTY_PRIV_OUTPUT" = "private" -a $pass_type = "complex" ]; then + for pub_output in $PUTTY_PUB_OUTPUTS; do + pub_key_file="putty_${KEY}_${bits}_${pub_output}_${pass_type}pass_${comm_type}comm" + puttygen --old-passphrase $pass_file -O $pub_output -o $pub_key_file $priv_key_file + keycert_load_key $pub_key_file + rm $pub_key_file + done + fi + rm $priv_key_file + done + done + done +} + +openssh_format_set(){ + if [ $format != "default" ]; then + OPENSSH_OPTS="$OPENSSH_OPTS -m $format" + fi +} + +openssh_keys_test(){ + local bit_lengths="$1" + local pass_type + local pass_file + local format + local priv_key_file + local pub_key_file + + for bits in $bit_lengths; do + for pass_type in $PASS_TYPES; do + pass_file="pass_file_${pass_type}" + for comm_type in $COMM_TYPES; do + comm_file="comm_file_${comm_type}" + for format in $OPENSSH_FORMATS; do + priv_key_file=openssh_${KEY}_${bits}_${format}_${pass_type}pass_${comm_type}comm + pub_key_file=$priv_key_file.pub + if [ $pass_type = "empty" ]; then + if [ $format = "PKCS8" ]; then + if [ $KEY = "ecdsa" -o $KEY = "rsa" -o $KEY = "dsa" ]; then + # Minimum 5 characters required for these combinations. + (>&2 echo "Not generating $format $KEY key with $pass_type password.") + continue + fi + fi + OPENSSH_OPTS="" + openssh_format_set + ssh-keygen -C "$(cat $comm_file)" -t $KEY -b $bits $OPENSSH_OPTS -f $priv_key_file -N "" + else + OPENSSH_OPTS="-N $(cat $pass_file)" + openssh_format_set + ssh-keygen -C "$(cat $comm_file)" -t $KEY -b $bits $OPENSSH_OPTS -f $priv_key_file + fi + keycert_load_key $priv_key_file $(cat $pass_file) + keycert_load_key $pub_key_file + rm $priv_key_file $pub_key_file + done + done + done + done +} + +tectia_keys_test(){ + local bit_lengths="$1" + local pass_type + local pass_file + local format + local fips_mode + local priv_key_file + local pub_key_file + local gen_opts + + for bits in $bit_lengths; do + # FIXME:53 + # Tectia tests are currently disabled. + break + for pass_type in $PASS_TYPES; do + pass_file="pass_file_${pass_type}" + for comm_type in $COMM_TYPES; do + comm_file="comm_file_${comm_type}" + for format in $TECTIA_FORMATS; do + for fips_mode in nofips fips; do + if [ $fips_mode = "fips" -a $KEY = "ed25519" ]; then + continue + elif [ $fips_mode = "fips" -a $pass_type = "empty" ]; then + continue + elif [ $fips_mode = "fips" -a "${format%openssh2*}" = "" ]; then + # "OpenSSH2 keys operations are forbidden when in FIPS mode." + continue + fi + for hash in $TECTIA_HASHES; do + gen_opts="-b $bits -t $KEY --key-format $format --key-hash $hash" + if [ $fips_mode = "fips" ]; then + gen_opts="$gen_opts --fips-mode" + fi + priv_key_file=tectia_${KEY}_${bits}_${format}_${hash}_${fips_mode}_${pass_type}_${comm_type} + pub_key_file=$priv_key_file.pub + if [ $pass_type = "empty" ]; then + ssh-keygen-g3 -c "$(cat $comm_file)" $gen_opts -P $(pwd)/$priv_key_file + else + ssh-keygen-g3 -c "$(cat $comm_file)" $gen_opts -p $(cat $pass_file) $(pwd)/$priv_key_file + fi + keycert_load_key $priv_key_file $(cat $pass_file) + keycert_load_key $pub_key_file + rm $priv_key_file $pub_key_file + done + done + done + done + done + done +} + + + +# Putty's puttygen tests. +for KEY in $KEY_TYPES; do + for PUTTY_PRIV_OUTPUT in $PUTTY_PRIV_OUTPUTS; do + if [ $KEY = "ed25519" -a $PUTTY_PRIV_OUTPUT = "private-openssh-new" ]; then + # No need to force new OpenSSH format for ED25519 keys. + continue + fi + if [ $PUTTY_PRIV_OUTPUT = "private-sshcom" ]; then + if [ $KEY = "ed25519" -o $KEY = "ecdsa" ]; then + # Not working in puttygen 0.74. + continue + fi + fi + # Test specific numbers of bits per key type. + case $KEY in + "ed25519") + putty_keys_test "256" + ;; + "ecdsa") + putty_keys_test "256 384 521" + ;; + "rsa") + putty_keys_test "512 2048 4096" + ;; + "dsa") + # An unusual prime size is also tested. + putty_keys_test "2111 3072 4096" + ;; + esac + done +done + +# OpenSSH's ssh-keygen tests. +for KEY in $KEY_TYPES; do + case $KEY in + "ed25519") + openssh_keys_test "256" + ;; + "ecdsa") + openssh_keys_test "256 384 521" + ;; + "rsa") + # An unusual prime size is also tested. + openssh_keys_test "1024 2111 3072 8192" + ;; + "dsa") + openssh_keys_test "1024" + ;; + esac +done + +# Tectia's ssh-keygen-g3 tests. +for KEY in $KEY_TYPES; do + case $KEY in + "ed25519") + tectia_keys_test "256" + ;; + "ecdsa") + tectia_keys_test "256 384 521" + ;; + "rsa") + tectia_keys_test "512 1024 2048 3072 4096 8192" + ;; + "dsa") + tectia_keys_test "1024 2048 3072 4096" + ;; + esac +done + +# Cleanup test files. +rm pass_file_* comm_file_* + +echo -ne "\nCombinations tested: " +cat $KEYCERT_NO_ERRORS_FILE $KEYCERT_EXPECTED_ERRORS_FILE $KEYCERT_UNEXPECTED_ERRORS_FILE | wc -l + +echo -ne "\nCombinations with no errors: " +cat $KEYCERT_NO_ERRORS_FILE | wc -l +cat $KEYCERT_NO_ERRORS_FILE +rm $KEYCERT_NO_ERRORS_FILE + +echo -ne "\nCombinations with demo script errors: " +cat $KEYCERT_DEMOSCRIPT_ERRORS_FILE | wc -l +cat $KEYCERT_DEMOSCRIPT_ERRORS_FILE +rm $KEYCERT_DEMOSCRIPT_ERRORS_FILE + +echo -ne "\nCombinations with expected errors: " +cat $KEYCERT_EXPECTED_ERRORS_FILE | wc -l +cat $KEYCERT_EXPECTED_ERRORS_FILE +rm $KEYCERT_EXPECTED_ERRORS_FILE + +echo -ne "\nCombinations with unexpected errors: " +cat $KEYCERT_UNEXPECTED_ERRORS_FILE | wc -l +cat $KEYCERT_UNEXPECTED_ERRORS_FILE + +if [ -s $KEYCERT_UNEXPECTED_ERRORS_FILE ]; then + rm $KEYCERT_UNEXPECTED_ERRORS_FILE + exit 13 +else + rm $KEYCERT_UNEXPECTED_ERRORS_FILE +fi diff --git a/src/chevah_keycert/tests/test_exceptions.py b/src/chevah_keycert/tests/test_exceptions.py new file mode 100644 index 0000000..f92f379 --- /dev/null +++ b/src/chevah_keycert/tests/test_exceptions.py @@ -0,0 +1,35 @@ +# Copyright (c) 2015 Adi Roiban. +# See LICENSE for details. +""" +Test for exceptions raise by this package. +""" +from __future__ import absolute_import +from chevah_compat.testing import mk, ChevahTestCase + +from chevah_keycert.exceptions import KeyCertException + + +class TestExceptions(ChevahTestCase): + """ + Test for exceptions + """ + + def test_KeyCertException(self): + """ + It provides a message. + """ + message = mk.string() + + error = KeyCertException(message) + + self.assertEqual(message, error.message) + + def test_KeyCertException_str(self): + """ + The message is the string serialization. + """ + message = mk.string() + + error = KeyCertException(message) + + self.assertEqual(message.encode('utf-8'), str(error)) diff --git a/src/chevah_keycert/tests/test_ssh.py b/src/chevah_keycert/tests/test_ssh.py new file mode 100644 index 0000000..5d9929c --- /dev/null +++ b/src/chevah_keycert/tests/test_ssh.py @@ -0,0 +1,2898 @@ +# Copyright (c) 2014 Adi Roiban. +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. +""" +Test for SSH keys management. +""" +from __future__ import absolute_import, division, unicode_literals + +from argparse import ArgumentParser +from StringIO import StringIO +import base64 +import textwrap + +from chevah_compat.testing import mk, ChevahTestCase +from nose.plugins.attrib import attr + +# Twisted test compatibility. +from chevah_keycert import ssh as keys, common, sexpy, _path +from chevah_keycert.exceptions import ( + BadKeyError, + KeyCertException, + EncryptedKeyError, + ) +from chevah_keycert.ssh import ( + Key, + generate_ssh_key, + generate_ssh_key_parser, + ) +from chevah_keycert.tests import keydata +from chevah_keycert.tests.helpers import CommandLineMixin + + +OPENSSH_RSA_PRIVATE = (b'''-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKAPkPAWzlu5BRHcmA +u0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAfp1YxCR +9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLwIDAQAB +AoGACB5cQDvxmBdgYVpuy43DduabTmR71HFaNFl+nE5vwFxUqX0qFOQpG0E2Cv56 +zesPzT1JWBiqffSir4iSjH/lnskZnM9J1xfpnoJ5HTzcGHaBYVFEEXS6fOsyWT15 +oY7Kb6rRBTnWV0Ins/05Hhp38r/RR/O4poB+3NwQJDl/6gECQQDoAnRdC+5SyjrZ +1JQUWUkapiYHIhFq6kWtGm3kWJn0IxCBtFhGvqIWJwZIAjf6tTKMUk6bjG9p7Jpe +tXUsTiDBAkEAy5EDU2F42Xm6tvQzM8bAgq7d2/x2iHRuOkDUb1bK3YwByTihl9BL +qvdRhRxpl21EcqWpB/RzAFbGa+60G/iV7wJABSz415KKkII+admaLBIJ1XRbaNFT +viTXxRLP3MY1OQMHPT1+sqVSDFh2hWi3QvqD1CmJ42JwodZLY018/a4IgQJAOsCg +yBjyyznB9PnoKUJs34rex5ZHE70e7zs01Omk5Wp6PXxVzz40CKUW5yc7JpRH1BsR +/RTFeEyTOiWL4CLQCwJAf4BF9eVLxRQ9A4Mm9Ikt4lF8ii6na4nxdtEzP8p2LP9t +LqHYUobNanxB+7Msi4f3gYyuKdOGnWHqD2U4HcLdMQ== +-----END RSA PRIVATE KEY-----''') + +# Converted from old format using OpenSSH without a password. +# $ ssh-keygen -e -p -f OPENSSH_RSA_PRIVATE.key +OPENSSH_V1_RSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAIEAuH1erUmpA0gemaL8oC7H9YgvT97SR3j0ygD5DwFs5buQUR3JgLtL +k45+xoa6cW441jMA1TdDsc8O7XiNDhCFZKZ5XMjOi+ZhpPl+i3OPpQH6dWMQkfaPfhVzFk +iT99o0cCPuC4VmMZ2FJXbwDuSw8/UHkdMiopsHI0U6Bev46S8AAAH4y/dH2sv3R9oAAAAH +c3NoLXJzYQAAAIEAuH1erUmpA0gemaL8oC7H9YgvT97SR3j0ygD5DwFs5buQUR3JgLtLk4 +5+xoa6cW441jMA1TdDsc8O7XiNDhCFZKZ5XMjOi+ZhpPl+i3OPpQH6dWMQkfaPfhVzFkiT +99o0cCPuC4VmMZ2FJXbwDuSw8/UHkdMiopsHI0U6Bev46S8AAAADAQABAAAAgAgeXEA78Z +gXYGFabsuNw3bmm05ke9RxWjRZfpxOb8BcVKl9KhTkKRtBNgr+es3rD809SVgYqn30oq+I +kox/5Z7JGZzPSdcX6Z6CeR083Bh2gWFRRBF0unzrMlk9eaGOym+q0QU51ldCJ7P9OR4ad/ +K/0UfzuKaAftzcECQ5f+oBAAAAQH+ARfXlS8UUPQODJvSJLeJRfIoup2uJ8XbRMz/Kdiz/ +bS6h2FKGzWp8QfuzLIuH94GMrinThp1h6g9lOB3C3TEAAABBAOgCdF0L7lLKOtnUlBRZSR +qmJgciEWrqRa0abeRYmfQjEIG0WEa+ohYnBkgCN/q1MoxSTpuMb2nsml61dSxOIMEAAABB +AMuRA1NheNl5urb0MzPGwIKu3dv8doh0bjpA1G9Wyt2MAck4oZfQS6r3UYUcaZdtRHKlqQ +f0cwBWxmvutBv4le8AAAAAAQID +-----END OPENSSH PRIVATE KEY-----''') + +# Converted from old format using OpenSSH with `test` as password. +# $ ssh-keygen -e -p -f OPENSSH_RSA_PRIVATE.key +OPENSSH_V1_ENCRYPTED_RSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCO5u6Nze +CPk3e+vkL9MmvWAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakD +SB6ZovygLsf1iC9P3tJHePTKAPkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI +0OEIVkpnlcyM6L5mGk+X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz +9QeR0yKimwcjRToF6/jpLwAAAgDuID/fk0osaBUXQ+M32lA677YjC9BX5bSwKHNdbaH/eD +H5T4mNZNe8IvZXsYGsVXKT5yaRP/19A/5pVivnTn2n0dOZ0tbfnqrPLJnEdPPTlLVv+YaR ++TZYRYfydOXpZ44MsJAzmOmCWVIlDNratEt/zoiqhF2T3q4ODFEABfDQ3LixRx+Jk90icy +FrL7DuDLsTdjXLnmUSh7Ytzd9v8XrQ8ku98EvOzqCCneYguYt2zHrRVd+jWivJ7Pdv86lg +kksqxIlY7TV+wqcbYvLDuZF6iP3jWAGoQYSUJpqVwp0PLz53hzxwcLMEg+V93e9fYiQjsE +psoQ/y8ZGmBIGqkAj+BC9Y6DXFPmstv0yHlSoB/A4FwVerZiVu4G239LF8Wt6gfAU7Bu7j +yvWKic87GsONUvp8iKFntCFgeX4aa9bVsl4N9APzEBPsj2ni4E3+UYYovGBo8jlmxBAj3V +evUSgiQfOTIM8UkZfk6plXchJTmshIeL1SMyjdNF2ziVh72T1RCOs/905gXXvw+Bl+zdtJ +5sRcoQii4HcPjK0WUZaSM/5LsxSsqDt+nBVoaq7k24ITTjXdHIuiT1YnKFjErzD3bznosW +wNe7YoLXxnuszUFaBAWthJuOsE1JVAScqo7oClPc1CHX8qEZz5vihkEploAOGe0hj5Kjt6 +vLDBLhI7ag== +-----END OPENSSH PRIVATE KEY-----''') + +OPENSSH_RSA_PUBLIC = ( + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKA' + 'PkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAf' + 'p1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLw==' + ) + +PKCS1_RSA_PUBLIC = (b'''-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBALh9Xq1JqQNIHpmi/KAux/WIL0/e0kd49MoA+Q8BbOW7kFEdyYC7S5OO +fsaGunFuONYzANU3Q7HPDu14jQ4QhWSmeVzIzovmYaT5fotzj6UB+nVjEJH2j34V +cxZIk/faNHAj7guFZjGdhSV28A7ksPP1B5HTIqKbByNFOgXr+OkvAgMBAAE= +-----END RSA PUBLIC KEY-----''') + +OPENSSH_DSA_PRIVATE = (b'''-----BEGIN DSA PRIVATE KEY----- +MIIBugIBAAKBgQDOwkKGnmVZ9bRl7ZCn/wSELV0n5ELsqVZFOtBpHleEOitsvjEB +BbTKX0fZ83vaMVnJFVw3DQSbi192krvk909Y6h3HVO2MKBRd9t29fr26VvCZQOxR +4fzkPuL+Px4+ShqE171sOzsuEDt0Mkxf152QxrA2vPowkj7fmzRH5xgDTQIVAIYb +/ljSUclo6TiNwoiF+9byafFJAoGAXA+TAGCmF2ZeNZN04mgxeyT34IAw37NGmLLP +/byi86dKcdz5htqPiOWcNmFzrA7a0o+erE3B+miwEm2sVz+eVWfNOCJQalHUqRrk +1iV542FL0BCePiJa91Baw4pVS5hnSNko/Wsp0VnW3q5OK/tPs1pRy+3qWUwwrg5i +zhYkBfwCgYB/6sL9MO4ZwtFzwbOKNOoZxfORwNbzzHf+IpzyBTxxQJcYS6QgbtSi +2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3pZ7SnoRJ7vATQnMJpwZYno +8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk204gIUTk7aRv65 +oTedYsAyi80L8phYBN4= +-----END DSA PRIVATE KEY-----''') + +# Converted from old format using OpenSSH without a password. +# $ ssh-keygen -e -p -f OPENSSH_DSA_PRIVATE.key +OPENSSH_V1_DSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH +NzAAAAgQDOwkKGnmVZ9bRl7ZCn/wSELV0n5ELsqVZFOtBpHleEOitsvjEBBbTKX0fZ83va +MVnJFVw3DQSbi192krvk909Y6h3HVO2MKBRd9t29fr26VvCZQOxR4fzkPuL+Px4+ShqE17 +1sOzsuEDt0Mkxf152QxrA2vPowkj7fmzRH5xgDTQAAABUAhhv+WNJRyWjpOI3CiIX71vJp +8UkAAACAXA+TAGCmF2ZeNZN04mgxeyT34IAw37NGmLLP/byi86dKcdz5htqPiOWcNmFzrA +7a0o+erE3B+miwEm2sVz+eVWfNOCJQalHUqRrk1iV542FL0BCePiJa91Baw4pVS5hnSNko +/Wsp0VnW3q5OK/tPs1pRy+3qWUwwrg5izhYkBfwAAACAf+rC/TDuGcLRc8GzijTqGcXzkc +DW88x3/iKc8gU8cUCXGEukIG7UotrVGNVnicZqv8ZDKFS4KZqSujTfjbgemrseeFO4eQd6 +We0p6ESe7wE0JzCacGWJ6PLmV4ceE5jqEsc8cto0yW+K7eEem28I2iPXGv9Yi2r6HENtfG +ZNtOIAAAHYZ8aTg2fGk4MAAAAHc3NoLWRzcwAAAIEAzsJChp5lWfW0Ze2Qp/8EhC1dJ+RC +7KlWRTrQaR5XhDorbL4xAQW0yl9H2fN72jFZyRVcNw0Em4tfdpK75PdPWOodx1TtjCgUXf +bdvX69ulbwmUDsUeH85D7i/j8ePkoahNe9bDs7LhA7dDJMX9edkMawNrz6MJI+35s0R+cY +A00AAAAVAIYb/ljSUclo6TiNwoiF+9byafFJAAAAgFwPkwBgphdmXjWTdOJoMXsk9+CAMN ++zRpiyz/28ovOnSnHc+Ybaj4jlnDZhc6wO2tKPnqxNwfposBJtrFc/nlVnzTgiUGpR1Kka +5NYleeNhS9AQnj4iWvdQWsOKVUuYZ0jZKP1rKdFZ1t6uTiv7T7NaUcvt6llMMK4OYs4WJA +X8AAAAgH/qwv0w7hnC0XPBs4o06hnF85HA1vPMd/4inPIFPHFAlxhLpCBu1KLa1RjVZ4nG +ar/GQyhUuCmakro03424Hpq7HnhTuHkHelntKehEnu8BNCcwmnBliejy5leHHhOY6hLHPH +LaNMlviu3hHptvCNoj1xr/WItq+hxDbXxmTbTiAAAAFE5O2kb+uaE3nWLAMovNC/KYWATe +AAAAAAECAw== +-----END OPENSSH PRIVATE KEY-----''') + +# Converted from old format using OpenSSH with `test` as the password. +# $ ssh-keygen -e -p -f OPENSSH_DSA_PRIVATE.key +OPENSSH_V1_ENCRYPTED_DSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCR+DbQqo +2salfbIh0HztjEAAAAEAAAAAEAAAGxAAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/ +BIQtXSfkQuypVkU60GkeV4Q6K2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHc +dU7YwoFF323b1+vbpW8JlA7FHh/OQ+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCS +Pt+bNEfnGANNAAAAFQCGG/5Y0lHJaOk4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaD +F7JPfggDDfs0aYss/9vKLzp0px3PmG2o+I5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804 +IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLmGdI2Sj9aynRWdberk4r+0+zWlHL7epZTD +CuDmLOFiQF/AAAAIB/6sL9MO4ZwtFzwbOKNOoZxfORwNbzzHf+IpzyBTxxQJcYS6QgbtSi +2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3pZ7SnoRJ7vATQnMJpwZYno8uZXhx +4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk204gAAAeBVUr2hdw/PN3S0QUwq +Ny7fOtmBVyuhRDvlS7OTsCaOs4cPF3j9o8K56Fk2Fdj69G8g56/2NrRPHvGyCtoN4olKwZ +Cc/MsePe0R7vWumVgTt1kDk6/CcnAUnTtCL7GW7a1w+8ZDwBotCZgznDD9NlnhfH0g0MZ9 +eLP4UY181lYC6452fy8E2pV9qyYufRnRYe5Gu0zoRjEuyYDbNzDBCU4WZ4O7InJDiHuVVE +hocQSVu4WzfABuCageM2wCkbKeM0mRZw1jljhO8a/T45wLmoYQxnUYFeUkUuy4akn5/uJ2 +xvIn3zl6fCqiWAnwbRjZeBfQ7q+5E/jUrUklGyBeEMn2RNo9kYTEOItuj6j8bXYELsTyjH +tJ8DplDkNN3/FYG+D8JYyhuaGd4cSLtjXS95nuazHvwyb60CQxPwbmUcojqsrM65Yu7+dQ +wwYEpG5w9/IlKJ62JmEqhEVMI4HHyDLcocYlU6OoD1Ivy09dcIO8uRBYc9jFccj/1ej5oI +tn6RsW0HRlVx06tbp6RDHBfAdg5suu5pW9uv2tESbEqpMHt4FQgqKcSQwzYLvo/bfPuxs0 +HNOQMLNwRg8yYbCG+u2HU9YTlQdTgG/5h+eYsQLObPU+TjYgS5p6sUZCkTCnOz8= +-----END OPENSSH PRIVATE KEY-----''') + + +OPENSSH_DSA_PUBLIC = ( + 'ssh-dss AAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/BIQtXSfkQuypVkU60GkeV4Q6K' + '2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU7YwoFF323b1+vbpW8JlA7FHh/O' + 'Q+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCSPt+bNEfnGANNAAAAFQCGG/5Y0lHJaOk' + '4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaDF7JPfggDDfs0aYss/9vKLzp0px3PmG2o+I' + '5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLm' + 'GdI2Sj9aynRWdberk4r+0+zWlHL7epZTDCuDmLOFiQF/AAAAIB/6sL9MO4ZwtFzwbOKNOoZxf' + 'ORwNbzzHf+IpzyBTxxQJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3p' + 'Z7SnoRJ7vATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk20' + '4g==' + ) + +# Same key as OPENSSH_RSA_PUBLIC, wrapped at 70 characters. +SSHCOM_RSA_PUBLIC = b"""---- BEGIN SSH2 PUBLIC KEY ---- +AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKAPkPAW +zlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAfp1 +YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLw== +---- END SSH2 PUBLIC KEY ----""" + +# Same key as OPENSSH_DSA_PUBLIC. +SSHCOM_DSA_PUBLIC = b"""---- BEGIN SSH2 PUBLIC KEY ---- +AAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/BIQtXSfkQuypVkU60GkeV4Q6K2y+MQ +EFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU7YwoFF323b1+vbpW8JlA7FHh/OQ+ +4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCSPt+bNEfnGANNAAAAFQCGG/5Y0lHJaO +k4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaDF7JPfggDDfs0aYss/9vKLzp0px3PmG +2o+I5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UF +rDilVLmGdI2Sj9aynRWdberk4r+0+zWlHL7epZTDCuDmLOFiQF/AAAAIB/6sL9MO4ZwtFz +wbOKNOoZxfORwNbzzHf+IpzyBTxxQJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB +6aux54U7h5B3pZ7SnoRJ7vATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca +/1iLavocQ218Zk204g== +---- END SSH2 PUBLIC KEY ----""" + +# Same as OPENSSH_RSA_PRIVATE +SSHCOM_RSA_PRIVATE_NO_PASSWORD = b"""---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- +P2/56wAAAi4AAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0e3JzYS +1wa2NzMXYyLW9hZXB9fQAAAARub25lAAAB3wAAAdsAAAARAQABAAAD+QgeXEA78ZgXYGFa +bsuNw3bmm05ke9RxWjRZfpxOb8BcVKl9KhTkKRtBNgr+es3rD809SVgYqn30oq+Ikox/5Z +7JGZzPSdcX6Z6CeR083Bh2gWFRRBF0unzrMlk9eaGOym+q0QU51ldCJ7P9OR4ad/K/0Ufz +uKaAftzcECQ5f+oBAAAD+bh9Xq1JqQNIHpmi/KAux/WIL0/e0kd49MoA+Q8BbOW7kFEdyY +C7S5OOfsaGunFuONYzANU3Q7HPDu14jQ4QhWSmeVzIzovmYaT5fotzj6UB+nVjEJH2j34V +cxZIk/faNHAj7guFZjGdhSV28A7ksPP1B5HTIqKbByNFOgXr+OkvAAAB+X+ARfXlS8UUPQ +ODJvSJLeJRfIoup2uJ8XbRMz/Kdiz/bS6h2FKGzWp8QfuzLIuH94GMrinThp1h6g9lOB3C +3TEAAAH5y5EDU2F42Xm6tvQzM8bAgq7d2/x2iHRuOkDUb1bK3YwByTihl9BLqvdRhRxpl2 +1EcqWpB/RzAFbGa+60G/iV7wAAAfnoAnRdC+5SyjrZ1JQUWUkapiYHIhFq6kWtGm3kWJn0 +IxCBtFhGvqIWJwZIAjf6tTKMUk6bjG9p7JpetXUsTiDB +---- END SSH2 ENCRYPTED PRIVATE KEY ----""" + +# Same as OPENSSH_RSA_PRIVATE and with 'chevah' password. +SSHCOM_RSA_PRIVATE_WITH_PASSWORD = ( + b"""---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- +P2/56wAAAjMAAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0e3JzYS +1wa2NzMXYyLW9hZXB9fQAAAAgzZGVzLWNiYwAAAeAqUfFcnQIi4HEOAvAoJp8nIsw3WZMc +MhWiSWenwY0tKZPxngo1s2p8QkIclw0Tu7twvtG2zABb4x/jfyqLPc5brvBdYiAXMg1xPS +xzJ7gmaYLbAJEeQxdzPqXmxJXvxSwElYhozCFHpTYm56PYBONUSbV2ORCA4eEn9VjFRxqX +Q/XQ433aF4ZlnCVl+tCJRxhfjDTw/p5jfVETVwqdm7XCM2rGYvHxqn5uUxOl+jUorDtPHu +aPZGuKND1rGWSve8p9RA662P/M6HNHMq5w5mEKKc6aOikSFWwFe3vKZ3nE1WtXEvE2bgBD +1rvYLBp9tFx4U3uQAMxvVQAeyYNeK9Qt11IMg7+seskBmVQNXo2h3Wbn8TRUxSscgQNfnm +BnNIQQbiaMEk1Em8K2I5L+DRrcOzSvkVBNguOaiLCuSbP4f4JkAvD743scRFrT3QgCdjqr +4FHJG/z/D7dEbeC3mJfXFrM7PgCGFx9L6/FqLC+piJmyEq8nggkg9P0o+oJ7/c/xGU7at9 +BsDKrM0FEXc8bFp39e8BNRbikCD61zfFp7B1s64y1mmqJkDYe2pH7FUA9mbC3vv6YM9tsY +fWGAGt8dHGIMM6MrzZYr8xJLwdmPDwAtFt2GR1Y8M0vnw6WtoL4= +---- END SSH2 ENCRYPTED PRIVATE KEY ----""") + +SSHCOM_DSA_PRIVATE_NO_PASSWORD = b"""---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- +P2/56wAAAgIAAAAmZGwtbW9kcHtzaWdue2RzYS1uaXN0LXNoYTF9LGRoe3BsYWlufX0AAA +AEbm9uZQAAAcQAAAHAAAAAAAAAA/nOwkKGnmVZ9bRl7ZCn/wSELV0n5ELsqVZFOtBpHleE +OitsvjEBBbTKX0fZ83vaMVnJFVw3DQSbi192krvk909Y6h3HVO2MKBRd9t29fr26VvCZQO +xR4fzkPuL+Px4+ShqE171sOzsuEDt0Mkxf152QxrA2vPowkj7fmzRH5xgDTQAAA/lcD5MA +YKYXZl41k3TiaDF7JPfggDDfs0aYss/9vKLzp0px3PmG2o+I5Zw2YXOsDtrSj56sTcH6aL +ASbaxXP55VZ804IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLmGdI2Sj9aynRWdberk4r ++0+zWlHL7epZTDCuDmLOFiQF/AAAAJmGG/5Y0lHJaOk4jcKIhfvW8mnxSQAAA/l/6sL9MO +4ZwtFzwbOKNOoZxfORwNbzzHf+IpzyBTxxQJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6 +NN+NuB6aux54U7h5B3pZ7SnoRJ7vATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbw +jaI9ca/1iLavocQ218Zk204gAAAJlOTtpG/rmhN51iwDKLzQvymFgE3g== +---- END SSH2 ENCRYPTED PRIVATE KEY ----""" + +# Same as OPENSSH_RSA_PRIVATE +# Make sure it has Windows newlines. +PUTTY_RSA_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-rsa\r +Encryption: none\r +Comment: imported-openssh-key\r +Public-Lines: 4\r +AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK\r +APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk\r ++X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj\r +RToF6/jpLw==\r +Private-Lines: 8\r +AAAAgAgeXEA78ZgXYGFabsuNw3bmm05ke9RxWjRZfpxOb8BcVKl9KhTkKRtBNgr+\r +es3rD809SVgYqn30oq+Ikox/5Z7JGZzPSdcX6Z6CeR083Bh2gWFRRBF0unzrMlk9\r +eaGOym+q0QU51ldCJ7P9OR4ad/K/0UfzuKaAftzcECQ5f+oBAAAAQQDoAnRdC+5S\r +yjrZ1JQUWUkapiYHIhFq6kWtGm3kWJn0IxCBtFhGvqIWJwZIAjf6tTKMUk6bjG9p\r +7JpetXUsTiDBAAAAQQDLkQNTYXjZebq29DMzxsCCrt3b/HaIdG46QNRvVsrdjAHJ\r +OKGX0Euq91GFHGmXbURypakH9HMAVsZr7rQb+JXvAAAAQH+ARfXlS8UUPQODJvSJ\r +LeJRfIoup2uJ8XbRMz/Kdiz/bS6h2FKGzWp8QfuzLIuH94GMrinThp1h6g9lOB3C\r +3TE=\r +Private-MAC: 7630b86be300c6302ce1390fb264487bb61e67ce""" + +# Same as OPENSSH_RSA_PRIVATE, with 'chevah' password. +PUTTY_RSA_PRIVATE_WITH_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-rsa\r +Encryption: aes256-cbc\r +Comment: imported-openssh-key\r +Public-Lines: 4\r +AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK\r +APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk\r ++X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj\r +RToF6/jpLw==\r +Private-Lines: 8\r +dqtZBETu8cK9VpOX/IB9iIehQE7r6ceVvzsDqrjwGnw64LkEoqlqobP7diV3/gpc\r +b1Vmf8EitczdQBUdWkVtSJVA7FYBUNQlBd4ghkDJm58goTVzdGxpoafpQ9nFNO72\r +iQFg1wfpJQn9fcR0vQL1s5uykCSeEy232rHeFO4tMssq4xrhLqK9vWaYilWJoBxM\r +jzmVdL04QJERTJXh7k3wsRWGO12r+PGnp/8upiHHfnjVZlzDw6Dw6WQ+EaqI99mm\r +Cgo4ZiBwubHtPZq+eeP8Db/m3lMaKQNKAyYe3VlKCUwkC8N4jZR8QQlaOjBfHfPR\r +vO+Znb71OYvwFHQbwA3K64M9KnWCdXZxdCrBvm2UuEcKBz7SDEXQV2UvtGueg0s0\r +EO5R1D0fXky8HGA6VciUGR6g2zclO6rNR+Ooc5ThsZQ9sKVrpcvYYC8WdZ5LB50B\r +J8IuFywygVI4PbRs98v9Dg==\r +Private-MAC: 3ffe2587759ff8f50c6acdcad44f62a67e88ef2b""" + +# This is the same key as OPENSSH_DSA_PRIVATE +PUTTY_DSA_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-dss\r +Encryption: none\r +Comment: imported-openssh-key\r +Public-Lines: 10\r +AAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/BIQtXSfkQuypVkU60GkeV4Q6\r +K2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU7YwoFF323b1+vbpW\r +8JlA7FHh/OQ+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCSPt+bNEfnGANN\r +AAAAFQCGG/5Y0lHJaOk4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaDF7JPfg\r +gDDfs0aYss/9vKLzp0px3PmG2o+I5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804\r +IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLmGdI2Sj9aynRWdberk4r+0+zWlHL\r +7epZTDCuDmLOFiQF/AAAAIB/6sL9MO4ZwtFzwbOKNOoZxfORwNbzzHf+IpzyBTxx\r +QJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3pZ7SnoRJ7v\r +ATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk20\r +4g==\r +Private-Lines: 1\r +AAAAFE5O2kb+uaE3nWLAMovNC/KYWATe\r +Private-MAC: 1b98c142780beaa5555ad5c23a0469e36f24b6f9""" + +PUTTY_ED25519_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-ed25519\r +Encryption: none\r +Comment: ed25519-key-20210106\r +Public-Lines: 2\r +AAAAC3NzaC1lZDI1NTE5AAAAIEjwKguKHPrqp3UEqSP7XTmOhBavcTxkHwnzQveQ\r +2MGG\r +Private-Lines: 1\r +AAAAINWl263e/oNph4x7jM94kE7BaSNcXD7G6bbWatylw61A\r +Private-MAC: ead2308fe2f6be87941f17e9d61ede28da2cde8a\r +""" + +# Password is: chevah +PUTTY_ED25519_PRIVATE_WITH_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-ed25519\r +Encryption: aes256-cbc\r +Comment: ed25519-key-20210106\r +Public-Lines: 2\r +AAAAC3NzaC1lZDI1NTE5AAAAIKY6CzyQPkESUswMjxdbK7XgpfAExYRc0ydzwzco\r +bmlL\r +Private-Lines: 1\r +jvO/yHUJlgjCCzEFlkYwDeSIYggO3Ry1/iP1lm49BU6GljU/miaUemDBHT9umt0o\r +Private-MAC: 6b753f6180f48d153a700c6734b46b2e52f1f7e9\r +""" + +PUTTY_ECDSA_SHA2_NISTP256_PRIVATE_NO_PASSWORD = ( +b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp256\r +Encryption: none\r +Comment: ecdsa-key-20210106\r +Public-Lines: 3\r +AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPA3+gjOpajd\r +9iRVm72ArvQfjVW+3bz9IMrPNMIANSmwTj+0NuFgXZGLaxT8BKslZLZvJX+XuUr/\r +Yvgn32oS7Iw=\r +Private-Lines: 1\r +AAAAIDe7fQUAaorrEkedXTSmXrCY4vabtFV7e4Z8xBSvty8Q\r +Private-MAC: a84b17c5dead6fed8f474406929312d45c096dfc\r +""") + +PUTTY_ECDSA_SHA2_NISTP384_PRIVATE_NO_PASSWORD = ( +b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp384\r +Encryption: none\r +Comment: ecdsa-key-20210106\r +Public-Lines: 3\r +AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEjK280ap/RD\r +R916Q00OI1LIHyRG1fcH6twBjmynTgl0uGlcb8bnbpGO1JOgbhBqqzVQHVckHzqT\r +fUif6rRRQuiUJEenXRmgjQ0uEcj21Rdomz7TJPz1k8tHmOZCHgJx6g==\r +Private-Lines: 2\r +AAAAMQCNcgWtnEeeTqFN383FBJdM90keHkJwproyLPgWQLlbZe+r8py0Pl7mUHvj\r +SGmXUVc=\r +Private-MAC: 1464df777d20427e2b99adb148ed4b8a1a839409\r +""") + +PUTTY_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD = ( +b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp521\r +Encryption: none\r +Comment: ecdsa-key-20210106\r +Public-Lines: 4\r +AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGtj24Kr7OY\r +21mtlHTFuH0NmrhI1mco0nND4FvDbNTTU/87t1ZDqbPEnRqmYBM6/dGPyOK82PH8\r +NmCrCjj0rmckNgC3+Jg/+ok1bJG7/WeTOObnIdDBJklxksIjMF6LG6hVngIibxgF\r +V3iBGD5eWUr40AK+6+wN7uKsaFHMBCg8lde5Mg==\r +Private-Lines: 2\r +AAAAQgE64XtEewBVYUz+sfojvHmsiwdT+2BBBw1IAcKuozuhsz8EkOEOBJGZqCBP\r +B9pAqlHsVHQJF/uVpFbJFUnjEokJ4w==\r +Private-MAC: e828d7207e0e73453005d606216ca36c64d1e304\r +""") + + +class DummyOpenContext(object): + """ + Helper for testing operations using open context manager. + + It keeps a record or all calls in self.calls. + """ + + def __init__(self): + self.calls = [] + self.last_stream = None + + def __call__(self, path, mode): + self.last_stream = StringIO() + self.calls.append( + {'path': path, 'mode': mode, 'stream': self.last_stream}) + return self + + def __enter__(self): + return self.last_stream + + def __exit__(self, exc_type, exc_value, tb): + return False + + +class TestHelpers(ChevahTestCase, CommandLineMixin): + """ + Unit tests for helper methods from this module. + """ + + def test_path(self): + """ + Will take an unicode and will return the os encoded path. + """ + result = _path(u'path-\N{sun}') + if self.os_name == 'windows': + self.assertEqual(u'path-\N{sun}', result) + else: + self.assertEqual(b'path-\xe2\x98\x89', result) + + +class TestKey(ChevahTestCase): + """ + Unit test for SSH key generation. + + The actual test creating real keys are located in functional. + """ + + def setUp(self): + super(TestKey, self).setUp() + self.rsaObj = keys.Key._fromRSAComponents( + n=keydata.RSAData['n'], + e=keydata.RSAData['e'], + d=keydata.RSAData['d'], + p=keydata.RSAData['p'], + q=keydata.RSAData['q'], + u=keydata.RSAData['u'], + )._keyObject + self.dsaObj = keys.Key._fromDSAComponents( + y=keydata.DSAData['y'], + p=keydata.DSAData['p'], + q=keydata.DSAData['q'], + g=keydata.DSAData['g'], + x=keydata.DSAData['x'], + )._keyObject + self.ecObj = keys.Key._fromECComponents( + x=keydata.ECDatanistp256['x'], + y=keydata.ECDatanistp256['y'], + privateValue=keydata.ECDatanistp256['privateValue'], + curve=keydata.ECDatanistp256['curve'] + )._keyObject + self.ecObj384 = keys.Key._fromECComponents( + x=keydata.ECDatanistp384['x'], + y=keydata.ECDatanistp384['y'], + privateValue=keydata.ECDatanistp384['privateValue'], + curve=keydata.ECDatanistp384['curve'] + )._keyObject + self.ecObj521 = keys.Key._fromECComponents( + x=keydata.ECDatanistp521['x'], + y=keydata.ECDatanistp521['y'], + privateValue=keydata.ECDatanistp521['privateValue'], + curve=keydata.ECDatanistp521['curve'] + )._keyObject + self.ed25519Obj = keys.Key._fromEd25519Components( + a=keydata.Ed25519Data['a'], + k=keydata.Ed25519Data['k'] + )._keyObject + self.rsaSignature = ( + b"\x00\x00\x00\x07ssh-rsa\x00\x00\x01\x00~Y\xa3\xd7\xfdW\xc6pu@" + b"\xd81\xa1S\xf3O\xdaE\xf4/\x1ex\x1d\xf1\x9a\xe1G3\xd9\xd6U\x1f" + b"\x8c\xd9\x1b\x8b\x90\x0e\x8a\xc1\x91\xd8\x0cd\xc9\x0c\xe7\xb2" + b"\xc9,'=\x15\x1cQg\xe7x\xb5j\xdbI\xc0\xde\xafb\xd7@\xcar\x0b" + b"\xce\xa3zM\x151q5\xde\xfa\x0c{wjKN\x88\xcbC\xe5\x89\xc3\xf9i" + b"\x96\x91\xdb\xca}\xdbR\x1a\x13T\xf9\x0cDJH\x0b\x06\xcfl\xf3" + b"\x13[\x82\xa2\x9d\x93\xfd\x8e\xce|\xfb^n\xd4\xed\xe2\xd1\x8a" + b"\xb7aY\x9bB\x8f\xa4\xc7\xbe7\xb5\x0b9j\xa4.\x87\x13\xf7\xf0" + b"\xda\xd7\xd2\xf9\x1f9p\xfd?\x18\x0f\xf2N\x9b\xcf/\x1e)\n>A\x19" + b"\xc2\xb5j\xf9UW\xd4\xae\x87B\xe6\x99t\xa2y\x90\x98\xa2\xaaf\xcb" + b"\x86\xe5k\xe3\xce\xe0u\x1c\xeb\x93\x1aN\x88\xc9\x93Y\xc3.V\xb1L" + b"44`C\xc7\xa66\xaf\xfa\x7f\x04Y\x92\xfa\xa4\x1a\x18%\x19\xd5 4^" + b"\xb9rY\xba \x01\xf9.\x89%H\xbe\x1c\x83A\x96" + ) + self.dsaSignature = ( + b'\x00\x00\x00\x07ssh-dss\x00\x00\x00(?\xc7\xeb\x86;\xd5TFA\xb4' + b'\xdf\x0c\xc4E@4,d\xbc\t\xd9\xae\xdd[\xed-\x82nQ\x8cf\x9b\xe8\xe1' + b'jrg\x84p<' + ) + self.oldSecureRandom = Key.secureRandom + Key.secureRandom = lambda me, x: '\xff' * x + + def tearDown(self): + Key.secureRandom = self.oldSecureRandom + del self.oldSecureRandom + super(TestKey, self).tearDown() + + def assertBadKey(self, content, message): + """ + Check the `content` raise a BadKeyError with `message`. + """ + with self.assertRaises(BadKeyError) as context: + Key.fromString(content) + + self.assertEqual(message, context.exception.message) + + def assertKeyIsTooShort(self, content): + """ + Check the key content is too short. + """ + self.assertBadKey(content, 'Key is too short.') + + def assertKeyParseError(self, content): + """ + Check that key content fail to parse. + """ + self.assertBadKey(content, 'Fail to parse key content.') + + def _getKeysForFingerprintTest(self): + """ + Return tuple with public RSA and DSA keys from the test data. + """ + rsa = keys.Key._fromRSAComponents( + n=keydata.RSAData['n'], + e=keydata.RSAData['e'], + d=keydata.RSAData['d'], + p=keydata.RSAData['p'], + q=keydata.RSAData['q'], + u=keydata.RSAData['u'], + )._keyObject + dsa = keys.Key._fromDSAComponents( + y=keydata.DSAData['y'], + p=keydata.DSAData['p'], + q=keydata.DSAData['q'], + g=keydata.DSAData['g'], + x=keydata.DSAData['x'], + )._keyObject + return (rsa, dsa) + + def _testPublicPrivateFromString(self, public, private, type, data): + self._testPublicFromString(public, type, data) + self._testPrivateFromString(private, type, data) + + def _testPublicFromString(self, public, type, data): + publicKey = keys.Key.fromString(public) + self.assertTrue(publicKey.isPublic()) + self.assertEqual(publicKey.type(), type) + for k, v in publicKey.data().items(): + self.assertEqual(data[k], v) + + def _testPrivateFromString(self, private, type, data): + privateKey = keys.Key.fromString(private) + self.assertFalse(privateKey.isPublic()) + self.assertEqual(privateKey.type(), type) + for k, v in data.items(): + self.assertEqual(privateKey.data()[k], v) + + def test_init(self): + """ + Test that the PublicKey object is initialized correctly. + """ + obj = keys.Key._fromRSAComponents(n=int(5), e=int(3))._keyObject + key = keys.Key(obj) + self.assertEqual(key._keyObject, obj) + + def test_equal(self): + """ + Test that Key objects are compared correctly. + """ + rsa1 = keys.Key(self.rsaObj) + rsa2 = keys.Key(self.rsaObj) + rsa3 = keys.Key( + keys.Key._fromRSAComponents(n=int(5), e=int(3))._keyObject) + dsa = keys.Key(self.dsaObj) + self.assertTrue(rsa1 == rsa2) + self.assertFalse(rsa1 == rsa3) + self.assertFalse(rsa1 == dsa) + self.assertFalse(rsa1 == object) + self.assertFalse(rsa1 == None) + + def test_notEqual(self): + """ + Test that Key objects are not-compared correctly. + """ + rsa1 = keys.Key(self.rsaObj) + rsa2 = keys.Key(self.rsaObj) + rsa3 = keys.Key( + keys.Key._fromRSAComponents(n=int(5), e=int(3))._keyObject) + dsa = keys.Key(self.dsaObj) + self.assertFalse(rsa1 != rsa2) + self.assertTrue(rsa1 != rsa3) + self.assertTrue(rsa1 != dsa) + self.assertTrue(rsa1 != object) + self.assertTrue(rsa1 != None) + + def test_type(self): + """ + Test that the type method returns the correct type for an object. + """ + self.assertEqual(keys.Key(self.rsaObj).type(), 'RSA') + self.assertEqual(keys.Key(self.rsaObj).sshType(), b'ssh-rsa') + self.assertEqual(keys.Key(self.dsaObj).type(), 'DSA') + self.assertEqual(keys.Key(self.dsaObj).sshType(), b'ssh-dss') + self.assertRaises(RuntimeError, keys.Key(None).type) + self.assertRaises(RuntimeError, keys.Key(None).sshType) + self.assertRaises(RuntimeError, keys.Key(self).type) + self.assertRaises(RuntimeError, keys.Key(self).sshType) + + def test_generate_no_key_type(self): + """ + An error is raised when generating a key with unknown type. + """ + with self.assertRaises(KeyCertException) as context: + Key.generate(key_type=None) + + self.assertEqual( + 'Unknown key type "not-specified".', context.exception.message) + + def test_generate_unknown_type(self): + """ + An error is raised when generating a key with unknown type. + """ + with self.assertRaises(KeyCertException) as context: + Key.generate(key_type='bad-type') + + self.assertEqual( + 'Unknown key type "bad-type".', context.exception.message) + + @attr('slow') + def test_generate_rsa(self): + """ + Check generation of an RSA key with a case insensitive type name. + """ + key = Key.generate(key_type='rSA', key_size=1024) + + self.assertEqual('RSA', key.type()) + self.assertEqual(1024, key.size()) + + @attr('slow') + def test_generate_dsa(self): + """ + Check generation of a DSA key with a case insensitive type name. + """ + key = Key.generate(key_type='dSA', key_size=1024) + + self.assertEqual('DSA', key.type()) + self.assertEqual(1024, key.size()) + + def test_generate_failed(self): + """ + A ServerError is raised when it fails to generate the key. + """ + with self.assertRaises(KeyCertException) as context: + Key.generate(key_type='dSa', key_size=2048) + + self.assertEqual( + u'Wrong key size "2048". Number of bits in p must be a multiple ' + 'of 64 between 512 and 1024, not 2048 bits.', + context.exception.message) + + def test_guessStringType(self): + """ + Test that the _guessStringType method guesses string types + correctly. + + Imported from Twisted. + """ + self.assertEqual( + keys.Key._guessStringType(keydata.publicRSA_openssh), + 'public_openssh') + self.assertEqual( + keys.Key._guessStringType(keydata.publicDSA_openssh), + 'public_openssh') + self.assertEqual( + keys.Key._guessStringType( + keydata.privateRSA_openssh), + 'private_openssh') + self.assertEqual( + keys.Key._guessStringType( + keydata.privateDSA_openssh), + 'private_openssh') + self.assertEqual( + keys.Key._guessStringType(keydata.publicRSA_lsh), + 'public_lsh') + self.assertEqual( + keys.Key._guessStringType(keydata.publicDSA_lsh), + 'public_lsh') + self.assertEqual( + keys.Key._guessStringType(keydata.privateRSA_lsh), + 'private_lsh') + self.assertEqual( + keys.Key._guessStringType(keydata.privateDSA_lsh), + 'private_lsh') + self.assertEqual( + keys.Key._guessStringType( + keydata.privateRSA_agentv3), + 'agentv3') + self.assertEqual( + keys.Key._guessStringType( + keydata.privateDSA_agentv3), + 'agentv3') + self.assertEqual( + keys.Key._guessStringType( + '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x01'), + 'blob') + self.assertEqual( + keys.Key._guessStringType( + '\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x01'), + 'blob') + self.assertEqual( + keys.Key._guessStringType('not a key'), + None) + + def test_guessStringType_unknown(self): + """ + None is returned when could not detect key type. + """ + content = mk.ascii() + + result = Key._guessStringType(content) + + self.assertIsNone(result) + + def test_guessStringType_X509_PEM_certificate(self): + """ + PEM certificates are recognized as public keys. + """ + content = ( + '-----BEGIN CERTIFICATE-----\n' + 'CONTENT\n' + '-----END CERTIFICATE-----\n' + ) + + result = Key._guessStringType(content) + + self.assertEqual('public_x509_certificate', result) + + def test_guessStringType_X509_PUBLIC(self): + """ + x509 public PEM are recognized as public keys. + """ + content = ( + '-----BEGIN PUBLIC KEY-----\n' + 'CONTENT\n' + '-----END PUBLIC KEY-----\n' + ) + + result = Key._guessStringType(content) + + self.assertEqual('public_x509', result) + + def test_guessStringType_PKCS8_PRIVATE(self): + """ + PKS#8 private PEM are recognized as private keys. + """ + content = ( + '-----BEGIN PRIVATE KEY-----\n' + 'CONTENT\n' + '-----END PRIVATE KEY-----\n' + ) + + result = Key._guessStringType(content) + + self.assertEqual('private_pkcs8', result) + + def test_guessStringType_PKCS8_PRIVATE_ENCRYPTED(self): + """ + PKS#8 encrypted private PEM are recognized as private keys. + """ + content = ( + '-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + 'CONTENT\n' + '-----END ENCRYPTED PRIVATE KEY-----\n' + ) + + result = Key._guessStringType(content) + + self.assertEqual('private_encrypted_pkcs8', result) + + def test_guessStringType_private_OpenSSH_RSA(self): + """ + Can recognize an OpenSSH RSA private key. + """ + result = Key._guessStringType(OPENSSH_RSA_PRIVATE) + + self.assertEqual('private_openssh', result) + + def test_guessStringType_private_OpenSSH_DSA(self): + """ + Can recognize an OpenSSH DSA private key. + """ + result = Key._guessStringType(OPENSSH_DSA_PRIVATE) + + self.assertEqual('private_openssh', result) + + def test_guessStringType_private_OpenSSH_ECDSA(self): + """ + Can recognize an OpenSSH ECDSA private key. + """ + result = Key._guessStringType(keydata.privateECDSA_256_openssh) + + self.assertEqual('private_openssh', result) + + def test_guessStringType_public_OpenSSH(self): + """ + Can recognize an OpenSSH public key. + """ + result = Key._guessStringType(OPENSSH_RSA_PUBLIC) + + self.assertEqual('public_openssh', result) + + def test_guessStringType_public_PKCS1(self): + """ + Can recognize an PKCS1 PEM public key. + """ + result = Key._guessStringType(PKCS1_RSA_PUBLIC) + + self.assertEqual('public_pkcs1_rsa', result) + + def test_guessStringType_public_OpenSSH_ECDSA(self): + """ + Can recognize an OpenSSH public key. + """ + result = Key._guessStringType(keydata.publicECDSA_256_openssh) + + self.assertEqual('public_openssh', result) + + result = Key._guessStringType(keydata.publicECDSA_384_openssh) + + self.assertEqual('public_openssh', result) + + result = Key._guessStringType(keydata.publicECDSA_521_openssh) + + self.assertEqual('public_openssh', result) + + def test_guessStringType_private_SSHCOM(self): + """ + Can recognize an SSH.com private key. + """ + result = Key._guessStringType(SSHCOM_RSA_PRIVATE_NO_PASSWORD) + + self.assertEqual('private_sshcom', result) + + def test_guessStringType_public_SSHCOM(self): + """ + Can recognize an SSH.com public key. + """ + result = Key._guessStringType(SSHCOM_RSA_PUBLIC) + + self.assertEqual('public_sshcom', result) + + def test_guessStringType_putty(self): + """ + Can recognize a Putty private key. + """ + result = Key._guessStringType(PUTTY_RSA_PRIVATE_NO_PASSWORD) + + self.assertEqual('private_putty', result) + + def test_getKeyFormat_unknown(self): + """ + Inform using a human readable text that format is not known. + """ + result = Key.getKeyFormat('no-such-format') + + self.assertEqual('Unknown format', result) + + def test_getKeyFormat_known(self): + """ + Return the human readable description of key format. + """ + + result = Key.getKeyFormat(SSHCOM_RSA_PUBLIC) + + self.assertEqual('SSH.com Public', result) + + def test_public_get(self): + """ + Return an instance of same class but with only public elements for + the private key. + """ + sut = Key.fromString(OPENSSH_RSA_PRIVATE) + + result = sut.public() + + self.assertFalse(sut.isPublic()) + self.assertIsInstance(Key, result) + self.assertTrue(result.isPublic()) + self.assertEqual(result.data()['e'], sut.data()['e']) + self.assertEqual(result.data()['n'], sut.data()['n']) + + def test_fromFile(self): + """ + Test that fromFile works correctly. + """ + self.test_segments = mk.fs.createFileInTemp( + content=keydata.privateRSA_openssh) + key_path = mk.fs.getRealPathFromSegments(self.test_segments) + + self.assertEqual( + keys.Key.fromFile(key_path), + keys.Key.fromString(keydata.privateRSA_openssh)) + + self.assertRaises( + keys.BadKeyError, keys.Key.fromFile, key_path, 'bad_type') + + def test_fromString_type_unkwown(self): + """ + An exceptions is raised when reading a key for which type could not + be detected. Exception only contains the beginning of the content. + """ + content = mk.ascii() * 100 + + self.assertBadKey( + content, 'Cannot guess the type for "%s"' % content[:80]) + + def test_fromString_struct_errors(self): + """ + Errors caused by parsing the content are raises as BadKeyError. + """ + content = OPENSSH_DSA_PUBLIC[:32] + + self.assertKeyParseError(content) + + def test_fromString_errors(self): + """ + keys.Key.fromString should raise BadKeyError when the key is invalid. + """ + self.assertRaises(keys.BadKeyError, keys.Key.fromString, '') + # no key data with a bad key type + self.assertRaises( + keys.BadKeyError, keys.Key.fromString, '', 'bad_type') + # trying to decrypt a key which doesn't support encryption + self.assertRaises( + keys.BadKeyError, + keys.Key.fromString, + keydata.publicRSA_lsh, passphrase='unencrypted') + # trying t fo decrypt a key with the wrong passphrase + self.assertRaises( + keys.EncryptedKeyError, + keys.Key.fromString, + keys.Key(self.rsaObj).toString('openssh', 'encrypted')) + # key with no key data + self.assertRaises( + keys.BadKeyError, + keys.Key.fromString, + '-----BEGIN RSA KEY-----\nwA==\n') + # key with invalid DEK Info + self.assertRaises( + keys.BadKeyError, + keys.Key.fromString, + """-----BEGIN ENCRYPTED RSA KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: weird type + +4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n +T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H +g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB +sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5 +9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV +gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW +0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE +vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS +hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk +2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf +qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk +4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY +EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n +8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0 +fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P +V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+ +0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5 +xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI +dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup +VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk +gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c +8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw +SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 +CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE +xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P +-----END RSA PRIVATE KEY-----""", passphrase='encrypted') + # key with invalid encryption type + self.assertRaises( + keys.BadKeyError, keys.Key.fromString, + """-----BEGIN ENCRYPTED RSA KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: FOO-123-BAR,01234567 + +4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n +T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H +g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB +sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5 +9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV +gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW +0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE +vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS +hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk +2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf +qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk +4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY +EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n +8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0 +fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P +V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+ +0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5 +xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI +dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup +VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk +gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c +8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw +SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 +CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE +xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P +-----END RSA PRIVATE KEY-----""", passphrase='encrypted') + # key with bad IV (AES) + self.assertRaises( + keys.BadKeyError, keys.Key.fromString, + """-----BEGIN ENCRYPTED RSA KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,01234 + +4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n +T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H +g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB +sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5 +9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV +gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW +0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE +vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS +hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk +2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf +qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk +4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY +EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n +8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0 +fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P +V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+ +0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5 +xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI +dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup +VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk +gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c +8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw +SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 +CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE +xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P +-----END RSA PRIVATE KEY-----""", passphrase='encrypted') + # key with bad IV (DES3) + self.assertRaises( + keys.BadKeyError, keys.Key.fromString, + """-----BEGIN ENCRYPTED RSA KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,01234 + +4Ed/a9OgJWHJsne7yOGWeWMzHYKsxuP9w1v0aYcp+puS75wvhHLiUnNwxz0KDi6n +T3YkKLBsoCWS68ApR2J9yeQ6R+EyS+UQDrO9nwqo3DB5BT3Ggt8S1wE7vjNLQD0H +g/SJnlqwsECNhh8aAx+Ag0m3ZKOZiRD5mCkcDQsZET7URSmFytDKOjhFn3u6ZFVB +sXrfpYc6TJtOQlHd/52JB6aAbjt6afSv955Z7enIi+5yEJ5y7oYQTaE5zrFMP7N5 +9LbfJFlKXxEddy/DErRLxEjmC+t4svHesoJKc2jjjyNPiOoGGF3kJXea62vsjdNV +gMK5Eged3TBVIk2dv8rtJUvyFeCUtjQ1UJZIebScRR47KrbsIpCmU8I4/uHWm5hW +0mOwvdx1L/mqx/BHqVU9Dw2COhOdLbFxlFI92chkovkmNk4P48ziyVnpm7ME22sE +vfCMsyirdqB1mrL4CSM7FXONv+CgfBfeYVkYW8RfJac9U1L/O+JNn7yee414O/rS +hRYw4UdWnH6Gg6niklVKWNY0ZwUZC8zgm2iqy8YCYuneS37jC+OEKP+/s6HSKuqk +2bzcl3/TcZXNSM815hnFRpz0anuyAsvwPNRyvxG2/DacJHL1f6luV4B0o6W410yf +qXQx01DLo7nuyhJqoH3UGCyyXB+/QUs0mbG2PAEn3f5dVs31JMdbt+PrxURXXjKk +4cexpUcIpqqlfpIRe3RD0sDVbH4OXsGhi2kiTfPZu7mgyFxKopRbn1KwU1qKinfY +EU9O4PoTak/tPT+5jFNhaP+HrURoi/pU8EAUNSktl7xAkHYwkN/9Cm7DeBghgf3n +8+tyCGYDsB5utPD0/Xe9yx0Qhc/kMm4xIyQDyA937dk3mUvLC9vulnAP8I+Izim0 +fZ182+D1bWwykoD0997mUHG/AUChWR01V1OLwRyPv2wUtiS8VNG76Y2aqKlgqP1P +V+IvIEqR4ERvSBVFzXNF8Y6j/sVxo8+aZw+d0L1Ns/R55deErGg3B8i/2EqGd3r+ +0jps9BqFHHWW87n3VyEB3jWCMj8Vi2EJIfa/7pSaViFIQn8LiBLf+zxG5LTOToK5 +xkN42fReDcqi3UNfKNGnv4dsplyTR2hyx65lsj4bRKDGLKOuB1y7iB0AGb0LtcAI +dcsVlcCeUquDXtqKvRnwfIMg+ZunyjqHBhj3qgRgbXbT6zjaSdNnih569aTg0Vup +VykzZ7+n/KVcGLmvX0NesdoI7TKbq4TnEIOynuG5Sf+2GpARO5bjcWKSZeN/Ybgk +gccf8Cqf6XWqiwlWd0B7BR3SymeHIaSymC45wmbgdstrbk7Ppa2Tp9AZku8M2Y7c +8mY9b+onK075/ypiwBm4L4GRNTFLnoNQJXx0OSl4FNRWsn6ztbD+jZhu8Seu10Jw +SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 +CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE +xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P +-----END RSA PRIVATE KEY-----""", passphrase='encrypted') + + def test_toStringErrors(self): + """ + Test that toString raises errors appropriately. + """ + self.assertRaises( + keys.BadKeyError, keys.Key(self.rsaObj).toString, 'bad_type') + + def test_fromString_BLOB_blob_type_non_ascii(self): + """ + Raise with printable information for the bad type, + even if blob type has non-ascii data. + """ + badBlob = common.NS('ssh-\xbd\xbd\xbd') + self.assertBadKey( + badBlob, + 'Cannot guess the type for "' + r'\x00\x00\x00' + '\n' + r'ssh-\xc2\xbd\xc2\xbd\xc2\xbd"' + ) + + def test_fromString_PRIVATE_BLOB(self): + """ + Test that a private key is correctly generated from a private key blob. + """ + rsaBlob = (common.NS('ssh-rsa') + common.MP(2) + common.MP(3) + + common.MP(4) + common.MP(5) + common.MP(6) + common.MP(7)) + rsaKey = keys.Key._fromString_PRIVATE_BLOB(rsaBlob) + dsaBlob = (common.NS('ssh-dss') + common.MP(2) + common.MP(3) + + common.MP(4) + common.MP(5) + common.MP(6)) + dsaKey = keys.Key._fromString_PRIVATE_BLOB(dsaBlob) + badBlob = common.NS('ssh-bad') + self.assertFalse(rsaKey.isPublic()) + self.assertEqual( + rsaKey.data(), + {'n': 2, 'e': 3, 'd': 4, 'u': 5, 'p': 6, 'q': 7}) + self.assertFalse(dsaKey.isPublic()) + self.assertEqual( + dsaKey.data(), {'p': 2, 'q': 3, 'g': 4, 'y': 5, 'x': 6}) + self.assertRaises( + keys.BadKeyError, keys.Key._fromString_PRIVATE_BLOB, badBlob) + + def test_blobRSA(self): + """ + Return the over-the-wire SSH format of the RSA public key. + """ + self.assertEqual( + keys.Key(self.rsaObj).blob(), + common.NS(b'ssh-rsa') + + common.MP(self.rsaObj.private_numbers().public_numbers.e) + + common.MP(self.rsaObj.private_numbers().public_numbers.n) + ) + + def test_blobDSA(self): + """ + Return the over-the-wire SSH format of the DSA public key. + """ + publicNumbers = self.dsaObj.private_numbers().public_numbers + + self.assertEqual( + keys.Key(self.dsaObj).blob(), + common.NS(b'ssh-dss') + + common.MP(publicNumbers.parameter_numbers.p) + + common.MP(publicNumbers.parameter_numbers.q) + + common.MP(publicNumbers.parameter_numbers.g) + + common.MP(publicNumbers.y) + ) + + def test_blobEC(self): + """ + Return the over-the-wire SSH format of the EC public key. + """ + from cryptography import utils + + byteLength = (self.ecObj.curve.key_size + 7) // 8 + self.assertEqual( + keys.Key(self.ecObj).blob(), + common.NS(keydata.ECDatanistp256['curve']) + + common.NS(keydata.ECDatanistp256['curve'][-8:]) + + common.NS(b'\x04' + + utils.int_to_bytes( + self.ecObj.private_numbers().public_numbers.x, byteLength) + + utils.int_to_bytes( + self.ecObj.private_numbers().public_numbers.y, byteLength)) + ) + + def test_blobEd25519(self): + """ + Return the over-the-wire SSH format of the Ed25519 public key. + """ + from cryptography.hazmat.primitives import serialization + + publicBytes = self.ed25519Obj.public_key().public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw + ) + + self.assertEqual( + keys.Key(self.ed25519Obj).blob(), + common.NS(b'ssh-ed25519') + + common.NS(publicBytes) + ) + + def test_blobNoKey(self): + """ + C{RuntimeError} is raised when the blob is requested for a Key + which is not wrapping anything. + """ + badKey = keys.Key(None) + + self.assertRaises(RuntimeError, badKey.blob) + + def test_privateBlobRSA(self): + """ + L{keys.Key.privateBlob} returns the SSH protocol-level format of an + RSA private key. + """ + numbers = self.rsaObj.private_numbers() + self.assertEqual( + keys.Key(self.rsaObj).privateBlob(), + common.NS(b'ssh-rsa') + + common.MP(numbers.public_numbers.n) + + common.MP(numbers.public_numbers.e) + + common.MP(numbers.d) + + common.MP(numbers.iqmp) + + common.MP(numbers.p) + + common.MP(numbers.q) + ) + + def test_privateBlobDSA(self): + """ + L{keys.Key.privateBlob} returns the SSH protocol-level format of a DSA + private key. + """ + publicNumbers = self.dsaObj.private_numbers().public_numbers + + self.assertEqual( + keys.Key(self.dsaObj).privateBlob(), + common.NS(b'ssh-dss') + + common.MP(publicNumbers.parameter_numbers.p) + + common.MP(publicNumbers.parameter_numbers.q) + + common.MP(publicNumbers.parameter_numbers.g) + + common.MP(publicNumbers.y) + + common.MP(self.dsaObj.private_numbers().x) + ) + + def test_privateBlobEC(self): + """ + L{keys.Key.privateBlob} returns the SSH ptotocol-level format of EC + private key. + """ + from cryptography.hazmat.primitives import serialization + self.assertEqual( + keys.Key(self.ecObj).privateBlob(), + common.NS(keydata.ECDatanistp256['curve']) + + common.NS(keydata.ECDatanistp256['curve'][-8:]) + + common.NS( + self.ecObj.public_key().public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint)) + + common.MP(self.ecObj.private_numbers().private_value) + ) + + def test_privateBlobEd25519(self): + """ + L{keys.Key.privateBlob} returns the SSH protocol-level format of an + Ed25519 private key. + """ + from cryptography.hazmat.primitives import serialization + publicBytes = self.ed25519Obj.public_key().public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw + ) + privateBytes = self.ed25519Obj.private_bytes( + serialization.Encoding.Raw, + serialization.PrivateFormat.Raw, + serialization.NoEncryption() + ) + + self.assertEqual( + keys.Key(self.ed25519Obj).privateBlob(), + common.NS(b'ssh-ed25519') + + common.NS(publicBytes) + + common.NS(privateBytes + publicBytes) + ) + + def test_privateBlobNoKeyObject(self): + """ + Raises L{RuntimeError} if the underlying key object does not exists. + """ + badKey = keys.Key(None) + + self.assertRaises(RuntimeError, badKey.privateBlob) + + def test_fromString_PUBLIC_OPENSSH_RSA(self): + """ + Can load public RSA OpenSSH key. + """ + sut = Key.fromString(OPENSSH_RSA_PUBLIC) + + self.checkParsedRSAPublic1024(sut) + + def test_fromString_PUBLIC_PKC1_RSA(self): + """ + Can load public RSA PKC1 key. + """ + sut = Key.fromString(PKCS1_RSA_PUBLIC) + + self.checkParsedRSAPublic1024(sut) + + def test_fromString_PUBLIC_OPENSSH_RSA_too_short(self): + """ + An exception is raised when public RSA OpenSSH key is bad formatted. + """ + self.assertKeyIsTooShort('ssh-rsa') + + def test_fromString_PUBLIC_OPENSSH_invalid_payload(self): + """ + Raise an exception when key blob has a bad format. + """ + self.assertKeyParseError('ssh-rsa AAAAB3NzaC1yc2EA') + + def test_fromString_PUBLIC_OPENSSH_DSA(self): + """ + Can load a public OpenSSH in DSA format. + """ + sut = Key.fromString(OPENSSH_DSA_PUBLIC) + + self.checkParsedDSAPublic1024(sut) + + def test_fromString_OpenSSH(self): + """ + Test that keys are correctly generated from OpenSSH strings. + """ + self._testPublicPrivateFromString( + keydata.publicRSA_openssh, + keydata.privateRSA_openssh, 'RSA', keydata.RSAData) + + self.assertEqual( + keys.Key.fromString( + keydata.privateRSA_openssh_encrypted, + passphrase='encrypted'), + keys.Key.fromString(keydata.privateRSA_openssh)) + + self.assertEqual( + keys.Key.fromString( + keydata.privateRSA_openssh_alternate), + keys.Key.fromString(keydata.privateRSA_openssh)) + + self._testPublicPrivateFromString( + keydata.publicDSA_openssh, + keydata.privateDSA_openssh, 'DSA', keydata.DSAData) + + def test_fromString_OpenSSH_private_missing_password(self): + """ + It fails to load an ecrypted key when password is not provided. + """ + with self.assertRaises(EncryptedKeyError) as context: + keys.Key.fromString(keydata.privateRSA_openssh_encrypted) + + self.assertEqual( + 'Passphrase must be provided for an encrypted key', + context.exception.message, + ) + + def test_fromString_PRIVATE_OPENSSH_with_whitespace(self): + """ + If key strings have trailing whitespace, it should be ignored. + """ + # from Twisted bug #3391, since our test key data doesn't have + # an issue with appended newlines + privateDSAData = """-----BEGIN DSA PRIVATE KEY----- +MIIBuwIBAAKBgQDylESNuc61jq2yatCzZbenlr9llG+p9LhIpOLUbXhhHcwC6hrh +EZIdCKqTO0USLrGoP5uS9UHAUoeN62Z0KXXWTwOWGEQn/syyPzNJtnBorHpNUT9D +Qzwl1yUa53NNgEctpo4NoEFOx8PuU6iFLyvgHCjNn2MsuGuzkZm7sI9ZpQIVAJiR +9dPc08KLdpJyRxz8T74b4FQRAoGAGBc4Z5Y6R/HZi7AYM/iNOM8su6hrk8ypkBwR +a3Dbhzk97fuV3SF1SDrcQu4zF7c4CtH609N5nfZs2SUjLLGPWln83Ysb8qhh55Em +AcHXuROrHS/sDsnqu8FQp86MaudrqMExCOYyVPE7jaBWW+/JWFbKCxmgOCSdViUJ +esJpBFsCgYEA7+jtVvSt9yrwsS/YU1QGP5wRAiDYB+T5cK4HytzAqJKRdC5qS4zf +C7R0eKcDHHLMYO39aPnCwXjscisnInEhYGNblTDyPyiyNxAOXuC8x7luTmwzMbNJ +/ow0IqSj0VF72VJN9uSoPpFd4lLT0zN8v42RWja0M8ohWNf+YNJluPgCFE0PT4Vm +SUrCyZXsNh6VXwjs3gKQ +-----END DSA PRIVATE KEY-----""" + self.assertEqual(keys.Key.fromString(privateDSAData), + keys.Key.fromString(privateDSAData + '\n')) + + def test_fromString_PRIVATE_OPENSSH_newer(self): + """ + Newer versions of OpenSSH generate encrypted keys which have a longer + IV than the older versions. These newer keys are also loaded. + """ + key = keys.Key.fromString(keydata.privateRSA_openssh_encrypted_aes, + passphrase='testxp') + self.assertEqual(key.type(), 'RSA') + key2 = keys.Key.fromString( + keydata.privateRSA_openssh_encrypted_aes + '\n', + passphrase='testxp') + self.assertEqual(key, key2) + + def test_fromString_PRIVATE_OPENSSH_not_encrypted_with_passphrase(self): + """ + When loading a unencrypted OpenSSH private key with passhphrase + will raise BadKeyError. + """ + + with self.assertRaises(BadKeyError) as context: + Key.fromString(OPENSSH_RSA_PRIVATE, passphrase='pass') + + self.assertEqual( + 'OpenSSH key not encrypted', + context.exception.message) + + def test_toString_OPENSSH(self): + """ + Test that the Key object generates OpenSSH keys correctly. + """ + key = keys.Key.fromString(keydata.privateRSA_lsh) + + self.assertEqual(key.toString('openssh'), keydata.privateRSA_openssh) + self.assertEqual( + key.toString('openssh', 'encrypted'), + keydata.privateRSA_openssh_encrypted) + self.assertEqual( + key.public().toString('openssh'), + keydata.publicRSA_openssh[:-8]) + self.assertEqual( + key.public().toString('openssh', 'comment'), + keydata.publicRSA_openssh) + + key = keys.Key.fromString(keydata.privateDSA_lsh) + + self.assertEqual(key.toString('openssh'), keydata.privateDSA_openssh) + self.assertEqual( + key.public().toString('openssh', 'comment'), + keydata.publicDSA_openssh) + self.assertEqual( + key.public().toString('openssh'), keydata.publicDSA_openssh[:-8]) + + def addSSHCOMKeyHeaders(self, source, headers): + """ + Add headers to a SSH.com key. + + Long headers are wrapped at 70 characters. + """ + lines = source.splitlines() + for key, value in headers.items(): + line = '%s: %s' % (key, value.encode('utf-8')) + header = '\\\n'.join(textwrap.wrap(line, 70)) + lines.insert(1, header) + return '\n'.join(lines) + + def checkParsedDSAPublic1024(self, sut): + """ + Check the default public DSA key of size 1024. + + This is a shared test for parsing DSA key from various formats. + """ + self.assertEqual(1024, sut.size()) + self.assertEqual('DSA', sut.type()) + self.assertTrue(sut.isPublic()) + self.checkParsedDSAPublic1024Data(sut) + + def checkParsedDSAPublic1024Data(self, sut): + """ + Check the public part values for the default DSA key of size 1024. + """ + data = sut.data() + self.assertEqual(int( + '89826398702575694025672739759021185748719093895775418981133245507' + '56542191015877768589699407493932539140865803919573940821357868468' + '55675657634384222748339103943127442354510383477300256462657784441' + '71019786268219332779725063911288445634960873466719023048095246499' + '763675183656402590703132265805882271082319033570L'), + data['y']) + self.assertEqual(int( + '14519098631088118929874535941241101897542246758347965800832728196' + '81139199597265476885338795620826004398884602230901691384070382776' + '92982149652731866793940314712388781003443391479314606037340161379' + '86631331044475413634865132557582890274917465191550388575486379853' + '0603422003777150811982254140040687593424378397517L'), + data['p']) + self.assertEqual( + int('765629040155792319453907037659138573169171493193L'), + data['q']) + self.assertEqual(int( + '64647318098084998690447943642968245369499209364165550549740815561' + '71156388976417089337555666453157891497405105710031098879473402131' + '15408225147127626829407642540707192214402604495716677723330515779' + '34611656548484464881147166978432509157365635746874869548636130785' + '946819310836368885242376237240564866586977240572L'), + data['g']) + + def checkParsedDSAPrivate1024(self, sut): + """ + Check the default private DSA key of size 1024. + """ + self.assertEqual(1024, sut.size()) + self.assertEqual('DSA', sut.type()) + self.assertFalse(sut.isPublic()) + data = sut.data() + self.checkParsedDSAPublic1024Data(sut) + self.assertEqual(int( + '447059752886431435417087644871194130561824720094L'), + data['x']) + + def checkParsedRSAPublic1024(self, sut): + """ + Check the default public RSA key of size 1024. + """ + self.assertEqual(1024, sut.size()) + self.assertEqual('RSA', sut.type()) + self.assertTrue(sut.isPublic()) + self.checkParsedRSAPublic1024Data(sut) + + def checkParsedRSAPublic1024Data(self, sut): + """ + Check data for public RSA key of size 1024. + """ + data = sut.data() + self.assertEqual(65537, data['e']) + self.assertEqual(int( + '12955309129371696361961156024018278506192853914566590418922947244' + '33008028380639675460754206681134187533029942882729688747039044313' + '67411245192523108247958392655021595783971049572916657240822239036' + '02442387266290082476044614892594356080524766995335587624348179950' + '6405887692619349988915280409504938876523941259567L'), + data['n']) + + def checkParsedRSAPrivate1024(self, sut): + """ + Check the default private RSA key of size 1024. + """ + self.assertEqual(1024, sut.size()) + self.assertEqual('RSA', sut.type()) + self.assertFalse(sut.isPublic()) + data = sut.data() + self.assertEqual(65537, data['e']) + self.checkParsedRSAPublic1024Data(sut) + self.assertEqual(int( + '57010713839675255669157840568333483699071044890077432241594488384' + '64981848192265169337649163172545274951948296799964023904757013291' + '17313931268194522463817291948793747715146018146051093951466872189' + '64147610108577761761364098616952641696814228146724216997423652825' + '24517268536277980834876649127946895862158846465L'), + data['d']) + self.assertEqual(int( + '10661640454627350493191065484215149934251067848734449698668476614' + '18981319570111200535213963399376281314470995958266981264747210946' + '6364885923117389812635119L'), + data['p']) + self.assertEqual(int( + '12151328104249520956550929707892880056509323657595783040548358917' + '98785549316902458371621691657702435263762556929800891556172971312' + '6473919204485168003686593L'), + data['q']) + self.assertEqual(int( + '66777727502990278851698381429390065987141247478987840061938912337' + '88877413103516203638312270220327073357315389300205491590285175084' + '040066037688353071226161L'), + data['u']) + + def test_fromString_PUBLIC_SSHCOM_RSA_no_headers(self): + """ + Can load a public RSA SSH.com key which has no headers. + """ + sut = Key.fromString(SSHCOM_RSA_PUBLIC) + + self.checkParsedRSAPublic1024(sut) + + def test_fromString_PUBLIC_SSHCOM_RSA_public_headers(self): + """ + Can import a public RSA SSH.com key with headers. + """ + key_content = self.addSSHCOMKeyHeaders( + source=SSHCOM_RSA_PUBLIC, + headers={ + 'Comment': '"short comment"', + 'Subject': 'Very very long subject' * 10, + 'x-private': mk.string(), + }, + ) + sut = Key.fromString(key_content) + + self.assertEqual(1024, sut.size()) + self.assertEqual('RSA', sut.type()) + self.assertTrue(sut.isPublic()) + data = sut.data() + self.assertEqual(65537, data['e']) + + def test_fromString_PUBLIC_SSHCOM_DSA(self): + """ + Can load a public SSH.com in DSA format. + """ + sut = Key.fromString(SSHCOM_DSA_PUBLIC) + + self.checkParsedDSAPublic1024(sut) + + def test_fromString_PUBLIC_SSHCOM_no_end_tag(self): + """ + Raise an exception when there is no END tag. + """ + content = '---- BEGIN SSH2 PUBLIC KEY ----' + + self.assertBadKey(content, 'Fail to find END tag for SSH.com key.') + + content = '---- BEGIN SSH2 PUBLIC KEY ----\nnext line' + + self.assertBadKey(content, 'Fail to find END tag for SSH.com key.') + + def test_fromString_PUBLIC_SSHCOM_RSA_invalid_payload(self): + """ + Raise an exception when key has a bad format. + """ + content = """---- BEGIN SSH2 PUBLIC KEY ---- +AAAAB3NzaC1yc2EA +---- END SSH2 PUBLIC KEY ----""" + + self.assertKeyParseError(content) + + def test_toString_SSHCOM_RSA_public_no_headers(self): + """ + Can export a public RSA SSH.com key with headers. + """ + sut = Key.fromString(OPENSSH_RSA_PUBLIC) + + result = sut.toString(type='sshcom') + + self.assertEqual(SSHCOM_RSA_PUBLIC, result) + + def test_toString_SSHCOM_RSA_public_with_comment(self): + """ + Can export a public RSA SSH.com key with headers. + """ + sut = Key.fromString(OPENSSH_RSA_PUBLIC) + comment = mk.string() * 20 + + result = sut.toString(type='sshcom', extra=comment) + + expected = self.addSSHCOMKeyHeaders( + source=SSHCOM_RSA_PUBLIC, + headers={'Comment': '"%s"' % comment}, + ) + self.assertEqual(expected, result) + + def test_toString_SSHCOM_DSA_public(self): + """ + Can export a public DSA SSH.com key. + """ + sut = Key.fromString(OPENSSH_DSA_PUBLIC) + + result = sut.toString(type='sshcom') + + self.assertEqual(SSHCOM_DSA_PUBLIC, result) + + def test_fromString_PRIVATE_OPENSSH_RSA(self): + """ + Can load a private OpenSSH RSA key. + """ + sut = Key.fromString(OPENSSH_RSA_PRIVATE) + + self.checkParsedRSAPrivate1024(sut) + + def test_fromString_PRIVATE_OPENSSH_v1_RSA(self): + """ + Can load a private OpenSSH v1 RSA key. + """ + sut = Key.fromString(OPENSSH_V1_RSA_PRIVATE) + + self.checkParsedRSAPrivate1024(sut) + + def test_fromString_PRIVATE_OPENSSH_DSA(self): + """ + Can load a private OpenSSH DSA key. + """ + sut = Key.fromString(OPENSSH_DSA_PRIVATE) + + self.checkParsedDSAPrivate1024(sut) + + def test_fromString_PRIVATE_OPENSSH_v1_DSA(self): + """ + Can load a private OpenSSH V1 DSA key. + """ + sut = Key.fromString(OPENSSH_V1_DSA_PRIVATE) + + self.checkParsedDSAPrivate1024(sut) + + def test_fromString_PRIVATE_OPENSSH_ECDSA(self): + """ + Can not load a private OPENSSH ECDSA. + """ + self.assertBadKey( + keydata.privateECDSA_256_openssh, + 'Key type \'EC\' not supported.' + ) + + def test_fromString_PRIVATE_OPENSSH_short(self): + """ + Raise an error when private OpenSSH key is too short. + """ + content = '-----BEGIN RSA PRIVATE KEY-----' + + self.assertKeyIsTooShort(content) + + content = '-----BEGIN RSA PRIVATE KEY-----\nAnother Line' + + self.assertBadKey( + content, + 'Failed to decode key (Bad Passphrase?): ' + 'Short octet stream on tag decoding') + + def test_fromString_PRIVATE_OPENSSH_bad_encoding(self): + """ + Raise an error when private OpenSSH key data can not be decoded. + """ + content = '-----BEGIN RSA PRIVATE KEY-----\nAnother Line\nLast' + + self.assertKeyParseError(content) + + def test_fromString_PRIVATE_SSHCOM_unencrypted_with_passphrase(self): + """ + When loading a unencrypted SSH.com private key with passhphrase + will raise BadKeyError. + """ + + with self.assertRaises(BadKeyError) as context: + Key.fromString(SSHCOM_RSA_PRIVATE_NO_PASSWORD, passphrase='pass') + + self.assertEqual( + 'SSH.com key not encrypted', + context.exception.message) + + def test_fromString_PRIVATE_SSHCOM_RSA_no_headers_no_password(self): + """ + Can load a private SSH.com key which has no headers and no password. + """ + sut = Key.fromString(SSHCOM_RSA_PRIVATE_NO_PASSWORD) + + self.checkParsedRSAPrivate1024(sut) + + def test_fromString_PRIVATE_SSHCOM_RSA_encrypted(self): + """ + Can load a private SSH.com key encrypted with password`. + """ + sut = Key.fromString( + SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase='chevah') + + self.checkParsedRSAPrivate1024(sut) + + def test_fromString_PRIVATE_SSHCOM_DSA_no_password(self): + """ + Can load a private SSH.com in DSA format. + """ + sut = Key.fromString(SSHCOM_DSA_PRIVATE_NO_PASSWORD) + + self.checkParsedDSAPrivate1024(sut) + + def test_fromString_PRIVATE_SSHCOM_short(self): + """ + Raise an exception when private key is too short. + """ + content = '---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----' + + self.assertKeyParseError(content) + + content = '---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\nnext line' + + self.assertKeyParseError(content) + + def test_fromString_PRIVATE_SSHCOM_RSA_encrypted_no_password(self): + """ + An exceptions is raised whey trying to load a private SSH.com key + which is encrypted, but without providing a password. + """ + with self.assertRaises(EncryptedKeyError) as context: + Key.fromString(SSHCOM_RSA_PRIVATE_WITH_PASSWORD) + + self.assertEqual( + 'Passphrase must be provided for an encrypted key.', + context.exception.message) + + def test_fromString_PRIVATE_SSHCOM_RSA_with_wrong_password(self): + """ + An exceptions is raised whey trying to load a private SSH.com key + which is encrypted, but providing a wrong password. + """ + with self.assertRaises(EncryptedKeyError) as context: + Key.fromString(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase='on') + + self.assertEqual( + 'Bad password or bad key format.', + context.exception.message) + + def test_fromString_PRIVATE_OPENSSH_bad_magic(self): + """ + Exception is raised when key data does not start with the key marker. + """ + content = """---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- +B2/56wAAAi4AAAA3 +---- END SSH2 ENCRYPTED PRIVATE KEY ----""" + + self.assertBadKey( + content, 'Bad magic number for SSH.com key "124778987"') + + def test_fromString_PRIVATE_OPENSSH_bad_key_type(self): + """ + Exception is raised when key has an unknown type. + """ + content = """---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- +P2/56wAAAi4AAAA3aWYtbW9kbntzaW== +---- END SSH2 ENCRYPTED PRIVATE KEY ----""" + + self.assertBadKey(content, 'Unknown SSH.com key type "if-modn{si"') + + def test_fromString_PRIVATE_OPENSSH_bad_structure(self): + """ + Exception is raised when key has no valid parts, ie too short. + """ + content = """---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- +P2/56wAAAi4AAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0e3JzYS +1wa2NzMXYyLW9hZXB9fQAAAARub25l +---- END SSH2 ENCRYPTED PRIVATE KEY ----""" + + self.assertKeyParseError(content) + + def test_fromString_X509_PEM_invalid_format(self): + """ + It fails to load invalid formated X509 PEM certificate. + """ + data = """-----BEGIN CERTIFICATE----- +MIIBNDCB66ADAgECAgEBMAoGCCqGSM49BAMCMDQxCzAJBgNVBAYTAkdCMQ8wDQYD +8J4wCgYIKoZIzj0EAwIDOAAwNQIZANYXcrq622yfNJSyjlzDvk3w59IaOlljqwIY +Gt7MBDMYYr8yfcZS94pZEUfhebR3CYAZ +-----END CERTIFICATE----- +""" + with self.assertRaises(BadKeyError) as context: + Key.fromString(data) + + self.assertStartsWith( + "Failed to load certificate. [('asn1 encoding routines'", + context.exception.message, + ) + + def test_fromString_X509_PEM_EC(self): + """ + EC public key from an X509 PEM certificate are not supported. + """ + data = """-----BEGIN CERTIFICATE----- +MIIBNDCB66ADAgECAgEBMAoGCCqGSM49BAMCMDQxCzAJBgNVBAYTAkdCMQ8wDQYD +VQQKEwZDaGV2YWgxFDASBgNVBAMTC3Rlc3QtZWMtc3NoMB4XDTE5MDYxOTEyNTQw +MFoXDTIwMDYxOTEyNTQwMFowNDELMAkGA1UEBhMCR0IxDzANBgNVBAoTBkNoZXZh +aDEUMBIGA1UEAxMLdGVzdC1lYy1zc2gwSTATBgcqhkjOPQIBBggqhkjOPQMBAQMy +AARzpUpSPLojoyouYH7HhSFV661wUKrRVqLyJlBb1cWU8f4wLZsGkXymZpAPClwu +8J4wCgYIKoZIzj0EAwIDOAAwNQIZANYXcrq622yfNJSyjlzDvk3w59IaOlljqwIY +Gt7MBDMYYr8yfcZS94pZEUfhebR3CYAZ +-----END CERTIFICATE----- +""" + with self.assertRaises(BadKeyError) as context: + Key.fromString(data) + + self.assertEqual( + 'Unsupported key found in the certificate.', + context.exception.message, + ) + + def test_fromString_PKCS1_PUBLIC_EC(self): + """ + It can extract RSA public key from an PKCS1 public EC PEM file. + """ + # This is the same as the X509 RSA cert. + # $ openssl x509 -in bla.cert -pubkey -noout + data = """-----BEGIN PUBLIC KEY----- +MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEc6VKUjy6I6MqLmB+x4UhVeutcFCq +0Vai8iZQW9XFlPH+MC2bBpF8pmaQDwpcLvCe +-----END PUBLIC KEY----- +""" + with self.assertRaises(BadKeyError) as context: + Key.fromString(data) + + self.assertEqual( + 'Unsupported key found in the X509 public PEM file.', + context.exception.message, + ) + + def test_fromString_X509_PEM_RSA(self): + """ + It can extract RSA public key from an X509 PEM certificate + """ + data = """-----BEGIN CERTIFICATE----- +MIICaDCCAdGgAwIBAgIBDjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJHQjEP +MA0GA1UEChMGQ2hldmFoMRIwEAYDVQQLEwlDaGV2YWggQ0ExEjAQBgNVBAMTCUNo +ZXZhaCBDQTAeFw0xNjA2MTUxNDM4MDBaFw0zNjA2MTUxNDM4MDBaMEgxCzAJBgNV +BAYTAkdCMQ8wDQYDVQQKEwZDaGV2YWgxFDASBgNVBAsTC0NoZXZhaCBUZXN0MRIw +EAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM6h +lRh3woxhut7nNkjBH5Xp07b5wJhVLjoEdtFuq3uBzOSghaEpapeL0/M4Rpw9ANjy +ulGy7rwJI9Me95aG53BrjMbBKk1qaHuNXa3PJjcgVmPelwPcbzk5Wl4E57dLN+eh +4Rf/Qyi9HBdtrDf19OzBmBs7W7pO9LPo5/usHlyVAgMBAAGjZDBiMBMGA1UdJQQM +MAoGCCsGAQUFBwMBMDgGA1UdHwQxMC8wLaAroCmGJ2h0dHA6Ly9sb2NhbGhvc3Q6 +ODA4MC9zb21lLWNoaWxkL2NhLmNybDARBglghkgBhvhCAQEEBAMCBkAwDQYJKoZI +hvcNAQEFBQADgYEAM8Ro0XZeIrR7+fi4pGMdqTAdNFNd2O86YgzpvGpUIbhmJnty +1k0aF2QNot4M6i6OhVQEwL4Ph/l6pbOnusv238nuzHyDHFWNPy1wV02hjacXF9EW +JZQaMjV9XxNTFOlNUTWswff3uE677wSVDPSuNkxo2FLRcGfPUxAQGsgL5Ts= +-----END CERTIFICATE----- +""" + + sut = Key.fromString(data) + + self.assertTrue(sut.isPublic()) + self.assertEqual('RSA', sut.type()) + self.assertEqual(1024, sut.size()) + + components = sut.data() + self.assertEqual(65537, components['e']) + n = int( + '14510135000543456324610075074919561379239940215773254633039625814' + '50191438083097108908667737243399472490927083264564327600896049375' + '92092816317169486450111458914839337717035721053431064458247582292' + '33425907841901335798792724220900289242783534069221630733833594745' + '1002424312049140771718167143894887320401855011989L' + ) + self.assertEqual(n, components['n']) + + def test_fromString_PKCS1_PUBLIC_PEM_invalid_format(self): + """ + It fails to load invalid formated PKCS1 public PEM file. + """ + data = """-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOoZUYd8KMYbre5zZIwR+V6dO2 +O1u6TvSz6Of7rB5clQIDAQAB +-----END PUBLIC KEY----- +""" + with self.assertRaises(BadKeyError) as context: + Key.fromString(data) + + self.assertStartsWith( + "Failed to load PKCS#1 public key. [('asn1 encoding routines'", + context.exception.message, + ) + + def test_fromString_PKCS1_PUBLIC_RSA(self): + """ + It can extract RSA public key from an PKCS1 public RSA PEM file. + """ + # This is the same as the X509 RSA cert. + # $ openssl x509 -in bla.cert -pubkey -noout + data = """-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOoZUYd8KMYbre5zZIwR+V6dO2 ++cCYVS46BHbRbqt7gczkoIWhKWqXi9PzOEacPQDY8rpRsu68CSPTHveWhudwa4zG +wSpNamh7jV2tzyY3IFZj3pcD3G85OVpeBOe3SzfnoeEX/0MovRwXbaw39fTswZgb +O1u6TvSz6Of7rB5clQIDAQAB +-----END PUBLIC KEY----- +""" + + sut = Key.fromString(data) + + self.assertTrue(sut.isPublic()) + self.assertEqual('RSA', sut.type()) + self.assertEqual(1024, sut.size()) + + components = sut.data() + self.assertEqual(65537, components['e']) + n = int( + '14510135000543456324610075074919561379239940215773254633039625814' + '50191438083097108908667737243399472490927083264564327600896049375' + '92092816317169486450111458914839337717035721053431064458247582292' + '33425907841901335798792724220900289242783534069221630733833594745' + '1002424312049140771718167143894887320401855011989L' + ) + self.assertEqual(n, components['n']) + + def test_fromString_X509_PEM_DSA(self): + """ + It can extract DSA public key from an X509 PEM certificate + """ + data = """-----BEGIN CERTIFICATE----- +MIICsDCCAm6gAwIBAgIBATALBglghkgBZQMEAwIwPTELMAkGA1UEBhMCR0IxDzAN +BgNVBAoTBkNoZXZhaDEdMBsGA1UEAxMUdGVzdC1zc2gtY29udmVyc3Rpb24wHhcN +MTkwNjE5MTIzNjAwWhcNMjAwNjE5MTIzNjAwWjA9MQswCQYDVQQGEwJHQjEPMA0G +A1UEChMGQ2hldmFoMR0wGwYDVQQDExR0ZXN0LXNzaC1jb252ZXJzdGlvbjCCAbcw +ggEsBgcqhkjOOAQBMIIBHwKBgQD/HJmstkyONrDh2iSafsRqxAzRG4dIUa70PdsE +gfMYBx95Nk1vhwGFyEQyCy305b2mgLG9+nkFkaLiD5UnoBbmO1NCggXlSNoe3ezq +akr80gV6dCwbM4T7B7lc3S0Eh5OJ2F5DKewzT65QyRrnkfECFlvjJqpeywhfucvg +nadoCwIVAIA92hGRUbX41P8zCqRBAMiEChlzAoGBALg27DhLThHhJHWdFX2gZYTm +NMjv/Z7mHCAda8/uqNXjAz97jI9w6KCSYIC7qiyl0lwGuW7kGqNCtnsZyxKWQzTy +HoONu9gfAmAxZbI3TuE49fYZJ/0m0mXyPpCg0VIeFJVcS6lA2W51UD1JrvCrUb1M +1SgNW+V/VHw6M54e+v1SA4GEAAKBgC/cCWpZpebhiEThZLd+eodR9vCntB8sIzrA +0JRCmi4t8vBOxLNAZQE7WdPWXZJA7d43+6B4//DZH+GOt6EoxLyPxcqM+GHqa99i +EwIuTKCIG6ucDtvzMSgwvYVFugfYaoJvu0Okc+6elNywpk9t3HLH5p2QbpPXPYgO +SH6qmzKdMAsGCWCGSAFlAwQDAgMvADAsAhR2vu0VK+loePjKDZcalym8vjgwkwIU +HNkVqo/9uKhSFkhbG6uKWUnOky0= +-----END CERTIFICATE----- +""" + + sut = Key.fromString(data) + + self.assertTrue(sut.isPublic()) + self.assertEqual('DSA', sut.type()) + self.assertEqual(1024, sut.size()) + + components = sut.data() + y = int( + '33608096932577498834618892325416552088960771123656082234885710486' + '75507586904443594643612585160476637613084634099891307779753871384' + '19072984388914093315900417736990449366567905225558889080164633948' + '75642330307431599331123161679260711587324602448450132263105327567' + '324900691359269978674482129301723462636106625693' + ) + p = int( + '17914554197956231476032656039682646299975055883332311875135017227' + '52180243454588892360869849018970437236700881503241838175380166833' + '56570852141623851276212449051705325396966909384918507908491159872' + '81118556760058432354600693107636249903432532125207156471720334839' + '5401646777661899361981163845950810903143363602443' + ) + g = int( + '12935985053463672691492638315705405640647316377002915690069266627' + '73032720642846501430445126372712764104983906841935717997673558164' + '74657088881395785073303554687569602926262408886111665706815822813' + '14448994749901282518897434324098506093655990924057550618491224583' + '7106339202519842112263186663472095769544164572498' + ) + self.assertEqual(y, components['y']) + self.assertEqual(p, components['p']) + self.assertEqual(g, components['g']) + self.assertEqual( + 732130160578857514768194964362219084190055012723, components['q']) + + def test_fromString_PCKS1_PUBLIC_DSA(self): + """ + It can extract RSA public key from an PKCS1 public DSA PEM file. + """ + # This is the same as the X509 DSA cert. + # $ openssl x509 -in bla.cert -pubkey -noout + data = """-----BEGIN PUBLIC KEY----- +MIIBtzCCASwGByqGSM44BAEwggEfAoGBAP8cmay2TI42sOHaJJp+xGrEDNEbh0hR +rvQ92wSB8xgHH3k2TW+HAYXIRDILLfTlvaaAsb36eQWRouIPlSegFuY7U0KCBeVI +2h7d7OpqSvzSBXp0LBszhPsHuVzdLQSHk4nYXkMp7DNPrlDJGueR8QIWW+Mmql7L +CF+5y+Cdp2gLAhUAgD3aEZFRtfjU/zMKpEEAyIQKGXMCgYEAuDbsOEtOEeEkdZ0V +faBlhOY0yO/9nuYcIB1rz+6o1eMDP3uMj3DooJJggLuqLKXSXAa5buQao0K2exnL +EpZDNPIeg4272B8CYDFlsjdO4Tj19hkn/SbSZfI+kKDRUh4UlVxLqUDZbnVQPUmu +8KtRvUzVKA1b5X9UfDoznh76/VIDgYQAAoGAL9wJalml5uGIROFkt356h1H28Ke0 +HywjOsDQlEKaLi3y8E7Es0BlATtZ09ZdkkDt3jf7oHj/8Nkf4Y63oSjEvI/Fyoz4 +Yepr32ITAi5MoIgbq5wO2/MxKDC9hUW6B9hqgm+7Q6Rz7p6U3LCmT23ccsfmnZBu +k9c9iA5IfqqbMp0= +-----END PUBLIC KEY----- +""" + + sut = Key.fromString(data) + + self.assertTrue(sut.isPublic()) + self.assertEqual('DSA', sut.type()) + self.assertEqual(1024, sut.size()) + + components = sut.data() + y = int( + '33608096932577498834618892325416552088960771123656082234885710486' + '75507586904443594643612585160476637613084634099891307779753871384' + '19072984388914093315900417736990449366567905225558889080164633948' + '75642330307431599331123161679260711587324602448450132263105327567' + '324900691359269978674482129301723462636106625693' + ) + p = int( + '17914554197956231476032656039682646299975055883332311875135017227' + '52180243454588892360869849018970437236700881503241838175380166833' + '56570852141623851276212449051705325396966909384918507908491159872' + '81118556760058432354600693107636249903432532125207156471720334839' + '5401646777661899361981163845950810903143363602443' + ) + g = int( + '12935985053463672691492638315705405640647316377002915690069266627' + '73032720642846501430445126372712764104983906841935717997673558164' + '74657088881395785073303554687569602926262408886111665706815822813' + '14448994749901282518897434324098506093655990924057550618491224583' + '7106339202519842112263186663472095769544164572498' + ) + self.assertEqual(y, components['y']) + self.assertEqual(p, components['p']) + self.assertEqual(g, components['g']) + self.assertEqual( + 732130160578857514768194964362219084190055012723, components['q']) + + def test_fromString_PRIVATE_PKCS8_invalid_format(self): + """ + It fails to load invalid formated PKCS8 PEM file. + """ + data = """-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAM6hlRh3woxhut7n +r3fAiJ9U0aDLrcUh +-----END PRIVATE KEY----- +""" + with self.assertRaises(BadKeyError) as context: + Key.fromString(data) + + self.assertStartsWith( + "Failed to load PKCS#8 PEM. [('asn1 encoding routines'", + context.exception.message, + ) + + def test_fromString_PRIVATE_PKCS8_RSA(self): + """ + It can extract RSA key from an PKCS8 private RSA PEM file, + without encryption. + """ + # openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in pkcs1.key + data = """-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBALh9Xq1JqQNIHpmi +/KAux/WIL0/e0kd49MoA+Q8BbOW7kFEdyYC7S5OOfsaGunFuONYzANU3Q7HPDu14 +jQ4QhWSmeVzIzovmYaT5fotzj6UB+nVjEJH2j34VcxZIk/faNHAj7guFZjGdhSV2 +8A7ksPP1B5HTIqKbByNFOgXr+OkvAgMBAAECgYAIHlxAO/GYF2BhWm7LjcN25ptO +ZHvUcVo0WX6cTm/AXFSpfSoU5CkbQTYK/nrN6w/NPUlYGKp99KKviJKMf+WeyRmc +z0nXF+megnkdPNwYdoFhUUQRdLp86zJZPXmhjspvqtEFOdZXQiez/TkeGnfyv9FH +87imgH7c3BAkOX/qAQJBAOgCdF0L7lLKOtnUlBRZSRqmJgciEWrqRa0abeRYmfQj +EIG0WEa+ohYnBkgCN/q1MoxSTpuMb2nsml61dSxOIMECQQDLkQNTYXjZebq29DMz +xsCCrt3b/HaIdG46QNRvVsrdjAHJOKGX0Euq91GFHGmXbURypakH9HMAVsZr7rQb ++JXvAkAFLPjXkoqQgj5p2ZosEgnVdFto0VO+JNfFEs/cxjU5Awc9PX6ypVIMWHaF +aLdC+oPUKYnjYnCh1ktjTXz9rgiBAkA6wKDIGPLLOcH0+egpQmzfit7HlkcTvR7v +OzTU6aTlano9fFXPPjQIpRbnJzsmlEfUGxH9FMV4TJM6JYvgItALAkB/gEX15UvF +FD0Dgyb0iS3iUXyKLqdrifF20TM/ynYs/20uodhShs1qfEH7syyLh/eBjK4p04ad +YeoPZTgdwt0x +-----END PRIVATE KEY----- +""" + sut = Key.fromString(data) + + self.checkParsedRSAPrivate1024(sut) + + def test_fromString_PRIVATE_PKCS8_RSA_ENCRYPTED(self): + """ + It can extract RSA key from an PKCS8 private RSA PEM file, + with encryption. + """ + # openssl pkcs8 -topk8 -inform PEM -outform PEM -in pkcs1.key + data = """-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIC3TBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQICxbcEPe+vjECAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAhBDpmQH4bpzIQSQqpw+GjBIIC +gMKX1CcvdGi6ZFxbhp9ycCnXU04bCsQrijAyYmndInf+EWSSTWpIzM86K6huOjdG +fKsTrmWb0bUM7LTu50GzNHwwGJgVMrUrL7rZQcTkht1D3mdLXWpanaCWyn2IYW8s +jXuzftEUn4AVHVzMeU95wlorgH33QlcAIDt/ZIDzeCfygsu3yJQW44kzWvp3/Eoy +tjBL+K6u7IRoHj67knh6YJ6cQxusK9cAFEpS8RfRLJpryAZyUfvwJteVK0LXQgcS +b8WsIwC+iv8E2QKExFmh4aoUsSsfOrdAb/H2iKTNU/qChCkeeYtzPFVLNmXYL1zG +9G80EGEKmaMgPTIt+oXx2cmY4W21jRGEQ/5KAUcLAWNR+3fEcDVdgfKxlCWQGSad +fQdemXnYhXW1emyb6RvWl0ml7f3ZzVFdeWgShLwx9ZVYdMT/ed4aCucK++XaXl55 +dK37TVTeVe6dzyhOADj8lNZ695Xt7+QO+O/hd+9K54xrjmt9TUKxFBbmS3Oqz9rI +T/0h4ym65OOio0CCePzj0vNrCvAD5rBo63B9Kjqxwnyzh2XmIBhUxcCzBEzm1pbS +FM6UHBQ3Jj595U0LGgParXRXxmt1A0i28Q9JhOQp5R1lxD+/q4q3eq/kV05bACyD +IdZR03u3euOWDtw0+Q6+DXvq53m1X1d9A4Dl14spNZoAdGnDLawrvdbWPvSeeXqR +5O9OYI0dake/SYROPlDvc2MgehllwSVU1IXdsrP3xChP2V4YupESRDcFcX+/zlph +HZ6BMxEKcYuIT9PKwhhp+FrwNo6J8mylpQLnCJ3hvXlhEPmyalg4rwVoeTHXRK6Y +TbW5RErmC8ifa/J4NdCv7MY= +-----END ENCRYPTED PRIVATE KEY----- +""" + sut = Key.fromString(data, passphrase='password') + + self.checkParsedRSAPrivate1024(sut) + + def test_fromString_PRIVATE_PKCS8_ENCRYPTED_no_pass(self): + """ + It fails to extract RSA key from an PKCS8 private RSA PEM file, + if no password is provided and file is encrypted. + """ + # openssl pkcs8 -topk8 -inform PEM -outform PEM -in pkcs1.key + data = """-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIC3TBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQICxbcEPe+vjECAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAhBDpmQH4bpzIQSQqpw+GjBIIC +gMKX1CcvdGi6ZFxbhp9ycCnXU04bCsQrijAyYmndInf+EWSSTWpIzM86K6huOjdG +fKsTrmWb0bUM7LTu50GzNHwwGJgVMrUrL7rZQcTkht1D3mdLXWpanaCWyn2IYW8s +jXuzftEUn4AVHVzMeU95wlorgH33QlcAIDt/ZIDzeCfygsu3yJQW44kzWvp3/Eoy +tjBL+K6u7IRoHj67knh6YJ6cQxusK9cAFEpS8RfRLJpryAZyUfvwJteVK0LXQgcS +b8WsIwC+iv8E2QKExFmh4aoUsSsfOrdAb/H2iKTNU/qChCkeeYtzPFVLNmXYL1zG +9G80EGEKmaMgPTIt+oXx2cmY4W21jRGEQ/5KAUcLAWNR+3fEcDVdgfKxlCWQGSad +fQdemXnYhXW1emyb6RvWl0ml7f3ZzVFdeWgShLwx9ZVYdMT/ed4aCucK++XaXl55 +dK37TVTeVe6dzyhOADj8lNZ695Xt7+QO+O/hd+9K54xrjmt9TUKxFBbmS3Oqz9rI +T/0h4ym65OOio0CCePzj0vNrCvAD5rBo63B9Kjqxwnyzh2XmIBhUxcCzBEzm1pbS +FM6UHBQ3Jj595U0LGgParXRXxmt1A0i28Q9JhOQp5R1lxD+/q4q3eq/kV05bACyD +IdZR03u3euOWDtw0+Q6+DXvq53m1X1d9A4Dl14spNZoAdGnDLawrvdbWPvSeeXqR +5O9OYI0dake/SYROPlDvc2MgehllwSVU1IXdsrP3xChP2V4YupESRDcFcX+/zlph +HZ6BMxEKcYuIT9PKwhhp+FrwNo6J8mylpQLnCJ3hvXlhEPmyalg4rwVoeTHXRK6Y +TbW5RErmC8ifa/J4NdCv7MY= +-----END ENCRYPTED PRIVATE KEY----- +""" + with self.assertRaises(EncryptedKeyError) as context: + Key.fromString(data) + + self.assertEqual( + 'Passphrase must be provided for an encrypted key', + context.exception.message, + ) + + def test_fromString_PRIVATE_PKCS8_DSA(self): + """ + It can extract DSA key from an PKCS8 private RSA PEM file, + without encryption. + """ + # Obtain from a P12 + # openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in pkcs1.key + data = """-----BEGIN PRIVATE KEY----- +MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAM7CQoaeZVn1tGXtkKf/BIQtXSfk +QuypVkU60GkeV4Q6K2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU +7YwoFF323b1+vbpW8JlA7FHh/OQ+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8 ++jCSPt+bNEfnGANNAhUAhhv+WNJRyWjpOI3CiIX71vJp8UkCgYBcD5MAYKYXZl41 +k3TiaDF7JPfggDDfs0aYss/9vKLzp0px3PmG2o+I5Zw2YXOsDtrSj56sTcH6aLAS +baxXP55VZ804IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLmGdI2Sj9aynRWdbe +rk4r+0+zWlHL7epZTDCuDmLOFiQF/AQWAhROTtpG/rmhN51iwDKLzQvymFgE3g== +-----END PRIVATE KEY----- +""" + sut = Key.fromString(data) + + self.checkParsedDSAPrivate1024(sut) + + def test_fromString_PRIVATE_PKCS8_EC(self): + """ + It fails to extract the EC key from an PKCS8 private EC PEM file, + """ + # openssl ecparam -name prime256v1 -genkey -noout -out private.ec.key + # openssl pkcs8 -topk8 -in private.ec.key -nocrypt + data = """-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrNfvVhrhJeyufkeZ +4oQ6i/kUFKudRU+xZ69FaAsw3MehRANCAASpL4fmdxdxbt317O8gV4Op5fVYwDnQ +7C/wsAsbx6monIz1qc1jje9RgggJL5pZ5HfbDInclQfV5T9rz6kWFEZS +-----END PRIVATE KEY----- +""" + with self.assertRaises(BadKeyError) as context: + Key.fromString(data) + + self.assertEqual( + 'Unsupported key found in the PKCS#8 private PEM file.', + context.exception.message, + ) + + def test_toString_SSHCOM_RSA_private_without_encryption(self): + """ + Can export a private RSA SSH.com without without encryption. + """ + sut = Key.fromString(OPENSSH_RSA_PRIVATE) + + result = sut.toString(type='sshcom') + + # Check that it looks like SSH.com private key. + self.assertEqual(SSHCOM_RSA_PRIVATE_NO_PASSWORD, result) + # Load the serialized key and see that we get the same key. + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + + def test_toString_SSHCOM_RSA_private_encrypted(self): + """ + Can export an encrypted private RSA SSH.com. + """ + sut = Key.fromString(OPENSSH_RSA_PRIVATE) + + result = sut.toString(type='sshcom', extra='chevah') + + # Check that it looks like SSH.com private key. + self.assertEqual(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, result) + # Load the serialized key and see that we get the same key. + reloaded = Key.fromString(result, passphrase='chevah') + self.assertEqual(sut, reloaded) + + def test_toString_SSHCOM_DSA_private(self): + """ + Can export a private DSA SSH.com key. + """ + sut = Key.fromString(OPENSSH_DSA_PRIVATE) + + result = sut.toString(type='sshcom') + + self.assertEqual(SSHCOM_DSA_PRIVATE_NO_PASSWORD, result) + # Load the serialized key and see that we get the same key. + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + + def test_fromString_PRIVATE_PUTTY_RSA_no_password(self): + """ + It can read private RSA keys in Putty format which are not + encrypted. + """ + sut = Key.fromString(PUTTY_RSA_PRIVATE_NO_PASSWORD) + + self.checkParsedRSAPrivate1024(sut) + + def test_fromString_PRIVATE_PUTTY_not_encrypted_with_passphrase(self): + """ + When loading a unencrypted PuTTY private key with passhphrase + will raise BadKeyError. + """ + with self.assertRaises(BadKeyError) as context: + Key.fromString(PUTTY_RSA_PRIVATE_NO_PASSWORD, passphrase='pass') + + self.assertEqual( + 'PuTTY key not encrypted', + context.exception.message) + + def test_fromString_PRIVATE_PUTTY_RSA_with_password(self): + """ + It can read private RSA keys in Putty format which are encrypted. + """ + sut = Key.fromString( + PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase='chevah') + + self.checkParsedRSAPrivate1024(sut) + + def test_fromString_PRIVATE_PUTTY_short(self): + """ + An exception is raised when key is too short. + """ + content = 'PuTTY-User-Key-File-2: ssh-rsa' + + self.assertKeyIsTooShort(content) + + content = ( + 'PuTTY-User-Key-File-2: ssh-rsa\n' + 'Encryption: aes256-cbc\n' + ) + + self.assertKeyIsTooShort(content) + + content = ( + 'PuTTY-User-Key-File-2: ssh-rsa\n' + 'Encryption: aes256-cbc\n' + 'Comment: bla\n' + ) + + self.assertKeyIsTooShort(content) + + def test_fromString_PRIVATE_PUTTY_RSA_bad_password(self): + """ + An exception is raised when password is not valid. + """ + with self.assertRaises(EncryptedKeyError) as context: + Key.fromString( + PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase='bad-pass') + + self.assertEqual( + 'Bad password or HMAC mismatch.', context.exception.message) + + def test_fromString_PRIVATE_PUTTY_RSA_missing_password(self): + """ + An exception is raised when key is encrypted but no password was + provided. + """ + with self.assertRaises(EncryptedKeyError) as context: + Key.fromString(PUTTY_RSA_PRIVATE_WITH_PASSWORD) + + self.assertEqual( + 'Passphrase must be provided for an encrypted key.', + context.exception.message) + + def test_fromString_PRIVATE_PUTTY_unsupported_type(self): + """ + An exception is raised when key contain a type which is not supported. + """ + content = """PuTTY-User-Key-File-2: ssh-bad +IGNORED +""" + self.assertBadKey( + content, 'Unsupported key type: ssh-bad') + + def test_fromString_PRIVATE_PUTTY_unsupported_encryption(self): + """ + An exception is raised when key contain an encryption method + which is not supported. + """ + content = """PuTTY-User-Key-File-2: ssh-dss +Encryption: aes126-cbc +IGNORED +""" + self.assertBadKey( + content, 'Unsupported encryption type: aes126-cbc') + + def test_fromString_PRIVATE_PUTTY_type_mismatch(self): + """ + An exception is raised when key header advertise one key type while + the public key another. + """ + content = """PuTTY-User-Key-File-2: ssh-rsa +Encryption: aes256-cbc +Comment: imported-openssh-key +Public-Lines: 4 +AAAAB3NzaC1kc3MAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK +APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk ++X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj +RToF6/jpLw== +IGNORED +""" + self.assertBadKey( + content, + ( + 'Mismatch key type. Header has "ssh-rsa",' + ' public has "ssh-dss"'), + ) + + def test_fromString_PRIVATE_PUTTY_hmac_mismatch(self): + """ + An exception is raised when key HMAC differs from the one + advertise by the key file. + """ + content = PUTTY_RSA_PRIVATE_NO_PASSWORD[:-1] + content += 'a' + + self.assertBadKey( + content, + 'HMAC mismatch: file declare ' + '"7630b86be300c6302ce1390fb264487bb61e67ca", actual is ' + '"7630b86be300c6302ce1390fb264487bb61e67ce"', + ) + + def test_fromString_PRIVATE_OpenSSH_DSA_no_password(self): + """ + It can read private DSA keys in OpenSSH format. + """ + sut = Key.fromString(OPENSSH_DSA_PRIVATE) + + self.checkParsedDSAPrivate1024(sut) + + def test_fromString_PRIVATE_PUTTY_DSA_no_password(self): + """ + It can read private DSA keys in Putty format which are not + encrypted. + """ + sut = Key.fromString(PUTTY_DSA_PRIVATE_NO_PASSWORD) + + self.checkParsedDSAPrivate1024(sut) + + def test_toString_PUTTY_RSA_plain(self): + """ + Can export to private RSA Putty without encryption. + """ + sut = Key.fromString(OPENSSH_RSA_PRIVATE) + + result = sut.toString(type='putty') + + # We can not check the exact text as comment is hardcoded in + # Twisted. + # Load the serialized key and see that we get the same key. + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + + def test_toString_PUTTY_RSA_encrypted(self): + """ + Can export to encrypted private RSA Putty key. + """ + sut = Key.fromString(OPENSSH_RSA_PRIVATE) + + result = sut.toString(type='putty', extra='write-pass') + + # We can not check the exact text as comment is hardcoded in + # Twisted. + # Load the serialized key and see that we get the same key. + reloaded = Key.fromString(result, passphrase='write-pass') + self.assertEqual(sut, reloaded) + + def test_toString_PUTTY_DSA_plain(self): + """ + Can export to private DSA Putty key without encryption. + """ + sut = Key.fromString(OPENSSH_DSA_PRIVATE) + + result = sut.toString(type='putty') + + # We can not check the exact text as comment is hardcoded in + # Twisted. + # Load the serialized key and see that we get the same key. + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + + def test_toString_PUTTY_public(self): + """ + Can export to public RSA Putty. + """ + sut = Key.fromString(OPENSSH_RSA_PRIVATE).public() + + result = sut.toString(type='putty') + + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + + def test_fromString_LSH(self): + """ + Test that keys are correctly generated from LSH strings. + """ + self._testPublicPrivateFromString( + keydata.publicRSA_lsh, + keydata.privateRSA_lsh, 'RSA', keydata.RSAData) + self._testPublicPrivateFromString( + keydata.publicDSA_lsh, + keydata.privateDSA_lsh, 'DSA', keydata.DSAData) + + sexp = sexpy.pack([['public-key', ['bad-key', ['p', '2']]]]) + self.assertRaises( + keys.BadKeyError, + keys.Key.fromString, + data='{' + base64.encodestring(sexp) + '}') + + sexp = sexpy.pack([['private-key', ['bad-key', ['p', '2']]]]) + self.assertRaises( + keys.BadKeyError, keys.Key.fromString, sexp) + + def test_toString_LSH(self): + """ + Test that the Key object generates LSH keys correctly. + """ + key = keys.Key.fromString(keydata.privateRSA_openssh) + self.assertEqual(key.toString('lsh'), keydata.privateRSA_lsh) + self.assertEqual( + key.public().toString('lsh'), keydata.publicRSA_lsh) + key = keys.Key.fromString(keydata.privateDSA_openssh) + self.assertEqual(key.toString('lsh'), keydata.privateDSA_lsh) + self.assertEqual( + key.public().toString('lsh'), keydata.publicDSA_lsh) + + def test_toString_AGENTV3(self): + """ + Test that the Key object generates Agent v3 keys correctly. + """ + key = keys.Key.fromString(keydata.privateRSA_openssh) + self.assertEqual(key.toString('agentv3'), keydata.privateRSA_agentv3) + key = keys.Key.fromString(keydata.privateDSA_openssh) + self.assertEqual(key.toString('agentv3'), keydata.privateDSA_agentv3) + + def test_fromString_AGENTV3(self): + """ + Test that keys are correctly generated from Agent v3 strings. + """ + self._testPrivateFromString( + keydata.privateRSA_agentv3, 'RSA', keydata.RSAData) + self._testPrivateFromString( + keydata.privateDSA_agentv3, 'DSA', keydata.DSAData) + self.assertRaises( + keys.BadKeyError, + keys.Key.fromString, + '\x00\x00\x00\x07ssh-foo' + '\x00\x00\x00\x01\x01' * 5) + + def test_fingerprint(self): + """ + Will return the md5 fingerprint with colons separator. + """ + key = keys.Key.fromString(keydata.privateRSA_openssh) + + result = key.fingerprint() + self.assertEqual(keydata.privateRSA_fingerprint_md5, result) + + def test_fingerprintdefault(self): + """ + Test that the fingerprint method returns fingerprint in + L{FingerprintFormats.MD5-HEX} format by default. + """ + rsaObj, dsaObj = self._getKeysForFingerprintTest() + + self.assertEqual( + keys.Key(rsaObj).fingerprint(), + '85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da') + self.assertEqual( + keys.Key(dsaObj).fingerprint(), + '63:15:b3:0e:e6:4f:50:de:91:48:3d:01:6b:b3:13:c1') + + def test_fingerprint_md5_hex(self): + """ + fingerprint method generates key fingerprint in + L{FingerprintFormats.MD5-HEX} format if explicitly specified. + """ + rsaObj, dsaObj = self._getKeysForFingerprintTest() + + self.assertEqual( + keys.Key(rsaObj).fingerprint( + keys.FingerprintFormats.MD5_HEX), + '85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da') + self.assertEqual( + keys.Key(dsaObj).fingerprint( + keys.FingerprintFormats.MD5_HEX), + '63:15:b3:0e:e6:4f:50:de:91:48:3d:01:6b:b3:13:c1') + + def test_fingerprintsha256(self): + """ + fingerprint method generates key fingerprint in + L{FingerprintFormats.SHA256-BASE64} format if explicitly specified. + """ + rsaObj, dsaObj = self._getKeysForFingerprintTest() + + self.assertEqual( + keys.Key(rsaObj).fingerprint( + keys.FingerprintFormats.SHA256_BASE64), + 'FBTCOoknq0mHy+kpfnY9tDdcAJuWtCpuQMaV3EsvbUI=') + self.assertEqual( + keys.Key(dsaObj).fingerprint( + keys.FingerprintFormats.SHA256_BASE64), + 'Wz5o2YbKyxOEcJn1au/UaALSVruUzfz0vaLI1xiIGyY=') + + def test_fingerprintsha1(self): + """ + fingerprint method generates key fingerprint in + L{FingerprintFormats.SHA1-BASE64} format if explicitly specified. + """ + rsaObj, dsaObj = self._getKeysForFingerprintTest() + + self.assertEqual( + keys.Key(rsaObj).fingerprint( + keys.FingerprintFormats.SHA1_BASE64), + 'tuUFlgv3kknie9WYExgS7OQj54k=') + self.assertEqual( + keys.Key(dsaObj).fingerprint( + keys.FingerprintFormats.SHA1_BASE64), + '9CCuTybG5aORtuW4jrFcp0PbK4U=') + + def test_fingerprintBadFormat(self): + """ + A C{BadFingerPrintFormat} error is raised when unsupported + formats are requested. + """ + rsaObj = self._getKeysForFingerprintTest()[0] + + with self.assertRaises(keys.BadFingerPrintFormat) as em: + keys.Key(rsaObj).fingerprint('sha256-base') + self.assertEqual( + 'Unsupported fingerprint format: sha256-base', + em.exception.args[0]) + + def test_sign(self): + """ + Test that the Key object generates correct signatures. + """ + key = keys.Key.fromString(keydata.privateRSA_openssh) + self.assertEqual(key.sign(''), self.rsaSignature) + key = keys.Key.fromString(keydata.privateDSA_openssh) + self.assertEqual(key.sign(''), self.dsaSignature) + + def test_verify(self): + """ + Test that the Key object correctly verifies signatures. + """ + key = keys.Key.fromString(keydata.publicRSA_openssh) + self.assertTrue(key.verify(self.rsaSignature, b'')) + self.assertFalse(key.verify(self.rsaSignature, b'a')) + self.assertFalse(key.verify(self.dsaSignature, b'')) + key = keys.Key.fromString(keydata.publicDSA_openssh) + self.assertTrue(key.verify(self.dsaSignature, b'')) + self.assertFalse(key.verify(self.dsaSignature, b'a')) + self.assertFalse(key.verify(self.rsaSignature, b'')) + + def test_verifyDSANoPrefix(self): + """ + Some commercial SSH servers send DSA keys as 2 20-byte numbers; + they are still verified as valid keys. + """ + key = keys.Key.fromString(keydata.publicDSA_openssh) + self.assertTrue(key.verify(self.dsaSignature[-40:], b'')) + + def test_repr(self): + """ + Test the pretty representation of Key. + """ + self.assertEqual( + repr(keys.Key(self.rsaObj)), + b"""\ +""") + + +class Test_generate_ssh_key_parser(ChevahTestCase, CommandLineMixin): + """ + Unit tests for generate_ssh_key_parser. + """ + + def setUp(self): + super(Test_generate_ssh_key_parser, self).setUp() + self.parser = ArgumentParser(prog='test-command') + self.subparser = self.parser.add_subparsers( + help='Available sub-commands', dest='sub_command') + + def test_default(self): + """ + It only need a subparser and sub-command name. + """ + generate_ssh_key_parser(self.subparser, 'key-gen') + + options = self.parseArguments(['key-gen']) + + self.assertNamespaceEqual({ + 'sub_command': 'key-gen', + 'key_comment': None, + 'key_file': None, + 'key_size': 2048, + 'key_type': 'rsa', + 'key_format': 'openssh_v1', + 'key_skip': False, + }, options) + + def test_value(self): + """ + Options are parsed from the command line. + """ + generate_ssh_key_parser(self.subparser, 'key-gen') + + options = self.parseArguments([ + 'key-gen', + '--key-comment', 'some comment', + '--key-file=id_dsa', + '--key-size', '1024', + '--key-type', 'dsa', + '--key-skip', + ]) + + self.assertNamespaceEqual({ + 'sub_command': 'key-gen', + 'key_comment': 'some comment', + 'key_file': 'id_dsa', + 'key_size': 1024, + 'key_type': 'dsa', + 'key_format': 'openssh_v1', + 'key_skip': True, + }, options) + + def test_default_overwrite(self): + """ + You can change default values. + """ + generate_ssh_key_parser( + self.subparser, 'key-gen', + default_key_size=1024, + default_key_type='dsa', + ) + + options = self.parseArguments(['key-gen']) + + self.assertNamespaceEqual({ + 'sub_command': 'key-gen', + 'key_comment': None, + 'key_file': None, + 'key_size': 1024, + 'key_type': 'dsa', + 'key_format': 'openssh_v1', + 'key_skip': False, + }, options) + + +class Testgenerate_ssh_key(ChevahTestCase, CommandLineMixin): + """ + Tests for generate_ssh_key. + """ + + def setUp(self): + super(Testgenerate_ssh_key, self).setUp() + self.parser = ArgumentParser(prog='test-command') + self.sub_command_name = 'gen-ssh-key' + subparser = self.parser.add_subparsers( + help='Available sub-commands', dest='sub_command') + generate_ssh_key_parser(subparser, self.sub_command_name) + + def assertPathEqual(self, expected, actual): + """ + Check that pats are equal. + """ + if self.os_family == 'posix': + expected = expected.encode('utf-8') + self.assertEqual(expected, actual) + + def test_generate_ssh_key_custom_values(self): + """ + When custom values are provided, the key is generated using those + values. + """ + file_name = mk.ascii().decode('ascii') + file_name_pub = file_name + '.pub' + options = self.parseArguments([ + self.sub_command_name, + u'--key-size=512', + u'--key-type=DSA', + u'--key-file=' + file_name, + u'--key-comment=this is a comment', + ]) + open_method = DummyOpenContext() + + exit_code, message, key = generate_ssh_key( + options, open_method=open_method) + + self.assertEqual('DSA', key.type()) + self.assertEqual(512, key.size()) + + # First it writes the private key. + first_file = open_method.calls.pop(0) + + self.assertPathEqual( + _path(file_name), first_file['path']) + self.assertEqual('wb', first_file['mode']) + self.assertEqual( + key.toString('openssh'), first_file['stream'].getvalue()) + + # Second it writes the public key. + second_file = open_method.calls.pop(0) + self.assertPathEqual( + _path(file_name_pub.decode('ascii')), second_file['path']) + self.assertEqual('wb', second_file['mode']) + self.assertEqual( + key.public().toString('openssh', 'this is a comment'), + second_file['stream'].getvalue()) + + self.assertEqual( + u'SSH key of type "dsa" and length "512" generated as public ' + u'key file "%s" and private key file "%s" ' + u'having comment "this is a comment".' % ( + file_name_pub, file_name), + message, + ) + self.assertEqual(0, exit_code) + + def test_generate_ssh_key_default_values(self): + """ + When no path and no comment are provided, it will use default + values. + """ + options = self.parseArguments([ + self.sub_command_name, + '--key-size=1024', + '--key-type=RSA', + ]) + open_method = DummyOpenContext() + + exit_code, message, key = generate_ssh_key( + options, open_method=open_method) + + self.assertEqual('RSA', key.type()) + self.assertEqual(1024, key.size()) + + # First it writes the private key. + first_file = open_method.calls.pop(0) + self.assertPathEqual(_path(u'id_rsa'), first_file['path']) + self.assertEqual('wb', first_file['mode']) + self.assertEqual( + key.toString('openssh'), first_file['stream'].getvalue()) + + # Second it writes the public key. + second_file = open_method.calls.pop(0) + self.assertPathEqual(u'id_rsa.pub', second_file['path']) + self.assertEqual('wb', second_file['mode']) + self.assertEqual( + key.public().toString('openssh'), second_file['stream'].getvalue()) + + # Message informs what default values were used. + self.assertEqual( + u'SSH key of type "rsa" and length "1024" generated as public ' + u'key file "id_rsa.pub" and private key file "id_rsa" without ' + u'a comment.', + message, + ) + + def test_generate_ssh_key_private_exist_no_migration(self): + """ + When no migration is done it will not generate the key, + if private file already exists and exit with error. + """ + self.test_segments = mk.fs.createFileInTemp() + path = mk.fs.getRealPathFromSegments(self.test_segments) + options = self.parseArguments([ + self.sub_command_name, + '--key-type=RSA', + '--key-size=2048', + '--key-file', path, + ]) + open_method = DummyOpenContext() + + exit_code, message, key = generate_ssh_key( + options, open_method=open_method) + + self.assertEqual(1, exit_code) + self.assertEqual(u'Private key already exists. %s' % path, message) + # Open is not called. + self.assertIsEmpty(open_method.calls) + + def test_generate_ssh_key_private_exist_skip(self): + """ + On skip, will not generate the key if private file already + exists and exit without error. + """ + self.test_segments = mk.fs.createFileInTemp() + path = mk.fs.getRealPathFromSegments(self.test_segments) + options = self.parseArguments([ + self.sub_command_name, + '--key-skip', + '--key-type=RSA', + '--key-size=2048', + '--key-file', path, + ]) + open_method = DummyOpenContext() + + exit_code, message, key = generate_ssh_key( + options, open_method=open_method) + + self.assertEqual(0, exit_code) + self.assertEqual(u'Key already exists.', message) + # Open is not called. + self.assertIsEmpty(open_method.calls) + + def test_generate_ssh_key_public_exist(self): + """ + Will not generate the key, if public file already exists. + """ + self.test_segments = mk.fs.createFileInTemp(suffix='.pub') + path = mk.fs.getRealPathFromSegments(self.test_segments) + options = self.parseArguments([ + self.sub_command_name, + '--key-type=RSA', + '--key-size=2048', + # path is for public key, but we pass the private path. + '--key-file', path[:-4], + ]) + open_method = DummyOpenContext() + + exit_code, message, key = generate_ssh_key( + options, open_method=open_method) + + self.assertEqual(1, exit_code) + self.assertEqual(u'Public key already exists. %s' % path, message) + # Open is not called. + self.assertIsEmpty(open_method.calls) + + def test_generate_ssh_key_fail_to_write(self): + """ + Will return an error when failing to write the key. + """ + options = self.parseArguments([ + self.sub_command_name, + '--key-type=RSA', + '--key-size=1024', + '--key-file', 'no-such-parent/ssh.key', + ]) + + exit_code, message, key = generate_ssh_key(options) + + self.assertEqual(1, exit_code) + self.assertEqual( + "[Errno 2] No such file or directory: 'no-such-parent/ssh.key'", + message) diff --git a/src/chevah_keycert/tests/test_ssl.py b/src/chevah_keycert/tests/test_ssl.py new file mode 100644 index 0000000..cf51b84 --- /dev/null +++ b/src/chevah_keycert/tests/test_ssl.py @@ -0,0 +1,568 @@ +# Copyright (c) 2015 Adi Roiban. +# See LICENSE for details. +""" +Test for SSL keys/cert management. +""" +from __future__ import unicode_literals +from __future__ import absolute_import +from argparse import ArgumentParser + +from bunch import Bunch +from chevah_compat.testing import mk, ChevahTestCase +from OpenSSL import crypto + +from chevah_keycert.exceptions import KeyCertException +from chevah_keycert.ssl import ( + generate_and_store_csr, + generate_csr, + generate_csr_parser, + generate_self_signed_parser, + generate_ssl_self_signed_certificate, + ) +from chevah_keycert.tests.helpers import CommandLineMixin + +RSA_PRIVATE = b"""-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKAPkPAWzlu5BRHcmA +u0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAfp1YxCR +9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLwIDAQAB +AoGACB5cQDvxmBdgYVpuy43DduabTmR71HFaNFl+nE5vwFxUqX0qFOQpG0E2Cv56 +zesPzT1JWBiqffSir4iSjH/lnskZnM9J1xfpnoJ5HTzcGHaBYVFEEXS6fOsyWT15 +oY7Kb6rRBTnWV0Ins/05Hhp38r/RR/O4poB+3NwQJDl/6gECQQDoAnRdC+5SyjrZ +1JQUWUkapiYHIhFq6kWtGm3kWJn0IxCBtFhGvqIWJwZIAjf6tTKMUk6bjG9p7Jpe +tXUsTiDBAkEAy5EDU2F42Xm6tvQzM8bAgq7d2/x2iHRuOkDUb1bK3YwByTihl9BL +qvdRhRxpl21EcqWpB/RzAFbGa+60G/iV7wJABSz415KKkII+admaLBIJ1XRbaNFT +viTXxRLP3MY1OQMHPT1+sqVSDFh2hWi3QvqD1CmJ42JwodZLY018/a4IgQJAOsCg +yBjyyznB9PnoKUJs34rex5ZHE70e7zs01Omk5Wp6PXxVzz40CKUW5yc7JpRH1BsR +/RTFeEyTOiWL4CLQCwJAf4BF9eVLxRQ9A4Mm9Ikt4lF8ii6na4nxdtEzP8p2LP9t +LqHYUobNanxB+7Msi4f3gYyuKdOGnWHqD2U4HcLdMQ== +-----END RSA PRIVATE KEY----- +""" + + +class CommandLineTestBase(ChevahTestCase, CommandLineMixin): + """ + Share code for testing methods which read SSL command line input. + """ + + def setUp(self): + super(CommandLineTestBase, self).setUp() + self.parser = ArgumentParser(prog='test-command') + subparser = self.parser.add_subparsers( + help='Available sub-commands', dest='sub_command') + self.command_name = 'gen-csr' + generate_csr_parser(subparser, self.command_name) + generate_self_signed_parser(subparser, 'self-gen') + + +class Test_generate_ssl_self_signed_certificate(CommandLineTestBase): + """ + Unit tests for generate_ssl_self_signed_certificate. + """ + + def test_generate(self): + """ + Will generate the key and self signed certificate for current + hostname. + """ + options = self.parseArguments([ + 'self-gen', + '--common-name', 'domain.com', + '--key-size=1024', + '--alternative-name=DNS:ex.com,IP:1.2.3.4', + '--constraints=critical,CA:TRUE', + '--key-usage=server-authentication,crl-sign', + '--sign-algorithm=sha512', + '--email=dev@chevah.com', + '--state=MS', + '--locality=Cluj', + '--organization=Chevah Team', + '--organization-unit=DevTeam', + '--country=UN', + ]) + + cert_pem, key_pem = generate_ssl_self_signed_certificate(options) + + key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem) + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem) + self.assertEqual(1024, key.bits()) + self.assertEqual(crypto.TYPE_RSA, key.type()) + self.assertEqual(u'domain.com', cert.get_subject().CN) + + self.assertEqual(u'dev@chevah.com', cert.get_subject().emailAddress) + + self.assertEqual(u'MS', cert.get_subject().ST) + self.assertEqual(u'Cluj', cert.get_subject().L) + self.assertEqual(u'Chevah Team', cert.get_subject().O) + self.assertEqual(u'DevTeam', cert.get_subject().OU) + self.assertEqual(u'UN', cert.get_subject().C) + + self.assertNotEqual(0, cert.get_serial_number()) + issuer = cert.get_issuer() + self.assertEqual(cert.subject_name_hash(), issuer.hash()) + + constraints = cert.get_extension(0) + self.assertEqual(b'basicConstraints', constraints.get_short_name()) + self.assertTrue(constraints.get_critical()) + self.assertEqual(b'0\x03\x01\x01\xff', constraints.get_data()) + + key_usage = cert.get_extension(1) + self.assertEqual(b'keyUsage', key_usage.get_short_name()) + self.assertFalse(key_usage.get_critical()) + + extended_usage = cert.get_extension(2) + self.assertEqual(b'extendedKeyUsage', extended_usage.get_short_name()) + self.assertFalse(extended_usage.get_critical()) + + alt_name = cert.get_extension(3) + self.assertEqual(b'subjectAltName', alt_name.get_short_name()) + self.assertFalse(alt_name.get_critical()) + self.assertEqual( + b'0\x0e\x82\x06ex.com\x87\x04\x01\x02\x03\x04', + alt_name.get_data()) + + def test_generate_basic_options(self): + """ + Can generate using just common name as the options. + """ + options = Bunch(common_name='test') + + cert_pem, key_pem = generate_ssl_self_signed_certificate(options) + + key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem) + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem) + self.assertEqual(2048, key.bits()) + self.assertEqual(crypto.TYPE_RSA, key.type()) + self.assertEqual(u'test', cert.get_subject().CN) + self.assertIsNone(cert.get_subject().C) + self.assertNotEqual(0, cert.get_serial_number()) + issuer = cert.get_issuer() + self.assertEqual(cert.subject_name_hash(), issuer.hash()) + # No extensions are set. + self.assertEqual(0, cert.get_extension_count()) + + +class Test_generate_csr_parser( + ChevahTestCase, CommandLineMixin): + """ + Unit tests for generate_csr_parser. + """ + + def setUp(self): + super(Test_generate_csr_parser, self).setUp() + self.parser = ArgumentParser(prog='test-command') + self.subparser = self.parser.add_subparsers( + help='Available sub-commands', dest='sub_command') + + def test_common_name_required(self): + """ + It can not be called without at least the common-name argument + """ + generate_csr_parser(self.subparser, 'key-gen') + + code, error = self.parseArgumentsFailure(['key-gen']) + + self.assertStartsWith('usage: test-command key-gen [-h]', error) + self.assertEndsWith( + '\ntest-command key-gen: ' + 'error: argument --common-name is required\n', + error) + + def test_default(self): + """ + It can be initialized with only a subparser and sub-command name. + """ + generate_csr_parser(self.subparser, 'key-gen') + + options = self.parseArguments([ + 'key-gen', + '--common-name', 'domain.com', + ]) + + self.assertNamespaceEqual({ + 'sub_command': 'key-gen', + 'key': None, + 'key_file': 'server.key', + 'key_size': 2048, + 'key_password': None, + 'common_name': 'domain.com', + 'alternative_name': None, + 'email': None, + 'organization': None, + 'organization_unit': None, + 'locality': None, + 'state': None, + 'country': None, + 'constraints': '', + 'key_usage': '', + 'sign_algorithm': 'sha256', + }, options) + + def test_value(self): + """ + Options are parsed form command line. + """ + generate_csr_parser(self.subparser, 'key-gen') + + options = self.parseArguments([ + 'key-gen', + '--common-name', 'sub.domain.com', + '--key-file=my_server.pem', + '--key-size', '1024', + '--key-password', u'valu\u20ac', + '--alternative-name', 'DNS:www.domain.com,IP:127.0.0.1', + '--email', 'admin@domain.com', + '--organization', 'OU Name', + '--organization-unit=OU Unit', + '--locality=somewhere', + '--state=without', + '--country=GB', + '--constraints=critical,CA:FALSE', + '--key-usage=crl-sign', + '--sign-algorithm=sha1', + ]) + + self.assertNamespaceEqual({ + 'sub_command': 'key-gen', + 'key': None, + 'key_file': 'my_server.pem', + 'key_size': 1024, + 'key_password': u'valu\u20ac', + 'common_name': 'sub.domain.com', + 'alternative_name': 'DNS:www.domain.com,IP:127.0.0.1', + 'email': 'admin@domain.com', + 'organization': 'OU Name', + 'organization_unit': 'OU Unit', + 'locality': 'somewhere', + 'state': 'without', + 'country': 'GB', + 'constraints': 'critical,CA:FALSE', + 'key_usage': 'crl-sign', + 'sign_algorithm': 'sha1', + + }, options) + + def test_default_overwrite(self): + """ + You can change default values. + """ + generate_csr_parser( + self.subparser, 'key-gen', + default_key_size=1024, + ) + + options = self.parseArguments([ + 'key-gen', + '--common-name', 'domain.com', + ]) + + self.assertNamespaceEqual({ + 'sub_command': 'key-gen', + 'key': None, + 'key_file': 'server.key', + 'key_size': 1024, + 'key_password': None, + 'common_name': 'domain.com', + 'alternative_name': None, + 'email': None, + 'organization': None, + 'organization_unit': None, + 'locality': None, + 'state': None, + 'country': None, + 'constraints': '', + 'key_usage': '', + 'sign_algorithm': 'sha256', + }, options) + + +class Test_generate_csr(CommandLineTestBase): + """ + Unit tests for generate_csr. + """ + + def test_bad_size(self): + """ + Raise an exception when failing to generate the key. + """ + options = self.parseArguments([ + self.command_name, + '--common-name=domain.com', + '--key-size=12', + ]) + + with self.assertRaises(KeyCertException) as context: + generate_csr(options) + + self.assertEqual( + 'Key size must be greater or equal to 512.', + context.exception.message) + + def test_bad_country_long(self): + """ + Raise an exception when country code is not correct. + """ + options = self.parseArguments([ + self.command_name, + '--common-name=domain.com', + '--country=USA', + ]) + + with self.assertRaises(KeyCertException) as context: + generate_csr(options) + + self.assertEqual('Invalid country code.', context.exception.message) + + def test_bad_country_short(self): + """ + Raise an exception when country code is not correct. + """ + options = self.parseArguments([ + self.command_name, + '--common-name=domain.com', + '--country=A', + ]) + + with self.assertRaises(KeyCertException) as context: + generate_csr(options) + + self.assertEqual('Invalid country code.', context.exception.message) + + def test_sign_algorithm_invalid(self): + """ + Raise an exception when the signing algorithm is not correct. + """ + options = self.parseArguments([ + self.command_name, + '--common-name=domain.com', + '--sign-algorithm=unknown', + ]) + + with self.assertRaises(KeyCertException) as context: + generate_csr(options) + + self.assertEqual( + 'Invalid signing algorithm. ' + 'Supported values: md5, sha1, sha256, sha512.', + context.exception.message) + + def test_email_invalid(self): + """ + Raise an exception when the email adress is not correct. + """ + options = self.parseArguments([ + self.command_name, + '--common-name=domain.com', + '--email=invalid', + ]) + + with self.assertRaises(KeyCertException) as context: + generate_csr(options) + + self.assertEqual('Invalid email address.', context.exception.message) + + def test_default_gen(self): + """ + By default it will serialized the key without password and generate + the csr without alternative name and just the common name. + """ + options = self.parseArguments([ + self.command_name, + '--common-name=domain.com', + ]) + + result = generate_csr(options) + + # OpenSSL.crypto.PKey has no equality so we need to compare the + # serialization. + self.assertEqual(2048, result['key'].bits()) + self.assertEqual(crypto.TYPE_RSA, result['key'].type()) + key = crypto.dump_privatekey(crypto.FILETYPE_PEM, result['key']) + self.assertEqual(key, result['key_pem']) + # For CSR we can not get extensions so we only check the subject. + csr = crypto.dump_certificate_request( + crypto.FILETYPE_PEM, result['csr']) + self.assertEqual(csr, result['csr_pem']) + subject = result['csr'].get_subject() + self.assertEqual(u'domain.com', subject.commonName) + self.assertIsNone(subject.emailAddress) + self.assertIsNone(subject.organizationName) + self.assertIsNone(subject.organizationalUnitName) + self.assertIsNone(subject.localityName) + self.assertIsNone(subject.stateOrProvinceName) + self.assertIsNone(subject.countryName) + self.assertEqual(2, result['csr'].get_version()) + + def test_gen_unicode(self): + """ + Domains are encoded using IDNA and names using Unicode. + """ + options = self.parseArguments([ + self.command_name, + u'--common-name=domain-\u20acuro.com', + u'--key-size=512', + u'--alternative-name=DNS:www.domain-\u20acuro.com,IP:127.0.0.1', + u'--email=name@domain-\u20acuro.com', + u'--organization=OU Nam\u20acuro', + u'--organization-unit=OU Unit\u20acuro', + u'--locality=Som\u20acwhere', + u'--state=Stat\u20ac', + u'--country=GB', + ]) + + result = generate_csr(options) + + csr = crypto.dump_certificate_request( + crypto.FILETYPE_PEM, result['csr']) + self.assertEqual(csr, result['csr_pem']) + subject = result['csr'].get_subject() + self.assertEqual(u'xn--domain-uro-x77e.com', subject.commonName) + self.assertEqual( + u'name@xn--domain-uro-x77e.com', subject.emailAddress) + self.assertEqual(u'OU Nam\u20acuro', subject.organizationName) + self.assertEqual(u'OU Unit\u20acuro', subject.organizationalUnitName) + self.assertEqual(u'Som\u20acwhere', subject.localityName) + self.assertEqual(u'Stat\u20ac', subject.stateOrProvinceName) + self.assertEqual(u'GB', subject.countryName) + + def test_encrypted_key(self): + """ + When asked it will serialize the key with a password. + """ + options = self.parseArguments([ + self.command_name, + u'--common-name=domain.com', + u'--key-size=512', + u'--key-password=\u20acuro', + ]) + + result = generate_csr(options) + + # We decrypt the key and compare the unencrypted serialization. + key = crypto.load_privatekey( + crypto.FILETYPE_PEM, + result['key_pem'], + u'\u20acuro'.encode('utf-8')) + self.assertEqual( + crypto.dump_privatekey(crypto.FILETYPE_PEM, key), + crypto.dump_privatekey(crypto.FILETYPE_PEM, result['key']), + ) + + def test_existing_key_string(self): + """ + It can generate a CSR from an existing private key as text. + """ + key_pem = RSA_PRIVATE + + options = self.parseArguments([ + self.command_name, + '--common-name=domain.com', + ]) + options.key = key_pem + + result = generate_csr(options) + + # OpenSSL.crypto.PKey has no equality so we need to compare the + # serialization. + self.assertEqual(1024, result['key'].bits()) + self.assertEqual(crypto.TYPE_RSA, result['key'].type()) + self.assertEqual(key_pem, result['key_pem']) + # For CSR we can not get extensions so we only check the subject. + csr = crypto.dump_certificate_request( + crypto.FILETYPE_PEM, result['csr']) + self.assertEqual(csr, result['csr_pem']) + subject = result['csr'].get_subject() + self.assertEqual(u'domain.com', subject.commonName) + + def test_existing_key_path(self): + """ + It can generate a CSR from an existing private key file. + """ + key_pem = RSA_PRIVATE + key_path, _ = self.tempFile(content=key_pem) + + options = self.parseArguments([ + self.command_name, + '--key', key_path, + '--common-name=domain.com', + ]) + + result = generate_csr(options) + + # OpenSSL.crypto.PKey has no equality so we need to compare the + # serialization. + self.assertEqual(1024, result['key'].bits()) + self.assertEqual(crypto.TYPE_RSA, result['key'].type()) + self.assertEqual(key_pem, result['key_pem']) + # For CSR we can not get extensions so we only check the subject. + csr = crypto.dump_certificate_request( + crypto.FILETYPE_PEM, result['csr']) + self.assertEqual(csr, result['csr_pem']) + subject = result['csr'].get_subject() + self.assertEqual(u'domain.com', subject.commonName) + + +class Test_generate_and_store_csr(CommandLineTestBase): + """ + Unit tests for generate_and_store_csr. + """ + + def test_key_exists(self): + """ + Raise an exception when server key already exists. + """ + self.test_segments = mk.fs.createFileInTemp() + path = mk.fs.getRealPathFromSegments(self.test_segments) + options = self.parseArguments([ + self.command_name, + '--common-name=domain.com', + '--key-file', path, + ]) + + with self.assertRaises(KeyCertException) as context: + generate_and_store_csr(options) + + self.assertEqual('Key file already exists.', context.exception.message) + + def test_key_and_csr(self): + """ + Will write the key an csr on local filesystem. + """ + key_path, self.test_segments = mk.fs.makePathInTemp() + csr_segments = self.test_segments[:] + csr_segments[-1] = u'%s.csr' % csr_segments[-1] + self.addCleanup(mk.fs.deleteFile, csr_segments) + + options = self.parseArguments([ + self.command_name, + '--common-name=domain.com', + '--key-file', key_path, + '--key-size=512', + ]) + + generate_and_store_csr(options) + + key_content = mk.fs.getFileContent(self.test_segments) + key = crypto.load_privatekey( + crypto.FILETYPE_PEM, key_content) + self.assertEqual(512, key.bits()) + csr_content = mk.fs.getFileContent(csr_segments) + csr = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_content) + self.assertEqual(u'domain.com', csr.get_subject().CN) + + def test_store_error(self): + """ + Raise an exception when failing to write the file. + """ + options = self.parseArguments([ + self.command_name, + '--common-name=domain.com', + '--key-file', 'no-such/parent/key.file', + '--key-size=512', + ]) + + with self.assertRaises(KeyCertException) as context: + generate_and_store_csr(options) + + self.assertStartsWith( + "[Errno 2] No such file or directory: ", + context.exception.message) From 17d7c46e88f73279be2db07823ca8b9a4e39718a Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Tue, 21 Mar 2023 00:37:39 +0000 Subject: [PATCH 03/41] Fix compat dependency --- setup.cfg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 69b631c..4348c57 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,12 +12,12 @@ url = 'https://github.com/chevah/chevah-keycert' [options] install_requires = - pyopenssl >=0.13 - pyasn1 >=0.1.7 + pyopenssl >= 0.13 + pyasn1 >= 0.1.7 cryptography >= 3.2 - chevah-compat >= 1 + chevah-compat >= 0.70 scandir >= 1.7 - constantly >=15.1.0 + constantly >= 15.1.0 packages = find: package_dir = =src From f638c905db0e52116cf839385f41de0ecadad5a9 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Tue, 21 Mar 2023 00:38:40 +0000 Subject: [PATCH 04/41] Add pythia scripts. --- pythia.conf | 12 + pythia.sh | 922 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 934 insertions(+) create mode 100644 pythia.conf create mode 100755 pythia.sh diff --git a/pythia.conf b/pythia.conf new file mode 100644 index 0000000..3360b1a --- /dev/null +++ b/pythia.conf @@ -0,0 +1,12 @@ +BASE_REQUIREMENTS='chevah-brink==1.0.7 paver==1.2.4 six==1.16.0' +PYTHON_CONFIGURATION='default@3.8.6.3b1a8ba' +# For production packages there are 2 options: +BINARY_DIST_URI='https://github.com/chevah/pythia/releases/download' +#BINARY_DIST_URI='https://bin.chevah.com:20443/production' +# For testing packages, make sure this one is the last uncommented instance: +#BINARY_DIST_URI='https://bin.chevah.com:20443/testing' +PIP_INDEX_URL='https://bin.chevah.com:20443/pypi/simple' +# There are 2 build directories used in this repo: +# * $BUILD_DIR is used for building libffi / OpenSSL / Python / etc. +# * $CHEVAH_BUILD_DIR is used by the Python that builds the above. +CHEVAH_BUILD_DIR='build-py3' diff --git a/pythia.sh b/pythia.sh new file mode 100755 index 0000000..baa9dd5 --- /dev/null +++ b/pythia.sh @@ -0,0 +1,922 @@ +#!/usr/bin/env bash +# Copyright (c) 2010-2020 Adi Roiban. +# See MIT LICENSE for details. +# +# This file has no version. Documentation is found in this comment. +# +# Helper script for bootstrapping a Python based build system on Unix/Msys. +# +# It is similar with a python-virtualenv but it will not used the local +# Python version and can be used on systems without a local Python. +# +# It will delegate the argument to the execute_venv function, +# with the exception of these commands: +# * clean - remove everything, except cache +# * purge - remove (empty) the cache +# * get_python - download Python distribution in cache +# +# It exports the following environment variables: +# * PYTHONPATH - path to the build directory +# * CHEVAH_PYTHON - name of the python versions +# * CHEVAH_OS - name of the current OS +# * CHEVAH_ARCH - CPU type of the current OS +# +# The build directory is used from CHEVAH_BUILD env, +# then read from pythia.conf as CHEVAH_BUILD_DIR, +# and will use a default value if not defined there. +# +# The cache directory is read the CHEVAH_CACHE env, +# and then read from pythia.conf as CHEVAH_CACHE_DIR, +# and will use a default value if not defined. +# +# You can define your own `execute_venv` function in pythia.conf with the +# command used to execute Python inside the newly virtual environment. +# + +# Bash checks +set -o nounset # always check if variables exist +set -o errexit # always exit on error +set -o errtrace # trap errors in functions as well +set -o pipefail # don't ignore exit codes when piping output + +# Initialize default value. +COMMAND=${1-''} +DEBUG=${DEBUG-0} + +# Set default locale. +# We use C (alias for POSIX) for having a basic default value and +# to make sure we explicitly convert all unicode values. +export LANG='C' +export LANGUAGE='C' +export LC_ALL='C' +export LC_CTYPE='C' +export LC_COLLATE='C' +export LC_MESSAGES='C' +export PATH=$PATH:'/sbin:/usr/sbin:/usr/local/bin' + +# +# Global variables. +# +# Used to return non-scalar value from functions. +RESULT='' +WAS_PYTHON_JUST_INSTALLED=0 +DIST_FOLDER='dist' + +# Path global variables. + +# Configuration variable. +CHEVAH_BUILD_DIR="" +# Variale used at runtime. +BUILD_FOLDER="" + +# Configuration variable +CHEVAH_CACHE_DIR= +# Varible used at runtime. +CACHE_FOLDER="" + +PYTHON_BIN="" +PYTHON_LIB="" +LOCAL_PYTHON_BINARY_DIST="" + +# Put default values and create them as global variables. +OS='not-detected-yet' +ARCH='not-detected-yet' + +# Initialize default values from pythia.conf +PYTHON_CONFIGURATION='NOT-YET-DEFINED' +PYTHON_VERSION='not.defined.yet' +PYTHON_PLATFORM='unknown-os-and-arch' +PYTHON_NAME='python3.8' +BINARY_DIST_URI='https://github.com/chevah/pythia/releases/download' +PIP_INDEX_URL='https://pypi.org/simple' +BASE_REQUIREMENTS='' + +# +# Check that we have a pavement.py file in the current dir. +# If not, we are out of the source's root dir and pythia.sh won't work. +# +check_source_folder() { + if [ ! -e pavement.py ]; then + (>&2 echo 'No "pavement.py" file found in current folder.') + (>&2 echo 'Make sure you are running "pythia.sh" from a source folder.') + exit 8 + fi +} + +# Called to trigger the entry point in the virtual environment. +# Can be overwritten in pythia.conf +execute_venv() { + ${PYTHON_BIN} $PYTHON3_CHECK -c 'from paver.tasks import main; main()' "$@" +} + + +# Called to update the dependencies inside the newly created virtual +# environment. +update_venv() { + # After updating the python version, the existing pyc files might no + # longer be valid. + _clean_pyc + + set +e + ${PYTHON_BIN} -c 'from paver.tasks import main; main()' deps + exit_code=$? + set -e + if [ $exit_code -ne 0 ]; then + (>&2 echo 'Failed to run the initial "./pythia.sh deps" command.') + exit 7 + fi +} + +# Load repo specific configuration. +source pythia.conf + + +clean_build() { + # Shortcut for clear since otherwise it will depend on python + echo "Removing ${BUILD_FOLDER}..." + delete_folder ${BUILD_FOLDER} + echo "Removing dist..." + delete_folder ${DIST_FOLDER} + echo "Removing publish..." + delete_folder 'publish' + + # In some case pip hangs with a build folder in temp and + # will not continue until it is manually removed. + # On the OSX build server tmp is in $TMPDIR + if [ ! -z "${TMPDIR-}" ]; then + # check if TMPDIR is set before trying to clean it. + rm -rf ${TMPDIR}/pip* + else + rm -rf /tmp/pip* + fi +} + + +_clean_pyc() { + echo "Cleaning pyc files ..." + # Faster than '-exec rm {} \;' and supported in most OS'es, + # details at https://www.in-ulm.de/~mascheck/various/find/#xargs + find ./ -name '*.pyc' -exec rm {} + +} + + +# +# Removes the download/pip cache entries. Must be called before +# building/generating the distribution. +# +purge_cache() { + clean_build + + echo "Cleaning download cache ..." + rm -rf $CACHE_FOLDER/* +} + + +# +# Delete the folder as quickly as possible. +# +delete_folder() { + local target="$1" + # On Windows, we use internal command prompt for maximum speed. + # See: https://stackoverflow.com/a/6208144/539264 + if [ $OS = "win" -a -d $target ]; then + cmd //c "del /f/s/q $target > nul" + cmd //c "rmdir /s/q $target" + else + rm -rf $target + fi +} + + +# +# Wrapper for executing a command and exiting on failure. +# +execute() { + if [ $DEBUG -ne 0 ]; then + echo "Executing:" $@ + fi + + # Make sure $@ is called in quotes as otherwise it will not work. + set +e + "$@" + exit_code=$? + set -e + if [ $exit_code -ne 0 ]; then + (>&2 echo "Failed:" $@) + exit 1 + fi +} + +# +# Update global variables with current paths. +# +update_path_variables() { + resolve_python_version + + if [ "${OS}" = "win" ] ; then + PYTHON_BIN="/lib/python.exe" + PYTHON_LIB="/lib/Lib/" + else + PYTHON_BIN="/bin/python" + PYTHON_LIB="/lib/${PYTHON_NAME}/" + fi + + # Read first from env var. + set +o nounset + BUILD_FOLDER="${CHEVAH_BUILD}" + CACHE_FOLDER="${CHEVAH_CACHE}" + set -o nounset + + if [ "${BUILD_FOLDER}" = "" ] ; then + # Use value from configuration file. + BUILD_FOLDER="${CHEVAH_BUILD_DIR}" + fi + + if [ "${BUILD_FOLDER}" = "" ] ; then + # Use default value if not yet defined. + BUILD_FOLDER="build-${OS}-${ARCH}" + fi + + if [ "${CACHE_FOLDER}" = "" ] ; then + # Use default if not yet defined. + CACHE_FOLDER="${CHEVAH_CACHE_DIR}" + fi + + if [ "${CACHE_FOLDER}" = "" ] ; then + # Use default if not yet defined. + CACHE_FOLDER="cache" + fi + + PYTHON_BIN="${BUILD_FOLDER}${PYTHON_BIN}" + PYTHON_LIB="${BUILD_FOLDER}${PYTHON_LIB}" + + LOCAL_PYTHON_BINARY_DIST="$PYTHON_NAME-$OS-$ARCH" + + export PYTHONPATH=${BUILD_FOLDER} + export CHEVAH_PYTHON=${PYTHON_NAME} + export CHEVAH_OS=${OS} + export CHEVAH_ARCH=${ARCH} + export CHEVAH_CACHE=${CACHE_FOLDER} + export PIP_INDEX_URL=${PIP_INDEX_URL} + +} + +# +# Called to update the Python version env var based on the platform +# advertised by the current environment. +# +resolve_python_version() { + local version_configuration=$PYTHON_CONFIGURATION + local version_configuration_array + local candidate + local candidate_platform + local candidate_version + + PYTHON_PLATFORM="$OS-$ARCH" + + # Using ':' as a delimiter, populate a dedicated array. + IFS=: read -a version_configuration_array <<< "$version_configuration" + # Iterate through all the elements of the array to find the best candidate. + for (( i=0 ; i < ${#version_configuration_array[@]}; i++ )); do + candidate="${version_configuration_array[$i]}" + candidate_platform=$(echo "$candidate" | cut -d "@" -f 1) + candidate_version=$(echo "$candidate" | cut -d "@" -f 2) + if [ "$candidate_platform" = "default" ]; then + # On first pass, we set the default version. + PYTHON_VERSION=$candidate_version + elif [ "${PYTHON_PLATFORM%$candidate_platform*}" = "" ]; then + # If matching a specific platform, we overwrite the default version. + PYTHON_VERSION=$candidate_version + fi + done +} + + +# +# Install base package. +# +install_base_deps() { + echo "Installing base requirements: $BASE_REQUIREMENTS." + pip_install "$BASE_REQUIREMENTS" +} + + +# +# Wrapper for python `pip install` command. +# * $1 - package_name and optional version. +# +pip_install() { + echo "::group::pip install $1" + + set +e + # There is a bug in pip/setuptools when using custom build folders. + # See https://github.com/pypa/pip/issues/3564 + rm -rf ${BUILD_FOLDER}/pip-build + ${PYTHON_BIN} -m \ + pip install \ + --index-url=$PIP_INDEX_URL \ + --build=${BUILD_FOLDER}/pip-build \ + $1 + + exit_code=$? + + echo "::endgroup::" + + set -e + if [ $exit_code -ne 0 ]; then + (>&2 echo "Failed to install $1.") + exit 2 + fi +} + +# +# Check for curl and set needed download commands accordingly. +# +set_download_commands() { + set +o errexit + command -v curl > /dev/null + if [ $? -eq 0 ]; then + # Options not used because of no support in CentOS 5.11's curl: + # --retry-connrefused (since curl 7.52.0) + # --retry-all-errors (since curl 7.71.0) + # Retry 2 times, allocating 10s for the connection phase, + # at most 300s for an attempt, sleeping for 5s between retries. + CURL_RETRY_OPTS="\ + --retry 2 \ + --connect-timeout 10 \ + --max-time 300 \ + --retry-delay 5 \ + " + DOWNLOAD_CMD="curl --remote-name --location $CURL_RETRY_OPTS" + ONLINETEST_CMD="curl --fail --silent --head $CURL_RETRY_OPTS \ + --output /dev/null" + set -o errexit + return + fi + (>&2 echo "Missing curl! It is needed for downloading the Python package.") + exit 3 +} + +# +# Download and extract a binary distribution. +# +get_binary_dist() { + local dist_name=$1 + local remote_base_url=$2 + + echo "Getting $dist_name from $remote_base_url..." + + tar_gz_file=${dist_name}.tar.gz + tar_file=${dist_name}.tar + + mkdir -p ${CACHE_FOLDER} + pushd ${CACHE_FOLDER} + + # Get and extract archive. + rm -rf $dist_name + rm -f $tar_gz_file + rm -f $tar_file + execute $DOWNLOAD_CMD $remote_base_url/${tar_gz_file} + execute gunzip -f $tar_gz_file + execute tar -xf $tar_file + rm -f $tar_gz_file + rm -f $tar_file + + popd +} + +# +# Check if we have a versioned Python distribution. +# +test_version_exists() { + local remote_base_url=$1 + local target_file=python-${PYTHON_VERSION}-${OS}-${ARCH}.tar.gz + + echo "Checking $remote_base_url/${PYTHON_VERSION}/$target_file" + $ONLINETEST_CMD $remote_base_url/${PYTHON_VERSION}/$target_file + return $? +} + +# +# Download and extract in cache the python distributable. +# +get_python_dist() { + local remote_base_url=$1 + local download_mode=$2 + local python_distributable=python-${PYTHON_VERSION}-${OS}-${ARCH} + local onlinetest_errorcode + + set +o errexit + test_version_exists $remote_base_url + onlinetest_errorcode=$? + set -o errexit + + if [ $onlinetest_errorcode -eq 0 ]; then + # We have the requested python version. + get_binary_dist $python_distributable $remote_base_url/${PYTHON_VERSION} + else + (>&2 echo "Couldn't find package on remote server. Full link:") + echo "$remote_base_url/$PYTHON_VERSION/$python_distributable.tar.gz" + exit 4 + fi +} + + +# copy_python can be called in a recursive way, and this is here to prevent +# accidental infinite loops. +COPY_PYTHON_RECURSIONS=0 +# +# Copy python to build folder from binary distribution. +# +copy_python() { + local python_distributable="${CACHE_FOLDER}/${LOCAL_PYTHON_BINARY_DIST}" + local python_installed_version + + COPY_PYTHON_RECURSIONS=`expr $COPY_PYTHON_RECURSIONS + 1` + + if [ $COPY_PYTHON_RECURSIONS -gt 2 ]; then + (>&2 echo "Too many calls to copy_python: $COPY_PYTHON_RECURSIONS") + exit 5 + fi + + # Check that python dist was installed + if [ ! -s ${PYTHON_BIN} ]; then + # We don't have a Python binary, so we install it since everything + # else depends on it. + echo "::group::Get Python" + echo "Bootstrapping ${LOCAL_PYTHON_BINARY_DIST} environment" \ + "to ${BUILD_FOLDER}..." + mkdir -p ${BUILD_FOLDER} + + if [ -d ${python_distributable} ]; then + # We have a cached distributable. + # Check if is at the right version. + local cache_ver_file + cache_ver_file=${python_distributable}/lib/PYTHIA_VERSION + cache_version='UNVERSIONED' + if [ -f $cache_ver_file ]; then + cache_version=`cat $cache_ver_file | cut -d - -f 1` + fi + if [ "$PYTHON_VERSION" != "$cache_version" ]; then + # We have a different version in the cache. + # Just remove it and hope that the next step will download + # the right one. + rm -rf ${python_distributable} + fi + fi + + if [ ! -d ${python_distributable} ]; then + # We don't have a cached python distributable. + echo "No ${LOCAL_PYTHON_BINARY_DIST} environment." \ + "Start downloading it..." + get_python_dist "$BINARY_DIST_URI" "strict" + fi + + echo "Copying Python distribution files... " + cp -R ${python_distributable}/* ${BUILD_FOLDER} + + echo "::endgroup::" + + install_base_deps + WAS_PYTHON_JUST_INSTALLED=1 + else + # We have a Python, but we are not sure if is the right version. + local version_file=${BUILD_FOLDER}/lib/PYTHIA_VERSION + + # If we are upgrading the cache from Python 2, + # cat fails if this file is missing, so we create it blank. + touch $version_file + python_installed_version=`cat $version_file | cut -d - -f 1` + if [ "$PYTHON_VERSION" != "$python_installed_version" ]; then + # We have a different python installed. + # Check if we have the to-be-updated version and fail if + # it does not exists. + set +o errexit + test_version_exists "$BINARY_DIST_URI" + local test_version=$? + set -o errexit + if [ $test_version -ne 0 ]; then + (>&2 echo "The build is now at $python_installed_version.") + (>&2 echo "Failed to find the required $PYTHON_VERSION.") + (>&2 echo "Check your configuration or the remote server.") + exit 6 + fi + + # Remove it and try to install it again. + echo "Updating Python from" \ + $python_installed_version to $PYTHON_VERSION + rm -rf ${BUILD_FOLDER}/* + rm -rf ${python_distributable} + copy_python + fi + fi +} + + +# +# Install dependencies after python was just installed. +# +install_dependencies(){ + if [ $WAS_PYTHON_JUST_INSTALLED -ne 1 ]; then + return + fi + + if [ "$COMMAND" == "deps" ] ; then + # Will be installed soon. + return + fi + + update_venv +} + + +# +# Check version of current OS to see if it is supported. +# If it's too old, exit with a nice informative message. +# If it's supported, return through eval the version numbers to be used for +# naming the package, for example: '8' for RHEL 8.2, '2004' for Ubuntu 20.04. +# +check_os_version() { + # First parameter should be the human-readable name for the current OS. + # For example: "Red Hat Enterprise Linux" for RHEL, "macOS" for Darwin etc. + # Second and third parameters must be strings composed of integers + # delimited with dots, representing, in order, the oldest version + # supported for the current OS and the current detected version. + # The fourth parameter is used to return through eval the relevant numbers + # for naming the Python package for the current OS, as detailed above. + local name_fancy="$1" + local version_good="$2" + local version_raw="$3" + local version_chevah="$4" + local version_constructed='' + local flag_supported='good_enough' + local version_raw_array + local version_good_array + + if [[ $version_raw =~ [^[:digit:]\.] ]]; then + (>&2 echo "OS version should only have numbers and periods, but:") + (>&2 echo " \$version_raw=$version_raw") + exit 12 + fi + + # Using '.' as a delimiter, populate the version_* arrays. + IFS=. read -a version_raw_array <<< "$version_raw" + IFS=. read -a version_good_array <<< "$version_good" + + # Iterate through all the integers from the good version to compare them + # one by one with the corresponding integers from the supported version. + for (( i=0 ; i < ${#version_good_array[@]}; i++ )); do + version_constructed="${version_constructed}${version_raw_array[$i]}" + if [ ${version_raw_array[$i]} -gt ${version_good_array[$i]} -a \ + "$flag_supported" = 'good_enough' ]; then + flag_supported='true' + elif [ ${version_raw_array[$i]} -lt ${version_good_array[$i]} -a \ + "$flag_supported" = 'good_enough' ]; then + flag_supported='false' + fi + done + + if [ "$flag_supported" = 'false' ]; then + (>&2 echo "Detected version of ${name_fancy} is: ${version_raw}.") + (>&2 echo "For versions older than ${name_fancy} ${version_good},") + if [ "$OS" = "Linux" ]; then + # For old and/or unsupported Linux distros there's a second chance! + (>&2 echo "the generic Linux runtime is used, if possible.") + check_linux_libc + else + (>&2 echo "there is currently no support.") + exit 13 + fi + fi + + # The sane way to return fancy values with a bash function is to use eval. + eval $version_chevah="'$version_constructed'" +} + +# +# For old unsupported Linux distros (some with no /etc/os-release) and for other +# unsupported Linux distros, we check if the system is based on glibc or musl. +# If so, we use a generic code path that builds everything statically, +# including OpenSSL, thus only requiring glibc or musl. +# +check_linux_libc() { + local ldd_output_file=".chevah_libc_version" + set +o errexit + + command -v ldd > /dev/null + if [ $? -ne 0 ]; then + (>&2 echo "No ldd binary found, can't check for glibc!") + exit 18 + fi + + ldd --version > $ldd_output_file 2>&1 + egrep "GNU libc|GLIBC" $ldd_output_file > /dev/null + if [ $? -eq 0 ]; then + check_glibc_version + else + egrep ^"musl libc" $ldd_output_file > /dev/null + if [ $? -eq 0 ]; then + check_musl_version + else + (>&2 echo "Unknown libc reported by ldd... Unsupported Linux.") + rm $ldd_output_file + exit 19 + fi + fi + + set -o errexit +} + +check_glibc_version(){ + local glibc_version + local glibc_version_array + local supported_glibc2_version + + # Supported minimum minor glibc 2.X versions for various arches. + # For x64, we build on CentOS 5.11 (Final) with glibc 2.5. + # For arm64, we build on Ubuntu 16.04 with glibc 2.23. + # Beware we haven't normalized arch names yet. + case "$ARCH" in + "amd64"|"x86_64"|"x64") + supported_glibc2_version=5 + ;; + "aarch64"|"arm64") + supported_glibc2_version=23 + ;; + *) + (>&2 echo "$ARCH is an unsupported arch for generic Linux!") + exit 17 + ;; + esac + + echo "No specific runtime for the current distribution / version / arch." + echo "Minimum glibc version for this arch: 2.${supported_glibc2_version}." + + # Tested with glibc 2.5/2.11.3/2.12/2.23/2.28-31 and eglibc 2.13/2.19. + glibc_version=$(head -n 1 $ldd_output_file | rev | cut -d\ -f1 | rev) + rm $ldd_output_file + + if [[ $glibc_version =~ [^[:digit:]\.] ]]; then + (>&2 echo "Glibc version should only have numbers and periods, but:") + (>&2 echo " \$glibc_version=$glibc_version") + exit 20 + fi + + IFS=. read -a glibc_version_array <<< "$glibc_version" + + if [ ${glibc_version_array[0]} -ne 2 ]; then + (>&2 echo "Only glibc 2 is supported! Detected version: $glibc_version") + exit 21 + fi + + # Decrement supported_glibc2_version if building against an older glibc. + if [ ${glibc_version_array[1]} -lt ${supported_glibc2_version} ]; then + (>&2 echo "NOT good. Detected version is older: ${glibc_version}!") + exit 22 + else + echo "All is good. Detected glibc version: ${glibc_version}." + fi + + # Supported glibc version detected, set $OS for a generic glibc Linux build. + OS="lnx" +} + +check_musl_version(){ + local musl_version + local musl_version_array + local supported_musl11_version=24 + + echo "No specific runtime for the current distribution / version / arch." + echo "Minimum musl version for this arch: 1.1.${supported_musl11_version}." + + # Tested with musl 1.1.24/1.2.2. + musl_version=$(egrep ^Version $ldd_output_file | cut -d\ -f2) + rm $ldd_output_file + + if [[ $musl_version =~ [^[:digit:]\.] ]]; then + (>&2 echo "Musl version should only have numbers and periods, but:") + (>&2 echo " \$musl_version=$musl_version") + exit 25 + fi + + IFS=. read -a musl_version_array <<< "$musl_version" + + if [ ${musl_version_array[0]} -lt 1 -o ${musl_version_array[1]} -lt 1 ];then + (>&2 echo "Only musl 1.1 or greater supported! Detected: $musl_version") + exit 26 + fi + + # Decrement supported_musl11_version if building against an older musl. + if [ ${musl_version_array[0]} -eq 1 -a ${musl_version_array[1]} -eq 1 \ + -a ${musl_version_array[2]} -lt ${supported_musl11_version} ]; then + (>&2 echo "NOT good. Detected version is older: ${musl_version}!") + exit 27 + else + echo "All is good. Detected musl version: ${musl_version}." + fi + + # Supported musl version detected, set $OS for a generic musl Linux build. + OS="lnx_musl" +} + +# +# For Linux distros with a supported libc, after checking if current version is +# supported with check_os_version(), $OS might be set to something like "lnx" +# if current version is too old, through check_linux_libc() and its subroutines. +# +set_os_if_not_generic() { + local distro_name="$1" + local distro_version="$2" + + if [ "${OS#lnx}" = "$OS" ]; then + # $OS doesn't start with lnx, not a generic Linux build. + OS="${distro_name}${distro_version}" + fi +} + + +# +# Detect OS and ARCH for the current system. +# In some cases we normalize or even override ARCH at the end of this function. +# +detect_os() { + OS=$(uname -s) + + case "$OS" in + MINGW*|MSYS*) + ARCH=$(uname -m) + OS="win" + ;; + Linux) + ARCH=$(uname -m) + if [ ! -f /etc/os-release ]; then + # No /etc/os-release file present, so we don't support this + # distro, but check for glibc, the generic build should work. + check_linux_libc + else + source /etc/os-release + linux_distro="$ID" + distro_fancy_name="$NAME" + # Some rolling-release distros (eg. Arch Linux) have + # no VERSION_ID here, so don't count on it unconditionally. + case "$linux_distro" in + rhel|centos|almalinux|rocky|ol) + os_version_raw="$VERSION_ID" + check_os_version "Red Hat Enterprise Linux" 8 \ + "$os_version_raw" os_version_chevah + if [ ${os_version_chevah} == "8" ]; then + set_os_if_not_generic "rhel" $os_version_chevah + else + # OpenSSL 3.0.x not supported by cryptography 3.3.x. + check_linux_libc + fi + ;; + ubuntu|ubuntu-core) + os_version_raw="$VERSION_ID" + # For versions with older OpenSSL, use generic build. + check_os_version "$distro_fancy_name" 18.04 \ + "$os_version_raw" os_version_chevah + # Only LTS versions are supported. If it doesn't end in + # 04 or first two digits are uneven, use generic build. + if [ ${os_version_chevah%%04} == ${os_version_chevah} \ + -o $(( ${os_version_chevah:0:2} % 2 )) -ne 0 ]; then + check_linux_libc + elif [ ${os_version_chevah} == "2204" ]; then + # OpenSSL 3.0.x not supported by cryptography 3.3.x. + check_linux_libc + fi + set_os_if_not_generic "ubuntu" $os_version_chevah + ;; + *) + # Supported distros with unsupported OpenSSL versions or + # distros not specifically supported: SLES, Debian, etc. + check_linux_libc + ;; + esac + fi + ;; + Darwin) + ARCH=$(uname -m) + os_version_raw=$(sw_vers -productVersion) + check_os_version "macOS" 10.13 "$os_version_raw" os_version_chevah + # Build a generic package to cover all supported versions. + OS="macos" + ;; + FreeBSD) + ARCH=$(uname -m) + os_version_raw=$(uname -r | cut -d'.' -f1) + check_os_version "FreeBSD" 12 "$os_version_raw" os_version_chevah + OS="fbsd${os_version_chevah}" + ;; + OpenBSD) + ARCH=$(uname -m) + os_version_raw=$(uname -r) + check_os_version "OpenBSD" 6.7 "$os_version_raw" os_version_chevah + OS="obsd${os_version_chevah}" + ;; + SunOS) + ARCH=$(isainfo -n) + ver_major=$(uname -r | cut -d'.' -f2) + case $ver_major in + 10) + ver_minor=$(\ + head -1 /etc/release | cut -d_ -f2 | sed s/[^0-9]*//g) + ;; + 11) + ver_minor=$(uname -v | cut -d'.' -f2) + ;; + *) + # Not sure if $ver_minor detection works on other versions. + (>&2 echo "Unsupported Solaris version: ${ver_major}.") + exit 15 + ;; + esac + os_version_raw="${ver_major}.${ver_minor}" + check_os_version "Solaris" 11.4 "$os_version_raw" os_version_chevah + OS="sol${os_version_chevah}" + ;; + *) + (>&2 echo "Unsupported operating system: ${OS}.") + exit 14 + ;; + esac + + # Normalize arch names. Force 32bit builds on some OS'es. + case "$ARCH" in + "i386"|"i686") + ARCH="x86" + ;; + "amd64"|"x86_64") + ARCH="x64" + case "$OS" in + win) + # 32bit build on Windows 2019, 64bit otherwise. + # Should work with a l10n pack too (tested with French). + win_ver=$(systeminfo.exe | head -n 3 | tail -n 1 \ + | cut -d ":" -f 2) + if [[ "$win_ver" =~ "Microsoft Windows Server 2019" ]]; then + ARCH="x86" + fi + ;; + esac + ;; + "aarch64") + ARCH="arm64" + ;; + esac +} + +detect_os +update_path_variables +set_download_commands + +if [ "$COMMAND" = "clean" ] ; then + clean_build + exit 0 +fi + +if [ "$COMMAND" = "purge" ] ; then + purge_cache + exit 0 +fi + +# Initialize BUILD_ENV_VARS file when building Python from scratch. +if [ "$COMMAND" == "detect_os" ]; then + echo "PYTHON_VERSION=$PYTHON_NAME" > BUILD_ENV_VARS + echo "OS=$OS" >> BUILD_ENV_VARS + echo "ARCH=$ARCH" >> BUILD_ENV_VARS + exit 0 +fi + +if [ "$COMMAND" = "get_python" ] ; then + OS=$2 + ARCH=$3 + resolve_python_version + get_python_dist "$BINARY_DIST_URI" "fallback" + exit 0 +fi + +check_source_folder +copy_python +install_dependencies + +# Update pythia.conf dependencies when running deps. +if [ "$COMMAND" == "deps" ] ; then + install_base_deps +fi + +case $COMMAND in + test_ci|test_py3) + PYTHON3_CHECK='-3' + ;; + *) + PYTHON3_CHECK='' + ;; +esac + +set +e +execute_venv "$@" +exit_code=$? +set -e + +exit $exit_code From 014f2f2d69f447b2061473b4bafbef97bd8327f6 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 22 Mar 2023 19:09:46 +0000 Subject: [PATCH 05/41] Add exception support for py3. --- release-notes.rst | 15 ++++++++++++++- setup.cfg | 2 +- src/chevah_keycert/exceptions.py | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index de50597..67e08d9 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,10 +1,23 @@ Release notes for Chevah KeyCert ################################ + +3.0.1 - 2023-03-22 +================== + +* Have exception str() return text, not bytes. + + +3.0.0 - 2023-03-21 +================== + +* Get py3 code and move into a non-namespace package. + + 2.1.2 - 2023-03-01 ================== -* Just an update to test our internal pypi server.. +* Just an update to test our internal pypi server. 2.1.1 - 2023-02-01 diff --git a/setup.cfg b/setup.cfg index 4348c57..53efc02 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.0 +version = 3.0.1 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/exceptions.py b/src/chevah_keycert/exceptions.py index 9b78279..1b9fd09 100644 --- a/src/chevah_keycert/exceptions.py +++ b/src/chevah_keycert/exceptions.py @@ -16,7 +16,7 @@ def __init__(self, message): self.message = message def __str__(self): - return self.message.encode('utf-8') + return self.message class BadKeyError(KeyCertException): From e87957d0d307136c1212fff4e9620c614e174aa8 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Fri, 24 Mar 2023 05:49:47 +0000 Subject: [PATCH 06/41] More py3 changes, --- release-notes.rst | 6 ++++ setup.cfg | 2 +- src/chevah_keycert/common.py | 55 +++++++++++++++++++++++++++++++++--- src/chevah_keycert/ssh.py | 3 +- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index 67e08d9..2ad36eb 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -2,6 +2,12 @@ Release notes for Chevah KeyCert ################################ +3.0.2 - 2023-03-24 +================== + +* Improve py2 and py3 support.. + + 3.0.1 - 2023-03-22 ================== diff --git a/setup.cfg b/setup.cfg index 53efc02..d167299 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.1 +version = 3.0.2 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/common.py b/src/chevah_keycert/common.py index c7b52b4..433d24b 100644 --- a/src/chevah_keycert/common.py +++ b/src/chevah_keycert/common.py @@ -15,12 +15,59 @@ from six.moves import range -def iterbytes(originalBytes): - return originalBytes +# Functions for dealing with Python 3's bytes type, which is somewhat +# different than Python 2's: +if six.PY3: + def iterbytes(originalBytes): + for i in range(len(originalBytes)): + yield originalBytes[i:i+1] -def native_string(s): - return s + def intToBytes(i): + return ("%d" % i).encode("ascii") + + + def lazyByteSlice(object, offset=0, size=None): + """ + Return a copy of the given bytes-like object. + + If an offset is given, the copy starts at that offset. If a size is + given, the copy will only be of that length. + + @param object: C{bytes} to be copied. + + @param offset: C{int}, starting index of copy. + + @param size: Optional, if an C{int} is given limit the length of copy + to this size. + """ + view = memoryview(object) + if size is None: + return view[offset:] + else: + return view[offset:(offset + size)] + + + def networkString(s): + if not isinstance(s, unicode): + raise TypeError("Can only convert text to bytes on Python 3") + return s.encode('ascii') +else: + def iterbytes(originalBytes): + return originalBytes + + + def intToBytes(i): + return b"%d" % i + + lazyByteSlice = buffer + + def networkString(s): + if not isinstance(s, str): + raise TypeError("Can only pass-through bytes on Python 2") + # Ensure we're limited to ASCII subset: + s.decode('ascii') + return s diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index 597280d..b4596fa 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -1099,8 +1099,9 @@ def fingerprint(self, format=FingerprintFormats.MD5_HEX): return base64.b64encode( sha1(self.blob()).digest()).decode('ascii') elif format is FingerprintFormats.MD5_HEX: - return ':'.join([binascii.hexlify(x) + result = b':'.join([binascii.hexlify(x) for x in iterbytes(md5(self.blob()).digest())]) + return result.decode('ascii') else: raise BadFingerPrintFormat( 'Unsupported fingerprint format: %s' % (format,)) From 4ee3854c751b6319957e9651afead5cfb4e0ecce Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sat, 1 Apr 2023 22:16:48 +0100 Subject: [PATCH 07/41] Release 3.0.3. --- release-notes.rst | 8 +++++++- setup.cfg | 2 +- src/chevah_keycert/__init__.py | 10 ++++++++++ src/chevah_keycert/ssh.py | 5 +++-- src/chevah_keycert/ssl.py | 16 ++++++++-------- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index 2ad36eb..a7c053e 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -2,10 +2,16 @@ Release notes for Chevah KeyCert ################################ +3.0.3 - 2023-03-27 +================== + +* Fix ssl.py CSR and cert generation on Py3. + + 3.0.2 - 2023-03-24 ================== -* Improve py2 and py3 support.. +* Improve py2 and py3 support. 3.0.1 - 2023-03-22 diff --git a/setup.cfg b/setup.cfg index d167299..fc59b22 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.2 +version = 3.0.3 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/__init__.py b/src/chevah_keycert/__init__.py index 2961557..78e6a3c 100644 --- a/src/chevah_keycert/__init__.py +++ b/src/chevah_keycert/__init__.py @@ -3,6 +3,7 @@ """ from __future__ import absolute_import import sys +import six def _path(path, encoding='utf-8'): @@ -11,3 +12,12 @@ def _path(path, encoding='utf-8'): return path # pragma: no cover return path.encode(encoding) + + +def native_string(string): + """ + Helper for some API that need bytes on Py2 and Unicode on Py3. + """ + if six.PY2: + string = string.encode('ascii') + return string diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index b4596fa..4af3a06 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -17,6 +17,7 @@ import unicodedata import struct import textwrap +import six import bcrypt from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm @@ -1512,7 +1513,7 @@ def _toString_OPENSSH_V1(self, comment=None, passphrase=None): padByte = 0 while len(privKeyList) % blockSize: padByte += 1 - privKeyList += chr(padByte & 0xFF) + privKeyList += six.int2byte(padByte & 0xFF) if passphrase: encKey = bcrypt.kdf(passphrase, salt, keySize + ivSize, 100) encryptor = Cipher( @@ -1595,7 +1596,7 @@ def _toString_OPENSSH(self, comment=None, passphrase=None): bb = md5(ba + passphrase + iv).digest() encKey = (ba + bb)[:24] padLen = 8 - (len(asn1Data) % 8) - asn1Data += chr(padLen) * padLen + asn1Data += six.int2byte(padLen) * padLen encryptor = Cipher( algorithms.TripleDES(encKey), diff --git a/src/chevah_keycert/ssl.py b/src/chevah_keycert/ssl.py index d5a4c52..abc3cba 100644 --- a/src/chevah_keycert/ssl.py +++ b/src/chevah_keycert/ssl.py @@ -10,11 +10,11 @@ from OpenSSL import crypto -from chevah_keycert import _path +from chevah_keycert import _path, native_string from chevah_keycert.exceptions import KeyCertException -_DEFAULT_SSL_KEY_CYPHER = b'aes-256-cbc' -_SUPPORTED_SIGN_ALGORITHMS = [b'md5', b'sha1', b'sha256', b'sha512'] +_DEFAULT_SSL_KEY_CYPHER = 'aes-256-cbc' +_SUPPORTED_SIGN_ALGORITHMS = ['md5', 'sha1', 'sha256', 'sha512'] # See https://www.openssl.org/docs/manmaster/man5/x509v3_config.html _KEY_USAGE_STANDARD = { @@ -244,7 +244,8 @@ def _set_subject_and_extensions(target, options): except ValueError: raise KeyCertException('Invalid email address.') - subject.emailAddress = u'%s@%s' % (address, domain.encode('idna')) + subject.emailAddress = '%s@%s' % ( + address, domain.encode('idna').decode('ascii')) critical_constraints = False critical_usage = False @@ -303,8 +304,7 @@ def _sign_cert_or_csr(target, key, options): """ Sign the certificate or CSR. """ - sign_algorithm = getattr( - options, 'sign_algorithm', 'sha256').encode('ascii') + sign_algorithm = getattr(options, 'sign_algorithm', 'sha256') if sign_algorithm not in _SUPPORTED_SIGN_ALGORITHMS: raise KeyCertException( @@ -312,7 +312,7 @@ def _sign_cert_or_csr(target, key, options): ', '.join(_SUPPORTED_SIGN_ALGORITHMS))) target.set_pubkey(key) - target.sign(key, sign_algorithm) + target.sign(key, native_string(sign_algorithm)) def _generate_csr(options): @@ -391,7 +391,7 @@ def generate_ssl_self_signed_certificate(options): certificate_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) - return (certificate_pem, key_pem) + return (certificate_pem.decode('utf-8'), key_pem.decode('utf-8')) def generate_and_store_csr(options, encoding='utf-8'): From 97e08e7d8388d7a4565fb2f57ba222f4141706f5 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sun, 2 Apr 2023 01:56:03 +0100 Subject: [PATCH 08/41] Release 3.0.4. --- release-notes.rst | 6 ++++++ setup.cfg | 2 +- src/chevah_keycert/ssl.py | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index a7c053e..4167d3e 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -2,6 +2,12 @@ Release notes for Chevah KeyCert ################################ +3.0.4 - 2023-04-01 +================== + +* More fixes for CSR generation. + + 3.0.3 - 2023-03-27 ================== diff --git a/setup.cfg b/setup.cfg index fc59b22..f395306 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.3 +version = 3.0.4 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/ssl.py b/src/chevah_keycert/ssl.py index abc3cba..71bb046 100644 --- a/src/chevah_keycert/ssl.py +++ b/src/chevah_keycert/ssl.py @@ -352,7 +352,9 @@ def _generate_csr(options): if options.key_password: key_pem = crypto.dump_privatekey( crypto.FILETYPE_PEM, key, - _DEFAULT_SSL_KEY_CYPHER, options.key_password.encode('utf-8')) + _DEFAULT_SSL_KEY_CYPHER.encode('ascii'), + options.key_password.encode('utf-8'), + ) else: key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) From f5802608a6abb9294b824116e7c8558415da2432 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sun, 2 Apr 2023 02:10:52 +0100 Subject: [PATCH 09/41] Release 3.0.5. --- release-notes.rst | 6 ++++++ setup.cfg | 2 +- src/chevah_keycert/ssl.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index 4167d3e..7512fa7 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -2,6 +2,12 @@ Release notes for Chevah KeyCert ################################ +3.0.5 - 2023-04-01 +================== + +* Get CSR generation working on py2 and py3.. + + 3.0.4 - 2023-04-01 ================== diff --git a/setup.cfg b/setup.cfg index f395306..a23f397 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.4 +version = 3.0.5 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/ssl.py b/src/chevah_keycert/ssl.py index 71bb046..d9f8980 100644 --- a/src/chevah_keycert/ssl.py +++ b/src/chevah_keycert/ssl.py @@ -5,7 +5,9 @@ """ from __future__ import unicode_literals from __future__ import absolute_import + import os +import six from random import randint from OpenSSL import crypto @@ -350,9 +352,13 @@ def _generate_csr(options): if not key_pem: if options.key_password: + cipher = _DEFAULT_SSL_KEY_CYPHER + if six.PY2: + cipher = cipher.encode('ascii') key_pem = crypto.dump_privatekey( - crypto.FILETYPE_PEM, key, - _DEFAULT_SSL_KEY_CYPHER.encode('ascii'), + crypto.FILETYPE_PEM, + key, + cipher, options.key_password.encode('utf-8'), ) else: From 2d1d9dc606c9d9fa2c34680e2ea7e0d0f174372f Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Fri, 28 Apr 2023 19:24:13 +0100 Subject: [PATCH 10/41] Release 3.0.6. --- release-notes.rst | 8 ++++- setup.cfg | 2 +- src/chevah_keycert/ssh.py | 72 +++++++++++++++++++-------------------- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index 7512fa7..4b4ef79 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -2,10 +2,16 @@ Release notes for Chevah KeyCert ################################ +3.0.6 - 2023-04-24 +================== + +* Get SSH.com and Putty ssh key handling working on py3. + + 3.0.5 - 2023-04-01 ================== -* Get CSR generation working on py2 and py3.. +* Get CSR generation working on py2 and py3. 3.0.4 - 2023-04-01 diff --git a/setup.cfg b/setup.cfg index a23f397..3c6b7ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.5 +version = 3.0.6 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index 4af3a06..2abf703 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -72,7 +72,7 @@ DEFAULT_KEY_SIZE = 2048 DEFAULT_KEY_TYPE = 'rsa' SSHCOM_MAGIC_NUMBER = int('3f6ff9eb', base=16) -PUTTY_HMAC_KEY = 'putty-private-key-file-mac-key' +PUTTY_HMAC_KEY = b'putty-private-key-file-mac-key' ID_SHA1 = b'\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' # Curve lookup table @@ -83,9 +83,9 @@ } _secToNist = { - b'secp256r1' : b'nistp256', - b'secp384r1' : b'nistp384', - b'secp521r1' : b'nistp521', + 'secp256r1' : b'nistp256', + 'secp384r1' : b'nistp384', + 'secp521r1' : b'nistp521', } @@ -256,6 +256,8 @@ def fromString(cls, data, type=None, passphrase=None): # we consider it too short. raise BadKeyError('Key is too short.') except (struct.error, binascii.Error, TypeError): + import traceback + ca = traceback.format_exc() raise BadKeyError('Fail to parse key content.') @classmethod @@ -369,7 +371,7 @@ def _fromString_PRIVATE_BLOB(cls, blob): elif keyType in _curveTable: curve = _curveTable[keyType] curveName, q, rest = common.getNS(rest, 2) - if curveName != _secToNist[curve.name.encode('ascii')]: + if curveName != _secToNist[curve.name]: raise BadKeyError( 'ECDSA curve name "%s" does not match key type "%s"' % ( force_unicode(curveName), force_unicode(keyType))) @@ -1147,7 +1149,7 @@ def sshType(self): if self.type() == 'EC': return ( b'ecdsa-sha2-' + - _secToNist[self._keyObject.curve.name.encode('ascii')]) + _secToNist[self._keyObject.curve.name]) else: return { 'RSA': b'ssh-rsa', @@ -1441,10 +1443,7 @@ def toString(self, type, extra=None, comment=None, comment = extra else: passphrase = extra - if isinstance(comment, six.text_type): - comment = comment.encode("utf-8") - if isinstance(passphrase, six.text_type): - passphrase = passphrase.encode("utf-8") + method = getattr(self, '_toString_%s' % (type.upper(),), None) if method is None: raise BadKeyError( @@ -1914,7 +1913,7 @@ def _getSSHCOMKeyContent(data): Return the raw content of the SSH.com key (private or public) without armor and headers. """ - lines = data.strip().splitlines() + lines = data.decode('utf-8').strip().splitlines() # Split in lines, ignoring the first and last armors. lines = lines[1:-1] @@ -1947,7 +1946,7 @@ def _getSSHCOMKeyContent(data): break content = ''.join(lines) - return base64.decodestring(content) + return base64.decodestring(content.encode('ascii')) @classmethod def _fromString_PUBLIC_SSHCOM(cls, data): @@ -1979,7 +1978,7 @@ def _fromString_PUBLIC_SSHCOM(cls, data): @return: A {Crypto.PublicKey.pubkey.pubkey} object @raises BadKeyError: if the blob type is unknown. """ - if not data.strip().endswith('---- END SSH2 PUBLIC KEY ----'): + if not data.strip().endswith(b'---- END SSH2 PUBLIC KEY ----'): raise BadKeyError("Fail to find END tag for SSH.com key.") blob = cls._getSSHCOMKeyContent(data) @@ -2046,9 +2045,9 @@ def _fromString_PRIVATE_SSHCOM(cls, data, passphrase): type_signature, rest = common.getNS(blob[8:]) key_type = None - if type_signature.startswith('if-modn{sign{rsa'): + if type_signature.startswith(b'if-modn{sign{rsa'): key_type = 'rsa' - elif type_signature.startswith('dl-modp{sign{dsa'): + elif type_signature.startswith(b'dl-modp{sign{dsa'): key_type = 'dsa' else: raise BadKeyError( @@ -2161,10 +2160,10 @@ def _toString_SSHCOM_public(self, extra): """ lines = ['---- BEGIN SSH2 PUBLIC KEY ----'] if extra: - line = 'Comment: "%s"' % (extra.encode('utf-8'),) + line = 'Comment: "%s"' % (extra,) lines.append('\\\n'.join(textwrap.wrap(line, 70))) - base64Data = base64.b64encode(self.blob()) + base64Data = base64.b64encode(self.blob()).decode('ascii') lines.extend(textwrap.wrap(base64Data, 70)) lines.append('---- END SSH2 PUBLIC KEY ----') return '\n'.join(lines) @@ -2241,11 +2240,11 @@ def _toString_SSHCOM_private(self, extra): ) # In the end, encode in base 64 and wrap it. - blob = base64.b64encode(blob) + blob = base64.b64encode(blob).decode('ascii') lines.extend(textwrap.wrap(blob, 70)) lines.append('---- END SSH2 ENCRYPTED PRIVATE KEY ----') - return '\n'.join(lines).encode('ascii') + return '\n'.join(lines) @classmethod def _fromString_PRIVATE_PUTTY(cls, data, passphrase): @@ -2310,23 +2309,23 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): Version 2 was introduced in PuTTY 0.52. Version 1 was an in-development format used in 0.52 snapshot """ - lines = data.strip().splitlines() + lines = data.decode('utf-8').strip().splitlines() key_type = lines[0][22:].strip().lower() if key_type not in [ - b'ssh-rsa', - b'ssh-dss', - b'ssh-ed25519', + 'ssh-rsa', + 'ssh-dss', + 'ssh-ed25519', ] and key_type not in _curveTable: raise BadKeyError( 'Unsupported key type: "%s"' % force_unicode(key_type[:30])) encryption_type = lines[1][11:].strip().lower() - if encryption_type == b'none': + if encryption_type == 'none': if passphrase: raise BadKeyError('PuTTY key not encrypted') - elif encryption_type != b'aes256-cbc': + elif encryption_type != 'aes256-cbc': raise BadKeyError( 'Unsupported encryption type: "%s"' % force_unicode( encryption_type[:30])) @@ -2338,10 +2337,10 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): 4: 4 + public_count ]) - public_blob = base64.decodestring(base64_content) + public_blob = base64.decodestring(base64_content.encode('utf-8')) public_type, public_payload = common.getNS(public_blob) - if public_type.lower() != key_type: + if public_type.decode('ascii').lower() != key_type: raise BadKeyError( 'Mismatch key type. Header has "%s", public has "%s"' % ( force_unicode(key_type[:30]), @@ -2354,13 +2353,13 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): private_start_line + 1: private_start_line + 1 + private_count ]) - private_blob = base64.decodestring(base64_content) + private_blob = base64.decodestring(base64_content.encode('ascii')) private_mac = lines[-1][12:].strip() hmac_key = PUTTY_HMAC_KEY encryption_key = None - if encryption_type == b'aes256-cbc': + if encryption_type == 'aes256-cbc': if not passphrase: raise EncryptedKeyError( 'Passphrase must be provided for an encrypted key.') @@ -2393,17 +2392,17 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): force_unicode(private_mac), force_unicode(computed_mac))) - if key_type == b'ssh-rsa': + if key_type == 'ssh-rsa': e, n, _ = common.getMP(public_payload, count=2) d, q, p, u, _ = common.getMP(private_blob, count=4) return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) - if key_type == b'ssh-dss': + if key_type == 'ssh-dss': p, q, g, y, _ = common.getMP(public_payload, count=4) x, _ = common.getMP(private_blob) return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) - if key_type == b'ssh-ed25519': + if key_type == 'ssh-ed25519': a, _ = common.getNS(public_payload) k, _ = common.getNS(private_blob) return cls._fromEd25519Components(a=a, k=k) @@ -2411,7 +2410,7 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): if key_type in _curveTable: curve = _curveTable[key_type] curveName, q, _ = common.getNS(public_payload, 2) - if curveName != _secToNist[curve.name.encode('ascii')]: + if curveName != _secToNist[curve.name]: raise BadKeyError( 'ECDSA curve name "%s" does not match key type "%s"' % ( force_unicode(curveName), @@ -2468,7 +2467,7 @@ def _toString_PUTTY_private(self, extra): hmac_key = PUTTY_HMAC_KEY if extra: - encryption_type = b'aes256-cbc' + encryption_type = 'aes256-cbc' hmac_key += extra else: encryption_type = 'none' @@ -2533,9 +2532,10 @@ def _toString_PUTTY_private(self, extra): private_blob_encrypted = ( encryptor.update(private_blob_plain) + encryptor.finalize()) - public_lines = textwrap.wrap(base64.b64encode(public_blob), 64) + public_lines = textwrap.wrap( + base64.b64encode(public_blob).decode('ascii'), 64) private_lines = textwrap.wrap( - base64.b64encode(private_blob_encrypted), 64) + base64.b64encode(private_blob_encrypted).decode('ascii'), 64) hmac_data = ( common.NS(key_type) + From 583f9f6f052b0316d57b4cb32e850dc17e89f644 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Fri, 28 Apr 2023 19:55:05 +0100 Subject: [PATCH 11/41] Release 3.0.7. --- release-notes.rst | 5 +++++ setup.cfg | 2 +- src/chevah_keycert/ssh.py | 13 ++++++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index 4b4ef79..1379be1 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,6 +1,11 @@ Release notes for Chevah KeyCert ################################ +3.0.7 - 2023-04-28 +================== + +* Fix generating and reading Putty v2 keys. + 3.0.6 - 2023-04-24 ================== diff --git a/setup.cfg b/setup.cfg index 3c6b7ae..787d39a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.6 +version = 3.0.7 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index 2abf703..97f117d 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -2316,7 +2316,7 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): 'ssh-rsa', 'ssh-dss', 'ssh-ed25519', - ] and key_type not in _curveTable: + ] and key_type.encode('ascii') not in _curveTable: raise BadKeyError( 'Unsupported key type: "%s"' % force_unicode(key_type[:30])) @@ -2407,8 +2407,8 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): k, _ = common.getNS(private_blob) return cls._fromEd25519Components(a=a, k=k) - if key_type in _curveTable: - curve = _curveTable[key_type] + if key_type.encode('ascii') in _curveTable: + curve = _curveTable[key_type.encode('ascii')] curveName, q, _ = common.getNS(public_payload, 2) if curveName != _secToNist[curve.name]: raise BadKeyError( @@ -2418,7 +2418,10 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): privateValue, _ = common.getMP(private_blob) return cls._fromECEncodedPoint( - encodedPoint=q, curve=key_type, privateValue=privateValue) + encodedPoint=q, + curve=key_type.encode('ascii'), + privateValue=privateValue, + ) @staticmethod def _getPuttyAES256EncryptionKey(passphrase): @@ -2547,7 +2550,7 @@ def _toString_PUTTY_private(self, extra): hmac_key = sha1(hmac_key).digest() private_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest() - lines.append('PuTTY-User-Key-File-2: %s' % key_type) + lines.append('PuTTY-User-Key-File-2: %s' % key_type.decode('ascii')) lines.append('Encryption: %s' % encryption_type) lines.append('Comment: %s' % comment) lines.append('Public-Lines: %s' % len(public_lines)) From a85220c2af6ad122469a68d624deb214a26e07f6 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 3 May 2023 19:15:39 +0100 Subject: [PATCH 12/41] Release 3.0.8. --- release-notes.rst | 6 ++++++ setup.cfg | 2 +- src/chevah_keycert/ssh.py | 17 ++++++++++++----- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index 1379be1..2b623ec 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,6 +1,12 @@ Release notes for Chevah KeyCert ################################ +3.0.8 - 2023-05-03 +================== + +* SSH.com and Putty string serialization is done to bytes. + + 3.0.7 - 2023-04-28 ================== diff --git a/setup.cfg b/setup.cfg index 787d39a..7ac0f8e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.7 +version = 3.0.8 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index 97f117d..5a0bbbc 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -50,8 +50,15 @@ import os import os.path from os import urandom -from base64 import encodestring as encodebytes -from base64 import decodestring as decodebytes + +try: + from base64 import encodebytes + from base64 import decodebytes +except ImportError: + # On py2 we don't have encodebytes. + from base64 import encodestring as encodebytes + from base64 import decodestring as decodebytes + from cryptography.utils import int_from_bytes, int_to_bytes from OpenSSL import crypto @@ -2166,7 +2173,7 @@ def _toString_SSHCOM_public(self, extra): base64Data = base64.b64encode(self.blob()).decode('ascii') lines.extend(textwrap.wrap(base64Data, 70)) lines.append('---- END SSH2 PUBLIC KEY ----') - return '\n'.join(lines) + return '\n'.join(lines).encode('utf-8') def _toString_SSHCOM_private(self, extra): """ @@ -2244,7 +2251,7 @@ def _toString_SSHCOM_private(self, extra): lines.extend(textwrap.wrap(blob, 70)) lines.append('---- END SSH2 ENCRYPTED PRIVATE KEY ----') - return '\n'.join(lines) + return '\n'.join(lines).encode('utf-8') @classmethod def _fromString_PRIVATE_PUTTY(cls, data, passphrase): @@ -2558,7 +2565,7 @@ def _toString_PUTTY_private(self, extra): lines.append('Private-Lines: %s' % len(private_lines)) lines.extend(private_lines) lines.append('Private-MAC: %s' % private_mac) - return '\r\n'.join(lines) + return '\r\n'.join(lines).encode('utf-8') @classmethod def _fromString_PUBLIC_X509_CERTIFICATE(cls, data): From c8d68f26b17292c01f79bcd74077ba44dd57f019 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Mon, 22 May 2023 16:41:20 +0100 Subject: [PATCH 13/41] Release 3.0.9. --- release-notes.rst | 7 +++++++ setup.cfg | 2 +- src/chevah_keycert/__init__.py | 6 +++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index 2b623ec..4c487f1 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,6 +1,13 @@ Release notes for Chevah KeyCert ################################ + +3.0.9 - 2023-05-22 +================== + +* Handle already encoded paths on Linux. + + 3.0.8 - 2023-05-03 ================== diff --git a/setup.cfg b/setup.cfg index 7ac0f8e..ceaf482 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.8 +version = 3.0.9 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/__init__.py b/src/chevah_keycert/__init__.py index 78e6a3c..52fa96c 100644 --- a/src/chevah_keycert/__init__.py +++ b/src/chevah_keycert/__init__.py @@ -8,9 +8,13 @@ def _path(path, encoding='utf-8'): if sys.platform.startswith('win'): - # On Windows and OSX we always use unicode. + # On Windows we always use unicode. return path # pragma: no cover + if isinstance(path, six.binary_type): + # Path is already encoded. + return path + return path.encode(encoding) From b25e8df3ae458a0fd315d5487b933555eb42f43f Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Tue, 4 Jul 2023 17:38:44 +0100 Subject: [PATCH 14/41] 3.0.10 Update for latest cryptography. --- release-notes.rst | 5 +++++ setup.cfg | 2 +- src/chevah_keycert/ssh.py | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index 4c487f1..80f925e 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,6 +1,11 @@ Release notes for Chevah KeyCert ################################ +3.0.10 - 2023-07-04 +================== + +* Update for cryptography 39 and newer. + 3.0.9 - 2023-05-22 ================== diff --git a/setup.cfg b/setup.cfg index ceaf482..533b69e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.9 +version = 3.0.10 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index 5a0bbbc..e7408c0 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -2514,10 +2514,14 @@ def _toString_PUTTY_private(self, extra): elif key_type in _curveTable: curve_name = _secToNist[self._keyObject.curve.name] + encode_point = self._keyObject.public_key().public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) public_blob = ( common.NS(key_type) + common.NS(curve_name) + - common.NS(self._keyObject.public_key().public_numbers().encode_point()) + common.NS(encode_point) ) private_blob = common.MP(data['privateValue']) From 3ebf3f82366a960d90186eb0ac9a4d384fce1206 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sat, 29 Jul 2023 22:21:32 +0100 Subject: [PATCH 15/41] 3.0.11 no more compat deps for non-testing. --- release-notes.rst | 6 ++++++ setup.cfg | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index 80f925e..6f78bf4 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,6 +1,12 @@ Release notes for Chevah KeyCert ################################ +3.0.11 - 2023-07-29 +=================== + +* No longer ask for compat and scandir as they are only needed for testing. + + 3.0.10 - 2023-07-04 ================== diff --git a/setup.cfg b/setup.cfg index 533b69e..87a47cf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.10 +version = 3.0.11 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT @@ -15,9 +15,8 @@ install_requires = pyopenssl >= 0.13 pyasn1 >= 0.1.7 cryptography >= 3.2 - chevah-compat >= 0.70 - scandir >= 1.7 constantly >= 15.1.0 + packages = find: package_dir = =src @@ -36,6 +35,8 @@ dev = pyflakes >= 1.5.0 pycodestyle ==2.3.1 + chevah-compat >= 0.70 + nose mock bunch From 192e7169e10096a111f79a0a5a52731fe2c3e327 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sat, 27 Jan 2024 03:23:55 +0000 Subject: [PATCH 16/41] Fix for pyopenssl 24.0.0 --- release-notes.rst | 6 ++++++ setup.cfg | 2 +- src/chevah_keycert/ssl.py | 5 +++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/release-notes.rst b/release-notes.rst index 6f78bf4..8671f3f 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,6 +1,12 @@ Release notes for Chevah KeyCert ################################ +3.0.12 - 2024-01-27 +=================== + +* Update to support pyOpenSSL 24.0.0. + + 3.0.11 - 2023-07-29 =================== diff --git a/setup.cfg b/setup.cfg index 87a47cf..a4b3fa6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.11 +version = 3.0.12 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT diff --git a/src/chevah_keycert/ssl.py b/src/chevah_keycert/ssl.py index d9f8980..91ab88b 100644 --- a/src/chevah_keycert/ssl.py +++ b/src/chevah_keycert/ssl.py @@ -215,8 +215,9 @@ def _set_subject_and_extensions(target, options): # RFC 2459 defines it as optional, and pyopenssl set it to `0` anyway. # But we got reports that Windows 2003 and Windows 2008 Servers - # can not parse CSR generated using this tool, so here we are. - target.set_version(2) + # can not parse CSR generated using this tool. + # PyOpenSSL 24.0.0 only supports version 0. + target.set_version(0) subject = target.get_subject() From a8e804770b91adced52bace44db684985485348c Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Mon, 11 Mar 2024 13:00:33 +0000 Subject: [PATCH 17/41] More fixes. --- .github/workflows/main.yml | 12 +- pavement.py | 75 ++- pythia.conf | 23 +- pythia.sh | 644 +++++++++----------- release-notes.rst | 8 + src/chevah_keycert/__init__.py | 21 +- src/chevah_keycert/common.py | 91 +-- src/chevah_keycert/sexpy.py | 48 -- src/chevah_keycert/ssh.py | 348 ++++++----- src/chevah_keycert/ssl.py | 2 +- src/chevah_keycert/tests/test_exceptions.py | 2 +- src/chevah_keycert/tests/test_ssh.py | 215 +++---- 12 files changed, 727 insertions(+), 762 deletions(-) delete mode 100644 src/chevah_keycert/sexpy.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 24221e7..9d2ea18 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ jobs: ubuntu: # The type of runner that the job will run on - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -29,22 +29,22 @@ jobs: key: ${{ runner.os }}-${{ hashFiles('setup.py') }} - name: Deps - run: ./brink.sh deps + run: ./pythia.sh deps - name: Lint - run: ./brink.sh lint + run: ./pythia.sh lint - name: Rename build to unicode - run: mv build-keycert build-keycert-ț + run: mv build-py3 build-py3-ț - name: Test run: ./brink.sh test env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - CHEVAH_BUILD: 'build-keycert-ț' + CHEVAH_BUILD: 'build-py3-ț' - name: Rename build from unicode for cache - run: mv build-keycert-ț build-keycert + run: mv build-py3-ț build-py3 macos: diff --git a/pavement.py b/pavement.py index 9365663..8797f3f 100644 --- a/pavement.py +++ b/pavement.py @@ -4,6 +4,7 @@ import os import re import sys +import threading from subprocess import call from pkg_resources import load_entry_point @@ -36,14 +37,76 @@ def test(args): """ Run the test tests. """ - import nose + _nose(args, cov=None) + + +def _nose(args, cov, base='chevah_keycert.tests'): + """ + Run nose tests in the same process. + """ + # Delay import after coverage is started. + import psutil + from nose.core import main as nose_main + from nose.plugins.base import Plugin + from chevah_compat.testing import ChevahTestCase + + from chevah_compat.testing.nose_memory_usage import MemoryUsage + from chevah_compat.testing.nose_test_timer import TestTimer + from chevah_compat.testing.nose_run_reporter import RunReporter + + import chevah_keycert + + class LoopPlugin(Plugin): + name = 'loop' + + new_arguments = [ + '--with-randomly', + '--with-run-reporter', + '--with-timer', + '-v', '-s', + ] + + have_tests = False + for argument in args: + if not argument.startswith('-'): + argument = '%s.%s' % (base, argument) + have_tests = True + new_arguments.append(argument) + + if not have_tests: + # Run all base tests if no specific tests was requested. + new_arguments.append(base) + + sys.argv = new_arguments + print(new_arguments) + + plugins = [ + TestTimer(), + RunReporter(), + MemoryUsage(), + LoopPlugin() + ] + + + os.chdir('build-py3') + ChevahTestCase.initialize(drop_user='-') + ChevahTestCase.dropPrivileges() + try: + nose_main(addplugins=plugins) + finally: + process = psutil.Process(os.getpid()) + print('Max RSS: {} MB'.format(process.memory_info().rss / 1000000)) + if cov: + cov.stop() + cov.save() + threads = threading.enumerate() + if len(threads) > 1: + print("There are still active threads: %s" % threads) + sys.stdout.flush() + sys.stderr.flush() + os._exit(1) - nose_args = ['nosetests'] - nose_args.extend(args) - nose_code = nose.run(argv=nose_args) - nose_code = 0 if nose_code else 1 - sys.exit(nose_code) @task diff --git a/pythia.conf b/pythia.conf index 3360b1a..0fc7a35 100644 --- a/pythia.conf +++ b/pythia.conf @@ -1,12 +1,13 @@ -BASE_REQUIREMENTS='chevah-brink==1.0.7 paver==1.2.4 six==1.16.0' -PYTHON_CONFIGURATION='default@3.8.6.3b1a8ba' -# For production packages there are 2 options: -BINARY_DIST_URI='https://github.com/chevah/pythia/releases/download' -#BINARY_DIST_URI='https://bin.chevah.com:20443/production' +# The content of this file is used to decide how to reused GitHub Actions +# cache builds. +PYTHON_CONFIGURATION="default@3.11.7.4666189" +# This is defined as a Bash array of options to be passed to commands. +BASE_REQUIREMENTS=("chevah-brink==1.0.15" "paver==1.3.4" "six==1.16.0") +# Use our production server instead of the GitHub releases set by default. +BINARY_DIST_URI="https://bin.chevah.com:20443/production" # For testing packages, make sure this one is the last uncommented instance: -#BINARY_DIST_URI='https://bin.chevah.com:20443/testing' -PIP_INDEX_URL='https://bin.chevah.com:20443/pypi/simple' -# There are 2 build directories used in this repo: -# * $BUILD_DIR is used for building libffi / OpenSSL / Python / etc. -# * $CHEVAH_BUILD_DIR is used by the Python that builds the above. -CHEVAH_BUILD_DIR='build-py3' +# BINARY_DIST_URI="https://bin.chevah.com:20443/testing" +# Also overwrite the default pypi.org site set by default in pythia.sh. +PIP_INDEX_URL="https://bin.chevah.com:20443/pypi/simple" +# This is used by the Python runtime. +CHEVAH_BUILD_DIR="build-py3" diff --git a/pythia.sh b/pythia.sh index baa9dd5..b218367 100755 --- a/pythia.sh +++ b/pythia.sh @@ -20,6 +20,8 @@ # * CHEVAH_PYTHON - name of the python versions # * CHEVAH_OS - name of the current OS # * CHEVAH_ARCH - CPU type of the current OS +# * CHEVAH_CACHE - path to the cache directory +# * PIP_INDEX_URL - URL for the used PyPI server. # # The build directory is used from CHEVAH_BUILD env, # then read from pythia.conf as CHEVAH_BUILD_DIR, @@ -39,34 +41,32 @@ set -o errexit # always exit on error set -o errtrace # trap errors in functions as well set -o pipefail # don't ignore exit codes when piping output -# Initialize default value. -COMMAND=${1-''} -DEBUG=${DEBUG-0} +# Initialize default values. +COMMAND="${1-''}" +DEBUG="${DEBUG-0}" # Set default locale. # We use C (alias for POSIX) for having a basic default value and # to make sure we explicitly convert all unicode values. -export LANG='C' -export LANGUAGE='C' -export LC_ALL='C' -export LC_CTYPE='C' -export LC_COLLATE='C' -export LC_MESSAGES='C' -export PATH=$PATH:'/sbin:/usr/sbin:/usr/local/bin' +export LANG="C" +export LANGUAGE="C" +export LC_ALL="C" +export LC_CTYPE="C" +export LC_COLLATE="C" +export LC_MESSAGES="C" +export PATH="$PATH:/sbin:/usr/sbin:/usr/local/bin" # # Global variables. # -# Used to return non-scalar value from functions. -RESULT='' WAS_PYTHON_JUST_INSTALLED=0 -DIST_FOLDER='dist' +DIST_FOLDER="dist" # Path global variables. # Configuration variable. CHEVAH_BUILD_DIR="" -# Variale used at runtime. +# Variable used at runtime. BUILD_FOLDER="" # Configuration variable @@ -79,17 +79,18 @@ PYTHON_LIB="" LOCAL_PYTHON_BINARY_DIST="" # Put default values and create them as global variables. -OS='not-detected-yet' -ARCH='not-detected-yet' +OS="not-detected-yet" +ARCH="not-detected-yet" -# Initialize default values from pythia.conf -PYTHON_CONFIGURATION='NOT-YET-DEFINED' -PYTHON_VERSION='not.defined.yet' -PYTHON_PLATFORM='unknown-os-and-arch' -PYTHON_NAME='python3.8' -BINARY_DIST_URI='https://github.com/chevah/pythia/releases/download' -PIP_INDEX_URL='https://pypi.org/simple' -BASE_REQUIREMENTS='' +# Initialize default values, some are overwritten from pythia.conf. +PYTHON_CONFIGURATION="NOT-YET-DEFINED" +PYTHON_NAME="not-yet-determined" +PYTHON_VERSION="not-determined-yet" +PYTHON_PLATFORM="unknown-os-and-arch" +BINARY_DIST_URI="https://github.com/chevah/pythia/releases/download" +PIP_INDEX_URL="https://pypi.org/simple" +# This is defined as an array to be passed as a chain of options. +BASE_REQUIREMENTS=() # # Check that we have a pavement.py file in the current dir. @@ -97,8 +98,8 @@ BASE_REQUIREMENTS='' # check_source_folder() { if [ ! -e pavement.py ]; then - (>&2 echo 'No "pavement.py" file found in current folder.') - (>&2 echo 'Make sure you are running "pythia.sh" from a source folder.') + (>&2 echo "No 'pavement.py' file found in current folder.") + (>&2 echo "Make sure you are running 'pythia.sh' from a source folder.") exit 8 fi } @@ -106,7 +107,7 @@ check_source_folder() { # Called to trigger the entry point in the virtual environment. # Can be overwritten in pythia.conf execute_venv() { - ${PYTHON_BIN} $PYTHON3_CHECK -c 'from paver.tasks import main; main()' "$@" + "$PYTHON_BIN" -X utf8 -c "from paver.tasks import main; main()" "$@" } @@ -118,13 +119,22 @@ update_venv() { _clean_pyc set +e - ${PYTHON_BIN} -c 'from paver.tasks import main; main()' deps - exit_code=$? + "$PYTHON_BIN" -c "from paver.tasks import main; main()" deps + exit_code="$?" set -e if [ $exit_code -ne 0 ]; then - (>&2 echo 'Failed to run the initial "./pythia.sh deps" command.') + (>&2 echo "Failed to run the initial './pythia.sh deps' command.") exit 7 fi + + set +e + "$PYTHON_BIN" -c "from paver.tasks import main; main()" build + exit_code="$?" + set -e + if [ $exit_code -ne 0 ]; then + (>&2 echo "Failed to run the initial './pythia.sh build' command.") + exit 8 + fi } # Load repo specific configuration. @@ -133,19 +143,21 @@ source pythia.conf clean_build() { # Shortcut for clear since otherwise it will depend on python - echo "Removing ${BUILD_FOLDER}..." - delete_folder ${BUILD_FOLDER} - echo "Removing dist..." - delete_folder ${DIST_FOLDER} - echo "Removing publish..." - delete_folder 'publish' + echo "Removing $BUILD_FOLDER..." + delete_folder "$BUILD_FOLDER" + echo "Removing $DIST_FOLDER..." + delete_folder "$DIST_FOLDER" + echo "Removing publish/..." + delete_folder publish/ + echo "Removing node_modules/..." + delete_folder node_modules/ # In some case pip hangs with a build folder in temp and # will not continue until it is manually removed. # On the OSX build server tmp is in $TMPDIR - if [ ! -z "${TMPDIR-}" ]; then + if [ -n "${TMPDIR-}" ]; then # check if TMPDIR is set before trying to clean it. - rm -rf ${TMPDIR}/pip* + rm -rf "$TMPDIR"/pip* else rm -rf /tmp/pip* fi @@ -168,7 +180,7 @@ purge_cache() { clean_build echo "Cleaning download cache ..." - rm -rf $CACHE_FOLDER/* + rm -rf "${CACHE_FOLDER:?}"/* } @@ -179,11 +191,13 @@ delete_folder() { local target="$1" # On Windows, we use internal command prompt for maximum speed. # See: https://stackoverflow.com/a/6208144/539264 - if [ $OS = "win" -a -d $target ]; then - cmd //c "del /f/s/q $target > nul" - cmd //c "rmdir /s/q $target" + if [ "$OS" = "windows" ]; then + if [ -d "$target" ]; then + cmd //c "del /f/s/q $target > nul" + cmd //c "rmdir /s/q $target" + fi else - rm -rf $target + rm -rf "$target" fi } @@ -192,17 +206,17 @@ delete_folder() { # Wrapper for executing a command and exiting on failure. # execute() { - if [ $DEBUG -ne 0 ]; then - echo "Executing:" $@ + if [ "$DEBUG" -ne 0 ]; then + echo "Executing:" "$@" fi # Make sure $@ is called in quotes as otherwise it will not work. set +e "$@" - exit_code=$? + exit_code="$?" set -e if [ $exit_code -ne 0 ]; then - (>&2 echo "Failed:" $@) + (>&2 echo "Failed:" "$@") exit 1 fi } @@ -213,51 +227,51 @@ execute() { update_path_variables() { resolve_python_version - if [ "${OS}" = "win" ] ; then + if [ "$OS" = "windows" ] ; then PYTHON_BIN="/lib/python.exe" PYTHON_LIB="/lib/Lib/" else PYTHON_BIN="/bin/python" - PYTHON_LIB="/lib/${PYTHON_NAME}/" + PYTHON_LIB="/lib/$PYTHON_NAME/" fi # Read first from env var. set +o nounset - BUILD_FOLDER="${CHEVAH_BUILD}" - CACHE_FOLDER="${CHEVAH_CACHE}" + BUILD_FOLDER="$CHEVAH_BUILD" + CACHE_FOLDER="$CHEVAH_CACHE" set -o nounset - if [ "${BUILD_FOLDER}" = "" ] ; then + if [ -z "$BUILD_FOLDER" ] ; then # Use value from configuration file. - BUILD_FOLDER="${CHEVAH_BUILD_DIR}" + BUILD_FOLDER="$CHEVAH_BUILD_DIR" fi - if [ "${BUILD_FOLDER}" = "" ] ; then + if [ -z "$BUILD_FOLDER" ] ; then # Use default value if not yet defined. - BUILD_FOLDER="build-${OS}-${ARCH}" + BUILD_FOLDER="build-$OS-$ARCH" fi - if [ "${CACHE_FOLDER}" = "" ] ; then + if [ -z "$CACHE_FOLDER" ] ; then # Use default if not yet defined. - CACHE_FOLDER="${CHEVAH_CACHE_DIR}" + CACHE_FOLDER="$CHEVAH_CACHE_DIR" fi - if [ "${CACHE_FOLDER}" = "" ] ; then + if [ -z "$CACHE_FOLDER" ] ; then # Use default if not yet defined. CACHE_FOLDER="cache" fi - PYTHON_BIN="${BUILD_FOLDER}${PYTHON_BIN}" - PYTHON_LIB="${BUILD_FOLDER}${PYTHON_LIB}" + PYTHON_BIN="$BUILD_FOLDER/$PYTHON_BIN" + PYTHON_LIB="$BUILD_FOLDER/$PYTHON_LIB" LOCAL_PYTHON_BINARY_DIST="$PYTHON_NAME-$OS-$ARCH" - export PYTHONPATH=${BUILD_FOLDER} - export CHEVAH_PYTHON=${PYTHON_NAME} - export CHEVAH_OS=${OS} - export CHEVAH_ARCH=${ARCH} - export CHEVAH_CACHE=${CACHE_FOLDER} - export PIP_INDEX_URL=${PIP_INDEX_URL} + export PYTHONPATH="$BUILD_FOLDER" + export CHEVAH_PYTHON="$PYTHON_NAME" + export CHEVAH_OS="$OS" + export CHEVAH_ARCH="$ARCH" + export CHEVAH_CACHE="$CACHE_FOLDER" + export PIP_INDEX_URL="$PIP_INDEX_URL" } @@ -266,7 +280,7 @@ update_path_variables() { # advertised by the current environment. # resolve_python_version() { - local version_configuration=$PYTHON_CONFIGURATION + local version_configuration="$PYTHON_CONFIGURATION" local version_configuration_array local candidate local candidate_platform @@ -275,18 +289,21 @@ resolve_python_version() { PYTHON_PLATFORM="$OS-$ARCH" # Using ':' as a delimiter, populate a dedicated array. - IFS=: read -a version_configuration_array <<< "$version_configuration" + IFS=: read -r -a version_configuration_array <<< "$version_configuration" # Iterate through all the elements of the array to find the best candidate. for (( i=0 ; i < ${#version_configuration_array[@]}; i++ )); do candidate="${version_configuration_array[$i]}" - candidate_platform=$(echo "$candidate" | cut -d "@" -f 1) - candidate_version=$(echo "$candidate" | cut -d "@" -f 2) + candidate_platform="$(echo "$candidate" | cut -d"@" -f1)" + candidate_version="$(echo "$candidate" | cut -d"@" -f2)" + candidate_name="$(echo "$candidate_version" | cut -d"." -f1-2)" if [ "$candidate_platform" = "default" ]; then - # On first pass, we set the default version. - PYTHON_VERSION=$candidate_version - elif [ "${PYTHON_PLATFORM%$candidate_platform*}" = "" ]; then - # If matching a specific platform, we overwrite the default version. - PYTHON_VERSION=$candidate_version + # On first pass, we set the default version and name. + PYTHON_VERSION="$candidate_version" + PYTHON_NAME="python${candidate_name}" + elif [ -z "${PYTHON_PLATFORM%"$candidate_platform"*}" ]; then + # If matching a specific platform, we overwrite the defaults. + PYTHON_VERSION="$candidate_version" + PYTHON_NAME="python${candidate_name}" fi done } @@ -296,35 +313,24 @@ resolve_python_version() { # Install base package. # install_base_deps() { - echo "Installing base requirements: $BASE_REQUIREMENTS." - pip_install "$BASE_REQUIREMENTS" -} - - -# -# Wrapper for python `pip install` command. -# * $1 - package_name and optional version. -# -pip_install() { - echo "::group::pip install $1" + echo "::group::Installing base requirements:" "${BASE_REQUIREMENTS[@]}" set +e # There is a bug in pip/setuptools when using custom build folders. # See https://github.com/pypa/pip/issues/3564 - rm -rf ${BUILD_FOLDER}/pip-build - ${PYTHON_BIN} -m \ + rm -rf "${BUILD_FOLDER:?}"/pip-build + "$PYTHON_BIN" -m \ pip install \ - --index-url=$PIP_INDEX_URL \ - --build=${BUILD_FOLDER}/pip-build \ - $1 + --index-url="$PIP_INDEX_URL" \ + "${BASE_REQUIREMENTS[@]}" - exit_code=$? + exit_code="$?" echo "::endgroup::" set -e if [ $exit_code -ne 0 ]; then - (>&2 echo "Failed to install $1.") + (>&2 echo "Failed to install" "${BASE_REQUIREMENTS[@]}") exit 2 fi } @@ -334,22 +340,22 @@ pip_install() { # set_download_commands() { set +o errexit - command -v curl > /dev/null - if [ $? -eq 0 ]; then - # Options not used because of no support in CentOS 5.11's curl: + if command -v curl > /dev/null; then + # Options not used because of no support in older curl versions: # --retry-connrefused (since curl 7.52.0) # --retry-all-errors (since curl 7.71.0) # Retry 2 times, allocating 10s for the connection phase, # at most 300s for an attempt, sleeping for 5s between retries. - CURL_RETRY_OPTS="\ + # Strings wouldn't work when quoted, using Bash arrays instead. + CURL_RETRY_OPTS=(\ --retry 2 \ --connect-timeout 10 \ --max-time 300 \ --retry-delay 5 \ - " - DOWNLOAD_CMD="curl --remote-name --location $CURL_RETRY_OPTS" - ONLINETEST_CMD="curl --fail --silent --head $CURL_RETRY_OPTS \ - --output /dev/null" + ) + DOWNLOAD_CMD=(curl --remote-name --location "${CURL_RETRY_OPTS[@]}") + ONLINETEST_CMD=(curl --fail --silent --head "${CURL_RETRY_OPTS[@]}" \ + --output /dev/null) set -o errexit return fi @@ -361,26 +367,26 @@ set_download_commands() { # Download and extract a binary distribution. # get_binary_dist() { - local dist_name=$1 - local remote_base_url=$2 + local dist_name="$1" + local remote_base_url="$2" - echo "Getting $dist_name from $remote_base_url..." + echo "Getting $dist_name from $remote_base_url ..." - tar_gz_file=${dist_name}.tar.gz - tar_file=${dist_name}.tar + tar_gz_file="$dist_name".tar.gz + tar_file="$dist_name".tar - mkdir -p ${CACHE_FOLDER} - pushd ${CACHE_FOLDER} + mkdir -p "$CACHE_FOLDER" + pushd "$CACHE_FOLDER" # Get and extract archive. - rm -rf $dist_name - rm -f $tar_gz_file - rm -f $tar_file - execute $DOWNLOAD_CMD $remote_base_url/${tar_gz_file} - execute gunzip -f $tar_gz_file - execute tar -xf $tar_file - rm -f $tar_gz_file - rm -f $tar_file + rm -rf "$dist_name" + rm -f "$tar_gz_file" + rm -f "$tar_file" + execute "${DOWNLOAD_CMD[@]}" "$remote_base_url"/"$tar_gz_file" + execute gunzip -f "$tar_gz_file" + execute tar -xf "$tar_file" + rm -f "$tar_gz_file" + rm -f "$tar_file" popd } @@ -389,11 +395,11 @@ get_binary_dist() { # Check if we have a versioned Python distribution. # test_version_exists() { - local remote_base_url=$1 - local target_file=python-${PYTHON_VERSION}-${OS}-${ARCH}.tar.gz + local remote_base_url="$1" + local target_file="python-$PYTHON_VERSION-$OS-$ARCH.tar.gz" - echo "Checking $remote_base_url/${PYTHON_VERSION}/$target_file" - $ONLINETEST_CMD $remote_base_url/${PYTHON_VERSION}/$target_file + echo "Checking $remote_base_url/$PYTHON_VERSION/$target_file ..." + "${ONLINETEST_CMD[@]}" "$remote_base_url"/"$PYTHON_VERSION"/"$target_file" return $? } @@ -401,19 +407,19 @@ test_version_exists() { # Download and extract in cache the python distributable. # get_python_dist() { - local remote_base_url=$1 - local download_mode=$2 - local python_distributable=python-${PYTHON_VERSION}-${OS}-${ARCH} + local remote_base_url="$1" + local python_distributable="python-$PYTHON_VERSION-$OS-$ARCH" local onlinetest_errorcode set +o errexit - test_version_exists $remote_base_url - onlinetest_errorcode=$? + test_version_exists "$remote_base_url" + onlinetest_errorcode="$?" set -o errexit if [ $onlinetest_errorcode -eq 0 ]; then # We have the requested python version. - get_binary_dist $python_distributable $remote_base_url/${PYTHON_VERSION} + get_binary_dist "$python_distributable" \ + "$remote_base_url"/"$PYTHON_VERSION" else (>&2 echo "Couldn't find package on remote server. Full link:") echo "$remote_base_url/$PYTHON_VERSION/$python_distributable.tar.gz" @@ -429,51 +435,51 @@ COPY_PYTHON_RECURSIONS=0 # Copy python to build folder from binary distribution. # copy_python() { - local python_distributable="${CACHE_FOLDER}/${LOCAL_PYTHON_BINARY_DIST}" + local python_distributable="$CACHE_FOLDER/$LOCAL_PYTHON_BINARY_DIST" local python_installed_version - COPY_PYTHON_RECURSIONS=`expr $COPY_PYTHON_RECURSIONS + 1` + COPY_PYTHON_RECURSIONS="$((COPY_PYTHON_RECURSIONS + 1))" - if [ $COPY_PYTHON_RECURSIONS -gt 2 ]; then + if [ "$COPY_PYTHON_RECURSIONS" -gt 2 ]; then (>&2 echo "Too many calls to copy_python: $COPY_PYTHON_RECURSIONS") exit 5 fi # Check that python dist was installed - if [ ! -s ${PYTHON_BIN} ]; then + if [ ! -s "$PYTHON_BIN" ]; then # We don't have a Python binary, so we install it since everything # else depends on it. echo "::group::Get Python" - echo "Bootstrapping ${LOCAL_PYTHON_BINARY_DIST} environment" \ - "to ${BUILD_FOLDER}..." - mkdir -p ${BUILD_FOLDER} + echo "Bootstrapping $LOCAL_PYTHON_BINARY_DIST environment" \ + "to $BUILD_FOLDER..." + mkdir -p "$BUILD_FOLDER" - if [ -d ${python_distributable} ]; then + if [ -d "$python_distributable" ]; then # We have a cached distributable. # Check if is at the right version. local cache_ver_file - cache_ver_file=${python_distributable}/lib/PYTHIA_VERSION - cache_version='UNVERSIONED' - if [ -f $cache_ver_file ]; then - cache_version=`cat $cache_ver_file | cut -d - -f 1` + cache_ver_file="$python_distributable"/lib/PYTHIA_VERSION + cache_version="UNVERSIONED" + if [ -f "$cache_ver_file" ]; then + cache_version="$(cut -d"-" -f1 < "$cache_ver_file")" fi if [ "$PYTHON_VERSION" != "$cache_version" ]; then # We have a different version in the cache. # Just remove it and hope that the next step will download # the right one. - rm -rf ${python_distributable} + rm -rf "$python_distributable" fi fi - if [ ! -d ${python_distributable} ]; then + if [ ! -d "$python_distributable" ]; then # We don't have a cached python distributable. - echo "No ${LOCAL_PYTHON_BINARY_DIST} environment." \ + echo "No $LOCAL_PYTHON_BINARY_DIST environment." \ "Start downloading it..." - get_python_dist "$BINARY_DIST_URI" "strict" + get_python_dist "$BINARY_DIST_URI" fi echo "Copying Python distribution files... " - cp -R ${python_distributable}/* ${BUILD_FOLDER} + cp -R "$python_distributable"/* "$BUILD_FOLDER" echo "::endgroup::" @@ -481,19 +487,19 @@ copy_python() { WAS_PYTHON_JUST_INSTALLED=1 else # We have a Python, but we are not sure if is the right version. - local version_file=${BUILD_FOLDER}/lib/PYTHIA_VERSION + local version_file="$BUILD_FOLDER"/lib/PYTHIA_VERSION # If we are upgrading the cache from Python 2, - # cat fails if this file is missing, so we create it blank. - touch $version_file - python_installed_version=`cat $version_file | cut -d - -f 1` + # This file is required, so we create it if non-existing. + touch "$version_file" + python_installed_version="$(cut -d"-" -f1 < "$version_file")" if [ "$PYTHON_VERSION" != "$python_installed_version" ]; then # We have a different python installed. # Check if we have the to-be-updated version and fail if # it does not exists. set +o errexit test_version_exists "$BINARY_DIST_URI" - local test_version=$? + local test_version="$?" set -o errexit if [ $test_version -ne 0 ]; then (>&2 echo "The build is now at $python_installed_version.") @@ -504,9 +510,9 @@ copy_python() { # Remove it and try to install it again. echo "Updating Python from" \ - $python_installed_version to $PYTHON_VERSION - rm -rf ${BUILD_FOLDER}/* - rm -rf ${python_distributable} + "$python_installed_version to $PYTHON_VERSION" + rm -rf "${BUILD_FOLDER:?}"/* + rm -rf "$python_distributable" copy_python fi fi @@ -517,85 +523,93 @@ copy_python() { # Install dependencies after python was just installed. # install_dependencies(){ - if [ $WAS_PYTHON_JUST_INSTALLED -ne 1 ]; then + if [ "$WAS_PYTHON_JUST_INSTALLED" -ne 1 ]; then return fi - if [ "$COMMAND" == "deps" ] ; then - # Will be installed soon. - return + update_venv + + # Deps command was just requested. + # End the process here so that we will not re-run it as part of the + # general command handling. + if [ "$COMMAND" = "deps" ] ; then + exit 0 fi - update_venv } # # Check version of current OS to see if it is supported. # If it's too old, exit with a nice informative message. -# If it's supported, return through eval the version numbers to be used for -# naming the package, for example: '8' for RHEL 8.2, '2004' for Ubuntu 20.04. +# If it's supported, return through eval the version digits to be used for +# naming the package, e.g.: "12" for FreeBSD 12.x or "114" for Solaris 11.4. # check_os_version() { # First parameter should be the human-readable name for the current OS. - # For example: "Red Hat Enterprise Linux" for RHEL, "macOS" for Darwin etc. - # Second and third parameters must be strings composed of integers + # For example: "Solaris" for SunOS ,"macOS" for Darwin, etc. + # Second and third parameters must be strings composed of digits # delimited with dots, representing, in order, the oldest version # supported for the current OS and the current detected version. - # The fourth parameter is used to return through eval the relevant numbers - # for naming the Python package for the current OS, as detailed above. + # The fourth parameter is used to return through eval the relevant digits + # for naming the Pythia package for the current OS, as detailed above. local name_fancy="$1" local version_good="$2" local version_raw="$3" local version_chevah="$4" - local version_constructed='' - local flag_supported='good_enough' + # Version string built in this function, passed back for naming the package. + # Uses the same number of version digits as the "$version_chevah" variable, + # e.g. for FreeBSD it would be "12", even if OS version is actually "12.1". + local version_built="" + # If major/minor/patch/etc. version digits are the same, it's good enough. + local flag_supported="good_enough" local version_raw_array local version_good_array - if [[ $version_raw =~ [^[:digit:]\.] ]]; then - (>&2 echo "OS version should only have numbers and periods, but:") + if [[ "$version_raw" =~ [^[:digit:]\.] ]]; then + (>&2 echo "OS version should only have digits and dots, but:") (>&2 echo " \$version_raw=$version_raw") exit 12 fi - # Using '.' as a delimiter, populate the version_* arrays. - IFS=. read -a version_raw_array <<< "$version_raw" - IFS=. read -a version_good_array <<< "$version_good" + # Using '.' as a delimiter, populate corresponding version_* arrays. + IFS=. read -r -a version_raw_array <<< "$version_raw" + IFS=. read -r -a version_good_array <<< "$version_good" - # Iterate through all the integers from the good version to compare them - # one by one with the corresponding integers from the supported version. + # Iterate through all the digits from the good version to compare them + # one by one with the corresponding digits from the detected version. for (( i=0 ; i < ${#version_good_array[@]}; i++ )); do - version_constructed="${version_constructed}${version_raw_array[$i]}" - if [ ${version_raw_array[$i]} -gt ${version_good_array[$i]} -a \ - "$flag_supported" = 'good_enough' ]; then - flag_supported='true' - elif [ ${version_raw_array[$i]} -lt ${version_good_array[$i]} -a \ - "$flag_supported" = 'good_enough' ]; then - flag_supported='false' + version_built="${version_built}${version_raw_array[$i]}" + # There is nothing to do if versions are the same, that's good enough. + if [ "${version_raw_array[$i]}" -gt "${version_good_array[$i]}" ]; then + # First newer version! Comparing more minor versions is irrelevant. + # Up to now, compared versions were the same, if there were others. + if [ "$flag_supported" = "good_enough" ]; then + flag_supported="newer_version" + fi + elif [ "${version_raw_array[$i]}" -lt "${version_good_array[$i]}" ];then + # First older version! Comparing more minor versions is irrelevant. + # Up to now, compared versions were the same, if there were others. + if [ "$flag_supported" = "good_enough" ]; then + flag_supported="false" + fi fi done - if [ "$flag_supported" = 'false' ]; then - (>&2 echo "Detected version of ${name_fancy} is: ${version_raw}.") - (>&2 echo "For versions older than ${name_fancy} ${version_good},") - if [ "$OS" = "Linux" ]; then - # For old and/or unsupported Linux distros there's a second chance! - (>&2 echo "the generic Linux runtime is used, if possible.") - check_linux_libc - else - (>&2 echo "there is currently no support.") - exit 13 - fi + # If "$flag_supported" is "newer_version" / "good_enough" is now irrelevant. + if [ "$flag_supported" = "false" ]; then + (>&2 echo "Detected version of $name_fancy is: $version_raw.") + (>&2 echo "For versions older than $name_fancy $version_good,") + (>&2 echo "there is currently no support.") + exit 13 fi - # The sane way to return fancy values with a bash function is to use eval. - eval $version_chevah="'$version_constructed'" + # The sane way to return fancy values with a Bash function is to use eval. + eval "$version_chevah"="'$version_built'" } # -# For old unsupported Linux distros (some with no /etc/os-release) and for other -# unsupported Linux distros, we check if the system is based on glibc or musl. +# On Linux, we check if the system is based on glibc or musl. # If so, we use a generic code path that builds everything statically, # including OpenSSL, thus only requiring glibc or musl. # @@ -603,23 +617,20 @@ check_linux_libc() { local ldd_output_file=".chevah_libc_version" set +o errexit - command -v ldd > /dev/null - if [ $? -ne 0 ]; then - (>&2 echo "No ldd binary found, can't check for glibc!") + if ! command -v ldd > /dev/null; then + (>&2 echo "No ldd binary found, can't check the libc version!") exit 18 fi - ldd --version > $ldd_output_file 2>&1 - egrep "GNU libc|GLIBC" $ldd_output_file > /dev/null - if [ $? -eq 0 ]; then + ldd --version > "$ldd_output_file" 2>&1 + if grep -E -q "GNU libc|GLIBC" "$ldd_output_file"; then check_glibc_version else - egrep ^"musl libc" $ldd_output_file > /dev/null - if [ $? -eq 0 ]; then + if grep -E -q ^"musl libc" $ldd_output_file; then check_musl_version else - (>&2 echo "Unknown libc reported by ldd... Unsupported Linux.") - rm $ldd_output_file + (>&2 echo "Unknown libc reported by ldd... Unsupported Linux!") + rm "$ldd_output_file" exit 19 fi fi @@ -633,105 +644,89 @@ check_glibc_version(){ local supported_glibc2_version # Supported minimum minor glibc 2.X versions for various arches. - # For x64, we build on CentOS 5.11 (Final) with glibc 2.5. - # For arm64, we build on Ubuntu 16.04 with glibc 2.23. - # Beware we haven't normalized arch names yet. - case "$ARCH" in - "amd64"|"x86_64"|"x64") - supported_glibc2_version=5 - ;; - "aarch64"|"arm64") - supported_glibc2_version=23 - ;; - *) - (>&2 echo "$ARCH is an unsupported arch for generic Linux!") - exit 17 - ;; - esac + # For x64, we build on Amazon 2 with glibc 2.26. + # For arm64, we also build on Amazon 2 with glibc 2.26 lately. + # If we get back to building against different libc versions per arch, + # beware we haven't normalized arch names yet. + supported_glibc2_version=26 echo "No specific runtime for the current distribution / version / arch." - echo "Minimum glibc version for this arch: 2.${supported_glibc2_version}." + echo "Minimum glibc version for this arch: 2.$supported_glibc2_version." - # Tested with glibc 2.5/2.11.3/2.12/2.23/2.28-31 and eglibc 2.13/2.19. - glibc_version=$(head -n 1 $ldd_output_file | rev | cut -d\ -f1 | rev) - rm $ldd_output_file + # Tested with glibc 2.5/2.11.3/2.12/2.23/2.28-35 and eglibc 2.13/2.19. + glibc_version="$(head -n 1 "$ldd_output_file" | rev | cut -d" " -f1 | rev)" + rm "$ldd_output_file" - if [[ $glibc_version =~ [^[:digit:]\.] ]]; then - (>&2 echo "Glibc version should only have numbers and periods, but:") + if [[ "$glibc_version" =~ [^[:digit:]\.] ]]; then + (>&2 echo "Glibc version should only have digits and dots, but:") (>&2 echo " \$glibc_version=$glibc_version") exit 20 fi - IFS=. read -a glibc_version_array <<< "$glibc_version" + IFS=. read -r -a glibc_version_array <<< "$glibc_version" - if [ ${glibc_version_array[0]} -ne 2 ]; then + if [ "${glibc_version_array[0]}" -ne 2 ]; then (>&2 echo "Only glibc 2 is supported! Detected version: $glibc_version") exit 21 fi - # Decrement supported_glibc2_version if building against an older glibc. - if [ ${glibc_version_array[1]} -lt ${supported_glibc2_version} ]; then - (>&2 echo "NOT good. Detected version is older: ${glibc_version}!") + # Decrement supported_glibc2_version above if building against older glibc. + if [ "${glibc_version_array[1]}" -lt "$supported_glibc2_version" ]; then + (>&2 echo "NOT good. Detected version is older: $glibc_version!") exit 22 else - echo "All is good. Detected glibc version: ${glibc_version}." + echo "All is good. Detected glibc version: $glibc_version." fi # Supported glibc version detected, set $OS for a generic glibc Linux build. - OS="lnx" + OS="linux" } check_musl_version(){ local musl_version local musl_version_array + local musl_version_unsupported="false" local supported_musl11_version=24 echo "No specific runtime for the current distribution / version / arch." - echo "Minimum musl version for this arch: 1.1.${supported_musl11_version}." + echo "Minimum musl version for this arch: 1.1.$supported_musl11_version." # Tested with musl 1.1.24/1.2.2. - musl_version=$(egrep ^Version $ldd_output_file | cut -d\ -f2) - rm $ldd_output_file + musl_version="$(grep -E ^"Version" "$ldd_output_file" | cut -d" " -f2)" + rm "$ldd_output_file" - if [[ $musl_version =~ [^[:digit:]\.] ]]; then - (>&2 echo "Musl version should only have numbers and periods, but:") + if [[ "$musl_version" =~ [^[:digit:]\.] ]]; then + (>&2 echo "Musl version should only have digits and dots, but:") (>&2 echo " \$musl_version=$musl_version") exit 25 fi - IFS=. read -a musl_version_array <<< "$musl_version" + IFS=. read -r -a musl_version_array <<< "$musl_version" + + # Decrement supported_musl11_version above if building against older musl. + if [ "${musl_version_array[0]}" -lt 1 ]; then + musl_version_unsupported="true" + elif [ "${musl_version_array[0]}" -eq 1 ]; then + if [ "${musl_version_array[1]}" -lt 1 ];then + musl_version_unsupported="true" + elif [ "${musl_version_array[1]}" -eq 1 ];then + if [ "${musl_version_array[2]}" -lt "$supported_musl11_version" ] + then + (>&2 echo "NOT good. Detected version is older: $musl_version!") + exit 27 + fi + fi + fi - if [ ${musl_version_array[0]} -lt 1 -o ${musl_version_array[1]} -lt 1 ];then + if [ "$musl_version_unsupported" = "true" ]; then (>&2 echo "Only musl 1.1 or greater supported! Detected: $musl_version") exit 26 fi - # Decrement supported_musl11_version if building against an older musl. - if [ ${musl_version_array[0]} -eq 1 -a ${musl_version_array[1]} -eq 1 \ - -a ${musl_version_array[2]} -lt ${supported_musl11_version} ]; then - (>&2 echo "NOT good. Detected version is older: ${musl_version}!") - exit 27 - else - echo "All is good. Detected musl version: ${musl_version}." - fi + echo "All is good. Detected musl version: $musl_version." # Supported musl version detected, set $OS for a generic musl Linux build. - OS="lnx_musl" -} - -# -# For Linux distros with a supported libc, after checking if current version is -# supported with check_os_version(), $OS might be set to something like "lnx" -# if current version is too old, through check_linux_libc() and its subroutines. -# -set_os_if_not_generic() { - local distro_name="$1" - local distro_version="$2" - - if [ "${OS#lnx}" = "$OS" ]; then - # $OS doesn't start with lnx, not a generic Linux build. - OS="${distro_name}${distro_version}" - fi + OS="linux_musl" } @@ -740,103 +735,56 @@ set_os_if_not_generic() { # In some cases we normalize or even override ARCH at the end of this function. # detect_os() { - OS=$(uname -s) + local os_version_chevah="" + OS="$(uname -s)" case "$OS" in MINGW*|MSYS*) - ARCH=$(uname -m) - OS="win" + ARCH="$(uname -m)" + OS="windows" ;; Linux) - ARCH=$(uname -m) - if [ ! -f /etc/os-release ]; then - # No /etc/os-release file present, so we don't support this - # distro, but check for glibc, the generic build should work. - check_linux_libc - else - source /etc/os-release - linux_distro="$ID" - distro_fancy_name="$NAME" - # Some rolling-release distros (eg. Arch Linux) have - # no VERSION_ID here, so don't count on it unconditionally. - case "$linux_distro" in - rhel|centos|almalinux|rocky|ol) - os_version_raw="$VERSION_ID" - check_os_version "Red Hat Enterprise Linux" 8 \ - "$os_version_raw" os_version_chevah - if [ ${os_version_chevah} == "8" ]; then - set_os_if_not_generic "rhel" $os_version_chevah - else - # OpenSSL 3.0.x not supported by cryptography 3.3.x. - check_linux_libc - fi - ;; - ubuntu|ubuntu-core) - os_version_raw="$VERSION_ID" - # For versions with older OpenSSL, use generic build. - check_os_version "$distro_fancy_name" 18.04 \ - "$os_version_raw" os_version_chevah - # Only LTS versions are supported. If it doesn't end in - # 04 or first two digits are uneven, use generic build. - if [ ${os_version_chevah%%04} == ${os_version_chevah} \ - -o $(( ${os_version_chevah:0:2} % 2 )) -ne 0 ]; then - check_linux_libc - elif [ ${os_version_chevah} == "2204" ]; then - # OpenSSL 3.0.x not supported by cryptography 3.3.x. - check_linux_libc - fi - set_os_if_not_generic "ubuntu" $os_version_chevah - ;; - *) - # Supported distros with unsupported OpenSSL versions or - # distros not specifically supported: SLES, Debian, etc. - check_linux_libc - ;; - esac - fi + ARCH="$(uname -m)" + check_linux_libc ;; Darwin) - ARCH=$(uname -m) - os_version_raw=$(sw_vers -productVersion) + ARCH="$(uname -m)" + os_version_raw="$(sw_vers -productVersion)" check_os_version "macOS" 10.13 "$os_version_raw" os_version_chevah # Build a generic package to cover all supported versions. OS="macos" ;; FreeBSD) - ARCH=$(uname -m) - os_version_raw=$(uname -r | cut -d'.' -f1) + ARCH="$(uname -m)" + os_version_raw="$(uname -r | cut -d'.' -f1)" check_os_version "FreeBSD" 12 "$os_version_raw" os_version_chevah - OS="fbsd${os_version_chevah}" + OS="fbsd$os_version_chevah" ;; OpenBSD) - ARCH=$(uname -m) - os_version_raw=$(uname -r) + ARCH="$(uname -m)" + os_version_raw="$(uname -r)" check_os_version "OpenBSD" 6.7 "$os_version_raw" os_version_chevah - OS="obsd${os_version_chevah}" + OS="obsd$os_version_chevah" ;; SunOS) - ARCH=$(isainfo -n) - ver_major=$(uname -r | cut -d'.' -f2) + ARCH="$(isainfo -n)" + ver_major="$(uname -r | cut -d"." -f2)" case $ver_major in - 10) - ver_minor=$(\ - head -1 /etc/release | cut -d_ -f2 | sed s/[^0-9]*//g) - ;; 11) - ver_minor=$(uname -v | cut -d'.' -f2) + ver_minor="$(uname -v | cut -d"." -f2)" ;; *) - # Not sure if $ver_minor detection works on other versions. - (>&2 echo "Unsupported Solaris version: ${ver_major}.") + # Note $ver_minor detection doesn't work on older versions. + (>&2 echo "Unsupported Solaris version: $ver_major.") exit 15 ;; esac - os_version_raw="${ver_major}.${ver_minor}" + os_version_raw="$ver_major.$ver_minor" check_os_version "Solaris" 11.4 "$os_version_raw" os_version_chevah - OS="sol${os_version_chevah}" + OS="sol$os_version_chevah" ;; *) - (>&2 echo "Unsupported operating system: ${OS}.") + (>&2 echo "Unsupported operating system: $OS.") exit 14 ;; esac @@ -848,17 +796,6 @@ detect_os() { ;; "amd64"|"x86_64") ARCH="x64" - case "$OS" in - win) - # 32bit build on Windows 2019, 64bit otherwise. - # Should work with a l10n pack too (tested with French). - win_ver=$(systeminfo.exe | head -n 3 | tail -n 1 \ - | cut -d ":" -f 2) - if [[ "$win_ver" =~ "Microsoft Windows Server 2019" ]]; then - ARCH="x86" - fi - ;; - esac ;; "aarch64") ARCH="arm64" @@ -880,8 +817,8 @@ if [ "$COMMAND" = "purge" ] ; then exit 0 fi -# Initialize BUILD_ENV_VARS file when building Python from scratch. -if [ "$COMMAND" == "detect_os" ]; then +# Pass OS-specific values through a file when building Python from scratch. +if [ "$COMMAND" = "detect_os" ]; then echo "PYTHON_VERSION=$PYTHON_NAME" > BUILD_ENV_VARS echo "OS=$OS" >> BUILD_ENV_VARS echo "ARCH=$ARCH" >> BUILD_ENV_VARS @@ -889,10 +826,10 @@ if [ "$COMMAND" == "detect_os" ]; then fi if [ "$COMMAND" = "get_python" ] ; then - OS=$2 - ARCH=$3 + OS="$2" + ARCH="$3" resolve_python_version - get_python_dist "$BINARY_DIST_URI" "fallback" + get_python_dist "$BINARY_DIST_URI" exit 0 fi @@ -901,22 +838,13 @@ copy_python install_dependencies # Update pythia.conf dependencies when running deps. -if [ "$COMMAND" == "deps" ] ; then +if [ "$COMMAND" = "deps" ] ; then install_base_deps fi -case $COMMAND in - test_ci|test_py3) - PYTHON3_CHECK='-3' - ;; - *) - PYTHON3_CHECK='' - ;; -esac - set +e execute_venv "$@" -exit_code=$? +exit_code="$?" set -e exit $exit_code diff --git a/release-notes.rst b/release-notes.rst index 8671f3f..588f15b 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,6 +1,14 @@ Release notes for Chevah KeyCert ################################ +3.1.0 - 2024-03-11 +================== + +* Remove support for py2 +* Remove support for LSH +* Add support for Putty key gen3 + + 3.0.12 - 2024-01-27 =================== diff --git a/src/chevah_keycert/__init__.py b/src/chevah_keycert/__init__.py index 52fa96c..25d61c3 100644 --- a/src/chevah_keycert/__init__.py +++ b/src/chevah_keycert/__init__.py @@ -1,9 +1,11 @@ """ SSL and SSH key management. """ -from __future__ import absolute_import +import collections import sys import six +import base64 +import inspect def _path(path, encoding='utf-8'): @@ -25,3 +27,20 @@ def native_string(string): if six.PY2: string = string.encode('ascii') return string + + +for member in ['Callable', 'Iterable', 'Mapping', 'Sequence']: + if not hasattr(collections, member): + setattr(collections, member, getattr(collections.abc, member)) +import cryptography.utils +if not hasattr(cryptography.utils, 'int_from_bytes'): + cryptography.utils.int_from_bytes = int.from_bytes + +if not hasattr(base64, 'encodestring'): + base64.encodestring = base64.encodebytes + +if not hasattr(base64, 'decodestring'): + base64.decodestring = base64.decodebytes + +if not hasattr(inspect, "getargspec"): + inspect.getargspec = lambda func: inspect.getfullargspec(func)[:4] diff --git a/src/chevah_keycert/common.py b/src/chevah_keycert/common.py index 433d24b..6cd6e75 100644 --- a/src/chevah_keycert/common.py +++ b/src/chevah_keycert/common.py @@ -11,76 +11,53 @@ import struct from cryptography.utils import int_from_bytes, int_to_bytes -import six -from six.moves import range -# Functions for dealing with Python 3's bytes type, which is somewhat -# different than Python 2's: -if six.PY3: - def iterbytes(originalBytes): - for i in range(len(originalBytes)): - yield originalBytes[i:i+1] +def iterbytes(originalBytes): + for i in range(len(originalBytes)): + yield originalBytes[i:i + 1] - def intToBytes(i): - return ("%d" % i).encode("ascii") +def intToBytes(i): + return ("%d" % i).encode("ascii") - def lazyByteSlice(object, offset=0, size=None): - """ - Return a copy of the given bytes-like object. - - If an offset is given, the copy starts at that offset. If a size is - given, the copy will only be of that length. - - @param object: C{bytes} to be copied. - - @param offset: C{int}, starting index of copy. - - @param size: Optional, if an C{int} is given limit the length of copy - to this size. - """ - view = memoryview(object) - if size is None: - return view[offset:] - else: - return view[offset:(offset + size)] - +def lazyByteSlice(object, offset=0, size=None): + """ + Return a copy of the given bytes-like object. - def networkString(s): - if not isinstance(s, unicode): - raise TypeError("Can only convert text to bytes on Python 3") - return s.encode('ascii') -else: - def iterbytes(originalBytes): - return originalBytes + If an offset is given, the copy starts at that offset. If a size is + given, the copy will only be of that length. + @param object: C{bytes} to be copied. - def intToBytes(i): - return b"%d" % i + @param offset: C{int}, starting index of copy. - lazyByteSlice = buffer + @param size: Optional, if an C{int} is given limit the length of copy + to this size. + """ + view = memoryview(object) + if size is None: + return view[offset:] + else: + return view[offset:(offset + size)] - def networkString(s): - if not isinstance(s, str): - raise TypeError("Can only pass-through bytes on Python 2") - # Ensure we're limited to ASCII subset: - s.decode('ascii') - return s +def networkString(s): + if not isinstance(s, str): + raise TypeError("Can only convert text to bytes on Python 3") + return s.encode('ascii') def NS(t): """ net string """ - if isinstance(t, six.text_type): + if isinstance(t, str): t = t.encode("utf-8") return struct.pack('!L', len(t)) + t - def getNS(s, count=1): """ get net string @@ -94,7 +71,6 @@ def getNS(s, count=1): return tuple(ns) + (s[c:],) - def MP(number): if number == 0: return b'\000' * 4 @@ -105,7 +81,6 @@ def MP(number): return struct.pack('>L', len(bn)) + bn - def getMP(data, count=1): """ Get multiple precision integer out of the string. A multiple precision @@ -123,7 +98,6 @@ def getMP(data, count=1): return tuple(mp) + (data[c:],) - def ffs(c, s): """ first from second @@ -134,7 +108,6 @@ def ffs(c, s): return i - def force_unicode(value): """ Decode the `value` to unicode. @@ -148,43 +121,43 @@ def force_unicode(value): def str_or_repr(value): - if isinstance(value, six.text_type): + if isinstance(value, str): return value try: - return six.text_type(value, encoding='utf-8') + return str(value, encoding='utf-8') except Exception: """ Not UTF-8 encoded value. """ try: - return six.text_type(value, encoding='windows-1252') + return str(value, encoding='windows-1252') except Exception: """ Not Windows encoded value. """ try: - return six.text_type(str(value), encoding='utf-8', errors='replace') + return str(str(value), encoding='utf-8', errors='replace') except (UnicodeDecodeError, UnicodeEncodeError): """ Not UTF-8 encoded value. """ try: - return six.text_type( + return str( str(value), encoding='windows-1252', errors='replace') except (UnicodeDecodeError, UnicodeEncodeError): pass # No luck with str, try repr() - return six.text_type(repr(value), encoding='windows-1252', errors='replace') + return str(repr(value), encoding='windows-1252', errors='replace') if value is None: return u'None' - if isinstance(value, six.text_type): + if isinstance(value, str): return value if isinstance(value, EnvironmentError) and value.errno: diff --git a/src/chevah_keycert/sexpy.py b/src/chevah_keycert/sexpy.py deleted file mode 100644 index e2eb99b..0000000 --- a/src/chevah_keycert/sexpy.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. -""" -S-expression read / write. - -Forked from twisted.conch.ssh.sexpy -""" - - -def parse(s): - s = s.strip() - expr = [] - while s: - if s[0] == '(': - newSexp = [] - if expr: - expr[-1].append(newSexp) - expr.append(newSexp) - s = s[1:] - continue - if s[0] == ')': - aList = expr.pop() - s = s[1:] - if not expr: - assert not s - return aList - continue - i = 0 - while s[i].isdigit(): - i += 1 - assert i - length = int(s[:i]) - data = s[i + 1:i + 1 + length] - expr[-1].append(data) - s = s[i + 1 + length:] - assert 0, "this should not happen" # pragma: no cover - - -def pack(sexp): - s = "" - for o in sexp: - if type(o) in (type(()), type([])): - s += '(' - s += pack(o) - s += ')' - else: - s += '%i:%s' % (len(o), o) - return s diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index e7408c0..4bbab45 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -29,7 +29,6 @@ load_pem_private_key, load_ssh_public_key) from cryptography import utils from six.moves import map -import six from six.moves import range try: @@ -62,7 +61,7 @@ from cryptography.utils import int_from_bytes, int_to_bytes from OpenSSL import crypto -from chevah_keycert import common, sexpy, _path +from chevah_keycert import common, _path from chevah_keycert.common import ( force_unicode, iterbytes, @@ -87,12 +86,14 @@ b'ecdsa-sha2-nistp256': ec.SECP256R1(), b'ecdsa-sha2-nistp384': ec.SECP384R1(), b'ecdsa-sha2-nistp521': ec.SECP521R1(), + b'ecdsa-sha2-nistp192': ec.SECP192R1(), } _secToNist = { 'secp256r1' : b'nistp256', 'secp384r1' : b'nistp384', 'secp521r1' : b'nistp521', + 'secp192r1' : b'nistp192', } @@ -263,8 +264,6 @@ def fromString(cls, data, type=None, passphrase=None): # we consider it too short. raise BadKeyError('Key is too short.') except (struct.error, binascii.Error, TypeError): - import traceback - ca = traceback.format_exc() raise BadKeyError('Fail to parse key content.') @classmethod @@ -631,75 +630,6 @@ def _fromString_PRIVATE_OPENSSH(cls, data, passphrase): raise BadKeyError('unknown key type "%s"' % (force_unicode(kind),)) - @classmethod - def _fromString_PUBLIC_LSH(cls, data): - """ - Return a public key corresponding to this LSH public key string. - The LSH public key string format is:: - , ()+))> - - The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e. - The names for a DSA (key type 'dsa') key are: y, g, p, q. - - @type data: L{bytes} - @param data: The key data. - - @return: A new key. - @rtype: L{twisted.conch.ssh.keys.Key} - @raises BadKeyError: if the key type is unknown - """ - sexp = sexpy.parse(decodebytes(data[1:-1])) - assert sexp[0] == b'public-key' - kd = {} - for name, data in sexp[1][1:]: - kd[name] = common.getMP(common.NS(data))[0] - if sexp[1][0] == b'dsa': - return cls._fromDSAComponents( - y=kd[b'y'], g=kd[b'g'], p=kd[b'p'], q=kd[b'q']) - - elif sexp[1][0] == b'rsa-pkcs1-sha1': - return cls._fromRSAComponents(n=kd[b'n'], e=kd[b'e']) - else: - raise BadKeyError('unknown lsh key type "%s"' % ( - force_unicode(sexp[1][0][:30]),)) - - @classmethod - def _fromString_PRIVATE_LSH(cls, data): - """ - Return a private key corresponding to this LSH private key string. - The LSH private key string format is:: - , (, )+))> - - The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e, d, p, q. - The names for a DSA (key type 'dsa') key are: y, g, p, q, x. - - @type data: L{bytes} - @param data: The key data. - - @return: A new key. - @rtype: L{twisted.conch.ssh.keys.Key} - @raises BadKeyError: if the key type is unknown - """ - sexp = sexpy.parse(data) - assert sexp[0] == b'private-key' - kd = {} - for name, data in sexp[1][1:]: - kd[name] = common.getMP(common.NS(data))[0] - if sexp[1][0] == b'dsa': - assert len(kd) == 5, len(kd) - return cls._fromDSAComponents( - y=kd[b'y'], g=kd[b'g'], p=kd[b'p'], q=kd[b'q'], x=kd[b'x']) - elif sexp[1][0] == b'rsa-pkcs1': - assert len(kd) == 8, len(kd) - if kd[b'p'] > kd[b'q']: # Make p smaller than q - kd[b'p'], kd[b'q'] = kd[b'q'], kd[b'p'] - return cls._fromRSAComponents( - n=kd[b'n'], e=kd[b'e'], d=kd[b'd'], p=kd[b'p'], q=kd[b'q']) - - else: - raise BadKeyError( - 'unknown lsh key type "%s"' % (force_unicode(sexp[1][0][:30]),)) - @classmethod def _fromString_AGENTV3(cls, data): """ @@ -790,10 +720,8 @@ def _guessStringType(cls, data): return 'private_encrypted_pkcs8' elif data.startswith(b'PuTTY-User-Key-File-2'): return 'private_putty' - elif data.startswith(b'{'): - return 'public_lsh' - elif data.startswith(b'('): - return 'private_lsh' + elif data.startswith(b'PuTTY-User-Key-File-3'): + return 'private_putty_v3' elif (data.startswith(b'\x00\x00\x00\x07ssh-') or data.startswith(b'\x00\x00\x00\x13ecdsa-') or data.startswith(b'\x00\x00\x00\x0bssh-ed25519')): @@ -1456,6 +1384,7 @@ def toString(self, type, extra=None, comment=None, raise BadKeyError( 'unknown key type: "%s"' % (force_unicode(type[:30]),)) + passphrase = _normalizePassphrase(passphrase) return method(comment=comment, passphrase=passphrase) def _toPublicOpenSSH(self, comment=None): @@ -1618,61 +1547,6 @@ def _toString_OPENSSH(self, comment=None, passphrase=None): b' PRIVATE KEY-----'))) return b'\n'.join(lines) - def _toString_LSH(self, **kwargs): - """ - Return a public or private LSH key. See _fromString_PUBLIC_LSH and - _fromString_PRIVATE_LSH for the key formats. - - @rtype: L{bytes} - """ - data = self.data() - type = self.type() - if self.isPublic(): - if type == 'RSA': - keyData = sexpy.pack([[b'public-key', - [b'rsa-pkcs1-sha1', - [b'n', common.MP(data['n'])[4:]], - [b'e', common.MP(data['e'])[4:]]]]]) - elif type == 'DSA': - keyData = sexpy.pack([[b'public-key', - [b'dsa', - [b'p', common.MP(data['p'])[4:]], - [b'q', common.MP(data['q'])[4:]], - [b'g', common.MP(data['g'])[4:]], - [b'y', common.MP(data['y'])[4:]]]]]) - else: - raise BadKeyError( - 'unknown key type "%s"' % (force_unicode(type,))) - return (b'{' + encodebytes(keyData).replace(b'\n', b'') + - b'}') - else: - if type == 'RSA': - p, q = data['p'], data['q'] - iqmp = rsa.rsa_crt_iqmp(p, q) - return sexpy.pack([[b'private-key', - [b'rsa-pkcs1', - [b'n', common.MP(data['n'])[4:]], - [b'e', common.MP(data['e'])[4:]], - [b'd', common.MP(data['d'])[4:]], - [b'p', common.MP(q)[4:]], - [b'q', common.MP(p)[4:]], - [b'a', common.MP( - data['d'] % (q - 1))[4:]], - [b'b', common.MP( - data['d'] % (p - 1))[4:]], - [b'c', common.MP(iqmp)[4:]]]]]) - elif type == 'DSA': - return sexpy.pack([[b'private-key', - [b'dsa', - [b'p', common.MP(data['p'])[4:]], - [b'q', common.MP(data['q'])[4:]], - [b'g', common.MP(data['g'])[4:]], - [b'y', common.MP(data['y'])[4:]], - [b'x', common.MP(data['x'])[4:]]]]]) - else: - raise BadKeyError( - 'unknown key type "%s"' % (force_unicode(type,))) - def _toString_AGENTV3(self, **kwargs): """ Return a private Secure Shell Agent v3 key. See @@ -1902,7 +1776,8 @@ def getKeyFormat(cls, data): 'private_openssh_v1': 'OpenSSH Private new format', 'public_sshcom': 'SSH.com Public', 'private_sshcom': 'SSH.com Private', - 'private_putty': 'PuTTY Private', + 'private_putty': 'PuTTY Private v2', + 'private_putty_v3': 'PuTTY Private v3', 'public_lsh': 'LSH Public', 'private_lsh': 'LSH Private', 'public_x509_certificate': 'X509 Certificate', @@ -2256,7 +2131,9 @@ def _toString_SSHCOM_private(self, extra): @classmethod def _fromString_PRIVATE_PUTTY(cls, data, passphrase): """ - Read a private Putty key. + Read a private Putty key v2. + + See https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html Format is: @@ -2312,7 +2189,6 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): Lines are terminated by CRLF, although CR-only and LF-only are tolerated on input. - Only version 2 is supported. Version 2 was introduced in PuTTY 0.52. Version 1 was an in-development format used in 0.52 snapshot """ @@ -2433,7 +2309,8 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): @staticmethod def _getPuttyAES256EncryptionKey(passphrase): """ - Return the encryption key used in Putty AES 256 cipher. + Return the encryption key used in Putty AES 256 cipher for + version 2 of the format. """ key_size = 32 part_1 = sha1(b'\x00\x00\x00\x00' + passphrase).digest() @@ -2571,6 +2448,199 @@ def _toString_PUTTY_private(self, extra): lines.append('Private-MAC: %s' % private_mac) return '\r\n'.join(lines).encode('utf-8') + + @classmethod + def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): + """ + Read a private Putty key v3. + + See https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html + + Format is: + + PuTTY-User-Key-File-3: ssh-rsa + Encryption: none + Comment: SINGLE_LINE_COMMENT + Public-Lines: PUBLIC_LINES + < base64 public part always in plain > + Private-Lines: 8 + < base64 private part > + Private-MAC: 1398fbfc7ce307d9ee0e42851f183f88c728398f + + PuTTY-User-Key-File-3: ssh-rsa + Encryption: aes256-cbc + Comment: SINGLE_LINE_COMMENT + Public-Lines: PUBLIC_LINES + < base64 public part always in plain > + Key-Derivation: Argon2id | Argon2d | Argon2i + Argon2-Memory: 8192 + Argon2-Passes: 34 + Argon2-Parallelism: 1 + Argon2-Salt: f50f57e4294c1db7e677cf38b7010ff2 + Private-Lines: 8 + < base64 private part > + Private-MAC: 1398fbfc7ce307d9ee0e42851f183f88c728398f + + Pulic part RSA: + * string type (ssh-rsa) + * mpint e + * mpint n + Private part RSA: + * mpint d + * mpint q + * mpint p + * mpint u + + Pulic part DSA: + * string type (ssh-dss) + * mpint p + * mpint q + * mpint g + * mpint v` + Private part DSA: + * mpint x + + Public part ECDSA-SHA2-*: + * string 'ecdsa-sha2-[identifier]' + * string identifier + * mpint x + * mpint y + Private part ECDSA-SHA2-*: + * string q + * mpint privateValue + + Public part Ed25519: + * string type (ssh-ed25519) + * string a + Private part Ed25519: + * string k + + Private part is padded for encryption. + + Encryption key is composed of concatenating, up to block size: + * uint32 sequence, starting from 0 + * passphrase + + Lines are terminated by CRLF, although CR-only and LF-only are + tolerated on input. + + Version 3 was introduced in PuTTY 0.75. + """ + lines = data.decode('utf-8').strip().splitlines() + + key_type = lines[0][22:].strip().lower() + if key_type not in [ + 'ssh-rsa', + 'ssh-dss', + 'ssh-ed25519', + ] and key_type.encode('ascii') not in _curveTable: + raise BadKeyError( + 'Unsupported key type: "%s"' % force_unicode(key_type[:30])) + + encryption_type = lines[1][11:].strip().lower() + + if encryption_type == 'none': + if passphrase: + raise BadKeyError('PuTTY key not encrypted') + elif encryption_type != 'aes256-cbc': + raise BadKeyError( + 'Unsupported encryption type: "%s"' % force_unicode( + encryption_type[:30])) + + comment = lines[2][9:].strip() + + public_count = int(lines[3][14:].strip()) + base64_content = ''.join(lines[ + 4: + 4 + public_count + ]) + public_blob = base64.decodestring(base64_content.encode('utf-8')) + public_type, public_payload = common.getNS(public_blob) + + if public_type.decode('ascii').lower() != key_type: + raise BadKeyError( + 'Mismatch key type. Header has "%s", public has "%s"' % ( + force_unicode(key_type[:30]), + force_unicode(public_type[:30]))) + + # We skip 4 lines so far and the total public lines. + private_start_line = 4 + public_count + private_count = int(lines[private_start_line][15:].strip()) + base64_content = ''.join(lines[ + private_start_line + 1: + private_start_line + 1 + private_count + ]) + private_blob = base64.decodestring(base64_content.encode('ascii')) + + private_mac = lines[-1][12:].strip() + + hmac_key = PUTTY_HMAC_KEY + encryption_key = None + if encryption_type == 'aes256-cbc': + if not passphrase: + raise EncryptedKeyError( + 'Passphrase must be provided for an encrypted key.') + hmac_key += passphrase + encryption_key = cls._getPuttyAES256EncryptionKey_v3(passphrase) + decryptor = Cipher( + algorithms.AES(encryption_key), + modes.CBC(b'\x00' * 16), + backend=default_backend() + ).decryptor() + private_blob = ( + decryptor.update(private_blob) + decryptor.finalize()) + + # I have no idea why these values are packed form HMAC as net strings. + hmac_data = ( + common.NS(key_type) + + common.NS(encryption_type) + + common.NS(comment) + + common.NS(public_blob) + + common.NS(private_blob) + ) + hmac_key = sha1(hmac_key).digest() + computed_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest() + if private_mac != computed_mac: + if encryption_key: + raise EncryptedKeyError('Bad password or HMAC mismatch.') + else: + raise BadKeyError( + 'HMAC mismatch: file declare "%s", actual is "%s"' % ( + force_unicode(private_mac), + force_unicode(computed_mac))) + + if key_type == 'ssh-rsa': + e, n, _ = common.getMP(public_payload, count=2) + d, q, p, u, _ = common.getMP(private_blob, count=4) + return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) + + if key_type == 'ssh-dss': + p, q, g, y, _ = common.getMP(public_payload, count=4) + x, _ = common.getMP(private_blob) + return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) + + if key_type == 'ssh-ed25519': + a, _ = common.getNS(public_payload) + k, _ = common.getNS(private_blob) + return cls._fromEd25519Components(a=a, k=k) + + if key_type.encode('ascii') in _curveTable: + curve = _curveTable[key_type.encode('ascii')] + curveName, q, _ = common.getNS(public_payload, 2) + if curveName != _secToNist[curve.name]: + raise BadKeyError( + 'ECDSA curve name "%s" does not match key type "%s"' % ( + force_unicode(curveName), + force_unicode(key_type))) + + privateValue, _ = common.getMP(private_blob) + return cls._fromECEncodedPoint( + encodedPoint=q, + curve=key_type.encode('ascii'), + privateValue=privateValue, + ) + + @classmethod def _fromString_PUBLIC_X509_CERTIFICATE(cls, data): """ @@ -2610,7 +2680,7 @@ def _fromString_PRIVATE_PKCS8(cls, data, passphrase=None): """ Read the private key from PKCS8 PEM format. """ - return cls._load_PRIVATE_PKCS8(data, passphrase='') + return cls._load_PRIVATE_PKCS8(data, passphrase=b'') @classmethod def _fromString_PRIVATE_ENCRYPTED_PKCS8(cls, data, passphrase=None): @@ -2777,7 +2847,7 @@ def generate_ssh_key(options, open_method=None): message = ( u'SSH key of type "%s" and length "%d" generated as ' u'public key file "%s" and private key file "%s" %s.') % ( - key.sshType(), + key.sshType().decode('ascii'), key.size(), public_file, private_file, @@ -2791,7 +2861,7 @@ def generate_ssh_key(options, open_method=None): message = error.message except Exception as error: exit_code = 1 - message = six.text_type(error) + message = force_unicode(error) return (exit_code, message, key) diff --git a/src/chevah_keycert/ssl.py b/src/chevah_keycert/ssl.py index 91ab88b..5528089 100644 --- a/src/chevah_keycert/ssl.py +++ b/src/chevah_keycert/ssl.py @@ -424,4 +424,4 @@ def generate_and_store_csr(options, encoding='utf-8'): with open(_path(csr_name, encoding), 'wb') as store_file: store_file.write(result['csr_pem']) except Exception as error: - raise KeyCertException(str(error).decode('utf-8', errors='replace')) + raise KeyCertException(str(error)) diff --git a/src/chevah_keycert/tests/test_exceptions.py b/src/chevah_keycert/tests/test_exceptions.py index f92f379..9578e4b 100644 --- a/src/chevah_keycert/tests/test_exceptions.py +++ b/src/chevah_keycert/tests/test_exceptions.py @@ -32,4 +32,4 @@ def test_KeyCertException_str(self): error = KeyCertException(message) - self.assertEqual(message.encode('utf-8'), str(error)) + self.assertEqual(message, str(error)) diff --git a/src/chevah_keycert/tests/test_ssh.py b/src/chevah_keycert/tests/test_ssh.py index 5d9929c..cf94f42 100644 --- a/src/chevah_keycert/tests/test_ssh.py +++ b/src/chevah_keycert/tests/test_ssh.py @@ -4,18 +4,15 @@ """ Test for SSH keys management. """ -from __future__ import absolute_import, division, unicode_literals - from argparse import ArgumentParser -from StringIO import StringIO -import base64 +from io import StringIO import textwrap from chevah_compat.testing import mk, ChevahTestCase from nose.plugins.attrib import attr # Twisted test compatibility. -from chevah_keycert import ssh as keys, common, sexpy, _path +from chevah_keycert import ssh as keys, common, _path from chevah_keycert.exceptions import ( BadKeyError, KeyCertException, @@ -86,9 +83,9 @@ -----END OPENSSH PRIVATE KEY-----''') OPENSSH_RSA_PUBLIC = ( - 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKA' - 'PkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAf' - 'p1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLw==' + b'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKA' + b'PkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAf' + b'p1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLw==' ) PKCS1_RSA_PUBLIC = (b'''-----BEGIN RSA PUBLIC KEY----- @@ -646,56 +643,44 @@ def test_guessStringType(self): Imported from Twisted. """ self.assertEqual( - keys.Key._guessStringType(keydata.publicRSA_openssh), + keys.Key._guessStringType(keydata.publicRSA_openssh.encode('ascii')), 'public_openssh') self.assertEqual( - keys.Key._guessStringType(keydata.publicDSA_openssh), + keys.Key._guessStringType(keydata.publicDSA_openssh.encode('ascii')), 'public_openssh') self.assertEqual( keys.Key._guessStringType( - keydata.privateRSA_openssh), + keydata.privateRSA_openssh.encode('ascii')), 'private_openssh') self.assertEqual( keys.Key._guessStringType( - keydata.privateDSA_openssh), + keydata.privateDSA_openssh.encode('ascii')), 'private_openssh') - self.assertEqual( - keys.Key._guessStringType(keydata.publicRSA_lsh), - 'public_lsh') - self.assertEqual( - keys.Key._guessStringType(keydata.publicDSA_lsh), - 'public_lsh') - self.assertEqual( - keys.Key._guessStringType(keydata.privateRSA_lsh), - 'private_lsh') - self.assertEqual( - keys.Key._guessStringType(keydata.privateDSA_lsh), - 'private_lsh') self.assertEqual( keys.Key._guessStringType( - keydata.privateRSA_agentv3), + keydata.privateRSA_agentv3.encode('ascii')), 'agentv3') self.assertEqual( keys.Key._guessStringType( - keydata.privateDSA_agentv3), + keydata.privateDSA_agentv3.encode('ascii')), 'agentv3') self.assertEqual( keys.Key._guessStringType( - '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x01'), + b'\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x01'), 'blob') self.assertEqual( keys.Key._guessStringType( - '\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x01'), + b'\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x01'), 'blob') self.assertEqual( - keys.Key._guessStringType('not a key'), + keys.Key._guessStringType(b'not a key'), None) def test_guessStringType_unknown(self): """ None is returned when could not detect key type. """ - content = mk.ascii() + content = mk.bytes() result = Key._guessStringType(content) @@ -706,9 +691,9 @@ def test_guessStringType_X509_PEM_certificate(self): PEM certificates are recognized as public keys. """ content = ( - '-----BEGIN CERTIFICATE-----\n' - 'CONTENT\n' - '-----END CERTIFICATE-----\n' + b'-----BEGIN CERTIFICATE-----\n' + b'CONTENT\n' + b'-----END CERTIFICATE-----\n' ) result = Key._guessStringType(content) @@ -720,9 +705,9 @@ def test_guessStringType_X509_PUBLIC(self): x509 public PEM are recognized as public keys. """ content = ( - '-----BEGIN PUBLIC KEY-----\n' - 'CONTENT\n' - '-----END PUBLIC KEY-----\n' + b'-----BEGIN PUBLIC KEY-----\n' + b'CONTENT\n' + b'-----END PUBLIC KEY-----\n' ) result = Key._guessStringType(content) @@ -734,9 +719,9 @@ def test_guessStringType_PKCS8_PRIVATE(self): PKS#8 private PEM are recognized as private keys. """ content = ( - '-----BEGIN PRIVATE KEY-----\n' - 'CONTENT\n' - '-----END PRIVATE KEY-----\n' + b'-----BEGIN PRIVATE KEY-----\n' + b'CONTENT\n' + b'-----END PRIVATE KEY-----\n' ) result = Key._guessStringType(content) @@ -748,9 +733,9 @@ def test_guessStringType_PKCS8_PRIVATE_ENCRYPTED(self): PKS#8 encrypted private PEM are recognized as private keys. """ content = ( - '-----BEGIN ENCRYPTED PRIVATE KEY-----\n' - 'CONTENT\n' - '-----END ENCRYPTED PRIVATE KEY-----\n' + b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' + b'CONTENT\n' + b'-----END ENCRYPTED PRIVATE KEY-----\n' ) result = Key._guessStringType(content) @@ -777,7 +762,8 @@ def test_guessStringType_private_OpenSSH_ECDSA(self): """ Can recognize an OpenSSH ECDSA private key. """ - result = Key._guessStringType(keydata.privateECDSA_256_openssh) + result = Key._guessStringType( + keydata.privateECDSA_256_openssh.encode('ascii')) self.assertEqual('private_openssh', result) @@ -801,15 +787,18 @@ def test_guessStringType_public_OpenSSH_ECDSA(self): """ Can recognize an OpenSSH public key. """ - result = Key._guessStringType(keydata.publicECDSA_256_openssh) + result = Key._guessStringType( + keydata.publicECDSA_256_openssh.encode('ascii')) self.assertEqual('public_openssh', result) - result = Key._guessStringType(keydata.publicECDSA_384_openssh) + result = Key._guessStringType( + keydata.publicECDSA_384_openssh.encode('ascii')) self.assertEqual('public_openssh', result) - result = Key._guessStringType(keydata.publicECDSA_521_openssh) + result = Key._guessStringType( + keydata.publicECDSA_521_openssh.encode('ascii')) self.assertEqual('public_openssh', result) @@ -817,7 +806,8 @@ def test_guessStringType_private_SSHCOM(self): """ Can recognize an SSH.com private key. """ - result = Key._guessStringType(SSHCOM_RSA_PRIVATE_NO_PASSWORD) + result = Key._guessStringType( + SSHCOM_RSA_PRIVATE_NO_PASSWORD) self.assertEqual('private_sshcom', result) @@ -833,7 +823,8 @@ def test_guessStringType_putty(self): """ Can recognize a Putty private key. """ - result = Key._guessStringType(PUTTY_RSA_PRIVATE_NO_PASSWORD) + result = Key._guessStringType( + PUTTY_RSA_PRIVATE_NO_PASSWORD) self.assertEqual('private_putty', result) @@ -841,7 +832,7 @@ def test_getKeyFormat_unknown(self): """ Inform using a human readable text that format is not known. """ - result = Key.getKeyFormat('no-such-format') + result = Key.getKeyFormat(b'no-such-format') self.assertEqual('Unknown format', result) @@ -914,12 +905,12 @@ def test_fromString_errors(self): self.assertRaises( keys.BadKeyError, keys.Key.fromString, - keydata.publicRSA_lsh, passphrase='unencrypted') + keydata.publicRSA_lsh, passphrase=b'unencrypted') # trying t fo decrypt a key with the wrong passphrase self.assertRaises( keys.EncryptedKeyError, keys.Key.fromString, - keys.Key(self.rsaObj).toString('openssh', 'encrypted')) + keys.Key(self.rsaObj).toString('openssh', b'encrypted')) # key with no key data self.assertRaises( keys.BadKeyError, @@ -958,7 +949,7 @@ def test_fromString_errors(self): SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase='encrypted') +-----END RSA PRIVATE KEY-----""", passphrase=b'encrypted') # key with invalid encryption type self.assertRaises( keys.BadKeyError, keys.Key.fromString, @@ -991,7 +982,7 @@ def test_fromString_errors(self): SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase='encrypted') +-----END RSA PRIVATE KEY-----""", passphrase=b'encrypted') # key with bad IV (AES) self.assertRaises( keys.BadKeyError, keys.Key.fromString, @@ -1024,7 +1015,7 @@ def test_fromString_errors(self): SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase='encrypted') +-----END RSA PRIVATE KEY-----""", passphrase=b'encrypted') # key with bad IV (DES3) self.assertRaises( keys.BadKeyError, keys.Key.fromString, @@ -1057,7 +1048,7 @@ def test_fromString_errors(self): SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase='encrypted') +-----END RSA PRIVATE KEY-----""", passphrase=b'encrypted') def test_toStringErrors(self): """ @@ -1298,7 +1289,7 @@ def test_fromString_OpenSSH(self): self.assertEqual( keys.Key.fromString( keydata.privateRSA_openssh_encrypted, - passphrase='encrypted'), + passphrase=b'encrypted'), keys.Key.fromString(keydata.privateRSA_openssh)) self.assertEqual( @@ -1349,11 +1340,11 @@ def test_fromString_PRIVATE_OPENSSH_newer(self): IV than the older versions. These newer keys are also loaded. """ key = keys.Key.fromString(keydata.privateRSA_openssh_encrypted_aes, - passphrase='testxp') + passphrase=b'testxp') self.assertEqual(key.type(), 'RSA') key2 = keys.Key.fromString( - keydata.privateRSA_openssh_encrypted_aes + '\n', - passphrase='testxp') + keydata.privateRSA_openssh_encrypted_aes + b'\n', + passphrase=b'testxp') self.assertEqual(key, key2) def test_fromString_PRIVATE_OPENSSH_not_encrypted_with_passphrase(self): @@ -1363,7 +1354,7 @@ def test_fromString_PRIVATE_OPENSSH_not_encrypted_with_passphrase(self): """ with self.assertRaises(BadKeyError) as context: - Key.fromString(OPENSSH_RSA_PRIVATE, passphrase='pass') + Key.fromString(OPENSSH_RSA_PRIVATE, passphrase=b'pass') self.assertEqual( 'OpenSSH key not encrypted', @@ -1403,7 +1394,7 @@ def addSSHCOMKeyHeaders(self, source, headers): """ lines = source.splitlines() for key, value in headers.items(): - line = '%s: %s' % (key, value.encode('utf-8')) + line = '{}: {}'.format(key, value) header = '\\\n'.join(textwrap.wrap(line, 70)) lines.insert(1, header) return '\n'.join(lines) @@ -1429,24 +1420,24 @@ def checkParsedDSAPublic1024Data(self, sut): '56542191015877768589699407493932539140865803919573940821357868468' '55675657634384222748339103943127442354510383477300256462657784441' '71019786268219332779725063911288445634960873466719023048095246499' - '763675183656402590703132265805882271082319033570L'), + '763675183656402590703132265805882271082319033570'), data['y']) self.assertEqual(int( '14519098631088118929874535941241101897542246758347965800832728196' '81139199597265476885338795620826004398884602230901691384070382776' '92982149652731866793940314712388781003443391479314606037340161379' '86631331044475413634865132557582890274917465191550388575486379853' - '0603422003777150811982254140040687593424378397517L'), + '0603422003777150811982254140040687593424378397517'), data['p']) self.assertEqual( - int('765629040155792319453907037659138573169171493193L'), + int('765629040155792319453907037659138573169171493193'), data['q']) self.assertEqual(int( '64647318098084998690447943642968245369499209364165550549740815561' '71156388976417089337555666453157891497405105710031098879473402131' '15408225147127626829407642540707192214402604495716677723330515779' '34611656548484464881147166978432509157365635746874869548636130785' - '946819310836368885242376237240564866586977240572L'), + '946819310836368885242376237240564866586977240572'), data['g']) def checkParsedDSAPrivate1024(self, sut): @@ -1459,7 +1450,7 @@ def checkParsedDSAPrivate1024(self, sut): data = sut.data() self.checkParsedDSAPublic1024Data(sut) self.assertEqual(int( - '447059752886431435417087644871194130561824720094L'), + '447059752886431435417087644871194130561824720094'), data['x']) def checkParsedRSAPublic1024(self, sut): @@ -1482,7 +1473,7 @@ def checkParsedRSAPublic1024Data(self, sut): '33008028380639675460754206681134187533029942882729688747039044313' '67411245192523108247958392655021595783971049572916657240822239036' '02442387266290082476044614892594356080524766995335587624348179950' - '6405887692619349988915280409504938876523941259567L'), + '6405887692619349988915280409504938876523941259567'), data['n']) def checkParsedRSAPrivate1024(self, sut): @@ -1500,22 +1491,22 @@ def checkParsedRSAPrivate1024(self, sut): '64981848192265169337649163172545274951948296799964023904757013291' '17313931268194522463817291948793747715146018146051093951466872189' '64147610108577761761364098616952641696814228146724216997423652825' - '24517268536277980834876649127946895862158846465L'), + '24517268536277980834876649127946895862158846465'), data['d']) self.assertEqual(int( '10661640454627350493191065484215149934251067848734449698668476614' '18981319570111200535213963399376281314470995958266981264747210946' - '6364885923117389812635119L'), + '6364885923117389812635119'), data['p']) self.assertEqual(int( '12151328104249520956550929707892880056509323657595783040548358917' '98785549316902458371621691657702435263762556929800891556172971312' - '6473919204485168003686593L'), + '6473919204485168003686593'), data['q']) self.assertEqual(int( '66777727502990278851698381429390065987141247478987840061938912337' '88877413103516203638312270220327073357315389300205491590285175084' - '040066037688353071226161L'), + '040066037688353071226161'), data['u']) def test_fromString_PUBLIC_SSHCOM_RSA_no_headers(self): @@ -1682,7 +1673,7 @@ def test_fromString_PRIVATE_SSHCOM_unencrypted_with_passphrase(self): """ with self.assertRaises(BadKeyError) as context: - Key.fromString(SSHCOM_RSA_PRIVATE_NO_PASSWORD, passphrase='pass') + Key.fromString(SSHCOM_RSA_PRIVATE_NO_PASSWORD, passphrase=b'pass') self.assertEqual( 'SSH.com key not encrypted', @@ -1701,7 +1692,7 @@ def test_fromString_PRIVATE_SSHCOM_RSA_encrypted(self): Can load a private SSH.com key encrypted with password`. """ sut = Key.fromString( - SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase='chevah') + SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase=b'chevah') self.checkParsedRSAPrivate1024(sut) @@ -1743,7 +1734,7 @@ def test_fromString_PRIVATE_SSHCOM_RSA_with_wrong_password(self): which is encrypted, but providing a wrong password. """ with self.assertRaises(EncryptedKeyError) as context: - Key.fromString(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase='on') + Key.fromString(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase=b'on') self.assertEqual( 'Bad password or bad key format.', @@ -1758,7 +1749,7 @@ def test_fromString_PRIVATE_OPENSSH_bad_magic(self): ---- END SSH2 ENCRYPTED PRIVATE KEY ----""" self.assertBadKey( - content, 'Bad magic number for SSH.com key "124778987"') + content, 'Fail to parse key content.') def test_fromString_PRIVATE_OPENSSH_bad_key_type(self): """ @@ -1821,24 +1812,21 @@ def test_fromString_X509_PEM_EC(self): context.exception.message, ) - def test_fromString_PKCS1_PUBLIC_EC(self): + def test_fromString_PKCS1_PUBLIC_ECDSA(self): """ - It can extract RSA public key from an PKCS1 public EC PEM file. + It can extract ECDSA 192 public key from an PKCS1 public EC PEM file. """ # This is the same as the X509 RSA cert. # $ openssl x509 -in bla.cert -pubkey -noout - data = """-----BEGIN PUBLIC KEY----- + data = b"""-----BEGIN PUBLIC KEY----- MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEc6VKUjy6I6MqLmB+x4UhVeutcFCq 0Vai8iZQW9XFlPH+MC2bBpF8pmaQDwpcLvCe -----END PUBLIC KEY----- """ - with self.assertRaises(BadKeyError) as context: - Key.fromString(data) + result = Key.fromString(data) - self.assertEqual( - 'Unsupported key found in the X509 public PEM file.', - context.exception.message, - ) + self.assertEqual('EC', result.type()) + self.assertEqual(b'ecdsa-sha2-nistp192', result.sshType()) def test_fromString_X509_PEM_RSA(self): """ @@ -1874,7 +1862,7 @@ def test_fromString_X509_PEM_RSA(self): '50191438083097108908667737243399472490927083264564327600896049375' '92092816317169486450111458914839337717035721053431064458247582292' '33425907841901335798792724220900289242783534069221630733833594745' - '1002424312049140771718167143894887320401855011989L' + '1002424312049140771718167143894887320401855011989' ) self.assertEqual(n, components['n']) @@ -1922,7 +1910,7 @@ def test_fromString_PKCS1_PUBLIC_RSA(self): '50191438083097108908667737243399472490927083264564327600896049375' '92092816317169486450111458914839337717035721053431064458247582292' '33425907841901335798792724220900289242783534069221630733833594745' - '1002424312049140771718167143894887320401855011989L' + '1002424312049140771718167143894887320401855011989' ) self.assertEqual(n, components['n']) @@ -2106,7 +2094,7 @@ def test_fromString_PRIVATE_PKCS8_RSA_ENCRYPTED(self): TbW5RErmC8ifa/J4NdCv7MY= -----END ENCRYPTED PRIVATE KEY----- """ - sut = Key.fromString(data, passphrase='password') + sut = Key.fromString(data, passphrase=b'password') self.checkParsedRSAPrivate1024(sut) @@ -2209,7 +2197,7 @@ def test_toString_SSHCOM_RSA_private_encrypted(self): # Check that it looks like SSH.com private key. self.assertEqual(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, result) # Load the serialized key and see that we get the same key. - reloaded = Key.fromString(result, passphrase='chevah') + reloaded = Key.fromString(result, passphrase=b'chevah') self.assertEqual(sut, reloaded) def test_toString_SSHCOM_DSA_private(self): @@ -2240,7 +2228,7 @@ def test_fromString_PRIVATE_PUTTY_not_encrypted_with_passphrase(self): will raise BadKeyError. """ with self.assertRaises(BadKeyError) as context: - Key.fromString(PUTTY_RSA_PRIVATE_NO_PASSWORD, passphrase='pass') + Key.fromString(PUTTY_RSA_PRIVATE_NO_PASSWORD, passphrase=b'pass') self.assertEqual( 'PuTTY key not encrypted', @@ -2251,7 +2239,7 @@ def test_fromString_PRIVATE_PUTTY_RSA_with_password(self): It can read private RSA keys in Putty format which are encrypted. """ sut = Key.fromString( - PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase='chevah') + PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase=b'chevah') self.checkParsedRSAPrivate1024(sut) @@ -2284,7 +2272,7 @@ def test_fromString_PRIVATE_PUTTY_RSA_bad_password(self): """ with self.assertRaises(EncryptedKeyError) as context: Key.fromString( - PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase='bad-pass') + PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase=b'bad-pass') self.assertEqual( 'Bad password or HMAC mismatch.', context.exception.message) @@ -2309,7 +2297,7 @@ def test_fromString_PRIVATE_PUTTY_unsupported_type(self): IGNORED """ self.assertBadKey( - content, 'Unsupported key type: ssh-bad') + content, 'Unsupported key type: "ssh-bad"') def test_fromString_PRIVATE_PUTTY_unsupported_encryption(self): """ @@ -2321,7 +2309,7 @@ def test_fromString_PRIVATE_PUTTY_unsupported_encryption(self): IGNORED """ self.assertBadKey( - content, 'Unsupported encryption type: aes126-cbc') + content, 'Unsupported encryption type: "aes126-cbc"') def test_fromString_PRIVATE_PUTTY_type_mismatch(self): """ @@ -2351,7 +2339,7 @@ def test_fromString_PRIVATE_PUTTY_hmac_mismatch(self): advertise by the key file. """ content = PUTTY_RSA_PRIVATE_NO_PASSWORD[:-1] - content += 'a' + content += b'a' self.assertBadKey( content, @@ -2430,40 +2418,6 @@ def test_toString_PUTTY_public(self): reloaded = Key.fromString(result) self.assertEqual(sut, reloaded) - def test_fromString_LSH(self): - """ - Test that keys are correctly generated from LSH strings. - """ - self._testPublicPrivateFromString( - keydata.publicRSA_lsh, - keydata.privateRSA_lsh, 'RSA', keydata.RSAData) - self._testPublicPrivateFromString( - keydata.publicDSA_lsh, - keydata.privateDSA_lsh, 'DSA', keydata.DSAData) - - sexp = sexpy.pack([['public-key', ['bad-key', ['p', '2']]]]) - self.assertRaises( - keys.BadKeyError, - keys.Key.fromString, - data='{' + base64.encodestring(sexp) + '}') - - sexp = sexpy.pack([['private-key', ['bad-key', ['p', '2']]]]) - self.assertRaises( - keys.BadKeyError, keys.Key.fromString, sexp) - - def test_toString_LSH(self): - """ - Test that the Key object generates LSH keys correctly. - """ - key = keys.Key.fromString(keydata.privateRSA_openssh) - self.assertEqual(key.toString('lsh'), keydata.privateRSA_lsh) - self.assertEqual( - key.public().toString('lsh'), keydata.publicRSA_lsh) - key = keys.Key.fromString(keydata.privateDSA_openssh) - self.assertEqual(key.toString('lsh'), keydata.privateDSA_lsh) - self.assertEqual( - key.public().toString('lsh'), keydata.publicDSA_lsh) - def test_toString_AGENTV3(self): """ Test that the Key object generates Agent v3 keys correctly. @@ -2682,7 +2636,6 @@ def test_default_overwrite(self): """ generate_ssh_key_parser( self.subparser, 'key-gen', - default_key_size=1024, default_key_type='dsa', ) @@ -2716,8 +2669,6 @@ def assertPathEqual(self, expected, actual): """ Check that pats are equal. """ - if self.os_family == 'posix': - expected = expected.encode('utf-8') self.assertEqual(expected, actual) def test_generate_ssh_key_custom_values(self): @@ -2725,7 +2676,7 @@ def test_generate_ssh_key_custom_values(self): When custom values are provided, the key is generated using those values. """ - file_name = mk.ascii().decode('ascii') + file_name = mk.ascii() file_name_pub = file_name + '.pub' options = self.parseArguments([ self.sub_command_name, From d1c38ee7df5b08d7995a76c282901edcc5a4b09f Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Tue, 12 Mar 2024 09:07:41 +0000 Subject: [PATCH 18/41] Remove py3. more fixes --- CONTRIBUTING.rst | 13 +- brink.conf | 5 - brink.sh | 934 --------------------------- keycert-demo.py | 24 +- pavement.py | 4 +- pythia.sh | 10 +- src/chevah_keycert/ssh.py | 36 +- src/chevah_keycert/ssl.py | 14 +- src/chevah_keycert/tests/test_ssh.py | 310 +++++++-- src/chevah_keycert/tests/test_ssl.py | 7 +- 10 files changed, 303 insertions(+), 1054 deletions(-) delete mode 100644 brink.conf delete mode 100755 brink.sh diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 65b5477..ebd3db5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -8,11 +8,11 @@ Auto-released on PyPi using Travis-CI for each tag. Build development environment and activate it. It uses the chevah-brink script to create the virtual environment :: - ./brink.sh deps + ./pythia.sh deps Run checks executed on Travis-CI: test, linters and coverage:: - ./brink.sh test + ./pythia.sh test Default virtual environment is created in build/venv. @@ -22,12 +22,3 @@ You can manually test the command line tools:: $ ./build/venv/bin/python keycert-demo.py -Linux, OS X and Windows tests executed on private Buildbot server as Travis CI -is Linux only:: - - # See available builders - ./brink.sh remote - # Trigger a builder - ./brink.sh remote [--wait] -b keycert-win-2008 - # Trigger a builder with running the clean step - ./brink.sh remote -b keycert-win-2008 --properties=force_purge=yes diff --git a/brink.conf b/brink.conf deleted file mode 100644 index 2eb825c..0000000 --- a/brink.conf +++ /dev/null @@ -1,5 +0,0 @@ -BASE_REQUIREMENTS='pip==20.3.4chevah paver==1.2.4' -PYTHON_CONFIGURATION='default@2.7.18.d2b7dcc' -BINARY_DIST_URI='https://github.com/chevah/python-package/releases/download' -PIP_INDEX_URL='https://bin.chevah.com:20443/pypi/simple' -CHEVAH_BUILD_DIR='build-keycert' diff --git a/brink.sh b/brink.sh deleted file mode 100755 index bafcabd..0000000 --- a/brink.sh +++ /dev/null @@ -1,934 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2010-2020 Adi Roiban. -# See MIT LICENSE for details. -# -# This file has no version. Documentation is found in this comment. -# -# Helper script for bootstrapping a Python based build system on Unix/Msys. -# -# It is similar with a python-virtualenv but it will not used the local -# Python version and can be used on systems without a local Python. -# -# It will delegate the argument to the execute_venv function, -# with the exception of these commands: -# * clean - remove everything, except cache -# * purge - remove (empty) the cache -# * get_python - download Python distribution in cache -# -# It exports the following environment variables: -# * PYTHONPATH - path to the build directory -# * CHEVAH_PYTHON - name of the python versions -# * CHEVAH_OS - name of the current OS -# * CHEVAH_ARCH - CPU type of the current OS -# -# The build directory is used from CHEVAH_BUILD env, -# then read from brink.conf as CHEVAH_BUILD_DIR, -# and will use a default value if not defined there. -# -# The cache directory is read the CHEVAH_CACHE env, -# and then read from brink.conf as CHEVAH_CACHE_DIR, -# and will use a default value if not defined. -# -# You can define your own `execute_venv` function in brink.conf with the -# command used to execute Python inside the newly virtual environment. -# - -# Bash checks -set -o nounset # always check if variables exist -set -o errexit # always exit on error -set -o errtrace # trap errors in functions as well -set -o pipefail # don't ignore exit codes when piping output - -# Initialize default value. -COMMAND=${1-''} -DEBUG=${DEBUG-0} - -# Set default locale. -# We use C (alias for POSIX) for having a basic default value and -# to make sure we explicitly convert all unicode values. -export LANG='C' -export LANGUAGE='C' -export LC_ALL='C' -export LC_CTYPE='C' -export LC_COLLATE='C' -export LC_MESSAGES='C' -export PATH=$PATH:'/sbin:/usr/sbin:/usr/local/bin' - -# -# Global variables. -# -# Used to return non-scalar value from functions. -RESULT='' -WAS_PYTHON_JUST_INSTALLED=0 -DIST_FOLDER='dist' - -# Path global variables. - -# Configuration variable. -CHEVAH_BUILD_DIR="" -# Variale used at runtime. -BUILD_FOLDER="" - -# Configuration variable -CHEVAH_CACHE_DIR= -# Varible used at runtime. -CACHE_FOLDER="" - -PYTHON_BIN="" -PYTHON_LIB="" -LOCAL_PYTHON_BINARY_DIST="" - -# Put default values and create them as global variables. -OS='not-detected-yet' -ARCH='not-detected-yet' - -# Initialize default values from brink.conf -PYTHON_CONFIGURATION='NOT-YET-DEFINED' -PYTHON_VERSION='not.defined.yet' -PYTHON_PLATFORM='unknown-os-and-arch' -PYTHON_NAME='python2.7' -BINARY_DIST_URI='https://github.com/chevah/python-package/releases/download' -PIP_INDEX_URL='https://pypi.org/simple' -BASE_REQUIREMENTS='' - -# -# Check that we have a pavement.py file in the current dir. -# If not, we are out of the source's root dir and brink.sh won't work. -# -check_source_folder() { - if [ ! -e pavement.py ]; then - (>&2 echo 'No "pavement.py" file found in current folder.') - (>&2 echo 'Make sure you are running "brink.sh" from a source folder.') - exit 8 - fi -} - -# Called to trigger the entry point in the virtual environment. -# Can be overwritten in brink.conf -execute_venv() { - ${PYTHON_BIN} $PYTHON3_CHECK -c 'from paver.tasks import main; main()' "$@" -} - - -# Called to update the dependencies inside the newly created virtual -# environment. -update_venv() { - # After updating the python version, the existing pyc files might no - # longer be valid. - _clean_pyc - - set +e - ${PYTHON_BIN} -c 'from paver.tasks import main; main()' deps - exit_code=$? - set -e - if [ $exit_code -ne 0 ]; then - (>&2 echo 'Failed to run the initial "./brink.sh deps" command.') - exit 7 - fi -} - -# Load repo specific configuration. -source brink.conf - - -clean_build() { - # Shortcut for clear since otherwise it will depend on python - echo "Removing ${BUILD_FOLDER}..." - delete_folder ${BUILD_FOLDER} - echo "Removing dist..." - delete_folder ${DIST_FOLDER} - echo "Removing publish..." - delete_folder 'publish' - echo "Removing node_modules..." - delete_folder node_modules - echo "Removing web build" - delete_folder chevah/server/static/build/ - - # In some case pip hangs with a build folder in temp and - # will not continue until it is manually removed. - # On the OSX build server tmp is in $TMPDIR - if [ ! -z "${TMPDIR-}" ]; then - # check if TMPDIR is set before trying to clean it. - rm -rf ${TMPDIR}/pip* - else - rm -rf /tmp/pip* - fi -} - - -_clean_pyc() { - echo "Cleaning pyc files ..." - # AIX's find complains if there are no matching files when using +. - [ $(uname) == AIX ] && touch ./dummy_file_for_AIX.pyc - # Faster than '-exec rm {} \;' and supported in most OS'es, - # details at https://www.in-ulm.de/~mascheck/various/find/#xargs - find ./ -name '*.pyc' -exec rm {} + -} - - -# -# Removes the download/pip cache entries. Must be called before -# building/generating the distribution. -# -purge_cache() { - clean_build - - echo "Cleaning download cache ..." - rm -rf $CACHE_FOLDER/* -} - - -# -# Delete the folder as quickly as possible. -# -delete_folder() { - local target="$1" - # On Windows, we use internal command prompt for maximum speed. - # See: https://stackoverflow.com/a/6208144/539264 - if [ $OS = "win" -a -d $target ]; then - cmd //c "del /f/s/q $target > nul" - cmd //c "rmdir /s/q $target" - else - rm -rf $target - fi -} - - -# -# Wrapper for executing a command and exiting on failure. -# -execute() { - if [ $DEBUG -ne 0 ]; then - echo "Executing:" $@ - fi - - # Make sure $@ is called in quotes as otherwise it will not work. - set +e - "$@" - exit_code=$? - set -e - if [ $exit_code -ne 0 ]; then - (>&2 echo "Failed:" $@) - exit 1 - fi -} - -# -# Update global variables with current paths. -# -update_path_variables() { - resolve_python_version - - if [ "${OS}" = "win" ] ; then - PYTHON_BIN="/lib/python.exe" - PYTHON_LIB="/lib/Lib/" - else - PYTHON_BIN="/bin/python" - PYTHON_LIB="/lib/${PYTHON_NAME}/" - fi - - # Read first from env var. - set +o nounset - BUILD_FOLDER="${CHEVAH_BUILD}" - CACHE_FOLDER="${CHEVAH_CACHE}" - set -o nounset - - if [ "${BUILD_FOLDER}" = "" ] ; then - # Use value from configuration file. - BUILD_FOLDER="${CHEVAH_BUILD_DIR}" - fi - - if [ "${BUILD_FOLDER}" = "" ] ; then - # Use default value if not yet defined. - BUILD_FOLDER="build-${OS}-${ARCH}" - fi - - if [ "${CACHE_FOLDER}" = "" ] ; then - # Use default if not yet defined. - CACHE_FOLDER="${CHEVAH_CACHE_DIR}" - fi - - if [ "${CACHE_FOLDER}" = "" ] ; then - # Use default if not yet defined. - CACHE_FOLDER="cache" - fi - - PYTHON_BIN="${BUILD_FOLDER}${PYTHON_BIN}" - PYTHON_LIB="${BUILD_FOLDER}${PYTHON_LIB}" - - LOCAL_PYTHON_BINARY_DIST="$PYTHON_NAME-$OS-$ARCH" - - export PYTHONPATH=${BUILD_FOLDER} - export CHEVAH_PYTHON=${PYTHON_NAME} - export CHEVAH_OS=${OS} - export CHEVAH_ARCH=${ARCH} - export CHEVAH_CACHE=${CACHE_FOLDER} - export PIP_INDEX_URL=${PIP_INDEX_URL} - -} - -# -# Called to update the Python version env var based on the platform -# advertised by the current environment. -# -resolve_python_version() { - local version_configuration=$PYTHON_CONFIGURATION - local version_configuration_array - local candidate - local candidate_platform - local candidate_version - - PYTHON_PLATFORM="$OS-$ARCH" - - # Using ':' as a delimiter, populate a dedicated array. - IFS=: read -a version_configuration_array <<< "$version_configuration" - # Iterate through all the elements of the array to find the best candidate. - for (( i=0 ; i < ${#version_configuration_array[@]}; i++ )); do - candidate="${version_configuration_array[$i]}" - candidate_platform=$(echo "$candidate" | cut -d "@" -f 1) - candidate_version=$(echo "$candidate" | cut -d "@" -f 2) - if [ "$candidate_platform" = "default" ]; then - # On first pass, we set the default version. - PYTHON_VERSION=$candidate_version - elif [ "${PYTHON_PLATFORM%$candidate_platform*}" = "" ]; then - # If matching a specific platform, we overwrite the default version. - PYTHON_VERSION=$candidate_version - fi - done -} - - -# -# Install base package. -# -install_base_deps() { - echo "Installing base requirements: $BASE_REQUIREMENTS." - pip_install "$BASE_REQUIREMENTS" -} - - -# -# Wrapper for python `pip install` command. -# * $1 - package_name and optional version. -# -pip_install() { - echo "::group::pip install $1" - - set +e - # There is a bug in pip/setuptools when using custom build folders. - # See https://github.com/pypa/pip/issues/3564 - rm -rf ${BUILD_FOLDER}/pip-build - ${PYTHON_BIN} -m \ - pip install \ - --index-url=$PIP_INDEX_URL \ - --build=${BUILD_FOLDER}/pip-build \ - $1 - - exit_code=$? - - echo "::endgroup::" - - set -e - if [ $exit_code -ne 0 ]; then - (>&2 echo "Failed to install $1.") - exit 2 - fi -} - -# -# Check for curl and set needed download commands accordingly. -# -set_download_commands() { - set +o errexit - command -v curl > /dev/null - if [ $? -eq 0 ]; then - # Options not used because of no support in CentOS 5.11's curl: - # --retry-connrefused (since curl 7.52.0) - # --retry-all-errors (since curl 7.71.0) - # Retry 2 times, allocating 10s for the connection phase, - # at most 300s for an attempt, sleeping for 5s between retries. - CURL_RETRY_OPTS="\ - --retry 2 \ - --connect-timeout 10 \ - --max-time 300 \ - --retry-delay 5 \ - " - DOWNLOAD_CMD="curl --remote-name --location $CURL_RETRY_OPTS" - ONLINETEST_CMD="curl --fail --silent --head $CURL_RETRY_OPTS \ - --output /dev/null" - set -o errexit - return - fi - (>&2 echo "Missing curl! It is needed for downloading the Python package.") - exit 3 -} - -# -# Download and extract a binary distribution. -# -get_binary_dist() { - local dist_name=$1 - local remote_base_url=$2 - - echo "Getting $dist_name from $remote_base_url..." - - tar_gz_file=${dist_name}.tar.gz - tar_file=${dist_name}.tar - - mkdir -p ${CACHE_FOLDER} - pushd ${CACHE_FOLDER} - - # Get and extract archive. - rm -rf $dist_name - rm -f $tar_gz_file - rm -f $tar_file - execute $DOWNLOAD_CMD $remote_base_url/${tar_gz_file} - execute gunzip -f $tar_gz_file - execute tar -xf $tar_file - rm -f $tar_gz_file - rm -f $tar_file - - popd -} - -# -# Check if we have a versioned Python distribution. -# -test_version_exists() { - local remote_base_url=$1 - local target_file=python-${PYTHON_VERSION}-${OS}-${ARCH}.tar.gz - - echo "Checking $remote_base_url/${PYTHON_VERSION}/$target_file" - $ONLINETEST_CMD $remote_base_url/${PYTHON_VERSION}/$target_file - return $? -} - -# -# Download and extract in cache the python distributable. -# -get_python_dist() { - local remote_base_url=$1 - local download_mode=$2 - local python_distributable=python-${PYTHON_VERSION}-${OS}-${ARCH} - local onlinetest_errorcode - - set +o errexit - test_version_exists $remote_base_url - onlinetest_errorcode=$? - set -o errexit - - if [ $onlinetest_errorcode -eq 0 ]; then - # We have the requested python version. - get_binary_dist $python_distributable $remote_base_url/${PYTHON_VERSION} - else - (>&2 echo "Couldn't find package on remote server. Full link:") - echo "$remote_base_url/$PYTHON_VERSION/$python_distributable.tar.gz" - exit 4 - fi -} - - -# copy_python can be called in a recursive way, and this is here to prevent -# accidental infinite loops. -COPY_PYTHON_RECURSIONS=0 -# -# Copy python to build folder from binary distribution. -# -copy_python() { - local python_distributable="${CACHE_FOLDER}/${LOCAL_PYTHON_BINARY_DIST}" - local python_installed_version - - COPY_PYTHON_RECURSIONS=`expr $COPY_PYTHON_RECURSIONS + 1` - - if [ $COPY_PYTHON_RECURSIONS -gt 2 ]; then - (>&2 echo "Too many calls to copy_python: $COPY_PYTHON_RECURSIONS") - exit 5 - fi - - # Check that python dist was installed - if [ ! -s ${PYTHON_BIN} ]; then - # We don't have a Python binary, so we install it since everything - # else depends on it. - echo "::group::Get Python" - echo "Bootstrapping ${LOCAL_PYTHON_BINARY_DIST} environment" \ - "to ${BUILD_FOLDER}..." - mkdir -p ${BUILD_FOLDER} - - if [ -d ${python_distributable} ]; then - # We have a cached distributable. - # Check if is at the right version. - local cache_ver_file - cache_ver_file=${python_distributable}/lib/PYTHON_PACKAGE_VERSION - cache_version='UNVERSIONED' - if [ -f $cache_ver_file ]; then - cache_version=`cat $cache_ver_file | cut -d - -f 1` - fi - if [ "$PYTHON_VERSION" != "$cache_version" ]; then - # We have a different version in the cache. - # Just remove it and hope that the next step will download - # the right one. - rm -rf ${python_distributable} - fi - fi - - if [ ! -d ${python_distributable} ]; then - # We don't have a cached python distributable. - echo "No ${LOCAL_PYTHON_BINARY_DIST} environment." \ - "Start downloading it..." - get_python_dist "$BINARY_DIST_URI" "strict" - fi - - echo "Copying Python distribution files... " - cp -R ${python_distributable}/* ${BUILD_FOLDER} - - echo "::endgroup::" - - install_base_deps - WAS_PYTHON_JUST_INSTALLED=1 - else - # We have a Python, but we are not sure if is the right version. - local version_file=${BUILD_FOLDER}/lib/PYTHON_PACKAGE_VERSION - - # If we are upgrading the cache from an unversioned Python, - # cat fails if this file is missing, so we create it blank. - touch $version_file - python_installed_version=`cat $version_file | cut -d - -f 1` - if [ "$PYTHON_VERSION" != "$python_installed_version" ]; then - # We have a different python installed. - # Check if we have the to-be-updated version and fail if - # it does not exists. - set +o errexit - test_version_exists "$BINARY_DIST_URI" - local test_version=$? - set -o errexit - if [ $test_version -ne 0 ]; then - (>&2 echo "The build is now at $python_installed_version.") - (>&2 echo "Failed to find the required $PYTHON_VERSION.") - (>&2 echo "Check your configuration or the remote server.") - exit 6 - fi - - # Remove it and try to install it again. - echo "Updating Python from" \ - $python_installed_version to $PYTHON_VERSION - rm -rf ${BUILD_FOLDER}/* - rm -rf ${python_distributable} - copy_python - fi - fi -} - - -# -# Install dependencies after python was just installed. -# -install_dependencies(){ - if [ $WAS_PYTHON_JUST_INSTALLED -ne 1 ]; then - return - fi - - if [ "$COMMAND" == "deps" ] ; then - # Will be installed soon. - return - fi - - update_venv -} - - -# -# Check version of current OS to see if it is supported. -# If it's too old, exit with a nice informative message. -# If it's supported, return through eval the version numbers to be used for -# naming the package, for example: '71' for AIX 7.1, '114' for Solaris 11.4. -# -check_os_version() { - # First parameter should be the human-readable name for the current OS. - # For example: "Red Hat Enterprise Linux" for RHEL, "macOS" for Darwin etc. - # Second and third parameters must be strings composed of integers - # delimited with dots, representing, in order, the oldest version - # supported for the current OS and the current detected version. - # The fourth parameter is used to return through eval the relevant numbers - # for naming the Python package for the current OS, as detailed above. - local name_fancy="$1" - local version_good="$2" - local version_raw="$3" - local version_chevah="$4" - local version_constructed='' - local flag_supported='good_enough' - local version_raw_array - local version_good_array - - if [[ $version_raw =~ [^[:digit:]\.] ]]; then - (>&2 echo "OS version should only have numbers and periods, but:") - (>&2 echo " \$version_raw=$version_raw") - exit 12 - fi - - # Using '.' as a delimiter, populate the version_* arrays. - IFS=. read -a version_raw_array <<< "$version_raw" - IFS=. read -a version_good_array <<< "$version_good" - - # Iterate through all the integers from the good version to compare them - # one by one with the corresponding integers from the supported version. - for (( i=0 ; i < ${#version_good_array[@]}; i++ )); do - version_constructed="${version_constructed}${version_raw_array[$i]}" - if [ ${version_raw_array[$i]} -gt ${version_good_array[$i]} -a \ - "$flag_supported" = 'good_enough' ]; then - flag_supported='true' - elif [ ${version_raw_array[$i]} -lt ${version_good_array[$i]} -a \ - "$flag_supported" = 'good_enough' ]; then - flag_supported='false' - fi - done - - if [ "$flag_supported" = 'false' ]; then - (>&2 echo "Detected version of ${name_fancy} is: ${version_raw}.") - (>&2 echo "For versions older than ${name_fancy} ${version_good},") - if [ "$OS" = "Linux" ]; then - # For old and/or unsupported Linux distros there's a second chance! - (>&2 echo "the generic Linux runtime is used, if possible.") - check_linux_libc - else - (>&2 echo "there is currently no support.") - exit 13 - fi - fi - - # The sane way to return fancy values with a bash function is to use eval. - eval $version_chevah="'$version_constructed'" -} - -# -# For old unsupported Linux distros (some with no /etc/os-release) and for other -# unsupported Linux distros, we check if the system is based on glibc or musl. -# If so, we use a generic code path that builds everything statically, -# including OpenSSL, thus only requiring glibc or musl. -# -check_linux_libc() { - local ldd_output_file=".chevah_libc_version" - set +o errexit - - command -v ldd > /dev/null - if [ $? -ne 0 ]; then - (>&2 echo "No ldd binary found, can't check for glibc!") - exit 18 - fi - - ldd --version > $ldd_output_file 2>&1 - egrep "GNU libc|GLIBC" $ldd_output_file > /dev/null - if [ $? -eq 0 ]; then - check_glibc_version - else - egrep ^"musl libc" $ldd_output_file > /dev/null - if [ $? -eq 0 ]; then - check_musl_version - else - (>&2 echo "Unknown libc reported by ldd... Unsupported Linux.") - rm $ldd_output_file - exit 19 - fi - fi - - set -o errexit -} - -check_glibc_version(){ - local glibc_version - local glibc_version_array - local supported_glibc2_version - - # Supported minimum minor glibc 2.X versions for various arches. - # For x64, we build on CentOS 5.11 (Final) with glibc 2.5. - # For arm64, we build on Ubuntu 16.04 with glibc 2.23. - # Beware we haven't normalized arch names yet. - case "$ARCH" in - "amd64"|"x86_64"|"x64") - supported_glibc2_version=5 - ;; - "aarch64"|"arm64") - supported_glibc2_version=23 - ;; - *) - (>&2 echo "$ARCH is an unsupported arch for generic Linux!") - exit 17 - ;; - esac - - echo "No specific runtime for the current distribution / version / arch." - echo "Minimum glibc version for this arch: 2.${supported_glibc2_version}." - - # Tested with glibc 2.5/2.11.3/2.12/2.23/2.28-31 and eglibc 2.13/2.19. - glibc_version=$(head -n 1 $ldd_output_file | rev | cut -d\ -f1 | rev) - rm $ldd_output_file - - if [[ $glibc_version =~ [^[:digit:]\.] ]]; then - (>&2 echo "Glibc version should only have numbers and periods, but:") - (>&2 echo " \$glibc_version=$glibc_version") - exit 20 - fi - - IFS=. read -a glibc_version_array <<< "$glibc_version" - - if [ ${glibc_version_array[0]} -ne 2 ]; then - (>&2 echo "Only glibc 2 is supported! Detected version: $glibc_version") - exit 21 - fi - - # Decrement supported_glibc2_version if building against an older glibc. - if [ ${glibc_version_array[1]} -lt ${supported_glibc2_version} ]; then - (>&2 echo "NOT good. Detected version is older: ${glibc_version}!") - exit 22 - else - echo "All is good. Detected glibc version: ${glibc_version}." - fi - - # Supported glibc version detected, set $OS for a generic glibc Linux build. - OS="lnx" -} - -check_musl_version(){ - local musl_version - local musl_version_array - local supported_musl11_version=24 - - echo "No specific runtime for the current distribution / version / arch." - echo "Minimum musl version for this arch: 1.1.${supported_musl11_version}." - - # Tested with musl 1.1.24/1.2.2. - musl_version=$(egrep ^Version $ldd_output_file | cut -d\ -f2) - rm $ldd_output_file - - if [[ $musl_version =~ [^[:digit:]\.] ]]; then - (>&2 echo "Musl version should only have numbers and periods, but:") - (>&2 echo " \$musl_version=$musl_version") - exit 25 - fi - - IFS=. read -a musl_version_array <<< "$musl_version" - - if [ ${musl_version_array[0]} -lt 1 -o ${musl_version_array[1]} -lt 1 ];then - (>&2 echo "Only musl 1.1 or greater supported! Detected: $musl_version") - exit 26 - fi - - # Decrement supported_musl11_version if building against an older musl. - if [ ${musl_version_array[0]} -eq 1 -a ${musl_version_array[1]} -eq 1 \ - -a ${musl_version_array[2]} -lt ${supported_musl11_version} ]; then - (>&2 echo "NOT good. Detected version is older: ${musl_version}!") - exit 27 - else - echo "All is good. Detected musl version: ${musl_version}." - fi - - # Supported musl version detected, set $OS for a generic musl Linux build. - OS="lnx_musl" -} - -# -# For Linux distros with a supported libc, after checking if current version is -# supported with check_os_version(), $OS might be set to something like "lnx" -# if current version is too old, through check_linux_libc() and its subroutines. -# -set_os_if_not_generic() { - local distro_name="$1" - local distro_version="$2" - - # Check if OS starts with "lnx", to match "lnx_musl" too, just in case. - if [ "${OS#lnx}" = "$OS" ]; then - OS="${distro_name}${distro_version}" - fi -} - -# -# Detect OS and ARCH for the current system. -# In some cases we normalize or even override ARCH at the end of this function. -# -detect_os() { - OS=$(uname -s) - - case "$OS" in - MINGW*|MSYS*) - ARCH=$(uname -m) - OS="win" - ;; - Linux) - ARCH=$(uname -m) - if [ ! -f /etc/os-release ]; then - # No /etc/os-release file present, so we don't support this - # distro, but check for glibc, the generic build should work. - check_linux_libc - else - source /etc/os-release - linux_distro="$ID" - distro_fancy_name="$NAME" - # Some rolling-release distros (eg. Arch Linux) have - # no VERSION_ID here, so don't count on it unconditionally. - case "$linux_distro" in - rhel|centos|almalinux|rocky|ol) - os_version_raw="$VERSION_ID" - check_os_version "Red Hat Enterprise Linux" 8 \ - "$os_version_raw" os_version_chevah - if [ ${os_version_chevah} == "8" ]; then - set_os_if_not_generic "rhel" $os_version_chevah - else - # OpenSSL 3.0.x not supported by cryptography 3.3.x. - check_linux_libc - fi - ;; - ubuntu|ubuntu-core) - os_version_raw="$VERSION_ID" - # For versions with older OpenSSL, use generic build. - check_os_version "$distro_fancy_name" 18.04 \ - "$os_version_raw" os_version_chevah - # Only LTS versions are supported. If it doesn't end in - # 04 or first two digits are uneven, use generic build. - if [ ${os_version_chevah%%04} == ${os_version_chevah} \ - -o $(( ${os_version_chevah:0:2} % 2 )) -ne 0 ]; then - check_linux_libc - elif [ ${os_version_chevah} == "2204" ]; then - # OpenSSL 3.0.x not supported by cryptography 3.3.x. - check_linux_libc - fi - set_os_if_not_generic "ubuntu" $os_version_chevah - ;; - *) - # Supported distros with unsupported OpenSSL versions or - # distros not specifically supported: SLES, Debian, etc. - check_linux_libc - ;; - esac - fi - ;; - Darwin) - ARCH=$(uname -m) - os_version_raw=$(sw_vers -productVersion) - # Tested on 10.13, but this works on 10.12 too. Older versions need - # "-Wl,-no_weak_imports" in LDFLAGS to avoid runtime issues. More - # details at https://github.com/Homebrew/homebrew-core/issues/3727. - check_os_version "macOS" 10.12 "$os_version_raw" os_version_chevah - # Build a generic package to cover all supported versions. - OS="macos" - ;; - FreeBSD) - ARCH=$(uname -m) - os_version_raw=$(uname -r | cut -d'.' -f1) - check_os_version "FreeBSD" 12 "$os_version_raw" os_version_chevah - OS="fbsd${os_version_chevah}" - ;; - OpenBSD) - ARCH=$(uname -m) - os_version_raw=$(uname -r) - check_os_version "OpenBSD" 6.7 "$os_version_raw" os_version_chevah - OS="obsd${os_version_chevah}" - ;; - SunOS) - ARCH=$(isainfo -n) - ver_major=$(uname -r | cut -d'.' -f2) - case $ver_major in - 10) - ver_minor=$(\ - head -1 /etc/release | cut -d_ -f2 | sed s/[^0-9]*//g) - ;; - 11) - ver_minor=$(uname -v | cut -d'.' -f2) - ;; - *) - # Not sure if $ver_minor detection works on other versions. - (>&2 echo "Unsupported Solaris version: ${ver_major}.") - exit 15 - ;; - esac - os_version_raw="${ver_major}.${ver_minor}" - check_os_version "Solaris" 11.4 "$os_version_raw" os_version_chevah - OS="sol${os_version_chevah}" - ;; - AIX) - ARCH="ppc$(getconf HARDWARE_BITMODE)" - os_version_raw=$(oslevel) - check_os_version AIX 7.1 "$os_version_raw" os_version_chevah - OS="aix${os_version_chevah}" - ;; - *) - (>&2 echo "Unsupported operating system: ${OS}.") - exit 14 - ;; - esac - - # Normalize arch names. Force 32bit builds on some OS'es. - case "$ARCH" in - "i386"|"i686") - ARCH="x86" - ;; - "amd64"|"x86_64") - ARCH="x64" - ;; - "aarch64") - ARCH="arm64" - ;; - "ppc64") - # Python has not been fully tested on AIX when compiled as a 64bit - # binary, and has math rounding error problems (at least with XL C). - ARCH="ppc" - ;; - "sparcv9") - # We build 32bit binaries on SPARC too. Use "sparc64" for 64bit. - ARCH="sparc" - ;; - esac -} - -detect_os -update_path_variables -set_download_commands - -if [ "$COMMAND" = "clean" ] ; then - clean_build - exit 0 -fi - -if [ "$COMMAND" = "purge" ] ; then - purge_cache - exit 0 -fi - -# Initialize BUILD_ENV_VARS file when building Python from scratch. -if [ "$COMMAND" == "detect_os" ]; then - echo "PYTHON_VERSION=$PYTHON_NAME" > BUILD_ENV_VARS - echo "OS=$OS" >> BUILD_ENV_VARS - echo "ARCH=$ARCH" >> BUILD_ENV_VARS - exit 0 -fi - -if [ "$COMMAND" = "get_python" ] ; then - OS=$2 - ARCH=$3 - resolve_python_version - get_python_dist "$BINARY_DIST_URI" "fallback" - exit 0 -fi - -check_source_folder -copy_python -install_dependencies - -# Update brink.conf dependencies when running deps. -if [ "$COMMAND" == "deps" ] ; then - install_base_deps -fi - -case $COMMAND in - test_ci|test_py3) - PYTHON3_CHECK='-3' - ;; - *) - PYTHON3_CHECK='' - ;; -esac - -set +e -execute_venv "$@" -exit_code=$? -set -e - -exit $exit_code diff --git a/keycert-demo.py b/keycert-demo.py index 33d983e..d7b4d3e 100644 --- a/keycert-demo.py +++ b/keycert-demo.py @@ -12,23 +12,19 @@ * 3 - Error raised by demo code itself. """ -from __future__ import print_function, unicode_literals -# Fix namespaced package import. -import chevah import os import sys import traceback -chevah.__path__.insert(0, os.path.join(os.getcwd(), 'chevah')) import argparse import sys -from chevah.keycert.exceptions import KeyCertException -from chevah.keycert.ssh import ( +from chevah_keycert.exceptions import KeyCertException +from chevah_keycert.ssh import ( generate_ssh_key, generate_ssh_key_parser, Key ) -from chevah.keycert.ssl import ( +from chevah_keycert.ssl import ( generate_csr_parser, generate_and_store_csr, generate_self_signed_parser, @@ -144,11 +140,11 @@ def ssh_verify_data(options): parser = argparse.ArgumentParser(prog='PROG') subparser = parser.add_subparsers( help='Available sub-commands', dest='sub_command') +subparser.required = False sub = generate_ssh_key_parser(subparser, 'ssh-gen-key') sub.set_defaults(handler=generate_ssh_key) - sub = subparser.add_parser( 'ssh-load-key', help='Load an SSH key and show its value.', @@ -225,10 +221,18 @@ def ssh_verify_data(options): sub.set_defaults(handler=lambda o: b'\n'.join(generate_ssl_self_signed_certificate(o))) -options = parser.parse_args() +namespace = parser.parse_args() + +if namespace.sub_command is None: + # On Py2 the parser will raise this error. + # Here on py3 we raise this to keep the same behaviour. + parser.print_usage() + parser.error('too few arguments') + # We shouldn't hit this as Parser.error() should exit. + sys.exit(1) try: - result = options.handler(options) + result = namespace.handler(namespace) if result is None: print_error('EXPECTED DEMO SCRIPT ERROR') sys.exit(3) diff --git a/pavement.py b/pavement.py index 8797f3f..299f4b2 100644 --- a/pavement.py +++ b/pavement.py @@ -107,8 +107,6 @@ class LoopPlugin(Plugin): os._exit(1) - - @task @consume_args def test_interop_load_dsa(args): @@ -127,6 +125,7 @@ def test_interop_load_dsa(args): sys.exit(exit_code) + @task @consume_args def test_interop_load_rsa(args): @@ -163,6 +162,7 @@ def test_interop_load_eced(args): sys.exit(exit_code) + @task @consume_args def test_interop_generate(args): diff --git a/pythia.sh b/pythia.sh index b218367..49141e7 100755 --- a/pythia.sh +++ b/pythia.sh @@ -127,14 +127,6 @@ update_venv() { exit 7 fi - set +e - "$PYTHON_BIN" -c "from paver.tasks import main; main()" build - exit_code="$?" - set -e - if [ $exit_code -ne 0 ]; then - (>&2 echo "Failed to run the initial './pythia.sh build' command.") - exit 8 - fi } # Load repo specific configuration. @@ -706,7 +698,7 @@ check_musl_version(){ # Decrement supported_musl11_version above if building against older musl. if [ "${musl_version_array[0]}" -lt 1 ]; then musl_version_unsupported="true" - elif [ "${musl_version_array[0]}" -eq 1 ]; then + elif [ "${musl_version_array[0]}" -eq 1 ]; then if [ "${musl_version_array[1]}" -lt 1 ];then musl_version_unsupported="true" elif [ "${musl_version_array[1]}" -eq 1 ];then diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index 4bbab45..0b30977 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -61,7 +61,7 @@ from cryptography.utils import int_from_bytes, int_to_bytes from OpenSSL import crypto -from chevah_keycert import common, _path +from chevah_keycert import common from chevah_keycert.common import ( force_unicode, iterbytes, @@ -213,7 +213,7 @@ def fromFile(cls, filename, type=None, passphrase=None, encoding='utf-8'): @rtype: L{Key} @return: The loaded key. """ - with open(_path(filename, encoding), 'rb') as file: + with open(filename, 'rb') as file: return cls.fromString(file.read(), type, passphrase) @classmethod @@ -246,7 +246,7 @@ def fromString(cls, data, type=None, passphrase=None): type = cls._guessStringType(data) if type is None: raise BadKeyError( - 'Cannot guess the type for "%s"' % force_unicode(data[:80])) + 'Cannot guess the type for "{}"'.format(repr(data[:80]))) try: method = getattr(cls, '_fromString_%s' % type.upper(), None) @@ -2448,7 +2448,6 @@ def _toString_PUTTY_private(self, extra): lines.append('Private-MAC: %s' % private_mac) return '\r\n'.join(lines).encode('utf-8') - @classmethod def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): """ @@ -2640,6 +2639,27 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): privateValue=privateValue, ) + def _toString_PUTTY_V3(self, comment=None, passphrase=None): + """ + Return a public or private Putty v3 string. + + See _fromString_PRIVATE_PUTTY for the private format. + See _fromString_PUBLIC_SSHCOM for the public format. + + If extra is present, it represents a comment for a + public key, or a passphrase for a private key. + + @param extra: Comment for a public key or passphrase for a private key. + @type extra: C{bytes} + + @rtype: C{bytes} + """ + if self.isPublic(): + # Putty uses SSH.com as public format. + return self._toString_SSHCOM_public(comment) + else: + return self._toString_PUTTY_V3_private(passphrase) + @classmethod def _fromString_PUBLIC_X509_CERTIFICATE(cls, data): @@ -2817,7 +2837,7 @@ def generate_ssh_key(options, open_method=None): key = Key.generate(key_type=key_type, key_size=key_size) - with open_method(_path(private_file), 'wb') as file_handler: + with open_method(private_file, 'wb') as file_handler: _store_SSHKey( key, private_file=file_handler, @@ -2836,7 +2856,7 @@ def generate_ssh_key(options, open_method=None): else: message_comment = u'without a comment' - with open_method(_path(public_file), 'wb') as file_handler: + with open_method(public_file, 'wb') as file_handler: _store_SSHKey( key, public_file=file_handler, @@ -2898,13 +2918,13 @@ def _skip_key_generation(options, private_file, public_file): Raise KeyCertException if file exists. """ - if os.path.exists(_path(private_file)): + if os.path.exists(private_file): if options.key_skip: return True else: raise KeyCertException( u'Private key already exists. %s' % private_file) - if os.path.exists(_path(public_file)): + if os.path.exists(public_file): raise KeyCertException(u'Public key already exists. %s' % public_file) return False diff --git a/src/chevah_keycert/ssl.py b/src/chevah_keycert/ssl.py index 5528089..bf762ac 100644 --- a/src/chevah_keycert/ssl.py +++ b/src/chevah_keycert/ssl.py @@ -12,7 +12,7 @@ from OpenSSL import crypto -from chevah_keycert import _path, native_string +from chevah_keycert import native_string from chevah_keycert.exceptions import KeyCertException _DEFAULT_SSL_KEY_CYPHER = 'aes-256-cbc' @@ -336,8 +336,8 @@ def _generate_csr(options): key_pem = None private_key = options.key if private_key: - if os.path.exists(_path(private_key)): - with open(_path(private_key), 'rb') as stream: + if os.path.exists(private_key): + with open(private_key, 'rb') as stream: private_key = stream.read() key_pem = private_key @@ -403,7 +403,7 @@ def generate_ssl_self_signed_certificate(options): return (certificate_pem.decode('utf-8'), key_pem.decode('utf-8')) -def generate_and_store_csr(options, encoding='utf-8'): +def generate_and_store_csr(options): """ Generate a key/csr and try to store it on disk. @@ -412,16 +412,16 @@ def generate_and_store_csr(options, encoding='utf-8'): name, _ = os.path.splitext(options.key_file) csr_name = u'%s.csr' % name - if os.path.exists(_path(options.key_file, encoding)): + if os.path.exists(options.key_file): raise KeyCertException('Key file already exists.') result = generate_csr(options) try: - with open(_path(options.key_file, encoding), 'wb') as store_file: + with open(options.key_file, 'wb') as store_file: store_file.write(result['key_pem']) - with open(_path(csr_name, encoding), 'wb') as store_file: + with open(csr_name, 'wb') as store_file: store_file.write(result['csr_pem']) except Exception as error: raise KeyCertException(str(error)) diff --git a/src/chevah_keycert/tests/test_ssh.py b/src/chevah_keycert/tests/test_ssh.py index cf94f42..d0454db 100644 --- a/src/chevah_keycert/tests/test_ssh.py +++ b/src/chevah_keycert/tests/test_ssh.py @@ -5,7 +5,7 @@ Test for SSH keys management. """ from argparse import ArgumentParser -from io import StringIO +from io import BytesIO import textwrap from chevah_compat.testing import mk, ChevahTestCase @@ -273,6 +273,112 @@ J8IuFywygVI4PbRs98v9Dg==\r Private-MAC: 3ffe2587759ff8f50c6acdcad44f62a67e88ef2b""" +# Same as PUTTY_RSA_PRIVATE_NO_PASSWORD but in v3 format +# puttygen test-v2.ppk -o test-v3.ppk --reencrypt +PUTTY_V3_RSA_PRIVATE_V3_NO_PASSWORD = b"""PuTTY-User-Key-File-3: ssh-rsa\r +Encryption: none\r +Comment: imported-openssh-key\r +Public-Lines: 4\r +AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK\r +APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk\r ++X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj\r +RToF6/jpLw==\r +Private-Lines: 8\r +AAAAgAgeXEA78ZgXYGFabsuNw3bmm05ke9RxWjRZfpxOb8BcVKl9KhTkKRtBNgr+\r +es3rD809SVgYqn30oq+Ikox/5Z7JGZzPSdcX6Z6CeR083Bh2gWFRRBF0unzrMlk9\r +eaGOym+q0QU51ldCJ7P9OR4ad/K/0UfzuKaAftzcECQ5f+oBAAAAQQDoAnRdC+5S\r +yjrZ1JQUWUkapiYHIhFq6kWtGm3kWJn0IxCBtFhGvqIWJwZIAjf6tTKMUk6bjG9p\r +7JpetXUsTiDBAAAAQQDLkQNTYXjZebq29DMzxsCCrt3b/HaIdG46QNRvVsrdjAHJ\r +OKGX0Euq91GFHGmXbURypakH9HMAVsZr7rQb+JXvAAAAQH+ARfXlS8UUPQODJvSJ\r +LeJRfIoup2uJ8XbRMz/Kdiz/bS6h2FKGzWp8QfuzLIuH94GMrinThp1h6g9lOB3C\r +3TE=\r +Private-MAC: 393d670fe58e8ce89e66f55e22523ec39bfcf8fa908e583b7c53823e142e52d3 +""" + +# Same as v3 +# With password "chevah" +# puttygen test-v2.ppk -o test-v3.ppk -O private -P --ppk-param kdf=argon2id +PUTTY_V3_RSA_PRIVATE_WITH_PASSWORD = b"""PuTTY-User-Key-File-3: ssh-rsa\r +Encryption: aes256-cbc\r +Comment: imported-openssh-key\r +Public-Lines: 4\r +AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK\r +APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk\r ++X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj\r +RToF6/jpLw==\r +Key-Derivation: Argon2id\r +Argon2-Memory: 8192\r +Argon2-Passes: 34\r +Argon2-Parallelism: 1\r +Argon2-Salt: 426aaa1672c0dbf7154b7610f5d45e23\r +Private-Lines: 8\r +AuVHQmNuEXDSMHEigSf7KDUB01HNPzINHhzeBlnRkKcU/sQJxortxwX84L/o/COp\r +GyDfUqZEObBgU7gIFezXLXkay/Qxw1AWuFgswqzXgKGPZ8+6S0D/ZhAcJlOrKGDf\r +yYtqs/8fswauzKahZx8dxFP3sN/pzCzSasLV6bJ/33SN7Q4czjVYoTuCBYb1qm6k\r +0bg+h+CHoIePFGXz3jhLbjSnf405M6MgznD3WMZPLbY+rTtnvroVuLqC0Mu+cdpR\r +30tkdJaHwstYDsB8yKlCYiIXWtRIKYUkBEZFKpWo7woEe2IqSaiiJ9hTghJuQ5ZQ\r +53OjORmnoevIn5eitewY0wwB+FsM+vWJ3PWvSu+DIEAEE1jbzjy2hSUVMvj/2f05\r +Nakw2IT6CC7TCkI8nAzzI48O10DDJ8BFVm3GOqFv1Pzmgh/VePiPRXussZhFpFnm\r +cDe7srHEqLjxnnOZw7KKqg==\r +Private-MAC: 2f56110ed1745e3153a70deb4bb57314b1b14dcf3ebc34d13f1ee47c9222cfc0\r +""" + +# Same as v3 but with argon21 +# With password "chevah" +# puttygen test-v2.ppk -o test-v3.ppk -O private -P --ppk-param kdf=argon2i +PUTTY_V3_RSA_PRIVATE_WITH_PASSWORD_ARGON2I = b"""PuTTY-User-Key-File-3: ssh-rsa\r +Encryption: aes256-cbc\r +Comment: imported-openssh-key\r +Public-Lines: 4\r +AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK\r +APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk\r ++X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj\r +RToF6/jpLw==\r +Key-Derivation: Argon2i\r +Argon2-Memory: 8192\r +Argon2-Passes: 13\r +Argon2-Parallelism: 1\r +Argon2-Salt: f2efdbaac9f11a994de82a1b9e418874\r +Private-Lines: 8\r +o9870SrOsNN5Nm1WI/TKqpyKgSRyGX0JDGW2a3uO9YCeqxb1AL8vEk2yRRlxkzy4\r +Hvh7xmI2KNnAeuZvkJJjUYHNNTp+KYbV17paU6Cf5GAwOaKJdrwX31zxrPqbYzmi\r +KmpNZLAGhoIEbFY6W5y7E1NnoM4zyZ1vsg6Z1eapTCtlgeOQr7rNkNlpoKjaopiA\r +s7G5h+A+FAeqfh6aaiZf3dswbw5mavcnWTPTnZNbWDziR/blRk/aOanCj4HVoWx+\r +o6dOZnwl9PxY07R9RUk2DNJhr9XiibTTb5ymRk9SVPUjbrV8uC3DLiuejDZ+drgG\r +qotb7VbS0s7+Dbe5ctRyOkr1yx1UQaEMV3OTrqlE6CGuRdfjyQJWidGYFHZTmgUR\r +wm3sW+T90MGCnFEukHxEmXWZJmL3pYO8+dYYRi+RGB9zuO7KskbyLgqm4m023gwT\r +5EpqsPFNUv3iL3kU1HtzVQ==\r +Private-MAC: 90a441b935e29c1e7fd3efb79a330554e0e99d2c15948efded9916afd8ba8626\r +""" + +# Same as v3 but with argon21 +# With password "chevah" +# puttygen test-v2.ppk -o test-v3.ppk -O private -P --ppk-param kdf=argon2d +PUTTY_V3_RSA_PRIVATE_WITH_PASSWORD_ARGON2D = b"""PuTTY-User-Key-File-3: ssh-rsa\r +Encryption: aes256-cbc\r +Comment: imported-openssh-key\r +Public-Lines: 4\r +AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK\r +APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk\r ++X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj\r +RToF6/jpLw==\r +Key-Derivation: Argon2d\r +Argon2-Memory: 8192\r +Argon2-Passes: 13\r +Argon2-Parallelism: 1\r +Argon2-Salt: f319597cb7bc378717a32b7809b466ef\r +Private-Lines: 8\r +mDtZZwVUvEKw6vgott8cT4rLzGfR6kOOdfhfjXR3KtRjbE32YthmuxtUF040kaLE\r +DubnQ5x+/LnbpycXWeSYsOgYOODC22s+XTqqsgqXHSIqjWZnVswOGfyY2x8VdnSS\r +i6G42BwSxZHU5bNBZkVn4t63USS8cyGIOzhhNebob4jR864JoQy7UpcJCt7dxzRy\r +iOAvFY6M11WJUN1g8lHQaJ0HuDjQEPD2CPw87bMGDayo4I5RvYa617pkKNGZ1AOJ\r +nzYrjkpv1kHBL0AKfA2ZTQKs2g4PGBa2YBK+UolYKACFaiRLX/du+6cW6fDQ7u92\r +kSGZED0rjP+Z6s9376I7E05AnPSFWqlE3XLtxaL1KqkbZ+ffOrRqIpPnYsmeYeIl\r +hYHH406V+VdlGZzNkqfrTH0m7X8Ra39Y48/nhPHmaJLhnVU15RVkoAdazoAEN779\r +WwsJE3IFnF0qDKB6p5wPyw==\r +Private-MAC: 7dd2f4638f52515edf5282d290179b63079f64b2c9bed65cdc1a99c60d710807\r +""" + # This is the same key as OPENSSH_DSA_PRIVATE PUTTY_DSA_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-dss\r Encryption: none\r @@ -292,6 +398,27 @@ AAAAFE5O2kb+uaE3nWLAMovNC/KYWATe\r Private-MAC: 1b98c142780beaa5555ad5c23a0469e36f24b6f9""" +# Same as PUTTY_DSA_PRIVATE_NO_PASSWORD but in v3 format +# puttygen dsa-v2.ppk -o dsa-v3.ppk --reencrypt +PUTTY_V3_DSA_PRIVATE_NO_PASSWORD_V3 = b"""PuTTY-User-Key-File-3: ssh-dss\r +Encryption: none\r +Comment: imported-openssh-key\r +Public-Lines: 10\r +AAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/BIQtXSfkQuypVkU60GkeV4Q6\r +K2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU7YwoFF323b1+vbpW\r +8JlA7FHh/OQ+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCSPt+bNEfnGANN\r +AAAAFQCGG/5Y0lHJaOk4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaDF7JPfg\r +gDDfs0aYss/9vKLzp0px3PmG2o+I5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804\r +IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLmGdI2Sj9aynRWdberk4r+0+zWlHL\r +7epZTDCuDmLOFiQF/AAAAIB/6sL9MO4ZwtFzwbOKNOoZxfORwNbzzHf+IpzyBTxx\r +QJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3pZ7SnoRJ7v\r +ATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk20\r +4g==\r +Private-Lines: 1\r +AAAAFE5O2kb+uaE3nWLAMovNC/KYWATe\r +Private-MAC: 9e617cd5bf19f880d3a6a1a0551b699f732e27ec78af65b764de465d82600e18\r +""" + PUTTY_ED25519_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-ed25519\r Encryption: none\r Comment: ed25519-key-20210106\r @@ -303,6 +430,18 @@ Private-MAC: ead2308fe2f6be87941f17e9d61ede28da2cde8a\r """ +# Same as PUTTY_ED25519_PRIVATE_NO_PASSWORD but in v3 format. +PUTTY_V3_ED25519_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-3: ssh-ed25519\r +Encryption: none\r +Comment: ed25519-key-20210106\r +Public-Lines: 2\r +AAAAC3NzaC1lZDI1NTE5AAAAIEjwKguKHPrqp3UEqSP7XTmOhBavcTxkHwnzQveQ\r +2MGG\r +Private-Lines: 1\r +AAAAINWl263e/oNph4x7jM94kE7BaSNcXD7G6bbWatylw61A\r +Private-MAC: b3617706ea98c2476aa733296636d7845a7d62e871a5dd0057d11d74f218d0e1\r +""" + # Password is: chevah PUTTY_ED25519_PRIVATE_WITH_PASSWORD = b"""PuTTY-User-Key-File-2: ssh-ed25519\r Encryption: aes256-cbc\r @@ -328,6 +467,18 @@ Private-MAC: a84b17c5dead6fed8f474406929312d45c096dfc\r """) +PUTTY_V3_ECDSA_SHA2_NISTP256_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-3: ecdsa-sha2-nistp256\r +Encryption: none\r +Comment: ecdsa-key-20210106\r +Public-Lines: 3\r +AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPA3+gjOpajd\r +9iRVm72ArvQfjVW+3bz9IMrPNMIANSmwTj+0NuFgXZGLaxT8BKslZLZvJX+XuUr/\r +Yvgn32oS7Iw=\r +Private-Lines: 1\r +AAAAIDe7fQUAaorrEkedXTSmXrCY4vabtFV7e4Z8xBSvty8Q\r +Private-MAC: 6488b1e2221448122e8884df9622350510e7cd266d174b307104a15e5669afb5\r +""" + PUTTY_ECDSA_SHA2_NISTP384_PRIVATE_NO_PASSWORD = ( b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp384\r Encryption: none\r @@ -342,6 +493,19 @@ Private-MAC: 1464df777d20427e2b99adb148ed4b8a1a839409\r """) +PUTTY_V3_ECDSA_SHA2_NISTP384_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-3: ecdsa-sha2-nistp384\r +Encryption: none\r +Comment: ecdsa-key-20210106\r +Public-Lines: 3\r +AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEjK280ap/RD\r +R916Q00OI1LIHyRG1fcH6twBjmynTgl0uGlcb8bnbpGO1JOgbhBqqzVQHVckHzqT\r +fUif6rRRQuiUJEenXRmgjQ0uEcj21Rdomz7TJPz1k8tHmOZCHgJx6g==\r +Private-Lines: 2\r +AAAAMQCNcgWtnEeeTqFN383FBJdM90keHkJwproyLPgWQLlbZe+r8py0Pl7mUHvj\r +SGmXUVc=\r +Private-MAC: 73cdd8880d60561a21bc23017b191471354158e2f343e1b48e8dbe0e46b74067\r +""" + PUTTY_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD = ( b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp521\r Encryption: none\r @@ -357,6 +521,20 @@ Private-MAC: e828d7207e0e73453005d606216ca36c64d1e304\r """) +PUTTY_V3_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-3: ecdsa-sha2-nistp521\r +Encryption: none\r +Comment: ecdsa-key-20210106\r +Public-Lines: 4\r +AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAGtj24Kr7OY\r +21mtlHTFuH0NmrhI1mco0nND4FvDbNTTU/87t1ZDqbPEnRqmYBM6/dGPyOK82PH8\r +NmCrCjj0rmckNgC3+Jg/+ok1bJG7/WeTOObnIdDBJklxksIjMF6LG6hVngIibxgF\r +V3iBGD5eWUr40AK+6+wN7uKsaFHMBCg8lde5Mg==\r +Private-Lines: 2\r +AAAAQgE64XtEewBVYUz+sfojvHmsiwdT+2BBBw1IAcKuozuhsz8EkOEOBJGZqCBP\r +B9pAqlHsVHQJF/uVpFbJFUnjEokJ4w==\r +Private-MAC: 3b713999a444c896d6ea7605aba44684693249d6de9b1a0775b60a9bf8e0f19a\r +""" + class DummyOpenContext(object): """ @@ -370,7 +548,7 @@ def __init__(self): self.last_stream = None def __call__(self, path, mode): - self.last_stream = StringIO() + self.last_stream = BytesIO() self.calls.append( {'path': path, 'mode': mode, 'stream': self.last_stream}) return self @@ -628,11 +806,11 @@ def test_generate_failed(self): A ServerError is raised when it fails to generate the key. """ with self.assertRaises(KeyCertException) as context: - Key.generate(key_type='dSa', key_size=2048) + Key.generate(key_type='dSa', key_size=512) self.assertEqual( - u'Wrong key size "2048". Number of bits in p must be a multiple ' - 'of 64 between 512 and 1024, not 2048 bits.', + 'Wrong key size "512". ' + 'Key size must be 1024, 2048, 3072, or 4096 bits.', context.exception.message) def test_guessStringType(self): @@ -880,10 +1058,13 @@ def test_fromString_type_unkwown(self): An exceptions is raised when reading a key for which type could not be detected. Exception only contains the beginning of the content. """ - content = mk.ascii() * 100 + content = 'some-value-' * 100 self.assertBadKey( - content, 'Cannot guess the type for "%s"' % content[:80]) + content, + 'Cannot guess the type for "b\'some-value-' + 'some-value-some-value-some-value-some-value-some-' + 'value-some-value-som\'"') def test_fromString_struct_errors(self): """ @@ -1065,8 +1246,8 @@ def test_fromString_BLOB_blob_type_non_ascii(self): badBlob = common.NS('ssh-\xbd\xbd\xbd') self.assertBadKey( badBlob, - 'Cannot guess the type for "' - r'\x00\x00\x00' + '\n' + r'ssh-\xc2\xbd\xc2\xbd\xc2\xbd"' + 'Cannot guess the type for ' + '"b\'\\x00\\x00\\x00\\nssh-\\xc2\\xbd\\xc2\\xbd\\xc2\\xbd\'"' ) def test_fromString_PRIVATE_BLOB(self): @@ -1354,7 +1535,7 @@ def test_fromString_PRIVATE_OPENSSH_not_encrypted_with_passphrase(self): """ with self.assertRaises(BadKeyError) as context: - Key.fromString(OPENSSH_RSA_PRIVATE, passphrase=b'pass') + Key.fromString(OPENSSH_RSA_PRIVATE, passphrase='pass') self.assertEqual( 'OpenSSH key not encrypted', @@ -1497,16 +1678,16 @@ def checkParsedRSAPrivate1024(self, sut): '10661640454627350493191065484215149934251067848734449698668476614' '18981319570111200535213963399376281314470995958266981264747210946' '6364885923117389812635119'), - data['p']) + data['q']) self.assertEqual(int( '12151328104249520956550929707892880056509323657595783040548358917' '98785549316902458371621691657702435263762556929800891556172971312' '6473919204485168003686593'), - data['q']) + data['p']) self.assertEqual(int( - '66777727502990278851698381429390065987141247478987840061938912337' - '88877413103516203638312270220327073357315389300205491590285175084' - '040066037688353071226161'), + '48025268260110814473325498559726067155427614012608550802573547885' + '48894562354231797601376827466469492368471033644629931755771678685' + '474342157953188378164913'), data['u']) def test_fromString_PUBLIC_SSHCOM_RSA_no_headers(self): @@ -1656,7 +1837,7 @@ def test_fromString_PRIVATE_OPENSSH_short(self): self.assertBadKey( content, 'Failed to decode key (Bad Passphrase?): ' - 'Short octet stream on tag decoding') + 'EndOfStreamError()') def test_fromString_PRIVATE_OPENSSH_bad_encoding(self): """ @@ -1786,13 +1967,13 @@ def test_fromString_X509_PEM_invalid_format(self): Key.fromString(data) self.assertStartsWith( - "Failed to load certificate. [('asn1 encoding routines'", + "Failed to load certificate. \"[('asn1 encoding routines'", context.exception.message, ) def test_fromString_X509_PEM_EC(self): """ - EC public key from an X509 PEM certificate are not supported. + EC public key from an X509 PEM certificate are supported. """ data = """-----BEGIN CERTIFICATE----- MIIBNDCB66ADAgECAgEBMAoGCCqGSM49BAMCMDQxCzAJBgNVBAYTAkdCMQ8wDQYD @@ -1804,13 +1985,11 @@ def test_fromString_X509_PEM_EC(self): Gt7MBDMYYr8yfcZS94pZEUfhebR3CYAZ -----END CERTIFICATE----- """ - with self.assertRaises(BadKeyError) as context: - Key.fromString(data) + result = Key.fromString(data) - self.assertEqual( - 'Unsupported key found in the certificate.', - context.exception.message, - ) + self.assertEqual('EC', result.type()) + self.assertEqual(b'ecdsa-sha2-nistp192', result.sshType()) + self.assertEqual(192, result.size()) def test_fromString_PKCS1_PUBLIC_ECDSA(self): """ @@ -1879,7 +2058,7 @@ def test_fromString_PKCS1_PUBLIC_PEM_invalid_format(self): Key.fromString(data) self.assertStartsWith( - "Failed to load PKCS#1 public key. [('asn1 encoding routines'", + "Failed to load PKCS#1 public key. \"[('DECODER routines'", context.exception.message, ) @@ -2038,7 +2217,7 @@ def test_fromString_PRIVATE_PKCS8_invalid_format(self): Key.fromString(data) self.assertStartsWith( - "Failed to load PKCS#8 PEM. [('asn1 encoding routines'", + "Failed to load PKCS#8 PEM. \"[('DECODER routines'", context.exception.message, ) @@ -2154,7 +2333,7 @@ def test_fromString_PRIVATE_PKCS8_DSA(self): def test_fromString_PRIVATE_PKCS8_EC(self): """ - It fails to extract the EC key from an PKCS8 private EC PEM file, + It cat extract the EC key from an PKCS8 private EC PEM file, """ # openssl ecparam -name prime256v1 -genkey -noout -out private.ec.key # openssl pkcs8 -topk8 -in private.ec.key -nocrypt @@ -2164,13 +2343,10 @@ def test_fromString_PRIVATE_PKCS8_EC(self): 7C/wsAsbx6monIz1qc1jje9RgggJL5pZ5HfbDInclQfV5T9rz6kWFEZS -----END PRIVATE KEY----- """ - with self.assertRaises(BadKeyError) as context: - Key.fromString(data) + result = Key.fromString(data) - self.assertEqual( - 'Unsupported key found in the PKCS#8 private PEM file.', - context.exception.message, - ) + self.assertEqual('EC', result.type()) + self.assertEqual(b'ecdsa-sha2-nistp256', result.sshType()) def test_toString_SSHCOM_RSA_private_without_encryption(self): """ @@ -2181,7 +2357,11 @@ def test_toString_SSHCOM_RSA_private_without_encryption(self): result = sut.toString(type='sshcom') # Check that it looks like SSH.com private key. - self.assertEqual(SSHCOM_RSA_PRIVATE_NO_PASSWORD, result) + self.assertStartsWith( + b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\n' + b'P2/56wAAAi4AAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0', + result) + # Load the serialized key and see that we get the same key. reloaded = Key.fromString(result) self.assertEqual(sut, reloaded) @@ -2195,7 +2375,11 @@ def test_toString_SSHCOM_RSA_private_encrypted(self): result = sut.toString(type='sshcom', extra='chevah') # Check that it looks like SSH.com private key. - self.assertEqual(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, result) + self.assertStartsWith( + b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\n' + b'P2/56wAAAjMAAAA3aWYtbW9kbntzaWdue3', + result) + # Load the serialized key and see that we get the same key. reloaded = Key.fromString(result, passphrase=b'chevah') self.assertEqual(sut, reloaded) @@ -2558,23 +2742,13 @@ def test_repr(self): """ Test the pretty representation of Key. """ - self.assertEqual( - repr(keys.Key(self.rsaObj)), - b"""\ -""") - + result = repr(keys.Key(self.rsaObj)) + self.assertContains( + ' Date: Tue, 12 Mar 2024 17:22:18 +0000 Subject: [PATCH 19/41] Fix more ssh tests. --- keycert-demo.py | 15 +- pavement.py | 7 +- src/chevah_keycert/__init__.py | 4 +- src/chevah_keycert/ssh.py | 227 +++++++++++++-- src/chevah_keycert/ssl.py | 3 +- src/chevah_keycert/tests/test_ssh.py | 396 +++++++++++++-------------- src/chevah_keycert/tests/test_ssl.py | 3 +- 7 files changed, 414 insertions(+), 241 deletions(-) diff --git a/keycert-demo.py b/keycert-demo.py index d7b4d3e..e1d22bb 100644 --- a/keycert-demo.py +++ b/keycert-demo.py @@ -59,6 +59,7 @@ def ssh_load_key(options, open_method=None): path = options.file output_format = options.type password = options.password + key_password = options.key_password if not path: return print_error('No path specified') @@ -72,14 +73,14 @@ def ssh_load_key(options, open_method=None): key = Key.fromString(key_content, passphrase=password) if key.isPublic(): - to_string = key.toString(output_format) + to_string = key.toString(output_format, extra=key_password) else: - to_string = key.toString(output_format) + to_string = key.toString(output_format, extra=key_password) result = '%r\nKey type %s\n\n%s' % ( key, Key.getKeyFormat(key_content), - to_string, + to_string.decode('utf-8'), ) return result @@ -164,7 +165,13 @@ def ssh_verify_data(options): '--password', metavar='PASSWORD', default=None, - help='Option password or commented used when re-encoding the loaded key.' + help='Option password used when loading the key.' + ) +sub.add_argument( + '--key-password', + metavar='PASSWORD', + default=None, + help='Option password used when writing key.' ) sub.set_defaults(handler=ssh_load_key) diff --git a/pavement.py b/pavement.py index 299f4b2..f60a4b6 100644 --- a/pavement.py +++ b/pavement.py @@ -200,7 +200,12 @@ def lint(): except SystemExit as pyflakes_exit: pass - sys.argv.extend(['--ignore=E741', '--hang-closing']) + sys.argv.extend([ + '--ignore=E741', + '--ignore=E741', + '--hang-closing', + '--max-line-length=80', + ]) pycodestyle_exit = pycodestyle_main() sys.exit(pyflakes_exit.code or pycodestyle_exit) diff --git a/src/chevah_keycert/__init__.py b/src/chevah_keycert/__init__.py index 25d61c3..82a5cd9 100644 --- a/src/chevah_keycert/__init__.py +++ b/src/chevah_keycert/__init__.py @@ -7,6 +7,8 @@ import base64 import inspect +import cryptography.utils + def _path(path, encoding='utf-8'): if sys.platform.startswith('win'): @@ -32,7 +34,7 @@ def native_string(string): for member in ['Callable', 'Iterable', 'Mapping', 'Sequence']: if not hasattr(collections, member): setattr(collections, member, getattr(collections.abc, member)) -import cryptography.utils + if not hasattr(cryptography.utils, 'int_from_bytes'): cryptography.utils.int_from_bytes = int.from_bytes diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index 0b30977..9be985e 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -20,6 +20,7 @@ import six import bcrypt +from argon2 import low_level from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization @@ -87,21 +88,21 @@ b'ecdsa-sha2-nistp384': ec.SECP384R1(), b'ecdsa-sha2-nistp521': ec.SECP521R1(), b'ecdsa-sha2-nistp192': ec.SECP192R1(), -} + } _secToNist = { 'secp256r1' : b'nistp256', 'secp384r1' : b'nistp384', 'secp521r1' : b'nistp521', 'secp192r1' : b'nistp192', -} + } _ecSizeTable = { 256: ec.SECP256R1(), 384: ec.SECP384R1(), 521: ec.SECP521R1(), -} + } class BadFingerPrintFormat(Exception): """ @@ -1900,8 +1901,8 @@ def _fromString_PRIVATE_SSHCOM(cls, data, passphrase): * mpint d * mpint n * mpint u - * mpint p * mpint q + * mpint p The payload for a DSA key: * uint32 0 @@ -1962,7 +1963,7 @@ def _fromString_PRIVATE_SSHCOM(cls, data, passphrase): try: payload, _ = common.getNS(key_data) if key_type == 'rsa': - e, d, n, u, p, q, rest = cls._unpackMPSSHCOM(payload, 6) + e, d, n, u, q, p, rest = cls._unpackMPSSHCOM(payload, 6) return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) if key_type == 'dsa': @@ -2070,8 +2071,8 @@ def _toString_SSHCOM_private(self, extra): self._packMPSSHCOM(data['d']) + self._packMPSSHCOM(data['n']) + self._packMPSSHCOM(data['u']) + - self._packMPSSHCOM(data['p']) + - self._packMPSSHCOM(data['q']) + self._packMPSSHCOM(data['q']) + + self._packMPSSHCOM(data['p']) ) elif type == 'DSA': type_signature = 'dl-modp{sign{dsa-nist-sha1},dh{plain}}' @@ -2152,8 +2153,8 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): * mpint n Private part RSA: * mpint d - * mpint q * mpint p + * mpint q * mpint u Pulic part DSA: @@ -2277,7 +2278,7 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): if key_type == 'ssh-rsa': e, n, _ = common.getMP(public_payload, count=2) - d, q, p, u, _ = common.getMP(private_blob, count=4) + d, p, q, u, _ = common.getMP(private_blob, count=4) return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) if key_type == 'ssh-dss': @@ -2367,8 +2368,8 @@ def _toString_PUTTY_private(self, extra): ) private_blob = ( common.MP(data['d']) + - common.MP(data['q']) + common.MP(data['p']) + + common.MP(data['q']) + common.MP(data['u']) ) elif key_type == b'ssh-dss': @@ -2486,8 +2487,8 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): * mpint n Private part RSA: * mpint d - * mpint q * mpint p + * mpint q * mpint u Pulic part DSA: @@ -2537,6 +2538,7 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): 'Unsupported key type: "%s"' % force_unicode(key_type[:30])) encryption_type = lines[1][11:].strip().lower() + private_offset = 0 if encryption_type == 'none': if passphrase: @@ -2545,6 +2547,9 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): raise BadKeyError( 'Unsupported encryption type: "%s"' % force_unicode( encryption_type[:30])) + else: + # Key is encrypted. + private_offset = 5 comment = lines[2][9:].strip() @@ -2562,8 +2567,9 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): force_unicode(key_type[:30]), force_unicode(public_type[:30]))) - # We skip 4 lines so far and the total public lines. - private_start_line = 4 + public_count + # We skip 4 lines so far and the total public lines and any option + # private key derivation parameters. + private_start_line = 4 + public_count + private_offset private_count = int(lines[private_start_line][15:].strip()) base64_content = ''.join(lines[ private_start_line + 1: @@ -2573,17 +2579,21 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): private_mac = lines[-1][12:].strip() - hmac_key = PUTTY_HMAC_KEY + # Default for non-encryption is empty HMAC key. + # THis is updated later if we have encrypted key. + hmac_key = b'' + encryption_key = None if encryption_type == 'aes256-cbc': if not passphrase: raise EncryptedKeyError( 'Passphrase must be provided for an encrypted key.') - hmac_key += passphrase - encryption_key = cls._getPuttyAES256EncryptionKey_v3(passphrase) + encryption_key, iv, hmac_key = cls._getPuttyAES256EncryptionKey_v3( + lines[4 + public_count:private_start_line], + passphrase) decryptor = Cipher( - algorithms.AES(encryption_key), - modes.CBC(b'\x00' * 16), + algorithms.AES256(encryption_key), + modes.CBC(iv), backend=default_backend() ).decryptor() private_blob = ( @@ -2597,8 +2607,8 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): common.NS(public_blob) + common.NS(private_blob) ) - hmac_key = sha1(hmac_key).digest() - computed_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest() + + computed_mac = hmac.new(hmac_key, hmac_data, sha256).hexdigest() if private_mac != computed_mac: if encryption_key: raise EncryptedKeyError('Bad password or HMAC mismatch.') @@ -2610,7 +2620,7 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): if key_type == 'ssh-rsa': e, n, _ = common.getMP(public_payload, count=2) - d, q, p, u, _ = common.getMP(private_blob, count=4) + d, p, q, u, _ = common.getMP(private_blob, count=4) return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) if key_type == 'ssh-dss': @@ -2639,6 +2649,63 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): privateValue=privateValue, ) + @classmethod + def _getPuttyAES256EncryptionKey_v3(cls, headers, passphrase): + """ + Return (cipher key, IV, MAC key) used to decrypt the private key. + """ + parameters = cls._getPuttyEncryptionKeyParameters(headers) + + argon_type = low_level.Type.ID + if parameters['Key-Derivation'] == 'Argon2id': + argon_type = low_level.Type.ID + elif parameters['Key-Derivation'] == 'Argon2i': + argon_type = low_level.Type.I + elif parameters['Key-Derivation'] == 'Argon2d': + argon_type = low_level.Type.D + else: + raise BadKeyError('Key-Derivation algorithm not supported.') + + result = low_level.hash_secret_raw( + secret=passphrase, + salt=bytes.fromhex(parameters['Argon2-Salt']), + time_cost=int(parameters['Argon2-Passes']), + memory_cost=int(parameters['Argon2-Memory']), + parallelism=int(parameters['Argon2-Parallelism']), + type=argon_type, + # cipher key length + IV length + MAC key length + hash_len=80, + version=19, + ) + return ( + result[:32], + result[32:48], + result[48:], + ) + + @classmethod + def _getPuttyEncryptionKeyParameters(cls, headers): + """ + Return a dictionary with the key->value for key derivation headers. + Returned values are text. + """ + result = {} + for line in headers: + parts = line.split(':', 1) + result[parts[0].strip()] = parts[1].strip() + + expected_headers = set([ + 'Key-Derivation', + 'Argon2-Memory', + 'Argon2-Passes', + 'Argon2-Parallelism', + 'Argon2-Salt', + ]) + if expected_headers != set(result.keys()): + raise BadKeyError( + 'Putty v3 encrypted key has invalid key derivation headers.') + return result + def _toString_PUTTY_V3(self, comment=None, passphrase=None): """ Return a public or private Putty v3 string. @@ -2660,6 +2727,124 @@ def _toString_PUTTY_V3(self, comment=None, passphrase=None): else: return self._toString_PUTTY_V3_private(passphrase) + def _toString_PUTTY_V3_private(self, extra): + """ + Return the Putty private key representation. + + See fromString for Putty file format. + """ + aes_block_size = 16 + lines = [] + key_type = self.sshType() + comment = 'Exported by chevah-keycert.' + data = self.data() + + hmac_key = b'' + encryption_headers = [] + if extra: + encryption_type = 'aes256-cbc' + hmac_key += extra + else: + encryption_type = 'none' + + if key_type == b'ssh-rsa': + public_blob = ( + common.NS(key_type) + + common.MP(data['e']) + + common.MP(data['n']) + ) + private_blob = ( + common.MP(data['d']) + + common.MP(data['p']) + + common.MP(data['q']) + + common.MP(data['u']) + ) + elif key_type == b'ssh-dss': + public_blob = ( + common.NS(key_type) + + common.MP(data['p']) + + common.MP(data['q']) + + common.MP(data['g']) + + common.MP(data['y']) + ) + private_blob = common.MP(data['x']) + + elif key_type == b'ssh-ed25519': + public_blob = ( + common.NS(key_type) + + common.NS(data['a']) + ) + private_blob = common.NS(data['k']) + + elif key_type in _curveTable: + + curve_name = _secToNist[self._keyObject.curve.name] + encode_point = self._keyObject.public_key().public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + public_blob = ( + common.NS(key_type) + + common.NS(curve_name) + + common.NS(encode_point) + ) + private_blob = common.MP(data['privateValue']) + + else: # pragma: no cover + raise BadKeyError('Unsupported key type.') + + private_blob_plain = private_blob + private_blob_encrypted = private_blob + + if extra: + # Encryption is requested. + # Padding is required for encryption. + padding_size = -1 * ( + (len(private_blob) % aes_block_size) - aes_block_size) + private_blob_plain += b'\x00' * padding_size + + encryption_headers.append('Key-Derivation: Argon2id') + encryption_headers.append('Argon2-Memory: 8192') + encryption_headers.append('Argon2-Passes: 34') + encryption_headers.append('Argon2-Parallelism: 1') + encryption_headers.append('Argon2-Salt: {}'.format( + self.secureRandom(16).hex())) + encryption_key, iv, hmac_key = self._getPuttyAES256EncryptionKey_v3( + encryption_headers, extra) + encryptor = Cipher( + algorithms.AES256(encryption_key), + modes.CBC(iv), + backend=default_backend() + ).encryptor() + private_blob_encrypted = ( + encryptor.update(private_blob_plain) + encryptor.finalize()) + + public_lines = textwrap.wrap( + base64.b64encode(public_blob).decode('ascii'), 64) + private_lines = textwrap.wrap( + base64.b64encode(private_blob_encrypted).decode('ascii'), 64) + + hmac_data = ( + common.NS(key_type) + + common.NS(encryption_type) + + common.NS(comment) + + common.NS(public_blob) + + common.NS(private_blob_plain) + ) + hmac_key = sha256(hmac_key).digest() + private_mac = hmac.new(hmac_key, hmac_data, sha256).hexdigest() + + lines.append('PuTTY-User-Key-File-3: %s' % key_type.decode('ascii')) + lines.append('Encryption: %s' % encryption_type) + lines.extend(encryption_headers) + lines.append('Comment: %s' % comment) + lines.append('Public-Lines: %s' % len(public_lines)) + lines.extend(public_lines) + lines.append('Private-Lines: %s' % len(private_lines)) + lines.extend(private_lines) + lines.append('Private-MAC: %s' % private_mac) + return '\r\n'.join(lines).encode('utf-8') + @classmethod def _fromString_PUBLIC_X509_CERTIFICATE(cls, data): diff --git a/src/chevah_keycert/ssl.py b/src/chevah_keycert/ssl.py index bf762ac..58f2944 100644 --- a/src/chevah_keycert/ssl.py +++ b/src/chevah_keycert/ssl.py @@ -70,7 +70,8 @@ def _generate_self_csr_parser(sub_command, default_key_size): 'To mark usage as critical, prefix the values with `critical,`. ' 'For example: "critical,key-agreement,digital-signature".' ) % (', '.join( - list(_KEY_USAGE_STANDARD.keys()) + list(_KEY_USAGE_EXTENDED.keys()))), + list(_KEY_USAGE_STANDARD.keys()) + + list(_KEY_USAGE_EXTENDED.keys()))), ) sub_command.add_argument( diff --git a/src/chevah_keycert/tests/test_ssh.py b/src/chevah_keycert/tests/test_ssh.py index d0454db..55d5c07 100644 --- a/src/chevah_keycert/tests/test_ssh.py +++ b/src/chevah_keycert/tests/test_ssh.py @@ -454,8 +454,8 @@ Private-MAC: 6b753f6180f48d153a700c6734b46b2e52f1f7e9\r """ -PUTTY_ECDSA_SHA2_NISTP256_PRIVATE_NO_PASSWORD = ( -b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp256\r +PUTTY_ECDSA_SHA2_NISTP256_PRIVATE_NO_PASSWORD = b""" +PuTTY-User-Key-File-2: ecdsa-sha2-nistp256\r Encryption: none\r Comment: ecdsa-key-20210106\r Public-Lines: 3\r @@ -465,9 +465,10 @@ Private-Lines: 1\r AAAAIDe7fQUAaorrEkedXTSmXrCY4vabtFV7e4Z8xBSvty8Q\r Private-MAC: a84b17c5dead6fed8f474406929312d45c096dfc\r -""") +""".strip() -PUTTY_V3_ECDSA_SHA2_NISTP256_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-3: ecdsa-sha2-nistp256\r +PUTTY_V3_ECDSA_SHA2_NISTP256_PRIVATE_NO_PASSWORD = b""" +PuTTY-User-Key-File-3: ecdsa-sha2-nistp256\r Encryption: none\r Comment: ecdsa-key-20210106\r Public-Lines: 3\r @@ -477,10 +478,10 @@ Private-Lines: 1\r AAAAIDe7fQUAaorrEkedXTSmXrCY4vabtFV7e4Z8xBSvty8Q\r Private-MAC: 6488b1e2221448122e8884df9622350510e7cd266d174b307104a15e5669afb5\r -""" +""".strip() -PUTTY_ECDSA_SHA2_NISTP384_PRIVATE_NO_PASSWORD = ( -b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp384\r +PUTTY_ECDSA_SHA2_NISTP384_PRIVATE_NO_PASSWORD = b""" +PuTTY-User-Key-File-2: ecdsa-sha2-nistp384\r Encryption: none\r Comment: ecdsa-key-20210106\r Public-Lines: 3\r @@ -491,9 +492,10 @@ AAAAMQCNcgWtnEeeTqFN383FBJdM90keHkJwproyLPgWQLlbZe+r8py0Pl7mUHvj\r SGmXUVc=\r Private-MAC: 1464df777d20427e2b99adb148ed4b8a1a839409\r -""") +""".strip() -PUTTY_V3_ECDSA_SHA2_NISTP384_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-3: ecdsa-sha2-nistp384\r +PUTTY_V3_ECDSA_SHA2_NISTP384_PRIVATE_NO_PASSWORD = b""" +PuTTY-User-Key-File-3: ecdsa-sha2-nistp384\r Encryption: none\r Comment: ecdsa-key-20210106\r Public-Lines: 3\r @@ -504,10 +506,10 @@ AAAAMQCNcgWtnEeeTqFN383FBJdM90keHkJwproyLPgWQLlbZe+r8py0Pl7mUHvj\r SGmXUVc=\r Private-MAC: 73cdd8880d60561a21bc23017b191471354158e2f343e1b48e8dbe0e46b74067\r -""" +""".strip() -PUTTY_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD = ( -b"""PuTTY-User-Key-File-2: ecdsa-sha2-nistp521\r +PUTTY_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD = """P +uTTY-User-Key-File-2: ecdsa-sha2-nistp521\r Encryption: none\r Comment: ecdsa-key-20210106\r Public-Lines: 4\r @@ -519,9 +521,10 @@ AAAAQgE64XtEewBVYUz+sfojvHmsiwdT+2BBBw1IAcKuozuhsz8EkOEOBJGZqCBP\r B9pAqlHsVHQJF/uVpFbJFUnjEokJ4w==\r Private-MAC: e828d7207e0e73453005d606216ca36c64d1e304\r -""") +""".strip() -PUTTY_V3_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD = b"""PuTTY-User-Key-File-3: ecdsa-sha2-nistp521\r +PUTTY_V3_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD = b""" +PuTTY-User-Key-File-3: ecdsa-sha2-nistp521\r Encryption: none\r Comment: ecdsa-key-20210106\r Public-Lines: 4\r @@ -533,7 +536,7 @@ AAAAQgE64XtEewBVYUz+sfojvHmsiwdT+2BBBw1IAcKuozuhsz8EkOEOBJGZqCBP\r B9pAqlHsVHQJF/uVpFbJFUnjEokJ4w==\r Private-MAC: 3b713999a444c896d6ea7605aba44684693249d6de9b1a0775b60a9bf8e0f19a\r -""" +""".strip() class DummyOpenContext(object): @@ -605,23 +608,23 @@ def setUp(self): y=keydata.ECDatanistp256['y'], privateValue=keydata.ECDatanistp256['privateValue'], curve=keydata.ECDatanistp256['curve'] - )._keyObject + )._keyObject self.ecObj384 = keys.Key._fromECComponents( x=keydata.ECDatanistp384['x'], y=keydata.ECDatanistp384['y'], privateValue=keydata.ECDatanistp384['privateValue'], curve=keydata.ECDatanistp384['curve'] - )._keyObject + )._keyObject self.ecObj521 = keys.Key._fromECComponents( x=keydata.ECDatanistp521['x'], y=keydata.ECDatanistp521['y'], privateValue=keydata.ECDatanistp521['privateValue'], curve=keydata.ECDatanistp521['curve'] - )._keyObject + )._keyObject self.ed25519Obj = keys.Key._fromEd25519Components( a=keydata.Ed25519Data['a'], k=keydata.Ed25519Data['k'] - )._keyObject + )._keyObject self.rsaSignature = ( b"\x00\x00\x00\x07ssh-rsa\x00\x00\x01\x00~Y\xa3\xd7\xfdW\xc6pu@" b"\xd81\xa1S\xf3O\xdaE\xf4/\x1ex\x1d\xf1\x9a\xe1G3\xd9\xd6U\x1f" @@ -636,19 +639,12 @@ def setUp(self): b"\x86\xe5k\xe3\xce\xe0u\x1c\xeb\x93\x1aN\x88\xc9\x93Y\xc3.V\xb1L" b"44`C\xc7\xa66\xaf\xfa\x7f\x04Y\x92\xfa\xa4\x1a\x18%\x19\xd5 4^" b"\xb9rY\xba \x01\xf9.\x89%H\xbe\x1c\x83A\x96" - ) + ) self.dsaSignature = ( b'\x00\x00\x00\x07ssh-dss\x00\x00\x00(?\xc7\xeb\x86;\xd5TFA\xb4' b'\xdf\x0c\xc4E@4,d\xbc\t\xd9\xae\xdd[\xed-\x82nQ\x8cf\x9b\xe8\xe1' b'jrg\x84p<' - ) - self.oldSecureRandom = Key.secureRandom - Key.secureRandom = lambda me, x: '\xff' * x - - def tearDown(self): - Key.secureRandom = self.oldSecureRandom - del self.oldSecureRandom - super(TestKey, self).tearDown() + ) def assertBadKey(self, content, message): """ @@ -731,7 +727,7 @@ def test_equal(self): self.assertFalse(rsa1 == rsa3) self.assertFalse(rsa1 == dsa) self.assertFalse(rsa1 == object) - self.assertFalse(rsa1 == None) + self.assertFalse(rsa1 is None) def test_notEqual(self): """ @@ -745,8 +741,8 @@ def test_notEqual(self): self.assertFalse(rsa1 != rsa2) self.assertTrue(rsa1 != rsa3) self.assertTrue(rsa1 != dsa) - self.assertTrue(rsa1 != object) - self.assertTrue(rsa1 != None) + self.assertTrue(rsa1 is not object) + self.assertTrue(rsa1 is None) def test_type(self): """ @@ -813,47 +809,6 @@ def test_generate_failed(self): 'Key size must be 1024, 2048, 3072, or 4096 bits.', context.exception.message) - def test_guessStringType(self): - """ - Test that the _guessStringType method guesses string types - correctly. - - Imported from Twisted. - """ - self.assertEqual( - keys.Key._guessStringType(keydata.publicRSA_openssh.encode('ascii')), - 'public_openssh') - self.assertEqual( - keys.Key._guessStringType(keydata.publicDSA_openssh.encode('ascii')), - 'public_openssh') - self.assertEqual( - keys.Key._guessStringType( - keydata.privateRSA_openssh.encode('ascii')), - 'private_openssh') - self.assertEqual( - keys.Key._guessStringType( - keydata.privateDSA_openssh.encode('ascii')), - 'private_openssh') - self.assertEqual( - keys.Key._guessStringType( - keydata.privateRSA_agentv3.encode('ascii')), - 'agentv3') - self.assertEqual( - keys.Key._guessStringType( - keydata.privateDSA_agentv3.encode('ascii')), - 'agentv3') - self.assertEqual( - keys.Key._guessStringType( - b'\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x01'), - 'blob') - self.assertEqual( - keys.Key._guessStringType( - b'\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x01'), - 'blob') - self.assertEqual( - keys.Key._guessStringType(b'not a key'), - None) - def test_guessStringType_unknown(self): """ None is returned when could not detect key type. @@ -936,15 +891,6 @@ def test_guessStringType_private_OpenSSH_DSA(self): self.assertEqual('private_openssh', result) - def test_guessStringType_private_OpenSSH_ECDSA(self): - """ - Can recognize an OpenSSH ECDSA private key. - """ - result = Key._guessStringType( - keydata.privateECDSA_256_openssh.encode('ascii')) - - self.assertEqual('private_openssh', result) - def test_guessStringType_public_OpenSSH(self): """ Can recognize an OpenSSH public key. @@ -961,25 +907,6 @@ def test_guessStringType_public_PKCS1(self): self.assertEqual('public_pkcs1_rsa', result) - def test_guessStringType_public_OpenSSH_ECDSA(self): - """ - Can recognize an OpenSSH public key. - """ - result = Key._guessStringType( - keydata.publicECDSA_256_openssh.encode('ascii')) - - self.assertEqual('public_openssh', result) - - result = Key._guessStringType( - keydata.publicECDSA_384_openssh.encode('ascii')) - - self.assertEqual('public_openssh', result) - - result = Key._guessStringType( - keydata.publicECDSA_521_openssh.encode('ascii')) - - self.assertEqual('public_openssh', result) - def test_guessStringType_private_SSHCOM(self): """ Can recognize an SSH.com private key. @@ -1250,27 +1177,6 @@ def test_fromString_BLOB_blob_type_non_ascii(self): '"b\'\\x00\\x00\\x00\\nssh-\\xc2\\xbd\\xc2\\xbd\\xc2\\xbd\'"' ) - def test_fromString_PRIVATE_BLOB(self): - """ - Test that a private key is correctly generated from a private key blob. - """ - rsaBlob = (common.NS('ssh-rsa') + common.MP(2) + common.MP(3) + - common.MP(4) + common.MP(5) + common.MP(6) + common.MP(7)) - rsaKey = keys.Key._fromString_PRIVATE_BLOB(rsaBlob) - dsaBlob = (common.NS('ssh-dss') + common.MP(2) + common.MP(3) + - common.MP(4) + common.MP(5) + common.MP(6)) - dsaKey = keys.Key._fromString_PRIVATE_BLOB(dsaBlob) - badBlob = common.NS('ssh-bad') - self.assertFalse(rsaKey.isPublic()) - self.assertEqual( - rsaKey.data(), - {'n': 2, 'e': 3, 'd': 4, 'u': 5, 'p': 6, 'q': 7}) - self.assertFalse(dsaKey.isPublic()) - self.assertEqual( - dsaKey.data(), {'p': 2, 'q': 3, 'g': 4, 'y': 5, 'x': 6}) - self.assertRaises( - keys.BadKeyError, keys.Key._fromString_PRIVATE_BLOB, badBlob) - def test_blobRSA(self): """ Return the over-the-wire SSH format of the RSA public key. @@ -1308,11 +1214,14 @@ def test_blobEC(self): keys.Key(self.ecObj).blob(), common.NS(keydata.ECDatanistp256['curve']) + common.NS(keydata.ECDatanistp256['curve'][-8:]) + - common.NS(b'\x04' + - utils.int_to_bytes( - self.ecObj.private_numbers().public_numbers.x, byteLength) + - utils.int_to_bytes( - self.ecObj.private_numbers().public_numbers.y, byteLength)) + common.NS( + b'\x04' + + utils.int_to_bytes( + self.ecObj.private_numbers().public_numbers.x, byteLength + ) + + utils.int_to_bytes( + self.ecObj.private_numbers().public_numbers.y, byteLength) + ) ) def test_blobEd25519(self): @@ -1324,13 +1233,13 @@ def test_blobEd25519(self): publicBytes = self.ed25519Obj.public_key().public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw - ) + ) self.assertEqual( keys.Key(self.ed25519Obj).blob(), common.NS(b'ssh-ed25519') + common.NS(publicBytes) - ) + ) def test_blobNoKey(self): """ @@ -1401,19 +1310,19 @@ def test_privateBlobEd25519(self): publicBytes = self.ed25519Obj.public_key().public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw - ) + ) privateBytes = self.ed25519Obj.private_bytes( serialization.Encoding.Raw, serialization.PrivateFormat.Raw, serialization.NoEncryption() - ) + ) self.assertEqual( keys.Key(self.ed25519Obj).privateBlob(), common.NS(b'ssh-ed25519') + common.NS(publicBytes) + common.NS(privateBytes + publicBytes) - ) + ) def test_privateBlobNoKeyObject(self): """ @@ -1459,28 +1368,13 @@ def test_fromString_PUBLIC_OPENSSH_DSA(self): self.checkParsedDSAPublic1024(sut) - def test_fromString_OpenSSH(self): + def test_fromString_OpenSSH_public(self): """ - Test that keys are correctly generated from OpenSSH strings. + It can load an OpenSSH public key. """ - self._testPublicPrivateFromString( - keydata.publicRSA_openssh, - keydata.privateRSA_openssh, 'RSA', keydata.RSAData) - - self.assertEqual( - keys.Key.fromString( - keydata.privateRSA_openssh_encrypted, - passphrase=b'encrypted'), - keys.Key.fromString(keydata.privateRSA_openssh)) - - self.assertEqual( - keys.Key.fromString( - keydata.privateRSA_openssh_alternate), - keys.Key.fromString(keydata.privateRSA_openssh)) + sut = Key.fromString(OPENSSH_RSA_PUBLIC) - self._testPublicPrivateFromString( - keydata.publicDSA_openssh, - keydata.privateDSA_openssh, 'DSA', keydata.DSAData) + self.checkParsedRSAPublic1024(sut) def test_fromString_OpenSSH_private_missing_password(self): """ @@ -1528,44 +1422,34 @@ def test_fromString_PRIVATE_OPENSSH_newer(self): passphrase=b'testxp') self.assertEqual(key, key2) - def test_fromString_PRIVATE_OPENSSH_not_encrypted_with_passphrase(self): + def test_toString_OPENSSH_rsa(self): """ - When loading a unencrypted OpenSSH private key with passhphrase - will raise BadKeyError. + Test that the Key object generates OpenSSH keys correctly. """ + key = Key.fromString(OPENSSH_V1_RSA_PRIVATE) - with self.assertRaises(BadKeyError) as context: - Key.fromString(OPENSSH_RSA_PRIVATE, passphrase='pass') + result = key.public().toString('openssh') + self.assertEqual(OPENSSH_RSA_PUBLIC, result) - self.assertEqual( - 'OpenSSH key not encrypted', - context.exception.message) + result = key.toString('openssh') + self.assertEqual(OPENSSH_RSA_PRIVATE, result) - def test_toString_OPENSSH(self): + def test_toString_OPENSSH_v1_rsa(self): """ Test that the Key object generates OpenSSH keys correctly. """ - key = keys.Key.fromString(keydata.privateRSA_lsh) + key = Key.fromString(OPENSSH_RSA_PRIVATE) - self.assertEqual(key.toString('openssh'), keydata.privateRSA_openssh) - self.assertEqual( - key.toString('openssh', 'encrypted'), - keydata.privateRSA_openssh_encrypted) - self.assertEqual( - key.public().toString('openssh'), - keydata.publicRSA_openssh[:-8]) - self.assertEqual( - key.public().toString('openssh', 'comment'), - keydata.publicRSA_openssh) + result = key.public().toString('openssh_v1') + self.assertEqual(OPENSSH_RSA_PUBLIC, result) - key = keys.Key.fromString(keydata.privateDSA_lsh) - - self.assertEqual(key.toString('openssh'), keydata.privateDSA_openssh) - self.assertEqual( - key.public().toString('openssh', 'comment'), - keydata.publicDSA_openssh) - self.assertEqual( - key.public().toString('openssh'), keydata.publicDSA_openssh[:-8]) + result = key.toString('openssh_v1') + self.assertStartsWith( + b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + b'b3BlbnNzaC1rZXk', + result) + reloaded = Key.fromString(result) + self.assertEqual(reloaded, key) def addSSHCOMKeyHeaders(self, source, headers): """ @@ -1577,8 +1461,8 @@ def addSSHCOMKeyHeaders(self, source, headers): for key, value in headers.items(): line = '{}: {}'.format(key, value) header = '\\\n'.join(textwrap.wrap(line, 70)) - lines.insert(1, header) - return '\n'.join(lines) + lines.insert(1, header.encode('utf-8')) + return b'\n'.join(lines) def checkParsedDSAPublic1024(self, sut): """ @@ -1663,7 +1547,11 @@ def checkParsedRSAPrivate1024(self, sut): """ self.assertEqual(1024, sut.size()) self.assertEqual('RSA', sut.type()) - self.assertFalse(sut.isPublic()) + self.assertEqual(b'ssh-rsa', sut.sshType()) + self.assertEqual( + 'fc:39:4c:d4:51:c8:5d:78:1e:4d:9d:1e:73:42:52:55', + sut.fingerprint()) + self.assertIsFalse(sut.isPublic()) data = sut.data() self.assertEqual(65537, data['e']) self.checkParsedRSAPublic1024Data(sut) @@ -1815,15 +1703,6 @@ def test_fromString_PRIVATE_OPENSSH_v1_DSA(self): self.checkParsedDSAPrivate1024(sut) - def test_fromString_PRIVATE_OPENSSH_ECDSA(self): - """ - Can not load a private OPENSSH ECDSA. - """ - self.assertBadKey( - keydata.privateECDSA_256_openssh, - 'Key type \'EC\' not supported.' - ) - def test_fromString_PRIVATE_OPENSSH_short(self): """ Raise an error when private OpenSSH key is too short. @@ -2708,14 +2587,14 @@ def test_fingerprintBadFormat(self): 'Unsupported fingerprint format: sha256-base', em.exception.args[0]) - def test_sign(self): + def test_sign_rsa(self): """ Test that the Key object generates correct signatures. """ key = keys.Key.fromString(keydata.privateRSA_openssh) - self.assertEqual(key.sign(''), self.rsaSignature) - key = keys.Key.fromString(keydata.privateDSA_openssh) - self.assertEqual(key.sign(''), self.dsaSignature) + signature = key.sign(b'') + self.assertTrue(key.verify(signature, b'')) + self.assertEqual(signature, self.rsaSignature) def test_verify(self): """ @@ -2750,6 +2629,97 @@ def test_repr(self): result ) + def test_fromString_PRIVATE_PUTTY_V3_short(self): + """ + An exception is raised when key is too short. + """ + content = 'PuTTY-User-Key-File-3: ssh-rsa' + + self.assertKeyIsTooShort(content) + + content = ( + 'PuTTY-User-Key-File-3: ssh-rsa\n' + 'Encryption: aes256-cbc\n' + ) + + self.assertKeyIsTooShort(content) + + content = ( + 'PuTTY-User-Key-File-3: ssh-rsa\n' + 'Encryption: aes256-cbc\n' + 'Comment: bla\n' + ) + + self.assertKeyIsTooShort(content) + + def test_fromString_PRIVATE_PUTTY_V3_RSA_bad_password(self): + """ + An exception is raised when password is not valid. + """ + with self.assertRaises(EncryptedKeyError) as context: + Key.fromString( + PUTTY_V3_RSA_PRIVATE_WITH_PASSWORD, passphrase=b'bad-pass') + + self.assertEqual( + 'Bad password or HMAC mismatch.', context.exception.message) + + def test_fromString_PRIVATE_PUTTY_V3_RSA_missing_password(self): + """ + An exception is raised when key is encrypted but no password was + provided. + """ + with self.assertRaises(EncryptedKeyError) as context: + Key.fromString(PUTTY_V3_RSA_PRIVATE_WITH_PASSWORD) + + self.assertEqual( + 'Passphrase must be provided for an encrypted key.', + context.exception.message) + + def test_fromString_PRIVATE_PUTTY_V3_unsupported_type(self): + """ + An exception is raised when key contain a type which is not supported. + """ + content = """PuTTY-User-Key-File-3: ssh-bad +IGNORED +""" + self.assertBadKey( + content, 'Unsupported key type: "ssh-bad"') + + def test_fromString_PRIVATE_PUTTY_V3_unsupported_encryption(self): + """ + An exception is raised when key contain an encryption method + which is not supported. + """ + content = """PuTTY-User-Key-File-3: ssh-dss +Encryption: aes126-cbc +IGNORED +""" + self.assertBadKey( + content, 'Unsupported encryption type: "aes126-cbc"') + + def test_fromString_PRIVATE_PUTTY_v3_type_mismatch(self): + """ + An exception is raised when key header advertise one key type while + the public key another. + """ + content = """PuTTY-User-Key-File-3: ssh-rsa +Encryption: aes256-cbc +Comment: imported-openssh-key +Public-Lines: 4 +AAAAB3NzaC1kc3MAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTK +APkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk ++X6Lc4+lAfp1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcj +RToF6/jpLw== +IGNORED +""" + self.assertBadKey( + content, + ( + 'Mismatch key type. Header has "ssh-rsa",' + ' public has "ssh-dss"'), + ) + + class Test_generate_ssh_key_parser(ChevahTestCase, CommandLineMixin): """ Unit tests for generate_ssh_key_parser. @@ -2857,7 +2827,7 @@ def test_generate_ssh_key_custom_values(self): file_name_pub = file_name + '.pub' options = self.parseArguments([ self.sub_command_name, - u'--key-size=512', + u'--key-size=2048', u'--key-type=DSA', u'--key-file=' + file_name, u'--key-comment=this is a comment', @@ -2867,36 +2837,39 @@ def test_generate_ssh_key_custom_values(self): exit_code, message, key = generate_ssh_key( options, open_method=open_method) + self.assertEqual( + 'SSH key of type "ssh-dss" and length "2048" generated as public ' + 'key file "%s" and private key file "%s" ' + 'without comment as not supported by the output format.' % ( + file_name_pub, file_name), + message, + ) + self.assertEqual(0, exit_code) + self.assertEqual('DSA', key.type()) - self.assertEqual(512, key.size()) + self.assertEqual(2048, key.size()) # First it writes the private key. first_file = open_method.calls.pop(0) - self.assertPathEqual( - _path(file_name), first_file['path']) + self.assertPathEqual(file_name, first_file['path']) self.assertEqual('wb', first_file['mode']) - self.assertEqual( - key.toString('openssh'), first_file['stream'].getvalue()) + # OpenSSH V1 format has a random value generated when storing + # the private key. + self.assertStartsWith( + b'-----BEGIN OPENSSH PRIVATE KEY-----\n' + b'b3BlbnNzaC1r', + first_file['stream'].getvalue()) # Second it writes the public key. second_file = open_method.calls.pop(0) self.assertPathEqual( - _path(file_name_pub.decode('ascii')), second_file['path']) + file_name_pub, second_file['path']) self.assertEqual('wb', second_file['mode']) self.assertEqual( - key.public().toString('openssh', 'this is a comment'), + key.public().toString('openssh_v1'), second_file['stream'].getvalue()) - self.assertEqual( - u'SSH key of type "dsa" and length "512" generated as public ' - u'key file "%s" and private key file "%s" ' - u'having comment "this is a comment".' % ( - file_name_pub, file_name), - message, - ) - self.assertEqual(0, exit_code) - def test_generate_ssh_key_default_values(self): """ When no path and no comment are provided, it will use default @@ -2939,7 +2912,6 @@ def test_generate_ssh_key_default_values(self): self.assertEqual( key.public().toString('openssh'), second_file['stream'].getvalue()) - def test_generate_ssh_key_private_exist_no_migration(self): """ When no migration is done it will not generate the key, diff --git a/src/chevah_keycert/tests/test_ssl.py b/src/chevah_keycert/tests/test_ssl.py index 18ee718..b2bad95 100644 --- a/src/chevah_keycert/tests/test_ssl.py +++ b/src/chevah_keycert/tests/test_ssl.py @@ -169,7 +169,8 @@ def test_common_name_required(self): def test_default(self): """ - It can be initialized with only a subparserfile has no and sub-command name. + It can be initialized with only a subparserfile has no and sub-command + name. """ generate_csr_parser(self.subparser, 'key-gen') From d44b154fd6337bf67bc7819a9ade57b71e553d92 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Tue, 12 Mar 2024 18:07:21 +0000 Subject: [PATCH 20/41] Update gha. --- .github/workflows/main.yml | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9d2ea18..1630ee9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,20 +25,17 @@ jobs: uses: actions/cache@v2 with: path: | - build-keycert + build-py3 key: ${{ runner.os }}-${{ hashFiles('setup.py') }} - name: Deps run: ./pythia.sh deps - - name: Lint - run: ./pythia.sh lint - - name: Rename build to unicode run: mv build-py3 build-py3-ț - name: Test - run: ./brink.sh test + run: ./pythia.sh test env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CHEVAH_BUILD: 'build-py3-ț' @@ -56,14 +53,14 @@ jobs: uses: actions/cache@v2 with: path: | - build-keycert + build-py3 key: ${{ runner.os }}-${{ hashFiles('setup.py') }} - name: Deps - run: ./brink.sh deps + run: ./pythia.sh deps - name: Test - run: ./brink.sh test + run: ./pythia.sh test env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -77,20 +74,20 @@ jobs: uses: actions/cache@v2 with: path: | - build-keycert + build-by3 key: ${{ runner.os }}-${{ hashFiles('setup.py') }} - name: Deps - run: sh ./brink.sh deps + run: sh ./pythia.sh deps - name: Test - run: sh ./brink.sh test + run: sh ./pythia.sh test env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} keys-interop: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: @@ -106,13 +103,13 @@ jobs: uses: actions/cache@v2 with: path: | - build-keycert + build-py3 key: ${{ runner.os }}-${{ hashFiles('setup.py') }} - name: Deps run: | sudo apt-get --quiet install putty-tools - ./brink.sh deps + ./pythia.sh deps - name: Test - run: ./brink.sh test_interop_${{ matrix.config.test_type }} + run: ./pythia.sh test_interop_${{ matrix.config.test_type }} From 9af16a43dc1a134e14157efc50ad783a06ff7d67 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Tue, 12 Mar 2024 20:44:38 +0000 Subject: [PATCH 21/41] FIx build dir. --- pavement.py | 35 ++++++++++++++-------------- src/chevah_keycert/common.py | 3 ++- src/chevah_keycert/tests/test_ssh.py | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/pavement.py b/pavement.py index f60a4b6..3c90206 100644 --- a/pavement.py +++ b/pavement.py @@ -11,6 +11,7 @@ from paver.easy import call_task, consume_args, task, pushd EXTRA_PYPI_INDEX = os.environ['PIP_INDEX_URL'] +BUILD_DIR = os.environ.get('CHEVAH_BUILD', 'build-py3') @task @@ -88,23 +89,23 @@ class LoopPlugin(Plugin): ] - os.chdir('build-py3') - ChevahTestCase.initialize(drop_user='-') - ChevahTestCase.dropPrivileges() - try: - nose_main(addplugins=plugins) - finally: - process = psutil.Process(os.getpid()) - print('Max RSS: {} MB'.format(process.memory_info().rss / 1000000)) - if cov: - cov.stop() - cov.save() - threads = threading.enumerate() - if len(threads) > 1: - print("There are still active threads: %s" % threads) - sys.stdout.flush() - sys.stderr.flush() - os._exit(1) + with pushd(BUILD_DIR): + ChevahTestCase.initialize(drop_user='-') + ChevahTestCase.dropPrivileges() + try: + nose_main(addplugins=plugins) + finally: + process = psutil.Process(os.getpid()) + print('Max RSS: {} MB'.format(process.memory_info().rss / 1000000)) + if cov: + cov.stop() + cov.save() + threads = threading.enumerate() + if len(threads) > 1: + print("There are still active threads: %s" % threads) + sys.stdout.flush() + sys.stderr.flush() + os._exit(1) @task diff --git a/src/chevah_keycert/common.py b/src/chevah_keycert/common.py index 6cd6e75..547a041 100644 --- a/src/chevah_keycert/common.py +++ b/src/chevah_keycert/common.py @@ -101,7 +101,8 @@ def getMP(data, count=1): def ffs(c, s): """ first from second - goes through the first list, looking for items in the second, returns the first one + goes through the first list, looking for items in the second, + returns the first one """ for i in c: if i in s: diff --git a/src/chevah_keycert/tests/test_ssh.py b/src/chevah_keycert/tests/test_ssh.py index 55d5c07..721ff55 100644 --- a/src/chevah_keycert/tests/test_ssh.py +++ b/src/chevah_keycert/tests/test_ssh.py @@ -742,7 +742,7 @@ def test_notEqual(self): self.assertTrue(rsa1 != rsa3) self.assertTrue(rsa1 != dsa) self.assertTrue(rsa1 is not object) - self.assertTrue(rsa1 is None) + self.assertTrue(rsa1 is not None) def test_type(self): """ From 62fc183e2d59e35fb8ee5604a72f2baa7b51e57c Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Tue, 12 Mar 2024 23:06:01 +0000 Subject: [PATCH 22/41] Add argon2 deps. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index a4b3fa6..3967c46 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ install_requires = pyasn1 >= 0.1.7 cryptography >= 3.2 constantly >= 15.1.0 + argon2-cffi >= 23.1.0 packages = find: package_dir = =src From 7fb0dc24fd865f10216736a3175d658c6d3c7c76 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Tue, 12 Mar 2024 23:19:22 +0000 Subject: [PATCH 23/41] Reformat tests to black. --- pavement.py | 109 +- setup.cfg | 4 +- src/chevah_keycert/ssh.py | 8 +- src/chevah_keycert/tests/helpers.py | 12 +- src/chevah_keycert/tests/keydata.py | 348 ++++--- src/chevah_keycert/tests/test_ssh.py | 1393 +++++++++++++------------- src/chevah_keycert/tests/test_ssl.py | 586 ++++++----- 7 files changed, 1299 insertions(+), 1161 deletions(-) diff --git a/pavement.py b/pavement.py index 3c90206..39985e4 100644 --- a/pavement.py +++ b/pavement.py @@ -1,6 +1,7 @@ """ Build file for the project. """ + import os import re import sys @@ -10,13 +11,13 @@ from paver.easy import call_task, consume_args, task, pushd -EXTRA_PYPI_INDEX = os.environ['PIP_INDEX_URL'] -BUILD_DIR = os.environ.get('CHEVAH_BUILD', 'build-py3') +EXTRA_PYPI_INDEX = os.environ["PIP_INDEX_URL"] +BUILD_DIR = os.environ.get("CHEVAH_BUILD", "build-py3") @task def default(): - call_task('test') + call_task("test") @task @@ -24,12 +25,17 @@ def deps(): """ Install all dependencies. """ - pip = load_entry_point('pip', 'console_scripts', 'pip') - pip(args=[ - 'install', '-U', - '--extra-index-url', EXTRA_PYPI_INDEX, - '-e', '.[dev]', - ]) + pip = load_entry_point("pip", "console_scripts", "pip") + pip( + args=[ + "install", + "-U", + "--extra-index-url", + EXTRA_PYPI_INDEX, + "-e", + ".[dev]", + ] + ) @task @@ -41,7 +47,7 @@ def test(args): _nose(args, cov=None) -def _nose(args, cov, base='chevah_keycert.tests'): +def _nose(args, cov, base="chevah_keycert.tests"): """ Run nose tests in the same process. """ @@ -58,19 +64,20 @@ def _nose(args, cov, base='chevah_keycert.tests'): import chevah_keycert class LoopPlugin(Plugin): - name = 'loop' + name = "loop" - new_arguments = [ - '--with-randomly', - '--with-run-reporter', - '--with-timer', - '-v', '-s', - ] + new_arguments = [ + "--with-randomly", + "--with-run-reporter", + "--with-timer", + "-v", + "-s", + ] have_tests = False for argument in args: - if not argument.startswith('-'): - argument = '%s.%s' % (base, argument) + if not argument.startswith("-"): + argument = "%s.%s" % (base, argument) have_tests = True new_arguments.append(argument) @@ -81,22 +88,16 @@ class LoopPlugin(Plugin): sys.argv = new_arguments print(new_arguments) - plugins = [ - TestTimer(), - RunReporter(), - MemoryUsage(), - LoopPlugin() - ] - + plugins = [TestTimer(), RunReporter(), MemoryUsage(), LoopPlugin()] with pushd(BUILD_DIR): - ChevahTestCase.initialize(drop_user='-') + ChevahTestCase.initialize(drop_user="-") ChevahTestCase.dropPrivileges() try: nose_main(addplugins=plugins) finally: process = psutil.Process(os.getpid()) - print('Max RSS: {} MB'.format(process.memory_info().rss / 1000000)) + print("Max RSS: {} MB".format(process.memory_info().rss / 1000000)) if cov: cov.stop() cov.save() @@ -115,14 +116,15 @@ def test_interop_load_dsa(args): Run the SSH key interoperability tests for loading external DSA keys. """ try: - os.mkdir('build') + os.mkdir("build") except OSError: """Already exists""" exit_code = 1 - with pushd('build'): + with pushd("build"): exit_code = call( - "../src/chevah_keycert/tests/ssh_load_keys_tests.sh dsa", shell=True) + "../src/chevah_keycert/tests/ssh_load_keys_tests.sh dsa", shell=True + ) sys.exit(exit_code) @@ -134,17 +136,19 @@ def test_interop_load_rsa(args): Run the SSH key interoperability tests for loading external RSA keys. """ try: - os.mkdir('build') + os.mkdir("build") except OSError: """Already exists""" exit_code = 1 - with pushd('build'): + with pushd("build"): exit_code = call( - "../src/chevah_keycert/tests/ssh_load_keys_tests.sh rsa", shell=True) + "../src/chevah_keycert/tests/ssh_load_keys_tests.sh rsa", shell=True + ) sys.exit(exit_code) + @task @consume_args def test_interop_load_eced(args): @@ -152,14 +156,16 @@ def test_interop_load_eced(args): Run the SSH key interoperability tests for loading external ECDSA and Ed25519 keys. """ try: - os.mkdir('build') + os.mkdir("build") except OSError: """Already exists""" exit_code = 1 - with pushd('build'): + with pushd("build"): exit_code = call( - "../src/chevah_keycert/tests/ssh_load_keys_tests.sh ecdsa ed25519", shell=True) + "../src/chevah_keycert/tests/ssh_load_keys_tests.sh ecdsa ed25519", + shell=True, + ) sys.exit(exit_code) @@ -171,14 +177,15 @@ def test_interop_generate(args): Run the SSH key interoperability tests for internally-generated keys. """ try: - os.mkdir('build') + os.mkdir("build") except OSError: """Already exists""" exit_code = 1 - with pushd('build'): + with pushd("build"): exit_code = call( - "../stc/chevah_keycert/tests/ssh_gen_keys_tests.sh", shell=True) + "../stc/chevah_keycert/tests/ssh_gen_keys_tests.sh", shell=True + ) sys.exit(exit_code) @@ -191,22 +198,8 @@ def lint(): from pyflakes.api import main as pyflakes_main from pycodestyle import _main as pycodestyle_main - sys.argv = [ - re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])] + [ - 'src/chevah_keycert', - ] + sys.argv = [re.sub(r"(-script\.pyw?|\.exe)?$", "", sys.argv[0])] + [ + "src/chevah_keycert", + ] - try: - pyflakes_main() - except SystemExit as pyflakes_exit: - pass - - sys.argv.extend([ - '--ignore=E741', - '--ignore=E741', - '--hang-closing', - '--max-line-length=80', - ]) - pycodestyle_exit = pycodestyle_main() - - sys.exit(pyflakes_exit.code or pycodestyle_exit) + pyflakes_main() diff --git a/setup.cfg b/setup.cfg index 3967c46..7b4a52b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,8 +33,8 @@ dev = future pocketlint ==1.4.4.c10 - pyflakes >= 1.5.0 - pycodestyle ==2.3.1 + pyflakes >= 3.2.0 + black == 24.2.0 chevah-compat >= 0.70 diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index 9be985e..cbd4c45 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -91,10 +91,10 @@ } _secToNist = { - 'secp256r1' : b'nistp256', - 'secp384r1' : b'nistp384', - 'secp521r1' : b'nistp521', - 'secp192r1' : b'nistp192', + 'secp256r1': b'nistp256', + 'secp384r1': b'nistp384', + 'secp521r1': b'nistp521', + 'secp192r1': b'nistp192', } diff --git a/src/chevah_keycert/tests/helpers.py b/src/chevah_keycert/tests/helpers.py index 5d2fb38..69d20cc 100644 --- a/src/chevah_keycert/tests/helpers.py +++ b/src/chevah_keycert/tests/helpers.py @@ -13,6 +13,7 @@ class CommandLineMixin(object): """ Helper to test command line tools. """ + def parseArguments(self, args): """ Parse arguments and return options and captured stdout. @@ -28,11 +29,13 @@ def parseArguments(self, args): return options except SystemExit as error: # pragma: no cover raise AssertionError( - 'Fail to parse %s\n-- stdout --\n%s\n-- stderr --\n%s' % ( + "Fail to parse %s\n-- stdout --\n%s\n-- stderr --\n%s" + % ( error.code, stdout.getvalue(), stderr.getvalue(), - )) + ) + ) finally: # We don't revert to sys.__stdout__ and the test runner might # have injected its logger. @@ -51,8 +54,9 @@ def parseArgumentsFailure(self, args): sys.stdout = stdout sys.stderr = stderr self.parser.parse_args(args) - raise AssertionError( # pragma: no cover - 'Failure not triggered when parsing the arguments.') + raise AssertionError( # pragma: no cover + "Failure not triggered when parsing the arguments." + ) except SystemExit as error: return error.code, stderr.getvalue() finally: diff --git a/src/chevah_keycert/tests/keydata.py b/src/chevah_keycert/tests/keydata.py index dc44a66..daec89f 100644 --- a/src/chevah_keycert/tests/keydata.py +++ b/src/chevah_keycert/tests/keydata.py @@ -8,105 +8,143 @@ from base64 import decodestring as decodebytes RSAData = { - 'n': int('269413617238113438198661010376758399219880277968382122687862697' - '296942471209955603071120391975773283844560230371884389952067978' - '789684135947515341209478065209455427327369102356204259106807047' - '964139525310539133073743116175821417513079706301100600025815509' - '786721808719302671068052414466483676821987505720384645561708425' - '794379383191274856941628512616355437197560712892001107828247792' - '561858327085521991407807015047750218508971611590850575870321007' - '991909043252470730134547038841839367764074379439843108550888709' - '430958143271417044750314742880542002948053835745429446485015316' - '60749404403945254975473896534482849256068133525751'), - 'e': int(65537), - 'd': int('420335724286999695680502438485489819800002417295071059780489811' - '840828351636754206234982682752076205397047218449504537476523960' - '987613148307573487322720481066677105211155388802079519869249746' - '774085882219244493290663802569201213676433159425782937159766786' - '329742053214957933941260042101377175565683849732354700525628975' - '239000548651346620826136200952740446562751690924335365940810658' - '931238410612521441739702170503547025018016868116037053013935451' - '477930426013703886193016416453215950072147440344656137718959053' - '897268663969428680144841987624962928576808352739627262941675617' - '7724661940425316604626522633351193810751757014073'), - 'p': int('152689878451107675391723141129365667732639179427453246378763774' - '448531436802867910180261906924087589684175595016060014593521649' - '964959248408388984465569934780790357826811592229318702991401054' - '226302790395714901636384511513449977061729214247279176398290513' - '085108930550446985490864812445551198848562639933888780317'), - 'q': int('176444974592327996338888725079951900172097062203378367409936859' - '072670162290963119826394224277287608693818012745872307600855894' - '647300295516866118620024751601329775653542084052616260193174546' - '400544176890518564317596334518015173606460860373958663673307503' - '231977779632583864454001476729233959405710696795574874403'), - 'u': int('936018002388095842969518498561007090965136403384715613439364803' - '229386793506402222847415019772053080458257034241832795210460612' - '924445085372678524176842007912276654532773301546269997020970818' - '155956828553418266110329867222673040098885651348225673298948529' - '93885224775891490070400861134282266967852120152546563278') + "n": int( + "269413617238113438198661010376758399219880277968382122687862697" + "296942471209955603071120391975773283844560230371884389952067978" + "789684135947515341209478065209455427327369102356204259106807047" + "964139525310539133073743116175821417513079706301100600025815509" + "786721808719302671068052414466483676821987505720384645561708425" + "794379383191274856941628512616355437197560712892001107828247792" + "561858327085521991407807015047750218508971611590850575870321007" + "991909043252470730134547038841839367764074379439843108550888709" + "430958143271417044750314742880542002948053835745429446485015316" + "60749404403945254975473896534482849256068133525751" + ), + "e": int(65537), + "d": int( + "420335724286999695680502438485489819800002417295071059780489811" + "840828351636754206234982682752076205397047218449504537476523960" + "987613148307573487322720481066677105211155388802079519869249746" + "774085882219244493290663802569201213676433159425782937159766786" + "329742053214957933941260042101377175565683849732354700525628975" + "239000548651346620826136200952740446562751690924335365940810658" + "931238410612521441739702170503547025018016868116037053013935451" + "477930426013703886193016416453215950072147440344656137718959053" + "897268663969428680144841987624962928576808352739627262941675617" + "7724661940425316604626522633351193810751757014073" + ), + "p": int( + "152689878451107675391723141129365667732639179427453246378763774" + "448531436802867910180261906924087589684175595016060014593521649" + "964959248408388984465569934780790357826811592229318702991401054" + "226302790395714901636384511513449977061729214247279176398290513" + "085108930550446985490864812445551198848562639933888780317" + ), + "q": int( + "176444974592327996338888725079951900172097062203378367409936859" + "072670162290963119826394224277287608693818012745872307600855894" + "647300295516866118620024751601329775653542084052616260193174546" + "400544176890518564317596334518015173606460860373958663673307503" + "231977779632583864454001476729233959405710696795574874403" + ), + "u": int( + "936018002388095842969518498561007090965136403384715613439364803" + "229386793506402222847415019772053080458257034241832795210460612" + "924445085372678524176842007912276654532773301546269997020970818" + "155956828553418266110329867222673040098885651348225673298948529" + "93885224775891490070400861134282266967852120152546563278" + ), } DSAData = { - 'g': int("10253261326864117157640690761723586967382334319435778695" - "29171533815411392477819921538350732400350395446211982054" - "96512489289702949127531056893725702005035043292195216541" - "11525058911428414042792836395195432445511200566318251789" - "10575695836669396181746841141924498545494149998282951407" - "18645344764026044855941864175"), - 'p': int("10292031726231756443208850082191198787792966516790381991" - "77502076899763751166291092085666022362525614129374702633" - "26262930887668422949051881895212412718444016917144560705" - "45675251775747156453237145919794089496168502517202869160" - "78674893099371444940800865897607102159386345313384716752" - "18590012064772045092956919481"), - 'q': int(1393384845225358996250882900535419012502712821577), - 'x': int(1220877188542930584999385210465204342686893855021), - 'y': int("14604423062661947579790240720337570315008549983452208015" - "39426429789435409684914513123700756086453120500041882809" - "10283610277194188071619191739512379408443695946763554493" - "86398594314468629823767964702559709430618263927529765769" - "10270265745700231533660131769648708944711006508965764877" - "684264272082256183140297951") + "g": int( + "10253261326864117157640690761723586967382334319435778695" + "29171533815411392477819921538350732400350395446211982054" + "96512489289702949127531056893725702005035043292195216541" + "11525058911428414042792836395195432445511200566318251789" + "10575695836669396181746841141924498545494149998282951407" + "18645344764026044855941864175" + ), + "p": int( + "10292031726231756443208850082191198787792966516790381991" + "77502076899763751166291092085666022362525614129374702633" + "26262930887668422949051881895212412718444016917144560705" + "45675251775747156453237145919794089496168502517202869160" + "78674893099371444940800865897607102159386345313384716752" + "18590012064772045092956919481" + ), + "q": int(1393384845225358996250882900535419012502712821577), + "x": int(1220877188542930584999385210465204342686893855021), + "y": int( + "14604423062661947579790240720337570315008549983452208015" + "39426429789435409684914513123700756086453120500041882809" + "10283610277194188071619191739512379408443695946763554493" + "86398594314468629823767964702559709430618263927529765769" + "10270265745700231533660131769648708944711006508965764877" + "684264272082256183140297951" + ), } ECDatanistp256 = { - 'x': int('762825130203920963171185031449647317742997734817505505433829043' - '45687059013883'), - 'y': int('815431978646028526322656647694416475343443758943143196810611371' - '59310646683104'), - 'privateValue': int('3463874347721034170096400845565569825355565567882605' - '9678074967909361042656500'), - 'curve': b'ecdsa-sha2-nistp256' + "x": int( + "762825130203920963171185031449647317742997734817505505433829043" + "45687059013883" + ), + "y": int( + "815431978646028526322656647694416475343443758943143196810611371" + "59310646683104" + ), + "privateValue": int( + "3463874347721034170096400845565569825355565567882605" + "9678074967909361042656500" + ), + "curve": b"ecdsa-sha2-nistp256", } ECDatanistp384 = { - 'privateValue': int('280814107134858470598753916394807521398239633534281633982576099083' - '35787109896602102090002196616273211495718603965098'), - 'x': int('10036914308591746758780165503819213553101287571902957054148542' - '504671046744460374996612408381962208627004841444205030'), - 'y': int('17337335659928075994560513699823544906448896792102247714689323' - '575406618073069185107088229463828921069465902299522926'), - 'curve': b'ecdsa-sha2-nistp384' + "privateValue": int( + "280814107134858470598753916394807521398239633534281633982576099083" + "35787109896602102090002196616273211495718603965098" + ), + "x": int( + "10036914308591746758780165503819213553101287571902957054148542" + "504671046744460374996612408381962208627004841444205030" + ), + "y": int( + "17337335659928075994560513699823544906448896792102247714689323" + "575406618073069185107088229463828921069465902299522926" + ), + "curve": b"ecdsa-sha2-nistp384", } ECDatanistp521 = { - 'x': int('12944742826257420846659527752683763193401384271391513286022917' - '29910013082920512632908350502247952686156279140016049549948975' - '670668730618745449113644014505462'), - 'y': int('10784108810271976186737587749436295782985563640368689081052886' - '16296815984553198866894145509329328086635278430266482551941240' - '591605833440825557820439734509311'), - 'privateValue': int('662751235215460886290293902658128847495347691199214706697089140769' - '672273950767961331442265530524063943548846724348048614239791498442' - '5997823106818915698960565'), - 'curve': b'ecdsa-sha2-nistp521' + "x": int( + "12944742826257420846659527752683763193401384271391513286022917" + "29910013082920512632908350502247952686156279140016049549948975" + "670668730618745449113644014505462" + ), + "y": int( + "10784108810271976186737587749436295782985563640368689081052886" + "16296815984553198866894145509329328086635278430266482551941240" + "591605833440825557820439734509311" + ), + "privateValue": int( + "662751235215460886290293902658128847495347691199214706697089140769" + "672273950767961331442265530524063943548846724348048614239791498442" + "5997823106818915698960565" + ), + "curve": b"ecdsa-sha2-nistp521", } Ed25519Data = { - 'a': (b'\xf1\x16\xd1\x15J\x1e\x15\x0e\x19^\x19F\xb5\xf2D\r\xb2R\xa0\xae*k' - b'#\x13sE\xfd@\xd9W{\x8b'), - 'k': (b'7/%\xda\x8d\xd4\xa8\x9ax|a\xf0\x98\x01\xc6\xf4^mg\x05i17Li\r\x05U' - b'\xbb\xc9DX') + "a": ( + b"\xf1\x16\xd1\x15J\x1e\x15\x0e\x19^\x19F\xb5\xf2D\r\xb2R\xa0\xae*k" + b"#\x13sE\xfd@\xd9W{\x8b" + ), + "k": ( + b"7/%\xda\x8d\xd4\xa8\x9ax|a\xf0\x98\x01\xc6\xf4^mg\x05i17Li\r\x05U" + b"\xbb\xc9DX" + ), } privateECDSA_openssh521 = b"""-----BEGIN EC PRIVATE KEY----- @@ -211,7 +249,7 @@ b"foNfICZgptyti8ZseZj3 comment" ) -privateRSA_openssh = b'''-----BEGIN RSA PRIVATE KEY----- +privateRSA_openssh = b"""-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEA1WqseCPW1hvsJaFQxHdjUIRFAVVCFCoq4NBg7tTpo61K+jkG XoRVdV8ANr9vqio/gyY3wWkuW/3w89J91pjNOkB41cqoGMARkyQJDIFMj/ec7RMW aqQE6Ul3w+RVZLN5aJ4sCOus6AQtIXcFp47vUzANpeW7PWriCTZv/TTTfW9G/4fa @@ -237,7 +275,7 @@ v7YnAoGAZhb5IDTQVCW8YTGsgvvvnDUefkpVAmiVDQqTvh6/4UD6kKdUcDHpePzg Zrcid5rr3dXSMEbK4tdeQZvPtUg1Uaol3N7bNClIIdvWdPx+5S9T95wJcLnkoHam rXp0IjScTxfLP+Cq5V6lJ94/pX8Ppoj1FdZfNxeS4NYFSRA7kvY= ------END RSA PRIVATE KEY-----''' +-----END RSA PRIVATE KEY-----""" # Some versions of OpenSSH generate these (slightly different keys): the PKCS#1 # structure is wrapped in an extra ASN.1 SEQUENCE and there's an empty SEQUENCE @@ -268,7 +306,7 @@ -----END RSA PRIVATE KEY-----""" # New format introduced in OpenSSH 6.5 -privateRSA_openssh_new = b'''-----BEGIN OPENSSH PRIVATE KEY----- +privateRSA_openssh_new = b"""-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn NhAAAAAwEAAQAAAQEA1WqseCPW1hvsJaFQxHdjUIRFAVVCFCoq4NBg7tTpo61K+jkGXoRV dV8ANr9vqio/gyY3wWkuW/3w89J91pjNOkB41cqoGMARkyQJDIFMj/ec7RMWaqQE6Ul3w+ @@ -295,7 +333,7 @@ R/FqSSEnShCcLEbpyMM++l5ffgK61PXBGqGoQ3W/166sPNfLDI5B9UY7XHr9/0Caf8xyX8 XOmR15LFmB5W07EjAAAAAAEC -----END OPENSSH PRIVATE KEY----- -''' +""" # Encrypted with the passphrase 'encrypted' privateRSA_openssh_encrypted = b"""-----BEGIN RSA PRIVATE KEY----- @@ -395,12 +433,12 @@ -----END RSA PRIVATE KEY-----""" publicRSA_lsh = ( - b'{KDEwOnB1YmxpYy1rZXkoMTQ6cnNhLXBrY3MxLXNoYTEoMTpuMjU3OgDVaqx4I9bWG+wloVD' - b'Ed2NQhEUBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHj' - b'VyqgYwBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auI' - b'JNm/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY' - b'6RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvbw' - b'7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3KSgxOmUzOgEAASkpKQ==}' + b"{KDEwOnB1YmxpYy1rZXkoMTQ6cnNhLXBrY3MxLXNoYTEoMTpuMjU3OgDVaqx4I9bWG+wloVD" + b"Ed2NQhEUBVUIUKirg0GDu1OmjrUr6OQZehFV1XwA2v2+qKj+DJjfBaS5b/fDz0n3WmM06QHj" + b"VyqgYwBGTJAkMgUyP95ztExZqpATpSXfD5FVks3loniwI66zoBC0hdwWnju9TMA2l5bs9auI" + b"JNm/9NNN9b0b/h9qpKSeq/631heY+Grh6HUqx6sBa9zDfH8Kk5O8/kUmWQNUZdy03w17snaY" + b"6RKXCpCnd1bqcPUWzxiwYZNW6Pd+rf81CrKfxGAugWBViC6QqbkPD5ASfNaNHjkbtM6Vlvbw" + b"7KW4CC1ffdOgTtDc1foNfICZgptyti8ZseZj3KSgxOmUzOgEAASkpKQ==}" ) privateRSA_lsh = ( @@ -435,7 +473,7 @@ b"x\x91P\x94\xd4\xc1\x1b\x898lFdZQ\xa0\x9a\x07=H\x8f\x03Q\xcck\x12\x8e}" b"\x1a\xb1e\xe7qu9\xe02\x05u\x8d\x18L\xaf\x93\xb1I\xb1f_xbz\xd1\x0c\xca" b"\xe6MC\xb3\x9c\xf4k}\xe6\x0c\x98\xdc\xcf!b\x8e\xd5.\x12\xde\x04\xae\xd7$" - b"n\x831\xa2\x15\xa2D=\"\xa9b&\"\xb9\xb2\xedT\n\x9d\x08\x83\xa7\x07\r\xff" + b'n\x831\xa2\x15\xa2D="\xa9b&"\xb9\xb2\xedT\n\x9d\x08\x83\xa7\x07\r\xff' b"\x19\x18\x8e\xd8\xab\x1d\xdaH\x9c1h\x11\xa1fm\xe3\xd8\x1d)(1:a128:if7" b"\xc6@\xdd!\xc5\x04\xf3\xb0\xb8>G\x94|v\xfc-\xeb?9<\x95\xc3C\x01Q\xc4B" b"\x97\xf3\xe8\x16\xa4\xc6\xee\xec\xd4I\x10P8\x04\xee;\xcd\xd7\xd0\xcc\xcc" @@ -447,11 +485,11 @@ b"\x14L&\x9b(\xa4\x023\x08_\xe1\xa7p\x98\x014y^R\x8e\xc4\xcf6\xbc\x1fKU" b"\xac\xeb\xc1S\x84\xc7\xe1a\xa8J\xd4\xa2\xff@\r\x80\x1f\x12\xa9P\xc0*\x18" b"u\x94\x0c\x06\x9b\x16P\xa8K\xecA\xcd{\xef\xf7K\xc9u\x02h\xc4\x98\xb8\x86" - b"\x88\x18ZC\xe7\x023\x97\"d\x93\x83\x0cE*|\xed)(1:c128:f\x16\xf9 4\xd0T%" + b'\x88\x18ZC\xe7\x023\x97"d\x93\x83\x0cE*|\xed)(1:c128:f\x16\xf9 4\xd0T%' b"\xbca1\xac\x82\xfb\xef\x9c5\x1e~JU\x02h\x95\r\n\x93\xbe\x1e\xbf\xe1@\xfa" - b"\x90\xa7Tp1\xe9x\xfc\xe0f\xb7\"w\x9a\xeb\xdd\xd5\xd20F\xca\xe2\xd7^A\x9b" + b'\x90\xa7Tp1\xe9x\xfc\xe0f\xb7"w\x9a\xeb\xdd\xd5\xd20F\xca\xe2\xd7^A\x9b' b"\xcf\xb5H5Q\xaa%\xdc\xde\xdb4)H!\xdb\xd6t\xfc~\xe5/S\xf7\x9c\tp\xb9\xe4" - b"\xa0v\xa6\xadzt\"4\x9cO\x17\xcb?\xe0\xaa\xe5^\xa5\'\xde?\xa5\x7f\x0f\xa6" + b"\xa0v\xa6\xadzt\"4\x9cO\x17\xcb?\xe0\xaa\xe5^\xa5'\xde?\xa5\x7f\x0f\xa6" b"\x88\xf5\x15\xd6_7\x17\x92\xe0\xd6\x05I\x10;\x92\xf6)))" ) @@ -486,8 +524,8 @@ b"=yT\xce\x00\x00\x00\x81\x00\xd9p\x06\xd8\xe2\xbc\xd4x\x91P\x94\xd4\xc1" b"\x1b\x898lFdZQ\xa0\x9a\x07=H\x8f\x03Q\xcck\x12\x8e}\x1a\xb1e\xe7qu9\xe02" b"\x05u\x8d\x18L\xaf\x93\xb1I\xb1f_xbz\xd1\x0c\xca\xe6MC\xb3\x9c\xf4k}\xe6" - b"\x0c\x98\xdc\xcf!b\x8e\xd5.\x12\xde\x04\xae\xd7$n\x831\xa2\x15\xa2D=\"" - b"\xa9b&\"\xb9\xb2\xedT\n\x9d\x08\x83\xa7\x07\r\xff\x19\x18\x8e\xd8\xab" + b'\x0c\x98\xdc\xcf!b\x8e\xd5.\x12\xde\x04\xae\xd7$n\x831\xa2\x15\xa2D="' + b'\xa9b&"\xb9\xb2\xedT\n\x9d\x08\x83\xa7\x07\r\xff\x19\x18\x8e\xd8\xab' b"\x1d\xdaH\x9c1h\x11\xa1fm\xe3\xd8\x1d\x00\x00\x00\x81\x00\xfbD\x17\x8b" b"\xa46\xbe\x1e7\x1d\xa7\xf6al\x04\xc4\xaa\xddx>\x07\x8c\x1e3\x02\xae\x03" b"\x14\x87\x83z\xe5\x9e}\x08g\xa8\xf2\xaa\xbf\x12p\xcfr\xa9\xa7\xc7\x0b" @@ -548,7 +586,8 @@ -----END OPENSSH PRIVATE KEY----- """ -publicDSA_lsh = decodebytes(b"""\ +publicDSA_lsh = decodebytes( + b"""\ e0tERXdPbkIxWW14cFl5MXJaWGtvTXpwa2MyRW9NVHB3TVRJNU9nQ1NrRHJGUkVWUTBDS1FEUngv aVFBTXBhUFM3eFdKaFJWVENMaHhScFdhU0MrN0lkeURKS3N2bkxMQ0RUUDVaeHc5MzVyQU1pNVZG MmJiZWp3L1M0R1VXczdEem9LYmJoL2hydVBCdnNoYmhQSmRIMVZWSXI2TFB6Sm1GU2V1cWsvZlli @@ -560,9 +599,11 @@ YUVoOFluamlhelFUTkVwa2xSWnFlQkdvMWdvdEpnZ05tVmFJUU5JQ2xHbEx5Q2kzNTllZkVVdVFj WjlTWHhNNTlQK2hlY2MvR1UvR0hha1c1WVdFNGRQMkdnZGdNUVdDN1M2V0ZJWGVQR0dYcU5RRGRX eGxYOHVtaGVudlFxYTFQbktyRlJoRHJKdzhaN0dqZEh4ZmxzeENFbVhQb0xOOHBLU2s9fQ== -""") +""" +) -privateDSA_lsh = decodebytes(b"""\ +privateDSA_lsh = decodebytes( + b"""\ KDExOnByaXZhdGUta2V5KDM6ZHNhKDE6cDEyOToAkpA6xURFUNAikA0cf4kADKWj0u8ViYUVUwi4 cUaVmkgvuyHcgySrL5yywg0z+WccPd+awDIuVRdm23o8P0uBlFrOw86Cm24f4a7jwb7IW4TyXR9V VSK+iz8yZhUnrqpP32G1xkpq2tNZmelVbuAUAGirD6F7YtnSCR5TT5LH1rkpKDE6cTIxOgD0EYmT @@ -572,9 +613,11 @@ mhIfGJ44ms0EzRKZJUWangRqNYKLSYIDZlWiEDSApRpS8got+fXnxFLkHGfUl8TOfT/oXnHPxlPx h2pFuWFhOHT9hoHYDEFgu0ulhSF3jxhl6jUA3VsZV/LpoXp70KmtT5yqxUYQ6ycPGexo3R8X5bMQ hJlz6CzfKSgxOngyMToA1doGy3M1HcQrrNKXxD9W3XWl0S0pKSk= -""") +""" +) -privateDSA_agentv3 = decodebytes(b"""\ +privateDSA_agentv3 = decodebytes( + b"""\ AAAAB3NzaC1kc3MAAACBAJKQOsVERVDQIpANHH+JAAylo9LvFYmFFVMIuHFGlZpIL7sh3IMkqy+c ssINM/lnHD3fmsAyLlUXZtt6PD9LgZRazsPOgptuH+Gu48G+yFuE8l0fVVUivos/MmYVJ66qT99h tcZKatrTWZnpVW7gFABoqw+he2LZ0gkeU0+Sx9a5AAAAFQD0EYmTNaFJ8CS0+vFSF4nYcyEnSQAA @@ -584,49 +627,66 @@ NIClGlLyCi359efEUuQcZ9SXxM59P+hecc/GU/GHakW5YWE4dP2GgdgMQWC7S6WFIXePGGXqNQDd WxlX8umhenvQqa1PnKrFRhDrJw8Z7GjdHxflsxCEmXPoLN8AAAAVANXaBstzNR3EK6zSl8Q/Vt11 pdEt -""") +""" +) # Custom code -privateRSA_fingerprint_md5 = '85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da' +privateRSA_fingerprint_md5 = "85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da" RSAData2 = { - 'n': int('106248668575524741116943830949539894737212779118943280948138' - '20729711061576321820845393835692814935201176341295575504152775' - '16685881326038852354459895734875625093273594925884531272867425' - '864910490065695876046999646807138717162833156501'), - 'e': int(35), - 'd': int('667848773903298372735075508825679338348194611604786337388297' - '30301040958479737159599618395783408164121679859572188879144827' - '13602371850869127033494910375212470664166001439410214474266799' - '85974425203903884190893469297150446322896587555'), - 'q': int('3395694744258061291019136154000709371890447462086362702627' - '9704149412726577280741108645721676968699696898960891593323'), - 'p': int('3128922844292337321766351031842562691837301298995834258844' - '4720539204069737532863831050930719431498338835415515173887'), - 'u': int('2777403202132551568802514199893235993376771442611051821485' - '0278129927603609294283482712900532542110958095343012272938') - } + "n": int( + "106248668575524741116943830949539894737212779118943280948138" + "20729711061576321820845393835692814935201176341295575504152775" + "16685881326038852354459895734875625093273594925884531272867425" + "864910490065695876046999646807138717162833156501" + ), + "e": int(35), + "d": int( + "667848773903298372735075508825679338348194611604786337388297" + "30301040958479737159599618395783408164121679859572188879144827" + "13602371850869127033494910375212470664166001439410214474266799" + "85974425203903884190893469297150446322896587555" + ), + "q": int( + "3395694744258061291019136154000709371890447462086362702627" + "9704149412726577280741108645721676968699696898960891593323" + ), + "p": int( + "3128922844292337321766351031842562691837301298995834258844" + "4720539204069737532863831050930719431498338835415515173887" + ), + "u": int( + "2777403202132551568802514199893235993376771442611051821485" + "0278129927603609294283482712900532542110958095343012272938" + ), +} DSAData2 = { - 'g': int("10253261326864117157640690761723586967382334319435778695" - "29171533815411392477819921538350732400350395446211982054" - "96512489289702949127531056893725702005035043292195216541" - "11525058911428414042792836395195432445511200566318251789" - "10575695836669396181746841141924498545494149998282951407" - "18645344764026044855941864175"), - 'p': int("10292031726231756443208850082191198787792966516790381991" - "77502076899763751166291092085666022362525614129374702633" - "26262930887668422949051881895212412718444016917144560705" - "45675251775747156453237145919794089496168502517202869160" - "78674893099371444940800865897607102159386345313384716752" - "18590012064772045092956919481"), - 'q': int(1393384845225358996250882900535419012502712821577), - 'x': int(1220877188542930584999385210465204342686893855021), - 'y': int("14604423062661947579790240720337570315008549983452208015" - "39426429789435409684914513123700756086453120500041882809" - "10283610277194188071619191739512379408443695946763554493" - "86398594314468629823767964702559709430618263927529765769" - "10270265745700231533660131769648708944711006508965764877" - "684264272082256183140297951") - } \ No newline at end of file + "g": int( + "10253261326864117157640690761723586967382334319435778695" + "29171533815411392477819921538350732400350395446211982054" + "96512489289702949127531056893725702005035043292195216541" + "11525058911428414042792836395195432445511200566318251789" + "10575695836669396181746841141924498545494149998282951407" + "18645344764026044855941864175" + ), + "p": int( + "10292031726231756443208850082191198787792966516790381991" + "77502076899763751166291092085666022362525614129374702633" + "26262930887668422949051881895212412718444016917144560705" + "45675251775747156453237145919794089496168502517202869160" + "78674893099371444940800865897607102159386345313384716752" + "18590012064772045092956919481" + ), + "q": int(1393384845225358996250882900535419012502712821577), + "x": int(1220877188542930584999385210465204342686893855021), + "y": int( + "14604423062661947579790240720337570315008549983452208015" + "39426429789435409684914513123700756086453120500041882809" + "10283610277194188071619191739512379408443695946763554493" + "86398594314468629823767964702559709430618263927529765769" + "10270265745700231533660131769648708944711006508965764877" + "684264272082256183140297951" + ), +} diff --git a/src/chevah_keycert/tests/test_ssh.py b/src/chevah_keycert/tests/test_ssh.py index 721ff55..afb529e 100644 --- a/src/chevah_keycert/tests/test_ssh.py +++ b/src/chevah_keycert/tests/test_ssh.py @@ -17,17 +17,17 @@ BadKeyError, KeyCertException, EncryptedKeyError, - ) +) from chevah_keycert.ssh import ( Key, generate_ssh_key, generate_ssh_key_parser, - ) +) from chevah_keycert.tests import keydata from chevah_keycert.tests.helpers import CommandLineMixin -OPENSSH_RSA_PRIVATE = (b'''-----BEGIN RSA PRIVATE KEY----- +OPENSSH_RSA_PRIVATE = b"""-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKAPkPAWzlu5BRHcmA u0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAfp1YxCR 9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLwIDAQAB @@ -41,11 +41,11 @@ yBjyyznB9PnoKUJs34rex5ZHE70e7zs01Omk5Wp6PXxVzz40CKUW5yc7JpRH1BsR /RTFeEyTOiWL4CLQCwJAf4BF9eVLxRQ9A4Mm9Ikt4lF8ii6na4nxdtEzP8p2LP9t LqHYUobNanxB+7Msi4f3gYyuKdOGnWHqD2U4HcLdMQ== ------END RSA PRIVATE KEY-----''') +-----END RSA PRIVATE KEY-----""" # Converted from old format using OpenSSH without a password. # $ ssh-keygen -e -p -f OPENSSH_RSA_PRIVATE.key -OPENSSH_V1_RSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- +OPENSSH_V1_RSA_PRIVATE = b"""-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn NhAAAAAwEAAQAAAIEAuH1erUmpA0gemaL8oC7H9YgvT97SR3j0ygD5DwFs5buQUR3JgLtL k45+xoa6cW441jMA1TdDsc8O7XiNDhCFZKZ5XMjOi+ZhpPl+i3OPpQH6dWMQkfaPfhVzFk @@ -60,11 +60,11 @@ qmJgciEWrqRa0abeRYmfQjEIG0WEa+ohYnBkgCN/q1MoxSTpuMb2nsml61dSxOIMEAAABB AMuRA1NheNl5urb0MzPGwIKu3dv8doh0bjpA1G9Wyt2MAck4oZfQS6r3UYUcaZdtRHKlqQ f0cwBWxmvutBv4le8AAAAAAQID ------END OPENSSH PRIVATE KEY-----''') +-----END OPENSSH PRIVATE KEY-----""" # Converted from old format using OpenSSH with `test` as password. # $ ssh-keygen -e -p -f OPENSSH_RSA_PRIVATE.key -OPENSSH_V1_ENCRYPTED_RSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- +OPENSSH_V1_ENCRYPTED_RSA_PRIVATE = b"""-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCO5u6Nze CPk3e+vkL9MmvWAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakD SB6ZovygLsf1iC9P3tJHePTKAPkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI @@ -80,21 +80,21 @@ 5sRcoQii4HcPjK0WUZaSM/5LsxSsqDt+nBVoaq7k24ITTjXdHIuiT1YnKFjErzD3bznosW wNe7YoLXxnuszUFaBAWthJuOsE1JVAScqo7oClPc1CHX8qEZz5vihkEploAOGe0hj5Kjt6 vLDBLhI7ag== ------END OPENSSH PRIVATE KEY-----''') +-----END OPENSSH PRIVATE KEY-----""" OPENSSH_RSA_PUBLIC = ( - b'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKA' - b'PkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAf' - b'p1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLw==' - ) + b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKA" + b"PkPAWzlu5BRHcmAu0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAf" + b"p1YxCR9o9+FXMWSJP32jRwI+4LhWYxnYUldvAO5LDz9QeR0yKimwcjRToF6/jpLw==" +) -PKCS1_RSA_PUBLIC = (b'''-----BEGIN RSA PUBLIC KEY----- +PKCS1_RSA_PUBLIC = b"""-----BEGIN RSA PUBLIC KEY----- MIGJAoGBALh9Xq1JqQNIHpmi/KAux/WIL0/e0kd49MoA+Q8BbOW7kFEdyYC7S5OO fsaGunFuONYzANU3Q7HPDu14jQ4QhWSmeVzIzovmYaT5fotzj6UB+nVjEJH2j34V cxZIk/faNHAj7guFZjGdhSV28A7ksPP1B5HTIqKbByNFOgXr+OkvAgMBAAE= ------END RSA PUBLIC KEY-----''') +-----END RSA PUBLIC KEY-----""" -OPENSSH_DSA_PRIVATE = (b'''-----BEGIN DSA PRIVATE KEY----- +OPENSSH_DSA_PRIVATE = b"""-----BEGIN DSA PRIVATE KEY----- MIIBugIBAAKBgQDOwkKGnmVZ9bRl7ZCn/wSELV0n5ELsqVZFOtBpHleEOitsvjEB BbTKX0fZ83vaMVnJFVw3DQSbi192krvk909Y6h3HVO2MKBRd9t29fr26VvCZQOxR 4fzkPuL+Px4+ShqE171sOzsuEDt0Mkxf152QxrA2vPowkj7fmzRH5xgDTQIVAIYb @@ -105,11 +105,11 @@ 2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3pZ7SnoRJ7vATQnMJpwZYno 8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk204gIUTk7aRv65 oTedYsAyi80L8phYBN4= ------END DSA PRIVATE KEY-----''') +-----END DSA PRIVATE KEY-----""" # Converted from old format using OpenSSH without a password. # $ ssh-keygen -e -p -f OPENSSH_DSA_PRIVATE.key -OPENSSH_V1_DSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- +OPENSSH_V1_DSA_PRIVATE = b"""-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH NzAAAAgQDOwkKGnmVZ9bRl7ZCn/wSELV0n5ELsqVZFOtBpHleEOitsvjEBBbTKX0fZ83va MVnJFVw3DQSbi192krvk909Y6h3HVO2MKBRd9t29fr26VvCZQOxR4fzkPuL+Px4+ShqE17 @@ -129,11 +129,11 @@ ar/GQyhUuCmakro03424Hpq7HnhTuHkHelntKehEnu8BNCcwmnBliejy5leHHhOY6hLHPH LaNMlviu3hHptvCNoj1xr/WItq+hxDbXxmTbTiAAAAFE5O2kb+uaE3nWLAMovNC/KYWATe AAAAAAECAw== ------END OPENSSH PRIVATE KEY-----''') +-----END OPENSSH PRIVATE KEY-----""" # Converted from old format using OpenSSH with `test` as the password. # $ ssh-keygen -e -p -f OPENSSH_DSA_PRIVATE.key -OPENSSH_V1_ENCRYPTED_DSA_PRIVATE = (b'''-----BEGIN OPENSSH PRIVATE KEY----- +OPENSSH_V1_ENCRYPTED_DSA_PRIVATE = b"""-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCR+DbQqo 2salfbIh0HztjEAAAAEAAAAAEAAAGxAAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/ BIQtXSfkQuypVkU60GkeV4Q6K2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHc @@ -153,20 +153,20 @@ wwYEpG5w9/IlKJ62JmEqhEVMI4HHyDLcocYlU6OoD1Ivy09dcIO8uRBYc9jFccj/1ej5oI tn6RsW0HRlVx06tbp6RDHBfAdg5suu5pW9uv2tESbEqpMHt4FQgqKcSQwzYLvo/bfPuxs0 HNOQMLNwRg8yYbCG+u2HU9YTlQdTgG/5h+eYsQLObPU+TjYgS5p6sUZCkTCnOz8= ------END OPENSSH PRIVATE KEY-----''') +-----END OPENSSH PRIVATE KEY-----""" OPENSSH_DSA_PUBLIC = ( - 'ssh-dss AAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/BIQtXSfkQuypVkU60GkeV4Q6K' - '2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU7YwoFF323b1+vbpW8JlA7FHh/O' - 'Q+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCSPt+bNEfnGANNAAAAFQCGG/5Y0lHJaOk' - '4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaDF7JPfggDDfs0aYss/9vKLzp0px3PmG2o+I' - '5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLm' - 'GdI2Sj9aynRWdberk4r+0+zWlHL7epZTDCuDmLOFiQF/AAAAIB/6sL9MO4ZwtFzwbOKNOoZxf' - 'ORwNbzzHf+IpzyBTxxQJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3p' - 'Z7SnoRJ7vATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk20' - '4g==' - ) + "ssh-dss AAAAB3NzaC1kc3MAAACBAM7CQoaeZVn1tGXtkKf/BIQtXSfkQuypVkU60GkeV4Q6K" + "2y+MQEFtMpfR9nze9oxWckVXDcNBJuLX3aSu+T3T1jqHcdU7YwoFF323b1+vbpW8JlA7FHh/O" + "Q+4v4/Hj5KGoTXvWw7Oy4QO3QyTF/XnZDGsDa8+jCSPt+bNEfnGANNAAAAFQCGG/5Y0lHJaOk" + "4jcKIhfvW8mnxSQAAAIBcD5MAYKYXZl41k3TiaDF7JPfggDDfs0aYss/9vKLzp0px3PmG2o+I" + "5Zw2YXOsDtrSj56sTcH6aLASbaxXP55VZ804IlBqUdSpGuTWJXnjYUvQEJ4+Ilr3UFrDilVLm" + "GdI2Sj9aynRWdberk4r+0+zWlHL7epZTDCuDmLOFiQF/AAAAIB/6sL9MO4ZwtFzwbOKNOoZxf" + "ORwNbzzHf+IpzyBTxxQJcYS6QgbtSi2tUY1WeJxmq/xkMoVLgpmpK6NN+NuB6aux54U7h5B3p" + "Z7SnoRJ7vATQnMJpwZYno8uZXhx4TmOoSxzxy2jTJb4rt4R6bbwjaI9ca/1iLavocQ218Zk20" + "4g==" +) # Same key as OPENSSH_RSA_PUBLIC, wrapped at 70 characters. SSHCOM_RSA_PUBLIC = b"""---- BEGIN SSH2 PUBLIC KEY ---- @@ -204,8 +204,7 @@ ---- END SSH2 ENCRYPTED PRIVATE KEY ----""" # Same as OPENSSH_RSA_PRIVATE and with 'chevah' password. -SSHCOM_RSA_PRIVATE_WITH_PASSWORD = ( - b"""---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- +SSHCOM_RSA_PRIVATE_WITH_PASSWORD = b"""---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- P2/56wAAAjMAAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0e3JzYS 1wa2NzMXYyLW9hZXB9fQAAAAgzZGVzLWNiYwAAAeAqUfFcnQIi4HEOAvAoJp8nIsw3WZMc MhWiSWenwY0tKZPxngo1s2p8QkIclw0Tu7twvtG2zABb4x/jfyqLPc5brvBdYiAXMg1xPS @@ -217,7 +216,7 @@ 4FHJG/z/D7dEbeC3mJfXFrM7PgCGFx9L6/FqLC+piJmyEq8nggkg9P0o+oJ7/c/xGU7at9 BsDKrM0FEXc8bFp39e8BNRbikCD61zfFp7B1s64y1mmqJkDYe2pH7FUA9mbC3vv6YM9tsY fWGAGt8dHGIMM6MrzZYr8xJLwdmPDwAtFt2GR1Y8M0vnw6WtoL4= ----- END SSH2 ENCRYPTED PRIVATE KEY ----""") +---- END SSH2 ENCRYPTED PRIVATE KEY ----""" SSHCOM_DSA_PRIVATE_NO_PASSWORD = b"""---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ---- P2/56wAAAgIAAAAmZGwtbW9kcHtzaWdue2RzYS1uaXN0LXNoYTF9LGRoe3BsYWlufX0AAA @@ -552,8 +551,7 @@ def __init__(self): def __call__(self, path, mode): self.last_stream = BytesIO() - self.calls.append( - {'path': path, 'mode': mode, 'stream': self.last_stream}) + self.calls.append({"path": path, "mode": mode, "stream": self.last_stream}) return self def __enter__(self): @@ -572,11 +570,11 @@ def test_path(self): """ Will take an unicode and will return the os encoded path. """ - result = _path(u'path-\N{sun}') - if self.os_name == 'windows': - self.assertEqual(u'path-\N{sun}', result) + result = _path("path-\N{sun}") + if self.os_name == "windows": + self.assertEqual("path-\N{sun}", result) else: - self.assertEqual(b'path-\xe2\x98\x89', result) + self.assertEqual(b"path-\xe2\x98\x89", result) class TestKey(ChevahTestCase): @@ -589,42 +587,41 @@ class TestKey(ChevahTestCase): def setUp(self): super(TestKey, self).setUp() self.rsaObj = keys.Key._fromRSAComponents( - n=keydata.RSAData['n'], - e=keydata.RSAData['e'], - d=keydata.RSAData['d'], - p=keydata.RSAData['p'], - q=keydata.RSAData['q'], - u=keydata.RSAData['u'], - )._keyObject + n=keydata.RSAData["n"], + e=keydata.RSAData["e"], + d=keydata.RSAData["d"], + p=keydata.RSAData["p"], + q=keydata.RSAData["q"], + u=keydata.RSAData["u"], + )._keyObject self.dsaObj = keys.Key._fromDSAComponents( - y=keydata.DSAData['y'], - p=keydata.DSAData['p'], - q=keydata.DSAData['q'], - g=keydata.DSAData['g'], - x=keydata.DSAData['x'], - )._keyObject + y=keydata.DSAData["y"], + p=keydata.DSAData["p"], + q=keydata.DSAData["q"], + g=keydata.DSAData["g"], + x=keydata.DSAData["x"], + )._keyObject self.ecObj = keys.Key._fromECComponents( - x=keydata.ECDatanistp256['x'], - y=keydata.ECDatanistp256['y'], - privateValue=keydata.ECDatanistp256['privateValue'], - curve=keydata.ECDatanistp256['curve'] - )._keyObject + x=keydata.ECDatanistp256["x"], + y=keydata.ECDatanistp256["y"], + privateValue=keydata.ECDatanistp256["privateValue"], + curve=keydata.ECDatanistp256["curve"], + )._keyObject self.ecObj384 = keys.Key._fromECComponents( - x=keydata.ECDatanistp384['x'], - y=keydata.ECDatanistp384['y'], - privateValue=keydata.ECDatanistp384['privateValue'], - curve=keydata.ECDatanistp384['curve'] - )._keyObject + x=keydata.ECDatanistp384["x"], + y=keydata.ECDatanistp384["y"], + privateValue=keydata.ECDatanistp384["privateValue"], + curve=keydata.ECDatanistp384["curve"], + )._keyObject self.ecObj521 = keys.Key._fromECComponents( - x=keydata.ECDatanistp521['x'], - y=keydata.ECDatanistp521['y'], - privateValue=keydata.ECDatanistp521['privateValue'], - curve=keydata.ECDatanistp521['curve'] - )._keyObject + x=keydata.ECDatanistp521["x"], + y=keydata.ECDatanistp521["y"], + privateValue=keydata.ECDatanistp521["privateValue"], + curve=keydata.ECDatanistp521["curve"], + )._keyObject self.ed25519Obj = keys.Key._fromEd25519Components( - a=keydata.Ed25519Data['a'], - k=keydata.Ed25519Data['k'] - )._keyObject + a=keydata.Ed25519Data["a"], k=keydata.Ed25519Data["k"] + )._keyObject self.rsaSignature = ( b"\x00\x00\x00\x07ssh-rsa\x00\x00\x01\x00~Y\xa3\xd7\xfdW\xc6pu@" b"\xd81\xa1S\xf3O\xdaE\xf4/\x1ex\x1d\xf1\x9a\xe1G3\xd9\xd6U\x1f" @@ -639,12 +636,12 @@ def setUp(self): b"\x86\xe5k\xe3\xce\xe0u\x1c\xeb\x93\x1aN\x88\xc9\x93Y\xc3.V\xb1L" b"44`C\xc7\xa66\xaf\xfa\x7f\x04Y\x92\xfa\xa4\x1a\x18%\x19\xd5 4^" b"\xb9rY\xba \x01\xf9.\x89%H\xbe\x1c\x83A\x96" - ) + ) self.dsaSignature = ( - b'\x00\x00\x00\x07ssh-dss\x00\x00\x00(?\xc7\xeb\x86;\xd5TFA\xb4' - b'\xdf\x0c\xc4E@4,d\xbc\t\xd9\xae\xdd[\xed-\x82nQ\x8cf\x9b\xe8\xe1' - b'jrg\x84p<' - ) + b"\x00\x00\x00\x07ssh-dss\x00\x00\x00(?\xc7\xeb\x86;\xd5TFA\xb4" + b"\xdf\x0c\xc4E@4,d\xbc\t\xd9\xae\xdd[\xed-\x82nQ\x8cf\x9b\xe8\xe1" + b"jrg\x84p<" + ) def assertBadKey(self, content, message): """ @@ -659,33 +656,33 @@ def assertKeyIsTooShort(self, content): """ Check the key content is too short. """ - self.assertBadKey(content, 'Key is too short.') + self.assertBadKey(content, "Key is too short.") def assertKeyParseError(self, content): """ Check that key content fail to parse. """ - self.assertBadKey(content, 'Fail to parse key content.') + self.assertBadKey(content, "Fail to parse key content.") def _getKeysForFingerprintTest(self): """ Return tuple with public RSA and DSA keys from the test data. """ rsa = keys.Key._fromRSAComponents( - n=keydata.RSAData['n'], - e=keydata.RSAData['e'], - d=keydata.RSAData['d'], - p=keydata.RSAData['p'], - q=keydata.RSAData['q'], - u=keydata.RSAData['u'], - )._keyObject + n=keydata.RSAData["n"], + e=keydata.RSAData["e"], + d=keydata.RSAData["d"], + p=keydata.RSAData["p"], + q=keydata.RSAData["q"], + u=keydata.RSAData["u"], + )._keyObject dsa = keys.Key._fromDSAComponents( - y=keydata.DSAData['y'], - p=keydata.DSAData['p'], - q=keydata.DSAData['q'], - g=keydata.DSAData['g'], - x=keydata.DSAData['x'], - )._keyObject + y=keydata.DSAData["y"], + p=keydata.DSAData["p"], + q=keydata.DSAData["q"], + g=keydata.DSAData["g"], + x=keydata.DSAData["x"], + )._keyObject return (rsa, dsa) def _testPublicPrivateFromString(self, public, private, type, data): @@ -720,8 +717,7 @@ def test_equal(self): """ rsa1 = keys.Key(self.rsaObj) rsa2 = keys.Key(self.rsaObj) - rsa3 = keys.Key( - keys.Key._fromRSAComponents(n=int(5), e=int(3))._keyObject) + rsa3 = keys.Key(keys.Key._fromRSAComponents(n=int(5), e=int(3))._keyObject) dsa = keys.Key(self.dsaObj) self.assertTrue(rsa1 == rsa2) self.assertFalse(rsa1 == rsa3) @@ -735,8 +731,7 @@ def test_notEqual(self): """ rsa1 = keys.Key(self.rsaObj) rsa2 = keys.Key(self.rsaObj) - rsa3 = keys.Key( - keys.Key._fromRSAComponents(n=int(5), e=int(3))._keyObject) + rsa3 = keys.Key(keys.Key._fromRSAComponents(n=int(5), e=int(3))._keyObject) dsa = keys.Key(self.dsaObj) self.assertFalse(rsa1 != rsa2) self.assertTrue(rsa1 != rsa3) @@ -748,10 +743,10 @@ def test_type(self): """ Test that the type method returns the correct type for an object. """ - self.assertEqual(keys.Key(self.rsaObj).type(), 'RSA') - self.assertEqual(keys.Key(self.rsaObj).sshType(), b'ssh-rsa') - self.assertEqual(keys.Key(self.dsaObj).type(), 'DSA') - self.assertEqual(keys.Key(self.dsaObj).sshType(), b'ssh-dss') + self.assertEqual(keys.Key(self.rsaObj).type(), "RSA") + self.assertEqual(keys.Key(self.rsaObj).sshType(), b"ssh-rsa") + self.assertEqual(keys.Key(self.dsaObj).type(), "DSA") + self.assertEqual(keys.Key(self.dsaObj).sshType(), b"ssh-dss") self.assertRaises(RuntimeError, keys.Key(None).type) self.assertRaises(RuntimeError, keys.Key(None).sshType) self.assertRaises(RuntimeError, keys.Key(self).type) @@ -764,37 +759,35 @@ def test_generate_no_key_type(self): with self.assertRaises(KeyCertException) as context: Key.generate(key_type=None) - self.assertEqual( - 'Unknown key type "not-specified".', context.exception.message) + self.assertEqual('Unknown key type "not-specified".', context.exception.message) def test_generate_unknown_type(self): """ An error is raised when generating a key with unknown type. """ with self.assertRaises(KeyCertException) as context: - Key.generate(key_type='bad-type') + Key.generate(key_type="bad-type") - self.assertEqual( - 'Unknown key type "bad-type".', context.exception.message) + self.assertEqual('Unknown key type "bad-type".', context.exception.message) - @attr('slow') + @attr("slow") def test_generate_rsa(self): """ Check generation of an RSA key with a case insensitive type name. """ - key = Key.generate(key_type='rSA', key_size=1024) + key = Key.generate(key_type="rSA", key_size=1024) - self.assertEqual('RSA', key.type()) + self.assertEqual("RSA", key.type()) self.assertEqual(1024, key.size()) - @attr('slow') + @attr("slow") def test_generate_dsa(self): """ Check generation of a DSA key with a case insensitive type name. """ - key = Key.generate(key_type='dSA', key_size=1024) + key = Key.generate(key_type="dSA", key_size=1024) - self.assertEqual('DSA', key.type()) + self.assertEqual("DSA", key.type()) self.assertEqual(1024, key.size()) def test_generate_failed(self): @@ -802,12 +795,12 @@ def test_generate_failed(self): A ServerError is raised when it fails to generate the key. """ with self.assertRaises(KeyCertException) as context: - Key.generate(key_type='dSa', key_size=512) + Key.generate(key_type="dSa", key_size=512) self.assertEqual( - 'Wrong key size "512". ' - 'Key size must be 1024, 2048, 3072, or 4096 bits.', - context.exception.message) + 'Wrong key size "512". ' "Key size must be 1024, 2048, 3072, or 4096 bits.", + context.exception.message, + ) def test_guessStringType_unknown(self): """ @@ -824,56 +817,50 @@ def test_guessStringType_X509_PEM_certificate(self): PEM certificates are recognized as public keys. """ content = ( - b'-----BEGIN CERTIFICATE-----\n' - b'CONTENT\n' - b'-----END CERTIFICATE-----\n' - ) + b"-----BEGIN CERTIFICATE-----\n" b"CONTENT\n" b"-----END CERTIFICATE-----\n" + ) result = Key._guessStringType(content) - self.assertEqual('public_x509_certificate', result) + self.assertEqual("public_x509_certificate", result) def test_guessStringType_X509_PUBLIC(self): """ x509 public PEM are recognized as public keys. """ content = ( - b'-----BEGIN PUBLIC KEY-----\n' - b'CONTENT\n' - b'-----END PUBLIC KEY-----\n' - ) + b"-----BEGIN PUBLIC KEY-----\n" b"CONTENT\n" b"-----END PUBLIC KEY-----\n" + ) result = Key._guessStringType(content) - self.assertEqual('public_x509', result) + self.assertEqual("public_x509", result) def test_guessStringType_PKCS8_PRIVATE(self): """ PKS#8 private PEM are recognized as private keys. """ content = ( - b'-----BEGIN PRIVATE KEY-----\n' - b'CONTENT\n' - b'-----END PRIVATE KEY-----\n' - ) + b"-----BEGIN PRIVATE KEY-----\n" b"CONTENT\n" b"-----END PRIVATE KEY-----\n" + ) result = Key._guessStringType(content) - self.assertEqual('private_pkcs8', result) + self.assertEqual("private_pkcs8", result) def test_guessStringType_PKCS8_PRIVATE_ENCRYPTED(self): """ PKS#8 encrypted private PEM are recognized as private keys. """ content = ( - b'-----BEGIN ENCRYPTED PRIVATE KEY-----\n' - b'CONTENT\n' - b'-----END ENCRYPTED PRIVATE KEY-----\n' - ) + b"-----BEGIN ENCRYPTED PRIVATE KEY-----\n" + b"CONTENT\n" + b"-----END ENCRYPTED PRIVATE KEY-----\n" + ) result = Key._guessStringType(content) - self.assertEqual('private_encrypted_pkcs8', result) + self.assertEqual("private_encrypted_pkcs8", result) def test_guessStringType_private_OpenSSH_RSA(self): """ @@ -881,7 +868,7 @@ def test_guessStringType_private_OpenSSH_RSA(self): """ result = Key._guessStringType(OPENSSH_RSA_PRIVATE) - self.assertEqual('private_openssh', result) + self.assertEqual("private_openssh", result) def test_guessStringType_private_OpenSSH_DSA(self): """ @@ -889,7 +876,7 @@ def test_guessStringType_private_OpenSSH_DSA(self): """ result = Key._guessStringType(OPENSSH_DSA_PRIVATE) - self.assertEqual('private_openssh', result) + self.assertEqual("private_openssh", result) def test_guessStringType_public_OpenSSH(self): """ @@ -897,7 +884,7 @@ def test_guessStringType_public_OpenSSH(self): """ result = Key._guessStringType(OPENSSH_RSA_PUBLIC) - self.assertEqual('public_openssh', result) + self.assertEqual("public_openssh", result) def test_guessStringType_public_PKCS1(self): """ @@ -905,16 +892,15 @@ def test_guessStringType_public_PKCS1(self): """ result = Key._guessStringType(PKCS1_RSA_PUBLIC) - self.assertEqual('public_pkcs1_rsa', result) + self.assertEqual("public_pkcs1_rsa", result) def test_guessStringType_private_SSHCOM(self): """ Can recognize an SSH.com private key. """ - result = Key._guessStringType( - SSHCOM_RSA_PRIVATE_NO_PASSWORD) + result = Key._guessStringType(SSHCOM_RSA_PRIVATE_NO_PASSWORD) - self.assertEqual('private_sshcom', result) + self.assertEqual("private_sshcom", result) def test_guessStringType_public_SSHCOM(self): """ @@ -922,24 +908,23 @@ def test_guessStringType_public_SSHCOM(self): """ result = Key._guessStringType(SSHCOM_RSA_PUBLIC) - self.assertEqual('public_sshcom', result) + self.assertEqual("public_sshcom", result) def test_guessStringType_putty(self): """ Can recognize a Putty private key. """ - result = Key._guessStringType( - PUTTY_RSA_PRIVATE_NO_PASSWORD) + result = Key._guessStringType(PUTTY_RSA_PRIVATE_NO_PASSWORD) - self.assertEqual('private_putty', result) + self.assertEqual("private_putty", result) def test_getKeyFormat_unknown(self): """ Inform using a human readable text that format is not known. """ - result = Key.getKeyFormat(b'no-such-format') + result = Key.getKeyFormat(b"no-such-format") - self.assertEqual('Unknown format', result) + self.assertEqual("Unknown format", result) def test_getKeyFormat_known(self): """ @@ -948,7 +933,7 @@ def test_getKeyFormat_known(self): result = Key.getKeyFormat(SSHCOM_RSA_PUBLIC) - self.assertEqual('SSH.com Public', result) + self.assertEqual("SSH.com Public", result) def test_public_get(self): """ @@ -962,36 +947,35 @@ def test_public_get(self): self.assertFalse(sut.isPublic()) self.assertIsInstance(Key, result) self.assertTrue(result.isPublic()) - self.assertEqual(result.data()['e'], sut.data()['e']) - self.assertEqual(result.data()['n'], sut.data()['n']) + self.assertEqual(result.data()["e"], sut.data()["e"]) + self.assertEqual(result.data()["n"], sut.data()["n"]) def test_fromFile(self): """ Test that fromFile works correctly. """ - self.test_segments = mk.fs.createFileInTemp( - content=keydata.privateRSA_openssh) + self.test_segments = mk.fs.createFileInTemp(content=keydata.privateRSA_openssh) key_path = mk.fs.getRealPathFromSegments(self.test_segments) self.assertEqual( - keys.Key.fromFile(key_path), - keys.Key.fromString(keydata.privateRSA_openssh)) + keys.Key.fromFile(key_path), keys.Key.fromString(keydata.privateRSA_openssh) + ) - self.assertRaises( - keys.BadKeyError, keys.Key.fromFile, key_path, 'bad_type') + self.assertRaises(keys.BadKeyError, keys.Key.fromFile, key_path, "bad_type") def test_fromString_type_unkwown(self): """ An exceptions is raised when reading a key for which type could not be detected. Exception only contains the beginning of the content. """ - content = 'some-value-' * 100 + content = "some-value-" * 100 self.assertBadKey( content, - 'Cannot guess the type for "b\'some-value-' - 'some-value-some-value-some-value-some-value-some-' - 'value-some-value-som\'"') + "Cannot guess the type for \"b'some-value-" + "some-value-some-value-some-value-some-value-some-" + "value-some-value-som'\"", + ) def test_fromString_struct_errors(self): """ @@ -1005,25 +989,26 @@ def test_fromString_errors(self): """ keys.Key.fromString should raise BadKeyError when the key is invalid. """ - self.assertRaises(keys.BadKeyError, keys.Key.fromString, '') + self.assertRaises(keys.BadKeyError, keys.Key.fromString, "") # no key data with a bad key type - self.assertRaises( - keys.BadKeyError, keys.Key.fromString, '', 'bad_type') + self.assertRaises(keys.BadKeyError, keys.Key.fromString, "", "bad_type") # trying to decrypt a key which doesn't support encryption self.assertRaises( keys.BadKeyError, keys.Key.fromString, - keydata.publicRSA_lsh, passphrase=b'unencrypted') + keydata.publicRSA_lsh, + passphrase=b"unencrypted", + ) # trying t fo decrypt a key with the wrong passphrase self.assertRaises( keys.EncryptedKeyError, keys.Key.fromString, - keys.Key(self.rsaObj).toString('openssh', b'encrypted')) + keys.Key(self.rsaObj).toString("openssh", b"encrypted"), + ) # key with no key data self.assertRaises( - keys.BadKeyError, - keys.Key.fromString, - '-----BEGIN RSA KEY-----\nwA==\n') + keys.BadKeyError, keys.Key.fromString, "-----BEGIN RSA KEY-----\nwA==\n" + ) # key with invalid DEK Info self.assertRaises( keys.BadKeyError, @@ -1057,10 +1042,13 @@ def test_fromString_errors(self): SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase=b'encrypted') +-----END RSA PRIVATE KEY-----""", + passphrase=b"encrypted", + ) # key with invalid encryption type self.assertRaises( - keys.BadKeyError, keys.Key.fromString, + keys.BadKeyError, + keys.Key.fromString, """-----BEGIN ENCRYPTED RSA KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: FOO-123-BAR,01234567 @@ -1090,10 +1078,13 @@ def test_fromString_errors(self): SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase=b'encrypted') +-----END RSA PRIVATE KEY-----""", + passphrase=b"encrypted", + ) # key with bad IV (AES) self.assertRaises( - keys.BadKeyError, keys.Key.fromString, + keys.BadKeyError, + keys.Key.fromString, """-----BEGIN ENCRYPTED RSA KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-128-CBC,01234 @@ -1123,10 +1114,13 @@ def test_fromString_errors(self): SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase=b'encrypted') +-----END RSA PRIVATE KEY-----""", + passphrase=b"encrypted", + ) # key with bad IV (DES3) self.assertRaises( - keys.BadKeyError, keys.Key.fromString, + keys.BadKeyError, + keys.Key.fromString, """-----BEGIN ENCRYPTED RSA KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: DES-EDE3-CBC,01234 @@ -1156,26 +1150,27 @@ def test_fromString_errors(self): SEJVJ+gmTKdRLYORJKyqhDet6g7kAxs4EoJ25WsOnX5nNr00rit+NkMPA7xbJT+7 CfI51GQLw7pUPeO2WNt6yZO/YkzZrqvTj5FEwybkUyBv7L0gkqu9wjfDdUw0fVHE xEm4DxjEoaIp8dW/JOzXQ2EF+WaSOgdYsw3Ac+rnnjnNptCdOEDGP6QBkt+oXj4P ------END RSA PRIVATE KEY-----""", passphrase=b'encrypted') +-----END RSA PRIVATE KEY-----""", + passphrase=b"encrypted", + ) def test_toStringErrors(self): """ Test that toString raises errors appropriately. """ - self.assertRaises( - keys.BadKeyError, keys.Key(self.rsaObj).toString, 'bad_type') + self.assertRaises(keys.BadKeyError, keys.Key(self.rsaObj).toString, "bad_type") def test_fromString_BLOB_blob_type_non_ascii(self): """ Raise with printable information for the bad type, even if blob type has non-ascii data. """ - badBlob = common.NS('ssh-\xbd\xbd\xbd') + badBlob = common.NS("ssh-\xbd\xbd\xbd") self.assertBadKey( badBlob, - 'Cannot guess the type for ' - '"b\'\\x00\\x00\\x00\\nssh-\\xc2\\xbd\\xc2\\xbd\\xc2\\xbd\'"' - ) + "Cannot guess the type for " + "\"b'\\x00\\x00\\x00\\nssh-\\xc2\\xbd\\xc2\\xbd\\xc2\\xbd'\"", + ) def test_blobRSA(self): """ @@ -1183,10 +1178,10 @@ def test_blobRSA(self): """ self.assertEqual( keys.Key(self.rsaObj).blob(), - common.NS(b'ssh-rsa') + - common.MP(self.rsaObj.private_numbers().public_numbers.e) + - common.MP(self.rsaObj.private_numbers().public_numbers.n) - ) + common.NS(b"ssh-rsa") + + common.MP(self.rsaObj.private_numbers().public_numbers.e) + + common.MP(self.rsaObj.private_numbers().public_numbers.n), + ) def test_blobDSA(self): """ @@ -1196,12 +1191,12 @@ def test_blobDSA(self): self.assertEqual( keys.Key(self.dsaObj).blob(), - common.NS(b'ssh-dss') + - common.MP(publicNumbers.parameter_numbers.p) + - common.MP(publicNumbers.parameter_numbers.q) + - common.MP(publicNumbers.parameter_numbers.g) + - common.MP(publicNumbers.y) - ) + common.NS(b"ssh-dss") + + common.MP(publicNumbers.parameter_numbers.p) + + common.MP(publicNumbers.parameter_numbers.q) + + common.MP(publicNumbers.parameter_numbers.g) + + common.MP(publicNumbers.y), + ) def test_blobEC(self): """ @@ -1212,17 +1207,18 @@ def test_blobEC(self): byteLength = (self.ecObj.curve.key_size + 7) // 8 self.assertEqual( keys.Key(self.ecObj).blob(), - common.NS(keydata.ECDatanistp256['curve']) + - common.NS(keydata.ECDatanistp256['curve'][-8:]) + - common.NS( - b'\x04' + - utils.int_to_bytes( + common.NS(keydata.ECDatanistp256["curve"]) + + common.NS(keydata.ECDatanistp256["curve"][-8:]) + + common.NS( + b"\x04" + + utils.int_to_bytes( self.ecObj.private_numbers().public_numbers.x, byteLength - ) + - utils.int_to_bytes( - self.ecObj.private_numbers().public_numbers.y, byteLength) ) - ) + + utils.int_to_bytes( + self.ecObj.private_numbers().public_numbers.y, byteLength + ) + ), + ) def test_blobEd25519(self): """ @@ -1231,15 +1227,13 @@ def test_blobEd25519(self): from cryptography.hazmat.primitives import serialization publicBytes = self.ed25519Obj.public_key().public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw - ) + serialization.Encoding.Raw, serialization.PublicFormat.Raw + ) self.assertEqual( keys.Key(self.ed25519Obj).blob(), - common.NS(b'ssh-ed25519') + - common.NS(publicBytes) - ) + common.NS(b"ssh-ed25519") + common.NS(publicBytes), + ) def test_blobNoKey(self): """ @@ -1258,14 +1252,14 @@ def test_privateBlobRSA(self): numbers = self.rsaObj.private_numbers() self.assertEqual( keys.Key(self.rsaObj).privateBlob(), - common.NS(b'ssh-rsa') + - common.MP(numbers.public_numbers.n) + - common.MP(numbers.public_numbers.e) + - common.MP(numbers.d) + - common.MP(numbers.iqmp) + - common.MP(numbers.p) + - common.MP(numbers.q) - ) + common.NS(b"ssh-rsa") + + common.MP(numbers.public_numbers.n) + + common.MP(numbers.public_numbers.e) + + common.MP(numbers.d) + + common.MP(numbers.iqmp) + + common.MP(numbers.p) + + common.MP(numbers.q), + ) def test_privateBlobDSA(self): """ @@ -1276,13 +1270,13 @@ def test_privateBlobDSA(self): self.assertEqual( keys.Key(self.dsaObj).privateBlob(), - common.NS(b'ssh-dss') + - common.MP(publicNumbers.parameter_numbers.p) + - common.MP(publicNumbers.parameter_numbers.q) + - common.MP(publicNumbers.parameter_numbers.g) + - common.MP(publicNumbers.y) + - common.MP(self.dsaObj.private_numbers().x) - ) + common.NS(b"ssh-dss") + + common.MP(publicNumbers.parameter_numbers.p) + + common.MP(publicNumbers.parameter_numbers.q) + + common.MP(publicNumbers.parameter_numbers.g) + + common.MP(publicNumbers.y) + + common.MP(self.dsaObj.private_numbers().x), + ) def test_privateBlobEC(self): """ @@ -1290,16 +1284,19 @@ def test_privateBlobEC(self): private key. """ from cryptography.hazmat.primitives import serialization + self.assertEqual( keys.Key(self.ecObj).privateBlob(), - common.NS(keydata.ECDatanistp256['curve']) + - common.NS(keydata.ECDatanistp256['curve'][-8:]) + - common.NS( + common.NS(keydata.ECDatanistp256["curve"]) + + common.NS(keydata.ECDatanistp256["curve"][-8:]) + + common.NS( self.ecObj.public_key().public_bytes( serialization.Encoding.X962, - serialization.PublicFormat.UncompressedPoint)) + - common.MP(self.ecObj.private_numbers().private_value) + serialization.PublicFormat.UncompressedPoint, + ) ) + + common.MP(self.ecObj.private_numbers().private_value), + ) def test_privateBlobEd25519(self): """ @@ -1307,22 +1304,22 @@ def test_privateBlobEd25519(self): Ed25519 private key. """ from cryptography.hazmat.primitives import serialization + publicBytes = self.ed25519Obj.public_key().public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw - ) + serialization.Encoding.Raw, serialization.PublicFormat.Raw + ) privateBytes = self.ed25519Obj.private_bytes( serialization.Encoding.Raw, serialization.PrivateFormat.Raw, - serialization.NoEncryption() - ) + serialization.NoEncryption(), + ) self.assertEqual( keys.Key(self.ed25519Obj).privateBlob(), - common.NS(b'ssh-ed25519') + - common.NS(publicBytes) + - common.NS(privateBytes + publicBytes) - ) + common.NS(b"ssh-ed25519") + + common.NS(publicBytes) + + common.NS(privateBytes + publicBytes), + ) def test_privateBlobNoKeyObject(self): """ @@ -1352,13 +1349,13 @@ def test_fromString_PUBLIC_OPENSSH_RSA_too_short(self): """ An exception is raised when public RSA OpenSSH key is bad formatted. """ - self.assertKeyIsTooShort('ssh-rsa') + self.assertKeyIsTooShort("ssh-rsa") def test_fromString_PUBLIC_OPENSSH_invalid_payload(self): """ Raise an exception when key blob has a bad format. """ - self.assertKeyParseError('ssh-rsa AAAAB3NzaC1yc2EA') + self.assertKeyParseError("ssh-rsa AAAAB3NzaC1yc2EA") def test_fromString_PUBLIC_OPENSSH_DSA(self): """ @@ -1384,9 +1381,9 @@ def test_fromString_OpenSSH_private_missing_password(self): keys.Key.fromString(keydata.privateRSA_openssh_encrypted) self.assertEqual( - 'Passphrase must be provided for an encrypted key', + "Passphrase must be provided for an encrypted key", context.exception.message, - ) + ) def test_fromString_PRIVATE_OPENSSH_with_whitespace(self): """ @@ -1406,20 +1403,23 @@ def test_fromString_PRIVATE_OPENSSH_with_whitespace(self): /ow0IqSj0VF72VJN9uSoPpFd4lLT0zN8v42RWja0M8ohWNf+YNJluPgCFE0PT4Vm SUrCyZXsNh6VXwjs3gKQ -----END DSA PRIVATE KEY-----""" - self.assertEqual(keys.Key.fromString(privateDSAData), - keys.Key.fromString(privateDSAData + '\n')) + self.assertEqual( + keys.Key.fromString(privateDSAData), + keys.Key.fromString(privateDSAData + "\n"), + ) def test_fromString_PRIVATE_OPENSSH_newer(self): """ Newer versions of OpenSSH generate encrypted keys which have a longer IV than the older versions. These newer keys are also loaded. """ - key = keys.Key.fromString(keydata.privateRSA_openssh_encrypted_aes, - passphrase=b'testxp') - self.assertEqual(key.type(), 'RSA') + key = keys.Key.fromString( + keydata.privateRSA_openssh_encrypted_aes, passphrase=b"testxp" + ) + self.assertEqual(key.type(), "RSA") key2 = keys.Key.fromString( - keydata.privateRSA_openssh_encrypted_aes + b'\n', - passphrase=b'testxp') + keydata.privateRSA_openssh_encrypted_aes + b"\n", passphrase=b"testxp" + ) self.assertEqual(key, key2) def test_toString_OPENSSH_rsa(self): @@ -1428,10 +1428,10 @@ def test_toString_OPENSSH_rsa(self): """ key = Key.fromString(OPENSSH_V1_RSA_PRIVATE) - result = key.public().toString('openssh') + result = key.public().toString("openssh") self.assertEqual(OPENSSH_RSA_PUBLIC, result) - result = key.toString('openssh') + result = key.toString("openssh") self.assertEqual(OPENSSH_RSA_PRIVATE, result) def test_toString_OPENSSH_v1_rsa(self): @@ -1440,14 +1440,13 @@ def test_toString_OPENSSH_v1_rsa(self): """ key = Key.fromString(OPENSSH_RSA_PRIVATE) - result = key.public().toString('openssh_v1') + result = key.public().toString("openssh_v1") self.assertEqual(OPENSSH_RSA_PUBLIC, result) - result = key.toString('openssh_v1') + result = key.toString("openssh_v1") self.assertStartsWith( - b'-----BEGIN OPENSSH PRIVATE KEY-----\n' - b'b3BlbnNzaC1rZXk', - result) + b"-----BEGIN OPENSSH PRIVATE KEY-----\n" b"b3BlbnNzaC1rZXk", result + ) reloaded = Key.fromString(result) self.assertEqual(reloaded, key) @@ -1459,10 +1458,10 @@ def addSSHCOMKeyHeaders(self, source, headers): """ lines = source.splitlines() for key, value in headers.items(): - line = '{}: {}'.format(key, value) - header = '\\\n'.join(textwrap.wrap(line, 70)) - lines.insert(1, header.encode('utf-8')) - return b'\n'.join(lines) + line = "{}: {}".format(key, value) + header = "\\\n".join(textwrap.wrap(line, 70)) + lines.insert(1, header.encode("utf-8")) + return b"\n".join(lines) def checkParsedDSAPublic1024(self, sut): """ @@ -1471,7 +1470,7 @@ def checkParsedDSAPublic1024(self, sut): This is a shared test for parsing DSA key from various formats. """ self.assertEqual(1024, sut.size()) - self.assertEqual('DSA', sut.type()) + self.assertEqual("DSA", sut.type()) self.assertTrue(sut.isPublic()) self.checkParsedDSAPublic1024Data(sut) @@ -1480,50 +1479,59 @@ def checkParsedDSAPublic1024Data(self, sut): Check the public part values for the default DSA key of size 1024. """ data = sut.data() - self.assertEqual(int( - '89826398702575694025672739759021185748719093895775418981133245507' - '56542191015877768589699407493932539140865803919573940821357868468' - '55675657634384222748339103943127442354510383477300256462657784441' - '71019786268219332779725063911288445634960873466719023048095246499' - '763675183656402590703132265805882271082319033570'), - data['y']) - self.assertEqual(int( - '14519098631088118929874535941241101897542246758347965800832728196' - '81139199597265476885338795620826004398884602230901691384070382776' - '92982149652731866793940314712388781003443391479314606037340161379' - '86631331044475413634865132557582890274917465191550388575486379853' - '0603422003777150811982254140040687593424378397517'), - data['p']) self.assertEqual( - int('765629040155792319453907037659138573169171493193'), - data['q']) - self.assertEqual(int( - '64647318098084998690447943642968245369499209364165550549740815561' - '71156388976417089337555666453157891497405105710031098879473402131' - '15408225147127626829407642540707192214402604495716677723330515779' - '34611656548484464881147166978432509157365635746874869548636130785' - '946819310836368885242376237240564866586977240572'), - data['g']) + int( + "89826398702575694025672739759021185748719093895775418981133245507" + "56542191015877768589699407493932539140865803919573940821357868468" + "55675657634384222748339103943127442354510383477300256462657784441" + "71019786268219332779725063911288445634960873466719023048095246499" + "763675183656402590703132265805882271082319033570" + ), + data["y"], + ) + self.assertEqual( + int( + "14519098631088118929874535941241101897542246758347965800832728196" + "81139199597265476885338795620826004398884602230901691384070382776" + "92982149652731866793940314712388781003443391479314606037340161379" + "86631331044475413634865132557582890274917465191550388575486379853" + "0603422003777150811982254140040687593424378397517" + ), + data["p"], + ) + self.assertEqual( + int("765629040155792319453907037659138573169171493193"), data["q"] + ) + self.assertEqual( + int( + "64647318098084998690447943642968245369499209364165550549740815561" + "71156388976417089337555666453157891497405105710031098879473402131" + "15408225147127626829407642540707192214402604495716677723330515779" + "34611656548484464881147166978432509157365635746874869548636130785" + "946819310836368885242376237240564866586977240572" + ), + data["g"], + ) def checkParsedDSAPrivate1024(self, sut): """ Check the default private DSA key of size 1024. """ self.assertEqual(1024, sut.size()) - self.assertEqual('DSA', sut.type()) + self.assertEqual("DSA", sut.type()) self.assertFalse(sut.isPublic()) data = sut.data() self.checkParsedDSAPublic1024Data(sut) - self.assertEqual(int( - '447059752886431435417087644871194130561824720094'), - data['x']) + self.assertEqual( + int("447059752886431435417087644871194130561824720094"), data["x"] + ) def checkParsedRSAPublic1024(self, sut): """ Check the default public RSA key of size 1024. """ self.assertEqual(1024, sut.size()) - self.assertEqual('RSA', sut.type()) + self.assertEqual("RSA", sut.type()) self.assertTrue(sut.isPublic()) self.checkParsedRSAPublic1024Data(sut) @@ -1532,51 +1540,66 @@ def checkParsedRSAPublic1024Data(self, sut): Check data for public RSA key of size 1024. """ data = sut.data() - self.assertEqual(65537, data['e']) - self.assertEqual(int( - '12955309129371696361961156024018278506192853914566590418922947244' - '33008028380639675460754206681134187533029942882729688747039044313' - '67411245192523108247958392655021595783971049572916657240822239036' - '02442387266290082476044614892594356080524766995335587624348179950' - '6405887692619349988915280409504938876523941259567'), - data['n']) + self.assertEqual(65537, data["e"]) + self.assertEqual( + int( + "12955309129371696361961156024018278506192853914566590418922947244" + "33008028380639675460754206681134187533029942882729688747039044313" + "67411245192523108247958392655021595783971049572916657240822239036" + "02442387266290082476044614892594356080524766995335587624348179950" + "6405887692619349988915280409504938876523941259567" + ), + data["n"], + ) def checkParsedRSAPrivate1024(self, sut): """ Check the default private RSA key of size 1024. """ self.assertEqual(1024, sut.size()) - self.assertEqual('RSA', sut.type()) - self.assertEqual(b'ssh-rsa', sut.sshType()) + self.assertEqual("RSA", sut.type()) + self.assertEqual(b"ssh-rsa", sut.sshType()) self.assertEqual( - 'fc:39:4c:d4:51:c8:5d:78:1e:4d:9d:1e:73:42:52:55', - sut.fingerprint()) + "fc:39:4c:d4:51:c8:5d:78:1e:4d:9d:1e:73:42:52:55", sut.fingerprint() + ) self.assertIsFalse(sut.isPublic()) data = sut.data() - self.assertEqual(65537, data['e']) + self.assertEqual(65537, data["e"]) self.checkParsedRSAPublic1024Data(sut) - self.assertEqual(int( - '57010713839675255669157840568333483699071044890077432241594488384' - '64981848192265169337649163172545274951948296799964023904757013291' - '17313931268194522463817291948793747715146018146051093951466872189' - '64147610108577761761364098616952641696814228146724216997423652825' - '24517268536277980834876649127946895862158846465'), - data['d']) - self.assertEqual(int( - '10661640454627350493191065484215149934251067848734449698668476614' - '18981319570111200535213963399376281314470995958266981264747210946' - '6364885923117389812635119'), - data['q']) - self.assertEqual(int( - '12151328104249520956550929707892880056509323657595783040548358917' - '98785549316902458371621691657702435263762556929800891556172971312' - '6473919204485168003686593'), - data['p']) - self.assertEqual(int( - '48025268260110814473325498559726067155427614012608550802573547885' - '48894562354231797601376827466469492368471033644629931755771678685' - '474342157953188378164913'), - data['u']) + self.assertEqual( + int( + "57010713839675255669157840568333483699071044890077432241594488384" + "64981848192265169337649163172545274951948296799964023904757013291" + "17313931268194522463817291948793747715146018146051093951466872189" + "64147610108577761761364098616952641696814228146724216997423652825" + "24517268536277980834876649127946895862158846465" + ), + data["d"], + ) + self.assertEqual( + int( + "10661640454627350493191065484215149934251067848734449698668476614" + "18981319570111200535213963399376281314470995958266981264747210946" + "6364885923117389812635119" + ), + data["q"], + ) + self.assertEqual( + int( + "12151328104249520956550929707892880056509323657595783040548358917" + "98785549316902458371621691657702435263762556929800891556172971312" + "6473919204485168003686593" + ), + data["p"], + ) + self.assertEqual( + int( + "48025268260110814473325498559726067155427614012608550802573547885" + "48894562354231797601376827466469492368471033644629931755771678685" + "474342157953188378164913" + ), + data["u"], + ) def test_fromString_PUBLIC_SSHCOM_RSA_no_headers(self): """ @@ -1593,18 +1616,18 @@ def test_fromString_PUBLIC_SSHCOM_RSA_public_headers(self): key_content = self.addSSHCOMKeyHeaders( source=SSHCOM_RSA_PUBLIC, headers={ - 'Comment': '"short comment"', - 'Subject': 'Very very long subject' * 10, - 'x-private': mk.string(), - }, - ) + "Comment": '"short comment"', + "Subject": "Very very long subject" * 10, + "x-private": mk.string(), + }, + ) sut = Key.fromString(key_content) self.assertEqual(1024, sut.size()) - self.assertEqual('RSA', sut.type()) + self.assertEqual("RSA", sut.type()) self.assertTrue(sut.isPublic()) data = sut.data() - self.assertEqual(65537, data['e']) + self.assertEqual(65537, data["e"]) def test_fromString_PUBLIC_SSHCOM_DSA(self): """ @@ -1618,13 +1641,13 @@ def test_fromString_PUBLIC_SSHCOM_no_end_tag(self): """ Raise an exception when there is no END tag. """ - content = '---- BEGIN SSH2 PUBLIC KEY ----' + content = "---- BEGIN SSH2 PUBLIC KEY ----" - self.assertBadKey(content, 'Fail to find END tag for SSH.com key.') + self.assertBadKey(content, "Fail to find END tag for SSH.com key.") - content = '---- BEGIN SSH2 PUBLIC KEY ----\nnext line' + content = "---- BEGIN SSH2 PUBLIC KEY ----\nnext line" - self.assertBadKey(content, 'Fail to find END tag for SSH.com key.') + self.assertBadKey(content, "Fail to find END tag for SSH.com key.") def test_fromString_PUBLIC_SSHCOM_RSA_invalid_payload(self): """ @@ -1642,7 +1665,7 @@ def test_toString_SSHCOM_RSA_public_no_headers(self): """ sut = Key.fromString(OPENSSH_RSA_PUBLIC) - result = sut.toString(type='sshcom') + result = sut.toString(type="sshcom") self.assertEqual(SSHCOM_RSA_PUBLIC, result) @@ -1653,12 +1676,12 @@ def test_toString_SSHCOM_RSA_public_with_comment(self): sut = Key.fromString(OPENSSH_RSA_PUBLIC) comment = mk.string() * 20 - result = sut.toString(type='sshcom', extra=comment) + result = sut.toString(type="sshcom", extra=comment) expected = self.addSSHCOMKeyHeaders( source=SSHCOM_RSA_PUBLIC, - headers={'Comment': '"%s"' % comment}, - ) + headers={"Comment": '"%s"' % comment}, + ) self.assertEqual(expected, result) def test_toString_SSHCOM_DSA_public(self): @@ -1667,7 +1690,7 @@ def test_toString_SSHCOM_DSA_public(self): """ sut = Key.fromString(OPENSSH_DSA_PUBLIC) - result = sut.toString(type='sshcom') + result = sut.toString(type="sshcom") self.assertEqual(SSHCOM_DSA_PUBLIC, result) @@ -1707,22 +1730,21 @@ def test_fromString_PRIVATE_OPENSSH_short(self): """ Raise an error when private OpenSSH key is too short. """ - content = '-----BEGIN RSA PRIVATE KEY-----' + content = "-----BEGIN RSA PRIVATE KEY-----" self.assertKeyIsTooShort(content) - content = '-----BEGIN RSA PRIVATE KEY-----\nAnother Line' + content = "-----BEGIN RSA PRIVATE KEY-----\nAnother Line" self.assertBadKey( - content, - 'Failed to decode key (Bad Passphrase?): ' - 'EndOfStreamError()') + content, "Failed to decode key (Bad Passphrase?): " "EndOfStreamError()" + ) def test_fromString_PRIVATE_OPENSSH_bad_encoding(self): """ Raise an error when private OpenSSH key data can not be decoded. """ - content = '-----BEGIN RSA PRIVATE KEY-----\nAnother Line\nLast' + content = "-----BEGIN RSA PRIVATE KEY-----\nAnother Line\nLast" self.assertKeyParseError(content) @@ -1733,11 +1755,9 @@ def test_fromString_PRIVATE_SSHCOM_unencrypted_with_passphrase(self): """ with self.assertRaises(BadKeyError) as context: - Key.fromString(SSHCOM_RSA_PRIVATE_NO_PASSWORD, passphrase=b'pass') + Key.fromString(SSHCOM_RSA_PRIVATE_NO_PASSWORD, passphrase=b"pass") - self.assertEqual( - 'SSH.com key not encrypted', - context.exception.message) + self.assertEqual("SSH.com key not encrypted", context.exception.message) def test_fromString_PRIVATE_SSHCOM_RSA_no_headers_no_password(self): """ @@ -1751,8 +1771,7 @@ def test_fromString_PRIVATE_SSHCOM_RSA_encrypted(self): """ Can load a private SSH.com key encrypted with password`. """ - sut = Key.fromString( - SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase=b'chevah') + sut = Key.fromString(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase=b"chevah") self.checkParsedRSAPrivate1024(sut) @@ -1768,11 +1787,11 @@ def test_fromString_PRIVATE_SSHCOM_short(self): """ Raise an exception when private key is too short. """ - content = '---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----' + content = "---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----" self.assertKeyParseError(content) - content = '---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\nnext line' + content = "---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\nnext line" self.assertKeyParseError(content) @@ -1785,8 +1804,9 @@ def test_fromString_PRIVATE_SSHCOM_RSA_encrypted_no_password(self): Key.fromString(SSHCOM_RSA_PRIVATE_WITH_PASSWORD) self.assertEqual( - 'Passphrase must be provided for an encrypted key.', - context.exception.message) + "Passphrase must be provided for an encrypted key.", + context.exception.message, + ) def test_fromString_PRIVATE_SSHCOM_RSA_with_wrong_password(self): """ @@ -1794,11 +1814,9 @@ def test_fromString_PRIVATE_SSHCOM_RSA_with_wrong_password(self): which is encrypted, but providing a wrong password. """ with self.assertRaises(EncryptedKeyError) as context: - Key.fromString(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase=b'on') + Key.fromString(SSHCOM_RSA_PRIVATE_WITH_PASSWORD, passphrase=b"on") - self.assertEqual( - 'Bad password or bad key format.', - context.exception.message) + self.assertEqual("Bad password or bad key format.", context.exception.message) def test_fromString_PRIVATE_OPENSSH_bad_magic(self): """ @@ -1808,8 +1826,7 @@ def test_fromString_PRIVATE_OPENSSH_bad_magic(self): B2/56wAAAi4AAAA3 ---- END SSH2 ENCRYPTED PRIVATE KEY ----""" - self.assertBadKey( - content, 'Fail to parse key content.') + self.assertBadKey(content, "Fail to parse key content.") def test_fromString_PRIVATE_OPENSSH_bad_key_type(self): """ @@ -1848,7 +1865,7 @@ def test_fromString_X509_PEM_invalid_format(self): self.assertStartsWith( "Failed to load certificate. \"[('asn1 encoding routines'", context.exception.message, - ) + ) def test_fromString_X509_PEM_EC(self): """ @@ -1866,8 +1883,8 @@ def test_fromString_X509_PEM_EC(self): """ result = Key.fromString(data) - self.assertEqual('EC', result.type()) - self.assertEqual(b'ecdsa-sha2-nistp192', result.sshType()) + self.assertEqual("EC", result.type()) + self.assertEqual(b"ecdsa-sha2-nistp192", result.sshType()) self.assertEqual(192, result.size()) def test_fromString_PKCS1_PUBLIC_ECDSA(self): @@ -1883,8 +1900,8 @@ def test_fromString_PKCS1_PUBLIC_ECDSA(self): """ result = Key.fromString(data) - self.assertEqual('EC', result.type()) - self.assertEqual(b'ecdsa-sha2-nistp192', result.sshType()) + self.assertEqual("EC", result.type()) + self.assertEqual(b"ecdsa-sha2-nistp192", result.sshType()) def test_fromString_X509_PEM_RSA(self): """ @@ -1910,19 +1927,19 @@ def test_fromString_X509_PEM_RSA(self): sut = Key.fromString(data) self.assertTrue(sut.isPublic()) - self.assertEqual('RSA', sut.type()) + self.assertEqual("RSA", sut.type()) self.assertEqual(1024, sut.size()) components = sut.data() - self.assertEqual(65537, components['e']) + self.assertEqual(65537, components["e"]) n = int( - '14510135000543456324610075074919561379239940215773254633039625814' - '50191438083097108908667737243399472490927083264564327600896049375' - '92092816317169486450111458914839337717035721053431064458247582292' - '33425907841901335798792724220900289242783534069221630733833594745' - '1002424312049140771718167143894887320401855011989' - ) - self.assertEqual(n, components['n']) + "14510135000543456324610075074919561379239940215773254633039625814" + "50191438083097108908667737243399472490927083264564327600896049375" + "92092816317169486450111458914839337717035721053431064458247582292" + "33425907841901335798792724220900289242783534069221630733833594745" + "1002424312049140771718167143894887320401855011989" + ) + self.assertEqual(n, components["n"]) def test_fromString_PKCS1_PUBLIC_PEM_invalid_format(self): """ @@ -1939,7 +1956,7 @@ def test_fromString_PKCS1_PUBLIC_PEM_invalid_format(self): self.assertStartsWith( "Failed to load PKCS#1 public key. \"[('DECODER routines'", context.exception.message, - ) + ) def test_fromString_PKCS1_PUBLIC_RSA(self): """ @@ -1958,19 +1975,19 @@ def test_fromString_PKCS1_PUBLIC_RSA(self): sut = Key.fromString(data) self.assertTrue(sut.isPublic()) - self.assertEqual('RSA', sut.type()) + self.assertEqual("RSA", sut.type()) self.assertEqual(1024, sut.size()) components = sut.data() - self.assertEqual(65537, components['e']) + self.assertEqual(65537, components["e"]) n = int( - '14510135000543456324610075074919561379239940215773254633039625814' - '50191438083097108908667737243399472490927083264564327600896049375' - '92092816317169486450111458914839337717035721053431064458247582292' - '33425907841901335798792724220900289242783534069221630733833594745' - '1002424312049140771718167143894887320401855011989' - ) - self.assertEqual(n, components['n']) + "14510135000543456324610075074919561379239940215773254633039625814" + "50191438083097108908667737243399472490927083264564327600896049375" + "92092816317169486450111458914839337717035721053431064458247582292" + "33425907841901335798792724220900289242783534069221630733833594745" + "1002424312049140771718167143894887320401855011989" + ) + self.assertEqual(n, components["n"]) def test_fromString_X509_PEM_DSA(self): """ @@ -1998,36 +2015,37 @@ def test_fromString_X509_PEM_DSA(self): sut = Key.fromString(data) self.assertTrue(sut.isPublic()) - self.assertEqual('DSA', sut.type()) + self.assertEqual("DSA", sut.type()) self.assertEqual(1024, sut.size()) components = sut.data() y = int( - '33608096932577498834618892325416552088960771123656082234885710486' - '75507586904443594643612585160476637613084634099891307779753871384' - '19072984388914093315900417736990449366567905225558889080164633948' - '75642330307431599331123161679260711587324602448450132263105327567' - '324900691359269978674482129301723462636106625693' - ) + "33608096932577498834618892325416552088960771123656082234885710486" + "75507586904443594643612585160476637613084634099891307779753871384" + "19072984388914093315900417736990449366567905225558889080164633948" + "75642330307431599331123161679260711587324602448450132263105327567" + "324900691359269978674482129301723462636106625693" + ) p = int( - '17914554197956231476032656039682646299975055883332311875135017227' - '52180243454588892360869849018970437236700881503241838175380166833' - '56570852141623851276212449051705325396966909384918507908491159872' - '81118556760058432354600693107636249903432532125207156471720334839' - '5401646777661899361981163845950810903143363602443' - ) + "17914554197956231476032656039682646299975055883332311875135017227" + "52180243454588892360869849018970437236700881503241838175380166833" + "56570852141623851276212449051705325396966909384918507908491159872" + "81118556760058432354600693107636249903432532125207156471720334839" + "5401646777661899361981163845950810903143363602443" + ) g = int( - '12935985053463672691492638315705405640647316377002915690069266627' - '73032720642846501430445126372712764104983906841935717997673558164' - '74657088881395785073303554687569602926262408886111665706815822813' - '14448994749901282518897434324098506093655990924057550618491224583' - '7106339202519842112263186663472095769544164572498' - ) - self.assertEqual(y, components['y']) - self.assertEqual(p, components['p']) - self.assertEqual(g, components['g']) + "12935985053463672691492638315705405640647316377002915690069266627" + "73032720642846501430445126372712764104983906841935717997673558164" + "74657088881395785073303554687569602926262408886111665706815822813" + "14448994749901282518897434324098506093655990924057550618491224583" + "7106339202519842112263186663472095769544164572498" + ) + self.assertEqual(y, components["y"]) + self.assertEqual(p, components["p"]) + self.assertEqual(g, components["g"]) self.assertEqual( - 732130160578857514768194964362219084190055012723, components['q']) + 732130160578857514768194964362219084190055012723, components["q"] + ) def test_fromString_PCKS1_PUBLIC_DSA(self): """ @@ -2052,36 +2070,37 @@ def test_fromString_PCKS1_PUBLIC_DSA(self): sut = Key.fromString(data) self.assertTrue(sut.isPublic()) - self.assertEqual('DSA', sut.type()) + self.assertEqual("DSA", sut.type()) self.assertEqual(1024, sut.size()) components = sut.data() y = int( - '33608096932577498834618892325416552088960771123656082234885710486' - '75507586904443594643612585160476637613084634099891307779753871384' - '19072984388914093315900417736990449366567905225558889080164633948' - '75642330307431599331123161679260711587324602448450132263105327567' - '324900691359269978674482129301723462636106625693' - ) + "33608096932577498834618892325416552088960771123656082234885710486" + "75507586904443594643612585160476637613084634099891307779753871384" + "19072984388914093315900417736990449366567905225558889080164633948" + "75642330307431599331123161679260711587324602448450132263105327567" + "324900691359269978674482129301723462636106625693" + ) p = int( - '17914554197956231476032656039682646299975055883332311875135017227' - '52180243454588892360869849018970437236700881503241838175380166833' - '56570852141623851276212449051705325396966909384918507908491159872' - '81118556760058432354600693107636249903432532125207156471720334839' - '5401646777661899361981163845950810903143363602443' - ) + "17914554197956231476032656039682646299975055883332311875135017227" + "52180243454588892360869849018970437236700881503241838175380166833" + "56570852141623851276212449051705325396966909384918507908491159872" + "81118556760058432354600693107636249903432532125207156471720334839" + "5401646777661899361981163845950810903143363602443" + ) g = int( - '12935985053463672691492638315705405640647316377002915690069266627' - '73032720642846501430445126372712764104983906841935717997673558164' - '74657088881395785073303554687569602926262408886111665706815822813' - '14448994749901282518897434324098506093655990924057550618491224583' - '7106339202519842112263186663472095769544164572498' - ) - self.assertEqual(y, components['y']) - self.assertEqual(p, components['p']) - self.assertEqual(g, components['g']) + "12935985053463672691492638315705405640647316377002915690069266627" + "73032720642846501430445126372712764104983906841935717997673558164" + "74657088881395785073303554687569602926262408886111665706815822813" + "14448994749901282518897434324098506093655990924057550618491224583" + "7106339202519842112263186663472095769544164572498" + ) + self.assertEqual(y, components["y"]) + self.assertEqual(p, components["p"]) + self.assertEqual(g, components["g"]) self.assertEqual( - 732130160578857514768194964362219084190055012723, components['q']) + 732130160578857514768194964362219084190055012723, components["q"] + ) def test_fromString_PRIVATE_PKCS8_invalid_format(self): """ @@ -2098,7 +2117,7 @@ def test_fromString_PRIVATE_PKCS8_invalid_format(self): self.assertStartsWith( "Failed to load PKCS#8 PEM. \"[('DECODER routines'", context.exception.message, - ) + ) def test_fromString_PRIVATE_PKCS8_RSA(self): """ @@ -2152,7 +2171,7 @@ def test_fromString_PRIVATE_PKCS8_RSA_ENCRYPTED(self): TbW5RErmC8ifa/J4NdCv7MY= -----END ENCRYPTED PRIVATE KEY----- """ - sut = Key.fromString(data, passphrase=b'password') + sut = Key.fromString(data, passphrase=b"password") self.checkParsedRSAPrivate1024(sut) @@ -2185,9 +2204,9 @@ def test_fromString_PRIVATE_PKCS8_ENCRYPTED_no_pass(self): Key.fromString(data) self.assertEqual( - 'Passphrase must be provided for an encrypted key', + "Passphrase must be provided for an encrypted key", context.exception.message, - ) + ) def test_fromString_PRIVATE_PKCS8_DSA(self): """ @@ -2224,8 +2243,8 @@ def test_fromString_PRIVATE_PKCS8_EC(self): """ result = Key.fromString(data) - self.assertEqual('EC', result.type()) - self.assertEqual(b'ecdsa-sha2-nistp256', result.sshType()) + self.assertEqual("EC", result.type()) + self.assertEqual(b"ecdsa-sha2-nistp256", result.sshType()) def test_toString_SSHCOM_RSA_private_without_encryption(self): """ @@ -2233,13 +2252,14 @@ def test_toString_SSHCOM_RSA_private_without_encryption(self): """ sut = Key.fromString(OPENSSH_RSA_PRIVATE) - result = sut.toString(type='sshcom') + result = sut.toString(type="sshcom") # Check that it looks like SSH.com private key. self.assertStartsWith( - b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\n' - b'P2/56wAAAi4AAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0', - result) + b"---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\n" + b"P2/56wAAAi4AAAA3aWYtbW9kbntzaWdue3JzYS1wa2NzMS1zaGExfSxlbmNyeXB0", + result, + ) # Load the serialized key and see that we get the same key. reloaded = Key.fromString(result) @@ -2251,16 +2271,17 @@ def test_toString_SSHCOM_RSA_private_encrypted(self): """ sut = Key.fromString(OPENSSH_RSA_PRIVATE) - result = sut.toString(type='sshcom', extra='chevah') + result = sut.toString(type="sshcom", extra="chevah") # Check that it looks like SSH.com private key. self.assertStartsWith( - b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\n' - b'P2/56wAAAjMAAAA3aWYtbW9kbntzaWdue3', - result) + b"---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\n" + b"P2/56wAAAjMAAAA3aWYtbW9kbntzaWdue3", + result, + ) # Load the serialized key and see that we get the same key. - reloaded = Key.fromString(result, passphrase=b'chevah') + reloaded = Key.fromString(result, passphrase=b"chevah") self.assertEqual(sut, reloaded) def test_toString_SSHCOM_DSA_private(self): @@ -2269,7 +2290,7 @@ def test_toString_SSHCOM_DSA_private(self): """ sut = Key.fromString(OPENSSH_DSA_PRIVATE) - result = sut.toString(type='sshcom') + result = sut.toString(type="sshcom") self.assertEqual(SSHCOM_DSA_PRIVATE_NO_PASSWORD, result) # Load the serialized key and see that we get the same key. @@ -2291,18 +2312,15 @@ def test_fromString_PRIVATE_PUTTY_not_encrypted_with_passphrase(self): will raise BadKeyError. """ with self.assertRaises(BadKeyError) as context: - Key.fromString(PUTTY_RSA_PRIVATE_NO_PASSWORD, passphrase=b'pass') + Key.fromString(PUTTY_RSA_PRIVATE_NO_PASSWORD, passphrase=b"pass") - self.assertEqual( - 'PuTTY key not encrypted', - context.exception.message) + self.assertEqual("PuTTY key not encrypted", context.exception.message) def test_fromString_PRIVATE_PUTTY_RSA_with_password(self): """ It can read private RSA keys in Putty format which are encrypted. """ - sut = Key.fromString( - PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase=b'chevah') + sut = Key.fromString(PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase=b"chevah") self.checkParsedRSAPrivate1024(sut) @@ -2310,22 +2328,19 @@ def test_fromString_PRIVATE_PUTTY_short(self): """ An exception is raised when key is too short. """ - content = 'PuTTY-User-Key-File-2: ssh-rsa' + content = "PuTTY-User-Key-File-2: ssh-rsa" self.assertKeyIsTooShort(content) - content = ( - 'PuTTY-User-Key-File-2: ssh-rsa\n' - 'Encryption: aes256-cbc\n' - ) + content = "PuTTY-User-Key-File-2: ssh-rsa\n" "Encryption: aes256-cbc\n" self.assertKeyIsTooShort(content) content = ( - 'PuTTY-User-Key-File-2: ssh-rsa\n' - 'Encryption: aes256-cbc\n' - 'Comment: bla\n' - ) + "PuTTY-User-Key-File-2: ssh-rsa\n" + "Encryption: aes256-cbc\n" + "Comment: bla\n" + ) self.assertKeyIsTooShort(content) @@ -2334,11 +2349,9 @@ def test_fromString_PRIVATE_PUTTY_RSA_bad_password(self): An exception is raised when password is not valid. """ with self.assertRaises(EncryptedKeyError) as context: - Key.fromString( - PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase=b'bad-pass') + Key.fromString(PUTTY_RSA_PRIVATE_WITH_PASSWORD, passphrase=b"bad-pass") - self.assertEqual( - 'Bad password or HMAC mismatch.', context.exception.message) + self.assertEqual("Bad password or HMAC mismatch.", context.exception.message) def test_fromString_PRIVATE_PUTTY_RSA_missing_password(self): """ @@ -2349,8 +2362,9 @@ def test_fromString_PRIVATE_PUTTY_RSA_missing_password(self): Key.fromString(PUTTY_RSA_PRIVATE_WITH_PASSWORD) self.assertEqual( - 'Passphrase must be provided for an encrypted key.', - context.exception.message) + "Passphrase must be provided for an encrypted key.", + context.exception.message, + ) def test_fromString_PRIVATE_PUTTY_unsupported_type(self): """ @@ -2359,8 +2373,7 @@ def test_fromString_PRIVATE_PUTTY_unsupported_type(self): content = """PuTTY-User-Key-File-2: ssh-bad IGNORED """ - self.assertBadKey( - content, 'Unsupported key type: "ssh-bad"') + self.assertBadKey(content, 'Unsupported key type: "ssh-bad"') def test_fromString_PRIVATE_PUTTY_unsupported_encryption(self): """ @@ -2371,8 +2384,7 @@ def test_fromString_PRIVATE_PUTTY_unsupported_encryption(self): Encryption: aes126-cbc IGNORED """ - self.assertBadKey( - content, 'Unsupported encryption type: "aes126-cbc"') + self.assertBadKey(content, 'Unsupported encryption type: "aes126-cbc"') def test_fromString_PRIVATE_PUTTY_type_mismatch(self): """ @@ -2391,10 +2403,8 @@ def test_fromString_PRIVATE_PUTTY_type_mismatch(self): """ self.assertBadKey( content, - ( - 'Mismatch key type. Header has "ssh-rsa",' - ' public has "ssh-dss"'), - ) + ('Mismatch key type. Header has "ssh-rsa",' ' public has "ssh-dss"'), + ) def test_fromString_PRIVATE_PUTTY_hmac_mismatch(self): """ @@ -2402,14 +2412,14 @@ def test_fromString_PRIVATE_PUTTY_hmac_mismatch(self): advertise by the key file. """ content = PUTTY_RSA_PRIVATE_NO_PASSWORD[:-1] - content += b'a' + content += b"a" self.assertBadKey( content, - 'HMAC mismatch: file declare ' + "HMAC mismatch: file declare " '"7630b86be300c6302ce1390fb264487bb61e67ca", actual is ' '"7630b86be300c6302ce1390fb264487bb61e67ce"', - ) + ) def test_fromString_PRIVATE_OpenSSH_DSA_no_password(self): """ @@ -2434,7 +2444,7 @@ def test_toString_PUTTY_RSA_plain(self): """ sut = Key.fromString(OPENSSH_RSA_PRIVATE) - result = sut.toString(type='putty') + result = sut.toString(type="putty") # We can not check the exact text as comment is hardcoded in # Twisted. @@ -2448,12 +2458,12 @@ def test_toString_PUTTY_RSA_encrypted(self): """ sut = Key.fromString(OPENSSH_RSA_PRIVATE) - result = sut.toString(type='putty', extra='write-pass') + result = sut.toString(type="putty", extra="write-pass") # We can not check the exact text as comment is hardcoded in # Twisted. # Load the serialized key and see that we get the same key. - reloaded = Key.fromString(result, passphrase='write-pass') + reloaded = Key.fromString(result, passphrase="write-pass") self.assertEqual(sut, reloaded) def test_toString_PUTTY_DSA_plain(self): @@ -2462,7 +2472,7 @@ def test_toString_PUTTY_DSA_plain(self): """ sut = Key.fromString(OPENSSH_DSA_PRIVATE) - result = sut.toString(type='putty') + result = sut.toString(type="putty") # We can not check the exact text as comment is hardcoded in # Twisted. @@ -2476,7 +2486,7 @@ def test_toString_PUTTY_public(self): """ sut = Key.fromString(OPENSSH_RSA_PRIVATE).public() - result = sut.toString(type='putty') + result = sut.toString(type="putty") reloaded = Key.fromString(result) self.assertEqual(sut, reloaded) @@ -2486,22 +2496,21 @@ def test_toString_AGENTV3(self): Test that the Key object generates Agent v3 keys correctly. """ key = keys.Key.fromString(keydata.privateRSA_openssh) - self.assertEqual(key.toString('agentv3'), keydata.privateRSA_agentv3) + self.assertEqual(key.toString("agentv3"), keydata.privateRSA_agentv3) key = keys.Key.fromString(keydata.privateDSA_openssh) - self.assertEqual(key.toString('agentv3'), keydata.privateDSA_agentv3) + self.assertEqual(key.toString("agentv3"), keydata.privateDSA_agentv3) def test_fromString_AGENTV3(self): """ Test that keys are correctly generated from Agent v3 strings. """ - self._testPrivateFromString( - keydata.privateRSA_agentv3, 'RSA', keydata.RSAData) - self._testPrivateFromString( - keydata.privateDSA_agentv3, 'DSA', keydata.DSAData) + self._testPrivateFromString(keydata.privateRSA_agentv3, "RSA", keydata.RSAData) + self._testPrivateFromString(keydata.privateDSA_agentv3, "DSA", keydata.DSAData) self.assertRaises( keys.BadKeyError, keys.Key.fromString, - '\x00\x00\x00\x07ssh-foo' + '\x00\x00\x00\x01\x01' * 5) + "\x00\x00\x00\x07ssh-foo" + "\x00\x00\x00\x01\x01" * 5, + ) def test_fingerprint(self): """ @@ -2521,10 +2530,12 @@ def test_fingerprintdefault(self): self.assertEqual( keys.Key(rsaObj).fingerprint(), - '85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da') + "85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da", + ) self.assertEqual( keys.Key(dsaObj).fingerprint(), - '63:15:b3:0e:e6:4f:50:de:91:48:3d:01:6b:b3:13:c1') + "63:15:b3:0e:e6:4f:50:de:91:48:3d:01:6b:b3:13:c1", + ) def test_fingerprint_md5_hex(self): """ @@ -2534,13 +2545,13 @@ def test_fingerprint_md5_hex(self): rsaObj, dsaObj = self._getKeysForFingerprintTest() self.assertEqual( - keys.Key(rsaObj).fingerprint( - keys.FingerprintFormats.MD5_HEX), - '85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da') + keys.Key(rsaObj).fingerprint(keys.FingerprintFormats.MD5_HEX), + "85:25:04:32:58:55:96:9f:57:ee:fb:a8:1a:ea:69:da", + ) self.assertEqual( - keys.Key(dsaObj).fingerprint( - keys.FingerprintFormats.MD5_HEX), - '63:15:b3:0e:e6:4f:50:de:91:48:3d:01:6b:b3:13:c1') + keys.Key(dsaObj).fingerprint(keys.FingerprintFormats.MD5_HEX), + "63:15:b3:0e:e6:4f:50:de:91:48:3d:01:6b:b3:13:c1", + ) def test_fingerprintsha256(self): """ @@ -2550,13 +2561,13 @@ def test_fingerprintsha256(self): rsaObj, dsaObj = self._getKeysForFingerprintTest() self.assertEqual( - keys.Key(rsaObj).fingerprint( - keys.FingerprintFormats.SHA256_BASE64), - 'FBTCOoknq0mHy+kpfnY9tDdcAJuWtCpuQMaV3EsvbUI=') + keys.Key(rsaObj).fingerprint(keys.FingerprintFormats.SHA256_BASE64), + "FBTCOoknq0mHy+kpfnY9tDdcAJuWtCpuQMaV3EsvbUI=", + ) self.assertEqual( - keys.Key(dsaObj).fingerprint( - keys.FingerprintFormats.SHA256_BASE64), - 'Wz5o2YbKyxOEcJn1au/UaALSVruUzfz0vaLI1xiIGyY=') + keys.Key(dsaObj).fingerprint(keys.FingerprintFormats.SHA256_BASE64), + "Wz5o2YbKyxOEcJn1au/UaALSVruUzfz0vaLI1xiIGyY=", + ) def test_fingerprintsha1(self): """ @@ -2566,13 +2577,13 @@ def test_fingerprintsha1(self): rsaObj, dsaObj = self._getKeysForFingerprintTest() self.assertEqual( - keys.Key(rsaObj).fingerprint( - keys.FingerprintFormats.SHA1_BASE64), - 'tuUFlgv3kknie9WYExgS7OQj54k=') + keys.Key(rsaObj).fingerprint(keys.FingerprintFormats.SHA1_BASE64), + "tuUFlgv3kknie9WYExgS7OQj54k=", + ) self.assertEqual( - keys.Key(dsaObj).fingerprint( - keys.FingerprintFormats.SHA1_BASE64), - '9CCuTybG5aORtuW4jrFcp0PbK4U=') + keys.Key(dsaObj).fingerprint(keys.FingerprintFormats.SHA1_BASE64), + "9CCuTybG5aORtuW4jrFcp0PbK4U=", + ) def test_fingerprintBadFormat(self): """ @@ -2582,18 +2593,18 @@ def test_fingerprintBadFormat(self): rsaObj = self._getKeysForFingerprintTest()[0] with self.assertRaises(keys.BadFingerPrintFormat) as em: - keys.Key(rsaObj).fingerprint('sha256-base') + keys.Key(rsaObj).fingerprint("sha256-base") self.assertEqual( - 'Unsupported fingerprint format: sha256-base', - em.exception.args[0]) + "Unsupported fingerprint format: sha256-base", em.exception.args[0] + ) def test_sign_rsa(self): """ Test that the Key object generates correct signatures. """ key = keys.Key.fromString(keydata.privateRSA_openssh) - signature = key.sign(b'') - self.assertTrue(key.verify(signature, b'')) + signature = key.sign(b"") + self.assertTrue(key.verify(signature, b"")) self.assertEqual(signature, self.rsaSignature) def test_verify(self): @@ -2601,13 +2612,13 @@ def test_verify(self): Test that the Key object correctly verifies signatures. """ key = keys.Key.fromString(keydata.publicRSA_openssh) - self.assertTrue(key.verify(self.rsaSignature, b'')) - self.assertFalse(key.verify(self.rsaSignature, b'a')) - self.assertFalse(key.verify(self.dsaSignature, b'')) + self.assertTrue(key.verify(self.rsaSignature, b"")) + self.assertFalse(key.verify(self.rsaSignature, b"a")) + self.assertFalse(key.verify(self.dsaSignature, b"")) key = keys.Key.fromString(keydata.publicDSA_openssh) - self.assertTrue(key.verify(self.dsaSignature, b'')) - self.assertFalse(key.verify(self.dsaSignature, b'a')) - self.assertFalse(key.verify(self.rsaSignature, b'')) + self.assertTrue(key.verify(self.dsaSignature, b"")) + self.assertFalse(key.verify(self.dsaSignature, b"a")) + self.assertFalse(key.verify(self.rsaSignature, b"")) def test_verifyDSANoPrefix(self): """ @@ -2615,7 +2626,7 @@ def test_verifyDSANoPrefix(self): they are still verified as valid keys. """ key = keys.Key.fromString(keydata.publicDSA_openssh) - self.assertTrue(key.verify(self.dsaSignature[-40:], b'')) + self.assertTrue(key.verify(self.dsaSignature[-40:], b"")) def test_repr(self): """ @@ -2623,32 +2634,26 @@ def test_repr(self): """ result = repr(keys.Key(self.rsaObj)) self.assertContains( - ' Date: Tue, 12 Mar 2024 23:26:36 +0000 Subject: [PATCH 24/41] Move to black. --- pavement.py | 22 +- src/chevah_keycert/__init__.py | 15 +- src/chevah_keycert/common.py | 47 +- src/chevah_keycert/exceptions.py | 1 + src/chevah_keycert/ssh.py | 1578 ++++++++++++++++-------------- src/chevah_keycert/ssl.py | 321 +++--- 6 files changed, 1041 insertions(+), 943 deletions(-) diff --git a/pavement.py b/pavement.py index 39985e4..860c10f 100644 --- a/pavement.py +++ b/pavement.py @@ -197,9 +197,23 @@ def lint(): """ from pyflakes.api import main as pyflakes_main from pycodestyle import _main as pycodestyle_main + from black import patched_main + + try: + pyflakes_main(args=['src/chevah_keycert']) + except SystemExit as error: + if error.code: + raise + + sys.argv = ['black', '--check', 'src'] + sys.exit(patched_main()) - sys.argv = [re.sub(r"(-script\.pyw?|\.exe)?$", "", sys.argv[0])] + [ - "src/chevah_keycert", - ] - pyflakes_main() +@task +def black(): + """ + Run black on the whole source code. + """ + from black import patched_main + sys.argv = ['black', 'src'] + sys.exit(patched_main()) diff --git a/src/chevah_keycert/__init__.py b/src/chevah_keycert/__init__.py index 82a5cd9..c61e5c0 100644 --- a/src/chevah_keycert/__init__.py +++ b/src/chevah_keycert/__init__.py @@ -1,6 +1,7 @@ """ SSL and SSH key management. """ + import collections import sys import six @@ -10,8 +11,8 @@ import cryptography.utils -def _path(path, encoding='utf-8'): - if sys.platform.startswith('win'): +def _path(path, encoding="utf-8"): + if sys.platform.startswith("win"): # On Windows we always use unicode. return path # pragma: no cover @@ -27,21 +28,21 @@ def native_string(string): Helper for some API that need bytes on Py2 and Unicode on Py3. """ if six.PY2: - string = string.encode('ascii') + string = string.encode("ascii") return string -for member in ['Callable', 'Iterable', 'Mapping', 'Sequence']: +for member in ["Callable", "Iterable", "Mapping", "Sequence"]: if not hasattr(collections, member): setattr(collections, member, getattr(collections.abc, member)) -if not hasattr(cryptography.utils, 'int_from_bytes'): +if not hasattr(cryptography.utils, "int_from_bytes"): cryptography.utils.int_from_bytes = int.from_bytes -if not hasattr(base64, 'encodestring'): +if not hasattr(base64, "encodestring"): base64.encodestring = base64.encodebytes -if not hasattr(base64, 'decodestring'): +if not hasattr(base64, "decodestring"): base64.decodestring = base64.decodebytes if not hasattr(inspect, "getargspec"): diff --git a/src/chevah_keycert/common.py b/src/chevah_keycert/common.py index 547a041..dc98cd9 100644 --- a/src/chevah_keycert/common.py +++ b/src/chevah_keycert/common.py @@ -15,7 +15,7 @@ def iterbytes(originalBytes): for i in range(len(originalBytes)): - yield originalBytes[i:i + 1] + yield originalBytes[i : i + 1] def intToBytes(i): @@ -40,13 +40,13 @@ def lazyByteSlice(object, offset=0, size=None): if size is None: return view[offset:] else: - return view[offset:(offset + size)] + return view[offset : (offset + size)] def networkString(s): if not isinstance(s, str): raise TypeError("Can only convert text to bytes on Python 3") - return s.encode('ascii') + return s.encode("ascii") def NS(t): @@ -55,7 +55,7 @@ def NS(t): """ if isinstance(t, str): t = t.encode("utf-8") - return struct.pack('!L', len(t)) + t + return struct.pack("!L", len(t)) + t def getNS(s, count=1): @@ -65,20 +65,20 @@ def getNS(s, count=1): ns = [] c = 0 for i in range(count): - l, = struct.unpack('!L', s[c:c + 4]) - ns.append(s[c + 4:4 + l + c]) + (l,) = struct.unpack("!L", s[c : c + 4]) + ns.append(s[c + 4 : 4 + l + c]) c += 4 + l return tuple(ns) + (s[c:],) def MP(number): if number == 0: - return b'\000' * 4 + return b"\000" * 4 assert number > 0 bn = int_to_bytes(number) if ord(bn[0:1]) & 128: - bn = b'\000' + bn - return struct.pack('>L', len(bn)) + bn + bn = b"\000" + bn + return struct.pack(">L", len(bn)) + bn def getMP(data, count=1): @@ -92,8 +92,8 @@ def getMP(data, count=1): mp = [] c = 0 for i in range(count): - length, = struct.unpack('>L', data[c:c + 4]) - mp.append(int_from_bytes(data[c + 4:c + 4 + length], 'big')) + (length,) = struct.unpack(">L", data[c : c + 4]) + mp.append(int_from_bytes(data[c + 4 : c + 4 + length], "big")) c += 4 + length return tuple(mp) + (data[c:],) @@ -126,37 +126,36 @@ def str_or_repr(value): return value try: - return str(value, encoding='utf-8') + return str(value, encoding="utf-8") except Exception: """ Not UTF-8 encoded value. """ try: - return str(value, encoding='windows-1252') + return str(value, encoding="windows-1252") except Exception: """ Not Windows encoded value. """ try: - return str(str(value), encoding='utf-8', errors='replace') + return str(str(value), encoding="utf-8", errors="replace") except (UnicodeDecodeError, UnicodeEncodeError): """ Not UTF-8 encoded value. """ try: - return str( - str(value), encoding='windows-1252', errors='replace') + return str(str(value), encoding="windows-1252", errors="replace") except (UnicodeDecodeError, UnicodeEncodeError): pass # No luck with str, try repr() - return str(repr(value), encoding='windows-1252', errors='replace') + return str(repr(value), encoding="windows-1252", errors="replace") if value is None: - return u'None' + return "None" if isinstance(value, str): return value @@ -169,14 +168,14 @@ def str_or_repr(value): if code == errno.ENOENT: # On Windows it is: # The system cannot find the file specified. - message = b'No such file or directory' + message = b"No such file or directory" if code == errno.EEXIST: # On Windows it is: # Cannot create a file when that file already exists - message = b'File exists' + message = b"File exists" if code == errno.EBADF: # On AIX: Bad file number - message = b'Bad file descriptor' + message = b"Bad file descriptor" if code and message: if value.filename: @@ -184,14 +183,14 @@ def str_or_repr(value): code, str_or_repr(message), str_or_repr(value.filename), - ) - return '[Errno %s] %s.' % (code, str_or_repr(message)) + ) + return "[Errno %s] %s." % (code, str_or_repr(message)) if isinstance(value, Exception): try: details = str(value) except (UnicodeDecodeError, UnicodeEncodeError): - details = getattr(value, 'message', '') + details = getattr(value, "message", "") result = str_or_repr(details) if result: return result diff --git a/src/chevah_keycert/exceptions.py b/src/chevah_keycert/exceptions.py index 1b9fd09..7be3d61 100644 --- a/src/chevah_keycert/exceptions.py +++ b/src/chevah_keycert/exceptions.py @@ -12,6 +12,7 @@ class KeyCertException(Exception): Code calling the public API should handle only this exception. The other exceptions are just for fine tunning. """ + def __init__(self, message): self.message = message diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index cbd4c45..a79866b 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -24,10 +24,11 @@ from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ( - dsa, ec, ed25519, padding, rsa) +from cryptography.hazmat.primitives.asymmetric import dsa, ec, ed25519, padding, rsa from cryptography.hazmat.primitives.serialization import ( - load_pem_private_key, load_ssh_public_key) + load_pem_private_key, + load_ssh_public_key, +) from cryptography import utils from six.moves import map from six.moves import range @@ -35,11 +36,14 @@ try: from cryptography.hazmat.primitives.asymmetric.utils import ( - encode_dss_signature, decode_dss_signature) + encode_dss_signature, + decode_dss_signature, + ) except ImportError: from cryptography.hazmat.primitives.asymmetric.utils import ( encode_rfc6979_signature as encode_dss_signature, - decode_rfc6979_signature as decode_dss_signature) + decode_rfc6979_signature as decode_dss_signature, + ) from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from pyasn1.error import PyAsn1Error @@ -66,43 +70,44 @@ from chevah_keycert.common import ( force_unicode, iterbytes, - ) +) from chevah_keycert.exceptions import ( BadKeyError, BadSignatureAlgorithmError, EncryptedKeyError, KeyCertException, - ) +) from constantly import NamedConstant, Names -DEFAULT_PUBLIC_KEY_EXTENSION = u'.pub' +DEFAULT_PUBLIC_KEY_EXTENSION = ".pub" DEFAULT_KEY_SIZE = 2048 -DEFAULT_KEY_TYPE = 'rsa' -SSHCOM_MAGIC_NUMBER = int('3f6ff9eb', base=16) -PUTTY_HMAC_KEY = b'putty-private-key-file-mac-key' -ID_SHA1 = b'\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' +DEFAULT_KEY_TYPE = "rsa" +SSHCOM_MAGIC_NUMBER = int("3f6ff9eb", base=16) +PUTTY_HMAC_KEY = b"putty-private-key-file-mac-key" +ID_SHA1 = b"\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14" # Curve lookup table _curveTable = { - b'ecdsa-sha2-nistp256': ec.SECP256R1(), - b'ecdsa-sha2-nistp384': ec.SECP384R1(), - b'ecdsa-sha2-nistp521': ec.SECP521R1(), - b'ecdsa-sha2-nistp192': ec.SECP192R1(), - } + b"ecdsa-sha2-nistp256": ec.SECP256R1(), + b"ecdsa-sha2-nistp384": ec.SECP384R1(), + b"ecdsa-sha2-nistp521": ec.SECP521R1(), + b"ecdsa-sha2-nistp192": ec.SECP192R1(), +} _secToNist = { - 'secp256r1': b'nistp256', - 'secp384r1': b'nistp384', - 'secp521r1': b'nistp521', - 'secp192r1': b'nistp192', - } + "secp256r1": b"nistp256", + "secp384r1": b"nistp384", + "secp521r1": b"nistp521", + "secp192r1": b"nistp192", +} _ecSizeTable = { 256: ec.SECP256R1(), 384: ec.SECP384R1(), 521: ec.SECP521R1(), - } +} + class BadFingerPrintFormat(Exception): """ @@ -110,7 +115,6 @@ class BadFingerPrintFormat(Exception): """ - class FingerprintFormats(Names): """ Constants representing the supported formats of key fingerprints. @@ -197,7 +201,7 @@ class Key(object): """ @classmethod - def fromFile(cls, filename, type=None, passphrase=None, encoding='utf-8'): + def fromFile(cls, filename, type=None, passphrase=None, encoding="utf-8"): """ Load a key from a file. @@ -214,7 +218,7 @@ def fromFile(cls, filename, type=None, passphrase=None, encoding='utf-8'): @rtype: L{Key} @return: The loaded key. """ - with open(filename, 'rb') as file: + with open(filename, "rb") as file: return cls.fromString(file.read(), type, passphrase) @classmethod @@ -246,26 +250,26 @@ def fromString(cls, data, type=None, passphrase=None): if type is None: type = cls._guessStringType(data) if type is None: - raise BadKeyError( - 'Cannot guess the type for "{}"'.format(repr(data[:80]))) + raise BadKeyError('Cannot guess the type for "{}"'.format(repr(data[:80]))) try: - method = getattr(cls, '_fromString_%s' % type.upper(), None) + method = getattr(cls, "_fromString_%s" % type.upper(), None) if method is None: raise BadKeyError( - 'no _fromString method for "%s"' % force_unicode(type[:30])) + 'no _fromString method for "%s"' % force_unicode(type[:30]) + ) if method.__code__.co_argcount == 2: # no passphrase if passphrase: - raise BadKeyError('key not encrypted') + raise BadKeyError("key not encrypted") return method(data) else: return method(data, passphrase) - except (IndexError): + except IndexError: # Most probably some parts are missing from the key, so # we consider it too short. - raise BadKeyError('Key is too short.') + raise BadKeyError("Key is too short.") except (struct.error, binascii.Error, TypeError): - raise BadKeyError('Fail to parse key content.') + raise BadKeyError("Fail to parse key content.") @classmethod def _fromString_BLOB(cls, blob): @@ -303,23 +307,24 @@ def _fromString_BLOB(cls, blob): @raises BadKeyError: if the key type (the first string) is unknown. """ keyType, rest = common.getNS(blob) - if keyType == b'ssh-rsa': + if keyType == b"ssh-rsa": e, n, rest = common.getMP(rest, 2) return cls._fromRSAComponents(n, e) - elif keyType == b'ssh-dss': + elif keyType == b"ssh-dss": p, q, g, y, rest = common.getMP(rest, 4) - return cls._fromDSAComponents( y, p, q, g) + return cls._fromDSAComponents(y, p, q, g) elif keyType in _curveTable: return cls._fromECEncodedPoint( encodedPoint=common.getNS(rest, 2)[1], curve=keyType, - ) - elif keyType == b'ssh-ed25519': + ) + elif keyType == b"ssh-ed25519": a, rest = common.getNS(rest) return cls._fromEd25519Components(a) else: - raise BadKeyError('unknown blob type: "{}"'.format( - force_unicode(keyType[:30]))) + raise BadKeyError( + 'unknown blob type: "{}"'.format(force_unicode(keyType[:30])) + ) @classmethod def _fromString_PRIVATE_BLOB(cls, blob): @@ -369,10 +374,10 @@ def _fromString_PRIVATE_BLOB(cls, blob): """ keyType, rest = common.getNS(blob) - if keyType == b'ssh-rsa': + if keyType == b"ssh-rsa": n, e, d, u, p, q, rest = common.getMP(rest, 6) return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q) - elif keyType == b'ssh-dss': + elif keyType == b"ssh-dss": p, q, g, y, x, rest = common.getMP(rest, 5) return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) elif keyType in _curveTable: @@ -380,21 +385,21 @@ def _fromString_PRIVATE_BLOB(cls, blob): curveName, q, rest = common.getNS(rest, 2) if curveName != _secToNist[curve.name]: raise BadKeyError( - 'ECDSA curve name "%s" does not match key type "%s"' % ( - force_unicode(curveName), force_unicode(keyType))) + 'ECDSA curve name "%s" does not match key type "%s"' + % (force_unicode(curveName), force_unicode(keyType)) + ) privateValue, rest = common.getMP(rest) return cls._fromECEncodedPoint( - encodedPoint=q, curve=keyType, privateValue=privateValue) - elif keyType == b'ssh-ed25519': + encodedPoint=q, curve=keyType, privateValue=privateValue + ) + elif keyType == b"ssh-ed25519": # OpenSSH's format repeats the public key bytes for some reason. # We're only interested in the private key here anyway. a, combined, rest = common.getNS(rest, 2) k = combined[:32] return cls._fromEd25519Components(a, k=k) else: - raise BadKeyError( - 'Unknown blob type: "%s"' % force_unicode(keyType[:30])) - + raise BadKeyError('Unknown blob type: "%s"' % force_unicode(keyType[:30])) @classmethod def _fromString_PUBLIC_OPENSSH(cls, data): @@ -412,12 +417,11 @@ def _fromString_PUBLIC_OPENSSH(cls, data): """ # ECDSA keys don't need base64 decoding which is required # for RSA or DSA key. - if data.startswith(b'ecdsa-sha2'): + if data.startswith(b"ecdsa-sha2"): return cls(load_ssh_public_key(data, default_backend())) blob = decodebytes(data.split()[1]) return cls._fromString_BLOB(blob) - @classmethod def _fromString_PRIVATE_OPENSSH_V1(cls, data, passphrase): """ @@ -448,62 +452,70 @@ def _fromString_PRIVATE_OPENSSH_V1(cls, data, passphrase): * a passphrase is not provided for an encrypted key """ lines = data.strip().splitlines() - keyList = decodebytes(b''.join(lines[1:-1])) - if not keyList.startswith(b'openssh-key-v1\0'): - raise BadKeyError('unknown OpenSSH private key format') - keyList = keyList[len(b'openssh-key-v1\0'):] + keyList = decodebytes(b"".join(lines[1:-1])) + if not keyList.startswith(b"openssh-key-v1\0"): + raise BadKeyError("unknown OpenSSH private key format") + keyList = keyList[len(b"openssh-key-v1\0") :] cipher, kdf, kdfOptions, rest = common.getNS(keyList, 3) - n = struct.unpack('!L', rest[:4])[0] + n = struct.unpack("!L", rest[:4])[0] if n != 1: - raise BadKeyError('only OpenSSH private key files containing ' - 'a single key are supported') + raise BadKeyError( + "only OpenSSH private key files containing " + "a single key are supported" + ) # Ignore public key _, encPrivKeyList, _ = common.getNS(rest[4:], 2) - if cipher != b'none': + if cipher != b"none": if not passphrase: - raise EncryptedKeyError('Passphrase must be provided ' - 'for an encrypted key') + raise EncryptedKeyError( + "Passphrase must be provided " "for an encrypted key" + ) # Determine cipher - if cipher in (b'aes128-ctr', b'aes192-ctr', b'aes256-ctr'): + if cipher in (b"aes128-ctr", b"aes192-ctr", b"aes256-ctr"): algorithmClass = algorithms.AES blockSize = 16 keySize = int(cipher[3:6]) // 8 ivSize = blockSize else: - raise BadKeyError('unknown encryption type "%s"' % ( - force_unicode(cipher),)) - if kdf == b'bcrypt': + raise BadKeyError( + 'unknown encryption type "%s"' % (force_unicode(cipher),) + ) + if kdf == b"bcrypt": salt, rest = common.getNS(kdfOptions) - rounds = struct.unpack('!L', rest[:4])[0] + rounds = struct.unpack("!L", rest[:4])[0] decKey = bcrypt.kdf( - passphrase, salt, keySize + ivSize, rounds, + passphrase, + salt, + keySize + ivSize, + rounds, # We can only use the number of rounds that OpenSSH used. - ignore_few_rounds=True) + ignore_few_rounds=True, + ) else: - raise BadKeyError( - 'unknown KDF type "%s"' % (force_unicode(kdf),)) + raise BadKeyError('unknown KDF type "%s"' % (force_unicode(kdf),)) if (len(encPrivKeyList) % blockSize) != 0: - raise BadKeyError('bad padding') + raise BadKeyError("bad padding") decryptor = Cipher( algorithmClass(decKey[:keySize]), - modes.CTR(decKey[keySize:keySize + ivSize]), - backend=default_backend() + modes.CTR(decKey[keySize : keySize + ivSize]), + backend=default_backend(), ).decryptor() - privKeyList = ( - decryptor.update(encPrivKeyList) + decryptor.finalize()) + privKeyList = decryptor.update(encPrivKeyList) + decryptor.finalize() else: - if kdf != b'none': - raise BadKeyError('private key specifies KDF "%s" but no ' - 'cipher' % (force_unicode(kdf),)) + if kdf != b"none": + raise BadKeyError( + 'private key specifies KDF "%s" but no ' + "cipher" % (force_unicode(kdf),) + ) privKeyList = encPrivKeyList - check1 = struct.unpack('!L', privKeyList[:4])[0] - check2 = struct.unpack('!L', privKeyList[4:8])[0] + check1 = struct.unpack("!L", privKeyList[:4])[0] + check2 = struct.unpack("!L", privKeyList[4:8])[0] if check1 != check2: raise BadKeyError( - 'Private key sanity check failed. Maybe invalid passphrase.') + "Private key sanity check failed. Maybe invalid passphrase." + ) return cls._fromString_PRIVATE_BLOB(privKeyList[8:]) - @classmethod def _fromString_PRIVATE_OPENSSH(cls, data, passphrase): """ @@ -543,74 +555,73 @@ def _fromString_PRIVATE_OPENSSH(cls, data, passphrase): """ lines = data.strip().splitlines() kind = lines[0][11:-17] - if lines[1].startswith(b'Proc-Type: 4,ENCRYPTED'): + if lines[1].startswith(b"Proc-Type: 4,ENCRYPTED"): if not passphrase: - raise EncryptedKeyError('Passphrase must be provided ' - 'for an encrypted key') + raise EncryptedKeyError( + "Passphrase must be provided " "for an encrypted key" + ) # Determine cipher and initialization vector try: - _, cipherIVInfo = lines[2].split(b' ', 1) - cipher, ivdata = cipherIVInfo.rstrip().split(b',', 1) + _, cipherIVInfo = lines[2].split(b" ", 1) + cipher, ivdata = cipherIVInfo.rstrip().split(b",", 1) except ValueError: - raise BadKeyError( - 'invalid DEK-info "%s"' % (force_unicode(lines[2]),)) + raise BadKeyError('invalid DEK-info "%s"' % (force_unicode(lines[2]),)) - if cipher in (b'AES-128-CBC', b'AES-256-CBC'): + if cipher in (b"AES-128-CBC", b"AES-256-CBC"): algorithmClass = algorithms.AES - keySize = int(cipher.split(b'-')[1]) // 8 + keySize = int(cipher.split(b"-")[1]) // 8 if len(ivdata) != 32: - raise BadKeyError('AES encrypted key with a bad IV') - elif cipher == b'DES-EDE3-CBC': + raise BadKeyError("AES encrypted key with a bad IV") + elif cipher == b"DES-EDE3-CBC": algorithmClass = algorithms.TripleDES keySize = 24 if len(ivdata) != 16: - raise BadKeyError('DES encrypted key with a bad IV') + raise BadKeyError("DES encrypted key with a bad IV") else: raise BadKeyError( - 'unknown encryption type "%s"' % (force_unicode(cipher),)) + 'unknown encryption type "%s"' % (force_unicode(cipher),) + ) # Extract keyData for decoding - iv = bytes(bytearray([int(ivdata[i:i + 2], 16) - for i in range(0, len(ivdata), 2)])) + iv = bytes( + bytearray( + [int(ivdata[i : i + 2], 16) for i in range(0, len(ivdata), 2)] + ) + ) ba = md5(passphrase + iv[:8]).digest() bb = md5(ba + passphrase + iv[:8]).digest() decKey = (ba + bb)[:keySize] - b64Data = decodebytes(b''.join(lines[3:-1])) + b64Data = decodebytes(b"".join(lines[3:-1])) decryptor = Cipher( - algorithmClass(decKey), - modes.CBC(iv), - backend=default_backend() + algorithmClass(decKey), modes.CBC(iv), backend=default_backend() ).decryptor() keyData = decryptor.update(b64Data) + decryptor.finalize() removeLen = ord(keyData[-1:]) keyData = keyData[:-removeLen] else: - b64Data = b''.join(lines[1:-1]) + b64Data = b"".join(lines[1:-1]) keyData = decodebytes(b64Data) try: decodedKey = berDecoder.decode(keyData)[0] except PyAsn1Error as e: raise BadKeyError( - 'Failed to decode key (Bad Passphrase?): %s' % ( - force_unicode(e),)) + "Failed to decode key (Bad Passphrase?): %s" % (force_unicode(e),) + ) - if kind == b'EC': - return cls( - load_pem_private_key(data, passphrase, default_backend())) + if kind == b"EC": + return cls(load_pem_private_key(data, passphrase, default_backend())) - if kind == b'RSA': + if kind == b"RSA": if len(decodedKey) == 2: # Alternate RSA key decodedKey = decodedKey[0] if len(decodedKey) < 6: - raise BadKeyError('RSA key failed to decode properly') + raise BadKeyError("RSA key failed to decode properly") - n, e, d, p, q, dmp1, dmq1, iqmp = [ - int(value) for value in decodedKey[1:9] - ] + n, e, d, p, q, dmp1, dmq1, iqmp = [int(value) for value in decodedKey[1:9]] return cls( rsa.RSAPrivateNumbers( p=p, @@ -622,15 +633,14 @@ def _fromString_PRIVATE_OPENSSH(cls, data, passphrase): public_numbers=rsa.RSAPublicNumbers(e=e, n=n), ).private_key(default_backend()) ) - elif kind == b'DSA': - p, q, g, y, x = [int(value) for value in decodedKey[1: 6]] + elif kind == b"DSA": + p, q, g, y, x = [int(value) for value in decodedKey[1:6]] if len(decodedKey) < 6: - raise BadKeyError('DSA key failed to decode properly') + raise BadKeyError("DSA key failed to decode properly") return cls._fromDSAComponents(y, p, q, g, x) else: raise BadKeyError('unknown key type "%s"' % (force_unicode(kind),)) - @classmethod def _fromString_AGENTV3(cls, data): """ @@ -662,14 +672,14 @@ def _fromString_AGENTV3(cls, data): @raises BadKeyError: if the key type (the first string) is unknown """ keyType, data = common.getNS(data) - if keyType == b'ssh-dss': + if keyType == b"ssh-dss": p, data = common.getMP(data) q, data = common.getMP(data) g, data = common.getMP(data) y, data = common.getMP(data) x, data = common.getMP(data) return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) - elif keyType == b'ssh-rsa': + elif keyType == b"ssh-rsa": e, data = common.getMP(data) d, data = common.getMP(data) n, data = common.getMP(data) @@ -678,8 +688,7 @@ def _fromString_AGENTV3(cls, data): q, data = common.getMP(data) return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) else: # pragma: no cover - raise BadKeyError( - 'unknown key type "%s"' % (force_unicode(keyType[:30]),)) + raise BadKeyError('unknown key type "%s"' % (force_unicode(keyType[:30]),)) @classmethod def _guessStringType(cls, data): @@ -690,51 +699,53 @@ def _guessStringType(cls, data): @type data: L{bytes} @param data: The key data. """ - if data.startswith(b'ssh-') or data.startswith(b'ecdsa-sha2-'): - return 'public_openssh' - elif data.startswith(b'---- BEGIN SSH2 PUBLIC KEY ----'): - return 'public_sshcom' - elif data.startswith(b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----'): - return 'private_sshcom' - elif data.startswith(b'-----BEGIN RSA PUBLIC'): - return 'public_pkcs1_rsa' + if data.startswith(b"ssh-") or data.startswith(b"ecdsa-sha2-"): + return "public_openssh" + elif data.startswith(b"---- BEGIN SSH2 PUBLIC KEY ----"): + return "public_sshcom" + elif data.startswith(b"---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----"): + return "private_sshcom" + elif data.startswith(b"-----BEGIN RSA PUBLIC"): + return "public_pkcs1_rsa" elif ( - data.startswith(b'-----BEGIN RSA PRIVATE') or - data.startswith(b'-----BEGIN DSA PRIVATE') or - data.startswith(b'-----BEGIN EC PRIVATE') - ): + data.startswith(b"-----BEGIN RSA PRIVATE") + or data.startswith(b"-----BEGIN DSA PRIVATE") + or data.startswith(b"-----BEGIN EC PRIVATE") + ): # This is also private PKCS#1 format. - return 'private_openssh' - elif data.startswith(b'-----BEGIN OPENSSH PRIVATE KEY-----'): - return 'private_openssh_v1' + return "private_openssh" + elif data.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----"): + return "private_openssh_v1" - elif data.startswith(b'-----BEGIN CERTIFICATE-----'): - return 'public_x509_certificate' + elif data.startswith(b"-----BEGIN CERTIFICATE-----"): + return "public_x509_certificate" - elif data.startswith(b'-----BEGIN PUBLIC KEY-----'): + elif data.startswith(b"-----BEGIN PUBLIC KEY-----"): # Public Key in X.509 format it's as follows - return 'public_x509' - - elif data.startswith(b'-----BEGIN PRIVATE KEY-----'): - return 'private_pkcs8' - elif data.startswith(b'-----BEGIN ENCRYPTED PRIVATE KEY-----'): - return 'private_encrypted_pkcs8' - elif data.startswith(b'PuTTY-User-Key-File-2'): - return 'private_putty' - elif data.startswith(b'PuTTY-User-Key-File-3'): - return 'private_putty_v3' - elif (data.startswith(b'\x00\x00\x00\x07ssh-') or - data.startswith(b'\x00\x00\x00\x13ecdsa-') or - data.startswith(b'\x00\x00\x00\x0bssh-ed25519')): + return "public_x509" + + elif data.startswith(b"-----BEGIN PRIVATE KEY-----"): + return "private_pkcs8" + elif data.startswith(b"-----BEGIN ENCRYPTED PRIVATE KEY-----"): + return "private_encrypted_pkcs8" + elif data.startswith(b"PuTTY-User-Key-File-2"): + return "private_putty" + elif data.startswith(b"PuTTY-User-Key-File-3"): + return "private_putty_v3" + elif ( + data.startswith(b"\x00\x00\x00\x07ssh-") + or data.startswith(b"\x00\x00\x00\x13ecdsa-") + or data.startswith(b"\x00\x00\x00\x0bssh-ed25519") + ): ignored, rest = common.getNS(data) count = 0 while rest: count += 1 ignored, rest = common.getMP(rest) if count > 4: - return 'agentv3' + return "agentv3" else: - return 'blob' + return "blob" @classmethod def _fromRSAComponents(cls, n, e, d=None, p=None, q=None, u=None): @@ -805,7 +816,8 @@ def _fromDSAComponents(cls, y, p, q, g, x=None): @return: A DSA key constructed from the values as given. """ publicNumbers = dsa.DSAPublicNumbers( - y=y, parameter_numbers=dsa.DSAParameterNumbers(p=p, q=q, g=g)) + y=y, parameter_numbers=dsa.DSAParameterNumbers(p=p, q=q, g=g) + ) if x is None: try: @@ -814,15 +826,16 @@ def _fromDSAComponents(cls, y, p, q, g, x=None): return cls(keyObject) except ValueError as error: raise BadKeyError( - 'Unsupported DSA public key: "%s"' % (force_unicode(error),)) + 'Unsupported DSA public key: "%s"' % (force_unicode(error),) + ) try: - privateNumbers = dsa.DSAPrivateNumbers( - x=x, public_numbers=publicNumbers) + privateNumbers = dsa.DSAPrivateNumbers(x=x, public_numbers=publicNumbers) keyObject = privateNumbers.private_key(default_backend()) except ValueError as error: raise BadKeyError( - 'Unsupported DSA private key: "%s"' % (force_unicode(error),)) + 'Unsupported DSA private key: "%s"' % (force_unicode(error),) + ) return cls(keyObject) @@ -845,13 +858,15 @@ def _fromECComponents(cls, x, y, curve, privateValue=None): """ publicNumbers = ec.EllipticCurvePublicNumbers( - x=x, y=y, curve=_curveTable[curve]) + x=x, y=y, curve=_curveTable[curve] + ) if privateValue is None: # We have public components. keyObject = publicNumbers.public_key(default_backend()) else: privateNumbers = ec.EllipticCurvePrivateNumbers( - private_value=privateValue, public_numbers=publicNumbers) + private_value=privateValue, public_numbers=publicNumbers + ) keyObject = privateNumbers.private_key(default_backend()) return cls(keyObject) @@ -905,7 +920,7 @@ def _fromEd25519Components(cls, a, k=None): return cls(keyObject) except UnsupportedAlgorithm: - raise BadKeyError('Ed25519 keys are not supported.') + raise BadKeyError("Ed25519 keys are not supported.") def __init__(self, keyObject): """ @@ -939,14 +954,14 @@ def __repr__(self): """ Return a pretty representation of this object. """ - if self.type() == 'EC': + if self.type() == "EC": data = self.data() - name = data['curve'].decode('utf-8') + name = data["curve"].decode("utf-8") if self.isPublic(): - out = '\n" else: lines = [ - '<%s %s (%s bits)' % ( + "<%s %s (%s bits)" + % ( self.type(), - self.isPublic() and 'Public Key' or 'Private Key', - self.size())] + self.isPublic() and "Public Key" or "Private Key", + self.size(), + ) + ] for k, v in sorted(self.data().items()): - lines.append('attr %s:' % (k,)) - by = v if self.type() == 'Ed25519' else common.MP(v)[4:] + lines.append("attr %s:" % (k,)) + by = v if self.type() == "Ed25519" else common.MP(v)[4:] while by: m = by[:15] by = by[15:] - o = '' + o = "" for c in iterbytes(m): - o = o + '%02x:' % (ord(c),) + o = o + "%02x:" % (ord(c),) if len(m) < 15: o = o[:-1] - lines.append('\t' + o) - lines[-1] = lines[-1] + '>' - return '\n'.join(lines) + lines.append("\t" + o) + lines[-1] = lines[-1] + ">" + return "\n".join(lines) def isPublic(self): """ @@ -981,8 +999,13 @@ def isPublic(self): """ return isinstance( self._keyObject, - (rsa.RSAPublicKey, dsa.DSAPublicKey, ec.EllipticCurvePublicKey, - ed25519.Ed25519PublicKey)) + ( + rsa.RSAPublicKey, + dsa.DSAPublicKey, + ec.EllipticCurvePublicKey, + ed25519.Ed25519PublicKey, + ), + ) def public(self): """ @@ -1032,18 +1055,16 @@ def fingerprint(self, format=FingerprintFormats.MD5_HEX): @rtype: L{str} """ if format is FingerprintFormats.SHA256_BASE64: - return base64.b64encode( - sha256(self.blob()).digest()).decode('ascii') + return base64.b64encode(sha256(self.blob()).digest()).decode("ascii") elif format is FingerprintFormats.SHA1_BASE64: - return base64.b64encode( - sha1(self.blob()).digest()).decode('ascii') + return base64.b64encode(sha1(self.blob()).digest()).decode("ascii") elif format is FingerprintFormats.MD5_HEX: - result = b':'.join([binascii.hexlify(x) - for x in iterbytes(md5(self.blob()).digest())]) - return result.decode('ascii') + result = b":".join( + [binascii.hexlify(x) for x in iterbytes(md5(self.blob()).digest())] + ) + return result.decode("ascii") else: - raise BadFingerPrintFormat( - 'Unsupported fingerprint format: %s' % (format,)) + raise BadFingerPrintFormat("Unsupported fingerprint format: %s" % (format,)) def type(self): """ @@ -1053,23 +1074,20 @@ def type(self): @rtype: L{str} @raises RuntimeError: If the object type is unknown. """ - if isinstance( - self._keyObject, (rsa.RSAPublicKey, rsa.RSAPrivateKey)): - return 'RSA' - elif isinstance( - self._keyObject, (dsa.DSAPublicKey, dsa.DSAPrivateKey)): - return 'DSA' + if isinstance(self._keyObject, (rsa.RSAPublicKey, rsa.RSAPrivateKey)): + return "RSA" + elif isinstance(self._keyObject, (dsa.DSAPublicKey, dsa.DSAPrivateKey)): + return "DSA" elif isinstance( - self._keyObject, - (ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey)): - return 'EC' + self._keyObject, (ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) + ): + return "EC" elif isinstance( - self._keyObject, - (ed25519.Ed25519PublicKey, ed25519.Ed25519PrivateKey)): - return 'Ed25519' + self._keyObject, (ed25519.Ed25519PublicKey, ed25519.Ed25519PrivateKey) + ): + return "Ed25519" else: - raise RuntimeError( - 'unknown type of object: %r' % (self._keyObject,)) + raise RuntimeError("unknown type of object: %r" % (self._keyObject,)) def sshType(self): """ @@ -1082,15 +1100,13 @@ def sshType(self): @return: The key type format. @rtype: L{bytes} """ - if self.type() == 'EC': - return ( - b'ecdsa-sha2-' + - _secToNist[self._keyObject.curve.name]) + if self.type() == "EC": + return b"ecdsa-sha2-" + _secToNist[self._keyObject.curve.name] else: return { - 'RSA': b'ssh-rsa', - 'DSA': b'ssh-dss', - 'Ed25519': b'ssh-ed25519', + "RSA": b"ssh-rsa", + "DSA": b"ssh-dss", + "Ed25519": b"ssh-ed25519", }[self.type()] def supportedSignatureAlgorithms(self): @@ -1140,9 +1156,9 @@ def size(self): """ if self._keyObject is None: return 0 - elif self.type() == 'EC': + elif self.type() == "EC": return self._keyObject.curve.key_size - elif self.type() == 'Ed25519': + elif self.type() == "Ed25519": return 256 return self._keyObject.key_size @@ -1204,20 +1220,18 @@ def data(self): elif isinstance(self._keyObject, ed25519.Ed25519PublicKey): return { "a": self._keyObject.public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw + serialization.Encoding.Raw, serialization.PublicFormat.Raw ), } elif isinstance(self._keyObject, ed25519.Ed25519PrivateKey): return { "a": self._keyObject.public_key().public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw + serialization.Encoding.Raw, serialization.PublicFormat.Raw ), "k": self._keyObject.private_bytes( serialization.Encoding.Raw, serialization.PrivateFormat.Raw, - serialization.NoEncryption() + serialization.NoEncryption(), ), } @@ -1258,25 +1272,38 @@ def blob(self): """ type = self.type() data = self.data() - if type == 'RSA': - return (common.NS(b'ssh-rsa') + common.MP(data['e']) + - common.MP(data['n'])) - elif type == 'DSA': - return (common.NS(b'ssh-dss') + common.MP(data['p']) + - common.MP(data['q']) + common.MP(data['g']) + - common.MP(data['y'])) - elif type == 'EC': + if type == "RSA": + return common.NS(b"ssh-rsa") + common.MP(data["e"]) + common.MP(data["n"]) + elif type == "DSA": + return ( + common.NS(b"ssh-dss") + + common.MP(data["p"]) + + common.MP(data["q"]) + + common.MP(data["g"]) + + common.MP(data["y"]) + ) + elif type == "EC": byteLength = (self._keyObject.curve.key_size + 7) // 8 return ( - common.NS(data['curve']) + common.NS(data["curve"][-8:]) + - common.NS( - b'\x04' + utils.int_to_bytes(data['x'], byteLength) + - utils.int_to_bytes(data['y'], byteLength))) - elif type == 'Ed25519': - return common.NS(b'ssh-ed25519') + common.NS(data['a']) + common.NS(data["curve"]) + + common.NS(data["curve"][-8:]) + + common.NS( + b"\x04" + + utils.int_to_bytes(data["x"], byteLength) + + utils.int_to_bytes(data["y"], byteLength) + ) + ) + elif type == "Ed25519": + return common.NS(b"ssh-ed25519") + common.NS(data["a"]) else: - raise BadKeyError('unknown key type: "%s"' % (force_unicode(type,))) - + raise BadKeyError( + 'unknown key type: "%s"' + % ( + force_unicode( + type, + ) + ) + ) def privateBlob(self): """ @@ -1317,31 +1344,54 @@ def privateBlob(self): """ type = self.type() data = self.data() - if type == 'RSA': - iqmp = rsa.rsa_crt_iqmp(data['p'], data['q']) - return (common.NS(b'ssh-rsa') + common.MP(data['n']) + - common.MP(data['e']) + common.MP(data['d']) + - common.MP(iqmp) + common.MP(data['p']) + - common.MP(data['q'])) - elif type == 'DSA': - return (common.NS(b'ssh-dss') + common.MP(data['p']) + - common.MP(data['q']) + common.MP(data['g']) + - common.MP(data['y']) + common.MP(data['x'])) - elif type == 'EC': + if type == "RSA": + iqmp = rsa.rsa_crt_iqmp(data["p"], data["q"]) + return ( + common.NS(b"ssh-rsa") + + common.MP(data["n"]) + + common.MP(data["e"]) + + common.MP(data["d"]) + + common.MP(iqmp) + + common.MP(data["p"]) + + common.MP(data["q"]) + ) + elif type == "DSA": + return ( + common.NS(b"ssh-dss") + + common.MP(data["p"]) + + common.MP(data["q"]) + + common.MP(data["g"]) + + common.MP(data["y"]) + + common.MP(data["x"]) + ) + elif type == "EC": encPub = self._keyObject.public_key().public_bytes( serialization.Encoding.X962, - serialization.PublicFormat.UncompressedPoint + serialization.PublicFormat.UncompressedPoint, + ) + return ( + common.NS(data["curve"]) + + common.NS(data["curve"][-8:]) + + common.NS(encPub) + + common.MP(data["privateValue"]) + ) + elif type == "Ed25519": + return ( + common.NS(b"ssh-ed25519") + + common.NS(data["a"]) + + common.NS(data["k"] + data["a"]) ) - return (common.NS(data['curve']) + common.NS(data['curve'][-8:]) + - common.NS(encPub) + common.MP(data['privateValue'])) - elif type == 'Ed25519': - return (common.NS(b'ssh-ed25519') + common.NS(data['a']) + - common.NS(data['k'] + data['a'])) else: - raise BadKeyError('unknown key type: "%s"' % (force_unicode(type,))) + raise BadKeyError( + 'unknown key type: "%s"' + % ( + force_unicode( + type, + ) + ) + ) - def toString(self, type, extra=None, comment=None, - passphrase=None): + def toString(self, type, extra=None, comment=None, passphrase=None): """ Create a string representation of this key. If the key is a private key and you want the representation of its public key, use @@ -1380,10 +1430,9 @@ def toString(self, type, extra=None, comment=None, else: passphrase = extra - method = getattr(self, '_toString_%s' % (type.upper(),), None) + method = getattr(self, "_toString_%s" % (type.upper(),), None) if method is None: - raise BadKeyError( - 'unknown key type: "%s"' % (force_unicode(type[:30]),)) + raise BadKeyError('unknown key type: "%s"' % (force_unicode(type[:30]),)) passphrase = _normalizePassphrase(passphrase) return method(comment=comment, passphrase=passphrase) @@ -1398,18 +1447,21 @@ def _toPublicOpenSSH(self, comment=None): @param comment: A comment to include with the key, or L{None} to omit the comment. """ - if self.type() == 'EC': + if self.type() == "EC": if not comment: - comment = b'' - return (self._keyObject.public_bytes( - serialization.Encoding.OpenSSH, - serialization.PublicFormat.OpenSSH - ) + b' ' + comment).strip() + comment = b"" + return ( + self._keyObject.public_bytes( + serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH + ) + + b" " + + comment + ).strip() - b64Data = encodebytes(self.blob()).replace(b'\n', b'') + b64Data = encodebytes(self.blob()).replace(b"\n", b"") if not comment: - comment = b'' - return (self.sshType() + b' ' + b64Data + b' ' + comment).strip() + comment = b"" + return (self.sshType() + b" " + b64Data + b" " + comment).strip() def _toString_OPENSSH_V1(self, comment=None, passphrase=None): """ @@ -1430,22 +1482,21 @@ def _toString_OPENSSH_V1(self, comment=None, passphrase=None): # OpenSSH. We could make this configurable later if it's # needed. cipher = algorithms.AES - cipherName = b'aes256-ctr' - kdfName = b'bcrypt' + cipherName = b"aes256-ctr" + kdfName = b"bcrypt" blockSize = cipher.block_size // 8 keySize = 32 ivSize = blockSize salt = self.secureRandom(ivSize) rounds = 100 - kdfOptions = common.NS(salt) + struct.pack('!L', rounds) + kdfOptions = common.NS(salt) + struct.pack("!L", rounds) else: - cipherName = b'none' - kdfName = b'none' + cipherName = b"none" + kdfName = b"none" blockSize = 8 - kdfOptions = b'' + kdfOptions = b"" check = self.secureRandom(4) - privKeyList = ( - check + check + self.privateBlob() + common.NS(comment or b'')) + privKeyList = check + check + self.privateBlob() + common.NS(comment or b"") padByte = 0 while len(privKeyList) % blockSize: padByte += 1 @@ -1454,26 +1505,28 @@ def _toString_OPENSSH_V1(self, comment=None, passphrase=None): encKey = bcrypt.kdf(passphrase, salt, keySize + ivSize, 100) encryptor = Cipher( cipher(encKey[:keySize]), - modes.CTR(encKey[keySize:keySize + ivSize]), - backend=default_backend() + modes.CTR(encKey[keySize : keySize + ivSize]), + backend=default_backend(), ).encryptor() - encPrivKeyList = ( - encryptor.update(privKeyList) + encryptor.finalize()) + encPrivKeyList = encryptor.update(privKeyList) + encryptor.finalize() else: encPrivKeyList = privKeyList blob = ( - b'openssh-key-v1\0' + - common.NS(cipherName) + - common.NS(kdfName) + common.NS(kdfOptions) + - struct.pack('!L', 1) + - common.NS(self.blob()) + - common.NS(encPrivKeyList)) - b64Data = encodebytes(blob).replace(b'\n', b'') + b"openssh-key-v1\0" + + common.NS(cipherName) + + common.NS(kdfName) + + common.NS(kdfOptions) + + struct.pack("!L", 1) + + common.NS(self.blob()) + + common.NS(encPrivKeyList) + ) + b64Data = encodebytes(blob).replace(b"\n", b"") lines = ( - [b'-----BEGIN OPENSSH PRIVATE KEY-----'] + - [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)] + - [b'-----END OPENSSH PRIVATE KEY-----']) - return b'\n'.join(lines) + b'\n' + [b"-----BEGIN OPENSSH PRIVATE KEY-----"] + + [b64Data[i : i + 64] for i in range(0, len(b64Data), 64)] + + [b"-----END OPENSSH PRIVATE KEY-----"] + ) + return b"\n".join(lines) + b"\n" def _toString_OPENSSH(self, comment=None, passphrase=None): """ @@ -1488,7 +1541,7 @@ def _toString_OPENSSH(self, comment=None, passphrase=None): if self.isPublic(): return self._toPublicOpenSSH(comment=comment) - if self.type() == 'EC': + if self.type() == "EC": # EC keys has complex ASN.1 structure hence we do this this way. if not passphrase: # unencrypted private key @@ -1499,35 +1552,46 @@ def _toString_OPENSSH(self, comment=None, passphrase=None): return self._keyObject.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, - encryptor) - elif self.type() == 'Ed25519': + encryptor, + ) + elif self.type() == "Ed25519": raise BadKeyError( - 'Cannot serialize Ed25519 key to openssh format; ' - 'use openssh_v1 instead.' + "Cannot serialize Ed25519 key to openssh format; " + "use openssh_v1 instead." ) data = self.data() - lines = [b''.join((b'-----BEGIN ', self.type().encode('ascii'), - b' PRIVATE KEY-----'))] - if self.type() == 'RSA': - p, q = data['p'], data['q'] + lines = [ + b"".join( + (b"-----BEGIN ", self.type().encode("ascii"), b" PRIVATE KEY-----") + ) + ] + if self.type() == "RSA": + p, q = data["p"], data["q"] iqmp = rsa.rsa_crt_iqmp(p, q) - objData = (0, data['n'], data['e'], data['d'], p, q, - data['d'] % (p - 1), data['d'] % (q - 1), - iqmp) + objData = ( + 0, + data["n"], + data["e"], + data["d"], + p, + q, + data["d"] % (p - 1), + data["d"] % (q - 1), + iqmp, + ) else: - objData = (0, data['p'], data['q'], data['g'], data['y'], - data['x']) + objData = (0, data["p"], data["q"], data["g"], data["y"], data["x"]) asn1Sequence = univ.Sequence() for index, value in zip(itertools.count(), objData): asn1Sequence.setComponentByPosition(index, univ.Integer(value)) asn1Data = berEncoder.encode(asn1Sequence) if passphrase: iv = self.secureRandom(8) - hexiv = ''.join(['%02X' % (ord(x),) for x in iterbytes(iv)]) - hexiv = hexiv.encode('ascii') - lines.append(b'Proc-Type: 4,ENCRYPTED') - lines.append(b'DEK-Info: DES-EDE3-CBC,' + hexiv + b'\n') + hexiv = "".join(["%02X" % (ord(x),) for x in iterbytes(iv)]) + hexiv = hexiv.encode("ascii") + lines.append(b"Proc-Type: 4,ENCRYPTED") + lines.append(b"DEK-Info: DES-EDE3-CBC," + hexiv + b"\n") ba = md5(passphrase + iv).digest() bb = md5(ba + passphrase + iv).digest() encKey = (ba + bb)[:24] @@ -1535,18 +1599,17 @@ def _toString_OPENSSH(self, comment=None, passphrase=None): asn1Data += six.int2byte(padLen) * padLen encryptor = Cipher( - algorithms.TripleDES(encKey), - modes.CBC(iv), - backend=default_backend() + algorithms.TripleDES(encKey), modes.CBC(iv), backend=default_backend() ).encryptor() asn1Data = encryptor.update(asn1Data) + encryptor.finalize() - b64Data = encodebytes(asn1Data).replace(b'\n', b'') - lines += [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)] - lines.append(b''.join((b'-----END ', self.type().encode('ascii'), - b' PRIVATE KEY-----'))) - return b'\n'.join(lines) + b64Data = encodebytes(asn1Data).replace(b"\n", b"") + lines += [b64Data[i : i + 64] for i in range(0, len(b64Data), 64)] + lines.append( + b"".join((b"-----END ", self.type().encode("ascii"), b" PRIVATE KEY-----")) + ) + return b"\n".join(lines) def _toString_AGENTV3(self, **kwargs): """ @@ -1557,13 +1620,18 @@ def _toString_AGENTV3(self, **kwargs): """ data = self.data() if not self.isPublic(): - if self.type() == 'RSA': - values = (data['e'], data['d'], data['n'], data['u'], - data['p'], data['q']) - elif self.type() == 'DSA': - values = (data['p'], data['q'], data['g'], data['y'], - data['x']) - return common.NS(self.sshType()) + b''.join(map(common.MP, values)) + if self.type() == "RSA": + values = ( + data["e"], + data["d"], + data["n"], + data["u"], + data["p"], + data["q"], + ) + elif self.type() == "DSA": + values = (data["p"], data["q"], data["g"], data["y"], data["x"]) + return common.NS(self.sshType()) + b"".join(map(common.MP, values)) def sign(self, data, signatureType=None): """ @@ -1578,7 +1646,7 @@ def sign(self, data, signatureType=None): @return: A signature for the given data. """ if self.isPublic(): - raise KeyCertException('A private key is require to sign data.') + raise KeyCertException("A private key is require to sign data.") keyType = self.type() @@ -1596,11 +1664,11 @@ def sign(self, data, signatureType=None): "defined for {} keys".format(signatureType, keyType) ) - if keyType == 'RSA': + if keyType == "RSA": sig = self._keyObject.sign(data, padding.PKCS1v15(), hashAlgorithm) ret = common.NS(sig) - elif keyType == 'DSA': + elif keyType == "DSA": sig = self._keyObject.sign(data, hashAlgorithm) (r, s) = decode_dss_signature(sig) # SSH insists that the DSS signature blob be two 160-bit integers @@ -1609,7 +1677,7 @@ def sign(self, data, signatureType=None): # Make sure they are padded out to 160 bits (20 bytes each) ret = common.NS(int_to_bytes(r, 20) + int_to_bytes(s, 20)) - elif keyType == 'EC': # Pragma: no branch + elif keyType == "EC": # Pragma: no branch signature = self._keyObject.sign(data, ec.ECDSA(hashAlgorithm)) (r, s) = decode_dss_signature(signature) @@ -1637,7 +1705,7 @@ def sign(self, data, signatureType=None): ret = common.NS(common.NS(rb) + common.NS(sb)) - elif keyType == 'Ed25519': + elif keyType == "Ed25519": ret = common.NS(self._keyObject.sign(data)) return common.NS(signatureType) + ret @@ -1657,7 +1725,7 @@ def verify(self, signature, data): """ if len(signature) == 40: # DSA key with no padding - signatureType, signature = b'ssh-dss', common.NS(signature) + signatureType, signature = b"ssh-dss", common.NS(signature) else: signatureType, signature = common.getNS(signature) @@ -1666,7 +1734,7 @@ def verify(self, signature, data): return False keyType = self.type() - if keyType == 'RSA': + if keyType == "RSA": k = self._keyObject if not self.isPublic(): k = k.public_key() @@ -1676,21 +1744,21 @@ def verify(self, signature, data): padding.PKCS1v15(), hashAlgorithm, ) - elif keyType == 'DSA': + elif keyType == "DSA": concatenatedSignature = common.getNS(signature)[0] - r = int_from_bytes(concatenatedSignature[:20], 'big') - s = int_from_bytes(concatenatedSignature[20:], 'big') + r = int_from_bytes(concatenatedSignature[:20], "big") + s = int_from_bytes(concatenatedSignature[20:], "big") signature = encode_dss_signature(r, s) k = self._keyObject if not self.isPublic(): k = k.public_key() args = (signature, data, hashAlgorithm) - elif keyType == 'EC': # Pragma: no branch + elif keyType == "EC": # Pragma: no branch concatenatedSignature = common.getNS(signature)[0] rstr, sstr, rest = common.getNS(concatenatedSignature, 2) - r = int_from_bytes(rstr, 'big') - s = int_from_bytes(sstr, 'big') + r = int_from_bytes(rstr, "big") + s = int_from_bytes(sstr, "big") signature = encode_dss_signature(r, s) k = self._keyObject @@ -1699,7 +1767,7 @@ def verify(self, signature, data): args = (signature, data, ec.ECDSA(hashAlgorithm)) - elif keyType == 'Ed25519': + elif keyType == "Ed25519": k = self._keyObject if not self.isPublic(): k = k.public_key() @@ -1726,45 +1794,43 @@ def generate(cls, key_type=DEFAULT_KEY_TYPE, key_size=None): `key_size` is ignored for ed25519. """ if not key_type: - key_type = 'not-specified' + key_type = "not-specified" key_type = key_type.lower() if not key_size: - if key_type == 'ecdsa': + if key_type == "ecdsa": key_size = 384 else: key_size = DEFAULT_KEY_SIZE key = None try: - if key_type == u'rsa': + if key_type == "rsa": key = rsa.generate_private_key( public_exponent=65537, key_size=key_size, - ) - elif key_type == u'dsa': + ) + elif key_type == "dsa": key = dsa.generate_private_key(key_size=key_size) - elif key_type == 'ecdsa': + elif key_type == "ecdsa": try: curve = _ecSizeTable[key_size] except KeyError: raise KeyCertException( - 'Wrong key size "%s". Supported: %s.' % ( - key_size, - ', '.join([str(s) for s in _ecSizeTable.keys()]))) + 'Wrong key size "%s". Supported: %s.' + % (key_size, ", ".join([str(s) for s in _ecSizeTable.keys()])) + ) key = ec.generate_private_key(curve) - elif key_type == 'ed25519': + elif key_type == "ed25519": key = ed25519.Ed25519PrivateKey.generate() else: raise KeyCertException('Unknown key type "%s".' % (key_type)) except ValueError as error: - raise KeyCertException( - u'Wrong key size "%d". %s' % (key_size, error)) + raise KeyCertException('Wrong key size "%d". %s' % (key_size, error)) return cls(key) - @classmethod def getKeyFormat(cls, data): """ @@ -1772,23 +1838,23 @@ def getKeyFormat(cls, data): """ key_type = cls._guessStringType(data) human_readable = { - 'public_openssh': 'OpenSSH Public', - 'private_openssh': 'OpenSSH Private old format', - 'private_openssh_v1': 'OpenSSH Private new format', - 'public_sshcom': 'SSH.com Public', - 'private_sshcom': 'SSH.com Private', - 'private_putty': 'PuTTY Private v2', - 'private_putty_v3': 'PuTTY Private v3', - 'public_lsh': 'LSH Public', - 'private_lsh': 'LSH Private', - 'public_x509_certificate': 'X509 Certificate', - 'public_x509': 'X509 Public', - 'public_pkcs1_rsa': 'PKCS#1 RSA Public', - 'private_pkcs8': 'PKCS#8 Private', - 'private_encrypted_pkcs8': 'PKCS#8 Encrypted Private', - } - - return human_readable.get(key_type, 'Unknown format') + "public_openssh": "OpenSSH Public", + "private_openssh": "OpenSSH Private old format", + "private_openssh_v1": "OpenSSH Private new format", + "public_sshcom": "SSH.com Public", + "private_sshcom": "SSH.com Private", + "private_putty": "PuTTY Private v2", + "private_putty_v3": "PuTTY Private v3", + "public_lsh": "LSH Public", + "private_lsh": "LSH Private", + "public_x509_certificate": "X509 Certificate", + "public_x509": "X509 Public", + "public_pkcs1_rsa": "PKCS#1 RSA Public", + "private_pkcs8": "PKCS#8 Private", + "private_encrypted_pkcs8": "PKCS#8 Encrypted Private", + } + + return human_readable.get(key_type, "Unknown format") @staticmethod def _getSSHCOMKeyContent(data): @@ -1796,7 +1862,7 @@ def _getSSHCOMKeyContent(data): Return the raw content of the SSH.com key (private or public) without armor and headers. """ - lines = data.decode('utf-8').strip().splitlines() + lines = data.decode("utf-8").strip().splitlines() # Split in lines, ignoring the first and last armors. lines = lines[1:-1] @@ -1812,14 +1878,14 @@ def _getSSHCOMKeyContent(data): if continuation: # We have a continued line. # ignore it and check if this line still continues. - if not line.endswith('\\'): + if not line.endswith("\\"): continuation = False continue - if ':' in line: + if ":" in line: # We have a header line # Ignore it and check if this is a long header. - if line.endswith('\\'): + if line.endswith("\\"): continuation = True continue # This is not a header and not a continuation, so it must be the @@ -1828,8 +1894,8 @@ def _getSSHCOMKeyContent(data): lines.insert(0, line) break - content = ''.join(lines) - return base64.decodestring(content.encode('ascii')) + content = "".join(lines) + return base64.decodestring(content.encode("ascii")) @classmethod def _fromString_PUBLIC_SSHCOM(cls, data): @@ -1861,7 +1927,7 @@ def _fromString_PUBLIC_SSHCOM(cls, data): @return: A {Crypto.PublicKey.pubkey.pubkey} object @raises BadKeyError: if the blob type is unknown. """ - if not data.strip().endswith(b'---- END SSH2 PUBLIC KEY ----'): + if not data.strip().endswith(b"---- END SSH2 PUBLIC KEY ----"): raise BadKeyError("Fail to find END tag for SSH.com key.") blob = cls._getSSHCOMKeyContent(data) @@ -1919,62 +1985,65 @@ def _fromString_PRIVATE_SSHCOM(cls, data, passphrase): * a passphrase is provided for an unencrypted key """ blob = cls._getSSHCOMKeyContent(data) - magic_number = struct.unpack('>I', blob[:4])[0] + magic_number = struct.unpack(">I", blob[:4])[0] if magic_number != SSHCOM_MAGIC_NUMBER: raise BadKeyError( - 'Bad magic number for SSH.com key "%s"' % ( - force_unicode(magic_number),)) - struct.unpack('>I', blob[4:8])[0] # Ignore value for total size. + 'Bad magic number for SSH.com key "%s"' % (force_unicode(magic_number),) + ) + struct.unpack(">I", blob[4:8])[0] # Ignore value for total size. type_signature, rest = common.getNS(blob[8:]) key_type = None - if type_signature.startswith(b'if-modn{sign{rsa'): - key_type = 'rsa' - elif type_signature.startswith(b'dl-modp{sign{dsa'): - key_type = 'dsa' + if type_signature.startswith(b"if-modn{sign{rsa"): + key_type = "rsa" + elif type_signature.startswith(b"dl-modp{sign{dsa"): + key_type = "dsa" else: raise BadKeyError( - 'Unknown SSH.com key type "%s"' % force_unicode(type_signature)) + 'Unknown SSH.com key type "%s"' % force_unicode(type_signature) + ) cipher_type, rest = common.getNS(rest) encrypted_blob, _ = common.getNS(rest) encryption_key = None - if cipher_type.lower() == b'none': + if cipher_type.lower() == b"none": if passphrase: - raise BadKeyError('SSH.com key not encrypted') + raise BadKeyError("SSH.com key not encrypted") key_data = encrypted_blob - elif cipher_type.lower() == b'3des-cbc': + elif cipher_type.lower() == b"3des-cbc": if not passphrase: raise EncryptedKeyError( - 'Passphrase must be provided for an encrypted key.') + "Passphrase must be provided for an encrypted key." + ) encryption_key = cls._getDES3EncryptionKey(passphrase) decryptor = Cipher( algorithms.TripleDES(encryption_key), - modes.CBC(b'\x00' * 8), - backend=default_backend() + modes.CBC(b"\x00" * 8), + backend=default_backend(), ).decryptor() key_data = decryptor.update(encrypted_blob) + decryptor.finalize() else: raise BadKeyError( - 'Encryption method not supported: "%s"' % ( - force_unicode(cipher_type[:30]))) + 'Encryption method not supported: "%s"' + % (force_unicode(cipher_type[:30])) + ) try: payload, _ = common.getNS(key_data) - if key_type == 'rsa': + if key_type == "rsa": e, d, n, u, q, p, rest = cls._unpackMPSSHCOM(payload, 6) return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) - if key_type == 'dsa': + if key_type == "dsa": # First 32bit is an uint with value 0. We just ignore it. p, g, q, y, x, rest = cls._unpackMPSSHCOM(payload[4:], 5) return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) except struct.error: if encryption_key: - raise EncryptedKeyError('Bad password or bad key format.') + raise EncryptedKeyError("Bad password or bad key format.") else: - BadKeyError('Failed to parse payload.') + BadKeyError("Failed to parse payload.") @staticmethod def _getDES3EncryptionKey(passphrase): @@ -1998,9 +2067,9 @@ def _unpackMPSSHCOM(data, count=1): c = 0 mp = [] for i in range(count): - length = struct.unpack('>I', data[c:c + 4])[0] + length = struct.unpack(">I", data[c : c + 4])[0] length = (length + 7) // 8 - mp.append(int_from_bytes(data[c + 4:c + 4 + length], 'big')) + mp.append(int_from_bytes(data[c + 4 : c + 4 + length], "big")) c += length + 4 return tuple(mp) + (data[c:],) @@ -2012,12 +2081,12 @@ def _packMPSSHCOM(number): Similar to Twisted MP method. """ if number == 0: - return '\000' * 4 + return "\000" * 4 wire_number = int_to_bytes(number) wire_length = (len(wire_number) * 8) - 7 - return struct.pack('>L', wire_length) + wire_number + return struct.pack(">L", wire_length) + wire_number def _toString_SSHCOM(self, comment=None, passphrase=None): """ @@ -2041,15 +2110,15 @@ def _toString_SSHCOM_public(self, extra): """ Return the public SSH.com string. """ - lines = ['---- BEGIN SSH2 PUBLIC KEY ----'] + lines = ["---- BEGIN SSH2 PUBLIC KEY ----"] if extra: line = 'Comment: "%s"' % (extra,) - lines.append('\\\n'.join(textwrap.wrap(line, 70))) + lines.append("\\\n".join(textwrap.wrap(line, 70))) - base64Data = base64.b64encode(self.blob()).decode('ascii') + base64Data = base64.b64encode(self.blob()).decode("ascii") lines.extend(textwrap.wrap(base64Data, 70)) - lines.append('---- END SSH2 PUBLIC KEY ----') - return '\n'.join(lines).encode('utf-8') + lines.append("---- END SSH2 PUBLIC KEY ----") + return "\n".join(lines).encode("utf-8") def _toString_SSHCOM_private(self, extra): """ @@ -2057,33 +2126,32 @@ def _toString_SSHCOM_private(self, extra): """ # Now we are left with a private key. # Both encrypted and unencrypted keys have the same armor. - lines = ['---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----'] + lines = ["---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----"] type_signature = None payload_blob = None data = self.data() type = self.type() - if type == 'RSA': - type_signature = ( - 'if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}}') + if type == "RSA": + type_signature = "if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}}" payload_blob = ( - self._packMPSSHCOM(data['e']) + - self._packMPSSHCOM(data['d']) + - self._packMPSSHCOM(data['n']) + - self._packMPSSHCOM(data['u']) + - self._packMPSSHCOM(data['q']) + - self._packMPSSHCOM(data['p']) - ) - elif type == 'DSA': - type_signature = 'dl-modp{sign{dsa-nist-sha1},dh{plain}}' + self._packMPSSHCOM(data["e"]) + + self._packMPSSHCOM(data["d"]) + + self._packMPSSHCOM(data["n"]) + + self._packMPSSHCOM(data["u"]) + + self._packMPSSHCOM(data["q"]) + + self._packMPSSHCOM(data["p"]) + ) + elif type == "DSA": + type_signature = "dl-modp{sign{dsa-nist-sha1},dh{plain}}" payload_blob = ( - struct.pack('>I', 0) + - self._packMPSSHCOM(data['p']) + - self._packMPSSHCOM(data['g']) + - self._packMPSSHCOM(data['q']) + - self._packMPSSHCOM(data['y']) + - self._packMPSSHCOM(data['x']) - ) + struct.pack(">I", 0) + + self._packMPSSHCOM(data["p"]) + + self._packMPSSHCOM(data["g"]) + + self._packMPSSHCOM(data["q"]) + + self._packMPSSHCOM(data["y"]) + + self._packMPSSHCOM(data["x"]) + ) else: # pragma: no cover raise BadKeyError('Unsupported key type "%s"' % force_unicode(type)) @@ -2091,20 +2159,19 @@ def _toString_SSHCOM_private(self, extra): if extra: # We got a password, so encrypt it. - cipher_type = '3des-cbc' - padding = b'\x00' * (8 - (len(payload_blob) % 8)) + cipher_type = "3des-cbc" + padding = b"\x00" * (8 - (len(payload_blob) % 8)) payload_blob = payload_blob + padding encryption_key = self._getDES3EncryptionKey(extra) encryptor = Cipher( algorithms.TripleDES(encryption_key), - modes.CBC(b'\x00' * 8), - backend=default_backend() + modes.CBC(b"\x00" * 8), + backend=default_backend(), ).encryptor() - encrypted_blob = ( - encryptor.update(payload_blob) + encryptor.finalize()) + encrypted_blob = encryptor.update(payload_blob) + encryptor.finalize() else: - cipher_type = 'none' + cipher_type = "none" encrypted_blob = payload_blob # We first create the content without magic number and @@ -2114,20 +2181,20 @@ def _toString_SSHCOM_private(self, extra): common.NS(type_signature) + common.NS(cipher_type) + common.NS(encrypted_blob) - ) + ) total_size = 8 + len(blob) blob = ( - struct.pack('>I', SSHCOM_MAGIC_NUMBER) - + struct.pack('>I', total_size) + struct.pack(">I", SSHCOM_MAGIC_NUMBER) + + struct.pack(">I", total_size) + blob - ) + ) # In the end, encode in base 64 and wrap it. - blob = base64.b64encode(blob).decode('ascii') + blob = base64.b64encode(blob).decode("ascii") lines.extend(textwrap.wrap(blob, 70)) - lines.append('---- END SSH2 ENCRYPTED PRIVATE KEY ----') - return '\n'.join(lines).encode('utf-8') + lines.append("---- END SSH2 ENCRYPTED PRIVATE KEY ----") + return "\n".join(lines).encode("utf-8") @classmethod def _fromString_PRIVATE_PUTTY(cls, data, passphrase): @@ -2193,119 +2260,121 @@ def _fromString_PRIVATE_PUTTY(cls, data, passphrase): Version 2 was introduced in PuTTY 0.52. Version 1 was an in-development format used in 0.52 snapshot """ - lines = data.decode('utf-8').strip().splitlines() + lines = data.decode("utf-8").strip().splitlines() key_type = lines[0][22:].strip().lower() - if key_type not in [ - 'ssh-rsa', - 'ssh-dss', - 'ssh-ed25519', - ] and key_type.encode('ascii') not in _curveTable: + if ( + key_type + not in [ + "ssh-rsa", + "ssh-dss", + "ssh-ed25519", + ] + and key_type.encode("ascii") not in _curveTable + ): raise BadKeyError( - 'Unsupported key type: "%s"' % force_unicode(key_type[:30])) + 'Unsupported key type: "%s"' % force_unicode(key_type[:30]) + ) encryption_type = lines[1][11:].strip().lower() - if encryption_type == 'none': + if encryption_type == "none": if passphrase: - raise BadKeyError('PuTTY key not encrypted') - elif encryption_type != 'aes256-cbc': + raise BadKeyError("PuTTY key not encrypted") + elif encryption_type != "aes256-cbc": raise BadKeyError( - 'Unsupported encryption type: "%s"' % force_unicode( - encryption_type[:30])) + 'Unsupported encryption type: "%s"' + % force_unicode(encryption_type[:30]) + ) comment = lines[2][9:].strip() public_count = int(lines[3][14:].strip()) - base64_content = ''.join(lines[ - 4: - 4 + public_count - ]) - public_blob = base64.decodestring(base64_content.encode('utf-8')) + base64_content = "".join(lines[4 : 4 + public_count]) + public_blob = base64.decodestring(base64_content.encode("utf-8")) public_type, public_payload = common.getNS(public_blob) - if public_type.decode('ascii').lower() != key_type: + if public_type.decode("ascii").lower() != key_type: raise BadKeyError( - 'Mismatch key type. Header has "%s", public has "%s"' % ( - force_unicode(key_type[:30]), - force_unicode(public_type[:30]))) + 'Mismatch key type. Header has "%s", public has "%s"' + % (force_unicode(key_type[:30]), force_unicode(public_type[:30])) + ) # We skip 4 lines so far and the total public lines. private_start_line = 4 + public_count private_count = int(lines[private_start_line][15:].strip()) - base64_content = ''.join(lines[ - private_start_line + 1: - private_start_line + 1 + private_count - ]) - private_blob = base64.decodestring(base64_content.encode('ascii')) + base64_content = "".join( + lines[private_start_line + 1 : private_start_line + 1 + private_count] + ) + private_blob = base64.decodestring(base64_content.encode("ascii")) private_mac = lines[-1][12:].strip() hmac_key = PUTTY_HMAC_KEY encryption_key = None - if encryption_type == 'aes256-cbc': + if encryption_type == "aes256-cbc": if not passphrase: raise EncryptedKeyError( - 'Passphrase must be provided for an encrypted key.') + "Passphrase must be provided for an encrypted key." + ) hmac_key += passphrase encryption_key = cls._getPuttyAES256EncryptionKey(passphrase) decryptor = Cipher( algorithms.AES(encryption_key), - modes.CBC(b'\x00' * 16), - backend=default_backend() + modes.CBC(b"\x00" * 16), + backend=default_backend(), ).decryptor() - private_blob = ( - decryptor.update(private_blob) + decryptor.finalize()) + private_blob = decryptor.update(private_blob) + decryptor.finalize() # I have no idea why these values are packed form HMAC as net strings. hmac_data = ( - common.NS(key_type) + - common.NS(encryption_type) + - common.NS(comment) + - common.NS(public_blob) + - common.NS(private_blob) - ) + common.NS(key_type) + + common.NS(encryption_type) + + common.NS(comment) + + common.NS(public_blob) + + common.NS(private_blob) + ) hmac_key = sha1(hmac_key).digest() computed_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest() if private_mac != computed_mac: if encryption_key: - raise EncryptedKeyError('Bad password or HMAC mismatch.') + raise EncryptedKeyError("Bad password or HMAC mismatch.") else: raise BadKeyError( - 'HMAC mismatch: file declare "%s", actual is "%s"' % ( - force_unicode(private_mac), - force_unicode(computed_mac))) + 'HMAC mismatch: file declare "%s", actual is "%s"' + % (force_unicode(private_mac), force_unicode(computed_mac)) + ) - if key_type == 'ssh-rsa': + if key_type == "ssh-rsa": e, n, _ = common.getMP(public_payload, count=2) d, p, q, u, _ = common.getMP(private_blob, count=4) return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) - if key_type == 'ssh-dss': + if key_type == "ssh-dss": p, q, g, y, _ = common.getMP(public_payload, count=4) x, _ = common.getMP(private_blob) return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) - if key_type == 'ssh-ed25519': + if key_type == "ssh-ed25519": a, _ = common.getNS(public_payload) k, _ = common.getNS(private_blob) return cls._fromEd25519Components(a=a, k=k) - if key_type.encode('ascii') in _curveTable: - curve = _curveTable[key_type.encode('ascii')] + if key_type.encode("ascii") in _curveTable: + curve = _curveTable[key_type.encode("ascii")] curveName, q, _ = common.getNS(public_payload, 2) if curveName != _secToNist[curve.name]: raise BadKeyError( - 'ECDSA curve name "%s" does not match key type "%s"' % ( - force_unicode(curveName), - force_unicode(key_type))) + 'ECDSA curve name "%s" does not match key type "%s"' + % (force_unicode(curveName), force_unicode(key_type)) + ) privateValue, _ = common.getMP(private_blob) return cls._fromECEncodedPoint( encodedPoint=q, - curve=key_type.encode('ascii'), + curve=key_type.encode("ascii"), privateValue=privateValue, - ) + ) @staticmethod def _getPuttyAES256EncryptionKey(passphrase): @@ -2314,8 +2383,8 @@ def _getPuttyAES256EncryptionKey(passphrase): version 2 of the format. """ key_size = 32 - part_1 = sha1(b'\x00\x00\x00\x00' + passphrase).digest() - part_2 = sha1(b'\x00\x00\x00\x01' + passphrase).digest() + part_1 = sha1(b"\x00\x00\x00\x00" + passphrase).digest() + part_2 = sha1(b"\x00\x00\x00\x01" + passphrase).digest() return (part_1 + part_2)[:key_size] def _toString_PUTTY(self, comment=None, passphrase=None): @@ -2350,44 +2419,39 @@ def _toString_PUTTY_private(self, extra): aes_block_size = 16 lines = [] key_type = self.sshType() - comment = 'Exported by chevah-keycert.' + comment = "Exported by chevah-keycert." data = self.data() hmac_key = PUTTY_HMAC_KEY if extra: - encryption_type = 'aes256-cbc' + encryption_type = "aes256-cbc" hmac_key += extra else: - encryption_type = 'none' + encryption_type = "none" - if key_type == b'ssh-rsa': + if key_type == b"ssh-rsa": public_blob = ( - common.NS(key_type) + - common.MP(data['e']) + - common.MP(data['n']) - ) + common.NS(key_type) + common.MP(data["e"]) + common.MP(data["n"]) + ) private_blob = ( - common.MP(data['d']) + - common.MP(data['p']) + - common.MP(data['q']) + - common.MP(data['u']) - ) - elif key_type == b'ssh-dss': + common.MP(data["d"]) + + common.MP(data["p"]) + + common.MP(data["q"]) + + common.MP(data["u"]) + ) + elif key_type == b"ssh-dss": public_blob = ( - common.NS(key_type) + - common.MP(data['p']) + - common.MP(data['q']) + - common.MP(data['g']) + - common.MP(data['y']) - ) - private_blob = common.MP(data['x']) + common.NS(key_type) + + common.MP(data["p"]) + + common.MP(data["q"]) + + common.MP(data["g"]) + + common.MP(data["y"]) + ) + private_blob = common.MP(data["x"]) - elif key_type == b'ssh-ed25519': - public_blob = ( - common.NS(key_type) + - common.NS(data['a']) - ) - private_blob = common.NS(data['k']) + elif key_type == b"ssh-ed25519": + public_blob = common.NS(key_type) + common.NS(data["a"]) + private_blob = common.NS(data["k"]) elif key_type in _curveTable: @@ -2395,16 +2459,14 @@ def _toString_PUTTY_private(self, extra): encode_point = self._keyObject.public_key().public_bytes( serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint, - ) + ) public_blob = ( - common.NS(key_type) + - common.NS(curve_name) + - common.NS(encode_point) - ) - private_blob = common.MP(data['privateValue']) + common.NS(key_type) + common.NS(curve_name) + common.NS(encode_point) + ) + private_blob = common.MP(data["privateValue"]) else: # pragma: no cover - raise BadKeyError('Unsupported key type.') + raise BadKeyError("Unsupported key type.") private_blob_plain = private_blob private_blob_encrypted = private_blob @@ -2412,42 +2474,42 @@ def _toString_PUTTY_private(self, extra): if extra: # Encryption is requested. # Padding is required for encryption. - padding_size = -1 * ( - (len(private_blob) % aes_block_size) - aes_block_size) - private_blob_plain += b'\x00' * padding_size + padding_size = -1 * ((len(private_blob) % aes_block_size) - aes_block_size) + private_blob_plain += b"\x00" * padding_size encryption_key = self._getPuttyAES256EncryptionKey(extra) encryptor = Cipher( algorithms.AES(encryption_key), - modes.CBC(b'\x00' * aes_block_size), - backend=default_backend() + modes.CBC(b"\x00" * aes_block_size), + backend=default_backend(), ).encryptor() private_blob_encrypted = ( - encryptor.update(private_blob_plain) + encryptor.finalize()) + encryptor.update(private_blob_plain) + encryptor.finalize() + ) - public_lines = textwrap.wrap( - base64.b64encode(public_blob).decode('ascii'), 64) + public_lines = textwrap.wrap(base64.b64encode(public_blob).decode("ascii"), 64) private_lines = textwrap.wrap( - base64.b64encode(private_blob_encrypted).decode('ascii'), 64) + base64.b64encode(private_blob_encrypted).decode("ascii"), 64 + ) hmac_data = ( - common.NS(key_type) + - common.NS(encryption_type) + - common.NS(comment) + - common.NS(public_blob) + - common.NS(private_blob_plain) - ) + common.NS(key_type) + + common.NS(encryption_type) + + common.NS(comment) + + common.NS(public_blob) + + common.NS(private_blob_plain) + ) hmac_key = sha1(hmac_key).digest() private_mac = hmac.new(hmac_key, hmac_data, sha1).hexdigest() - lines.append('PuTTY-User-Key-File-2: %s' % key_type.decode('ascii')) - lines.append('Encryption: %s' % encryption_type) - lines.append('Comment: %s' % comment) - lines.append('Public-Lines: %s' % len(public_lines)) + lines.append("PuTTY-User-Key-File-2: %s" % key_type.decode("ascii")) + lines.append("Encryption: %s" % encryption_type) + lines.append("Comment: %s" % comment) + lines.append("Public-Lines: %s" % len(public_lines)) lines.extend(public_lines) - lines.append('Private-Lines: %s' % len(private_lines)) + lines.append("Private-Lines: %s" % len(private_lines)) lines.extend(private_lines) - lines.append('Private-MAC: %s' % private_mac) - return '\r\n'.join(lines).encode('utf-8') + lines.append("Private-MAC: %s" % private_mac) + return "\r\n".join(lines).encode("utf-8") @classmethod def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): @@ -2526,27 +2588,33 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): Version 3 was introduced in PuTTY 0.75. """ - lines = data.decode('utf-8').strip().splitlines() + lines = data.decode("utf-8").strip().splitlines() key_type = lines[0][22:].strip().lower() - if key_type not in [ - 'ssh-rsa', - 'ssh-dss', - 'ssh-ed25519', - ] and key_type.encode('ascii') not in _curveTable: + if ( + key_type + not in [ + "ssh-rsa", + "ssh-dss", + "ssh-ed25519", + ] + and key_type.encode("ascii") not in _curveTable + ): raise BadKeyError( - 'Unsupported key type: "%s"' % force_unicode(key_type[:30])) + 'Unsupported key type: "%s"' % force_unicode(key_type[:30]) + ) encryption_type = lines[1][11:].strip().lower() private_offset = 0 - if encryption_type == 'none': + if encryption_type == "none": if passphrase: - raise BadKeyError('PuTTY key not encrypted') - elif encryption_type != 'aes256-cbc': + raise BadKeyError("PuTTY key not encrypted") + elif encryption_type != "aes256-cbc": raise BadKeyError( - 'Unsupported encryption type: "%s"' % force_unicode( - encryption_type[:30])) + 'Unsupported encryption type: "%s"' + % force_unicode(encryption_type[:30]) + ) else: # Key is encrypted. private_offset = 5 @@ -2554,100 +2622,96 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): comment = lines[2][9:].strip() public_count = int(lines[3][14:].strip()) - base64_content = ''.join(lines[ - 4: - 4 + public_count - ]) - public_blob = base64.decodestring(base64_content.encode('utf-8')) + base64_content = "".join(lines[4 : 4 + public_count]) + public_blob = base64.decodestring(base64_content.encode("utf-8")) public_type, public_payload = common.getNS(public_blob) - if public_type.decode('ascii').lower() != key_type: + if public_type.decode("ascii").lower() != key_type: raise BadKeyError( - 'Mismatch key type. Header has "%s", public has "%s"' % ( - force_unicode(key_type[:30]), - force_unicode(public_type[:30]))) + 'Mismatch key type. Header has "%s", public has "%s"' + % (force_unicode(key_type[:30]), force_unicode(public_type[:30])) + ) # We skip 4 lines so far and the total public lines and any option # private key derivation parameters. private_start_line = 4 + public_count + private_offset private_count = int(lines[private_start_line][15:].strip()) - base64_content = ''.join(lines[ - private_start_line + 1: - private_start_line + 1 + private_count - ]) - private_blob = base64.decodestring(base64_content.encode('ascii')) + base64_content = "".join( + lines[private_start_line + 1 : private_start_line + 1 + private_count] + ) + private_blob = base64.decodestring(base64_content.encode("ascii")) private_mac = lines[-1][12:].strip() # Default for non-encryption is empty HMAC key. # THis is updated later if we have encrypted key. - hmac_key = b'' + hmac_key = b"" encryption_key = None - if encryption_type == 'aes256-cbc': + if encryption_type == "aes256-cbc": if not passphrase: raise EncryptedKeyError( - 'Passphrase must be provided for an encrypted key.') + "Passphrase must be provided for an encrypted key." + ) encryption_key, iv, hmac_key = cls._getPuttyAES256EncryptionKey_v3( - lines[4 + public_count:private_start_line], - passphrase) + lines[4 + public_count : private_start_line], passphrase + ) decryptor = Cipher( algorithms.AES256(encryption_key), modes.CBC(iv), - backend=default_backend() + backend=default_backend(), ).decryptor() - private_blob = ( - decryptor.update(private_blob) + decryptor.finalize()) + private_blob = decryptor.update(private_blob) + decryptor.finalize() # I have no idea why these values are packed form HMAC as net strings. hmac_data = ( - common.NS(key_type) + - common.NS(encryption_type) + - common.NS(comment) + - common.NS(public_blob) + - common.NS(private_blob) - ) + common.NS(key_type) + + common.NS(encryption_type) + + common.NS(comment) + + common.NS(public_blob) + + common.NS(private_blob) + ) computed_mac = hmac.new(hmac_key, hmac_data, sha256).hexdigest() if private_mac != computed_mac: if encryption_key: - raise EncryptedKeyError('Bad password or HMAC mismatch.') + raise EncryptedKeyError("Bad password or HMAC mismatch.") else: raise BadKeyError( - 'HMAC mismatch: file declare "%s", actual is "%s"' % ( - force_unicode(private_mac), - force_unicode(computed_mac))) + 'HMAC mismatch: file declare "%s", actual is "%s"' + % (force_unicode(private_mac), force_unicode(computed_mac)) + ) - if key_type == 'ssh-rsa': + if key_type == "ssh-rsa": e, n, _ = common.getMP(public_payload, count=2) d, p, q, u, _ = common.getMP(private_blob, count=4) return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) - if key_type == 'ssh-dss': + if key_type == "ssh-dss": p, q, g, y, _ = common.getMP(public_payload, count=4) x, _ = common.getMP(private_blob) return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) - if key_type == 'ssh-ed25519': + if key_type == "ssh-ed25519": a, _ = common.getNS(public_payload) k, _ = common.getNS(private_blob) return cls._fromEd25519Components(a=a, k=k) - if key_type.encode('ascii') in _curveTable: - curve = _curveTable[key_type.encode('ascii')] + if key_type.encode("ascii") in _curveTable: + curve = _curveTable[key_type.encode("ascii")] curveName, q, _ = common.getNS(public_payload, 2) if curveName != _secToNist[curve.name]: raise BadKeyError( - 'ECDSA curve name "%s" does not match key type "%s"' % ( - force_unicode(curveName), - force_unicode(key_type))) + 'ECDSA curve name "%s" does not match key type "%s"' + % (force_unicode(curveName), force_unicode(key_type)) + ) privateValue, _ = common.getMP(private_blob) return cls._fromECEncodedPoint( encodedPoint=q, - curve=key_type.encode('ascii'), + curve=key_type.encode("ascii"), privateValue=privateValue, - ) + ) @classmethod def _getPuttyAES256EncryptionKey_v3(cls, headers, passphrase): @@ -2657,31 +2721,31 @@ def _getPuttyAES256EncryptionKey_v3(cls, headers, passphrase): parameters = cls._getPuttyEncryptionKeyParameters(headers) argon_type = low_level.Type.ID - if parameters['Key-Derivation'] == 'Argon2id': + if parameters["Key-Derivation"] == "Argon2id": argon_type = low_level.Type.ID - elif parameters['Key-Derivation'] == 'Argon2i': + elif parameters["Key-Derivation"] == "Argon2i": argon_type = low_level.Type.I - elif parameters['Key-Derivation'] == 'Argon2d': + elif parameters["Key-Derivation"] == "Argon2d": argon_type = low_level.Type.D else: - raise BadKeyError('Key-Derivation algorithm not supported.') + raise BadKeyError("Key-Derivation algorithm not supported.") result = low_level.hash_secret_raw( secret=passphrase, - salt=bytes.fromhex(parameters['Argon2-Salt']), - time_cost=int(parameters['Argon2-Passes']), - memory_cost=int(parameters['Argon2-Memory']), - parallelism=int(parameters['Argon2-Parallelism']), + salt=bytes.fromhex(parameters["Argon2-Salt"]), + time_cost=int(parameters["Argon2-Passes"]), + memory_cost=int(parameters["Argon2-Memory"]), + parallelism=int(parameters["Argon2-Parallelism"]), type=argon_type, # cipher key length + IV length + MAC key length hash_len=80, version=19, - ) + ) return ( result[:32], result[32:48], result[48:], - ) + ) @classmethod def _getPuttyEncryptionKeyParameters(cls, headers): @@ -2691,19 +2755,22 @@ def _getPuttyEncryptionKeyParameters(cls, headers): """ result = {} for line in headers: - parts = line.split(':', 1) + parts = line.split(":", 1) result[parts[0].strip()] = parts[1].strip() - expected_headers = set([ - 'Key-Derivation', - 'Argon2-Memory', - 'Argon2-Passes', - 'Argon2-Parallelism', - 'Argon2-Salt', - ]) + expected_headers = set( + [ + "Key-Derivation", + "Argon2-Memory", + "Argon2-Passes", + "Argon2-Parallelism", + "Argon2-Salt", + ] + ) if expected_headers != set(result.keys()): raise BadKeyError( - 'Putty v3 encrypted key has invalid key derivation headers.') + "Putty v3 encrypted key has invalid key derivation headers." + ) return result def _toString_PUTTY_V3(self, comment=None, passphrase=None): @@ -2736,45 +2803,40 @@ def _toString_PUTTY_V3_private(self, extra): aes_block_size = 16 lines = [] key_type = self.sshType() - comment = 'Exported by chevah-keycert.' + comment = "Exported by chevah-keycert." data = self.data() - hmac_key = b'' + hmac_key = b"" encryption_headers = [] if extra: - encryption_type = 'aes256-cbc' + encryption_type = "aes256-cbc" hmac_key += extra else: - encryption_type = 'none' + encryption_type = "none" - if key_type == b'ssh-rsa': + if key_type == b"ssh-rsa": public_blob = ( - common.NS(key_type) + - common.MP(data['e']) + - common.MP(data['n']) - ) + common.NS(key_type) + common.MP(data["e"]) + common.MP(data["n"]) + ) private_blob = ( - common.MP(data['d']) + - common.MP(data['p']) + - common.MP(data['q']) + - common.MP(data['u']) - ) - elif key_type == b'ssh-dss': + common.MP(data["d"]) + + common.MP(data["p"]) + + common.MP(data["q"]) + + common.MP(data["u"]) + ) + elif key_type == b"ssh-dss": public_blob = ( - common.NS(key_type) + - common.MP(data['p']) + - common.MP(data['q']) + - common.MP(data['g']) + - common.MP(data['y']) - ) - private_blob = common.MP(data['x']) + common.NS(key_type) + + common.MP(data["p"]) + + common.MP(data["q"]) + + common.MP(data["g"]) + + common.MP(data["y"]) + ) + private_blob = common.MP(data["x"]) - elif key_type == b'ssh-ed25519': - public_blob = ( - common.NS(key_type) + - common.NS(data['a']) - ) - private_blob = common.NS(data['k']) + elif key_type == b"ssh-ed25519": + public_blob = common.NS(key_type) + common.NS(data["a"]) + private_blob = common.NS(data["k"]) elif key_type in _curveTable: @@ -2782,16 +2844,14 @@ def _toString_PUTTY_V3_private(self, extra): encode_point = self._keyObject.public_key().public_bytes( serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint, - ) + ) public_blob = ( - common.NS(key_type) + - common.NS(curve_name) + - common.NS(encode_point) - ) - private_blob = common.MP(data['privateValue']) + common.NS(key_type) + common.NS(curve_name) + common.NS(encode_point) + ) + private_blob = common.MP(data["privateValue"]) else: # pragma: no cover - raise BadKeyError('Unsupported key type.') + raise BadKeyError("Unsupported key type.") private_blob_plain = private_blob private_blob_encrypted = private_blob @@ -2799,52 +2859,53 @@ def _toString_PUTTY_V3_private(self, extra): if extra: # Encryption is requested. # Padding is required for encryption. - padding_size = -1 * ( - (len(private_blob) % aes_block_size) - aes_block_size) - private_blob_plain += b'\x00' * padding_size - - encryption_headers.append('Key-Derivation: Argon2id') - encryption_headers.append('Argon2-Memory: 8192') - encryption_headers.append('Argon2-Passes: 34') - encryption_headers.append('Argon2-Parallelism: 1') - encryption_headers.append('Argon2-Salt: {}'.format( - self.secureRandom(16).hex())) + padding_size = -1 * ((len(private_blob) % aes_block_size) - aes_block_size) + private_blob_plain += b"\x00" * padding_size + + encryption_headers.append("Key-Derivation: Argon2id") + encryption_headers.append("Argon2-Memory: 8192") + encryption_headers.append("Argon2-Passes: 34") + encryption_headers.append("Argon2-Parallelism: 1") + encryption_headers.append( + "Argon2-Salt: {}".format(self.secureRandom(16).hex()) + ) encryption_key, iv, hmac_key = self._getPuttyAES256EncryptionKey_v3( - encryption_headers, extra) + encryption_headers, extra + ) encryptor = Cipher( algorithms.AES256(encryption_key), modes.CBC(iv), - backend=default_backend() + backend=default_backend(), ).encryptor() private_blob_encrypted = ( - encryptor.update(private_blob_plain) + encryptor.finalize()) + encryptor.update(private_blob_plain) + encryptor.finalize() + ) - public_lines = textwrap.wrap( - base64.b64encode(public_blob).decode('ascii'), 64) + public_lines = textwrap.wrap(base64.b64encode(public_blob).decode("ascii"), 64) private_lines = textwrap.wrap( - base64.b64encode(private_blob_encrypted).decode('ascii'), 64) + base64.b64encode(private_blob_encrypted).decode("ascii"), 64 + ) hmac_data = ( - common.NS(key_type) + - common.NS(encryption_type) + - common.NS(comment) + - common.NS(public_blob) + - common.NS(private_blob_plain) - ) + common.NS(key_type) + + common.NS(encryption_type) + + common.NS(comment) + + common.NS(public_blob) + + common.NS(private_blob_plain) + ) hmac_key = sha256(hmac_key).digest() private_mac = hmac.new(hmac_key, hmac_data, sha256).hexdigest() - lines.append('PuTTY-User-Key-File-3: %s' % key_type.decode('ascii')) - lines.append('Encryption: %s' % encryption_type) + lines.append("PuTTY-User-Key-File-3: %s" % key_type.decode("ascii")) + lines.append("Encryption: %s" % encryption_type) lines.extend(encryption_headers) - lines.append('Comment: %s' % comment) - lines.append('Public-Lines: %s' % len(public_lines)) + lines.append("Comment: %s" % comment) + lines.append("Public-Lines: %s" % len(public_lines)) lines.extend(public_lines) - lines.append('Private-Lines: %s' % len(private_lines)) + lines.append("Private-Lines: %s" % len(private_lines)) lines.extend(private_lines) - lines.append('Private-MAC: %s' % private_mac) - return '\r\n'.join(lines).encode('utf-8') - + lines.append("Private-MAC: %s" % private_mac) + return "\r\n".join(lines).encode("utf-8") @classmethod def _fromString_PUBLIC_X509_CERTIFICATE(cls, data): @@ -2855,9 +2916,10 @@ def _fromString_PUBLIC_X509_CERTIFICATE(cls, data): cert = crypto.load_certificate(crypto.FILETYPE_PEM, data) except crypto.Error as error: raise BadKeyError( - 'Failed to load certificate. "%s"' % (force_unicode(error),)) + 'Failed to load certificate. "%s"' % (force_unicode(error),) + ) - return cls._fromOpenSSLPublic(cert.get_pubkey(), 'certificate') + return cls._fromOpenSSLPublic(cert.get_pubkey(), "certificate") @classmethod def _fromString_PUBLIC_X509(cls, data): @@ -2868,10 +2930,10 @@ def _fromString_PUBLIC_X509(cls, data): pkey = crypto.load_publickey(crypto.FILETYPE_PEM, data) except crypto.Error as error: raise BadKeyError( - 'Failed to load PKCS#1 public key. "%s"' % ( - force_unicode(error),)) + 'Failed to load PKCS#1 public key. "%s"' % (force_unicode(error),) + ) - return cls._fromOpenSSLPublic(pkey, 'X509 public PEM file') + return cls._fromOpenSSLPublic(pkey, "X509 public PEM file") @classmethod def _fromOpenSSLPublic(cls, pkey, source_type): @@ -2885,7 +2947,7 @@ def _fromString_PRIVATE_PKCS8(cls, data, passphrase=None): """ Read the private key from PKCS8 PEM format. """ - return cls._load_PRIVATE_PKCS8(data, passphrase=b'') + return cls._load_PRIVATE_PKCS8(data, passphrase=b"") @classmethod def _fromString_PRIVATE_ENCRYPTED_PKCS8(cls, data, passphrase=None): @@ -2893,8 +2955,7 @@ def _fromString_PRIVATE_ENCRYPTED_PKCS8(cls, data, passphrase=None): Read the encrypted private key from PKCS8 PEM format. """ if not passphrase: - raise EncryptedKeyError( - 'Passphrase must be provided for an encrypted key') + raise EncryptedKeyError("Passphrase must be provided for an encrypted key") return cls._load_PRIVATE_PKCS8(data, passphrase) @@ -2905,10 +2966,12 @@ def _load_PRIVATE_PKCS8(cls, data, passphrase): """ try: key = crypto.load_privatekey( - crypto.FILETYPE_PEM, data, passphrase=passphrase) + crypto.FILETYPE_PEM, data, passphrase=passphrase + ) except crypto.Error as error: raise BadKeyError( - 'Failed to load PKCS#8 PEM. "%s"' % (force_unicode(error),)) + 'Failed to load PKCS#8 PEM. "%s"' % (force_unicode(error),) + ) return cls(key.to_cryptography_key()) @@ -2926,61 +2989,65 @@ def _fromString_PUBLIC_PKCS1_RSA(cls, data): """ lines = data.strip().splitlines() - data = base64.decodestring(b''.join(lines[1:-1])) + data = base64.decodestring(b"".join(lines[1:-1])) decodedKey = berDecoder.decode(data)[0] if len(decodedKey) != 2: - raise BadKeyError('Invalid ASN.1 payload for PKCS1 PEM.') + raise BadKeyError("Invalid ASN.1 payload for PKCS1 PEM.") n = int(decodedKey[0]) e = int(decodedKey[1]) return cls._fromRSAComponents(n=n, e=e) -def generate_ssh_key_parser(subparsers, name, default_key_type='rsa'): +def generate_ssh_key_parser(subparsers, name, default_key_type="rsa"): """ Create an argparse sub-command with `name` attached to `subparsers`. """ generate_ssh_key = subparsers.add_parser( name, - help='Create a SSH public and private key pair.', - ) + help="Create a SSH public and private key pair.", + ) generate_ssh_key.add_argument( - '--key-file', - metavar='FILE', - help=( - 'Store the keys pair in FILE and FILE.pub. Default id_TYPE.'), - ) + "--key-file", + metavar="FILE", + help=("Store the keys pair in FILE and FILE.pub. Default id_TYPE."), + ) generate_ssh_key.add_argument( - '--key-size', - type=int, metavar="SIZE", default=None, - help='Generate a SSH key of size SIZE', - ) + "--key-size", + type=int, + metavar="SIZE", + default=None, + help="Generate a SSH key of size SIZE", + ) generate_ssh_key.add_argument( - '--key-type', - metavar="[rsa|dsa|ecdsa|ed25519]", default=default_key_type, - help='Generate a new SSH private and public key. Default %(default)s.', - ) + "--key-type", + metavar="[rsa|dsa|ecdsa|ed25519]", + default=default_key_type, + help="Generate a new SSH private and public key. Default %(default)s.", + ) generate_ssh_key.add_argument( - '--key-comment', + "--key-comment", metavar="COMMENT_TEXT", - help=( - 'Generate the public key using this comment. Default no comment.'), - ) + help=("Generate the public key using this comment. Default no comment."), + ) generate_ssh_key.add_argument( - '--key-format', - metavar="[openssh|openssh_v1|putty]", default='openssh_v1', - help='Generate a new SSH private and public key. Default %(default)s.', - ) + "--key-format", + metavar="[openssh|openssh_v1|putty]", + default="openssh_v1", + help="Generate a new SSH private and public key. Default %(default)s.", + ) generate_ssh_key.add_argument( - '--key-password', - metavar="PLAIN-PASS", default=None, - help='Password used to store the SSH private key.', - ) + "--key-password", + metavar="PLAIN-PASS", + default=None, + help="Password used to store the SSH private key.", + ) generate_ssh_key.add_argument( - '--key-skip', - action='store_true', default=False, - help='Do not create a new key if a key file already exists.', - ) + "--key-skip", + action="store_true", + default=False, + help="Do not create a new key if a key file already exists.", + ) return generate_ssh_key @@ -3002,62 +3069,63 @@ def generate_ssh_key(options, open_method=None): open_method = open exit_code = 0 - message = '' + message = "" try: key_size = options.key_size key_type = options.key_type.lower() key_format = options.key_format.lower() - if not hasattr(options, 'key_file') or options.key_file is None: - options.key_file = u'id_%s' % (key_type) + if not hasattr(options, "key_file") or options.key_file is None: + options.key_file = "id_%s" % (key_type) private_file = options.key_file - public_file = u'%s%s' % ( - options.key_file, DEFAULT_PUBLIC_KEY_EXTENSION) + public_file = "%s%s" % (options.key_file, DEFAULT_PUBLIC_KEY_EXTENSION) skip = _skip_key_generation(options, private_file, public_file) if skip: - return (0, u'Key already exists.', key) + return (0, "Key already exists.", key) key = Key.generate(key_type=key_type, key_size=key_size) - with open_method(private_file, 'wb') as file_handler: + with open_method(private_file, "wb") as file_handler: _store_SSHKey( key, private_file=file_handler, key_format=key_format, password=options.key_password, - ) + ) key_comment = None - if hasattr(options, 'key_comment') and options.key_comment: + if hasattr(options, "key_comment") and options.key_comment: key_comment = options.key_comment - message_comment = u'having comment "%s"' % key_comment - if key_format != 'openssh': + message_comment = 'having comment "%s"' % key_comment + if key_format != "openssh": key_comment = None message_comment = ( - 'without comment as not supported by the output format') + "without comment as not supported by the output format" + ) else: - message_comment = u'without a comment' + message_comment = "without a comment" - with open_method(public_file, 'wb') as file_handler: + with open_method(public_file, "wb") as file_handler: _store_SSHKey( key, public_file=file_handler, comment=key_comment, key_format=key_format, - ) + ) message = ( - u'SSH key of type "%s" and length "%d" generated as ' - u'public key file "%s" and private key file "%s" %s.') % ( - key.sshType().decode('ascii'), + 'SSH key of type "%s" and length "%d" generated as ' + 'public key file "%s" and private key file "%s" %s.' + ) % ( + key.sshType().decode("ascii"), key.size(), public_file, private_file, message_comment, - ) + ) exit_code = 0 @@ -3073,19 +3141,20 @@ def generate_ssh_key(options, open_method=None): def _store_SSHKey( key, - public_file=None, private_file=None, - comment=None, password=None, key_format='openssh_v1', - ): + public_file=None, + private_file=None, + comment=None, + password=None, + key_format="openssh_v1", +): """ Store the public and private key into a file like object using OpenSSH format. """ if public_file: - public_serialization = key.public().toString( - type=key_format) + public_serialization = key.public().toString(type=key_format) if comment: - public_content = '%s %s' % ( - public_serialization, comment.encode('utf-8')) + public_content = "%s %s" % (public_serialization, comment.encode("utf-8")) else: public_content = public_serialization public_file.write(public_content) @@ -3107,9 +3176,8 @@ def _skip_key_generation(options, private_file, public_file): if options.key_skip: return True else: - raise KeyCertException( - u'Private key already exists. %s' % private_file) + raise KeyCertException("Private key already exists. %s" % private_file) if os.path.exists(public_file): - raise KeyCertException(u'Public key already exists. %s' % public_file) + raise KeyCertException("Public key already exists. %s" % public_file) return False diff --git a/src/chevah_keycert/ssl.py b/src/chevah_keycert/ssl.py index 58f2944..7bf6ab4 100644 --- a/src/chevah_keycert/ssl.py +++ b/src/chevah_keycert/ssl.py @@ -15,27 +15,27 @@ from chevah_keycert import native_string from chevah_keycert.exceptions import KeyCertException -_DEFAULT_SSL_KEY_CYPHER = 'aes-256-cbc' -_SUPPORTED_SIGN_ALGORITHMS = ['md5', 'sha1', 'sha256', 'sha512'] +_DEFAULT_SSL_KEY_CYPHER = "aes-256-cbc" +_SUPPORTED_SIGN_ALGORITHMS = ["md5", "sha1", "sha256", "sha512"] # See https://www.openssl.org/docs/manmaster/man5/x509v3_config.html _KEY_USAGE_STANDARD = { - 'digital-signature': b'digitalSignature', - 'non-repudiation': b'nonRepudiation', - 'key-encipherment': b'keyEncipherment', - 'data-encipherment': b'dataEncipherment', - 'key-agreement': b'keyAgreement', - 'key-cert-sign': b'keyCertSign', - 'crl-sign': b'cRLSign', - 'encipher-only': b'encipherOnly', - 'decipher-only': b'decipherOnly', - } + "digital-signature": b"digitalSignature", + "non-repudiation": b"nonRepudiation", + "key-encipherment": b"keyEncipherment", + "data-encipherment": b"dataEncipherment", + "key-agreement": b"keyAgreement", + "key-cert-sign": b"keyCertSign", + "crl-sign": b"cRLSign", + "encipher-only": b"encipherOnly", + "decipher-only": b"decipherOnly", +} _KEY_USAGE_EXTENDED = { - 'server-authentication': b'serverAuth', - 'client-authentication': b'clientAuth', - 'code-signing': b'codeSigning', - 'email-protection': b'emailProtection', - } + "server-authentication": b"serverAuth", + "client-authentication": b"clientAuth", + "code-signing": b"codeSigning", + "email-protection": b"emailProtection", +} def _generate_self_csr_parser(sub_command, default_key_size): @@ -43,83 +43,86 @@ def _generate_self_csr_parser(sub_command, default_key_size): Add share configuration options for CSR and self-signed generation. """ sub_command.add_argument( - '--common-name', - help='Common name associated with the certificate.', + "--common-name", + help="Common name associated with the certificate.", required=True, - ) + ) sub_command.add_argument( - '--key-size', - type=int, metavar="SIZE", default=default_key_size, - help='Size of the generate RSA private key. Default %(default)s', - ) + "--key-size", + type=int, + metavar="SIZE", + default=default_key_size, + help="Size of the generate RSA private key. Default %(default)s", + ) sub_command.add_argument( - '--sign-algorithm', - default='sha256', - metavar='STRING', - help='Signature algorithm: sha1, sha256, sha512. Default: sha256.' - ) + "--sign-algorithm", + default="sha256", + metavar="STRING", + help="Signature algorithm: sha1, sha256, sha512. Default: sha256.", + ) sub_command.add_argument( - '--key-usage', - default='', + "--key-usage", + default="", help=( - 'Comma-separated key usage. ' - 'The following key usage extensions are supported: %s. ' - 'To mark usage as critical, prefix the values with `critical,`. ' + "Comma-separated key usage. " + "The following key usage extensions are supported: %s. " + "To mark usage as critical, prefix the values with `critical,`. " 'For example: "critical,key-agreement,digital-signature".' - ) % (', '.join( - list(_KEY_USAGE_STANDARD.keys()) + - list(_KEY_USAGE_EXTENDED.keys()))), ) + % ( + ", ".join( + list(_KEY_USAGE_STANDARD.keys()) + list(_KEY_USAGE_EXTENDED.keys()) + ) + ), + ) sub_command.add_argument( - '--constraints', - default='', + "--constraints", + default="", help=( - 'Comma-separated basic constraints. ' - 'To mark constraints as critical, prefix the values with ' - '`critical,`. ' + "Comma-separated basic constraints. " + "To mark constraints as critical, prefix the values with " + "`critical,`. " 'For example: "critical,CA:TRUE,pathlen:0".' - ), - ) + ), + ) sub_command.add_argument( - '--email', - help='Email address.', - ) + "--email", + help="Email address.", + ) sub_command.add_argument( - '--alternative-name', + "--alternative-name", help=( - 'Optional list of alternative names. ' + "Optional list of alternative names. " 'Use "DNS:your.domain.tld" for domain names. ' 'Use "IP:1.2.3.4" for IP addresses. ' 'Example: "DNS:top.com,DNS:www.top.com,IP:11.0.21.12".' - ) - ) + ), + ) sub_command.add_argument( - '--organization', - help='Organization.', - ) + "--organization", + help="Organization.", + ) sub_command.add_argument( - '--organization-unit', - help='Organization unit.', - ) + "--organization-unit", + help="Organization unit.", + ) sub_command.add_argument( - '--locality', - help='Full name of the locality.', - ) + "--locality", + help="Full name of the locality.", + ) sub_command.add_argument( - '--state', - help=( - 'Full name of the state/county/region/province.'), - ) + "--state", + help=("Full name of the state/county/region/province."), + ) sub_command.add_argument( - '--country', - help=( - 'Two-letter country code.'), - ) + "--country", + help=("Two-letter country code."), + ) def generate_csr_parser(subparsers, name, default_key_size=2048): @@ -130,38 +133,40 @@ def generate_csr_parser(subparsers, name, default_key_size=2048): sub_command = subparsers.add_parser( name, help=( - 'Create an SSL private key and an associated certificate ' - 'signing request.'), - ) + "Create an SSL private key and an associated certificate " + "signing request." + ), + ) sub_command.add_argument( - '--key', + "--key", metavar="FILE", default=None, help=( - 'Sign the CSR using this private key. ' - 'Private key loaded as PEM PKCS#8 format. ' - ), - ) + "Sign the CSR using this private key. " + "Private key loaded as PEM PKCS#8 format. " + ), + ) sub_command.add_argument( - '--key-file', + "--key-file", metavar="FILE", - default='server.key', + default="server.key", help=( - 'Store the keys/CSR pair in FILE and FILE.csr. ' - 'Private key stored using PEM PKCS#8 format. ' - 'CSR file stored in PEM x509 format. ' - 'Default names: server.key and server.csr.'), - ) + "Store the keys/CSR pair in FILE and FILE.csr. " + "Private key stored using PEM PKCS#8 format. " + "CSR file stored in PEM x509 format. " + "Default names: server.key and server.csr." + ), + ) sub_command.add_argument( - '--key-password', + "--key-password", metavar="PASSPHRASE", help=( - 'Password used to encrypt the generated key. ' - 'Default no encryption. Encrypted with %s.' % ( - _DEFAULT_SSL_KEY_CYPHER,)), - ) + "Password used to encrypt the generated key. " + "Default no encryption. Encrypted with %s." % (_DEFAULT_SSL_KEY_CYPHER,) + ), + ) _generate_self_csr_parser(sub_command, default_key_size) return sub_command @@ -175,9 +180,9 @@ def generate_self_signed_parser(subparsers, name, default_key_size=2048): sub_command = subparsers.add_parser( name, help=( - 'Create an SSL private key ' - 'and an associated self-signed certificate.'), - ) + "Create an SSL private key " "and an associated self-signed certificate." + ), + ) _generate_self_csr_parser(sub_command, default_key_size) return sub_command @@ -193,9 +198,9 @@ def generate_csr(options): return _generate_csr(options) except crypto.Error as error: try: - message = error[0][0][2].decode('utf-8', errors='replace') + message = error[0][0][2].decode("utf-8", errors="replace") except IndexError: # pragma: no cover - message = 'no error details.' + message = "no error details." raise KeyCertException(message) @@ -204,15 +209,15 @@ def _set_subject_and_extensions(target, options): Set the subject and option for `target` CRS or certificate. """ common_name = options.common_name - constraints = getattr(options, 'constraints', '') - key_usage = getattr(options, 'key_usage', '').lower() - email = getattr(options, 'email', '') - alternative_name = getattr(options, 'alternative_name', '') - country = getattr(options, 'country', '') - state = getattr(options, 'state', '') - locality = getattr(options, 'locality', '') - organization = getattr(options, 'organization', '') - organization_unit = getattr(options, 'organization_unit', '') + constraints = getattr(options, "constraints", "") + key_usage = getattr(options, "key_usage", "").lower() + email = getattr(options, "email", "") + alternative_name = getattr(options, "alternative_name", "") + country = getattr(options, "country", "") + state = getattr(options, "state", "") + locality = getattr(options, "locality", "") + organization = getattr(options, "organization", "") + organization_unit = getattr(options, "organization_unit", "") # RFC 2459 defines it as optional, and pyopenssl set it to `0` anyway. # But we got reports that Windows 2003 and Windows 2008 Servers @@ -222,11 +227,11 @@ def _set_subject_and_extensions(target, options): subject = target.get_subject() - subject.CN = common_name.encode('idna') + subject.CN = common_name.encode("idna") if country: if len(country) != 2: - raise KeyCertException('Invalid country code.') + raise KeyCertException("Invalid country code.") subject.C = country @@ -244,12 +249,14 @@ def _set_subject_and_extensions(target, options): if email: try: - address, domain = options.email.split('@', 1) + address, domain = options.email.split("@", 1) except ValueError: - raise KeyCertException('Invalid email address.') + raise KeyCertException("Invalid email address.") - subject.emailAddress = '%s@%s' % ( - address, domain.encode('idna').decode('ascii')) + subject.emailAddress = "%s@%s" % ( + address, + domain.encode("idna").decode("ascii"), + ) critical_constraints = False critical_usage = False @@ -257,15 +264,15 @@ def _set_subject_and_extensions(target, options): extended_usage = [] extensions = [] - if constraints.lower().startswith('critical'): + if constraints.lower().startswith("critical"): critical_constraints = True - constraints = constraints[8:].strip(',').strip() + constraints = constraints[8:].strip(",").strip() - if key_usage.startswith('critical'): + if key_usage.startswith("critical"): critical_usage = True key_usage = key_usage[8:] - for usage in key_usage.split(','): + for usage in key_usage.split(","): usage = usage.strip() if not usage: continue @@ -275,32 +282,39 @@ def _set_subject_and_extensions(target, options): extended_usage.append(_KEY_USAGE_EXTENDED[usage]) if constraints: - extensions.append(crypto.X509Extension( - b'basicConstraints', - critical_constraints, - constraints.encode('ascii'), - )) + extensions.append( + crypto.X509Extension( + b"basicConstraints", + critical_constraints, + constraints.encode("ascii"), + ) + ) if standard_usage: - extensions.append(crypto.X509Extension( - b'keyUsage', - critical_usage, - b','.join(standard_usage), - )) + extensions.append( + crypto.X509Extension( + b"keyUsage", + critical_usage, + b",".join(standard_usage), + ) + ) if extended_usage: - extensions.append(crypto.X509Extension( - b'extendedKeyUsage', - critical_usage, - b','.join(extended_usage), - )) + extensions.append( + crypto.X509Extension( + b"extendedKeyUsage", + critical_usage, + b",".join(extended_usage), + ) + ) # Alternate name is optional. if alternative_name: - extensions.append(crypto.X509Extension( - b'subjectAltName', - False, - alternative_name.encode('idna'))) + extensions.append( + crypto.X509Extension( + b"subjectAltName", False, alternative_name.encode("idna") + ) + ) target.add_extensions(extensions) @@ -308,12 +322,13 @@ def _sign_cert_or_csr(target, key, options): """ Sign the certificate or CSR. """ - sign_algorithm = getattr(options, 'sign_algorithm', 'sha256') + sign_algorithm = getattr(options, "sign_algorithm", "sha256") if sign_algorithm not in _SUPPORTED_SIGN_ALGORITHMS: raise KeyCertException( - 'Invalid signing algorithm. Supported values: %s.' % ( - ', '.join(_SUPPORTED_SIGN_ALGORITHMS))) + "Invalid signing algorithm. Supported values: %s." + % (", ".join(_SUPPORTED_SIGN_ALGORITHMS)) + ) target.set_pubkey(key) target.sign(key, native_string(sign_algorithm)) @@ -323,10 +338,10 @@ def _generate_csr(options): """ Helper to catch all crypto errors and reduce indentation. """ - key_size = getattr(options, 'key_size', 2048) + key_size = getattr(options, "key_size", 2048) if key_size < 512: - raise KeyCertException('Key size must be greater or equal to 512.') + raise KeyCertException("Key size must be greater or equal to 512.") key_type = crypto.TYPE_RSA @@ -338,7 +353,7 @@ def _generate_csr(options): private_key = options.key if private_key: if os.path.exists(private_key): - with open(private_key, 'rb') as stream: + with open(private_key, "rb") as stream: private_key = stream.read() key_pem = private_key @@ -356,22 +371,22 @@ def _generate_csr(options): if options.key_password: cipher = _DEFAULT_SSL_KEY_CYPHER if six.PY2: - cipher = cipher.encode('ascii') + cipher = cipher.encode("ascii") key_pem = crypto.dump_privatekey( crypto.FILETYPE_PEM, key, cipher, - options.key_password.encode('utf-8'), - ) + options.key_password.encode("utf-8"), + ) else: key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) return { - 'csr_pem': csr_pem, - 'key_pem': key_pem, - 'csr': csr, - 'key': key, - } + "csr_pem": csr_pem, + "key_pem": key_pem, + "csr": csr, + "key": key, + } def generate_ssl_self_signed_certificate(options): @@ -380,7 +395,7 @@ def generate_ssl_self_signed_certificate(options): Returns a tuple of (certificate_pem, key_pem) """ - key_size = getattr(options, 'key_size', 2048) + key_size = getattr(options, "key_size", 2048) serial = randint(0, 1000000000000) @@ -401,7 +416,7 @@ def generate_ssl_self_signed_certificate(options): certificate_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) - return (certificate_pem.decode('utf-8'), key_pem.decode('utf-8')) + return (certificate_pem.decode("utf-8"), key_pem.decode("utf-8")) def generate_and_store_csr(options): @@ -411,18 +426,18 @@ def generate_and_store_csr(options): Raise KeyCertException when failing to create the key or csr. """ name, _ = os.path.splitext(options.key_file) - csr_name = u'%s.csr' % name + csr_name = "%s.csr" % name if os.path.exists(options.key_file): - raise KeyCertException('Key file already exists.') + raise KeyCertException("Key file already exists.") result = generate_csr(options) try: - with open(options.key_file, 'wb') as store_file: - store_file.write(result['key_pem']) + with open(options.key_file, "wb") as store_file: + store_file.write(result["key_pem"]) - with open(csr_name, 'wb') as store_file: - store_file.write(result['csr_pem']) + with open(csr_name, "wb") as store_file: + store_file.write(result["csr_pem"]) except Exception as error: raise KeyCertException(str(error)) From cc4bbc2bb9446f5f5ab51d45763894213baa4f0b Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 01:37:45 +0000 Subject: [PATCH 25/41] Fail on deps errors. --- pavement.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pavement.py b/pavement.py index 860c10f..a473d08 100644 --- a/pavement.py +++ b/pavement.py @@ -26,7 +26,7 @@ def deps(): Install all dependencies. """ pip = load_entry_point("pip", "console_scripts", "pip") - pip( + exit_code = pip( args=[ "install", "-U", @@ -36,6 +36,8 @@ def deps(): ".[dev]", ] ) + if exit_code: + raise Exception('Failed to install the deps.') @task From c3b3ddfc72ff0f78e7c9646c087db17a724fa579 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 02:09:08 +0000 Subject: [PATCH 26/41] Fix interop test. --- src/chevah_keycert/tests/ssh_load_keys_tests.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chevah_keycert/tests/ssh_load_keys_tests.sh b/src/chevah_keycert/tests/ssh_load_keys_tests.sh index a48d91b..5e6834d 100755 --- a/src/chevah_keycert/tests/ssh_load_keys_tests.sh +++ b/src/chevah_keycert/tests/ssh_load_keys_tests.sh @@ -13,7 +13,7 @@ if [ -z "$KEY_TYPES" ]; then KEY_TYPES="ed25519 ecdsa dsa rsa" fi -KEYCERT_CMD="../build-keycert/bin/python ../keycert-demo.py" +KEYCERT_CMD="../build-py3/bin/python ../keycert-demo.py" KEYCERT_NO_ERRORS_FILE="load_keys_tests_errors_none" KEYCERT_EXPECTED_ERRORS_FILE="load_keys_tests_errors_expected" KEYCERT_UNEXPECTED_ERRORS_FILE="load_keys_tests_errors_unexpected" @@ -37,7 +37,7 @@ TECTIA_HASHES="sha1 sha224 sha256 sha384 sha512" > $KEYCERT_DEMOSCRIPT_ERRORS_FILE # Common routines like setting password files. -source ../chevah/keycert/tests/ssh_common_test_inc.sh +source ../src/chevah_keycert/tests/ssh_common_test_inc.sh # FIXME:50 # Unicode comments are not supported. COMM_TYPES="empty simple complex" From 8fcd5d0ac6a58db1355e318e9f5e787617a83402 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 02:18:40 +0000 Subject: [PATCH 27/41] Install non dev on CI. --- pavement.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/pavement.py b/pavement.py index a473d08..480c439 100644 --- a/pavement.py +++ b/pavement.py @@ -13,6 +13,7 @@ EXTRA_PYPI_INDEX = os.environ["PIP_INDEX_URL"] BUILD_DIR = os.environ.get("CHEVAH_BUILD", "build-py3") +HAVE_CI = os.environ.get('CI', 'false') == 'true' @task @@ -26,16 +27,18 @@ def deps(): Install all dependencies. """ pip = load_entry_point("pip", "console_scripts", "pip") - exit_code = pip( - args=[ - "install", - "-U", - "--extra-index-url", - EXTRA_PYPI_INDEX, - "-e", - ".[dev]", - ] - ) + pip_args = [ + "install", + "-U", + "--extra-index-url", + EXTRA_PYPI_INDEX, + ] + + if not HAVE_CI: + pip_args.append('-e') + + pip_args.append('.dev') + exit_code = pip(args=pip_args) if exit_code: raise Exception('Failed to install the deps.') @@ -186,7 +189,7 @@ def test_interop_generate(args): exit_code = 1 with pushd("build"): exit_code = call( - "../stc/chevah_keycert/tests/ssh_gen_keys_tests.sh", shell=True + "../src/chevah_keycert/tests/ssh_gen_keys_tests.sh", shell=True ) sys.exit(exit_code) From f29f7673b4f4cd8c04def8a61f28b03e251a8b32 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 02:19:21 +0000 Subject: [PATCH 28/41] Add gha concurency. --- .github/workflows/main.yml | 8 ++++++++ pavement.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1630ee9..3f159a7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,14 @@ on: paths: - '**.py' +permissions: + contents: read + + +concurrency: + group: chevah-keycert-${{ github.ref }} + cancel-in-progress: true + jobs: ubuntu: diff --git a/pavement.py b/pavement.py index 480c439..7478090 100644 --- a/pavement.py +++ b/pavement.py @@ -37,7 +37,7 @@ def deps(): if not HAVE_CI: pip_args.append('-e') - pip_args.append('.dev') + pip_args.append('.[dev]') exit_code = pip(args=pip_args) if exit_code: raise Exception('Failed to install the deps.') From c9aca2fcf9d1f70bf234943c013aa01f7383aff1 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 02:25:08 +0000 Subject: [PATCH 29/41] Add wheel to deps. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 7b4a52b..405cd94 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ where = src dev = zope.interface future + wheel pocketlint ==1.4.4.c10 pyflakes >= 3.2.0 From 2585b74ebf9d400ecc1d8fe6b0285ed06e958016 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 02:31:17 +0000 Subject: [PATCH 30/41] Install wheel. --- .github/workflows/main.yml | 3 ++- pavement.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3f159a7..92d9cd4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,7 +37,8 @@ jobs: key: ${{ runner.os }}-${{ hashFiles('setup.py') }} - name: Deps - run: ./pythia.sh deps + run: | + ./pythia.sh deps - name: Rename build to unicode run: mv build-py3 build-py3-ț diff --git a/pavement.py b/pavement.py index 7478090..d4b7f18 100644 --- a/pavement.py +++ b/pavement.py @@ -34,6 +34,9 @@ def deps(): EXTRA_PYPI_INDEX, ] + # Install wheel. + pip(args=pip_args + ['wheel']) + if not HAVE_CI: pip_args.append('-e') From a5776f7e973eceb0534190f5ce04f1b6c259d313 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 02:32:17 +0000 Subject: [PATCH 31/41] Fix interop command. --- src/chevah_keycert/tests/ssh_gen_keys_tests.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chevah_keycert/tests/ssh_gen_keys_tests.sh b/src/chevah_keycert/tests/ssh_gen_keys_tests.sh index e304cb2..fa0df52 100755 --- a/src/chevah_keycert/tests/ssh_gen_keys_tests.sh +++ b/src/chevah_keycert/tests/ssh_gen_keys_tests.sh @@ -13,7 +13,7 @@ if [ -z "$KEY_TYPES" ]; then KEY_TYPES="ed25519 ecdsa rsa dsa" fi -KEYCERT_CMD="../build-keycert/bin/python ../keycert-demo.py" +KEYCERT_CMD="../build-py3/bin/python ../keycert-demo.py" KEYCERT_FORMATS="openssh openssh_v1 putty" SUCCESS_FILE="gen_keys_tests_success" @@ -24,7 +24,7 @@ ERROR_FILE="gen_keys_tests_error" > $ERROR_FILE # Common routines like setting password files. -source ../chevah/keycert/tests/ssh_common_test_inc.sh +source ../src/chevah_keycert/tests/ssh_common_test_inc.sh sort_tests_per_error(){ local cmd_to_test=$* From 434ded7f9f86ae2f282fbe5343c63fdfba9500fb Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 02:34:22 +0000 Subject: [PATCH 32/41] Remove future deps. --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 405cd94..4930cad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,6 @@ where = src ; Try to pin them as much as possible. dev = zope.interface - future wheel pocketlint ==1.4.4.c10 From 11e782529c0c83ee9cee61b675bd1e9eb667521f Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 07:58:46 +0000 Subject: [PATCH 33/41] add isort. --- .github/workflows/main.yml | 11 +- pavement.py | 133 +++++++----------- src/chevah_keycert/__init__.py | 6 +- src/chevah_keycert/common.py | 1 + src/chevah_keycert/ssh.py | 40 +++--- src/chevah_keycert/ssl.py | 5 +- src/chevah_keycert/tests/__init__.py | 1 + src/chevah_keycert/tests/helpers.py | 3 +- .../tests/ssh_gen_keys_tests.sh | 4 +- .../tests/ssh_load_keys_tests.sh | 2 +- src/chevah_keycert/tests/test_exceptions.py | 3 +- src/chevah_keycert/tests/test_ssh.py | 20 +-- src/chevah_keycert/tests/test_ssl.py | 6 +- 13 files changed, 98 insertions(+), 137 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 92d9cd4..4d440a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -101,10 +101,11 @@ jobs: fail-fast: false matrix: config: - - { test_type: load_dsa } - - { test_type: load_rsa } - - { test_type: load_eced } - - { test_type: generate } + - { test_type: "load dsa" } + - { test_type: "load rsa" } + - { test_type: "load ecdsa" } + - { test_type: "load ed25519" } + - { test_type: "generate" } steps: - uses: actions/checkout@v2 @@ -121,4 +122,4 @@ jobs: ./pythia.sh deps - name: Test - run: ./pythia.sh test_interop_${{ matrix.config.test_type }} + run: ./pythia.sh test_interop ${{ matrix.config.test_type }} diff --git a/pavement.py b/pavement.py index d4b7f18..484ffd3 100644 --- a/pavement.py +++ b/pavement.py @@ -3,17 +3,17 @@ """ import os -import re import sys import threading from subprocess import call -from pkg_resources import load_entry_point -from paver.easy import call_task, consume_args, task, pushd +from paver.easy import call_task, cmdopts, consume_args, pushd, task +from pkg_resources import load_entry_point EXTRA_PYPI_INDEX = os.environ["PIP_INDEX_URL"] BUILD_DIR = os.environ.get("CHEVAH_BUILD", "build-py3") -HAVE_CI = os.environ.get('CI', 'false') == 'true' +HAVE_CI = os.environ.get("CI", "false") == "true" +SOURCE_FILES = ["pavement.py", "src"] @task @@ -35,15 +35,15 @@ def deps(): ] # Install wheel. - pip(args=pip_args + ['wheel']) + pip(args=pip_args + ["wheel"]) if not HAVE_CI: - pip_args.append('-e') + pip_args.append("-e") - pip_args.append('.[dev]') + pip_args.append(".[dev]") exit_code = pip(args=pip_args) if exit_code: - raise Exception('Failed to install the deps.') + raise Exception("Failed to install the deps.") @task @@ -61,13 +61,12 @@ def _nose(args, cov, base="chevah_keycert.tests"): """ # Delay import after coverage is started. import psutil - from nose.core import main as nose_main - from nose.plugins.base import Plugin from chevah_compat.testing import ChevahTestCase - from chevah_compat.testing.nose_memory_usage import MemoryUsage - from chevah_compat.testing.nose_test_timer import TestTimer from chevah_compat.testing.nose_run_reporter import RunReporter + from chevah_compat.testing.nose_test_timer import TestTimer + from nose.core import main as nose_main + from nose.plugins.base import Plugin import chevah_keycert @@ -118,81 +117,43 @@ class LoopPlugin(Plugin): @task -@consume_args -def test_interop_load_dsa(args): - """ - Run the SSH key interoperability tests for loading external DSA keys. - """ - try: - os.mkdir("build") - except OSError: - """Already exists""" - - exit_code = 1 - with pushd("build"): - exit_code = call( - "../src/chevah_keycert/tests/ssh_load_keys_tests.sh dsa", shell=True - ) - - sys.exit(exit_code) - - -@task -@consume_args -def test_interop_load_rsa(args): +@cmdopts( + [ + ("load=", "l", "Run key loading tests."), + ("generate", "g", "Run key generation tests."), + ] +) +def test_interop(options): """ - Run the SSH key interoperability tests for loading external RSA keys. + Run the SSH key interoperability tests. """ - try: - os.mkdir("build") - except OSError: - """Already exists""" + import pdb + import sys - exit_code = 1 - with pushd("build"): - exit_code = call( - "../src/chevah_keycert/tests/ssh_load_keys_tests.sh rsa", shell=True - ) + sys.stdout = sys.__stdout__ + pdb.set_trace() + test_type = args[0] - sys.exit(exit_code) + environ = os.environ.copy() + environ["CHEVAH_BUILD"] = BUILD_DIR + if test_type == "load": + key_type = args[1] + test_command = "ssh_load_keys_tests.sh {}".format(key_type) + else: + test_command = "ssh_gen_keys_tests.sh" -@task -@consume_args -def test_interop_load_eced(args): - """ - Run the SSH key interoperability tests for loading external ECDSA and Ed25519 keys. - """ try: - os.mkdir("build") + os.mkdir(BUILD_DIR) except OSError: """Already exists""" exit_code = 1 - with pushd("build"): + with pushd(BUILD_DIR): exit_code = call( - "../src/chevah_keycert/tests/ssh_load_keys_tests.sh ecdsa ed25519", + "../src/chevah_keycert/tests/{}".format(test_command), shell=True, - ) - - sys.exit(exit_code) - - -@task -@consume_args -def test_interop_generate(args): - """ - Run the SSH key interoperability tests for internally-generated keys. - """ - try: - os.mkdir("build") - except OSError: - """Already exists""" - - exit_code = 1 - with pushd("build"): - exit_code = call( - "../src/chevah_keycert/tests/ssh_gen_keys_tests.sh", shell=True + env=environ, ) sys.exit(exit_code) @@ -203,18 +164,24 @@ def lint(): """ Run the static code analyzer. """ - from pyflakes.api import main as pyflakes_main - from pycodestyle import _main as pycodestyle_main from black import patched_main + from isort.main import main as isort_main + from pyflakes.api import main as pyflakes_main try: - pyflakes_main(args=['src/chevah_keycert']) + pyflakes_main(args=["src/chevah_keycert"]) except SystemExit as error: if error.code: raise - sys.argv = ['black', '--check', 'src'] - sys.exit(patched_main()) + exit_code = isort_main(argv=["--check"] + SOURCE_FILES) + if exit_code: + raise Exception("isort needs to update the code.") + + sys.argv = ["black", "--check"] + SOURCE_FILES + exit_code = patched_main() + if exit_code: + raise Exception("Black needs to update the code.") @task @@ -223,5 +190,9 @@ def black(): Run black on the whole source code. """ from black import patched_main - sys.argv = ['black', 'src'] - sys.exit(patched_main()) + from isort.main import main as isort_main + + isort_main(argv=SOURCE_FILES) + + sys.argv = ["black"] + SOURCE_FILES + patched_main() diff --git a/src/chevah_keycert/__init__.py b/src/chevah_keycert/__init__.py index c61e5c0..3cdeaba 100644 --- a/src/chevah_keycert/__init__.py +++ b/src/chevah_keycert/__init__.py @@ -2,13 +2,13 @@ SSL and SSH key management. """ -import collections -import sys -import six import base64 +import collections import inspect +import sys import cryptography.utils +import six def _path(path, encoding="utf-8"): diff --git a/src/chevah_keycert/common.py b/src/chevah_keycert/common.py index dc98cd9..1f5c862 100644 --- a/src/chevah_keycert/common.py +++ b/src/chevah_keycert/common.py @@ -8,6 +8,7 @@ """ from __future__ import absolute_import, division + import struct from cryptography.utils import int_from_bytes, int_to_bytes diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index a79866b..aba9a21 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -8,19 +8,19 @@ from __future__ import absolute_import, division, unicode_literals -import binascii -import itertools - -from hashlib import md5, sha1, sha256 import base64 +import binascii import hmac -import unicodedata +import itertools import struct import textwrap -import six +import unicodedata +from hashlib import md5, sha1, sha256 import bcrypt +import six from argon2 import low_level +from cryptography import utils from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization @@ -29,55 +29,49 @@ load_pem_private_key, load_ssh_public_key, ) -from cryptography import utils -from six.moves import map -from six.moves import range +from six.moves import map, range try: from cryptography.hazmat.primitives.asymmetric.utils import ( - encode_dss_signature, decode_dss_signature, + encode_dss_signature, ) except ImportError: from cryptography.hazmat.primitives.asymmetric.utils import ( encode_rfc6979_signature as encode_dss_signature, decode_rfc6979_signature as decode_dss_signature, ) -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - -from pyasn1.error import PyAsn1Error -from pyasn1.type import univ -from pyasn1.codec.ber import decoder as berDecoder -from pyasn1.codec.ber import encoder as berEncoder import os import os.path from os import urandom +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from pyasn1.codec.ber import decoder as berDecoder +from pyasn1.codec.ber import encoder as berEncoder +from pyasn1.error import PyAsn1Error +from pyasn1.type import univ + try: - from base64 import encodebytes - from base64 import decodebytes + from base64 import decodebytes, encodebytes except ImportError: # On py2 we don't have encodebytes. from base64 import encodestring as encodebytes from base64 import decodestring as decodebytes +from constantly import NamedConstant, Names from cryptography.utils import int_from_bytes, int_to_bytes from OpenSSL import crypto from chevah_keycert import common -from chevah_keycert.common import ( - force_unicode, - iterbytes, -) +from chevah_keycert.common import force_unicode, iterbytes from chevah_keycert.exceptions import ( BadKeyError, BadSignatureAlgorithmError, EncryptedKeyError, KeyCertException, ) -from constantly import NamedConstant, Names DEFAULT_PUBLIC_KEY_EXTENSION = ".pub" DEFAULT_KEY_SIZE = 2048 diff --git a/src/chevah_keycert/ssl.py b/src/chevah_keycert/ssl.py index 7bf6ab4..9412c88 100644 --- a/src/chevah_keycert/ssl.py +++ b/src/chevah_keycert/ssl.py @@ -3,13 +3,12 @@ """ SSL keys and certificates. """ -from __future__ import unicode_literals -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import os -import six from random import randint +import six from OpenSSL import crypto from chevah_keycert import native_string diff --git a/src/chevah_keycert/tests/__init__.py b/src/chevah_keycert/tests/__init__.py index 7f87b8f..0ad93c4 100644 --- a/src/chevah_keycert/tests/__init__.py +++ b/src/chevah_keycert/tests/__init__.py @@ -1,4 +1,5 @@ from __future__ import absolute_import + from chevah_compat.testing import mk diff --git a/src/chevah_keycert/tests/helpers.py b/src/chevah_keycert/tests/helpers.py index 69d20cc..fb53482 100644 --- a/src/chevah_keycert/tests/helpers.py +++ b/src/chevah_keycert/tests/helpers.py @@ -4,9 +4,10 @@ Helpers for testing the project. """ from __future__ import absolute_import + +import sys from argparse import Namespace from io import StringIO -import sys class CommandLineMixin(object): diff --git a/src/chevah_keycert/tests/ssh_gen_keys_tests.sh b/src/chevah_keycert/tests/ssh_gen_keys_tests.sh index fa0df52..5869410 100755 --- a/src/chevah_keycert/tests/ssh_gen_keys_tests.sh +++ b/src/chevah_keycert/tests/ssh_gen_keys_tests.sh @@ -46,7 +46,7 @@ sort_tests_per_error(){ puttygen_tests(){ local priv_key=$1 local pub_key=${1}.pub - + sort_tests_per_error puttygen -O fingerprint $pub_key sort_tests_per_error puttygen -o /dev/null --old-passphrase pass_file_${2} -L $priv_key } @@ -54,7 +54,7 @@ puttygen_tests(){ sshkeygen_tests(){ local priv_key=$1 local pub_key=${1}.pub - + sort_tests_per_error ssh-keygen -l -f $pub_key if [ $2 = "empty" ]; then sort_tests_per_error ssh-keygen -y -f $priv_key diff --git a/src/chevah_keycert/tests/ssh_load_keys_tests.sh b/src/chevah_keycert/tests/ssh_load_keys_tests.sh index 5e6834d..a63fd03 100755 --- a/src/chevah_keycert/tests/ssh_load_keys_tests.sh +++ b/src/chevah_keycert/tests/ssh_load_keys_tests.sh @@ -13,7 +13,7 @@ if [ -z "$KEY_TYPES" ]; then KEY_TYPES="ed25519 ecdsa dsa rsa" fi -KEYCERT_CMD="../build-py3/bin/python ../keycert-demo.py" +KEYCERT_CMD="../${CHEVAH_BUILD}/bin/python ../keycert-demo.py" KEYCERT_NO_ERRORS_FILE="load_keys_tests_errors_none" KEYCERT_EXPECTED_ERRORS_FILE="load_keys_tests_errors_expected" KEYCERT_UNEXPECTED_ERRORS_FILE="load_keys_tests_errors_unexpected" diff --git a/src/chevah_keycert/tests/test_exceptions.py b/src/chevah_keycert/tests/test_exceptions.py index 9578e4b..66f040c 100644 --- a/src/chevah_keycert/tests/test_exceptions.py +++ b/src/chevah_keycert/tests/test_exceptions.py @@ -4,7 +4,8 @@ Test for exceptions raise by this package. """ from __future__ import absolute_import -from chevah_compat.testing import mk, ChevahTestCase + +from chevah_compat.testing import ChevahTestCase, mk from chevah_keycert.exceptions import KeyCertException diff --git a/src/chevah_keycert/tests/test_ssh.py b/src/chevah_keycert/tests/test_ssh.py index afb529e..4505ef3 100644 --- a/src/chevah_keycert/tests/test_ssh.py +++ b/src/chevah_keycert/tests/test_ssh.py @@ -4,29 +4,21 @@ """ Test for SSH keys management. """ +import textwrap from argparse import ArgumentParser from io import BytesIO -import textwrap -from chevah_compat.testing import mk, ChevahTestCase +from chevah_compat.testing import ChevahTestCase, mk from nose.plugins.attrib import attr # Twisted test compatibility. -from chevah_keycert import ssh as keys, common, _path -from chevah_keycert.exceptions import ( - BadKeyError, - KeyCertException, - EncryptedKeyError, -) -from chevah_keycert.ssh import ( - Key, - generate_ssh_key, - generate_ssh_key_parser, -) +from chevah_keycert import _path, common +from chevah_keycert import ssh as keys +from chevah_keycert.exceptions import BadKeyError, EncryptedKeyError, KeyCertException +from chevah_keycert.ssh import Key, generate_ssh_key, generate_ssh_key_parser from chevah_keycert.tests import keydata from chevah_keycert.tests.helpers import CommandLineMixin - OPENSSH_RSA_PRIVATE = b"""-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgQC4fV6tSakDSB6ZovygLsf1iC9P3tJHePTKAPkPAWzlu5BRHcmA u0uTjn7GhrpxbjjWMwDVN0Oxzw7teI0OEIVkpnlcyM6L5mGk+X6Lc4+lAfp1YxCR diff --git a/src/chevah_keycert/tests/test_ssl.py b/src/chevah_keycert/tests/test_ssl.py index 0bfef49..963ce25 100644 --- a/src/chevah_keycert/tests/test_ssl.py +++ b/src/chevah_keycert/tests/test_ssl.py @@ -3,12 +3,12 @@ """ Test for SSL keys/cert management. """ -from __future__ import unicode_literals -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals + from argparse import ArgumentParser from bunch import Bunch -from chevah_compat.testing import mk, ChevahTestCase +from chevah_compat.testing import ChevahTestCase, mk from OpenSSL import crypto from chevah_keycert.exceptions import KeyCertException From 1fa3b4b56264b1ef2a0edafcf33948264729b23a Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 08:23:23 +0000 Subject: [PATCH 34/41] Add interop test cmd. --- .github/workflows/main.yml | 41 ++++++++++++++++++++++++++++++-------- pavement.py | 37 +++++++++++++++++++++------------- pyproject.toml | 2 ++ setup.cfg | 1 + 4 files changed, 59 insertions(+), 22 deletions(-) create mode 100644 pyproject.toml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d440a3..cbe13e9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,6 +18,10 @@ concurrency: group: chevah-keycert-${{ github.ref }} cancel-in-progress: true + +env: + CHEVAH_BUILD: "build-py3" + jobs: ubuntu: @@ -27,10 +31,10 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Cache build - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | build-py3 @@ -56,10 +60,10 @@ jobs: macos: runs-on: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Cache build - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | build-py3 @@ -77,10 +81,10 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Cache build - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | build-by3 @@ -107,10 +111,10 @@ jobs: - { test_type: "load ed25519" } - { test_type: "generate" } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Cache build - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | build-py3 @@ -123,3 +127,24 @@ jobs: - name: Test run: ./pythia.sh test_interop ${{ matrix.config.test_type }} + + lint: + runs-on: ubuntu-latest + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + - name: Cache build + uses: actions/cache@v3 + with: + path: | + build-py3 + key: ${{ runner.os }}-${{ hashFiles('setup.py') }} + + - name: Deps + run: | + ./pythia.sh deps + + - name: Lint + run: ./pythia.sh lint diff --git a/pavement.py b/pavement.py index 484ffd3..8191be5 100644 --- a/pavement.py +++ b/pavement.py @@ -16,6 +16,24 @@ SOURCE_FILES = ["pavement.py", "src"] +def _get_option(options, name, default=None): + """ + Helper to extract the command line options from paver. + """ + option_keys = list(options.keys()) + option_keys.remove("dry_run") + option_keys.remove("pavement_file") + bunch = options.get(option_keys[0]) + value = bunch.get(name, None) + if value is None: + return default + + if value is True: + return True + + return value.lstrip("=") + + @task def default(): call_task("test") @@ -68,8 +86,6 @@ def _nose(args, cov, base="chevah_keycert.tests"): from nose.core import main as nose_main from nose.plugins.base import Plugin - import chevah_keycert - class LoopPlugin(Plugin): name = "loop" @@ -127,21 +143,14 @@ def test_interop(options): """ Run the SSH key interoperability tests. """ - import pdb - import sys - - sys.stdout = sys.__stdout__ - pdb.set_trace() - test_type = args[0] - environ = os.environ.copy() environ["CHEVAH_BUILD"] = BUILD_DIR - if test_type == "load": - key_type = args[1] - test_command = "ssh_load_keys_tests.sh {}".format(key_type) - else: + if _get_option(options, "generate"): test_command = "ssh_gen_keys_tests.sh" + else: + key_type = _get_option(options, "load") + test_command = "ssh_load_keys_tests.sh {}".format(key_type) try: os.mkdir(BUILD_DIR) @@ -169,7 +178,7 @@ def lint(): from pyflakes.api import main as pyflakes_main try: - pyflakes_main(args=["src/chevah_keycert"]) + pyflakes_main(args=SOURCE_FILES) except SystemExit as error: if error.code: raise diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5d7bf33 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.isort] +profile = "black" diff --git a/setup.cfg b/setup.cfg index 4930cad..c2c00cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,7 @@ dev = pocketlint ==1.4.4.c10 pyflakes >= 3.2.0 black == 24.2.0 + isort == 5.13.2 chevah-compat >= 0.70 From 5bf9bb03adfe605888ac07508a19e0b7ea41cb7e Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Wed, 13 Mar 2024 09:21:11 +0000 Subject: [PATCH 35/41] Fix interop tests. --- .github/workflows/main.yml | 10 +++++----- pavement.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cbe13e9..a2dc32a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -105,11 +105,11 @@ jobs: fail-fast: false matrix: config: - - { test_type: "load dsa" } - - { test_type: "load rsa" } - - { test_type: "load ecdsa" } - - { test_type: "load ed25519" } - - { test_type: "generate" } + - { test_type: "--load dsa" } + - { test_type: "--load rsa" } + - { test_type: "--load ecdsa" } + - { test_type: "--load ed25519" } + - { test_type: "--generate" } steps: - uses: actions/checkout@v4 diff --git a/pavement.py b/pavement.py index 8191be5..b6a4cad 100644 --- a/pavement.py +++ b/pavement.py @@ -159,6 +159,7 @@ def test_interop(options): exit_code = 1 with pushd(BUILD_DIR): + print("Testing: {}".format(test_command)) exit_code = call( "../src/chevah_keycert/tests/{}".format(test_command), shell=True, From 6c48654f48cc02b9a8ab64628e39c044d9daf820 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sat, 23 Mar 2024 01:25:27 +0000 Subject: [PATCH 36/41] Update tests. --- pavement.py | 9 +- src/chevah_keycert/ssh.py | 8 +- .../tests/ssh_gen_keys_tests.sh | 12 +- src/chevah_keycert/tests/test_ssh.py | 286 +++++++++++++++++- 4 files changed, 299 insertions(+), 16 deletions(-) diff --git a/pavement.py b/pavement.py index b6a4cad..393f3e6 100644 --- a/pavement.py +++ b/pavement.py @@ -136,7 +136,7 @@ class LoopPlugin(Plugin): @cmdopts( [ ("load=", "l", "Run key loading tests."), - ("generate", "g", "Run key generation tests."), + ("generate=", "g", "Run key generation tests."), ] ) def test_interop(options): @@ -146,10 +146,11 @@ def test_interop(options): environ = os.environ.copy() environ["CHEVAH_BUILD"] = BUILD_DIR - if _get_option(options, "generate"): - test_command = "ssh_gen_keys_tests.sh" + generate_option = _get_option(options, "generate") + key_type = _get_option(options, "load") + if generate_option: + test_command = "ssh_gen_keys_tests.sh {}".format(generate_option) else: - key_type = _get_option(options, "load") test_command = "ssh_load_keys_tests.sh {}".format(key_type) try: diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index aba9a21..a9e2453 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -950,7 +950,7 @@ def __repr__(self): """ if self.type() == "EC": data = self.data() - name = data["curve"].decode("utf-8") + name = data["curve"] if self.isPublic(): out = "\xc6\xe9\xb6\xd6j\xdc\xa5\xc3\xad@", + data["k"], + ) + + def checkParseECDSA256Private(self, sut): + """ + Check the default private ECDSA key of size 256. + """ + self.assertEqual(256, sut.size()) + self.assertEqual("EC", sut.type()) + self.assertEqual(b"ecdsa-sha2-nistp256", sut.sshType()) + self.assertFalse(sut.isPublic()) + data = sut.data() + self.assertEqual( + int( + "108653985922575495831455438688025548017135775794055889135985" + "150468304120654256" + ), + data["x"], + ) + self.assertEqual( + int( + "3539295734849026692713678957269176284433037397838158425727137" + "8201444091096204" + ), + data["y"], + ) + self.assertEqual( + int( + "252084699263301204901777793191881954449812697988584991308139" + "15648781475852048" + ), + data["privateValue"], + ) + self.assertEqual("ecdsa-sha2-nistp256", data["curve"]) + + def checkParseECDSA384Private(self, sut): + """ + Check the default private ECDSA key of size 384. + """ + self.assertEqual(384, sut.size()) + self.assertEqual("EC", sut.type()) + self.assertEqual(b"ecdsa-sha2-nistp384", sut.sshType()) + self.assertFalse(sut.isPublic()) + data = sut.data() + self.assertEqual( + int( + "1120377828922608503816705989895402195704724469432815819813909" + "3563493784434074664770757552630188877039345451446332075" + ), + data["x"], + ) + self.assertEqual( + int( + "820561365460938594173613421894075233970023220702364300455709" + "9067975115514374468669516341218754297628118025144267242" + ), + data["y"], + ) + self.assertEqual( + int( + "217704394275079527449821219041780952018954068994705805522096" + "48672017052607392742400750553189721892676300516837773655" + ), + data["privateValue"], + ) + self.assertEqual("ecdsa-sha2-nistp384", data["curve"]) + + def checkParseECDSA521Private(self, sut): + """ + Check the default private ECDSA key of size 384. + """ + self.assertEqual(521, sut.size()) + self.assertEqual("EC", sut.type()) + self.assertEqual(b"ecdsa-sha2-nistp521", sut.sshType()) + self.assertFalse(sut.isPublic()) + data = sut.data() + self.assertEqual( + int( + "575946163275684216572287655819753290168045735639065337167146" + "2757410336252172929914930328513418153583025932019025257555921" + "977303915549355871374495357809927222" + ), + data["x"], + ) + self.assertEqual( + int( + "2466648813452073098641976930882615172021518938344202222286332" + "6007817176633618054662362944525041064939804805715990911995870" + "75381269720405103254588443613182258" + ), + data["y"], + ) + self.assertEqual( + int( + "4221861115108077704182122958166381218745996746275086003675630" + "8531396812933733891747966663579986273453584268921082684125610" + "37807083546341161456593999982299619" + ), + data["privateValue"], + ) + self.assertEqual("ecdsa-sha2-nistp521", data["curve"]) + def checkParsedRSAPublic1024(self, sut): """ Check the default public RSA key of size 1024. @@ -2620,14 +2739,94 @@ def test_verifyDSANoPrefix(self): key = keys.Key.fromString(keydata.publicDSA_openssh) self.assertTrue(key.verify(self.dsaSignature[-40:], b"")) - def test_repr(self): + def test_repr_rsa(self): """ - Test the pretty representation of Key. + It can repr a RSA private and public key.. """ - result = repr(keys.Key(self.rsaObj)) + sut = keys.Key.fromString(OPENSSH_RSA_PRIVATE) + result = repr(sut) + + self.assertContains( + " Date: Sat, 23 Mar 2024 01:38:29 +0000 Subject: [PATCH 37/41] Fix gen tests. --- .github/workflows/main.yml | 10 +++++----- src/chevah_keycert/tests/ssh_gen_keys_tests.sh | 2 +- src/chevah_keycert/tests/ssh_load_keys_tests.sh | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a2dc32a..9c9389f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -105,11 +105,11 @@ jobs: fail-fast: false matrix: config: - - { test_type: "--load dsa" } - - { test_type: "--load rsa" } - - { test_type: "--load ecdsa" } - - { test_type: "--load ed25519" } - - { test_type: "--generate" } + - test_type: "--load dsa" + - test_type: "--load rsa" + - test_type: "--load ecdsa" + - test_type: "--load ed25519" + - test_type: "--generate ' '" # We generate all key types steps: - uses: actions/checkout@v4 diff --git a/src/chevah_keycert/tests/ssh_gen_keys_tests.sh b/src/chevah_keycert/tests/ssh_gen_keys_tests.sh index eebda10..e693d1e 100755 --- a/src/chevah_keycert/tests/ssh_gen_keys_tests.sh +++ b/src/chevah_keycert/tests/ssh_gen_keys_tests.sh @@ -30,7 +30,7 @@ sort_tests_per_error(){ local cmd_to_test=$* local cmd_err_code - echo "CHECKINg: $*" + echo "CHECKING: $*" set +e $cmd_to_test cmd_err_code=$? diff --git a/src/chevah_keycert/tests/ssh_load_keys_tests.sh b/src/chevah_keycert/tests/ssh_load_keys_tests.sh index a63fd03..4b1e37b 100755 --- a/src/chevah_keycert/tests/ssh_load_keys_tests.sh +++ b/src/chevah_keycert/tests/ssh_load_keys_tests.sh @@ -228,11 +228,11 @@ for KEY in $KEY_TYPES; do putty_keys_test "256 384 521" ;; "rsa") - putty_keys_test "512 2048 4096" + putty_keys_test "1024 4096" ;; "dsa") # An unusual prime size is also tested. - putty_keys_test "2111 3072 4096" + putty_keys_test "1024 2111" ;; esac done From b2722206eaf8003020d771da55e27a9d585a63c8 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sat, 23 Mar 2024 02:02:16 +0000 Subject: [PATCH 38/41] Reduce rsa test size. --- src/chevah_keycert/tests/ssh_load_keys_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chevah_keycert/tests/ssh_load_keys_tests.sh b/src/chevah_keycert/tests/ssh_load_keys_tests.sh index 4b1e37b..98c5991 100755 --- a/src/chevah_keycert/tests/ssh_load_keys_tests.sh +++ b/src/chevah_keycert/tests/ssh_load_keys_tests.sh @@ -228,7 +228,7 @@ for KEY in $KEY_TYPES; do putty_keys_test "256 384 521" ;; "rsa") - putty_keys_test "1024 4096" + putty_keys_test "1024 2048" ;; "dsa") # An unusual prime size is also tested. From 27bf7fd23e43ded8993201b4a2f85652334f6118 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sat, 23 Mar 2024 02:31:59 +0000 Subject: [PATCH 39/41] Update for 3.1.0. --- release-notes.rst | 1 + setup.cfg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/release-notes.rst b/release-notes.rst index 588f15b..d011ca2 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -7,6 +7,7 @@ Release notes for Chevah KeyCert * Remove support for py2 * Remove support for LSH * Add support for Putty key gen3 +* Update automated tests 3.0.12 - 2024-01-27 diff --git a/setup.cfg b/setup.cfg index c2c00cd..8b09657 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chevah-keycert -version = 3.0.12 +version = 3.1.0 maintainer = Adi Roiban maintainer_email = adi.roiban@proatria.com license = MIT From d339d0577a1e03b4a312275db9c78fede6f8da15 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sat, 23 Mar 2024 03:45:44 +0000 Subject: [PATCH 40/41] Fix writing putty v3 keys. --- src/chevah_keycert/ssh.py | 9 +- .../tests/ssh_load_keys_tests.sh | 1 + src/chevah_keycert/tests/test_ssh.py | 86 ++++++++++++++++++- 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/chevah_keycert/ssh.py b/src/chevah_keycert/ssh.py index a9e2453..00785e1 100644 --- a/src/chevah_keycert/ssh.py +++ b/src/chevah_keycert/ssh.py @@ -2667,6 +2667,7 @@ def _fromString_PRIVATE_PUTTY_V3(cls, data, passphrase): ) computed_mac = hmac.new(hmac_key, hmac_data, sha256).hexdigest() + if private_mac != computed_mac: if encryption_key: raise EncryptedKeyError("Bad password or HMAC mismatch.") @@ -2887,15 +2888,14 @@ def _toString_PUTTY_V3_private(self, extra): + common.NS(public_blob) + common.NS(private_blob_plain) ) - hmac_key = sha256(hmac_key).digest() - private_mac = hmac.new(hmac_key, hmac_data, sha256).hexdigest() + private_mac = hmac.new(hmac_key, hmac_data, sha256).hexdigest() lines.append("PuTTY-User-Key-File-3: %s" % key_type.decode("ascii")) lines.append("Encryption: %s" % encryption_type) - lines.extend(encryption_headers) lines.append("Comment: %s" % comment) lines.append("Public-Lines: %s" % len(public_lines)) lines.extend(public_lines) + lines.extend(encryption_headers) lines.append("Private-Lines: %s" % len(private_lines)) lines.extend(private_lines) lines.append("Private-MAC: %s" % private_mac) @@ -3111,11 +3111,12 @@ def generate_ssh_key(options, open_method=None): ) message = ( - 'SSH key of type "%s" and length "%d" generated as ' + 'SSH key of type "%s" and length "%d" generated as %s ' 'public key file "%s" and private key file "%s" %s.' ) % ( key.sshType().decode("ascii"), key.size(), + key_format, public_file, private_file, message_comment, diff --git a/src/chevah_keycert/tests/ssh_load_keys_tests.sh b/src/chevah_keycert/tests/ssh_load_keys_tests.sh index 98c5991..fc8e25e 100755 --- a/src/chevah_keycert/tests/ssh_load_keys_tests.sh +++ b/src/chevah_keycert/tests/ssh_load_keys_tests.sh @@ -53,6 +53,7 @@ keycert_load_key(){ local keycert_opts="$keycert_opts --password $2" fi set +e + echo "GENERATING: $KEYCERT_CMD $keycert_opts" $KEYCERT_CMD $keycert_opts local keycert_err_code=$? set -e diff --git a/src/chevah_keycert/tests/test_ssh.py b/src/chevah_keycert/tests/test_ssh.py index 2fbfc81..3625a29 100644 --- a/src/chevah_keycert/tests/test_ssh.py +++ b/src/chevah_keycert/tests/test_ssh.py @@ -499,8 +499,7 @@ Private-MAC: 73cdd8880d60561a21bc23017b191471354158e2f343e1b48e8dbe0e46b74067\r """.strip() -PUTTY_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD = """P -uTTY-User-Key-File-2: ecdsa-sha2-nistp521\r +PUTTY_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD = """PuTTY-User-Key-File-2: ecdsa-sha2-nistp521\r Encryption: none\r Comment: ecdsa-key-20210106\r Public-Lines: 4\r @@ -2563,6 +2562,17 @@ def test_toString_PUTTY_RSA_plain(self): reloaded = Key.fromString(result) self.assertEqual(sut, reloaded) + def test_toString_PUTTY_v3_RSA_plain(self): + """ + Can export to private RSA Putty v3 without encryption. + """ + sut = Key.fromString(OPENSSH_RSA_PRIVATE) + + result = sut.toString(type="putty_v3") + # Load the serialized key and see that we get the same key. + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + def test_toString_PUTTY_RSA_encrypted(self): """ Can export to encrypted private RSA Putty key. @@ -2577,6 +2587,17 @@ def test_toString_PUTTY_RSA_encrypted(self): reloaded = Key.fromString(result, passphrase="write-pass") self.assertEqual(sut, reloaded) + def test_toString_PUTTY_v3_RSA_encrypted(self): + """ + Can export to encrypted private RSA Putty key v3. + """ + sut = Key.fromString(OPENSSH_RSA_PRIVATE) + + result = sut.toString(type="putty_v3", extra="write-pass") + + reloaded = Key.fromString(result, passphrase="write-pass") + self.assertEqual(sut, reloaded) + def test_toString_PUTTY_DSA_plain(self): """ Can export to private DSA Putty key without encryption. @@ -2591,6 +2612,61 @@ def test_toString_PUTTY_DSA_plain(self): reloaded = Key.fromString(result) self.assertEqual(sut, reloaded) + def test_toString_PUTTY_v3_DSA_plain(self): + """ + Can export to private DSA Putty key in v3 format without encryption. + """ + sut = Key.fromString(OPENSSH_DSA_PRIVATE) + + result = sut.toString(type="putty_v3") + + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + + def test_toString_PUTTY_v3_ed25519_plain(self): + """ + Can export to private ed25519 Putty key in v3 format without encryption. + """ + sut = Key.fromString(PUTTY_ED25519_PRIVATE_NO_PASSWORD) + + result = sut.toString(type="putty_v3") + + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + + def test_toString_PUTTY_v3_ecdsa256_plain(self): + """ + Can export to private ecdsa 256 Putty key in v3 format without encryption. + """ + sut = Key.fromString(PUTTY_ECDSA_SHA2_NISTP256_PRIVATE_NO_PASSWORD) + + result = sut.toString(type="putty_v3") + + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + + def test_toString_PUTTY_v3_ecdsa384_plain(self): + """ + Can export to private ecdsa 384 Putty key in v3 format without encryption. + """ + sut = Key.fromString(PUTTY_ECDSA_SHA2_NISTP384_PRIVATE_NO_PASSWORD) + + result = sut.toString(type="putty_v3") + + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + + def test_toString_PUTTY_v3_ecdsa521_plain(self): + """ + Can export to private ecdsa 384 Putty key in v3 format without encryption. + """ + sut = Key.fromString(PUTTY_ECDSA_SHA2_NISTP521_PRIVATE_NO_PASSWORD) + + result = sut.toString(type="putty_v3") + + reloaded = Key.fromString(result) + self.assertEqual(sut, reloaded) + def test_toString_PUTTY_public(self): """ Can export to public RSA Putty. @@ -3126,7 +3202,8 @@ def test_generate_ssh_key_custom_values(self): exit_code, message, key = generate_ssh_key(options, open_method=open_method) self.assertEqual( - 'SSH key of type "ssh-dss" and length "2048" generated as public ' + 'SSH key of type "ssh-dss" and length "2048" generated as ' + "openssh_v1 public " 'key file "%s" and private key file "%s" ' "without comment as not supported by the output format." % (file_name_pub, file_name), @@ -3175,7 +3252,8 @@ def test_generate_ssh_key_default_values(self): # Message informs what default values were used. self.assertEqual( - 'SSH key of type "ssh-rsa" and length "1024" generated as public ' + 'SSH key of type "ssh-rsa" and length "1024" generated as ' + "openssh_v1 public " 'key file "id_rsa.pub" and private key file "id_rsa" without ' "a comment.", message, From d5507b9156ab1f82aadaa87fd93b37dd856c1bc5 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sat, 23 Mar 2024 04:28:05 +0000 Subject: [PATCH 41/41] Update release date. --- release-notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes.rst b/release-notes.rst index d011ca2..9975e84 100644 --- a/release-notes.rst +++ b/release-notes.rst @@ -1,7 +1,7 @@ Release notes for Chevah KeyCert ################################ -3.1.0 - 2024-03-11 +3.1.0 - 2024-03-23 ================== * Remove support for py2