Skip to content

crypto.ecdsa #

ecdsa

ecdsa module for V language. Its currently (expanded) to support the following curves:

  • NIST P-256 curve, commonly referred as prime256v1 or secp256r1
  • NIST P-384 curve, commonly referred as secp384r1
  • NIST P-521 curve, commonly referred as secp521r1
  • A famous Bitcoin curve, commonly referred as secp256k1

Backends

crypto.ecdsa has two independent, interchangeable backends -- one is compiled in, never both, selected by the presence of the -d use_openssl build flag:

  • mbedTLS (default, -d use_openssl absent): uses the mbedTLS library already vendored under thirdparty/mbedtls for this repo's own TLS 1.3/ QUIC stack, so it needs no external dependency to build or run. This is the backend most callers get automatically.
  • OpenSSL (opt-in, -d use_openssl present): uses the system's OpenSSL development headers/libraries. Requires OpenSSL to be installed and discoverable by the C compiler.

Both backends implement the same public API (generate_key, PrivateKey.new, .sign, .public_key, .derive_shared_secret, .equal, .free, PublicKey.verify, .equal, .free, .uncompressed_bytes, PublicKey.from_uncompressed_bytes), so calling code compiles unchanged regardless of which backend is selected.

The default mbedTLS backend does not (yet) implement everything the OpenSSL backend does -- these return a clear, informative error instead of silently behaving differently, and are only available under -d use_openssl today:

  • new_key_from_seed -- OpenSSL's current implementation performs no range check against the curve order; mbedTLS's closest equivalent validates the resulting scalar is in [1, curve_order-1] and rejects it otherwise. A seed that "works" under -d use_openssl could be rejected here, so this is a genuine behavioral divergence, not just missing plumbing, and needs its own design pass before porting.
  • PrivateKey.sign()/sign_with_options() with hash_config: .with_custom_hash -- mbedTLS's deterministic-nonce signing (RFC 6979, compiled in by default) needs a real digest-algorithm identifier to seed its internal HMAC-DRBG, and an arbitrary caller-supplied hash.Hash carries no such identifier. Verifying a signature with a custom hash is unaffected (mbedTLS's own verify call takes no digest-algorithm parameter at all).
  • pubkey_from_bytes, pubkey_from_string, privkey_from_string (PEM/DER loading) -- format-parsing work not yet ported; no caller in this repo currently needs them on the default backend.

Example

import crypto.ecdsa

fn main() {
    // create default NIST P-256 secp256r1 curve key pair. If you wish to generate another curve,
    // use: `pbkey, pvkey := ecdsa.generate_key(nid: .secp521r1)!` instead.
    pbkey, pvkey := ecdsa.generate_key()!

    message_tobe_signed := 'Hello ecdsa'.bytes()
    // create a signature with the recommended hash
    signature := pvkey.sign(message_tobe_signed)!

    // verify the message with the signature
    verified := pbkey.verify(message_tobe_signed, signature)!
    dump(verified) // should be true

    // free allocated keys when you have done with your work.
    pbkey.free()
    pvkey.free()
}

Constants #

const C.MBEDTLS_ECP_DP_SECP256R1 int

Curve/group identifiers (mbedtls_ecp_group_id, ecp.h).

const C.MBEDTLS_ECP_DP_SECP384R1 int
const C.MBEDTLS_ECP_DP_SECP521R1 int
const C.MBEDTLS_ECP_DP_SECP256K1 int // Point format (mbedtls_ecp_point_write_binary's `format` param, ecp.h).
const C.MBEDTLS_ECP_PF_UNCOMPRESSED int // Message-digest type identifiers (mbedtls_md_type_t, md.h) -- only the
// three this module's own curve->hash mapping needs.
const C.MBEDTLS_MD_SHA256 int
const C.MBEDTLS_MD_SHA384 int
const C.MBEDTLS_MD_SHA512 int // Upper bounds for stack/heap buffer sizing (ecp.h/ecdsa.h): the largest
// possible uncompressed SEC1 point (1 tag byte + 2*66 for P-521) and the
// largest possible DER Ecdsa-Sig-Value for any curve this module supports.
const C.MBEDTLS_ECP_MAX_PT_LEN int
const C.MBEDTLS_ECDSA_MAX_LEN int

fn generate_key #

fn generate_key(opt CurveOptions) !(PublicKey, PrivateKey)

generate_key generates a new key pair. If opt was not provided, its default to prime256v1 curve. If you want another curve, use pubkey, pivkey := ecdsa.generate_key(nid: .secp384r1)! instead.

fn new_key_from_seed #

fn new_key_from_seed(seed []u8, opt CurveOptions) !PrivateKey

