Header menu logo FSharp.ATProto

Cryptography

The AT Protocol uses ECDSA with two elliptic curves: P-256 (NIST) and K-256 (secp256k1). The FSharp.ATProto.Crypto package handles key generation, signing, verification, and encoding to the did:key format used in DID documents.

P-256 uses the native .NET ECDsa API. K-256 uses BouncyCastle, since .NET does not support secp256k1 natively.

Algorithm

The Algorithm discriminated union selects a curve:

type Algorithm = P256 | K256

All functions that deal with keys or signatures take an Algorithm to identify the curve. Use Algorithm.P256 for P-256 and Algorithm.K256 for secp256k1.

Key Types

type PublicKey =
    { Algorithm : Algorithm
      CompressedBytes : byte[] }  // 33 bytes: 0x02/0x03 prefix + 32-byte X

type KeyPair =
    { Algorithm : Algorithm
      PrivateKeyBytes : byte[]        // 32 bytes
      CompressedPublicKey : byte[] }  // 33 bytes

Key Management

The Keys module provides generation, import, and decompression:

open FSharp.ATProto.Crypto

// Generate a new key pair
let keyPair = Keys.generate Algorithm.P256

// Import from raw private key bytes (32 bytes)
let imported = Keys.importPrivateKey Algorithm.K256 somePrivateBytes

// Import a compressed public key (33 bytes) -- returns Result
match Keys.importPublicKey Algorithm.P256 compressedBytes with
| Ok pubKey ->
    // Decompress to 65-byte uncompressed form (0x04 + X + Y)
    let uncompressed = Keys.decompress pubKey
    printfn "Uncompressed: %d bytes" uncompressed.Length
| Error msg -> printfn "Import failed: %s" msg

// Extract the public key from a key pair
let pub = Keys.publicKey keyPair

Signing and Verification

The Signing module produces 64-byte compact ECDSA signatures (r || s, 32 bytes each) with low-S normalization as required by the AT Protocol. Data is hashed with SHA-256 internally.

let data = System.Text.Encoding.UTF8.GetBytes "hello"

// Sign -- returns 64-byte compact signature
let signature = Signing.sign keyPair2 data

// Verify -- rejects high-S signatures
let isValid = Signing.verify (Keys.publicKey keyPair2) data signature
// true

Signing.verify returns false for invalid, high-S, or DER-encoded signatures. It never throws.

Multikey and did:key Encoding

The Multikey module encodes and decodes public keys in the did:key format used by AT Protocol DID documents. Keys are serialized as base58btc with a multicodec prefix (P-256: 0x1200, K-256: 0xe7).

// Encode as did:key (e.g. "did:key:zDnae...")
let didKey = Multikey.keyPairToDid keyPair3

// Encode just the multibase portion (e.g. "zDnae...")
let multibase = Multikey.encodeMultibase (Keys.publicKey keyPair3)

// Decode a did:key string back to a PublicKey
match Multikey.decodeDid didKey with
| Ok pubKey -> printfn "Algorithm: %A" pubKey.Algorithm
| Error msg -> printfn "Invalid did:key: %s" msg

// Decode a multibase string
match Multikey.decodeMultibase multibase with
| Ok pubKey -> printfn "Got key with %d bytes" pubKey.CompressedBytes.Length
| Error msg -> printfn "Decode failed: %s" msg

Base58

The Base58 module provides standalone base58btc encoding and decoding using the Bitcoin alphabet. This is used internally by Multikey but is available for direct use if needed.

let encoded = Base58.encode [| 0uy; 1uy; 2uy |]
// "12c"

match Base58.decode encoded with
| Ok bytes -> printfn "Decoded %d bytes" bytes.Length
| Error msg -> printfn "Invalid base58: %s" msg
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.ATProto
namespace FSharp.ATProto.Syntax
namespace FSharp.ATProto.Crypto
type Algorithm = | P256 | K256
type PublicKey = { Algorithm: Algorithm CompressedBytes: byte array }
Multiple items
val byte: value: 'T -> byte (requires member op_Explicit)

--------------------
type byte = System.Byte

--------------------
type byte<'Measure> = byte
type KeyPair = { Algorithm: Algorithm PrivateKeyBytes: byte array CompressedPublicKey: byte array }
val somePrivateBytes: byte array
module Unchecked from Microsoft.FSharp.Core.Operators
val defaultof<'T> : 'T
val compressedBytes: byte array
val keyPair: KeyPair
module Keys from FSharp.ATProto.Crypto
<summary> Key management for P-256 and secp256k1 curves. </summary>
val generate: algorithm: Algorithm -> KeyPair
<summary> Generate a new key pair for the given algorithm. </summary>
type Algorithm = | P256 | K256
<summary> The elliptic curve algorithm used for a key. </summary>
Multiple items
union case Algorithm.P256: Algorithm

--------------------
union case Algorithm.P256: Algorithm
val imported: KeyPair
val importPrivateKey: algorithm: Algorithm -> privateKeyBytes: byte array -> KeyPair
<summary> Import a private key from raw bytes. </summary>
Multiple items
union case Algorithm.K256: Algorithm

