Skip to content

Commit 9d893d6

Browse files
committed
Add support for PKCS#8 private keys
Closes #113
1 parent d2f787e commit 9d893d6

File tree

3 files changed

+133
-41
lines changed

3 files changed

+133
-41
lines changed

src/ecdsa/keys.py

Lines changed: 107 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@
7878
from .ecdsa import RSZeroError
7979
from .util import string_to_number, number_to_string, randrange
8080
from .util import sigencode_string, sigdecode_string
81-
from .util import oid_ecPublicKey, encoded_oid_ecPublicKey, MalformedSignature
81+
from .util import (
82+
oid_ecPublicKey,
83+
encoded_oid_ecPublicKey,
84+
oid_ecDH,
85+
oid_ecMQV,
86+
MalformedSignature,
87+
)
8288
from ._compat import normalise_bytes
8389

8490

@@ -841,16 +847,13 @@ def from_pem(cls, string, hashfunc=sha1):
841847
"""
842848
Initialise from key stored in :term:`PEM` format.
843849
844-
Note, the only PEM format supported is the un-encrypted RFC5915
845-
(the sslay format) supported by OpenSSL, the more common PKCS#8 format
846-
is NOT supported (see:
847-
https://github.com/warner/python-ecdsa/issues/113 )
848-
849-
``openssl ec -in pkcs8.pem -out sslay.pem`` can be used to
850-
convert PKCS#8 file to this legacy format.
850+
The PEM formats supported are the un-encrypted RFC5915
851+
(the sslay format) supported by OpenSSL, and the more common RFC5958
852+
(the PKCS #8 format).
851853
852854
The legacy format files have the header with the string
853855
``BEGIN EC PRIVATE KEY``.
856+
PKCS#8 files have the header ``BEGIN PRIVATE KEY``.
854857
Encrypted files (ones that include the string
855858
``Proc-Type: 4,ENCRYPTED``
856859
right after the PEM header) are not supported.
@@ -870,30 +873,36 @@ def from_pem(cls, string, hashfunc=sha1):
870873
:return: Initialised SigningKey object
871874
:rtype: SigningKey
872875
"""
873-
# the privkey pem may have multiple sections, commonly it also has
874-
# "EC PARAMETERS", we need just "EC PRIVATE KEY".
875876
if not PY2 and isinstance(string, str):
876877
string = string.encode()
877-
privkey_pem = string[
878-
string.index(b("-----BEGIN EC PRIVATE KEY-----")) :
879-
]
880-
return cls.from_der(der.unpem(privkey_pem), hashfunc)
878+
879+
# the privkey pem may have multiple sections, commonly it also has
880+
# "EC PARAMETERS", we need just "EC PRIVATE KEY".
881+
ec_private_key_index = string.find(b"-----BEGIN EC PRIVATE KEY-----")
882+
if ec_private_key_index != -1:
883+
return cls.from_der(
884+
der.unpem(string[ec_private_key_index:]), hashfunc, pkcs8=False
885+
)
886+
887+
private_key_index = string.find(b"-----BEGIN PRIVATE KEY-----")
888+
if private_key_index != -1:
889+
return cls.from_der(
890+
der.unpem(string[private_key_index:]), hashfunc, pkcs8=True
891+
)
892+
893+
raise ValueError("No EC PRIVATE KEY or PRIVATE KEY section in PEM")
881894

