Skip to content

Commit

Permalink
loading ECDSA private keys - WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
tomato42 committed Oct 25, 2017
1 parent 8004236 commit c47c9df
Show file tree
Hide file tree
Showing 5 changed files with 473 additions and 74 deletions.
213 changes: 213 additions & 0 deletions tlslite/utils/ecdsakey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# Author: Stanislav Zidek
# See the LICENSE file for legal information regarding use of this file.

"""Abstract class for ECDSA."""

from .cryptomath import *
from . import tlshashlib as hashlib
from ..errors import MaskTooLongError, MessageTooLongError, EncodingError, \
InvalidSignature


class ECDSAKey(object):
"""This is an abstract base class for ECDSA keys.
Particular implementations of ECDSA keys, such as
:py:class:`~.python_ecdsakey.Python_ECDSAKey`
... more coming
inherit from this.
To create or parse an ECDSA key, don't use one of these classes
directly. Instead, use the factory functions in
:py:class:`~tlslite.utils.keyfactory`.
"""

def __init__(self, public_key, private_key):
"""Create a new ECDSA key.
If public_key or private_key are passed in, the new key
will be initialized.
:param public_key: ECDSA public key.
:param private_key: ECDSA private key.
"""
raise NotImplementedError()

def __len__(self):
"""Return the length of this key in bits.
:rtype: int
"""
raise NotImplementedError()

def hasPrivateKey(self):
"""Return whether or not this key has a private component.
:rtype: bool
"""
raise NotImplementedError()

def _sign(self, data):
raise NotImplementedError()

def _hashAndSign(self, data, hAlg):
raise NotImplementedError()

def hashAndSign(self, bytes, rsaScheme=None, hAlg='sha1', sLen=None):
"""Hash and sign the passed-in bytes.
This requires the key to have a private component. It performs
a signature on the passed-in data with selected hash algorithm.
:type bytes: str or bytearray
:param bytes: The value which will be hashed and signed.
:type rsaScheme: str
:param rsaScheme: Ignored
:type hAlg: str
:param hAlg: The hash algorithm that will be used
:type sLen: int
:param sLen: Ignored
:rtype: bytearray
:returns: A PKCS1 or PSS signature on the passed-in data.
"""
rsaScheme = rsaScheme.lower()
hAlg = hAlg.lower()
hashBytes = secureHash(bytearray(bytes), hAlg)
return self.sign(hashBytes, padding=rsaScheme, hashAlg=hAlg,
saltLen=sLen)

def hashAndVerify(self, sigBytes, bytes, rsaScheme='PKCS1', hAlg='sha1',
sLen=0):
"""Hash and verify the passed-in bytes with the signature.
This verifies a PKCS1 or PSS signature on the passed-in data
with selected hash algorithm.
:type sigBytes: bytearray
:param sigBytes: A PKCS1 or PSS signature.
:type bytes: str or bytearray
:param bytes: The value which will be hashed and verified.
:type rsaScheme: str
:param rsaScheme: The type of ECDSA scheme that will be applied,
"PKCS1" for ECDSASSA-PKCS#1 v1.5 signature and "PSS"
for ECDSASSA-PSS with MGF1 signature method
:type hAlg: str
:param hAlg: The hash algorithm that will be used
:type sLen: int
:param sLen: The length of intended salt value, applicable only
for ECDSASSA-PSS signatures
:rtype: bool
:returns: Whether the signature matches the passed-in data.
"""
rsaScheme = rsaScheme.lower()
hAlg = hAlg.lower()

hashBytes = secureHash(bytearray(bytes), hAlg)
return self.verify(sigBytes, hashBytes, rsaScheme, hAlg, sLen)

def sign(self, bytes, padding='pkcs1', hashAlg=None, saltLen=None):
"""Sign the passed-in bytes.
This requires the key to have a private component. It performs
a PKCS1 signature on the passed-in data.
:type bytes: bytearray
:param bytes: The value which will be signed.
:type padding: str
:param padding: name of the rsa padding mode to use, supported:
"pkcs1" for ECDSASSA-PKCS1_1_5 and "pss" for ECDSASSA-PSS.
:type hashAlg: str
:param hashAlg: name of hash to be encoded using the PKCS#1 prefix
for "pkcs1" padding or the hash used for MGF1 in "pss". Parameter
is mandatory for "pss" padding.
:type saltLen: int
:param saltLen: length of salt used for the PSS padding. Default
is the length of the hash output used.
:rtype: bytearray
:returns: A PKCS1 signature on the passed-in data.
"""
padding = padding.lower()
if padding == 'pkcs1':
if hashAlg is not None:
bytes = self.addPKCS1Prefix(bytes, hashAlg)
sigBytes = self._raw_pkcs1_sign(bytes)
elif padding == "pss":
sigBytes = self.ECDSASSA_PSS_sign(bytes, hashAlg, saltLen)
else:
raise UnknownECDSAType("Unknown ECDSA algorithm type")
return sigBytes

