iyi

Standard library, 0.18.0

std/p256

ECDSA signatures made on P-256 (FIPS 186-5, SEC 1, RFC 6979), and its Diffie-Hellman (ECDH, SEC 1 section 3.3.1), written in iyi, in constant time.

import std/p256 to use it. Source: src/std/p256.iyi at v0.18.0.

  import std/p256::{P256PrivateKey}
  import std/ecdsa::{EcdsaHash, EcdsaError}
  key = P256PrivateKey.from_pkcs8(der).or_panic     # or from_sec1, from_bytes
  signature = key.sign(EcdsaHash::SHA256, message)  # DER Ecdsa-Sig-Value
  key.public_key.verify(EcdsaHash::SHA256, message, signature)  # true
  key.public_point                    # 0x04, x, y: what a key share sends
  key.shared_secret(peer_point).or_panic  # x of [d]Q, 32 bytes

std/ecdsa checks signatures on std/big's arithmetic, which takes as long as its numbers make it: fine for public values, and a leak for a private key, whose bits would show in how long each signature takes. This module is the other half for P-256 alone, the curve a TLS server's certificate is most often on and the one browsers take.

Constant time, the way std/curve25519 is: nothing branches on a secret and no memory index depends on one. Numbers modulo p (the field) and modulo n (the group's order) are four 64-bit limbs in Montgomery form, products taken in UInt128, the final subtraction chosen by a mask. The points are projective, added and doubled by the complete formulas of Renes, Costello and Batina (2016, algorithms 4 and 6 for a = -3), which have no case to branch on - not the identity, not a point added to itself. [k]P runs 64 windows of four bits, each four doublings and one addition of a table entry read whole: all sixteen entries are read and the one wanted kept by masks. Inversion is Fermat's, x^(m - 2), whose exponent is public and fixed.

The nonce is RFC 6979's: HMAC over the hash the signature names, from the private key and the message's hash, so one key and one message give one signature and no random source is trusted with the key. Where the RFC loops - a candidate k of n or more, an r or s of zero, each about 2^-32 likely - the loop shows that it ran again, and nothing else.

ECDH takes the peer's point uncompressed (0x04, x, y) and refuses one that is not on the curve, or whose coordinates are not below p, before any arithmetic with the private key: an invalid-curve point would draw d out a residue at a time. The answer is x of [d]Q, as TLS 1.3's secp256r1 shares (RFC 8446 section 7.4.2) and every other user of SEC 1 take it.

A private key is 32 bytes, d in [1, n - 1]: from_bytes, or read from what OpenSSL and others write - PKCS #8's OneAsymmetricKey (RFC 5958, RFC 5480's id-ecPublicKey with prime256v1) or SEC 1's ECPrivateKey (RFC 5915), whose public key, when it carries one, must be d's.

Pure computation: std/digest, std/crypto's HMAC, std/asn1, and std/ecdsa for the public key and its checking.

P256PrivateKeystruct

A P-256 private key, and the public key that goes with it.

std/p256iyi doc src/std/p256.iyi
pub struct P256PrivateKey
  # The key whose d is *bytes*, 32 big-endian, or why it is not one: not
  # 32 bytes, or not in [1, n - 1].
  def self.from_bytes(bytes : String | Array(UInt8)) : P256PrivateKey | EcdsaError
  # A PKCS #8 OneAsymmetricKey (RFC 5958) holding a P-256 key: version 0 or
  # 1, the algorithm id-ecPublicKey with prime256v1, and an ECPrivateKey
  # inside the privateKey OCTET STRING.
  def self.from_pkcs8(der : Array(UInt8)) : P256PrivateKey | EcdsaError
  # SEC 1's ECPrivateKey (RFC 5915): version 1, the 32-byte private key,
  # then optionally the curve ([0], which must be prime256v1) and the
  # public key ([1], which must be the private key's).
  def self.from_sec1(der : Array(UInt8)) : P256PrivateKey | EcdsaError
  def initialize(d : P256Num, public_key : EcdsaPublicKey, public_point : Array(UInt8))
  def public_key : EcdsaPublicKey
  # The public key's point, uncompressed: 0x04, then x and y, 32 bytes each.
  def public_point : Array(UInt8)
  # ECDH: x of [d]Q for the peer's point Q, 32 bytes big-endian - or why
  # *peer* is not a point of P-256 (checked before d is used).
  def shared_secret(peer : Array(UInt8)) : Array(UInt8) | EcdsaError
  # The signature of *message* under *hash*: the DER of Ecdsa-Sig-Value,
  # SEQUENCE { r INTEGER, s INTEGER }, as X.509 and TLS 1.3 carry it. The
  # nonce is RFC 6979's, so the same key and message sign alike.
  def sign(hash : EcdsaHash, message : String | Array(UInt8)) : Array(UInt8)
end