--------------------
union case Algorithm.K256: Algorithm
val importPublicKey: algorithm: Algorithm -> compressedBytes: byte array -> Result<PublicKey,string>
<summary> Import a compressed public key (33 bytes). </summary>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val pubKey: PublicKey
val uncompressed: byte array
val decompress: key: PublicKey -> byte array
<summary> Decompress a 33-byte compressed key to 65-byte uncompressed (0x04 + X + Y). </summary>
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
property System.Array.Length: int with get
<summary>Gets the total number of elements in all the dimensions of the <see cref="T:System.Array" />.</summary>
<exception cref="T:System.OverflowException">The array is multidimensional and contains more than <see cref="F:System.Int32.MaxValue">Int32.MaxValue</see> elements.</exception>
<returns>The total number of elements in all the dimensions of the <see cref="T:System.Array" />; zero if there are no elements in the array.</returns>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val msg: string
val pub: PublicKey
val publicKey: keyPair: KeyPair -> PublicKey
<summary> Get the public key from a key pair. </summary>
val keyPair2: KeyPair
val data: byte array
namespace System
namespace System.Text
type Encoding = interface ICloneable member Clone: unit -> obj member Equals: value: obj -> bool member GetByteCount: chars: nativeptr<char> * count: int -> int + 5 overloads member GetBytes: chars: nativeptr<char> * charCount: int * bytes: nativeptr<byte> * byteCount: int -> int + 7 overloads member GetCharCount: bytes: nativeptr<byte> * count: int -> int + 3 overloads member GetChars: bytes: nativeptr<byte> * byteCount: int * chars: nativeptr<char> * charCount: int -> int + 4 overloads member GetDecoder: unit -> Decoder member GetEncoder: unit -> Encoder member GetHashCode: unit -> int ...
<summary>Represents a character encoding.</summary>
property System.Text.Encoding.UTF8: System.Text.Encoding with get
<summary>Gets an encoding for the UTF-8 format.</summary>
<returns>An encoding for the UTF-8 format.</returns>
System.Text.Encoding.GetBytes(s: string) : byte array
System.Text.Encoding.GetBytes(chars: char array) : byte array
System.Text.Encoding.GetBytes(chars: System.ReadOnlySpan<char>, bytes: System.Span<byte>) : int
System.Text.Encoding.GetBytes(s: string, index: int, count: int) : byte array
System.Text.Encoding.GetBytes(chars: char array, index: int, count: int) : byte array
System.Text.Encoding.GetBytes(chars: nativeptr<char>, charCount: int, bytes: nativeptr<byte>, byteCount: int) : int
System.Text.Encoding.GetBytes(s: string, charIndex: int, charCount: int, bytes: byte array, byteIndex: int) : int
System.Text.Encoding.GetBytes(chars: char array, charIndex: int, charCount: int, bytes: byte array, byteIndex: int) : int
val signature: byte array
module Signing from FSharp.ATProto.Crypto
<summary> ECDSA signing and verification with low-S normalization as required by AT Protocol. All signatures use compact format (64 bytes: r || s, 32 bytes each). </summary>
val sign: keyPair: KeyPair -> data: byte array -> byte array
<summary> Sign data with a key pair. Returns a 64-byte compact signature with low-S. The data is hashed with SHA-256 internally. </summary>
val isValid: bool
val verify: publicKey: PublicKey -> data: byte array -> signature: byte array -> bool
<summary> Verify a compact signature against data and a public key. Enforces low-S requirement. Returns false for high-S or DER-encoded signatures. </summary>
val keyPair3: KeyPair
val didKey: string
module Multikey from FSharp.ATProto.Crypto
<summary> Multicodec and did:key encoding/decoding for AT Protocol public keys. Supports P-256 (multicodec 0x1200) and secp256k1 (multicodec 0xe7). </summary>
val keyPairToDid: keyPair: KeyPair -> string
<summary> Format a key pair's public key as a did:key string. </summary>
val multibase: string
val encodeMultibase: key: PublicKey -> string
<summary> Encode a public key to multibase (base58btc, 'z' prefix) format. This is the format used in DID document verificationMethod publicKeyMultibase fields. </summary>
val decodeDid: didKey: string -> Result<PublicKey,string>
<summary> Decode a did:key string to a public key. </summary>
PublicKey.Algorithm: Algorithm
val decodeMultibase: multibase: string -> Result<PublicKey,string>
<summary> Decode a multibase-encoded public key string. Expects 'z' prefix (base58btc) followed by multicodec prefix + compressed key. </summary>
PublicKey.CompressedBytes: byte array
<summary> Compressed public key bytes (33 bytes: 0x02/0x03 prefix + 32-byte X). </summary>
val encoded: string
module Base58 from FSharp.ATProto.Crypto
<summary> Base58btc encoding and decoding using the Bitcoin alphabet. Used by multibase (prefix 'z') for encoding public keys in did:key and multikey formats. </summary>
val encode: input: byte array -> string
<summary> Encode bytes to base58btc string. </summary>
val decode: input: string -> Result<byte array,string>
<summary> Decode a base58btc string to bytes. Returns Error on invalid input. </summary>
val bytes: byte array

Type something to start searching.