Ed25519 signatures

This module provides the protocol layer for Ed25519 as specified by RFC 8032. The underlying curve group is the native twisted Edwards group provided by sage.schemes.elliptic_curves.ell_edwards. Keeping the two layers separate is important: a generic twisted Edwards curve has an equation and a group law, while Ed25519 additionally fixes the field, parameters, base point, cofactor, hash function, scalar pruning, and byte encoding.

The public functions implement deterministic Ed25519 signing and verification using the 32-byte private-key seed format from RFC 8032:

sage: seed = bytes.fromhex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
sage: public_key = ed25519_public_key(seed)
sage: public_key.hex()
'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'
sage: signature = ed25519_sign(seed, b"")
sage: signature.hex()
'e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b'
sage: ed25519_verify(public_key, b"", signature)
True
>>> from sage.all import *
>>> seed = bytes.fromhex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
>>> public_key = ed25519_public_key(seed)
>>> public_key.hex()
'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'
>>> signature = ed25519_sign(seed, b"")
>>> signature.hex()
'e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b'
>>> ed25519_verify(public_key, b"", signature)
True
seed = bytes.fromhex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
public_key = ed25519_public_key(seed)
public_key.hex()
signature = ed25519_sign(seed, b"")
signature.hex()
ed25519_verify(public_key, b"", signature)

The curve constructor and base point are cached, so ordinary Sage additive group operations can be used directly without a second hand-written scalar multiplication implementation:

sage: C = Ed25519()
sage: B = Ed25519BasePoint()
sage: C(B) == B
True
sage: ed25519_decode(ed25519_encode(B)) == B
True
>>> from sage.all import *
>>> C = Ed25519()
>>> B = Ed25519BasePoint()
>>> C(B) == B
True
>>> ed25519_decode(ed25519_encode(B)) == B
True
C = Ed25519()
B = Ed25519BasePoint()
C(B) == B
ed25519_decode(ed25519_encode(B)) == B

This module intentionally does not provide X25519 or an interchangeable private-key object. X25519 is a different protocol using the Montgomery form and Ed25519’s signing key is a seed, not the already-pruned scalar.

REFERENCES:

[RFC8032]

S. Josefsson and I. Liusvaara, Edwards-Curve Digital Signature Algorithm (EdDSA), RFC 8032, January 2017.

AUTHORS:

  • SageMath developers (2026): initial Ed25519 protocol support

sage.crypto.ed25519.Ed25519()[source]

Return the standard Ed25519 twisted Edwards curve.

The returned parent is the curve

\[-x^2 + y^2 = 1 + d x^2 y^2, \qquad d = -121665/121666\]

over GF(2^255 - 19). The protocol-specific encoding and signing functions are ed25519_encode(), ed25519_decode(), ed25519_sign(), and ed25519_verify().

EXAMPLES:

sage: C = Ed25519()
sage: C.base_ring().order() == ED25519_FIELD_SIZE
True
sage: C.a() == -1
True
sage: C.d() == C.base_ring()(-121665) / C.base_ring()(121666)
True
>>> from sage.all import *
>>> C = Ed25519()
>>> C.base_ring().order() == ED25519_FIELD_SIZE
True
>>> C.a() == -Integer(1)
True
>>> C.d() == C.base_ring()(-Integer(121665)) / C.base_ring()(Integer(121666))
True
C = Ed25519()
C.base_ring().order() == ED25519_FIELD_SIZE
C.a() == -1
C.d() == C.base_ring()(-121665) / C.base_ring()(121666)
sage.crypto.ed25519.Ed25519BasePoint()[source]

Return the standard Ed25519 base point B.

EXAMPLES:

sage: B = Ed25519BasePoint()
sage: B.parent() is Ed25519()
True
sage: B.coordinates() == (ED25519_BASE_X, ED25519_BASE_Y)
True
>>> from sage.all import *
>>> B = Ed25519BasePoint()
>>> B.parent() is Ed25519()
True
>>> B.coordinates() == (ED25519_BASE_X, ED25519_BASE_Y)
True
B = Ed25519BasePoint()
B.parent() is Ed25519()
B.coordinates() == (ED25519_BASE_X, ED25519_BASE_Y)
sage.crypto.ed25519.ed25519_decode(data)[source]

Decode a canonical 32-byte Ed25519 point encoding.

The square root is recovered in GF(2^255 - 19) using the p = 5 mod 8 square-root method. The returned point is a point on the full curve; callers that need a prime-order point must apply the protocol’s cofactor handling.

