std/curve25519
import std/curve25519 to use it. Source: src/std/curve25519.iyi at v0.18.0.
import std/curve25519::{X25519, Ed25519, Ed25519PublicKey, CurveError} mine = X25519.public_key(private_key) # 32 bytes
secret = X25519.shared(private_key, their_public)! # or a CurveError
public = Ed25519.public_key(seed) # 32 bytes
signature = Ed25519.sign(seed, message) # 64 bytes
Ed25519.verify(public, message, signature) # true or false
key = Ed25519PublicKey.from_bytes(public)! # checked once, kept
key.verify(message, signature)Keys, seeds, messages and signatures are a String, whose bytes are taken, or an Array(UInt8); what comes back is a String of raw bytes. The caller brings every secret: a private key or a seed is 32 bytes from wherever the caller draws them. This module imports nothing past std/int and std/digest's SHA-512, so a package that signs or agrees on a key is not counted as reaching the operating system (iyi mod reach); Random::Secure.random_bytes(32) from std/random is where a fresh key comes from.
The field. An element is five 51-bit limbs in UInt64s, the products taken in UInt128 and folded back with 2^255 = 19 (mod p), as curve25519-donna-c64 and Go's crypto/internal/edwards25519/field do; inversion is Fermat's, the fixed chain of 254 squarings and 11 products for z^(p-2). Every operation on an element runs the same instructions whatever its value: no branch and no memory index depends on a secret. The two things that pick by a secret bit are a mask: the Montgomery ladder's swap (cswap) and the window lookup in a scalar multiplication, which reads all sixteen entries and keeps one with & and |.
X25519 is the RFC 7748 §5 ladder over the clamped scalar, 255 steps. The peer's u-coordinate has its top bit masked and is otherwise taken as it comes, non-canonical values too, as the RFC says it must be. shared refuses, as a CurveError, an all-zero shared secret (RFC 7748 §6.1: the peer sent a point of small order, and the secret is one the peer chose rather than agreed), and a peer key that is not 32 bytes.
Ed25519 is RFC 8032 §5.1, points in extended coordinates (X:Y:Z:T) with the complete addition formulas of §5.1.4. Signing multiplies the base point by secret scalars in constant time: 4-bit windows, each looked up from a table of sixteen multiples by masks. Verification works on public values only, so it skips zero windows and branches freely, and it is strict where RFC 8032 §5.1.7 is: a signature whose S is not below the group order L is refused (the S + L malleation), and so is a public key that is not a canonical encoding (a y of p or more, or x = 0 with the sign bit set) or not a point on the curve. The equation checked is the cofactorless one, [S]B = R + [k]A, as R' = [S]B - [k]A encoded and compared with the signature's R bytes: an encoding this module writes is always canonical, so the bytes are equal exactly when R decodes and is the same point, which is §5.1.7's decoding of R without a square root. The cofactored equation, [8][S]B = [8]R + [8][k]A, accepts more signatures (ones with a small-order component, which no signer following §5.1.6 makes), and RFC 8032 allows either. A public key of small order is not refused: the RFC does not ask it, and the identity's canonical encoding decodes. Set beside OpenSSL, through Python's cryptography, this verify agreed on 2,500 signatures good and bad (the gate's kinds) and is stricter in one place: OpenSSL takes the identity spelled with y = p + 1, or with x = 0 and the sign bit set, as a key, and §5.1.3 says decoding fails for both.
Measured on one x86_64 Linux machine, --release, the best of five runs of 200: a shared secret took 21 microseconds against cryptography's (OpenSSL's) 19, a verification 43 against 47, and a signature 68 against 17; a signature works the public key out from the seed again and multiplies the base point twice, by a table it builds each time, where OpenSSL keeps both. A plain build took 116, 228 and 522, the average of 100 or 200.
Refused with a sentence, as the caller's own misuse: a private key or a seed that is not 32 bytes. Data from outside is answered as a value: a peer key of the wrong size or an all-zero secret is a CurveError, and a public key or signature that is malformed, of the wrong size or non-canonical makes verify answer false.
CurveErrorstruct
What is wrong with a peer's key: message is the sentence.
pub struct CurveError
def initialize(message : String)
def to_s : String
endEd25519struct
Ed25519, RFC 8032 §5.1: the seed is the 32-byte private key; SHA-512 of it gives the secret scalar (its first half, clamped) and the prefix the per-message nonce is hashed from (its second half). Signing is deterministic: one seed and one message give one signature.
pub struct Ed25519
def initialize
# The 32-byte public key of *seed*, RFC 8032 §5.1.5.
def self.public_key(seed : String | Array(UInt8)) : String
# The 64-byte signature of *message* under *seed*, RFC 8032 §5.1.6:
# R = [r]B for r = SHA-512(prefix || M) mod L, then
# S = (r + SHA-512(R || A || M) * s) mod L. The public key A is worked
# out again from the seed, which costs a second multiplication of the
# base point; the two are each a fixed-window constant-time one.
def self.sign(seed : String | Array(UInt8), message : String | Array(UInt8)) : String
# Whether *signature* is *public_key*'s signature of *message*, RFC 8032
# §5.1.7 with the cofactorless equation (the header says why). False,
# not a panic, for a key or signature of the wrong size, a key that is
# not a canonical encoding of a point on the curve, and an S of L or
# more.
def self.verify(public_key : String | Array(UInt8), message : String | Array(UInt8), signature : String | Array(UInt8)) : Bool
endEd25519PublicKeystruct
An Ed25519 public key held as a value, as a certificate carries one (RFC 8410): the 32 bytes, checked once to be the canonical encoding of a point on the curve, so a key read from outside that is not one is refused where it is read rather than failing every signature later.
pub struct Ed25519PublicKey
def bytes : Array(UInt8)
# The key *bytes* encode, or a `CurveError`: not 32 bytes, or not a
# canonical encoding of a point on the curve (RFC 8032 §5.1.3).
def self.from_bytes(bytes : String | Array(UInt8)) : Ed25519PublicKey | CurveError
def initialize(bytes : Array(UInt8))
# Whether *signature* is this key's signature of *message*, as
# `Ed25519.verify` answers it.
def verify(message : String | Array(UInt8), signature : String | Array(UInt8)) : Bool
endX25519struct
X25519, RFC 7748 §5: the u-coordinate of a clamped scalar times a point on Curve25519. The private key is any 32 bytes; it is clamped here (bits 0 to 2 and 255 cleared, bit 254 set), so the caller need not.
pub struct X25519
def initialize
# The public key of *private_key*: its multiple of the base point, u = 9.
def self.public_key(private_key : String | Array(UInt8)) : String
# The secret *private_key* shares with the owner of *peer_public*. A
# peer key that is not 32 bytes, and a secret of all zeros (a peer key of
# small order, RFC 7748 §6.1), are a `CurveError`. Whether the secret was
# zero is the one thing that leaves the ladder by a branch, and it says
# nothing of the private key: which keys give zero depends on the peer's.
def self.shared(private_key : String | Array(UInt8), peer_public : String | Array(UInt8)) : String | CurveError
endError for CurveErrorimpl
impl Error for CurveError
def message : String
end
X25519 (RFC 7748) and Ed25519 (RFC 8032), written in iyi on the field of integers modulo 2^255 - 19.