[Python-modules-commits] [sshpubkeys] 01/02: import sshpubkeys_2.2.0.orig.tar.gz

Vincent Bernat bernat at moszumanska.debian.org
Thu Nov 23 12:59:54 UTC 2017


This is an automated email from the git hooks/post-receive script.

bernat pushed a commit to annotated tag debian/2.2.0-1
in repository sshpubkeys.

commit 585ed6a652489da4df7c2d9e165808819d5baab1
Author: Vincent Bernat <vincent at bernat.im>
Date:   Thu Nov 23 13:41:10 2017 +0100

    import sshpubkeys_2.2.0.orig.tar.gz
---
 PKG-INFO                                 | 115 +++++++++
 README.rst                               |  91 +++++++
 setup.cfg                                |   8 +
 setup.py                                 |  45 ++++
 sshpubkeys.egg-info/PKG-INFO             | 115 +++++++++
 sshpubkeys.egg-info/SOURCES.txt          |  10 +
 sshpubkeys.egg-info/dependency_links.txt |   1 +
 sshpubkeys.egg-info/requires.txt         |   6 +
 sshpubkeys.egg-info/top_level.txt        |   1 +
 sshpubkeys/__init__.py                   | 416 +++++++++++++++++++++++++++++++
 sshpubkeys/exceptions.py                 |  55 ++++
 11 files changed, 863 insertions(+)

diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..ededb69
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,115 @@
+Metadata-Version: 1.1
+Name: sshpubkeys
+Version: 2.2.0
+Summary: SSH public key parser
+Home-page: https://github.com/ojarva/python-sshpubkeys
+Author: Olli Jarva
+Author-email: olli at jarva.fi
+License: BSD
+Description: OpenSSH Public Key Parser for Python
+        ====================================
+        
+        .. image:: https://travis-ci.org/ojarva/python-sshpubkeys.svg?branch=master
+            :target: https://travis-ci.org/ojarva/python-sshpubkeys
+        
+        .. image:: https://pypip.in/v/sshpubkeys/badge.png
+            :target: https://pypi.python.org/pypi/sshpubkeys
+        
+        Native implementation for validating OpenSSH public keys.
+        
+        Currently ssh-rsa, ssh-dss (DSA), ssh-ed25519 and ecdsa keys with NIST curves are supported.
+        
+        Installation:
+        
+        ::
+        
+          pip install sshpubkeys
+        
+        or clone the `repository <https://github.com/ojarva/sshpubkeys>`_ and use
+        
+        ::
+        
+          python setup.py install
+        
+        Usage:
+        
+        ::
+        
+          import sys
+          from sshpubkeys import SSHKey
+        
+          ssh = SSHKey("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAYQCxO38tKAJXIs9ivPxt7AY"
+                "dfybgtAR1ow3Qkb9GPQ6wkFHQqcFDe6faKCxH6iDRteo4D8L8B"
+                "xwzN42uZSB0nfmjkIxFTcEU3mFSXEbWByg78aoddMrAAjatyrh"
+                "H1pON6P0= ojarva at ojar-laptop", strict_mode=True)
+          try:
+              ssh.parse()
+          except InvalidKeyException as err:
+              print("Invalid key:", err)
+              sys.exit(1)
+          except NotImplementedError as err:
+              print("Invalid key type:", err)
+              sys.exit(1)
+        
+          print(ssh.bits)  # 768
+          print(ssh.hash_md5())  # 56:84:1e:90:08:3b:60:c7:29:70:5f:5e:25:a6:3b:86
+          print(ssh.hash_sha256())  # SHA256:xk3IEJIdIoR9MmSRXTP98rjDdZocmXJje/28ohMQEwM
+          print(ssh.hash_sha512())  # SHA512:1C3lNBhjpDVQe39hnyy+xvlZYU3IPwzqK1rVneGavy6O3/ebjEQSFvmeWoyMTplIanmUK1hmr9nA8Skmj516HA
+          print(ssh.comment)  # ojar at ojar-laptop
+          print(ssh.options_raw)  # None (string of optional options at the beginning of public key)
+          print(ssh.options)  # None (options as a dictionary, parsed and validated)
+        
+        Options
+        -------
+        
+        Set options in constructor as a keywords (i.e., `SSHKey(None, strict_mode=False)`)
+        
+        - strict_mode: defaults to True. Disallows keys OpenSSH's ssh-keygen refuses to create. For instance, this includes DSA keys where length != 1024 bits and RSA keys shorter than 1024-bit. If set to False, tries to allow all keys OpenSSH accepts, including highly insecure 1-bit DSA keys.
+        - skip_option_parsing: if set to True, options string is not parsed (ssh.options_raw is populated, but ssh.options is not).
+        
+        Exceptions
+        ----------
+        
+        - NotImplementedError if invalid ecdsa curve or unknown key type is encountered.
+        - InvalidKeyException if any other error is encountered:
+            - TooShortKeyException if key is too short (<768 bits for RSA, <1024 for DSA, <256 for ED25519)
+            - TooLongKeyException if key is too long (>16384 for RSA, >1024 for DSA, >256 for ED25519)
+            - InvalidTypeException if key type ("ssh-rsa" in above example) does not match to what is included in base64 encoded data.
+            - MalformedDataException if decoding and extracting the data fails.
+            - InvalidOptionsException if options string is invalid.
+                - InvalidOptionNameException if option name contains invalid characters.
+                    - UnknownOptionNameException if option name is not recognized.
+                - MissingMandatoryOptionValueException if option needs to have parameter, but it is absent.
+        
+        Tests
+        -----
+        
+        See "`tests/ <https://github.com/ojarva/sshpubkeys/tree/master/tests>`_" folder for unit tests. Use
+        
+        ::
+        
+          python setup.py test
+        
+        or
+        
+        ::
+        
+          python3 setup.py test
+        
+        to run test suite. If you have keys that are not parsed properly, or malformed keys that raise incorrect exception, please send your *public key* to olli at jarva.fi, and I'll include it. Alternatively, `create a new issue <https://github.com/ojarva/sshpubkeys/issues/new>`_ or make `a pull request <https://github.com/ojarva/sshpubkeys/compare>`_ in github.
+        
+Keywords: ssh pubkey public key openssh ssh-rsa ssh-dss ssh-ed25519
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: System Administrators
+Classifier: Topic :: Security
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: Implementation :: PyPy
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..e49f2a3
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,91 @@
+OpenSSH Public Key Parser for Python
+====================================
+
+.. image:: https://travis-ci.org/ojarva/python-sshpubkeys.svg?branch=master
+    :target: https://travis-ci.org/ojarva/python-sshpubkeys
+
+.. image:: https://pypip.in/v/sshpubkeys/badge.png
+    :target: https://pypi.python.org/pypi/sshpubkeys
+
+Native implementation for validating OpenSSH public keys.
+
+Currently ssh-rsa, ssh-dss (DSA), ssh-ed25519 and ecdsa keys with NIST curves are supported.
+
+Installation:
+
+::
+
+  pip install sshpubkeys
+
+or clone the `repository <https://github.com/ojarva/sshpubkeys>`_ and use
+
+::
+
+  python setup.py install
+
+Usage:
+
+::
+
+  import sys
+  from sshpubkeys import SSHKey
+
+  ssh = SSHKey("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAYQCxO38tKAJXIs9ivPxt7AY"
+        "dfybgtAR1ow3Qkb9GPQ6wkFHQqcFDe6faKCxH6iDRteo4D8L8B"
+        "xwzN42uZSB0nfmjkIxFTcEU3mFSXEbWByg78aoddMrAAjatyrh"
+        "H1pON6P0= ojarva at ojar-laptop", strict_mode=True)
+  try:
+      ssh.parse()
+  except InvalidKeyException as err:
+      print("Invalid key:", err)
+      sys.exit(1)
+  except NotImplementedError as err:
+      print("Invalid key type:", err)
+      sys.exit(1)
+
+  print(ssh.bits)  # 768
+  print(ssh.hash_md5())  # 56:84:1e:90:08:3b:60:c7:29:70:5f:5e:25:a6:3b:86
+  print(ssh.hash_sha256())  # SHA256:xk3IEJIdIoR9MmSRXTP98rjDdZocmXJje/28ohMQEwM
+  print(ssh.hash_sha512())  # SHA512:1C3lNBhjpDVQe39hnyy+xvlZYU3IPwzqK1rVneGavy6O3/ebjEQSFvmeWoyMTplIanmUK1hmr9nA8Skmj516HA
+  print(ssh.comment)  # ojar at ojar-laptop
+  print(ssh.options_raw)  # None (string of optional options at the beginning of public key)
+  print(ssh.options)  # None (options as a dictionary, parsed and validated)
+
+Options
+-------
+
+Set options in constructor as a keywords (i.e., `SSHKey(None, strict_mode=False)`)
+
+- strict_mode: defaults to True. Disallows keys OpenSSH's ssh-keygen refuses to create. For instance, this includes DSA keys where length != 1024 bits and RSA keys shorter than 1024-bit. If set to False, tries to allow all keys OpenSSH accepts, including highly insecure 1-bit DSA keys.
+- skip_option_parsing: if set to True, options string is not parsed (ssh.options_raw is populated, but ssh.options is not).
+
+Exceptions
+----------
+
+- NotImplementedError if invalid ecdsa curve or unknown key type is encountered.
+- InvalidKeyException if any other error is encountered:
+    - TooShortKeyException if key is too short (<768 bits for RSA, <1024 for DSA, <256 for ED25519)
+    - TooLongKeyException if key is too long (>16384 for RSA, >1024 for DSA, >256 for ED25519)
+    - InvalidTypeException if key type ("ssh-rsa" in above example) does not match to what is included in base64 encoded data.
+    - MalformedDataException if decoding and extracting the data fails.
+    - InvalidOptionsException if options string is invalid.
+        - InvalidOptionNameException if option name contains invalid characters.
+            - UnknownOptionNameException if option name is not recognized.
+        - MissingMandatoryOptionValueException if option needs to have parameter, but it is absent.
+
+Tests
+-----
+
+See "`tests/ <https://github.com/ojarva/sshpubkeys/tree/master/tests>`_" folder for unit tests. Use
+
+::
+
+  python setup.py test
+
+or
+
+::
+
+  python3 setup.py test
+
+to run test suite. If you have keys that are not parsed properly, or malformed keys that raise incorrect exception, please send your *public key* to olli at jarva.fi, and I'll include it. Alternatively, `create a new issue <https://github.com/ojarva/sshpubkeys/issues/new>`_ or make `a pull request <https://github.com/ojarva/sshpubkeys/compare>`_ in github.
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..6f08d0e
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,8 @@
+[bdist_wheel]
+universal = 1
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..812586e
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,45 @@
+from setuptools import setup
+from codecs import open as codecs_open
+from os import path
+
+here = path.abspath(path.dirname(__file__))
+
+with codecs_open(path.join(here, 'README.rst'), encoding='utf-8') as f:
+    long_description = f.read()
+
+setup(
+    name='sshpubkeys',
+    version='2.2.0',
+    description='SSH public key parser',
+    long_description=long_description,
+    url='https://github.com/ojarva/python-sshpubkeys',
+    author='Olli Jarva',
+    author_email='olli at jarva.fi',
+    license='BSD',
+
+    classifiers=[
+        'Development Status :: 4 - Beta',
+
+        'Intended Audience :: Developers',
+        'Intended Audience :: System Administrators',
+        'Topic :: Security',
+        'License :: OSI Approved :: BSD License',
+
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.2',
+        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: Implementation :: PyPy',
+    ],
+    keywords='ssh pubkey public key openssh ssh-rsa ssh-dss ssh-ed25519',
+    packages=["sshpubkeys"],
+    test_suite="tests",
+    install_requires=['pycrypto>=2.6', 'ecdsa>=0.13'],
+
+    extras_require={
+        'dev': ['twine', 'wheel'],
+    },
+)
diff --git a/sshpubkeys.egg-info/PKG-INFO b/sshpubkeys.egg-info/PKG-INFO
new file mode 100644
index 0000000..ededb69
--- /dev/null
+++ b/sshpubkeys.egg-info/PKG-INFO
@@ -0,0 +1,115 @@
+Metadata-Version: 1.1
+Name: sshpubkeys
+Version: 2.2.0
+Summary: SSH public key parser
+Home-page: https://github.com/ojarva/python-sshpubkeys
+Author: Olli Jarva
+Author-email: olli at jarva.fi
+License: BSD
+Description: OpenSSH Public Key Parser for Python
+        ====================================
+        
+        .. image:: https://travis-ci.org/ojarva/python-sshpubkeys.svg?branch=master
+            :target: https://travis-ci.org/ojarva/python-sshpubkeys
+        
+        .. image:: https://pypip.in/v/sshpubkeys/badge.png
+            :target: https://pypi.python.org/pypi/sshpubkeys
+        
+        Native implementation for validating OpenSSH public keys.
+        
+        Currently ssh-rsa, ssh-dss (DSA), ssh-ed25519 and ecdsa keys with NIST curves are supported.
+        
+        Installation:
+        
+        ::
+        
+          pip install sshpubkeys
+        
+        or clone the `repository <https://github.com/ojarva/sshpubkeys>`_ and use
+        
+        ::
+        
+          python setup.py install
+        
+        Usage:
+        
+        ::
+        
+          import sys
+          from sshpubkeys import SSHKey
+        
+          ssh = SSHKey("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAYQCxO38tKAJXIs9ivPxt7AY"
+                "dfybgtAR1ow3Qkb9GPQ6wkFHQqcFDe6faKCxH6iDRteo4D8L8B"
+                "xwzN42uZSB0nfmjkIxFTcEU3mFSXEbWByg78aoddMrAAjatyrh"
+                "H1pON6P0= ojarva at ojar-laptop", strict_mode=True)
+          try:
+              ssh.parse()
+          except InvalidKeyException as err:
+              print("Invalid key:", err)
+              sys.exit(1)
+          except NotImplementedError as err:
+              print("Invalid key type:", err)
+              sys.exit(1)
+        
+          print(ssh.bits)  # 768
+          print(ssh.hash_md5())  # 56:84:1e:90:08:3b:60:c7:29:70:5f:5e:25:a6:3b:86
+          print(ssh.hash_sha256())  # SHA256:xk3IEJIdIoR9MmSRXTP98rjDdZocmXJje/28ohMQEwM
+          print(ssh.hash_sha512())  # SHA512:1C3lNBhjpDVQe39hnyy+xvlZYU3IPwzqK1rVneGavy6O3/ebjEQSFvmeWoyMTplIanmUK1hmr9nA8Skmj516HA
+          print(ssh.comment)  # ojar at ojar-laptop
+          print(ssh.options_raw)  # None (string of optional options at the beginning of public key)
+          print(ssh.options)  # None (options as a dictionary, parsed and validated)
+        
+        Options
+        -------
+        
+        Set options in constructor as a keywords (i.e., `SSHKey(None, strict_mode=False)`)
+        
+        - strict_mode: defaults to True. Disallows keys OpenSSH's ssh-keygen refuses to create. For instance, this includes DSA keys where length != 1024 bits and RSA keys shorter than 1024-bit. If set to False, tries to allow all keys OpenSSH accepts, including highly insecure 1-bit DSA keys.
+        - skip_option_parsing: if set to True, options string is not parsed (ssh.options_raw is populated, but ssh.options is not).
+        
+        Exceptions
+        ----------
+        
+        - NotImplementedError if invalid ecdsa curve or unknown key type is encountered.
+        - InvalidKeyException if any other error is encountered:
+            - TooShortKeyException if key is too short (<768 bits for RSA, <1024 for DSA, <256 for ED25519)
+            - TooLongKeyException if key is too long (>16384 for RSA, >1024 for DSA, >256 for ED25519)
+            - InvalidTypeException if key type ("ssh-rsa" in above example) does not match to what is included in base64 encoded data.
+            - MalformedDataException if decoding and extracting the data fails.
+            - InvalidOptionsException if options string is invalid.
+                - InvalidOptionNameException if option name contains invalid characters.
+                    - UnknownOptionNameException if option name is not recognized.
+                - MissingMandatoryOptionValueException if option needs to have parameter, but it is absent.
+        
+        Tests
+        -----
+        
+        See "`tests/ <https://github.com/ojarva/sshpubkeys/tree/master/tests>`_" folder for unit tests. Use
+        
+        ::
+        
+          python setup.py test
+        
+        or
+        
+        ::
+        
+          python3 setup.py test
+        
+        to run test suite. If you have keys that are not parsed properly, or malformed keys that raise incorrect exception, please send your *public key* to olli at jarva.fi, and I'll include it. Alternatively, `create a new issue <https://github.com/ojarva/sshpubkeys/issues/new>`_ or make `a pull request <https://github.com/ojarva/sshpubkeys/compare>`_ in github.
+        
+Keywords: ssh pubkey public key openssh ssh-rsa ssh-dss ssh-ed25519
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: System Administrators
+Classifier: Topic :: Security
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: Implementation :: PyPy
diff --git a/sshpubkeys.egg-info/SOURCES.txt b/sshpubkeys.egg-info/SOURCES.txt
new file mode 100644
index 0000000..d08251b
--- /dev/null
+++ b/sshpubkeys.egg-info/SOURCES.txt
@@ -0,0 +1,10 @@
+README.rst
+setup.cfg
+setup.py
+sshpubkeys/__init__.py
+sshpubkeys/exceptions.py
+sshpubkeys.egg-info/PKG-INFO
+sshpubkeys.egg-info/SOURCES.txt
+sshpubkeys.egg-info/dependency_links.txt
+sshpubkeys.egg-info/requires.txt
+sshpubkeys.egg-info/top_level.txt
\ No newline at end of file
diff --git a/sshpubkeys.egg-info/dependency_links.txt b/sshpubkeys.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/sshpubkeys.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/sshpubkeys.egg-info/requires.txt b/sshpubkeys.egg-info/requires.txt
new file mode 100644
index 0000000..6eb5fd2
--- /dev/null
+++ b/sshpubkeys.egg-info/requires.txt
@@ -0,0 +1,6 @@
+pycrypto>=2.6
+ecdsa>=0.13
+
+[dev]
+twine
+wheel
\ No newline at end of file
diff --git a/sshpubkeys.egg-info/top_level.txt b/sshpubkeys.egg-info/top_level.txt
new file mode 100644
index 0000000..c61c368
--- /dev/null
+++ b/sshpubkeys.egg-info/top_level.txt
@@ -0,0 +1 @@
+sshpubkeys
diff --git a/sshpubkeys/__init__.py b/sshpubkeys/__init__.py
new file mode 100644
index 0000000..45f2f48
--- /dev/null
+++ b/sshpubkeys/__init__.py
@@ -0,0 +1,416 @@
+# pylint:disable=line-too-long
+
+"""
+Parser for ssh public keys. Currently supports ssh-rsa, ssh-dsa, ssh-ed25519 and ssh-dss keys.
+
+import sys
+
+
+key_data = open("ssh-pubkey-file.pem").read()
+ssh_key = SSHKey(key_data)
+try:
+    ssh_key.parse()
+except InvalidKeyException:
+    print("Invalid key")
+    sys.exit(1)
+print(ssh_key.bits)
+
+"""
+
+import base64
+import binascii
+import hashlib
+import re
+import struct
+import sys
+import warnings
+import ecdsa
+
+from Crypto.PublicKey import RSA, DSA
+
+from .exceptions import *  # pylint:disable=wildcard-import
+
+__all__ = ["SSHKey"]
+
+
+class SSHKey(object):  # pylint:disable=too-many-instance-attributes
+    """
+    ssh_key = SSHKey(key_data, strict=True)
+    ssh_key.parse()
+
+    strict=True (default) only allows keys ssh-keygen generates. Setting strict mode to false allows
+    all keys OpenSSH actually accepts, including highly insecure ones. For example, OpenSSH accepts
+    512-bit DSA keys and 64-bit RSA keys which are highly insecure.
+    """
+
+    DSA_MIN_LENGTH_STRICT = 1024
+    DSA_MAX_LENGTH_STRICT = 1024
+    DSA_MIN_LENGTH_LOOSE = 1
+    DSA_MAX_LENGTH_LOOSE = 16384
+
+    DSA_N_LENGTH = 160
+
+    ECDSA_CURVE_DATA = {
+        b"nistp256": (ecdsa.curves.NIST256p, hashlib.sha256),
+        b"nistp192": (ecdsa.curves.NIST192p, hashlib.sha256),
+        b"nistp224": (ecdsa.curves.NIST224p, hashlib.sha256),
+        b"nistp384": (ecdsa.curves.NIST384p, hashlib.sha384),
+        b"nistp521": (ecdsa.curves.NIST521p, hashlib.sha512),
+    }
+
+    RSA_MIN_LENGTH_STRICT = 1024
+    RSA_MAX_LENGTH_STRICT = 16384
+    RSA_MIN_LENGTH_LOOSE = 768
+    RSA_MAX_LENGTH_LOOSE = 16384
+
+    # Valid as of OpenSSH_6.9p1
+    # argument name, value is mandatory. Options are case-insensitive, but this list must be in lowercase.
+    OPTIONS_SPEC = [
+        ("agent-forwarding", False),
+        ("cert-authority", False),
+        ("command", True),
+        ("environment", True),
+        ("from", True),
+        ("no-agent-forwarding", False),
+        ("no-port-forwarding", False),
+        ("no-pty", False),
+        ("no-user-rc", False),
+        ("no-x11-forwarding", False),
+        ("permitopen", True),
+        ("port-forwarding", False),
+        ("principals", True),
+        ("pty", False),
+        ("restrict", False),
+        ("tunnel", True),
+        ("user-rc", False),
+        ("x11-forwarding", False),
+    ]
+    OPTION_NAME_RE = re.compile("^[A-Za-z0-9-]+$")
+
+    INT_LEN = 4
+
+    FIELDS = ["rsa", "dsa", "ecdsa", "bits", "comment", "options", "options_raw", "key_type"]
+
+    def __init__(self, keydata=None, **kwargs):
+        self.keydata = keydata
+        self._decoded_key = None
+        self.rsa = None
+        self.dsa = None
+        self.ecdsa = None
+        self.bits = None
+        self.comment = None
+        self.options = None
+        self.options_raw = None
+        self.key_type = None
+        self.strict_mode = bool(kwargs.get("strict", True))
+        self.skip_option_parsing = bool(kwargs.get("skip_option_parsing", False))
+        if keydata:
+            try:
+                self.parse(keydata)
+            except (InvalidKeyException, NotImplementedError):
+                pass
+
+    def reset(self):
+        """ Reset all data fields """
+        for field in self.FIELDS:
+            setattr(self, field, None)
+
+    def hash(self):
+        """ Calculate md5 fingerprint.
+
+        Deprecated, use .hash_md5() instead.
+        """
+        warnings.warn("hash() is deprecated. Use hash_md5(), hash_sha256() or hash_sha512() instead.")
+        return self.hash_md5().replace(b"MD5:", b"")
+
+    def hash_md5(self):
+        """ Calculate md5 fingerprint.
+
+        Shamelessly copied from http://stackoverflow.com/questions/6682815/deriving-an-ssh-fingerprint-from-a-public-key-in-python
+
+        For specification, see RFC4716, section 4.
+        """
+        fp_plain = hashlib.md5(self._decoded_key).hexdigest()
+        return "MD5:" + ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2]))
+
+    def hash_sha256(self):
+        """ Calculate sha256 fingerprint. """
+        fp_plain = hashlib.sha256(self._decoded_key).digest()
+        return (b"SHA256:" + base64.b64encode(fp_plain).replace(b"=", b"")).decode("utf-8")
+
+    def hash_sha512(self):
+        """ Calculates sha512 fingerprint. """
+        fp_plain = hashlib.sha512(self._decoded_key).digest()
+        return (b"SHA512:" + base64.b64encode(fp_plain).replace(b"=", b"")).decode("utf-8")
+
+    def _unpack_by_int(self, data, current_position):
+        """ Returns a tuple with (location of next data field, contents of requested data field). """
+        # Unpack length of data field
+        try:
+            requested_data_length = struct.unpack('>I', data[current_position:current_position + self.INT_LEN])[0]
+        except struct.error:
+            raise MalformedDataException("Unable to unpack %s bytes from the data" % self.INT_LEN)
+
+        # Move pointer to the beginning of the data field
+        current_position += self.INT_LEN
+        remaining_data_length = len(data[current_position:])
+
+        if remaining_data_length < requested_data_length:
+            raise MalformedDataException("Requested %s bytes, but only %s bytes available." % (requested_data_length, remaining_data_length))
+
+        next_data = data[current_position:current_position + requested_data_length]
+        # Move pointer to the end of the data field
+        current_position += requested_data_length
+        return current_position, next_data
+
+    @classmethod
+    def _parse_long(cls, data):
+        """ Calculate two's complement """
+        if sys.version < '3':
+            ret = long(0)
+            for byte in data:
+                ret = (ret << 8) + ord(byte)
+        else:
+            ret = 0  # pylint:disable=redefined-variable-type
+            for byte in data:
+                ret = (ret << 8) + byte
+        return ret
+
+    def _split_key(self, data):
+        options_raw = None
+        # Terribly inefficient way to remove options, but hey, it works.
+        if not data.startswith("ssh-") and not data.startswith("ecdsa-"):
+            quote_open = False
+            for i, character in enumerate(data):
+                if character == '"':  # only double quotes are allowed, no need to care about single quotes
+                    quote_open = not quote_open
+                if quote_open:
+                    continue
+                if character == " ":
+                    # Data begins after the first space
+                    options_raw = data[:i]
+                    data = data[i + 1:]
+                    break
+            else:
+                raise MalformedDataException("Couldn't find beginning of the key data")
+        key_parts = data.strip().split(None, 2)
+        if len(key_parts) < 2:  # Key type and content are mandatory fields.
+            raise InvalidKeyException("Unexpected key format: at least type and base64 encoded value is required")
+        if len(key_parts) == 3:
+            self.comment = key_parts[2]
+            key_parts = key_parts[0:2]
+        if options_raw:
+            # Populate and parse options field.
+            self.options_raw = options_raw
+            if not self.skip_option_parsing:
+                self.options = self.parse_options(self.options_raw)
+        else:
+            # Set empty defaults for fields
+            self.options_raw = None
+            self.options = {}
+        return key_parts
+
+    @classmethod
+    def decode_key(cls, pubkey_content):
+        """ Decode base64 coded part of the key. """
+        try:
+            decoded_key = base64.b64decode(pubkey_content.encode("ascii"))
+        except (TypeError, binascii.Error):
+            raise MalformedDataException("Unable to decode the key")
+        return decoded_key
+
+    @classmethod
+    def _bits_in_number(cls, number):
+        return len(format(number, "b"))
+
+    def parse_options(self, options):
+        """ Parses ssh options string """
+        quote_open = False
+        parsed_options = {}
+
+        def parse_add_single_option(opt):
+            """ Parses and validates a single option, and adds it to parsed_options field. """
+            if "=" in opt:
+                opt_name, opt_value = opt.split("=", 1)
+                opt_value = opt_value.replace('"', '')
+            else:
+                opt_name = opt
+                opt_value = True
+            if " " in opt_name or not self.OPTION_NAME_RE.match(opt_name):
+                raise InvalidOptionNameException("%s is not valid option name." % opt_name)
+            if self.strict_mode:
+                for valid_opt_name, value_required in self.OPTIONS_SPEC:
+                    if opt_name.lower() == valid_opt_name:
+                        if value_required and opt_value is True:
+                            raise MissingMandatoryOptionValueException("%s is missing mandatory value." % opt_name)
+                        break
+                else:
+                    raise UnknownOptionNameException("%s is unrecognized option name." % opt_name)
+            if opt_name not in parsed_options:
+                parsed_options[opt_name] = []
+            parsed_options[opt_name].append(opt_value)
+
+        start_of_current_opt = 0
+        i = 1  # Need to be set for empty options strings
+        for i, character in enumerate(options):
+            if character == '"':  # only double quotes are allowed, no need to care about single quotes
+                quote_open = not quote_open
+            if quote_open:
+                continue
+            if character == ",":
+                opt = options[start_of_current_opt:i]
+                parse_add_single_option(opt)
+                start_of_current_opt = i + 1
+                # Data begins after the first space
+        if start_of_current_opt + 1 != i:
+            opt = options[start_of_current_opt:]
+            parse_add_single_option(opt)
+        if quote_open:
+            raise InvalidOptionsException("Unbalanced quotes.")
+        return parsed_options
+
+    def _process_ssh_rsa(self, data):
+        """ Parses ssh-rsa public keys """
+        current_position, raw_e = self._unpack_by_int(data, 0)
+        current_position, raw_n = self._unpack_by_int(data, current_position)
+
+        unpacked_e = self._parse_long(raw_e)
+        unpacked_n = self._parse_long(raw_n)
+
+        self.rsa = RSA.construct((unpacked_n, unpacked_e))
+        self.bits = self.rsa.size() + 1
+
+        if self.strict_mode:
+            min_length = self.RSA_MIN_LENGTH_STRICT
+            max_length = self.RSA_MAX_LENGTH_STRICT
+        else:
+            min_length = self.RSA_MIN_LENGTH_LOOSE
+            max_length = self.RSA_MAX_LENGTH_LOOSE
+        if self.bits < min_length:
+            raise TooShortKeyException("%s key data can not be shorter than %s bits (was %s)" % (self.key_type, min_length, self.bits))
+        if self.bits > max_length:
+            raise TooLongKeyException("%s key data can not be longer than %s bits (was %s)" % (self.key_type, max_length, self.bits))
+        return current_position
+
+    def _process_ssh_dss(self, data):
+        """ Parses ssh-dsa public keys """
+        data_fields = {}
+        current_position = 0
+        for item in ("p", "q", "g", "y"):
+            current_position, value = self._unpack_by_int(data, current_position)
+            data_fields[item] = self._parse_long(value)
+
+        self.dsa = DSA.construct((data_fields["y"], data_fields["g"], data_fields["p"], data_fields["q"]))
+        self.bits = self.dsa.size() + 1
+
+        q_bits = self._bits_in_number(data_fields["q"])
+        if q_bits != self.DSA_N_LENGTH:
+            raise InvalidKeyException("Incorrect DSA key parameters: bits(p)=%s, q=%s" % (self.bits, q_bits))
+        if self.strict_mode:
+            min_length = self.DSA_MIN_LENGTH_STRICT
+            max_length = self.DSA_MAX_LENGTH_STRICT
+        else:
+            min_length = self.DSA_MIN_LENGTH_LOOSE
+            max_length = self.DSA_MAX_LENGTH_LOOSE
+        if self.bits < min_length:
+            raise TooShortKeyException("%s key can not be shorter than %s bits (was %s)" % (self.key_type, min_length, self.bits))
+        if self.bits > max_length:
+            raise TooLongKeyException("%s key data can not be longer than %s bits (was %s)" % (self.key_type, max_length, self.bits))
+        return current_position
+
+    def _process_ecdsa_sha(self, data):
+        """ Parses ecdsa-sha public keys """
+        current_position, curve_information = self._unpack_by_int(data, 0)
+        if curve_information not in self.ECDSA_CURVE_DATA:
+            raise NotImplementedError("Invalid curve type: %s" % curve_information)
+        curve, hash_algorithm = self.ECDSA_CURVE_DATA[curve_information]
+
+        current_position, key_data = self._unpack_by_int(data, current_position)
+        try:
+            # data starts with \x04, which should be discarded.
+            ecdsa_key = ecdsa.VerifyingKey.from_string(key_data[1:], curve, hash_algorithm)
+        except AssertionError:
+            raise InvalidKeyException("Invalid ecdsa key")
+        self.bits = int(curve_information.replace(b"nistp", b""))
+        self.ecdsa = ecdsa_key
+        return current_position
+
+    def _process_ed25516(self, data):
+        """ Parses ed25516 keys.
+
+        There is no (apparent) way to validate ed25519 keys. This only
+        checks data length (256 bits), but does not try to validate
+        the key in any way.
+        """
+
+        current_position, verifying_key = self._unpack_by_int(data, 0)
+        verifying_key_length = len(verifying_key) * 8
+        verifying_key = self._parse_long(verifying_key)
+
+        if verifying_key < 0:
+            raise InvalidKeyException("ed25519 verifying key must be >0.")
+
+        self.bits = verifying_key_length
+        if self.bits != 256:
+            raise InvalidKeyLengthException("ed25519 keys must be 256 bits (was %s bits)" % self.bits)
+        return current_position
+
+    def _process_key(self, data):
+        if self.key_type == b"ssh-rsa":
+            return self._process_ssh_rsa(data)
+        elif self.key_type == b"ssh-dss":
+            return self._process_ssh_dss(data)
+        elif self.key_type.strip().startswith(b"ecdsa-sha"):
+            return self._process_ecdsa_sha(data)
+        elif self.key_type == b"ssh-ed25519":
+            return self._process_ed25516(data)
+        else:
+            raise NotImplementedError("Invalid key type: %s" % self.key_type)
+
+    def parse(self, keydata=None):
+        """ Validates SSH public key
+
+        Throws exception for invalid keys. Otherwise returns None.
+
+        Populates key_type, bits and bits fields.
+
+        For rsa keys, see field "rsa" for raw public key data.
+        For dsa keys, see field "dsa".
+        For ecdsa keys, see field "ecdsa". """
+        if keydata is None:
+            if self.keydata is None:
+                raise ValueError("Key data must be supplied either in constructor or to parse()")
+            keydata = self.keydata
+        else:
+            self.reset()
+            self.keydata = keydata
+
+        if keydata.startswith("---- BEGIN SSH2 PUBLIC KEY ----"):
+            # SSH2 key format
+            key_type = None  # There is no redundant key-type field - skip comparing plain-text and encoded data.
+            pubkey_content = ""
+            for line in keydata.split("\n"):
+                if ":" in line:  # key-value lines
+                    continue
+                if "----" in line:  # begin/end lines
+                    continue
+                pubkey_content += line
+        else:
+            key_parts = self._split_key(keydata)
+            key_type = key_parts[0]
+            pubkey_content = key_parts[1]
+
+        self._decoded_key = self.decode_key(pubkey_content)
+
+        # Check key type
+        current_position, unpacked_key_type = self._unpack_by_int(self._decoded_key, 0)
+        if key_type is not None and key_type != unpacked_key_type.decode():
+            raise InvalidTypeException("Keytype mismatch: %s != %s" % (key_type, unpacked_key_type))
+
+        self.key_type = unpacked_key_type
+
+        key_data_length = self._process_key(self._decoded_key[current_position:])
+        current_position = current_position + key_data_length
+
+        if current_position != len(self._decoded_key):
+            raise MalformedDataException("Leftover data: %s bytes" % (len(self._decoded_key) - current_position))
diff --git a/sshpubkeys/exceptions.py b/sshpubkeys/exceptions.py
new file mode 100644
index 0000000..d7f0ccf
--- /dev/null
+++ b/sshpubkeys/exceptions.py
@@ -0,0 +1,55 @@
+# pylint:disable=line-too-long
+
+""" Exceptions for sshpubkeys """
+
+
+class InvalidKeyException(Exception):
+    """ Invalid key - something is wrong with the key, and it should not be accepted, as OpenSSH will not work with it. """
+    pass
+
+
+class InvalidKeyLengthException(InvalidKeyException):
+    """ Invalid key length - either too short or too long.
+
+    See also TooShortKeyException and TooLongKeyException """
+    pass
+
+
+class TooShortKeyException(InvalidKeyLengthException):
+    """ Key is shorter than what the specification allows """
+    pass
+
+
+class TooLongKeyException(InvalidKeyLengthException):
+    """ Key is longer than what the specification allows """
+    pass
+
+
+class InvalidTypeException(InvalidKeyException):
+    """ Key type is invalid or unrecognized """
+    pass
+
+
+class MalformedDataException(InvalidKeyException):
+    """ The key is invalid - unable to parse the data. The data may be corrupted, truncated, or includes extra content that is not allowed. """
+    pass
+
+
+class InvalidOptionsException(MalformedDataException):
+    """ Options string is invalid: it contains invalid characters, unrecognized options, or is otherwise malformed. """
+    pass
+
+
+class InvalidOptionNameException(InvalidOptionsException):
+    """ Invalid option name (contains disallowed characters, or is unrecognized.) """
+    pass
+
+
+class UnknownOptionNameException(InvalidOptionsException):
+    """ Unrecognized option name. """
+    pass
+
+
+class MissingMandatoryOptionValueException(InvalidOptionsException):
+    """ Mandatory option value is missing. """
+    pass

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/sshpubkeys.git



More information about the Python-modules-commits mailing list