Skip to content

Commit

Permalink
parsing of DSA keys from X.509 certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
FrantisekKrenzelok committed Jul 30, 2020
1 parent b4f059a commit 95e9881
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 5 deletions.
1 change: 1 addition & 0 deletions tlslite/utils/__init__.py
Expand Up @@ -22,6 +22,7 @@
"python_aes",
"python_rc4",
"python_rsakey",
"python_dsakey",
"rc4",
"rijndael",
"rsakey",
Expand Down
41 changes: 41 additions & 0 deletions tlslite/utils/dsakey.py
@@ -0,0 +1,41 @@


"""Abstract class for DSA."""

from .cryptomath import secureHash


class DSAKey(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):
raise NotImplementedError()

def __len__(self):
raise NotImplementedError()

def hasPrivateKey(self):
raise NotImplementedError()

def sign(self, data, hash_alg):
raise NotImplementedError()

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

def verify(self, signature, hash_bytes):
raise NotImplementedError()

@staticmethod
def generate(bits):
raise NotImplementedError()
20 changes: 20 additions & 0 deletions tlslite/utils/keyfactory.py
Expand Up @@ -8,6 +8,7 @@
from .rsakey import RSAKey
from .python_rsakey import Python_RSAKey
from .python_ecdsakey import Python_ECDSAKey
from .python_dsakey import Python_DSAKey
from tlslite.utils import cryptomath