EXAMPLES:

sage: ed25519_decode(bytes.fromhex('5866666666666666666666666666666666666666666666666666666666666666')) == Ed25519BasePoint()
True
sage: ed25519_decode(b'\xff' * 32)
Traceback (most recent call last):
...
ValueError: the encoded y-coordinate is not canonical
>>> from sage.all import *
>>> ed25519_decode(bytes.fromhex('5866666666666666666666666666666666666666666666666666666666666666')) == Ed25519BasePoint()
True
>>> ed25519_decode(b'\xff' * Integer(32))
Traceback (most recent call last):
...
ValueError: the encoded y-coordinate is not canonical
ed25519_decode(bytes.fromhex('5866666666666666666666666666666666666666666666666666666666666666')) == Ed25519BasePoint()
ed25519_decode(b'\xff' * 32)
sage.crypto.ed25519.ed25519_encode(point)[source]

Encode an Ed25519 point as 32 little-endian bytes.

The low 255 bits encode the affine y coordinate and the high bit encodes the least significant bit of x. Coordinates are required to be canonical field elements and point must belong to Ed25519().

EXAMPLES:

sage: ed25519_encode(Ed25519BasePoint()).hex()
'5866666666666666666666666666666666666666666666666666666666666666'
sage: ed25519_decode(ed25519_encode(Ed25519BasePoint())) == Ed25519BasePoint()
True
>>> from sage.all import *
>>> ed25519_encode(Ed25519BasePoint()).hex()
'5866666666666666666666666666666666666666666666666666666666666666'
>>> ed25519_decode(ed25519_encode(Ed25519BasePoint())) == Ed25519BasePoint()
True
ed25519_encode(Ed25519BasePoint()).hex()
ed25519_decode(ed25519_encode(Ed25519BasePoint())) == Ed25519BasePoint()
sage.crypto.ed25519.ed25519_public_key(seed)[source]

Derive the 32-byte Ed25519 public key from a 32-byte seed.

EXAMPLES:

sage: seed = bytes.fromhex('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')
sage: ed25519_public_key(seed).hex()
'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'
>>> from sage.all import *
>>> seed = bytes.fromhex('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')
>>> ed25519_public_key(seed).hex()
'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'
seed = bytes.fromhex('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')
ed25519_public_key(seed).hex()
sage.crypto.ed25519.ed25519_sign(seed, message)[source]

Create a deterministic Ed25519 signature.

seed is the 32-byte private-key seed and message is a bytes-like message. The result is the 64-byte concatenation ENC(R) || ENC(S).

EXAMPLES:

sage: seed = bytes.fromhex('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')
sage: ed25519_sign(seed, b'').hex()
'e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b'
>>> from sage.all import *
>>> seed = bytes.fromhex('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')
>>> ed25519_sign(seed, b'').hex()
'e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b'
seed = bytes.fromhex('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')
ed25519_sign(seed, b'').hex()
sage.crypto.ed25519.ed25519_verify(public_key, message, signature)[source]

Verify an Ed25519 signature.

The RFC 8032 verification equation is checked with the cofactor:

\[[8][S]B = [8]R + [8][k]A,\]

where k is the reduced SHA-512 challenge. Malformed encodings and non-canonical S values return False.

EXAMPLES:

sage: seed = bytes.fromhex('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')
sage: public_key = ed25519_public_key(seed)
sage: signature = ed25519_sign(seed, b'')
sage: ed25519_verify(public_key, b'', signature)
True
sage: ed25519_verify(public_key, b'bad', signature)
False
sage: import operator
sage: ed25519_verify(public_key, b'', signature[:-1] + bytes([operator.xor(signature[-1], 1)]))
False
>>> from sage.all import *
>>> seed = bytes.fromhex('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')
>>> public_key = ed25519_public_key(seed)
>>> signature = ed25519_sign(seed, b'')
>>> ed25519_verify(public_key, b'', signature)
True
>>> ed25519_verify(public_key, b'bad', signature)
False
>>> import operator
>>> ed25519_verify(public_key, b'', signature[:-Integer(1)] + bytes([operator.xor(signature[-Integer(1)], Integer(1))]))
False
seed = bytes.fromhex('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')
public_key = ed25519_public_key(seed)
signature = ed25519_sign(seed, b'')
ed25519_verify(public_key, b'', signature)
ed25519_verify(public_key, b'bad', signature)
import operator
ed25519_verify(public_key, b'', signature[:-1] + bytes([operator.xor(signature[-1], 1)]))