aboutsummaryrefslogtreecommitdiff
path: root/frozen_deps/Cryptodome/PublicKey/ECC.py
diff options
context:
space:
mode:
Diffstat (limited to 'frozen_deps/Cryptodome/PublicKey/ECC.py')
-rw-r--r--frozen_deps/Cryptodome/PublicKey/ECC.py1036
1 files changed, 823 insertions, 213 deletions
diff --git a/frozen_deps/Cryptodome/PublicKey/ECC.py b/frozen_deps/Cryptodome/PublicKey/ECC.py
index e83664b..84a4e07 100644
--- a/frozen_deps/Cryptodome/PublicKey/ECC.py
+++ b/frozen_deps/Cryptodome/PublicKey/ECC.py
@@ -31,7 +31,6 @@
from __future__ import print_function
import re
-import sys
import struct
import binascii
from collections import namedtuple
@@ -43,13 +42,15 @@ from Cryptodome.Math.Numbers import Integer
from Cryptodome.Util.asn1 import (DerObjectId, DerOctetString, DerSequence,
DerBitString)
+from Cryptodome.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
+ SmartPointer, c_size_t, c_uint8_ptr,
+ c_ulonglong, null_pointer)
+
from Cryptodome.PublicKey import (_expand_subject_public_key_info,
_create_subject_public_key_info,
_extract_subject_public_key_info)
-from Cryptodome.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
- SmartPointer, c_size_t, c_uint8_ptr,
- c_ulonglong)
+from Cryptodome.Hash import SHA512, SHAKE256
from Cryptodome.Random import get_random_bytes
from Cryptodome.Random.random import getrandbits
@@ -70,7 +71,7 @@ int ec_ws_new_point(EcPoint **pecp,
const uint8_t *y,
size_t len,
const EcContext *ec_ctx);
-void ec_free_point(EcPoint *ecp);
+void ec_ws_free_point(EcPoint *ecp);
int ec_ws_get_xy(uint8_t *x,
uint8_t *y,
size_t len,
@@ -82,17 +83,173 @@ int ec_ws_scalar(EcPoint *ecp,
size_t len,
uint64_t seed);
int ec_ws_clone(EcPoint **pecp2, const EcPoint *ecp);
-int ec_ws_copy(EcPoint *ecp1, const EcPoint *ecp2);
int ec_ws_cmp(const EcPoint *ecp1, const EcPoint *ecp2);
int ec_ws_neg(EcPoint *p);
-int ec_ws_normalize(EcPoint *ecp);
-int ec_ws_is_pai(EcPoint *ecp);
""")
-_Curve = namedtuple("_Curve", "p b order Gx Gy G modulus_bits oid context desc openssh")
+_ed25519_lib = load_pycryptodome_raw_lib("Cryptodome.PublicKey._ed25519", """
+typedef void Point;
+int ed25519_new_point(Point **out,
+ const uint8_t x[32],
+ const uint8_t y[32],
+ size_t modsize,
+ const void *context);
+int ed25519_clone(Point **P, const Point *Q);
+void ed25519_free_point(Point *p);
+int ed25519_cmp(const Point *p1, const Point *p2);
+int ed25519_neg(Point *p);
+int ed25519_get_xy(uint8_t *xb, uint8_t *yb, size_t modsize, Point *p);
+int ed25519_double(Point *p);
+int ed25519_add(Point *P1, const Point *P2);
+int ed25519_scalar(Point *P, uint8_t *scalar, size_t scalar_len, uint64_t seed);
+""")
+
+_ed448_lib = load_pycryptodome_raw_lib("Cryptodome.PublicKey._ed448", """
+typedef void EcContext;
+typedef void PointEd448;
+int ed448_new_context(EcContext **pec_ctx);
+void ed448_context(EcContext *ec_ctx);
+void ed448_free_context(EcContext *ec_ctx);
+int ed448_new_point(PointEd448 **out,
+ const uint8_t x[56],
+ const uint8_t y[56],
+ size_t len,
+ const EcContext *context);
+int ed448_clone(PointEd448 **P, const PointEd448 *Q);
+void ed448_free_point(PointEd448 *p);
+int ed448_cmp(const PointEd448 *p1, const PointEd448 *p2);
+int ed448_neg(PointEd448 *p);
+int ed448_get_xy(uint8_t *xb, uint8_t *yb, size_t len, const PointEd448 *p);
+int ed448_double(PointEd448 *p);
+int ed448_add(PointEd448 *P1, const PointEd448 *P2);
+int ed448_scalar(PointEd448 *P, const uint8_t *scalar, size_t scalar_len, uint64_t seed);
+""")
+
+
+def lib_func(ecc_obj, func_name):
+ if ecc_obj._curve.desc == "Ed25519":
+ result = getattr(_ed25519_lib, "ed25519_" + func_name)
+ elif ecc_obj._curve.desc == "Ed448":
+ result = getattr(_ed448_lib, "ed448_" + func_name)
+ else:
+ result = getattr(_ec_lib, "ec_ws_" + func_name)
+ return result
+
+#
+# _curves is a database of curve parameters. Items are indexed by their
+# human-friendly name, suchas "P-256". Each item has the following fields:
+# - p: the prime number that defines the finite field for all modulo operations
+# - b: the constant in the Short Weierstrass curve equation
+# - order: the number of elements in the group with the generator below
+# - Gx the affine coordinate X of the generator point
+# - Gy the affine coordinate Y of the generator point
+# - G the generator, as an EccPoint object
+# - modulus_bits the minimum number of bits for encoding the modulus p
+# - oid an ASCII string with the registered ASN.1 Object ID
+# - context a raw pointer to memory holding a context for all curve operations (can be NULL)
+# - desc an ASCII string describing the curve
+# - openssh the ASCII string used in OpenSSH id files for public keys on this curve
+# - name the ASCII string which is also a valid key in _curves
+
+
+_Curve = namedtuple("_Curve", "p b order Gx Gy G modulus_bits oid context desc openssh name")
_curves = {}
+p192_names = ["p192", "NIST P-192", "P-192", "prime192v1", "secp192r1",
+ "nistp192"]
+
+
+def init_p192():
+ p = 0xfffffffffffffffffffffffffffffffeffffffffffffffff
+ b = 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1
+ order = 0xffffffffffffffffffffffff99def836146bc9b1b4d22831
+ Gx = 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012
+ Gy = 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811
+
+ p192_modulus = long_to_bytes(p, 24)
+ p192_b = long_to_bytes(b, 24)
+ p192_order = long_to_bytes(order, 24)
+
+ ec_p192_context = VoidPointer()
+ result = _ec_lib.ec_ws_new_context(ec_p192_context.address_of(),
+ c_uint8_ptr(p192_modulus),
+ c_uint8_ptr(p192_b),
+ c_uint8_ptr(p192_order),
+ c_size_t(len(p192_modulus)),
+ c_ulonglong(getrandbits(64))
+ )
+ if result:
+ raise ImportError("Error %d initializing P-192 context" % result)
+
+ context = SmartPointer(ec_p192_context.get(), _ec_lib.ec_free_context)
+ p192 = _Curve(Integer(p),
+ Integer(b),
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 192,
+ "1.2.840.10045.3.1.1", # ANSI X9.62 / SEC2
+ context,
+ "NIST P-192",
+ "ecdsa-sha2-nistp192",
+ "p192")
+ global p192_names
+ _curves.update(dict.fromkeys(p192_names, p192))
+
+
+init_p192()
+del init_p192
+
+
+p224_names = ["p224", "NIST P-224", "P-224", "prime224v1", "secp224r1",
+ "nistp224"]
+
+
+def init_p224():
+ p = 0xffffffffffffffffffffffffffffffff000000000000000000000001
+ b = 0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4
+ order = 0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d
+ Gx = 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21
+ Gy = 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34
+
+ p224_modulus = long_to_bytes(p, 28)
+ p224_b = long_to_bytes(b, 28)
+ p224_order = long_to_bytes(order, 28)
+
+ ec_p224_context = VoidPointer()
+ result = _ec_lib.ec_ws_new_context(ec_p224_context.address_of(),
+ c_uint8_ptr(p224_modulus),
+ c_uint8_ptr(p224_b),
+ c_uint8_ptr(p224_order),
+ c_size_t(len(p224_modulus)),
+ c_ulonglong(getrandbits(64))
+ )
+ if result:
+ raise ImportError("Error %d initializing P-224 context" % result)
+
+ context = SmartPointer(ec_p224_context.get(), _ec_lib.ec_free_context)
+ p224 = _Curve(Integer(p),
+ Integer(b),
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 224,
+ "1.3.132.0.33", # SEC 2
+ context,
+ "NIST P-224",
+ "ecdsa-sha2-nistp224",
+ "p224")
+ global p224_names
+ _curves.update(dict.fromkeys(p224_names, p224))
+
+
+init_p224()
+del init_p224
+
+
p256_names = ["p256", "NIST P-256", "P-256", "prime256v1", "secp256r1",
"nistp256"]
@@ -127,10 +284,11 @@ def init_p256():
Integer(Gy),
None,
256,
- "1.2.840.10045.3.1.7", # ANSI X9.62
+ "1.2.840.10045.3.1.7", # ANSI X9.62 / SEC2
context,
"NIST P-256",
- "ecdsa-sha2-nistp256")
+ "ecdsa-sha2-nistp256",
+ "p256")
global p256_names
_curves.update(dict.fromkeys(p256_names, p256))
@@ -176,7 +334,8 @@ def init_p384():
"1.3.132.0.34", # SEC 2
context,
"NIST P-384",
- "ecdsa-sha2-nistp384")
+ "ecdsa-sha2-nistp384",
+ "p384")
global p384_names
_curves.update(dict.fromkeys(p384_names, p384))
@@ -222,7 +381,8 @@ def init_p521():
"1.3.132.0.35", # SEC 2
context,
"NIST P-521",
- "ecdsa-sha2-nistp521")
+ "ecdsa-sha2-nistp521",
+ "p521")
global p521_names
_curves.update(dict.fromkeys(p521_names, p521))
@@ -231,19 +391,84 @@ init_p521()
del init_p521
+ed25519_names = ["ed25519", "Ed25519"]
+
+
+def init_ed25519():
+ p = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed # 2**255 - 19
+ order = 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed
+ Gx = 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51a
+ Gy = 0x6666666666666666666666666666666666666666666666666666666666666658
+
+ ed25519 = _Curve(Integer(p),
+ None,
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 255,
+ "1.3.101.112", # RFC8410
+ None,
+ "Ed25519", # Used throughout; do not change
+ "ssh-ed25519",
+ "ed25519")
+ global ed25519_names
+ _curves.update(dict.fromkeys(ed25519_names, ed25519))
+
+
+init_ed25519()
+del init_ed25519
+
+
+ed448_names = ["ed448", "Ed448"]
+
+
+def init_ed448():
+ p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffff # 2**448 - 2**224 - 1
+ order = 0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffff7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3
+ Gx = 0x4f1970c66bed0ded221d15a622bf36da9e146570470f1767ea6de324a3d3a46412ae1af72ab66511433b80e18b00938e2626a82bc70cc05e
+ Gy = 0x693f46716eb6bc248876203756c9c7624bea73736ca3984087789c1e05a0c2d73ad3ff1ce67c39c4fdbd132c4ed7c8ad9808795bf230fa14
+
+ ed448_context = VoidPointer()
+ result = _ed448_lib.ed448_new_context(ed448_context.address_of())
+ if result:
+ raise ImportError("Error %d initializing Ed448 context" % result)
+
+ context = SmartPointer(ed448_context.get(), _ed448_lib.ed448_free_context)
+
+ ed448 = _Curve(Integer(p),
+ None,
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 448,
+ "1.3.101.113", # RFC8410
+ context,
+ "Ed448", # Used throughout; do not change
+ None,
+ "ed448")
+ global ed448_names
+ _curves.update(dict.fromkeys(ed448_names, ed448))
+
+
+init_ed448()
+del init_ed448
+
+
class UnsupportedEccFeature(ValueError):
pass
class EccPoint(object):
- """A class to abstract a point over an Elliptic Curve.
+ """A class to model a point on an Elliptic Curve.
- The class support special methods for:
+ The class supports operators for:
* Adding two points: ``R = S + T``
* In-place addition: ``S += T``
* Negating a point: ``R = -T``
- * Comparing two points: ``if S == T: ...``
+ * Comparing two points: ``if S == T: ...`` or ``if S != T: ...``
* Multiplying a point by a scalar: ``R = S*k``
* In-place multiplication by a scalar: ``T *= k``
@@ -253,7 +478,7 @@ class EccPoint(object):
:ivar y: The affine Y-coordinate of the ECC point
:vartype y: integer
- :ivar xy: The tuple with X- and Y- coordinates
+ :ivar xy: The tuple with affine X- and Y- coordinates
"""
def __init__(self, x, y, curve="p256"):
@@ -265,19 +490,26 @@ class EccPoint(object):
self._curve_name = curve
modulus_bytes = self.size_in_bytes()
- context = self._curve.context
xb = long_to_bytes(x, modulus_bytes)
yb = long_to_bytes(y, modulus_bytes)
if len(xb) != modulus_bytes or len(yb) != modulus_bytes:
raise ValueError("Incorrect coordinate length")
+ new_point = lib_func(self, "new_point")
+ free_func = lib_func(self, "free_point")
+
self._point = VoidPointer()
- result = _ec_lib.ec_ws_new_point(self._point.address_of(),
- c_uint8_ptr(xb),
- c_uint8_ptr(yb),
- c_size_t(modulus_bytes),
- context.get())
+ try:
+ context = self._curve.context.get()
+ except AttributeError:
+ context = null_pointer
+ result = new_point(self._point.address_of(),
+ c_uint8_ptr(xb),
+ c_uint8_ptr(yb),
+ c_size_t(modulus_bytes),
+ context)
+
if result:
if result == 15:
raise ValueError("The EC point does not belong to the curve")
@@ -285,26 +517,34 @@ class EccPoint(object):
# Ensure that object disposal of this Python object will (eventually)
# free the memory allocated by the raw library for the EC point
- self._point = SmartPointer(self._point.get(),
- _ec_lib.ec_free_point)
+ self._point = SmartPointer(self._point.get(), free_func)
def set(self, point):
+ clone = lib_func(self, "clone")
+ free_func = lib_func(self, "free_point")
+
self._point = VoidPointer()
- result = _ec_lib.ec_ws_clone(self._point.address_of(),
- point._point.get())
+ result = clone(self._point.address_of(),
+ point._point.get())
+
if result:
raise ValueError("Error %d while cloning an EC point" % result)
- self._point = SmartPointer(self._point.get(),
- _ec_lib.ec_free_point)
+ self._point = SmartPointer(self._point.get(), free_func)
return self
def __eq__(self, point):
- return 0 == _ec_lib.ec_ws_cmp(self._point.get(), point._point.get())
+ cmp_func = lib_func(self, "cmp")
+ return 0 == cmp_func(self._point.get(), point._point.get())
+
+ # Only needed for Python 2
+ def __ne__(self, point):
+ return not self == point
def __neg__(self):
+ neg_func = lib_func(self, "neg")
np = self.copy()
- result = _ec_lib.ec_ws_neg(np._point.get())
+ result = neg_func(np._point.get())
if result:
raise ValueError("Error %d while inverting an EC point" % result)
return np
@@ -315,13 +555,24 @@ class EccPoint(object):
np = EccPoint(x, y, self._curve_name)
return np
+ def _is_eddsa(self):
+ return self._curve.name in ("ed25519", "ed448")
+
def is_point_at_infinity(self):
- """``True`` if this is the point-at-infinity."""
- return self.xy == (0, 0)
+ """``True`` if this is the *point-at-infinity*."""
+
+ if self._is_eddsa():
+ return self.x == 0
+ else:
+ return self.xy == (0, 0)
def point_at_infinity(self):
- """Return the point-at-infinity for the curve this point is on."""
- return EccPoint(0, 0, self._curve_name)
+ """Return the *point-at-infinity* for the curve."""
+
+ if self._is_eddsa():
+ return EccPoint(0, 1, self._curve_name)
+ else:
+ return EccPoint(0, 0, self._curve_name)
@property
def x(self):
@@ -336,10 +587,11 @@ class EccPoint(object):
modulus_bytes = self.size_in_bytes()
xb = bytearray(modulus_bytes)
yb = bytearray(modulus_bytes)
- result = _ec_lib.ec_ws_get_xy(c_uint8_ptr(xb),
- c_uint8_ptr(yb),
- c_size_t(modulus_bytes),
- self._point.get())
+ get_xy = lib_func(self, "get_xy")
+ result = get_xy(c_uint8_ptr(xb),
+ c_uint8_ptr(yb),
+ c_size_t(modulus_bytes),
+ self._point.get())
if result:
raise ValueError("Error %d while encoding an EC point" % result)
@@ -356,11 +608,12 @@ class EccPoint(object):
def double(self):
"""Double this point (in-place operation).
- :Return:
- :class:`EccPoint` : this same object (to enable chaining)
+ Returns:
+ This same object (to enable chaining).
"""
- result = _ec_lib.ec_ws_double(self._point.get())
+ double_func = lib_func(self, "double")
+ result = double_func(self._point.get())
if result:
raise ValueError("Error %d while doubling an EC point" % result)
return self
@@ -368,7 +621,8 @@ class EccPoint(object):
def __iadd__(self, point):
"""Add a second point to this one"""
- result = _ec_lib.ec_ws_add(self._point.get(), point._point.get())
+ add_func = lib_func(self, "add")
+ result = add_func(self._point.get(), point._point.get())
if result:
if result == 16:
raise ValueError("EC points are not on the same curve")
@@ -385,13 +639,14 @@ class EccPoint(object):
def __imul__(self, scalar):
"""Multiply this point by a scalar"""
+ scalar_func = lib_func(self, "scalar")
if scalar < 0:
raise ValueError("Scalar multiplication is only defined for non-negative integers")
sb = long_to_bytes(scalar)
- result = _ec_lib.ec_ws_scalar(self._point.get(),
- c_uint8_ptr(sb),
- c_size_t(len(sb)),
- c_ulonglong(getrandbits(64)))
+ result = scalar_func(self._point.get(),
+ c_uint8_ptr(sb),
+ c_size_t(len(sb)),
+ c_ulonglong(getrandbits(64)))
if result:
raise ValueError("Error %d during scalar multiplication" % result)
return self
@@ -408,6 +663,16 @@ class EccPoint(object):
# Last piece of initialization
+p192_G = EccPoint(_curves['p192'].Gx, _curves['p192'].Gy, "p192")
+p192 = _curves['p192']._replace(G=p192_G)
+_curves.update(dict.fromkeys(p192_names, p192))
+del p192_G, p192, p192_names
+
+p224_G = EccPoint(_curves['p224'].Gx, _curves['p224'].Gy, "p224")
+p224 = _curves['p224']._replace(G=p224_G)
+_curves.update(dict.fromkeys(p224_names, p224))
+del p224_G, p224, p224_names
+
p256_G = EccPoint(_curves['p256'].Gx, _curves['p256'].Gy, "p256")
p256 = _curves['p256']._replace(G=p256_G)
_curves.update(dict.fromkeys(p256_names, p256))
@@ -423,20 +688,37 @@ p521 = _curves['p521']._replace(G=p521_G)
_curves.update(dict.fromkeys(p521_names, p521))
del p521_G, p521, p521_names
+ed25519_G = EccPoint(_curves['Ed25519'].Gx, _curves['Ed25519'].Gy, "Ed25519")
+ed25519 = _curves['Ed25519']._replace(G=ed25519_G)
+_curves.update(dict.fromkeys(ed25519_names, ed25519))
+del ed25519_G, ed25519, ed25519_names
+
+ed448_G = EccPoint(_curves['Ed448'].Gx, _curves['Ed448'].Gy, "Ed448")
+ed448 = _curves['Ed448']._replace(G=ed448_G)
+_curves.update(dict.fromkeys(ed448_names, ed448))
+del ed448_G, ed448, ed448_names
+
class EccKey(object):
r"""Class defining an ECC key.
Do not instantiate directly.
Use :func:`generate`, :func:`construct` or :func:`import_key` instead.
- :ivar curve: The name of the ECC as defined in :numref:`curve_names`.
+ :ivar curve: The name of the curve as defined in the `ECC table`_.
:vartype curve: string
- :ivar pointQ: an ECC point representating the public component
+ :ivar pointQ: an ECC point representating the public component.
:vartype pointQ: :class:`EccPoint`
- :ivar d: A scalar representating the private component
+ :ivar d: A scalar that represents the private component
+ in NIST P curves. It is smaller than the
+ order of the generator point.
:vartype d: integer
+
+ :ivar seed: A seed that representats the private component
+ in EdDSA curves
+ (Ed25519, 32 bytes; Ed448, 57 bytes).
+ :vartype seed: bytes
"""
def __init__(self, **kwargs):
@@ -444,34 +726,79 @@ class EccKey(object):
Keywords:
curve : string
- It must be *"p256"*, *"P-256"*, *"prime256v1"* or *"secp256r1"*.
+ The name of the curve.
d : integer
- Only for a private key. It must be in the range ``[1..order-1]``.
+ Mandatory for a private key one NIST P curves.
+ It must be in the range ``[1..order-1]``.
+ seed : bytes
+ Mandatory for a private key on the Ed25519 (32 bytes)
+ or Ed448 (57 bytes) curve.
point : EccPoint
Mandatory for a public key. If provided for a private key,
the implementation will NOT check whether it matches ``d``.
+
+ Only one parameter among ``d``, ``seed`` or ``point`` may be used.
"""
kwargs_ = dict(kwargs)
curve_name = kwargs_.pop("curve", None)
self._d = kwargs_.pop("d", None)
+ self._seed = kwargs_.pop("seed", None)
self._point = kwargs_.pop("point", None)
+ if curve_name is None and self._point:
+ curve_name = self._point._curve_name
if kwargs_:
raise TypeError("Unknown parameters: " + str(kwargs_))
if curve_name not in _curves:
- raise ValueError("Unsupported curve (%s)", curve_name)
+ raise ValueError("Unsupported curve (%s)" % curve_name)
self._curve = _curves[curve_name]
+ self.curve = curve_name
+
+ count = int(self._d is not None) + int(self._seed is not None)
- if self._d is None:
+ if count == 0:
if self._point is None:
- raise ValueError("Either private or public ECC component must be specified, not both")
- else:
+ raise ValueError("At lest one between parameters 'point', 'd' or 'seed' must be specified")
+ return
+
+ if count == 2:
+ raise ValueError("Parameters d and seed are mutually exclusive")
+
+ # NIST P curves work with d, EdDSA works with seed
+
+ if not self._is_eddsa():
+ if self._seed is not None:
+ raise ValueError("Parameter 'seed' can only be used with Ed25519 or Ed448")
self._d = Integer(self._d)
if not 1 <= self._d < self._curve.order:
- raise ValueError("Invalid ECC private component")
-
- self.curve = self._curve.desc
+ raise ValueError("Parameter d must be an integer smaller than the curve order")
+ else:
+ if self._d is not None:
+ raise ValueError("Parameter d can only be used with NIST P curves")
+ # RFC 8032, 5.1.5
+ if self._curve.name == "ed25519":
+ if len(self._seed) != 32:
+ raise ValueError("Parameter seed must be 32 bytes long for Ed25519")
+ seed_hash = SHA512.new(self._seed).digest() # h
+ self._prefix = seed_hash[32:]
+ tmp = bytearray(seed_hash[:32])
+ tmp[0] &= 0xF8
+ tmp[31] = (tmp[31] & 0x7F) | 0x40
+ # RFC 8032, 5.2.5
+ elif self._curve.name == "ed448":
+ if len(self._seed) != 57:
+ raise ValueError("Parameter seed must be 57 bytes long for Ed448")
+ seed_hash = SHAKE256.new(self._seed).read(114) # h
+ self._prefix = seed_hash[57:]
+ tmp = bytearray(seed_hash[:57])
+ tmp[0] &= 0xFC
+ tmp[55] |= 0x80
+ tmp[56] = 0
+ self._d = Integer.from_bytes(tmp, byteorder='little')
+
+ def _is_eddsa(self):
+ return self._curve.desc in ("Ed25519", "Ed448")
def __eq__(self, other):
if other.has_private() != self.has_private():
@@ -481,7 +808,10 @@ class EccKey(object):
def __repr__(self):
if self.has_private():
- extra = ", d=%d" % int(self._d)
+ if self._is_eddsa():
+ extra = ", seed=%s" % self._seed.hex()
+ else:
+ extra = ", d=%d" % int(self._d)
else:
extra = ""
x, y = self.pointQ.xy
@@ -492,6 +822,7 @@ class EccKey(object):
return self._d is not None
+ # ECDSA
def _sign(self, z, k):
assert 0 < k < self._curve.order
@@ -506,6 +837,7 @@ class EccKey(object):
s = inv_blind_k * (blind * z + blind_d * r) % order
return (r, s)
+ # ECDSA
def _verify(self, z, rs):
order = self._curve.order
sinv = rs[1].inverse(order)
@@ -520,6 +852,12 @@ class EccKey(object):
return self._d
@property
+ def seed(self):
+ if not self.has_private():
+ raise ValueError("This is not a private ECC key")
+ return self._seed
+
+ @property
def pointQ(self):
if self._point is None:
self._point = self._curve.G * self._d
@@ -534,9 +872,12 @@ class EccKey(object):
return EccKey(curve=self._curve.desc, point=self.pointQ)
- def _export_subjectPublicKeyInfo(self, compress):
+ def _export_SEC1(self, compress):
+ if self._is_eddsa():
+ raise ValueError("SEC1 format is unsupported for EdDSA curves")
# See 2.2 in RFC5480 and 2.3.3 in SEC1
+ #
# The first byte is:
# - 0x02: compressed, only X-coordinate, Y-coordinate is even
# - 0x03: compressed, only X-coordinate, Y-coordinate is odd
@@ -547,20 +888,45 @@ class EccKey(object):
modulus_bytes = self.pointQ.size_in_bytes()
if compress:
- first_byte = 2 + self.pointQ.y.is_odd()
- public_key = (bchr(first_byte) +
+ if self.pointQ.y.is_odd():
+ first_byte = b'\x03'
+ else:
+ first_byte = b'\x02'
+ public_key = (first_byte +
self.pointQ.x.to_bytes(modulus_bytes))
else:
public_key = (b'\x04' +
self.pointQ.x.to_bytes(modulus_bytes) +
self.pointQ.y.to_bytes(modulus_bytes))
+ return public_key
- unrestricted_oid = "1.2.840.10045.2.1"
- return _create_subject_public_key_info(unrestricted_oid,
+ def _export_eddsa(self):
+ x, y = self.pointQ.xy
+ if self._curve.name == "ed25519":
+ result = bytearray(y.to_bytes(32, byteorder='little'))
+ result[31] = ((x & 1) << 7) | result[31]
+ elif self._curve.name == "ed448":
+ result = bytearray(y.to_bytes(57, byteorder='little'))
+ result[56] = (x & 1) << 7
+ else:
+ raise ValueError("Not an EdDSA key to export")
+ return bytes(result)
+
+ def _export_subjectPublicKeyInfo(self, compress):
+ if self._is_eddsa():
+ oid = self._curve.oid
+ public_key = self._export_eddsa()
+ params = None
+ else:
+ oid = "1.2.840.10045.2.1" # unrestricted
+ public_key = self._export_SEC1(compress)
+ params = DerObjectId(self._curve.oid)
+
+ return _create_subject_public_key_info(oid,
public_key,
- DerObjectId(self._curve.oid))
+ params)
- def _export_private_der(self, include_ec_params=True):
+ def _export_rfc5915_private_der(self, include_ec_params=True):
assert self.has_private()
@@ -593,11 +959,18 @@ class EccKey(object):
if kwargs.get('passphrase', None) is not None and 'protection' not in kwargs:
raise ValueError("At least the 'protection' parameter should be present")
- unrestricted_oid = "1.2.840.10045.2.1"
- private_key = self._export_private_der(include_ec_params=False)
+ if self._is_eddsa():
+ oid = self._curve.oid
+ private_key = DerOctetString(self._seed).encode()
+ params = None
+ else:
+ oid = "1.2.840.10045.2.1" # unrestricted
+ private_key = self._export_rfc5915_private_der(include_ec_params=False)
+ params = DerObjectId(self._curve.oid)
+
result = PKCS8.wrap(private_key,
- unrestricted_oid,
- key_params=DerObjectId(self._curve.oid),
+ oid,
+ key_params=params,
**kwargs)
return result
@@ -610,7 +983,7 @@ class EccKey(object):
def _export_private_pem(self, passphrase, **kwargs):
from Cryptodome.IO import PEM
- encoded_der = self._export_private_der()
+ encoded_der = self._export_rfc5915_private_der()
return PEM.encode(encoded_der, "EC PRIVATE KEY", passphrase, **kwargs)
def _export_private_clear_pkcs8_in_clear_pem(self):
@@ -633,19 +1006,27 @@ class EccKey(object):
raise ValueError("Cannot export OpenSSH private keys")
desc = self._curve.openssh
- modulus_bytes = self.pointQ.size_in_bytes()
- if compress:
- first_byte = 2 + self.pointQ.y.is_odd()
- public_key = (bchr(first_byte) +
- self.pointQ.x.to_bytes(modulus_bytes))
+ if desc is None:
+ raise ValueError("Cannot export %s keys as OpenSSH" % self._curve.name)
+ elif desc == "ssh-ed25519":
+ public_key = self._export_eddsa()
+ comps = (tobytes(desc), tobytes(public_key))
else:
- public_key = (b'\x04' +
- self.pointQ.x.to_bytes(modulus_bytes) +
- self.pointQ.y.to_bytes(modulus_bytes))
+ modulus_bytes = self.pointQ.size_in_bytes()
+
+ if compress:
+ first_byte = 2 + self.pointQ.y.is_odd()
+ public_key = (bchr(first_byte) +
+ self.pointQ.x.to_bytes(modulus_bytes))
+ else:
+ public_key = (b'\x04' +
+ self.pointQ.x.to_bytes(modulus_bytes) +
+ self.pointQ.y.to_bytes(modulus_bytes))
+
+ middle = desc.split("-")[2]
+ comps = (tobytes(desc), tobytes(middle), public_key)
- middle = desc.split("-")[2]
- comps = (tobytes(desc), tobytes(middle), public_key)
blob = b"".join([struct.pack(">I", len(x)) + x for x in comps])
return desc + " " + tostr(binascii.b2a_base64(blob))
@@ -665,6 +1046,15 @@ class EccKey(object):
- ``'PEM'``. The key will be encoded in a PEM_ envelope (ASCII).
- ``'OpenSSH'``. The key will be encoded in the OpenSSH_ format
(ASCII, public keys only).
+ - ``'SEC1'``. The public key (i.e., the EC point) will be encoded
+ into ``bytes`` according to Section 2.3.3 of `SEC1`_
+ (which is a subset of the older X9.62 ITU standard).
+ Only for NIST P-curves.
+ - ``'raw'``. The public key will be encoded as ``bytes``,
+ without any metadata.
+
+ * For NIST P-curves: equivalent to ``'SEC1'``.
+ * For EdDSA curves: ``bytes`` in the format defined in `RFC8032`_.
passphrase (byte string or string):
The passphrase to use for protecting the private key.
@@ -673,9 +1063,7 @@ class EccKey(object):
Only relevant for private keys.
If ``True`` (default and recommended), the `PKCS#8`_ representation
- will be used.
-
- If ``False``, the much weaker `PEM encryption`_ mechanism will be used.
+ will be used. It must be ``True`` for EdDSA curves.
protection (string):
When a private key is exported with password-protection
@@ -684,10 +1072,13 @@ class EccKey(object):
It is recommended to use ``PBKDF2WithHMAC-SHA1AndAES128-CBC``.
compress (boolean):
- If ``True``, a more compact representation of the public key
- with the X-coordinate only is used.
+ If ``True``, the method returns a more compact representation
+ of the public key, with the X-coordinate only.
+
+ If ``False`` (default), the method returns the full public key.
- If ``False`` (default), the full public key will be exported.
+ This parameter is ignored for EdDSA curves, as compression is
+ mandatory.
.. warning::
If you don't provide a passphrase, the private key will be
@@ -700,18 +1091,18 @@ class EccKey(object):
.. _PEM: http://www.ietf.org/rfc/rfc1421.txt
.. _`PEM encryption`: http://www.ietf.org/rfc/rfc1423.txt
- .. _`PKCS#8`: http://www.ietf.org/rfc/rfc5208.txt
.. _OpenSSH: http://www.openssh.com/txt/rfc5656.txt
.. _RFC5480: https://tools.ietf.org/html/rfc5480
- .. _RFC5915: http://www.ietf.org/rfc/rfc5915.txt
+ .. _SEC1: https://www.secg.org/sec1-v2.pdf
Returns:
- A multi-line string (for PEM and OpenSSH) or bytes (for DER) with the encoded key.
+ A multi-line string (for ``'PEM'`` and ``'OpenSSH'``) or
+ ``bytes`` (for ``'DER'``, ``'SEC1'``, and ``'raw'``) with the encoded key.
"""
args = kwargs.copy()
ext_format = args.pop("format")
- if ext_format not in ("PEM", "DER", "OpenSSH"):
+ if ext_format not in ("PEM", "DER", "OpenSSH", "SEC1", "raw"):
raise ValueError("Unknown format '%s'" % ext_format)
compress = args.pop("compress", False)
@@ -723,6 +1114,10 @@ class EccKey(object):
if not passphrase:
raise ValueError("Empty passphrase")
use_pkcs8 = args.pop("use_pkcs8", True)
+
+ if not use_pkcs8 and self._is_eddsa():
+ raise ValueError("'pkcs8' must be True for EdDSA curves")
+
if ext_format == "PEM":
if use_pkcs8:
if passphrase:
@@ -738,9 +1133,10 @@ class EccKey(object):
if use_pkcs8:
return self._export_pkcs8(passphrase=passphrase, **args)
else:
- return self._export_private_der()
+ return self._export_rfc5915_private_der()
else:
- raise ValueError("Private keys cannot be exported in OpenSSH format")
+ raise ValueError("Private keys cannot be exported "
+ "in the '%s' format" % ext_format)
else: # Public key
if args:
raise ValueError("Unexpected parameters: '%s'" % args)
@@ -748,6 +1144,13 @@ class EccKey(object):
return self._export_public_pem(compress)
elif ext_format == "DER":
return self._export_subjectPublicKeyInfo(compress)
+ elif ext_format == "SEC1":
+ return self._export_SEC1(compress)
+ elif ext_format == "raw":
+ if self._curve.name in ('ed25519', 'ed448'):
+ return self._export_eddsa()
+ else:
+ return self._export_SEC1(compress)
else:
return self._export_openssh(compress)
@@ -758,7 +1161,7 @@ def generate(**kwargs):
Args:
curve (string):
- Mandatory. It must be a curve name defined in :numref:`curve_names`.
+ Mandatory. It must be a curve name defined in the `ECC table`_.
randfunc (callable):
Optional. The RNG to read randomness from.
@@ -771,30 +1174,46 @@ def generate(**kwargs):
if kwargs:
raise TypeError("Unknown parameters: " + str(kwargs))
- d = Integer.random_range(min_inclusive=1,
- max_exclusive=curve.order,
- randfunc=randfunc)
+ if _curves[curve_name].name == "ed25519":
+ seed = randfunc(32)
+ new_key = EccKey(curve=curve_name, seed=seed)
+ elif _curves[curve_name].name == "ed448":
+ seed = randfunc(57)
+ new_key = EccKey(curve=curve_name, seed=seed)
+ else:
+ d = Integer.random_range(min_inclusive=1,
+ max_exclusive=curve.order,
+ randfunc=randfunc)
+ new_key = EccKey(curve=curve_name, d=d)
- return EccKey(curve=curve_name, d=d)
+ return new_key
def construct(**kwargs):
"""Build a new ECC key (private or public) starting
from some base components.
- Args:
+ In most cases, you will already have an existing key
+ which you can read in with :func:`import_key` instead
+ of this function.
+ Args:
curve (string):
- Mandatory. It must be a curve name defined in :numref:`curve_names`.
+ Mandatory. The name of the elliptic curve, as defined in the `ECC table`_.
d (integer):
- Only for a private key. It must be in the range ``[1..order-1]``.
+ Mandatory for a private key and a NIST P-curve (e.g., P-256):
+ the integer in the range ``[1..order-1]`` that represents the key.
+
+ seed (bytes):
+ Mandatory for a private key and an EdDSA curve.
+ It must be 32 bytes for Ed25519, and 57 bytes for Ed448.
point_x (integer):
- Mandatory for a public key. X coordinate (affine) of the ECC point.
+ Mandatory for a public key: the X coordinate (affine) of the ECC point.
point_y (integer):
- Mandatory for a public key. Y coordinate (affine) of the ECC point.
+ Mandatory for a public key: the Y coordinate (affine) of the ECC point.
Returns:
:class:`EccKey` : a new ECC key object
@@ -812,29 +1231,39 @@ def construct(**kwargs):
# ValueError is raised if the point is not on the curve
kwargs["point"] = EccPoint(point_x, point_y, curve_name)
+ new_key = EccKey(**kwargs)
+
# Validate that the private key matches the public one
- d = kwargs.get("d", None)
- if d is not None and "point" in kwargs:
- pub_key = curve.G * d
+ # because EccKey will not do that automatically
+ if new_key.has_private() and 'point' in kwargs:
+ pub_key = curve.G * new_key.d
if pub_key.xy != (point_x, point_y):
raise ValueError("Private and public ECC keys do not match")
- return EccKey(**kwargs)
+ return new_key
-def _import_public_der(curve_oid, ec_point):
+def _import_public_der(ec_point, curve_oid=None, curve_name=None):
"""Convert an encoded EC point into an EccKey object
+ ec_point: byte string with the EC point (SEC1-encoded)
+ curve_oid: string with the name the curve
curve_name: string with the OID of the curve
- ec_point: byte string with the EC point (not DER encoded)
+
+ Either curve_id or curve_name must be specified
"""
- for curve_name, curve in _curves.items():
- if curve.oid == curve_oid:
+ for _curve_name, curve in _curves.items():
+ if curve_oid and curve.oid == curve_oid:
+ break
+ if curve_name == _curve_name:
break
else:
- raise UnsupportedEccFeature("Unsupported ECC curve (OID: %s)" % curve_oid)
+ if curve_oid:
+ raise UnsupportedEccFeature("Unsupported ECC curve (OID: %s)" % curve_oid)
+ else:
+ raise UnsupportedEccFeature("Unsupported ECC curve (%s)" % curve_name)
# See 2.2 in RFC5480 and 2.3.3 in SEC1
# The first byte is:
@@ -854,11 +1283,12 @@ def _import_public_der(curve_oid, ec_point):
x = Integer.from_bytes(ec_point[1:modulus_bytes+1])
y = Integer.from_bytes(ec_point[modulus_bytes+1:])
# Compressed point
- elif point_type in (0x02, 0x3):
+ elif point_type in (0x02, 0x03):
if len(ec_point) != (1 + modulus_bytes):
raise ValueError("Incorrect EC point length")
x = Integer.from_bytes(ec_point[1:])
- y = (x**3 - x*3 + curve.b).sqrt(curve.p) # Short Weierstrass
+ # Right now, we only support Short Weierstrass curves
+ y = (x**3 - x*3 + curve.b).sqrt(curve.p)
if point_type == 0x02 and y.is_odd():
y = curve.p - y
if point_type == 0x03 and y.is_even():
@@ -866,7 +1296,7 @@ def _import_public_der(curve_oid, ec_point):
else:
raise ValueError("Incorrect EC point encoding")
- return construct(curve=curve_name, point_x=x, point_y=y)
+ return construct(curve=_curve_name, point_x=x, point_y=y)
def _import_subjectPublicKeyInfo(encoded, *kwargs):
@@ -877,38 +1307,51 @@ def _import_subjectPublicKeyInfo(encoded, *kwargs):
# Parse the generic subjectPublicKeyInfo structure
oid, ec_point, params = _expand_subject_public_key_info(encoded)
- # ec_point must be an encoded OCTET STRING
- # params is encoded ECParameters
-
- # We accept id-ecPublicKey, id-ecDH, id-ecMQV without making any
- # distiction for now.
-
- # Restrictions can be captured in the key usage certificate
- # extension
- unrestricted_oid = "1.2.840.10045.2.1"
- ecdh_oid = "1.3.132.1.12"
- ecmqv_oid = "1.3.132.1.13"
+ nist_p_oids = (
+ "1.2.840.10045.2.1", # id-ecPublicKey (unrestricted)
+ "1.3.132.1.12", # id-ecDH
+ "1.3.132.1.13" # id-ecMQV
+ )
+ eddsa_oids = {
+ "1.3.101.112": ("Ed25519", _import_ed25519_public_key), # id-Ed25519
+ "1.3.101.113": ("Ed448", _import_ed448_public_key) # id-Ed448
+ }
+
+ if oid in nist_p_oids:
+ # See RFC5480
+
+ # Parameters are mandatory and encoded as ECParameters
+ # ECParameters ::= CHOICE {
+ # namedCurve OBJECT IDENTIFIER
+ # -- implicitCurve NULL
+ # -- specifiedCurve SpecifiedECDomain
+ # }
+ # implicitCurve and specifiedCurve are not supported (as per RFC)
+ if not params:
+ raise ValueError("Missing ECC parameters for ECC OID %s" % oid)
+ try:
+ curve_oid = DerObjectId().decode(params).value
+ except ValueError:
+ raise ValueError("Error decoding namedCurve")
- if oid not in (unrestricted_oid, ecdh_oid, ecmqv_oid):
- raise UnsupportedEccFeature("Unsupported ECC purpose (OID: %s)" % oid)
+ # ECPoint ::= OCTET STRING
+ return _import_public_der(ec_point, curve_oid=curve_oid)
- # Parameters are mandatory for all three types
- if not params:
- raise ValueError("Missing ECC parameters")
+ elif oid in eddsa_oids:
+ # See RFC8410
+ curve_name, import_eddsa_public_key = eddsa_oids[oid]
- # ECParameters ::= CHOICE {
- # namedCurve OBJECT IDENTIFIER
- # -- implicitCurve NULL
- # -- specifiedCurve SpecifiedECDomain
- # }
- #
- # implicitCurve and specifiedCurve are not supported (as per RFC)
- curve_oid = DerObjectId().decode(params).value
+ # Parameters must be absent
+ if params:
+ raise ValueError("Unexpected ECC parameters for ECC OID %s" % oid)
- return _import_public_der(curve_oid, ec_point)
+ x, y = import_eddsa_public_key(ec_point)
+ return construct(point_x=x, point_y=y, curve=curve_name)
+ else:
+ raise UnsupportedEccFeature("Unsupported ECC OID: %s" % oid)
-def _import_private_der(encoded, passphrase, curve_oid=None):
+def _import_rfc5915_der(encoded, passphrase, curve_oid=None):
# See RFC5915 https://tools.ietf.org/html/rfc5915
#
@@ -949,7 +1392,7 @@ def _import_private_der(encoded, passphrase, curve_oid=None):
# Decode public key (if any)
if len(private_key) == 4:
public_key_enc = DerBitString(explicit=1).decode(private_key[3]).value
- public_key = _import_public_der(curve_oid, public_key_enc)
+ public_key = _import_public_der(public_key_enc, curve_oid=curve_oid)
point_x = public_key.pointQ.x
point_y = public_key.pointQ.y
else:
@@ -961,28 +1404,30 @@ def _import_private_der(encoded, passphrase, curve_oid=None):
def _import_pkcs8(encoded, passphrase):
from Cryptodome.IO import PKCS8
- # From RFC5915, Section 1:
- #
- # Distributing an EC private key with PKCS#8 [RFC5208] involves including:
- # a) id-ecPublicKey, id-ecDH, or id-ecMQV (from [RFC5480]) with the
- # namedCurve as the parameters in the privateKeyAlgorithm field; and
- # b) ECPrivateKey in the PrivateKey field, which is an OCTET STRING.
-
algo_oid, private_key, params = PKCS8.unwrap(encoded, passphrase)
- # We accept id-ecPublicKey, id-ecDH, id-ecMQV without making any
- # distiction for now.
- unrestricted_oid = "1.2.840.10045.2.1"
- ecdh_oid = "1.3.132.1.12"
- ecmqv_oid = "1.3.132.1.13"
-
- if algo_oid not in (unrestricted_oid, ecdh_oid, ecmqv_oid):
+ nist_p_oids = (
+ "1.2.840.10045.2.1", # id-ecPublicKey (unrestricted)
+ "1.3.132.1.12", # id-ecDH
+ "1.3.132.1.13" # id-ecMQV
+ )
+ eddsa_oids = {
+ "1.3.101.112": "Ed25519", # id-Ed25519
+ "1.3.101.113": "Ed448", # id-Ed448
+ }
+
+ if algo_oid in nist_p_oids:
+ curve_oid = DerObjectId().decode(params).value
+ return _import_rfc5915_der(private_key, passphrase, curve_oid)
+ elif algo_oid in eddsa_oids:
+ if params is not None:
+ raise ValueError("EdDSA ECC private key must not have parameters")
+ curve_oid = None
+ seed = DerOctetString().decode(private_key).payload
+ return construct(curve=eddsa_oids[algo_oid], seed=seed)
+ else:
raise UnsupportedEccFeature("Unsupported ECC purpose (OID: %s)" % algo_oid)
- curve_oid = DerObjectId().decode(params).value
-
- return _import_private_der(private_key, passphrase, curve_oid)
-
def _import_x509_cert(encoded, *kwargs):
@@ -1007,7 +1452,7 @@ def _import_der(encoded, passphrase):
pass
try:
- return _import_private_der(encoded, passphrase)
+ return _import_rfc5915_der(encoded, passphrase)
except UnsupportedEccFeature as err:
raise err
except (ValueError, TypeError, IndexError):
@@ -1024,22 +1469,49 @@ def _import_der(encoded, passphrase):
def _import_openssh_public(encoded):
- keystring = binascii.a2b_base64(encoded.split(b' ')[1])
+ parts = encoded.split(b' ')
+ if len(parts) not in (2, 3):
+ raise ValueError("Not an openssh public key")
- keyparts = []
- while len(keystring) > 4:
- lk = struct.unpack(">I", keystring[:4])[0]
- keyparts.append(keystring[4:4 + lk])
- keystring = keystring[4 + lk:]
+ try:
+ keystring = binascii.a2b_base64(parts[1])
+
+ keyparts = []
+ while len(keystring) > 4:
+ lk = struct.unpack(">I", keystring[:4])[0]
+ keyparts.append(keystring[4:4 + lk])
+ keystring = keystring[4 + lk:]
+
+ if parts[0] != keyparts[0]:
+ raise ValueError("Mismatch in openssh public key")
+
+ # NIST P curves
+ if parts[0].startswith(b"ecdsa-sha2-"):
+
+ for curve_name, curve in _curves.items():
+ if curve.openssh is None:
+ continue
+ if not curve.openssh.startswith("ecdsa-sha2"):
+ continue
+ middle = tobytes(curve.openssh.split("-")[2])
+ if keyparts[1] == middle:
+ break
+ else:
+ raise ValueError("Unsupported ECC curve: " + middle)
- for curve_name, curve in _curves.items():
- middle = tobytes(curve.openssh.split("-")[2])
- if keyparts[1] == middle:
- break
- else:
- raise ValueError("Unsupported ECC curve")
+ ecc_key = _import_public_der(keyparts[2], curve_oid=curve.oid)
- return _import_public_der(curve.oid, keyparts[2])
+ # EdDSA
+ elif parts[0] == b"ssh-ed25519":
+ x, y = _import_ed25519_public_key(keyparts[1])
+ ecc_key = construct(curve="Ed25519", point_x=x, point_y=y)
+ else:
+ raise ValueError("Unsupported SSH key type: " + parts[0])
+
+ except (IndexError, TypeError, binascii.Error):
+ raise ValueError("Error parsing SSH key type: " + parts[0])
+
+ return ecc_key
def _import_openssh_private_ecc(data, password):
@@ -1047,51 +1519,168 @@ def _import_openssh_private_ecc(data, password):
from ._openssh import (import_openssh_private_generic,
read_bytes, read_string, check_padding)
- ssh_name, decrypted = import_openssh_private_generic(data, password)
+ key_type, decrypted = import_openssh_private_generic(data, password)
+
+ eddsa_keys = {
+ "ssh-ed25519": ("Ed25519", _import_ed25519_public_key, 32),
+ }
+
+ # https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-04
+ if key_type.startswith("ecdsa-sha2"):
+
+ ecdsa_curve_name, decrypted = read_string(decrypted)
+ if ecdsa_curve_name not in _curves:
+ raise UnsupportedEccFeature("Unsupported ECC curve %s" % ecdsa_curve_name)
+ curve = _curves[ecdsa_curve_name]
+ modulus_bytes = (curve.modulus_bits + 7) // 8
+
+ public_key, decrypted = read_bytes(decrypted)
+
+ if bord(public_key[0]) != 4:
+ raise ValueError("Only uncompressed OpenSSH EC keys are supported")
+ if len(public_key) != 2 * modulus_bytes + 1:
+ raise ValueError("Incorrect public key length")
- name, decrypted = read_string(decrypted)
- if name not in _curves:
- raise UnsupportedEccFeature("Unsupported ECC curve %s" % name)
- curve = _curves[name]
- modulus_bytes = (curve.modulus_bits + 7) // 8
+ point_x = Integer.from_bytes(public_key[1:1+modulus_bytes])
+ point_y = Integer.from_bytes(public_key[1+modulus_bytes:])
- public_key, decrypted = read_bytes(decrypted)
+ private_key, decrypted = read_bytes(decrypted)
+ d = Integer.from_bytes(private_key)
- if bord(public_key[0]) != 4:
- raise ValueError("Only uncompressed OpenSSH EC keys are supported")
- if len(public_key) != 2 * modulus_bytes + 1:
- raise ValueError("Incorrect public key length")
+ params = {'d': d, 'curve': ecdsa_curve_name}
- point_x = Integer.from_bytes(public_key[1:1+modulus_bytes])
- point_y = Integer.from_bytes(public_key[1+modulus_bytes:])
- point = EccPoint(point_x, point_y, curve=name)
+ elif key_type in eddsa_keys:
- private_key, decrypted = read_bytes(decrypted)
- d = Integer.from_bytes(private_key)
+ curve_name, import_eddsa_public_key, seed_len = eddsa_keys[key_type]
+
+ public_key, decrypted = read_bytes(decrypted)
+ point_x, point_y = import_eddsa_public_key(public_key)
+
+ private_public_key, decrypted = read_bytes(decrypted)
+ seed = private_public_key[:seed_len]
+
+ params = {'seed': seed, 'curve': curve_name}
+ else:
+ raise ValueError("Unsupport SSH agent key type:" + key_type)
_, padded = read_string(decrypted) # Comment
check_padding(padded)
- return EccKey(curve=name, d=d, point=point)
+ return construct(point_x=point_x, point_y=point_y, **params)
+
+
+def _import_ed25519_public_key(encoded):
+ """Import an Ed25519 ECC public key, encoded as raw bytes as described
+ in RFC8032_.
+
+ Args:
+ encoded (bytes):
+ The Ed25519 public key to import. It must be 32 bytes long.
+
+ Returns:
+ :class:`EccKey` : a new ECC key object
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+
+ .. _RFC8032: https://datatracker.ietf.org/doc/html/rfc8032
+ """
+
+ if len(encoded) != 32:
+ raise ValueError("Incorrect length. Only Ed25519 public keys are supported.")
+
+ p = Integer(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed) # 2**255 - 19
+ d = 37095705934669439343138083508754565189542113879843219016388785533085940283555
+
+ y = bytearray(encoded)
+ x_lsb = y[31] >> 7
+ y[31] &= 0x7F
+ point_y = Integer.from_bytes(y, byteorder='little')
+ if point_y >= p:
+ raise ValueError("Invalid Ed25519 key (y)")
+ if point_y == 1:
+ return 0, 1
+ u = (point_y**2 - 1) % p
+ v = ((point_y**2 % p) * d + 1) % p
+ try:
+ v_inv = v.inverse(p)
+ x2 = (u * v_inv) % p
+ point_x = Integer._tonelli_shanks(x2, p)
+ if (point_x & 1) != x_lsb:
+ point_x = p - point_x
+ except ValueError:
+ raise ValueError("Invalid Ed25519 public key")
+ return point_x, point_y
+
+
+def _import_ed448_public_key(encoded):
+ """Import an Ed448 ECC public key, encoded as raw bytes as described
+ in RFC8032_.
+
+ Args:
+ encoded (bytes):
+ The Ed448 public key to import. It must be 57 bytes long.
+
+ Returns:
+ :class:`EccKey` : a new ECC key object
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+
+ .. _RFC8032: https://datatracker.ietf.org/doc/html/rfc8032
+ """
-def import_key(encoded, passphrase=None):
+ if len(encoded) != 57:
+ raise ValueError("Incorrect length. Only Ed448 public keys are supported.")
+
+ p = Integer(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffff) # 2**448 - 2**224 - 1
+ d = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffff6756
+
+ y = encoded[:56]
+ x_lsb = bord(encoded[56]) >> 7
+ point_y = Integer.from_bytes(y, byteorder='little')
+ if point_y >= p:
+ raise ValueError("Invalid Ed448 key (y)")
+ if point_y == 1:
+ return 0, 1
+
+ u = (point_y**2 - 1) % p
+ v = ((point_y**2 % p) * d - 1) % p
+ try:
+ v_inv = v.inverse(p)
+ x2 = (u * v_inv) % p
+ point_x = Integer._tonelli_shanks(x2, p)
+ if (point_x & 1) != x_lsb:
+ point_x = p - point_x
+ except ValueError:
+ raise ValueError("Invalid Ed448 public key")
+ return point_x, point_y
+
+
+def import_key(encoded, passphrase=None, curve_name=None):
"""Import an ECC key (public or private).
Args:
encoded (bytes or multi-line string):
The ECC key to import.
+ The function will try to automatically detect the right format.
- An ECC **public** key can be:
+ Supported formats for an ECC **public** key:
- - An X.509 certificate, binary (DER) or ASCII (PEM)
- - An X.509 ``subjectPublicKeyInfo``, binary (DER) or ASCII (PEM)
- - An OpenSSH line (e.g. the content of ``~/.ssh/id_ecdsa``, ASCII)
+ * X.509 certificate: binary (DER) or ASCII (PEM).
+ * X.509 ``subjectPublicKeyInfo``: binary (DER) or ASCII (PEM).
+ * SEC1_ (or X9.62), as ``bytes``. NIST P curves only.
+ You must also provide the ``curve_name`` (with a value from the `ECC table`_)
+ * OpenSSH line, defined in RFC5656_ and RFC8709_ (ASCII).
+ This is normally the content of files like ``~/.ssh/id_ecdsa.pub``.
- An ECC **private** key can be:
+ Supported formats for an ECC **private** key:
- - In binary format (DER, see section 3 of `RFC5915`_ or `PKCS#8`_)
- - In ASCII format (PEM or `OpenSSH 6.5+`_)
+ * A binary ``ECPrivateKey`` structure, as defined in `RFC5915`_ (DER).
+ NIST P curves only.
+ * A `PKCS#8`_ structure (or the more recent Asymmetric Key Package, RFC5958_): binary (DER) or ASCII (PEM).
+ * `OpenSSH 6.5`_ and newer versions (ASCII).
Private keys can be in the clear or password-protected.
@@ -1099,9 +1688,21 @@ def import_key(encoded, passphrase=None):
passphrase (byte string):
The passphrase to use for decrypting a private key.
- Encryption may be applied protected at the PEM level or at the PKCS#8 level.
+ Encryption may be applied protected at the PEM level (not recommended)
+ or at the PKCS#8 level (recommended).
This parameter is ignored if the key in input is not encrypted.
+ curve_name (string):
+ For a SEC1 encoding only. This is the name of the curve,
+ as defined in the `ECC table`_.
+
+ .. note::
+
+ To import EdDSA private and public keys, when encoded as raw ``bytes``, use:
+
+ * :func:`Cryptodome.Signature.eddsa.import_public_key`, or
+ * :func:`Cryptodome.Signature.eddsa.import_private_key`.
+
Returns:
:class:`EccKey` : a new ECC key object
@@ -1109,11 +1710,15 @@ def import_key(encoded, passphrase=None):
ValueError: when the given key cannot be parsed (possibly because
the pass phrase is wrong).
- .. _RFC1421: http://www.ietf.org/rfc/rfc1421.txt
- .. _RFC1423: http://www.ietf.org/rfc/rfc1423.txt
- .. _RFC5915: http://www.ietf.org/rfc/rfc5915.txt
- .. _`PKCS#8`: http://www.ietf.org/rfc/rfc5208.txt
- .. _`OpenSSH 6.5+`: https://flak.tedunangst.com/post/new-openssh-key-format-and-bcrypt-pbkdf
+ .. _RFC1421: https://datatracker.ietf.org/doc/html/rfc1421
+ .. _RFC1423: https://datatracker.ietf.org/doc/html/rfc1423
+ .. _RFC5915: https://datatracker.ietf.org/doc/html/rfc5915
+ .. _RFC5656: https://datatracker.ietf.org/doc/html/rfc5656
+ .. _RFC8709: https://datatracker.ietf.org/doc/html/rfc8709
+ .. _RFC5958: https://datatracker.ietf.org/doc/html/rfc5958
+ .. _`PKCS#8`: https://datatracker.ietf.org/doc/html/rfc5208
+ .. _`OpenSSH 6.5`: https://flak.tedunangst.com/post/new-openssh-key-format-and-bcrypt-pbkdf
+ .. _SEC1: https://www.secg.org/sec1-v2.pdf
"""
from Cryptodome.IO import PEM
@@ -1135,12 +1740,11 @@ def import_key(encoded, passphrase=None):
# Remove any EC PARAMETERS section
# Ignore its content because the curve type must be already given in the key
- if sys.version_info[:2] != (2, 6):
- ecparams_start = "-----BEGIN EC PARAMETERS-----"
- ecparams_end = "-----END EC PARAMETERS-----"
- text_encoded = re.sub(ecparams_start + ".*?" + ecparams_end, "",
- text_encoded,
- flags=re.DOTALL)
+ ecparams_start = "-----BEGIN EC PARAMETERS-----"
+ ecparams_end = "-----END EC PARAMETERS-----"
+ text_encoded = re.sub(ecparams_start + ".*?" + ecparams_end, "",
+ text_encoded,
+ flags=re.DOTALL)
der_encoded, marker, enc_flag = PEM.decode(text_encoded, passphrase)
if enc_flag:
@@ -1154,13 +1758,19 @@ def import_key(encoded, passphrase=None):
return result
# OpenSSH
- if encoded.startswith(b'ecdsa-sha2-'):
+ if encoded.startswith((b'ecdsa-sha2-', b'ssh-ed25519')):
return _import_openssh_public(encoded)
# DER
if len(encoded) > 0 and bord(encoded[0]) == 0x30:
return _import_der(encoded, passphrase)
+ # SEC1
+ if len(encoded) > 0 and bord(encoded[0]) in b'\x02\x03\x04':
+ if curve_name is None:
+ raise ValueError("No curve name was provided")
+ return _import_public_der(encoded, curve_name=curve_name)
+
raise ValueError("ECC key format is not supported")