def verify(self, sigBytes, bytes, padding='pkcs1', hashAlg=None,
saltLen=None):
"""Verify the passed-in bytes with the signature.
This verifies a PKCS1 signature on the passed-in data.
:type sigBytes: bytearray
:param sigBytes: A PKCS1 signature.
:type bytes: bytearray
:param bytes: The value which will be verified.
:rtype: bool
:returns: Whether the signature matches the passed-in data.
"""
if padding == "pkcs1" and hashAlg == 'sha1':
# Try it with/without the embedded NULL
prefixedHashBytes1 = self.addPKCS1SHA1Prefix(bytes, False)
prefixedHashBytes2 = self.addPKCS1SHA1Prefix(bytes, True)
result1 = self._raw_pkcs1_verify(sigBytes, prefixedHashBytes1)
result2 = self._raw_pkcs1_verify(sigBytes, prefixedHashBytes2)
return (result1 or result2)
elif padding == 'pkcs1':
if hashAlg is not None:
bytes = self.addPKCS1Prefix(bytes, hashAlg)
res = self._raw_pkcs1_verify(sigBytes, bytes)
return res
elif padding == "pss":
try:
res = self.ECDSASSA_PSS_verify(bytes, sigBytes, hashAlg, saltLen)
except InvalidSignature:
res = False
return res
else:
raise UnknownECDSAType("Unknown ECDSA algorithm type")

def acceptsPassword(self):
"""Return True if the write() method accepts a password for use
in encrypting the private key.
:rtype: bool
"""
raise NotImplementedError()

def write(self, password=None):
"""Return a string containing the key.
:rtype: str
:returns: A string describing the key, in whichever format (PEM)
is native to the implementation.
"""
raise NotImplementedError()

@staticmethod
def generate(bits):
"""Generate a new key with the specified bit length.
:rtype: ~tlslite.utils.ECDSAKey.ECDSAKey
"""
raise NotImplementedError()
31 changes: 31 additions & 0 deletions tlslite/utils/python_ecdsakey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

from .ecdsakey import ECDSAKey
from ecdsa.util import sigencode_der
from .tlshashlib import new

class Python_ECDSAKey(ECDSAKey):
def __init__(self, public_key, private_key=None):
if not public_key and private_key:
self.public_key = private_key.get_verifying_key()
else:
self.public_key = public_key

self.private_key = private_key

def hasPrivateKey(self):
return bool(self.private_key)

def acceptsPassword(self):
return False

def generate(bits):
raise NotImplementedError()

def _sign(self, data):
return private_key.sign_digest_deterministic(data,
sigencode=sigencode_der)

def _hashAndSign(self, data, hAlg):
return private_key.sign_deterministic(data,
hash=new(hAlg),
sigencode=sigencode_der)
127 changes: 127 additions & 0 deletions tlslite/utils/python_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@

from .python_rsakey import Python_RSAKey
from .python_ecdsakey import Python_ECDSAKey
from .pem import dePem, pemSniff
from .asn1parser import ASN1Parser
from .cryptomath import bytesToNumber
from ecdsa.curves import NIST256p
from ecdsa.keys import SigningKey, VerifyingKey

class Python_Key(object):

@staticmethod
def parsePEM(s, passwordCallback=None):
"""Parse a string containing a PEM-encoded <privateKey>."""

if pemSniff(s, "PRIVATE KEY"):
bytes = dePem(s, "PRIVATE KEY")
return Python_Key._parsePKCS8(bytes)
elif pemSniff(s, "RSA PRIVATE KEY"):
bytes = dePem(s, "RSA PRIVATE KEY")
return Python_Key._parseSSLeay(bytes)
elif pemSniff(s, "EC PRIVATE KEY"):
bytes = dePem(s, "EC PRIVATE KEY")
return Python_Key._parseECSSLeay(bytes)
else:
raise SyntaxError("Not a PEM private key file")