882895
@classmethod
883-
def from_der(cls, string, hashfunc=sha1):
896+
def from_der(cls, string, hashfunc=sha1, pkcs8=False):
884897
"""
885898
Initialise from key stored in :term:`DER` format.
886899
887-
Note, the only DER format supported is the RFC5915
888-
(the sslay format) supported by OpenSSL, the more common PKCS#8 format
889-
is NOT supported (see:
890-
https://github.com/warner/python-ecdsa/issues/113 )
900+
The DER formats supported are the un-encrypted RFC5915
901+
(the sslay format) supported by OpenSSL, and the more common RFC5958
902+
(the PKCS #8 format).
891903
892-
``openssl ec -in pkcs8.pem -outform der -out sslay.der`` can be
893-
used to convert PKCS#8 file to this legacy format.
894-
895-
The encoding of the ASN.1 object in those files follows following
896-
syntax specified in RFC5915::
904+
Both formats contain an ASN.1 object following the syntax specified
905+
in RFC5915::
897906
898907
ECPrivateKey ::= SEQUENCE {
899908
version INTEGER { ecPrivkeyVer1(1) }} (ecPrivkeyVer1),
@@ -904,16 +913,31 @@ def from_der(cls, string, hashfunc=sha1):
904913
905914
The only format supported for the `parameters` field is the named
906915
curve method. Explicit encoding of curve parameters is not supported.
907-
908-
While `parameters` field is defined as optional, this implementation
909-
requires its presence for correct parsing of the keys.
916+
In the legacy sslay format, this implementation requires the optional
917+
`parameters` field to get the curve name.
918+
919+
The PKCS #8 format includes this object as the `privateKey` field
920+
within a larger structure:
921+
922+
OneAsymmetricKey ::= SEQUENCE {
923+
version Version,
924+
privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
925+
privateKey PrivateKey,
926+
attributes [0] Attributes OPTIONAL,
927+
...,
928+
[[2: publicKey [1] PublicKey OPTIONAL ]],
929+
...
930+
}
910931
911932
`publicKey` field is ignored completely (errors, if any, in it will
912933
be undetected).
913934
914935
:param string: binary string with DER-encoded private ECDSA key
915936
:type string: bytes like object
916937
938+
:param pkcs8: whether to expect the data in PKCS #8 format
939+
:type pkcs8: boolean
940+
917941
:raises MalformedPointError: if the length of encoding doesn't match
918942
the provided curve or the encoded values is too large
919943
:raises RuntimeError: if the generation of public key from private
@@ -923,8 +947,48 @@ def from_der(cls, string, hashfunc=sha1):
923947
:return: Initialised SigningKey object
924948
:rtype: SigningKey
925949
"""
926-
string = normalise_bytes(string)
927-
s, empty = der.remove_sequence(string)
950+
s = normalise_bytes(string)
951+
curve = None
952+
953+
# PKCS #8 has the algorithm identifier, including the curve name,
954+
# before the actual key. Then it contains the key data within an
955+
# octet string.
956+
if pkcs8:
957+
s, empty = der.remove_sequence(s)
958+
if empty != b(""):
959+
raise der.UnexpectedDER(
960+
"trailing junk after DER privkey: %s"
961+
% binascii.hexlify(empty)
962+
)
963+
964+
version, s = der.remove_integer(s)
965+
if version != 0 and version != 1:
966+
raise der.UnexpectedDER(
967+
"expected '0' or '1' at start of DER privkey, got %d"
968+
% version
969+
)
970+
971+
algorithm_identifier, s = der.remove_sequence(s)
972+
algorithm_oid, algorithm_identifier = der.remove_object(
973+
algorithm_identifier
974+
)
975+
curve_oid, empty = der.remove_object(algorithm_identifier)
976+
curve = find_curve(curve_oid)
977+
978+
if algorithm_oid not in (oid_ecPublicKey, oid_ecDH, oid_ecMQV):
979+
raise der.UnexpectedDER(
980+
"unexpected algorithm identifier '%s'" % algorithm_oid
981+
)
982+
if empty != b"":
983+
raise der.UnexpectedDER(
984+
"unexpected data after algorithm identifier: %s"
985+
% binascii.hexlify(empty)
986+
)
987+
988+
# We don't care about the optional fields after the key data.
989+
s, _ = der.remove_octet_string(s)
990+
991+
s, empty = der.remove_sequence(s)
928992
if empty != b(""):
929993
raise der.UnexpectedDER(
930994
"trailing junk after DER privkey: %s" % binascii.hexlify(empty)
@@ -935,18 +999,20 @@ def from_der(cls, string, hashfunc=sha1):
935999
"expected '1' at start of DER privkey, got %d" % one
9361000
)
9371001
privkey_str, s = der.remove_octet_string(s)
938-
tag, curve_oid_str, s = der.remove_constructed(s)
939-
if tag != 0:
940-
raise der.UnexpectedDER(
941-
"expected tag 0 in DER privkey, got %d" % tag
942-
)
943-
curve_oid, empty = der.remove_object(curve_oid_str)
944-
if empty != b(""):
945-
raise der.UnexpectedDER(
946-
"trailing junk after DER privkey "
947-
"curve_oid: %s" % binascii.hexlify(empty)
948-
)
949-
curve = find_curve(curve_oid)
1002+
1003+
if not curve:
1004+
tag, curve_oid_str, s = der.remove_constructed(s)
1005+
if tag != 0:
1006+
raise der.UnexpectedDER(
1007+
"expected tag 0 in DER privkey, got %d" % tag
1008+
)
1009+
curve_oid, empty = der.remove_object(curve_oid_str)
1010+
if empty != b(""):
1011+
raise der.UnexpectedDER(
1012+
"trailing junk after DER privkey "
1013+
"curve_oid: %s" % binascii.hexlify(empty)
1014+
)
1015+
curve = find_curve(curve_oid)
9501016

