Current section
Files
Jump to
Current section
Files
src/gose.gleam
//// A Gleam library for JOSE (JSON Object Signing and Encryption) and
//// COSE (CBOR Object Signing and Encryption).
////
//// Core:
//// - `gose`: error type, algorithm identifiers, and key management (types,
//// generators, builders, accessors, serializers)
//// - `gose/cbor`: CBOR encoding for COSE
////
//// `gose/key`, `gose/algorithm`, `gose/jose/algorithm`, `gose/cose/key`, and
//// `gose/cose/algorithm` are deprecated shims retained for the v2.x migration
//// window. They will be removed in v3.0. New code should import `gose`,
//// `gose/jose`, and `gose/cose` directly.
////
//// JOSE:
//// - `gose/jose`: JOSE algorithm string conversion ([RFC 7518](https://www.rfc-editor.org/rfc/rfc7518.html))
//// - `gose/jose/jws`: JSON Web Signature ([RFC 7515](https://www.rfc-editor.org/rfc/rfc7515.html))
//// - `gose/jose/jws_multi`: JWS JSON Serialization for multi-signer workflows
//// - `gose/jose/jwe`: JSON Web Encryption ([RFC 7516](https://www.rfc-editor.org/rfc/rfc7516.html))
//// - `gose/jose/jwe_multi`: JWE JSON Serialization for multi-recipient workflows
//// - `gose/jose/jwk`: JSON Web Key serialization ([RFC 7517](https://www.rfc-editor.org/rfc/rfc7517.html))
//// - `gose/jose/key_set`: JWK Set ([RFC 7517 Section 5](https://www.rfc-editor.org/rfc/rfc7517.html#section-5))
//// - `gose/jose/encrypted_key`: encrypted JWK export/import
//// - `gose/jose/jwt`: JSON Web Token ([RFC 7519](https://www.rfc-editor.org/rfc/rfc7519.html))
//// - `gose/jose/encrypted_jwt`: encrypted JWT (JWE-based)
////
//// COSE:
//// - `gose/cose`: header parameters, the `Key` alias, COSE_Key CBOR serialization, and COSE algorithm ID mapping ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052.html), [RFC 9053](https://www.rfc-editor.org/rfc/rfc9053.html))
//// - `gose/cose/sign1`: COSE_Sign1 ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052.html))
//// - `gose/cose/sign`: COSE_Sign multi-signer ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052.html))
//// - `gose/cose/encrypt0`: COSE_Encrypt0 ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052.html))
//// - `gose/cose/encrypt`: COSE_Encrypt multi-recipient ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052.html))
//// - `gose/cose/mac0`: COSE_Mac0 ([RFC 9052](https://www.rfc-editor.org/rfc/rfc9052.html))
//// - `gose/cose/cwt`: CBOR Web Token ([RFC 8392](https://www.rfc-editor.org/rfc/rfc8392.html))
//// - `gose/cose/encrypted_cwt`: encrypted CWT (Encrypt0-wrapped Sign1)
import gleam/bit_array
import gleam/bool
import gleam/list
import gleam/option.{type Option}
import gleam/result
import kryptos/crypto
import kryptos/ec
import kryptos/eddsa
import kryptos/rsa
import kryptos/xdh
/// Error type for JOSE and COSE operations.
///
/// Used by low-level JOSE/COSE primitives. The JWT and CWT layers wrap these
/// errors in their own domain-specific variants for token validation.
pub type GoseError {
/// Parsing failed: invalid base64, malformed JSON, unexpected structure, etc.
/// The `String` provides a human-readable description of what went wrong.
ParseError(String)
/// A cryptographic operation failed: signature verification, decryption,
/// key derivation, etc. The `String` describes the failure.
CryptoError(String)
/// An operation was attempted in an invalid state: wrong key type for the
/// chosen algorithm, missing required header field, etc. The `String`
/// explains which invariant was violated.
InvalidState(String)
/// Signature or MAC verification failed. Intentionally carries no detail
/// to avoid leaking information that could enable oracle attacks.
VerificationFailed
}
/// Extract the message string from a GoseError, regardless of variant.
pub fn error_message(error: GoseError) -> String {
case error {
ParseError(msg) -> msg
CryptoError(msg) -> msg
InvalidState(msg) -> msg
VerificationFailed -> "verification failed"
}
}
/// Public Key Use parameter.
///
/// Indicates whether the key is for signing or encryption.
pub type KeyUse {
/// Key is used for signature operations
Signing
/// Key is used for encryption operations
Encrypting
}
/// Key Operations parameter.
///
/// Identifies the operation(s) for which the key is intended.
pub type KeyOp {
/// Compute digital signature or MAC
Sign
/// Verify digital signature or MAC
Verify
/// Encrypt content
Encrypt
/// Decrypt content and validate decryption
Decrypt
/// Encrypt key
WrapKey
/// Decrypt key and validate decryption
UnwrapKey
/// Derive key
DeriveKey
/// Derive bits not to be used as a key
DeriveBits
}
/// AES key sizes.
pub type AesKeySize {
/// 128-bit AES key
Aes128
/// 192-bit AES key
Aes192
/// 256-bit AES key
Aes256
}
/// AES key wrapping modes.
pub type AesKwMode {
/// AES Key Wrap (RFC 3394)
AesKw
/// AES-GCM Key Wrap
AesGcmKw
}
/// ChaCha20-Poly1305 key wrapping variants.
pub type ChaCha20Kw {
/// ChaCha20-Poly1305 Key Wrap (12-byte nonce)
C20PKw
/// XChaCha20-Poly1305 Key Wrap (24-byte nonce)
XC20PKw
}
/// HMAC signing algorithm variants.
pub type HmacAlg {
/// HMAC using SHA-256
HmacSha256
/// HMAC using SHA-384
HmacSha384
/// HMAC using SHA-512
HmacSha512
}
/// RSA PKCS#1 v1.5 signing algorithm variants.
pub type RsaPkcs1Alg {
/// RSA PKCSv1.5 using SHA-256
RsaPkcs1Sha256
/// RSA PKCSv1.5 using SHA-384
RsaPkcs1Sha384
/// RSA PKCSv1.5 using SHA-512
RsaPkcs1Sha512
}
/// RSA-PSS signing algorithm variants.
pub type RsaPssAlg {
/// RSA-PSS using SHA-256 (RSASSA-PSS)
RsaPssSha256
/// RSA-PSS using SHA-384 (RSASSA-PSS)
RsaPssSha384
/// RSA-PSS using SHA-512 (RSASSA-PSS)
RsaPssSha512
}
/// ECDSA signing algorithm variants.
pub type EcdsaAlg {
/// ECDSA using P-256 and SHA-256
EcdsaP256
/// ECDSA using P-384 and SHA-384
EcdsaP384
/// ECDSA using P-521 and SHA-512
EcdsaP521
/// ECDSA using secp256k1 and SHA-256 (RFC 8812)
EcdsaSecp256k1
}
/// Asymmetric signature algorithms.
pub type DigitalSignatureAlg {
/// RSA PKCS#1 v1.5 signing
RsaPkcs1(RsaPkcs1Alg)
/// RSA-PSS signing
RsaPss(RsaPssAlg)
/// ECDSA signing
Ecdsa(EcdsaAlg)
/// EdDSA (Ed25519 or Ed448, curve determined by key)
Eddsa
}
/// MAC algorithms.
pub type MacAlg {
/// HMAC-based MAC
Hmac(HmacAlg)
}
/// Signing and MAC algorithms (union of asymmetric signatures and MACs).
pub type SigningAlg {
/// Asymmetric digital signature algorithm
DigitalSignature(DigitalSignatureAlg)
/// MAC algorithm
Mac(MacAlg)
}
/// RSA key encryption algorithm variants.
pub type RsaEncryptionAlg {
/// RSAES PKCS1 v1.5 key encryption.
///
/// **Security Warning:** Vulnerable to padding oracle attacks (Bleichenbacher).
/// Use only for interoperability with legacy systems that require RSA1_5.
/// Prefer `RsaOaepSha256` for new applications.
///
/// **Note:** Decryption may fail on Node.js 20.x (CVE-2023-46809).
RsaPkcs1v15
/// RSAES OAEP using default parameters
RsaOaepSha1
/// RSAES OAEP using SHA-256 and MGF1 with SHA-256
RsaOaepSha256
}
/// ECDH-ES key agreement algorithm variants.
pub type EcdhEsAlg {
/// ECDH-ES direct key agreement
EcdhEsDirect
/// ECDH-ES with AES Key Wrap
EcdhEsAesKw(AesKeySize)
/// ECDH-ES with ChaCha20-Poly1305 Key Wrap
EcdhEsChaCha20Kw(ChaCha20Kw)
}
/// PBES2 key encryption algorithm variants.
pub type Pbes2Alg {
/// PBES2 with HMAC-SHA-256 and A128KW wrapping
Pbes2Sha256Aes128Kw
/// PBES2 with HMAC-SHA-384 and A192KW wrapping
Pbes2Sha384Aes192Kw
/// PBES2 with HMAC-SHA-512 and A256KW wrapping
Pbes2Sha512Aes256Kw
}
/// Key encryption algorithms.
pub type KeyEncryptionAlg {
/// Direct use of a shared symmetric key
Direct
/// AES Key Wrap (standard or GCM mode)
AesKeyWrap(AesKwMode, AesKeySize)
/// ChaCha20-Poly1305 Key Wrap
ChaCha20KeyWrap(ChaCha20Kw)
/// RSA key encryption
RsaEncryption(RsaEncryptionAlg)
/// ECDH-ES key agreement
EcdhEs(EcdhEsAlg)
/// PBES2 password-based encryption
Pbes2(Pbes2Alg)
}
/// Content encryption algorithms.
pub type ContentAlg {
/// AES-GCM content encryption
AesGcm(AesKeySize)
/// AES-CBC with HMAC composite AEAD (CEK is double the AES key size)
AesCbcHmac(AesKeySize)
/// ChaCha20-Poly1305
ChaCha20Poly1305
/// XChaCha20-Poly1305
XChaCha20Poly1305
}
/// Algorithm union type for the key `alg` field.
/// A key can specify either a signing algorithm or a key encryption algorithm.
pub type Alg {
/// Signing algorithm
SigningAlg(SigningAlg)
/// Key encryption algorithm
KeyEncryptionAlg(KeyEncryptionAlg)
/// Content encryption algorithm
ContentAlg(ContentAlg)
}
@internal
pub type RsaKeyMaterial {
RsaPrivate(key: rsa.PrivateKey, public: rsa.PublicKey)
RsaPublic(key: rsa.PublicKey)
}
@internal
pub type EcKeyMaterial {
EcPrivate(key: ec.PrivateKey, public: ec.PublicKey, curve: ec.Curve)
EcPublic(key: ec.PublicKey, curve: ec.Curve)
}
@internal
pub type EddsaKeyMaterial {
EddsaPrivate(
key: eddsa.PrivateKey,
public: eddsa.PublicKey,
curve: eddsa.Curve,
)
EddsaPublic(key: eddsa.PublicKey, curve: eddsa.Curve)
}
@internal
pub type XdhKeyMaterial {
XdhPrivate(key: xdh.PrivateKey, public: xdh.PublicKey, curve: xdh.Curve)
XdhPublic(key: xdh.PublicKey, curve: xdh.Curve)
}
@internal
pub type KeyMaterial {
OctetKey(secret: BitArray)
Rsa(RsaKeyMaterial)
Elliptic(EcKeyMaterial)
Edwards(EddsaKeyMaterial)
Xdh(XdhKeyMaterial)
}
/// Key type identifier (kty parameter).
pub type KeyType {
/// Symmetric key (oct)
OctKeyType
/// RSA key
RsaKeyType
/// Elliptic Curve key
EcKeyType
/// Octet Key Pair (EdDSA, XDH)
OkpKeyType
}
/// A cryptographic key.
///
/// Use constructor functions like `from_octet_bits`, `from_der`,
/// `from_pem`, or `generate_*` to create keys.
pub opaque type Key(kid) {
Key(
material: KeyMaterial,
kid: Option(kid),
key_use: Option(KeyUse),
key_ops: Option(List(KeyOp)),
alg: Option(Alg),
)
}
@internal
pub fn new_key(material: KeyMaterial) -> Key(kid) {
Key(
material:,
kid: option.None,
key_use: option.None,
key_ops: option.None,
alg: option.None,
)
}
@internal
pub fn is_private_key(key: Key(kid)) -> Bool {
case key.material {
OctetKey(..) -> True
Rsa(RsaPrivate(..)) -> True
Rsa(RsaPublic(..)) -> False
Elliptic(EcPrivate(..)) -> True
Elliptic(EcPublic(..)) -> False
Edwards(EddsaPrivate(..)) -> True
Edwards(EddsaPublic(..)) -> False
Xdh(XdhPrivate(..)) -> True
Xdh(XdhPublic(..)) -> False
}
}
@internal
pub fn material(key: Key(kid)) -> KeyMaterial {
key.material
}
@internal
pub fn material_octet_secret(mat: KeyMaterial) -> Result(BitArray, GoseError) {
case mat {
OctetKey(secret:) -> Ok(secret)
Rsa(..) | Elliptic(..) | Edwards(..) | Xdh(..) ->
Error(InvalidState("expected octet key"))
}
}
@internal
pub fn material_rsa(mat: KeyMaterial) -> Result(RsaKeyMaterial, GoseError) {
case mat {
Rsa(rsa) -> Ok(rsa)
OctetKey(..) | Elliptic(..) | Edwards(..) | Xdh(..) ->
Error(InvalidState("expected RSA key"))
}
}
@internal
pub fn material_ec(mat: KeyMaterial) -> Result(EcKeyMaterial, GoseError) {
case mat {
Elliptic(ec) -> Ok(ec)
OctetKey(..) | Rsa(..) | Edwards(..) | Xdh(..) ->
Error(InvalidState("expected EC key"))
}
}
@internal
pub fn material_eddsa(mat: KeyMaterial) -> Result(EddsaKeyMaterial, GoseError) {
case mat {
Edwards(eddsa) -> Ok(eddsa)
OctetKey(..) | Rsa(..) | Elliptic(..) | Xdh(..) ->
Error(InvalidState("expected EdDSA key"))
}
}
@internal
pub fn material_xdh(mat: KeyMaterial) -> Result(XdhKeyMaterial, GoseError) {
case mat {
Xdh(xdh) -> Ok(xdh)
OctetKey(..) | Rsa(..) | Elliptic(..) | Edwards(..) ->
Error(InvalidState("expected XDH key"))
}
}
/// Create a key from DER-encoded data.
///
/// Auto-detects key type (RSA, EC, EdDSA, XDH) and format (PKCS#1, PKCS#8, SPKI).
/// Supports both private and public keys.
pub fn from_der(der: BitArray) -> Result(Key(kid), GoseError) {
parse_rsa_der(der)
|> result.lazy_or(fn() { parse_eddsa_der(der) })
|> result.lazy_or(fn() { parse_xdh_der(der) })
|> result.lazy_or(fn() { parse_ec_der(der) })
|> result.map_error(fn(_) {
ParseError(
"invalid DER: not a recognized RSA, EC, EdDSA, or XDH key format",
)
})
}
fn ec_private_key(pair: #(ec.PrivateKey, ec.PublicKey)) -> Key(kid) {
let #(private, public) = pair
let curve = ec.curve(private)
new_key(Elliptic(EcPrivate(key: private, public:, curve:)))
}
fn ec_public_key_internal(public: ec.PublicKey) -> Key(kid) {
let curve = ec.public_key_curve(public)
new_key(Elliptic(EcPublic(key: public, curve:)))
}
fn eddsa_private_key(pair: #(eddsa.PrivateKey, eddsa.PublicKey)) -> Key(kid) {
let #(private, public) = pair
let curve = eddsa.curve(private)
new_key(Edwards(EddsaPrivate(key: private, public:, curve:)))
}
fn eddsa_public_key_internal(public: eddsa.PublicKey) -> Key(kid) {
let curve = eddsa.public_key_curve(public)
new_key(Edwards(EddsaPublic(key: public, curve:)))
}
fn rsa_private_key_internal(pair: #(rsa.PrivateKey, rsa.PublicKey)) -> Key(kid) {
let #(private, public) = pair
new_key(Rsa(RsaPrivate(key: private, public:)))
}
fn rsa_public_key_internal(public: rsa.PublicKey) -> Key(kid) {
new_key(Rsa(RsaPublic(key: public)))
}
fn xdh_private_key(pair: #(xdh.PrivateKey, xdh.PublicKey)) -> Key(kid) {
let #(private, public) = pair
let curve = xdh.curve(private)
new_key(Xdh(XdhPrivate(key: private, public:, curve:)))
}
fn xdh_public_key_internal(public: xdh.PublicKey) -> Key(kid) {
let curve = xdh.public_key_curve(public)
new_key(Xdh(XdhPublic(key: public, curve:)))
}
fn parse_ec_der(der: BitArray) -> Result(Key(kid), Nil) {
ec.from_der(der)
|> result.map(ec_private_key)
|> result.lazy_or(fn() {
ec.public_key_from_der(der) |> result.map(ec_public_key_internal)
})
}
fn parse_eddsa_der(der: BitArray) -> Result(Key(kid), Nil) {
eddsa.from_der(der)
|> result.map(eddsa_private_key)
|> result.lazy_or(fn() {
eddsa.public_key_from_der(der) |> result.map(eddsa_public_key_internal)
})
}
fn parse_rsa_der(der: BitArray) -> Result(Key(kid), Nil) {
rsa.from_der(der, rsa.Pkcs8)
|> result.map(rsa_private_key_internal)
|> result.lazy_or(fn() {
rsa.from_der(der, rsa.Pkcs1) |> result.map(rsa_private_key_internal)
})
|> result.lazy_or(fn() {
rsa.public_key_from_der(der, rsa.Spki)
|> result.map(rsa_public_key_internal)
})
|> result.lazy_or(fn() {
rsa.public_key_from_der(der, rsa.RsaPublicKey)
|> result.map(rsa_public_key_internal)
})
}
fn parse_xdh_der(der: BitArray) -> Result(Key(kid), Nil) {
xdh.from_der(der)
|> result.map(xdh_private_key)
|> result.lazy_or(fn() {
xdh.public_key_from_der(der) |> result.map(xdh_public_key_internal)
})
}
/// Create an EdDSA key pair from raw private key bytes.
///
/// The public key is derived from the private key.
/// This is the inverse of `to_octet_bits` for EdDSA private keys.
pub fn from_eddsa_bits(
curve: eddsa.Curve,
private_bits private_bits: BitArray,
) -> Result(Key(kid), GoseError) {
eddsa.from_bytes(curve, private_bits)
|> result.map(fn(pair) {
let #(private, public) = pair
new_key(Edwards(EddsaPrivate(key: private, public:, curve:)))
})
|> result.replace_error(ParseError("invalid EdDSA private key bits"))
}
/// Create an EdDSA public key from raw bytes.
///
/// This is the inverse of `to_octet_bits` for EdDSA public keys.
pub fn from_eddsa_public_bits(
curve: eddsa.Curve,
public_bits public_bits: BitArray,
) -> Result(Key(kid), GoseError) {
eddsa.public_key_from_bytes(curve, public_bits)
|> result.map(fn(public) {
new_key(Edwards(EddsaPublic(key: public, curve:)))
})
|> result.replace_error(ParseError("invalid EdDSA public key bits"))
}
/// Create a symmetric key from raw bytes.
///
/// Used for HMAC signing (HS256/384/512) and direct encryption.
/// Returns an error if the secret is empty.
///
/// ## Example
///
/// ```gleam
/// let secret = crypto.random_bytes(32)
/// let assert Ok(key) = gose.from_octet_bits(secret)
/// ```
pub fn from_octet_bits(secret: BitArray) -> Result(Key(kid), GoseError) {
case bit_array.byte_size(secret) {
0 -> Error(InvalidState("oct key must not be empty"))
_ -> Ok(new_key(OctetKey(secret:)))
}
}
/// Create a key from PEM-encoded data.
///
/// Auto-detects key type (RSA, EC, EdDSA, XDH) and format (PKCS#1, PKCS#8, SPKI).
/// Supports both private and public keys.
pub fn from_pem(pem: String) -> Result(Key(kid), GoseError) {
parse_rsa_pem(pem)
|> result.lazy_or(fn() { parse_eddsa_pem(pem) })
|> result.lazy_or(fn() { parse_xdh_pem(pem) })
|> result.lazy_or(fn() { parse_ec_pem(pem) })
|> result.map_error(fn(_) {
ParseError(
"invalid PEM: not a recognized RSA, EC, EdDSA, or XDH key format",
)
})
}
fn parse_ec_pem(pem: String) -> Result(Key(kid), Nil) {
ec.from_pem(pem)
|> result.map(ec_private_key)
|> result.lazy_or(fn() {
ec.public_key_from_pem(pem) |> result.map(ec_public_key_internal)
})
}
fn parse_eddsa_pem(pem: String) -> Result(Key(kid), Nil) {
eddsa.from_pem(pem)
|> result.map(eddsa_private_key)
|> result.lazy_or(fn() {
eddsa.public_key_from_pem(pem) |> result.map(eddsa_public_key_internal)
})
}
fn parse_rsa_pem(pem: String) -> Result(Key(kid), Nil) {
rsa.from_pem(pem, rsa.Pkcs8)
|> result.map(rsa_private_key_internal)
|> result.lazy_or(fn() {
rsa.from_pem(pem, rsa.Pkcs1) |> result.map(rsa_private_key_internal)
})
|> result.lazy_or(fn() {
rsa.public_key_from_pem(pem, rsa.Spki)
|> result.map(rsa_public_key_internal)
})
|> result.lazy_or(fn() {
rsa.public_key_from_pem(pem, rsa.RsaPublicKey)
|> result.map(rsa_public_key_internal)
})
}
fn parse_xdh_pem(pem: String) -> Result(Key(kid), Nil) {
xdh.from_pem(pem)
|> result.map(xdh_private_key)
|> result.lazy_or(fn() {
xdh.public_key_from_pem(pem) |> result.map(xdh_public_key_internal)
})
}
/// Create an XDH key pair from raw private key bytes.
///
/// The public key is derived from the private key.
/// This is the inverse of `to_octet_bits` for XDH private keys.
pub fn from_xdh_bits(
curve: xdh.Curve,
private_bits private_bits: BitArray,
) -> Result(Key(kid), GoseError) {
xdh.from_bytes(curve, private_bits)
|> result.map(fn(pair) {
let #(private, public) = pair
new_key(Xdh(XdhPrivate(key: private, public:, curve:)))
})
|> result.replace_error(ParseError("invalid XDH private key bits"))
}
/// Create an XDH public key from raw bytes.
///
/// This is the inverse of `to_octet_bits` for XDH public keys.
pub fn from_xdh_public_bits(
curve: xdh.Curve,
public_bits public_bits: BitArray,
) -> Result(Key(kid), GoseError) {
xdh.public_key_from_bytes(curve, public_bits)
|> result.map(fn(public) { new_key(Xdh(XdhPublic(key: public, curve:))) })
|> result.replace_error(ParseError("invalid XDH public key bits"))
}
/// Generate a new EC key pair for the given curve.
///
/// Supported curves: P256, P384, P521, Secp256k1.
pub fn generate_ec(curve: ec.Curve) -> Key(kid) {
let #(private, public) = ec.generate_key_pair(curve)
new_key(Elliptic(EcPrivate(key: private, public:, curve:)))
}
/// Generate a new EdDSA key pair for the given curve.
///
/// Supported curves: Ed25519, Ed448.
pub fn generate_eddsa(curve: eddsa.Curve) -> Key(kid) {
let #(private, public) = eddsa.generate_key_pair(curve)
new_key(Edwards(EddsaPrivate(key: private, public:, curve:)))
}
/// Generate a symmetric key for HMAC signing.
///
/// The key size is derived from the algorithm:
/// - `HmacSha256` → 32 bytes
/// - `HmacSha384` → 48 bytes
/// - `HmacSha512` → 64 bytes
pub fn generate_hmac_key(alg: HmacAlg) -> Key(kid) {
let size = hmac_alg_key_size(alg)
let secret = crypto.random_bytes(size)
new_key(OctetKey(secret:))
}
/// Generate a symmetric key for JWE content encryption.
///
/// The key size is derived from the encryption algorithm:
/// - `AesGcm(Aes128)` → 16 bytes
/// - `AesGcm(Aes192)` → 24 bytes
/// - `AesGcm(Aes256)` → 32 bytes
/// - `AesCbcHmac(Aes128)` → 32 bytes (16 + 16 for MAC)
/// - `AesCbcHmac(Aes192)` → 48 bytes (24 + 24 for MAC)
/// - `AesCbcHmac(Aes256)` → 64 bytes (32 + 32 for MAC)
/// - `ChaCha20Poly1305` → 32 bytes
/// - `XChaCha20Poly1305` → 32 bytes
pub fn generate_enc_key(enc: ContentAlg) -> Key(kid) {
let size = content_alg_key_size(enc)
let secret = crypto.random_bytes(size)
new_key(OctetKey(secret:))
}
/// Generate a symmetric key for AES Key Wrap.
///
/// The key size is derived from the AES variant:
/// - `Aes128` → 16 bytes
/// - `Aes192` → 24 bytes
/// - `Aes256` → 32 bytes
pub fn generate_aes_kw_key(size: AesKeySize) -> Key(kid) {
let byte_count = aes_key_size(size)
let secret = crypto.random_bytes(byte_count)
new_key(OctetKey(secret:))
}
/// Generate a symmetric key for ChaCha20-Poly1305 Key Wrap (C20PKW / XC20PKW).
///
/// Always generates a 32-byte key, as both ChaCha20 and XChaCha20 use 256-bit keys.
pub fn generate_chacha20_kw_key() -> Key(kid) {
let secret = crypto.random_bytes(32)
new_key(OctetKey(secret:))
}
/// Generate a new RSA key pair with the given key size in bits.
/// Common sizes are 2048, 3072, and 4096. Keys smaller than 2048
/// bits are not recommended for security.
pub fn generate_rsa(bits: Int) -> Result(Key(kid), GoseError) {
case rsa.generate_key_pair(bits) {
Ok(#(private, public)) ->
Ok(new_key(Rsa(RsaPrivate(key: private, public:))))
Error(_) -> Error(CryptoError("RSA key generation failed"))
}
}
/// Generate a new XDH key pair for key agreement.
///
/// Supported curves: X25519, X448.
pub fn generate_xdh(curve: xdh.Curve) -> Key(kid) {
let #(private, public) = xdh.generate_key_pair(curve)
new_key(Xdh(XdhPrivate(key: private, public:, curve:)))
}
/// Create an EC public key from curve and x,y coordinates (big-endian bytes).
pub fn ec_public_key_from_coordinates(
curve: ec.Curve,
x x: BitArray,
y y: BitArray,
) -> Result(Key(kid), GoseError) {
ec_public_key_from_raw_coordinates(curve, x:, y:)
|> result.map(fn(public) { new_key(Elliptic(EcPublic(key: public, curve:))) })
}
@internal
pub fn ec_public_key_from_raw_coordinates(
curve: ec.Curve,
x x: BitArray,
y y: BitArray,
) -> Result(ec.PublicKey, GoseError) {
let coord_size = ec.coordinate_size(curve)
use <- bool.guard(
when: bit_array.byte_size(x) != coord_size,
return: Error(ParseError("EC x coordinate wrong length")),
)
use <- bool.guard(
when: bit_array.byte_size(y) != coord_size,
return: Error(ParseError("EC y coordinate wrong length")),
)
let raw_point = bit_array.concat([<<0x04>>, x, y])
ec.public_key_from_raw_point(curve, raw_point)
|> result.replace_error(ParseError("invalid EC coordinates"))
}
@internal
pub fn ec_raw_coordinates(
public: ec.PublicKey,
curve curve: ec.Curve,
) -> Result(#(BitArray, BitArray), GoseError) {
let coord_size = ec.coordinate_size(curve)
let raw_point = ec.public_key_to_raw_point(public)
let expected_size = 1 + coord_size * 2
case bit_array.byte_size(raw_point) == expected_size, raw_point {
True, <<0x04, rest:bits>> -> {
let error = InvalidState("invalid raw point format")
use x <- result.try(
bit_array.slice(rest, 0, coord_size)
|> result.replace_error(error),
)
use y <- result.try(
bit_array.slice(rest, coord_size, coord_size)
|> result.replace_error(error),
)
Ok(#(x, y))
}
_, _ -> Error(InvalidState("invalid raw point format"))
}
}
/// Set the algorithm (`alg`) metadata parameter on a key.
pub fn with_alg(key: Key(kid), alg: Alg) -> Key(kid) {
Key(..key, alg: option.Some(alg))
}
/// Set the key operations parameter.
///
/// Per RFC 7517, the values should be consistent with `key_use` if both are present:
/// - `Signing` use implies `Sign` and/or `Verify` operations
/// - `Encrypting` use implies `Encrypt`, `Decrypt`, `WrapKey`, `UnwrapKey`, `DeriveKey`, `DeriveBits`
///
/// Returns an error if the list is empty, contains duplicates, or is
/// incompatible with the key's existing `key_use`.
pub fn with_key_ops(
key: Key(kid),
ops: List(KeyOp),
) -> Result(Key(kid), GoseError) {
case ops {
[] -> Error(InvalidState("key_ops must not be empty"))
_ -> {
use <- bool.guard(
when: list.unique(ops) != ops,
return: Error(InvalidState("key_ops must not contain duplicates")),
)
validate_key_use_ops(key.key_use, option.Some(ops))
|> result.replace(Key(..key, key_ops: option.Some(ops)))
}
}
}
/// Set the public key use parameter.
///
/// Returns an error if the key already has `key_ops` that are incompatible with
/// the specified use, or if the use is incompatible with the key type per RFC
/// 8037 (EdDSA keys can only be used for signing, XDH keys can only be used for
/// encryption).
pub fn with_key_use(key: Key(kid), use_: KeyUse) -> Result(Key(kid), GoseError) {
use _ <- result.try(validate_key_use_ops(option.Some(use_), key.key_ops))
use _ <- result.try(validate_rfc8037_key_use(key.material, option.Some(use_)))
Ok(Key(..key, key_use: option.Some(use_)))
}
/// Validate key use against RFC 8037 curve restrictions.
/// - EdDSA keys (Ed25519/Ed448): only `sig` allowed
/// - XDH keys (X25519/X448): only `enc` allowed
fn validate_rfc8037_key_use(
material: KeyMaterial,
use_: Option(KeyUse),
) -> Result(Nil, GoseError) {
case material, use_ {
Edwards(..), option.Some(Encrypting) ->
Error(InvalidState(
"EdDSA keys (Ed25519/Ed448) cannot be used for encryption",
))
Xdh(..), option.Some(Signing) ->
Error(InvalidState("XDH keys (X25519/X448) cannot be used for signing"))
_, _ -> Ok(Nil)
}
}
/// Set the key ID (`kid`) metadata parameter on a key.
pub fn with_kid(key: Key(a), kid: String) -> Key(String) {
Key(..key, kid: option.Some(kid))
}
/// Set the key ID (`kid`) metadata parameter on a key using raw bytes.
///
/// In COSE (RFC 9052), kid is a bstr that may contain arbitrary bytes.
/// For JWK interoperability where kid is a JSON string, use `with_kid`.
pub fn with_kid_bits(key: Key(a), kid: BitArray) -> Key(BitArray) {
Key(..key, kid: option.Some(kid))
}
fn is_signing_op(op: KeyOp) -> Bool {
case op {
Sign | Verify -> True
Encrypt | Decrypt | WrapKey | UnwrapKey | DeriveKey | DeriveBits -> False
}
}
fn is_encrypting_op(op: KeyOp) -> Bool {
case op {
Encrypt | Decrypt | WrapKey | UnwrapKey | DeriveKey | DeriveBits -> True
Sign | Verify -> False
}
}
@internal
pub fn validate_key_use_ops(
key_use: Option(KeyUse),
key_ops: Option(List(KeyOp)),
) -> Result(Nil, GoseError) {
case key_use, key_ops {
option.None, _ | _, option.None -> Ok(Nil)
option.Some(Signing), option.Some(ops) ->
case list.all(ops, is_signing_op) {
True -> Ok(Nil)
False -> Error(InvalidState("key_ops incompatible with use=sig"))
}
option.Some(Encrypting), option.Some(ops) ->
case list.all(ops, is_encrypting_op) {
True -> Ok(Nil)
False -> Error(InvalidState("key_ops incompatible with use=enc"))
}
}
}
/// Get the algorithm (`alg`) parameter.
pub fn alg(key: Key(kid)) -> Result(Alg, Nil) {
option.to_result(key.alg, Nil)
}
/// Get the curve used by an EC key.
///
/// Returns an error if the key is not an EC key.
pub fn ec_curve(key: Key(kid)) -> Result(ec.Curve, GoseError) {
material_ec(key.material)
|> result.map(fn(ec) {
case ec {
EcPrivate(curve:, ..) | EcPublic(curve:, ..) -> curve
}
})
}
/// Extract the EC public key.
///
/// Works with both EC private keys (extracts the public component)
/// and EC public keys.
///
/// Returns an error if the key is not an EC key.
pub fn ec_public_key(key: Key(kid)) -> Result(ec.PublicKey, GoseError) {
material_ec(key.material)
|> result.map(fn(ec) {
case ec {
EcPrivate(public:, ..) -> public
EcPublic(key: k, ..) -> k
}
})
}
/// Get the x and y coordinates from an EC public key.
///
/// The coordinates are returned as raw big-endian bytes, padded to
/// the coordinate size for the curve.
///
/// Returns an error if the key is not an EC key.
pub fn ec_public_key_coordinates(
key: Key(kid),
) -> Result(#(BitArray, BitArray), GoseError) {
use public <- result.try(ec_public_key(key))
use curve <- result.try(ec_curve(key))
ec_raw_coordinates(public, curve:)
}
/// Get the curve used by an EdDSA key.
///
/// Returns an error if the key is not an EdDSA key.
pub fn eddsa_curve(key: Key(kid)) -> Result(eddsa.Curve, GoseError) {
material_eddsa(key.material)
|> result.map(fn(eddsa) {
case eddsa {
EddsaPrivate(curve:, ..) | EddsaPublic(curve:, ..) -> curve
}
})
}
/// Extract the EdDSA public key.
///
/// Works with both EdDSA private keys (extracts the public component)
/// and EdDSA public keys.
///
/// Returns an error if the key is not an EdDSA key.
pub fn eddsa_public_key(key: Key(kid)) -> Result(eddsa.PublicKey, GoseError) {
material_eddsa(key.material)
|> result.map(fn(eddsa) {
case eddsa {
EddsaPrivate(public:, ..) -> public
EddsaPublic(key: k, ..) -> k
}
})
}
/// Get the key operations parameter.
pub fn key_ops(key: Key(kid)) -> Result(List(KeyOp), Nil) {
option.to_result(key.key_ops, Nil)
}
/// Get the key type (kty) for this key.
pub fn key_type(key: Key(kid)) -> KeyType {
case key.material {
OctetKey(..) -> OctKeyType
Rsa(..) -> RsaKeyType
Elliptic(..) -> EcKeyType
Edwards(..) | Xdh(..) -> OkpKeyType
}
}
/// Get the public key use parameter.
pub fn key_use(key: Key(kid)) -> Result(KeyUse, Nil) {
option.to_result(key.key_use, Nil)
}
/// Get the key ID (kid) parameter.
///
/// The return type depends on the key's kid type parameter:
/// - `Key(String)` (from JWK) → `Result(String, Nil)`
/// - `Key(BitArray)` (from COSE) → `Result(BitArray, Nil)`
pub fn kid(key: Key(kid)) -> Result(kid, Nil) {
option.to_result(key.kid, Nil)
}
/// Get the size of an octet (symmetric) key in bytes.
///
/// Returns an error if the key is not an octet key.
pub fn octet_key_size(key: Key(kid)) -> Result(Int, GoseError) {
case material_octet_secret(key.material) {
Ok(secret) -> Ok(bit_array.byte_size(secret))
Error(_) -> Error(InvalidState("key is not an octet key"))
}
}
/// Extract the RSA public key.
///
/// Works with both RSA private keys (extracts the public component)
/// and RSA public keys.
///
/// Returns an error if the key is not an RSA key.
pub fn rsa_public_key(key: Key(kid)) -> Result(rsa.PublicKey, GoseError) {
material_rsa(key.material)
|> result.map(fn(rsa) {
case rsa {
RsaPrivate(public:, ..) -> public
RsaPublic(key: k) -> k
}
})
}
/// Get the curve used by an XDH key.
///
/// Returns an error if the key is not an XDH key.
pub fn xdh_curve(key: Key(kid)) -> Result(xdh.Curve, GoseError) {
material_xdh(key.material)
|> result.map(fn(xdh) {
case xdh {
XdhPrivate(curve:, ..) | XdhPublic(curve:, ..) -> curve
}
})
}
/// Extract the XDH public key (X25519/X448).
///
/// Works with both XDH private keys (extracts the public component)
/// and XDH public keys.
///
/// Returns an error if the key is not an XDH key.
pub fn xdh_public_key(key: Key(kid)) -> Result(xdh.PublicKey, GoseError) {
material_xdh(key.material)
|> result.map(fn(xdh) {
case xdh {
XdhPrivate(public:, ..) -> public
XdhPublic(key: k, ..) -> k
}
})
}
/// Extract the public key from an asymmetric key.
///
/// For private keys, extracts the corresponding public key.
/// For public keys, returns the key unchanged.
/// Returns an error for symmetric octet keys.
///
/// When extracting a public key, `key_ops` are filtered to public-safe operations:
/// - `Sign` is mapped to `Verify`
/// - `Decrypt` and `UnwrapKey` are removed (private-only)
/// - Other operations are preserved
///
/// ## Example
///
/// ```gleam
/// let private_key = gose.generate_ec(ec.P256)
/// let assert Ok(pub_key) = gose.public_key(private_key)
/// ```
pub fn public_key(key: Key(kid)) -> Result(Key(kid), GoseError) {
let filtered_ops =
key.key_ops
|> option.map(filter_public_key_ops)
|> option.then(option.from_result)
case key.material {
Rsa(RsaPrivate(public:, ..)) ->
Ok(
Key(..key, material: Rsa(RsaPublic(key: public)), key_ops: filtered_ops),
)
Rsa(RsaPublic(..)) -> Ok(Key(..key, key_ops: filtered_ops))
Elliptic(EcPrivate(public:, curve:, ..)) ->
Ok(
Key(
..key,
material: Elliptic(EcPublic(key: public, curve:)),
key_ops: filtered_ops,
),
)
Elliptic(EcPublic(..)) -> Ok(Key(..key, key_ops: filtered_ops))
Edwards(EddsaPrivate(public:, curve:, ..)) ->
Ok(
Key(
..key,
material: Edwards(EddsaPublic(key: public, curve:)),
key_ops: filtered_ops,
),
)
Edwards(EddsaPublic(..)) -> Ok(Key(..key, key_ops: filtered_ops))
Xdh(XdhPrivate(public:, curve:, ..)) ->
Ok(
Key(
..key,
material: Xdh(XdhPublic(key: public, curve:)),
key_ops: filtered_ops,
),
)
Xdh(XdhPublic(..)) -> Ok(Key(..key, key_ops: filtered_ops))
OctetKey(..) -> Error(InvalidState("octet keys are not asymmetric"))
}
}
fn filter_public_key_ops(ops: List(KeyOp)) -> Result(List(KeyOp), Nil) {
case list.unique(list.filter_map(ops, map_public_key_op)) {
[] -> Error(Nil)
filtered -> Ok(filtered)
}
}
fn map_public_key_op(op: KeyOp) -> Result(KeyOp, Nil) {
case op {
Sign -> Ok(Verify)
Decrypt | UnwrapKey -> Error(Nil)
Verify | Encrypt | WrapKey | DeriveKey | DeriveBits -> Ok(op)
}
}
/// Serialize a key to DER format.
///
/// Supports RSA, EC, EdDSA, and XDH keys (both private and public).
/// Uses PKCS#8 for private keys and SPKI for public keys.
pub fn to_der(key: Key(kid)) -> Result(BitArray, GoseError) {
case key.material {
Rsa(RsaPrivate(key: private, ..)) ->
rsa.to_der(private, rsa.Pkcs8)
|> result.replace_error(InvalidState(
"failed to serialize RSA private key",
))
Rsa(RsaPublic(key: public)) ->
rsa.public_key_to_der(public, rsa.Spki)
|> result.replace_error(InvalidState("failed to serialize RSA public key"))
Elliptic(EcPrivate(key: private, ..)) ->
ec.to_der(private)
|> result.replace_error(InvalidState("failed to serialize EC private key"))
Elliptic(EcPublic(key: public, ..)) ->
ec.public_key_to_der(public)
|> result.replace_error(InvalidState("failed to serialize EC public key"))
Edwards(EddsaPrivate(key: private, ..)) ->
eddsa.to_der(private)
|> result.replace_error(InvalidState(
"failed to serialize EdDSA private key",
))
Edwards(EddsaPublic(key: public, ..)) ->
eddsa.public_key_to_der(public)
|> result.replace_error(InvalidState(
"failed to serialize EdDSA public key",
))
Xdh(XdhPrivate(key: private, ..)) ->
xdh.to_der(private)
|> result.replace_error(InvalidState(
"failed to serialize XDH private key",
))
Xdh(XdhPublic(key: public, ..)) ->
xdh.public_key_to_der(public)
|> result.replace_error(InvalidState("failed to serialize XDH public key"))
OctetKey(..) ->
Error(InvalidState("octet keys cannot be serialized to DER"))
}
}
/// Export the raw bytes of a key.
///
/// Supported key types:
/// - Octet keys: returns the secret bytes
/// - EdDSA/XDH private keys: returns the private key bytes (d)
/// - EdDSA/XDH public keys: returns the public key bytes (x)
pub fn to_octet_bits(key: Key(kid)) -> Result(BitArray, GoseError) {
case key.material {
OctetKey(secret:) -> Ok(secret)
Edwards(EddsaPrivate(key: private, ..)) -> Ok(eddsa.to_bytes(private))
Edwards(EddsaPublic(key: public, ..)) ->
Ok(eddsa.public_key_to_bytes(public))
Xdh(XdhPrivate(key: private, ..)) -> Ok(xdh.to_bytes(private))
Xdh(XdhPublic(key: public, ..)) -> Ok(xdh.public_key_to_bytes(public))
Rsa(..) | Elliptic(..) ->
Error(InvalidState("key has no single-value byte representation"))
}
}
/// Serialize a key to PEM format.
///
/// Supports RSA, EC, EdDSA, and XDH keys (both private and public).
/// Uses PKCS#8 for private keys and SPKI for public keys.
pub fn to_pem(key: Key(kid)) -> Result(String, GoseError) {
case key.material {
Rsa(RsaPrivate(key: private, ..)) ->
rsa.to_pem(private, rsa.Pkcs8)
|> result.replace_error(InvalidState(
"failed to serialize RSA private key",
))
Rsa(RsaPublic(key: public)) ->
rsa.public_key_to_pem(public, rsa.Spki)
|> result.replace_error(InvalidState("failed to serialize RSA public key"))
Elliptic(EcPrivate(key: private, ..)) ->
ec.to_pem(private)
|> result.replace_error(InvalidState("failed to serialize EC private key"))
Elliptic(EcPublic(key: public, ..)) ->
ec.public_key_to_pem(public)
|> result.replace_error(InvalidState("failed to serialize EC public key"))
Edwards(EddsaPrivate(key: private, ..)) ->
eddsa.to_pem(private)
|> result.replace_error(InvalidState(
"failed to serialize EdDSA private key",
))
Edwards(EddsaPublic(key: public, ..)) ->
eddsa.public_key_to_pem(public)
|> result.replace_error(InvalidState(
"failed to serialize EdDSA public key",
))
Xdh(XdhPrivate(key: private, ..)) ->
xdh.to_pem(private)
|> result.replace_error(InvalidState(
"failed to serialize XDH private key",
))
Xdh(XdhPublic(key: public, ..)) ->
xdh.public_key_to_pem(public)
|> result.replace_error(InvalidState("failed to serialize XDH public key"))
OctetKey(..) ->
Error(InvalidState("octet keys cannot be serialized to PEM"))
}
}
@internal
pub fn build(
material material: KeyMaterial,
kid kid: Option(kid),
key_use key_use: Option(KeyUse),
key_ops key_ops: Option(List(KeyOp)),
alg alg: Option(Alg),
) -> Key(kid) {
Key(material:, kid:, key_use:, key_ops:, alg:)
}
@internal
pub fn validate_rfc8037_key_use_public(
material: KeyMaterial,
use_: Option(KeyUse),
) -> Result(Nil, GoseError) {
validate_rfc8037_key_use(material, use_)
}
@internal
pub fn aes_key_size(size: AesKeySize) -> Int {
case size {
Aes128 -> 16
Aes192 -> 24
Aes256 -> 32
}
}
@internal
pub fn hmac_alg_key_size(alg: HmacAlg) -> Int {
case alg {
HmacSha256 -> 32
HmacSha384 -> 48
HmacSha512 -> 64
}
}
@internal
pub fn content_alg_key_size(enc: ContentAlg) -> Int {
case enc {
AesGcm(size) -> aes_key_size(size)
AesCbcHmac(size) -> aes_key_size(size) * 2
ChaCha20Poly1305 -> 32
XChaCha20Poly1305 -> 32
}
}
@internal
pub fn chacha20_kw_nonce_size(variant: ChaCha20Kw) -> Int {
case variant {
C20PKw -> 12
XC20PKw -> 24
}
}