new_key_from_seed is NOT implemented for the default mbedTLS backend: unlike OpenSSL's current implementation (raw BN_bin2bn(seed), no range check), mbedTLS's closest equivalent validates the resulting scalar is in [1, curve_order-1] and rejects it otherwise -- a seed that deterministically "works" under -d use_openssl today could be rejected here. That's a real behavioral divergence, not just missing plumbing, and needs its own design pass (reduce mod n first? accept the stricter rejection and document it?) before porting -- not silently reimplemented as if the two were equivalent. Build with -d use_openssl if you need this today.

fn privkey_from_string #

fn privkey_from_string(s string) !PrivateKey

privkey_from_string loads a PrivateKey from a PEM-formatted string. Not implemented for the default mbedTLS backend -- see this file's own module doc comment.

fn pubkey_from_bytes #

fn pubkey_from_bytes(bytes []u8) !PublicKey

pubkey_from_bytes loads a PublicKey from DER-encoded bytes in der. Not implemented for the default mbedTLS backend -- see this file's own module doc comment.

fn pubkey_from_string #

fn pubkey_from_string(s string) !PublicKey

pubkey_from_string loads a PublicKey from a PEM-formatted string. Not implemented for the default mbedTLS backend -- see this file's own module doc comment.

fn HashConfig.from #

fn HashConfig.from[W](input W) !HashConfig

fn KeyFlag.from #

fn KeyFlag.from[W](input W) !KeyFlag

fn Nid.from #

fn Nid.from[W](input W) !Nid

fn PrivateKey.new #

fn PrivateKey.new(opt CurveOptions) !PrivateKey

PrivateKey.new creates a new key pair. By default, it would create a prime256v1 based key. Dont forget to call .free() after finish with your key.

fn PublicKey.from_uncompressed_bytes #

fn PublicKey.from_uncompressed_bytes(bytes []u8, opt CurveOptions) !PublicKey

PublicKey.from_uncompressed_bytes reconstructs a public key (no private component) from an uncompressed EC point 0x04 || X || Y, as received on the wire in a TLS 1.3 key_share extension. opt.nid must match the curve the peer actually used (v1 callers of net.quic only ever use .prime256v1 here, since X25519 is handled entirely by crypto.x25519 instead).

enum HashConfig #

enum HashConfig {
	with_recommended_hash
	with_no_hash
	with_custom_hash
}

HashConfig is an enumeration of the possible options for key signing (verifying).

enum Nid #

enum Nid {
	prime256v1
	secp384r1
	secp521r1
	secp256k1
}

Nid is an enumeration of the supported curves. Independent of ecdsa_d_use_openssl.v's own Nid (only one of the two files is ever compiled at once, so there's no conflict) -- this one carries no C.* value binding, since mbedTLS's curve identifiers are a different integer space (mbedtls_ecp_group_id) than OpenSSL's NIDs.

struct CurveOptions #

@[params]
struct CurveOptions {
pub mut:
	// default to NIST P-256 curve
	nid Nid = .prime256v1
	// by default, allow arbitrary size of seed bytes as key.
	// Set it to `true` when you need fixed size, using the curve key size.
	// Its main purposes is to support the `.new_key_from_seed` call.
	fixed_size bool
}

CurveOptions represents configuration options to drive keypair generation.

struct PrivateKey #

struct PrivateKey {
	// mbedtls_ecdsa_context is mbedTLS's own typedef of mbedtls_ecp_keypair --
	// one opaque type covers both PrivateKey (grp+d+Q all set) and PublicKey
	// (grp+Q set, d left zero), mirroring how the OpenSSL backend reuses one
	// &C.EVP_PKEY for both.
	ctx &C.mbedtls_ecdsa_context
mut:
	// nid records which curve this key uses -- mbedTLS's raw ecp/ecdsa/ecdh
	// API (unlike OpenSSL's self-describing EVP_PKEY) needs the caller to
	// remember and re-supply the curve id on several calls.
	nid Nid
	// ks_flag with .flexible value allowing
	// flexible-size seed bytes as key.
	// When it is `.fixed`, it will use the underlying key size.
	ks_flag KeyFlag = .fixed
	// ks_size stores size of the seed bytes when ks_flag was .flexible.
	// You should set it to a non zero value
	ks_size int
}

PrivateKey represents ECDSA private key. Actually its a key pair, contains private key and public key parts.

fn (PrivateKey) sign #

fn (pv PrivateKey) sign(message []u8, opt SignerOpts) ![]u8

sign performs signing the message with the options. By default options, it will perform hashing before signing the message.

