Skip to content

Commit a776084

Browse files
authored
Merge pull request #195 from oyamauchi/pkcs8
Add support for PKCS#8 private keys
2 parents d2f787e + 2cc9246 commit a776084

File tree

5 files changed

+258
-51
lines changed

5 files changed

+258
-51
lines changed

src/ecdsa/der.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ def encode_number(n):
125125
return b("").join([int2byte(d) for d in b128_digits])
126126

127127

128+
def is_sequence(string):
129+
return string and string[:1] == b"\x30"
130+
131+
128132
def remove_constructed(string):
129133
s0 = str_idx_as_int(string, 0)
130134
if (s0 & 0xE0) != 0xA0:

src/ecdsa/keys.py

Lines changed: 137 additions & 50 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 ssleay format) supported by OpenSSL, and the more common
852+
un-encrypted RFC5958 (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,29 @@ 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". PKCS#8 should not
881+
# have the "EC PARAMETERS" section; it's just "PRIVATE KEY".
882+
private_key_index = string.find(b"-----BEGIN EC PRIVATE KEY-----")
883+
if private_key_index == -1:
884+
private_key_index = string.index(b"-----BEGIN PRIVATE KEY-----")
885+
886+
return cls.from_der(der.unpem(string[private_key_index:]), hashfunc)
881887

882888
@classmethod
883889
def from_der(cls, string, hashfunc=sha1):
884890
"""
885891
Initialise from key stored in :term:`DER` format.
886892
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 )
891-
892-
``openssl ec -in pkcs8.pem -outform der -out sslay.der`` can be
893-
used to convert PKCS#8 file to this legacy format.
893+
The DER formats supported are the un-encrypted RFC5915
894+
(the ssleay format) supported by OpenSSL, and the more common
895+
un-encrypted RFC5958 (the PKCS #8 format).
894896
895-
The encoding of the ASN.1 object in those files follows following
896-
syntax specified in RFC5915::
897+
Both formats contain an ASN.1 object following the syntax specified
898+
in RFC5915::
897899
898900
ECPrivateKey ::= SEQUENCE {
899901
version INTEGER { ecPrivkeyVer1(1) }} (ecPrivkeyVer1),
@@ -902,14 +904,30 @@ def from_der(cls, string, hashfunc=sha1):
902904
publicKey [1] BIT STRING OPTIONAL
903905
}
904906
907+
`publicKey` field is ignored completely (errors, if any, in it will
908+
be undetected).
909+
905910
The only format supported for the `parameters` field is the named
906911
curve method. Explicit encoding of curve parameters is not supported.
912+
In the legacy ssleay format, this implementation requires the optional
913+
`parameters` field to get the curve name. In PKCS #8 format, the curve
914+
is part of the PrivateKeyAlgorithmIdentifier.
915+
916+
The PKCS #8 format includes an ECPrivateKey object as the `privateKey`
917+
field within a larger structure:
918+
919+
OneAsymmetricKey ::= SEQUENCE {
920+
version Version,
921+
privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
922+
privateKey PrivateKey,
923+
attributes [0] Attributes OPTIONAL,
924+
...,
925+
[[2: publicKey [1] PublicKey OPTIONAL ]],
926+
...
927+
}
907928
908-
While `parameters` field is defined as optional, this implementation
909-
requires its presence for correct parsing of the keys.
910-
911-
`publicKey` field is ignored completely (errors, if any, in it will
912-
be undetected).
929+
The `attributes` and `publicKey` fields are completely ignored; errors
930+
in them will not be detected.
913931
914932
:param string: binary string with DER-encoded private ECDSA key
915933
:type string: bytes like object
@@ -923,30 +941,80 @@ def from_der(cls, string, hashfunc=sha1):
923941
:return: Initialised SigningKey object
924942
:rtype: SigningKey
925943
"""
926-
string = normalise_bytes(string)
927-
s, empty = der.remove_sequence(string)
944+
s = normalise_bytes(string)
945+
curve = None
946+
947+
s, empty = der.remove_sequence(s)
928948
if empty != b(""):
929949
raise der.UnexpectedDER(
930950
"trailing junk after DER privkey: %s" % binascii.hexlify(empty)
931951
)
932-
one, s = der.remove_integer(s)
933-
if one != 1:
952+
953+
version, s = der.remove_integer(s)
954+
955+
# At this point, PKCS #8 has a sequence containing the algorithm
956+
# identifier and the curve identifier. The ssleay format instead has
957+
# an octet string containing the key data, so this is how we can
958+
# distinguish the two formats.
959+
if der.is_sequence(s):
960+
if version not in (0, 1):
961+
raise der.UnexpectedDER(
962+
"expected version '0' or '1' at start of privkey, got %d"
963+
% version
964+
)
965+
966+
sequence, s = der.remove_sequence(s)
967+
algorithm_oid, algorithm_identifier = der.remove_object(sequence)
968+
curve_oid, empty = der.remove_object(algorithm_identifier)
969+
curve = find_curve(curve_oid)
970+
971+
if algorithm_oid not in (oid_ecPublicKey, oid_ecDH, oid_ecMQV):
972+
raise der.UnexpectedDER(
973+
"unexpected algorithm identifier '%s'" % (algorithm_oid,)
974+
)
975+
if empty != b"":
976+
raise der.UnexpectedDER(
977+
"unexpected data after algorithm identifier: %s"
978+
% binascii.hexlify(empty)
979+
)
980+
981+
# Up next is an octet string containing an ECPrivateKey. Ignore
982+
# the optional "attributes" and "publicKey" fields that come after.
983+
s, _ = der.remove_octet_string(s)
984+
985+
# Unpack the ECPrivateKey to get to the key data octet string,
986+
# and rejoin the ssleay parsing path.
987+
s, empty = der.remove_sequence(s)
988+
if empty != b(""):
989+
raise der.UnexpectedDER(
990+
"trailing junk after DER privkey: %s"
991+
% binascii.hexlify(empty)
992+
)
993+
994+
version, s = der.remove_integer(s)
995+
996+
# The version of the ECPrivateKey must be 1.
997+
if version != 1:
934998
raise der.UnexpectedDER(
935-
"expected '1' at start of DER privkey, got %d" % one
999+
"expected version '1' at start of DER privkey, got %d"
1000+
% version
9361001
)
1002+
9371003
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)
1004+
1005+
if not curve:
1006+
tag, curve_oid_str, s = der.remove_constructed(s)
1007+
if tag != 0:
1008+
raise der.UnexpectedDER(
1009+
"expected tag 0 in DER privkey, got %d" % tag
1010+
)
1011+
curve_oid, empty = der.remove_object(curve_oid_str)
1012+
if empty != b(""):
1013+
raise der.UnexpectedDER(
1014+
"trailing junk after DER privkey "
1015+
"curve_oid: %s" % binascii.hexlify(empty)
1016+
)
1017+
curve = find_curve(curve_oid)
9501018

