Atex.Crypto (atex v0.9.1)

View Source

Cryptographic operations for the AT Protocol.

Supports the two elliptic curves required by atproto:

  • p256 - NIST P-256 / secp256r1 (JWK curve "P-256")
  • k256 - secp256k1 (JWK curve "secp256k1")

Key encoding

Public keys are represented as JOSE.JWK structs throughout this module. The multikey / did:key encoding used in DID documents is the canonical external representation: a base58btc-encoded (multibase z prefix) binary consisting of a varint multicodec prefix followed by the 33-byte compressed EC point.

Signing and verification

Signatures are DER-encoded ECDSA byte sequences as produced by Erlang's :public_key application. All produced signatures are normalised to the low-S form required by the atproto specification.

Summary

Types

A multikey-encoded public key string, optionally prefixed with did:key:.

Functions

Decodes a multikey or did:key string into a JOSE.JWK public key struct.

Decodes a legacy (pre-Multikey) atproto verification method public key into a JOSE.JWK.

Encodes a JOSE.JWK public key as a multikey string.

Signs a payload with a private key, returning a low-S DER-encoded ECDSA signature.

Verifies a DER-encoded ECDSA signature against a payload and a public key.

Types

multikey()

@type multikey() :: String.t()

A multikey-encoded public key string, optionally prefixed with did:key:.

Examples:

  • "zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo" (P-256 multikey)
  • "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc" (K-256 did:key)

Functions

decode_did_key(input)

@spec decode_did_key(multikey()) :: {:ok, JOSE.JWK.t()} | {:error, term()}

Decodes a multikey or did:key string into a JOSE.JWK public key struct.

Accepts both bare multikey strings (e.g. "z...") and full did:key URIs (e.g. "did:key:z..."). Supports P-256 (p256-pub) and secp256k1 (secp256k1-pub) keys.

Examples

iex> {:ok, jwk} = Atex.Crypto.decode_did_key("zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo")
iex> match?(%JOSE.JWK{}, jwk)
true

iex> {:ok, jwk} = Atex.Crypto.decode_did_key("did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc")
iex> match?(%JOSE.JWK{}, jwk)
true

iex> Atex.Crypto.decode_did_key("not-a-valid-key")
{:error, :invalid_multikey}

decode_legacy_multibase(type, multibase)

@spec decode_legacy_multibase(type :: String.t(), multibase :: String.t()) ::
  {:ok, JOSE.JWK.t()} | {:error, term()}

Decodes a legacy (pre-Multikey) atproto verification method public key into a JOSE.JWK.

Legacy verificationMethod entries encode the public key as an uncompressed EC point (65 bytes: 0x04 || x || y) in base58btc multibase, without any multicodec prefix. The curve is identified by the type field of the verification method rather than a multicodec byte.

Accepted type values:

  • "EcdsaSecp256r1VerificationKey2019" - P-256 / secp256r1
  • "EcdsaSecp256k1VerificationKey2019" - secp256k1

Examples

iex> {:ok, jwk} = Atex.Crypto.decode_legacy_multibase(
...>   "EcdsaSecp256k1VerificationKey2019",
...>   "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR"
...> )
iex> match?(%JOSE.JWK{}, jwk)
true

iex> Atex.Crypto.decode_legacy_multibase("UnknownType", "zQYEBzXeuTM")
{:error, :unsupported_curve}

encode_did_key(jwk, opts \\ [])

@spec encode_did_key(
  JOSE.JWK.t(),
  keyword()
) :: {:ok, multikey()} | {:error, term()}

Encodes a JOSE.JWK public key as a multikey string.

Accepts both public and private key JWKs; the private component is discarded. Supports P-256 and secp256k1 keys.

Options

  • :as_did_key - when true, prepends the did:key: URI scheme to the returned string. Defaults to false.

Examples

iex> jwk = JOSE.JWK.generate_key({:ec, "P-256"})
iex> {:ok, mk} = Atex.Crypto.encode_did_key(jwk)
iex> String.starts_with?(mk, "z")
true

iex> jwk = JOSE.JWK.generate_key({:ec, "P-256"})
iex> {:ok, mk} = Atex.Crypto.encode_did_key(jwk, as_did_key: true)
iex> String.starts_with?(mk, "did:key:z")
true

generate_k256()

generate_p256()

sign(payload, private_key)

@spec sign(payload :: binary(), private_key :: JOSE.JWK.t()) ::
  {:ok, binary()} | {:error, term()}

Signs a payload with a private key, returning a low-S DER-encoded ECDSA signature.

The payload is hashed with SHA-256 internally before signing, matching the atproto signing convention.

Examples

iex> jwk = JOSE.JWK.generate_key({:ec, "P-256"})
iex> {:ok, sig} = Atex.Crypto.sign("hello", jwk)
iex> is_binary(sig)
true

verify(payload, signature, public_key)

@spec verify(payload :: binary(), signature :: binary(), public_key :: JOSE.JWK.t()) ::
  :ok | {:error, term()}

Verifies a DER-encoded ECDSA signature against a payload and a public key.

The payload is hashed with SHA-256 internally before verification, matching the atproto signing convention.

Returns :ok on success, or {:error, :invalid_signature} if the signature does not match.

Examples

iex> jwk = JOSE.JWK.generate_key({:ec, "P-256"})
iex> {:ok, sig} = Atex.Crypto.sign("hello", jwk)
iex> Atex.Crypto.verify("hello", sig, JOSE.JWK.to_public(jwk))
:ok

iex> jwk = JOSE.JWK.generate_key({:ec, "P-256"})
iex> {:ok, sig} = Atex.Crypto.sign("hello", jwk)
iex> Atex.Crypto.verify("tampered", sig, JOSE.JWK.to_public(jwk))
{:error, :invalid_signature}