hash_config: .with_custom_hash is NOT implemented for the default mbedTLS backend: mbedtls_ecdsa_write_signature's RFC 6979 deterministic- nonce path (compiled in for this build) needs a real mbedtls_md_type_t to seed its internal HMAC-DRBG, and hash.Hash (an arbitrary caller- supplied hasher) carries no such identifier -- passing MBEDTLS_MD_NONE through, which an earlier version of this file's own doc comments claimed was harmless, is actually rejected outright by mbedtls_md_info_from_type()/mbedtls_ecdsa_sign_det_restartable(). Verify() has no equivalent problem (mbedtls_ecdsa_read_signature takes no md_alg parameter at all), so only signing is restricted here. Build with -d use_openssl if you need custom-hash signing today.

fn (PrivateKey) sign_with_options #

deprecated: use PrivateKey.sign() instead
fn (pv PrivateKey) sign_with_options(message []u8, opt SignerOpts) ![]u8

sign_with_options signs message with the options. It will be deprecated, Use PrivateKey.sign() instead.

fn (PrivateKey) bytes #

fn (pv PrivateKey) bytes() ![]u8

bytes represent private key as bytes.

fn (PrivateKey) seed #

deprecated: use PrivateKey.bytes() instead
fn (pv PrivateKey) seed() ![]u8

seed gets the seed (private key bytes). It will be deprecated. Use PrivateKey.bytes() instead.

fn (PrivateKey) public_key #

fn (pv PrivateKey) public_key() !PublicKey

public_key gets the PublicKey from private key.

fn (PrivateKey) derive_shared_secret #

fn (pv PrivateKey) derive_shared_secret(peer PublicKey) ![]u8

derive_shared_secret performs ECDH key agreement between this private key and a peer's public key, returning the raw shared secret (the X-coordinate of the resulting curve point only, per SEC1 — this is what mbedTLS's mbedtls_ecdh_compute_shared already returns, and is exactly what TLS 1.3's key schedule expects as ECDHE input; it is NOT the full uncompressed point). Both keys must use the same curve.

fn (PrivateKey) equal #

fn (priv_key PrivateKey) equal(other PrivateKey) bool

equal compares two private keys was equal. Delegates to the public components' equality, matching OpenSSL's own EVP_PKEY_eq semantics of comparing public (not raw private-scalar) material.

fn (PrivateKey) free #

fn (pv &PrivateKey) free()

free clears out allocated memory for PrivateKey. Dont use PrivateKey after calling .free()

Double-free behavior is backend-dependent: -d use_openssl aborts the process on a true double-free (OpenSSL's EVP_PKEY_free); this default mbedTLS backend's mbedtls_ecdsa_free is documented safe on an already-freed/zeroed context (a silent no-op). Never rely on either -- call .free() exactly once.

struct PublicKey #

struct PublicKey {
	ctx &C.mbedtls_ecdsa_context
mut:
	nid Nid
}

PublicKey represents ECDSA public key for verifying message.

fn (PublicKey) bytes #

fn (pbk PublicKey) bytes() ![]u8

bytes gets the public key as an uncompressed EC point -- identical to PublicKey.uncompressed_bytes() (ecdsa_notd_use_openssl.v), which IS implemented for this backend; delegates rather than duplicating the mbedTLS call sequence.

fn (PublicKey) equal #

fn (pub_key PublicKey) equal(other PublicKey) bool

equal compares two public keys was equal.

fn (PublicKey) free #

fn (pb &PublicKey) free()

free clears out allocated memory for PublicKey. Dont use PublicKey after calling .free()

See PrivateKey.free()'s own doc comment for this backend's double-free behavior (a silent no-op here, unlike -d use_openssl's abort).

fn (PublicKey) uncompressed_bytes #

fn (pb PublicKey) uncompressed_bytes() ![]u8

uncompressed_bytes returns the public key as an uncompressed EC point, 0x04 || X || Y (SEC1 §2.3.3) — the wire format TLS 1.3's key_share extension uses for the secp256r1 (P-256) group (RFC 8446 §4.2.8.2).

fn (PublicKey) verify #

fn (pb PublicKey) verify(message []u8, sig []u8, opt SignerOpts) !bool

verify verifies a message with the signature are valid with public key provided . You should provide it with the same SignerOpts used with the .sign() call. or verify would fail (false).

struct SignerOpts #

@[params]
struct SignerOpts {
pub mut:
	// default to .with_recommended_hash
	hash_config HashConfig = .with_recommended_hash
	// make sense when HashConfig != with_recommended_hash
	allow_smaller_size bool
	allow_custom_hash  bool
	// set to non-nil if allow_custom_hash was true
	custom_hash &hash.Hash = unsafe { nil }
}

SignerOpts represents configuration options to drive signing and verifying process.