std/signature/p256

std/signature/src/p256.trb

ECDSA over P-256 with SHA-256 (FIPS 186-5 section 6.4.2, the curve of SP 800-186 section 3.2.1.3): what JOSE and WebAuthn call ES256. Only verification is here - P256PublicKey verifies, P256Signature is r and s - so nothing it computes is secret, and it takes variable time.

A point is computed in projective coordinates (X : Y : Z) with the complete formulas of Renes, Costello and Batina ("Complete addition formulas for prime order elliptic curves", 2016, algorithms 4 and 6 for a = -3): they add any two points - the point at infinity and a point to itself included - without a case, which leaves no special case for a crafted key or signature to reach. The numbers are those of ./montgomery.

type P256PublicKey

type P256PublicKey with Show, Equals, Hash, TryFrom<String, SignatureError>

A P-256 public key: the point Q of the curve that verifies, read from the uncompressed encoding of SEC 1 section 2.3.3 - the byte 4, then x and y as 32 bytes each, big endian - or from the two coordinates (a JSON Web Key's and a COSE key's x and y). Made only by factories that check that both coordinates are below p and that the point is on the curve, so a key is always a point of the group: the cofactor of P-256 is 1, and every point of the curve other than infinity, which has no encoding, is in the group of the base point.

Examples

const text = "0460fed4ba255a9d31c961eb74c6356d68c049b8923b61fa6ce669622e60f29fb6"
  + "7903fe1008b8bc99a41ae9e95628bc64f2f1b20c2d7e9f5177a3c294d4462299"
const key = P256PublicKey.tryFrom text
print key.isOk()

Pitfalls

  • The compressed encoding of SEC 1 (33 bytes, starting with 2 or 3) is not read: it fails with SignatureError.WrongLength. WebAuthn and JSON Web Keys give both coordinates.

fn fromBytes

static fn fromBytes(bytes: Bytes): Result<Self, SignatureError>

The key the 65 bytes of the uncompressed encoding stand for: 4, x, y.

Errors

fn fromCoordinates

static fn fromCoordinates(x: Bytes, y: Bytes): Result<Self, SignatureError>

The key of the two coordinates, 32 bytes each, big endian: how a JSON Web Key (RFC 7518 section 6.2.1) and a COSE key (RFC 9053 section 7.1.1) write it.

Errors

fn tryFrom

static fn tryFrom(value: String): Result<Self, SignatureError>

The key 130 hexadecimal digits of either case write: the uncompressed encoding.

Errors

fn bytes

fn bytes(): Bytes

The 65 bytes of the uncompressed encoding: 4, x, y.

fn hex

fn hex(): String

The 65 bytes as 130 lowercase hexadecimal digits.

fn show

fn show(): String

The same as P256PublicKey.hex.

fn equals

fn equals(other: Self): Bool

Whether the two keys are the same 65 bytes.

fn hash

fn hash(): Int

The hash of the 65 bytes, so that a key is a key of a Map or a member of a Set by its bytes alone.

fn verifies

fn verifies(message: Bytes, signature: P256Signature): Bool

Whether signature is this key's ECDSA signature of message with SHA-256 (FIPS 186-5 section 6.4.2): with e the SHA-256 of the message, w the inverse of s modulo n, u1 = e w and u2 = r w, whether the point u1 G + u2 Q is not infinity and has an x that is r modulo n. The x is compared without a division: X / Z is r where X is r Z, or r + n where that is below p.

It takes variable time - everything it reads is public - and about 6,000 multiplications modulo p.

Pitfalls

  • A signature (r, s) verifies, and so does (r, n - s): ECDSA signatures are malleable, as FIPS 186-5, WebAuthn and OpenSSL accept both. A protocol that tells signatures apart by their bytes - a replay cache, a transaction id - needs its own rule, such as only ever storing the message.

extend String with From<P256PublicKey>

extend String with From<P256PublicKey>

A public key as its 130 lowercase hexadecimal digits: the way back of P256PublicKey.tryFrom.

fn from

static fn from(value: P256PublicKey): Self

type P256Signature

type P256Signature with Show, TryFrom<String, SignatureError>

A P-256 ECDSA signature: the two scalars r and s, each from 1 to n - 1. As bytes it is r and then s, 32 bytes each, big endian - the form of IEEE P1363 that JOSE's ES256 uses (RFC 7518 section 3.4) - and as text the 128 lowercase hexadecimal digits of those bytes; P256Signature.fromDer reads the DER that WebAuthn, X.509 and OpenSSL use.

Examples

const text = "efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716"
  + "f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8"
print P256Signature.tryFrom(text).isOk()

fn fromBytes

static fn fromBytes(bytes: Bytes): Result<Self, SignatureError>

The signature 64 bytes are: r, then s.

Errors

fn fromDer

static fn fromDer(bytes: Bytes): Result<Self, SignatureError>

The signature its DER encoding is (RFC 3279 section 2.2.3): a SEQUENCE of the two INTEGERs r and s. Only the one strict encoding is read - lengths in their short form, each integer positive and in its fewest bytes, and nothing after the sequence - so that a signature has one encoding and a lenient reading leaves nothing to vary.

Errors

fn tryFrom

static fn tryFrom(value: String): Result<Self, SignatureError>

The signature 128 hexadecimal digits of either case write: r, then s.

Errors

fn bytes

fn bytes(): Bytes

The 64 bytes: r, then s.

fn hex

fn hex(): String

The 64 bytes as 128 lowercase hexadecimal digits.

fn show

fn show(): String

The same as P256Signature.hex.

extend String with From<P256Signature>

extend String with From<P256Signature>

A signature as its 128 lowercase hexadecimal digits: the way back of P256Signature.tryFrom.

fn from

static fn from(value: P256Signature): Self