@staticmethod
def _parsePKCS8(bytes):
p = ASN1Parser(bytes)

# first element in PrivateKeyInfo is an INTEGER
version = p.getChild(0).value
if bytesToNumber(version) != 0:
raise SyntaxError("Unrecognized PKCS8 version")

# second element in PrivateKeyInfo is a SEQUENCE of type
# AlgorithmIdentifier
algIdent = p.getChild(1)
seqLen = algIdent.getChildCount()
# first item of AlgorithmIdentifier is an OBJECT (OID)
oid = algIdent.getChild(0)
if list(oid.value) == [42, 134, 72, 134, 247, 13, 1, 1, 1]:
keyType = "rsa"
elif list(oid.value) == [42, 134, 72, 134, 247, 13, 1, 1, 10]:
keyType = "rsa-pss"
elif list(oid.value) == [42, 134, 72, 206, 61, 2, 1]:
keyType = "ecdsa"
else:
raise SyntaxError("Unrecognized AlgorithmIdentifier: {0}"
.format(list(oid.value)))
# second item of AlgorithmIdentifier are parameters (defined by
# above algorithm)
if keyType == "rsa":
if seqLen != 2:
raise SyntaxError("Missing parameters for RSA algorithm ID")
parameters = algIdent.getChild(1)
if parameters.value != bytearray(0):
raise SyntaxError("RSA parameters are not NULL")
elif keyType == "ecdsa":
if seqLen != 2:
raise SyntaxError("Invalid encoding of algorithm identifier")
curveID = algIdent.getChild(1)
if list(curveID.value) == [42, 134, 72, 206, 61, 3, 1, 7]:
curve = NIST256p
else:
raise SyntaxError("Unknown curve")
else: # rsa-pss
pass # ignore parameters - don't apply restrictions

if seqLen > 2:
raise SyntaxError("Invalid encoding of AlgorithmIdentifier")

#Get the privateKey
privateKeyP = p.getChild(2)

#Adjust for OCTET STRING encapsulation
privateKeyP = ASN1Parser(privateKeyP.value)

if keyType == "ecdsa":
return Python_Key._parseECDSAPrivateKey(privateKeyP,
curve)
else:
return Python_Key._parseASN1PrivateKey(privateKeyP)

@staticmethod
def _parseSSLeay(bytes):
privateKeyP = ASN1Parser(bytes)
return Python_Key._parseASN1PrivateKey(privateKeyP)

@staticmethod
def _parseECSSLeay(bytes):
return Python_ECDSAKey(None, SigningKey.from_der(bytes))

@staticmethod
def _parseECDSAPrivateKey(private, curve):
ver = private.getChild(0)
if ver.value != b'\x01':
raise SyntaxError("Unexpect EC key version")
private_key = private.getChild(1)
public_key = private.getChild(2)
# first two bytes are the ASN.1 custom type and the length of payload
# while the latter two bytes are just specification of the public
# key encoding (uncompressed)
if list(public_key.value[:1]) != [3] or \
list(public_key.value[2:4]) != [0, 4]:
raise SyntaxError("Invalid or unsupported encoding of public key")
return Python_ECDSAKey(VerifyingKey.from_string(public_key.value[4:],
curve),
SigningKey.from_string(private_key.value,
curve))

@staticmethod
def _parseASN1PrivateKey(privateKeyP):
version = privateKeyP.getChild(0).value[0]
if version != 0:
raise SyntaxError("Unrecognized RSAPrivateKey version")
n = bytesToNumber(privateKeyP.getChild(1).value)
e = bytesToNumber(privateKeyP.getChild(2).value)
d = bytesToNumber(privateKeyP.getChild(3).value)
p = bytesToNumber(privateKeyP.getChild(4).value)
q = bytesToNumber(privateKeyP.getChild(5).value)
dP = bytesToNumber(privateKeyP.getChild(6).value)
dQ = bytesToNumber(privateKeyP.getChild(7).value)
qInv = bytesToNumber(privateKeyP.getChild(8).value)
return Python_RSAKey(n, e, d, p, q, dP, dQ, qInv)

Loading

0 comments on commit c47c9df

Please sign in to comment.