if cryptomath.m2cryptoLoaded:
Expand Down Expand Up @@ -233,3 +234,22 @@ def _create_public_ecdsa_key(point_x, point_y, curve_name,
if impl == "python":
return Python_ECDSAKey(point_x, point_y, curve_name)
raise ValueError("No acceptable implementation")

def _create_public_dsa_key(p, q, g, A,
implementations=("python",)):
"""
Convert public key parameters into concrete implementation of verifier.
The public key in DSA consists of four integers.
:type {p, q, g, A}: int
:param {p, q, g, A}: integers definig public key
:type implementations: iterable of str
:param implementations: list of implementations that can be used as the
concrete implementation of the verifying key (only 'python' is
supported currently)
"""
for impl in implementations:
if impl == "python":
return Python_DSAKey(p=p, q=q, g=g, A=A)
raise ValueError("No acceptable implementation")
61 changes: 61 additions & 0 deletions tlslite/utils/python_dsakey.py
@@ -0,0 +1,61 @@
from .dsakey import DSAKey
from .cryptomath import *

class Python_DSAKey(DSAKey):

def __init__(self, p = 0, q = 0, g = 0, A = 0, a = 0):
# TODO: add asserts
self.p = p
self.q = q
self.g = g
self.private_key = a
self.public_key = A
self.key_type = "dsa"


def __len__(self):
return numBits(self.public_key)

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

@staticmethod
def generate(L, N):
# TODO: add asserts
key = Python_DSAKey()
(q,p) = Python_DSAKey.generate_qp(L, N)

x = getRandomNumber(1, (p-1))
g = powMod(x,((p-1)/q),p)
a = getRandomNumber(1,q-1)
A = powMod(g, a, p)
key.q = q
key.p = p
key.g = g
key.private_key = a
key.public_key = A
return key

@staticmethod
def generate_qp(L, N):
# TODO: optimize
q = getRandomPrime(N)
while True:
p = getRandomPrime(L)
if ((p-1) % q):
break
return (q, p)


def hashAndSign(self, data, hAlg = "sha1"):
# TODO: add assert for hash size < (q-1) constrain
hashBytes = secureHash(bytearray(data), hAlg)
k = getRandomNumber(1,(self.q-1))
r = powMod(self.g, k, self.p) % self.q
s = ((k ** -1) * (bytesToNumber(hashBytes) + self.private_key * r)) % self.q
print(r,s)
return (r, s)

def verify(self, signature, hash_bytes):
raise NotImplementedError

36 changes: 33 additions & 3 deletions tlslite/utils/python_key.py
Expand Up @@ -2,6 +2,7 @@

from .python_rsakey import Python_RSAKey
from .python_ecdsakey import Python_ECDSAKey
from .python_dsakey import Python_DSAKey
from .pem import dePem, pemSniff
from .asn1parser import ASN1Parser
from .cryptomath import bytesToNumber
Expand All @@ -25,7 +26,10 @@ def parsePEM(s, passwordCallback=None):
return Python_Key._parse_pkcs8(bytes)
elif pemSniff(s, "RSA PRIVATE KEY"):
bytes = dePem(s, "RSA PRIVATE KEY")
return Python_Key._parse_ssleay(bytes)
return Python_Key._parse_ssleay(bytes, "rsa")
elif pemSniff(s, "DSA PRIVATE KEY"):
bytes = dePem(s, "DSA PRIVATE KEY")
return Python_Key._parse_dsa_ssleay(bytes)
elif pemSniff(s, "EC PRIVATE KEY"):
bytes = dePem(s, "EC PRIVATE KEY")
return Python_Key._parse_ecc_ssleay(bytes)
Expand All @@ -51,6 +55,8 @@ def _parse_pkcs8(bytes):
key_type = "rsa"
elif list(oid.value) == [42, 134, 72, 134, 247, 13, 1, 1, 10]:
key_type = "rsa-pss"
elif list(oid.value) == [42, 134, 72, 206, 56, 4, 1]:
key_type = "dsa"
elif list(oid.value) == [42, 134, 72, 206, 61, 2, 1]:
key_type = "ecdsa"
else:
Expand All @@ -64,6 +70,8 @@ def _parse_pkcs8(bytes):
parameters = alg_ident.getChild(1)
if parameters.value != bytearray(0):
raise SyntaxError("RSA parameters are not NULL")
if key_type == "dsa":
parameters = alg_ident.getChild(1)
elif key_type == "ecdsa":
if seq_len != 2:
raise SyntaxError("Invalid encoding of algorithm identifier")
Expand Down Expand Up @@ -91,20 +99,32 @@ def _parse_pkcs8(bytes):
if key_type == "ecdsa":
return Python_Key._parse_ecdsa_private_key(private_key_parser,
curve)
elif key_type == "dsa":
return Python_Key._parse_dsa_private_key(private_key_parser)
else:
return Python_Key._parse_asn1_private_key(private_key_parser,
key_type)

@staticmethod
def _parse_ssleay(data):
def _parse_ssleay(data, key_type="rsa"):
"""
Parse binary structure of the old SSLeay file format used by OpenSSL.
For RSA keys.
"""
private_key_parser = ASN1Parser(data)
# "rsa" type as old format doesn't support rsa-pss parameters
return Python_Key._parse_asn1_private_key(private_key_parser, "rsa")
return Python_Key._parse_asn1_private_key(private_key_parser, key_type)

@staticmethod
def _parse_dsa_ssleay(data):
"""
Parse binary structure of the old SSLeay file format used by OpenSSL.
For DSA keys.
"""
private_key_parser = ASN1Parser(data)
return Python_Key._parse_dsa_private_key(private_key_parser)

@staticmethod
def _parse_ecc_ssleay(data):
Expand Down Expand Up @@ -167,3 +187,13 @@ def _parse_asn1_private_key(private_key_parser, key_type):
qInv = bytesToNumber(private_key_parser.getChild(8).value)
return Python_RSAKey(n, e, d, p, q, dP, dQ, qInv, key_type)


@staticmethod
def _parse_dsa_private_key(private_key_parser):
a = bytesToNumber(private_key_parser.getChild(1).value)
A = bytesToNumber(private_key_parser.getChild(2).value)
p = bytesToNumber(private_key_parser.getChild(3).value)
g = bytesToNumber(private_key_parser.getChild(4).value)
q = bytesToNumber(private_key_parser.getChild(5).value)
return Python_DSAKey(p, q, g, A, a)

45 changes: 43 additions & 2 deletions tlslite/x509.py
@@ -1,4 +1,4 @@
# Authors:
# Authors:
# Trevor Perrin
# Google - parsing subject field
#
Expand Down Expand Up @@ -143,12 +143,14 @@ def parseBinary(self, bytes):
self.certAlg = "rsa"
elif list(alg_oid) == [42, 134, 72, 134, 247, 13, 1, 1, 10]:
self.certAlg = "rsa-pss"
elif list(alg_oid) == [42, 134, 72, 206, 56, 4, 1]:
self.certAlg = "dsa"
elif list(alg_oid) == [42, 134, 72, 206, 61, 2, 1]:
self.certAlg = "ecdsa"
else:
raise SyntaxError("Unrecognized AlgorithmIdentifier")

# for RSA the parameters of AlgorithmIdentifier should be a NULL
# for RSA the parameters of AlgorithmIdentifier shuld be a NULL
if self.certAlg == "rsa":
if alg_identifier_len != 2:
raise SyntaxError("Missing parameters in AlgorithmIdentifier")
Expand All @@ -160,6 +162,10 @@ def parseBinary(self, bytes):
self._ecdsa_pubkey_parsing(
tbs_certificate.getChildBytes(subject_public_key_info_index))
return
elif self.certAlg == "dsa":
self._ecdsa_pubkey_parsing(
tbs_certificate.getChildBytes(subject_public_key_info_index))
return
else: # rsa-pss
pass # ignore parameters, if any - don't apply key restrictions

Expand Down Expand Up @@ -218,6 +224,41 @@ def _ecdsa_pubkey_parsing(self, subject_public_key_info):
curve_name = public_key.curve.name
self.publicKey = _create_public_ecdsa_key(x, y, curve_name)

def _dsa_pubkey_parsing(self, subject_public_key_info):

# Get the subjectPublicKey
subject_public_key = subject_public_key_info.getChild(1)
self.subject_public_key = subject_public_key_info.getChildBytes(1)
self.subject_public_key = ASN1Parser(self.subject_public_key).value[1:]

# Adjust for BIT STRING encapsulation
if subject_public_key.value[0]:
raise SyntaxError()
subject_public_key = ASN1Parser(subject_public_key.value[1:])

# Get the {A, p, q}
A = subject_public_key.getChild(0)
p = subject_public_key.getChild(1)
g = subject_public_key.getChild(2)

# q member is in the third element of subject_public_key_info
subject_public_key = subject_public_key_info.getChild(2)
# Adjust for BIT STRING encapsulation
if subject_public_key.value[0]:
raise SyntaxError()
subject_public_key = ASN1Parser(subject_public_key.value[1:])
# Get the {q}
q = subject_public_key.getChild(0)

# Decode them into numbers
A = bytesToNumber(A.value)
p = bytesToNumber(p.value)
q = bytesToNumber(q.value)
g = bytesToNumber(g.value)

# Create a public key instance
self.publicKey = _createPublicDSAKey(p, q, g, A)

def getFingerprint(self):
"""
Get the hex-encoded fingerprint of this certificate.
Expand Down
61 changes: 61 additions & 0 deletions unit_tests/test_tlslite_utils_python_dsakey.py
@@ -0,0 +1,61 @@

try:
import unittest2 as unittest
except ImportError:
import unittest

try:
import mock
from mock import call
except ImportError:
import unittest.mock as mock
from unittest.mock import call

from tlslite.utils.python_key import Python_Key
from tlslite.utils.python_dsakey import Python_DSAKey

class TestDSAKey(unittest.TestCase):
@classmethod
def setUpClass(cls):
# TODO: probably change the size for faster testing
cls.key = Python_DSAKey(p=283, q = 47, g = 60, A = 158, a = 24)

# sha1 signature of message 'some message to sign'
cls.sha1_sig = \
bytearray(b'0E\x02!\x00\xf7Q\x97.\xcfv\x03\xf0\xff,^\xb9'
b'\nZ\xbd\x0e\xaaf\xf2]\xe0\xb0\x91\xa6cY\xa9\xff'
b'{@\x18\xc8\x02 <\x80\x1a\xfa\x14\xd2\\\x02\xfe'
b'\x1a\xb7\x07X\xba\xd8`\xd4\x1d\xa9\x9cm\xc7\xcd'
b'\x11\xbb\x1b\xd1A\xcdO\xa2?')

def test_parse_from_pem(self):
# TODO: change the bit size from 2048 to smaller?
key = (
"-----BEGIN DSA PRIVATE KEY-----\n"
"MIIBvQIBAAKBgQD9f1OBHXUSKVLfSpwu7OTn9hG3UjzvRADDHj+AtlEmaUVdQCJR\n"
"+1k9jVj6v8X1ujD2y5tVbNeBO4AdNG/yZmC3a5lQpaSfn+gEexAiwk+7qdf+t8Yb\n"
"+DtX58aophUPBPuD9tPFHsMCNVQTWhaRMvZ1864rYdcq7/IiAxmd0UgBxwIVAJdg\n"
"UI8VIwvMspK5gqLrhAvwWBz1AoGBAPfhoIXWmz3ey7yrXDa4V7l5lK+7+jrqgvlX\n"
"TAs9B4JnUVlXjrrUWU/mcQcQgYC0SRZxI+hMKBYTt88JMozIpuE8FnqLVHyNKOCj\n"
"rh4rs6Z1kW6jfwv6ITVi8ftiegEkO8yk8b6oUZCJqIPf4VrlnwaSi2ZegHtVJWQB\n"
"TDv+z0kqAoGBAISYj2hobCdcblJ3nDyVJ99CLlWJIkm3kZBpBsA0RQ2q2sKRWM7L\n"
"HgLUgKgX02slDEp2LkJg0H1ccuzUbphcFRP3iqY88mK74R9evDM2tani3NCA/ZLK\n"
"GI58gJqqNmdoDd6nKIrz0NarBinR0j3UTfXncRdkCwVLFa88FhmXGR19AhUAjuz0\n"
"M8NFitlfzW/FpFUWV7hYfRs=\n"
"-----END DSA PRIVATE KEY-----")

parsed_key = Python_Key.parsePEM(key)
self.assertIsInstance(parsed_key, Python_DSAKey)
self.assertTrue(parsed_key.hasPrivateKey())

def test_generate(self):
key = Python_DSAKey.generate(1024, 160)
self.assertIsInstance(key, Python_DSAKey)
self.assertTrue(key.hasPrivateKey())
# TODO: test length

def test_sign_default(self):
msg = b"some message to sign"

sig = self.key.hashAndSign(msg)

0 comments on commit 95e9881

Please sign in to comment.