9511017
# we don't actually care about the following fields
9521018
#

src/ecdsa/test_keys.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,15 @@ def setUpClass(cls):
219219
)
220220
cls.sk1 = SigningKey.from_pem(prv_key_str)
221221

222+
prv_key_str = (
223+
"-----BEGIN PRIVATE KEY-----\n"
224+
"MG8CAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQEEVTBTAgEBBBheyEIL1u+SUqlC6YkE\n"
225+
"PKKfVh+lJXcOscWhNAMyAAS4gXfQhO8X9eRWOUCAKDYPn1m0pNcmTmLaBlHc5Ho1\n"
226+
"pMW0XPUVk0I6i1V7nCCZ82w=\n"
227+
"-----END PRIVATE KEY-----\n"
228+
)
229+
cls.sk1_pkcs8 = SigningKey.from_pem(prv_key_str)
230+
222231
prv_key_str = (
223232
"-----BEGIN EC PRIVATE KEY-----\n"
224233
"MHcCAQEEIKlL2EAm5NPPZuXwxRf4nXMk0A80y6UUbiQ17be/qFhRoAoGCCqGSM49\n"
@@ -233,6 +242,7 @@ def test_equality_on_signing_keys(self):
233242
self.sk1.privkey.secret_multiplier, self.sk1.curve
234243
)
235244
self.assertEqual(self.sk1, sk)
245+
self.assertEqual(self.sk1_pkcs8, sk)
236246

237247
def test_inequality_on_signing_keys(self):
238248
self.assertNotEqual(self.sk1, self.sk2)

src/ecdsa/util.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@
1717
oid_ecPublicKey = (1, 2, 840, 10045, 2, 1)
1818
encoded_oid_ecPublicKey = der.encode_oid(*oid_ecPublicKey)
1919

20+
# RFC5480:
21+
# The ECDH algorithm uses the following object identifier:
22+
# id-ecDH OBJECT IDENTIFIER ::= {
23+
# iso(1) identified-organization(3) certicom(132) schemes(1)
24+
# ecdh(12) }
25+
26+
oid_ecDH = (1, 3, 132, 1, 12)
27+
28+
# RFC5480:
29+
# The ECMQV algorithm uses the following object identifier:
30+
# id-ecMQV OBJECT IDENTIFIER ::= {
31+
# iso(1) identified-organization(3) certicom(132) schemes(1)
32+
# ecmqv(13) }
33+
34+
oid_ecMQV = (1, 3, 132, 1, 13)
35+
2036
if sys.version_info >= (3,):
2137

2238
def entropy_to_bits(ent_256):

0 commit comments

Comments
 (0)