9511019
# we don't actually care about the following fields
9521020
#
@@ -981,7 +1049,7 @@ def to_string(self):
9811049
s = number_to_string(secexp, self.privkey.order)
9821050
return s
9831051

984-
def to_pem(self, point_encoding="uncompressed"):
1052+
def to_pem(self, point_encoding="uncompressed", format="ssleay"):
9851053
"""
9861054
Convert the private key to the :term:`PEM` format.
9871055
@@ -990,9 +1058,11 @@ def to_pem(self, point_encoding="uncompressed"):
9901058
Only the named curve format is supported.
9911059
The public key will be included in generated string.
9921060
993-
The PEM header will specify ``BEGIN EC PRIVATE KEY``
1061+
The PEM header will specify ``BEGIN EC PRIVATE KEY`` or
1062+
``BEGIN PRIVATE KEY``, depending on the desired format.
9941063
9951064
:param str point_encoding: format to use for encoding public point
1065+
:param str format: either ``ssleay`` (default) or ``pkcs8``
9961066
9971067
:return: PEM encoded private key
9981068
:rtype: bytes
@@ -1001,9 +1071,11 @@ def to_pem(self, point_encoding="uncompressed"):
10011071
re-encoded if the system is incompatible (e.g. uses UTF-16)
10021072
"""
10031073
# TODO: "BEGIN ECPARAMETERS"
1004-
return der.topem(self.to_der(point_encoding), "EC PRIVATE KEY")
1074+
assert format in ("ssleay", "pkcs8")
1075+
header = "EC PRIVATE KEY" if format == "ssleay" else "PRIVATE KEY"
1076+
return der.topem(self.to_der(point_encoding, format), header)
10051077

1006-
def to_der(self, point_encoding="uncompressed"):
1078+
def to_der(self, point_encoding="uncompressed", format="ssleay"):
10071079
"""
10081080
Convert the private key to the :term:`DER` format.
10091081
@@ -1013,6 +1085,7 @@ def to_der(self, point_encoding="uncompressed"):
10131085
The public key will be included in the generated string.
10141086
10151087
:param str point_encoding: format to use for encoding public point
1088+
:param str format: either ``ssleay`` (default) or ``pkcs8``
10161089
10171090
:return: DER encoded private key
10181091
:rtype: bytes
@@ -1021,16 +1094,30 @@ def to_der(self, point_encoding="uncompressed"):
10211094
# cont[1],bitstring])
10221095
if point_encoding == "raw":
10231096
raise ValueError("raw encoding not allowed in DER")
1097+
assert format in ("ssleay", "pkcs8")
10241098
encoded_vk = self.get_verifying_key().to_string(point_encoding)
10251099
# the 0 in encode_bitstring specifies the number of unused bits
10261100
# in the `encoded_vk` string
1027-
return der.encode_sequence(
1101+
ec_private_key = der.encode_sequence(
10281102
der.encode_integer(1),
10291103
der.encode_octet_string(self.to_string()),
10301104
der.encode_constructed(0, self.curve.encoded_oid),
10311105
der.encode_constructed(1, der.encode_bitstring(encoded_vk, 0)),
10321106
)
10331107

1108+
if format == "ssleay":
1109+
return ec_private_key
1110+
else:
1111+
return der.encode_sequence(
1112+
# version = 1 means the public key is not present in the
1113+
# top-level structure.
1114+
der.encode_integer(1),
1115+
der.encode_sequence(
1116+
der.encode_oid(*oid_ecPublicKey), self.curve.encoded_oid
1117+
),
1118+
der.encode_octet_string(ec_private_key),
1119+
)
1120+
10341121
def get_verifying_key(self):
10351122
"""
10361123
Return the VerifyingKey associated with this private key.

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)

0 commit comments

Comments
 (0)