Skip to main content
Glama
ed25519.rs11 kB
use anyhow::Context as _; use aws_lc_rs::{ encoding::AsBigEndian, signature::{ self, Ed25519KeyPair, KeyPair, UnparsedPublicKey, }, }; use indexmap::{ indexset, IndexSet, }; use serde::Serialize; use spki::der::{ asn1::BitStringRef, AnyRef, Decode as _, Encode as _, }; use super::{ check_usages_subset, CryptoKey, CryptoKeyKind, CryptoKeyPair, ImportKeyInput, JsonWebKey, KeyData, KeyFormat, KeyType, KeyUsage, URL_SAFE_FORGIVING, }; use crate::{ convert_v8::{ DOMException, DOMExceptionName, }, environment::crypto_rng::CryptoRng, }; // id-Ed25519 OBJECT IDENTIFIER ::= { 1 3 101 112 } const ED25519_OID: const_oid::ObjectIdentifier = const_oid::ObjectIdentifier::new_unwrap("1.3.101.112"); #[derive(Serialize)] #[serde(tag = "name")] #[serde(rename = "Ed25519")] pub(crate) struct Ed25519Algorithm {} pub(crate) struct Ed25519PrivateKey { keypair: Ed25519KeyPair, } pub(crate) struct Ed25519PublicKey { x: [u8; 32], } pub(crate) fn generate_keypair( _rng: &CryptoRng, extractable: bool, usages: IndexSet<KeyUsage>, ) -> anyhow::Result<CryptoKeyPair> { let keypair = Ed25519KeyPair::generate().context("failed to generate ed25519 keypair")?; let public_key = <[u8; 32]>::try_from(keypair.public_key().as_ref())?; check_usages_subset(&usages, &[KeyUsage::Sign, KeyUsage::Verify])?; Ok(CryptoKeyPair { private_key: CryptoKey { kind: CryptoKeyKind::Ed25519Private { algorithm: Ed25519Algorithm {}, key: Ed25519PrivateKey { keypair }, }, r#type: KeyType::Private, extractable, usages: usages .intersection(&indexset![KeyUsage::Sign]) .copied() .collect(), }, public_key: CryptoKey { kind: CryptoKeyKind::Ed25519Public { algorithm: Ed25519Algorithm {}, key: Ed25519PublicKey { x: public_key }, }, r#type: KeyType::Public, extractable: true, // N.B.: public key is always extractable usages: usages .intersection(&indexset![KeyUsage::Verify]) .copied() .collect(), }, }) } pub(crate) fn import_key( format: ImportKeyInput, extractable: bool, usages: IndexSet<KeyUsage>, ) -> anyhow::Result<CryptoKey> { match format { ImportKeyInput::Raw(raw) => { check_usages_subset(&usages, &[KeyUsage::Verify])?; let raw = <[u8; 32]>::try_from(raw).map_err(|_| { DOMException::new( "Ed25519 public key must be 256 bits", DOMExceptionName::DataError, ) })?; Ok(CryptoKey { kind: CryptoKeyKind::Ed25519Public { algorithm: Ed25519Algorithm {}, key: Ed25519PublicKey { x: raw }, }, r#type: KeyType::Public, extractable, usages, }) }, ImportKeyInput::Pkcs8(der) => { check_usages_subset(&usages, &[KeyUsage::Sign])?; let keypair = Ed25519KeyPair::from_pkcs8(&der).map_err(|_| { DOMException::new("Invalid Ed25519 private key", DOMExceptionName::DataError) })?; Ok(CryptoKey { kind: CryptoKeyKind::Ed25519Private { algorithm: Ed25519Algorithm {}, key: Ed25519PrivateKey { keypair }, }, r#type: KeyType::Private, extractable, usages, }) }, ImportKeyInput::Spki(der) => { check_usages_subset(&usages, &[KeyUsage::Verify])?; let spki = spki::SubjectPublicKeyInfo::<AnyRef, BitStringRef<'_>>::from_der(&der) .map_err(|_| { DOMException::new( "invalid SubjectPublicKeyInfo document", DOMExceptionName::DataError, ) })?; anyhow::ensure!( spki.algorithm.oid == ED25519_OID, DOMException::new( "SubjectPublicKeyInfo algorithm is not id-Ed25519", DOMExceptionName::DataError ) ); anyhow::ensure!( spki.algorithm.parameters.is_none(), DOMException::new( "SubjectPublicKeyInfo parameters must not be present", DOMExceptionName::DataError ) ); let x = spki .subject_public_key .as_bytes() .and_then(|x| <[u8; 32]>::try_from(x).ok()) .ok_or_else(|| { DOMException::new( "SubjectPublicKeyInfo public key has wrong length", DOMExceptionName::DataError, ) })?; Ok(CryptoKey { kind: CryptoKeyKind::Ed25519Public { algorithm: Ed25519Algorithm {}, key: Ed25519PublicKey { x }, }, r#type: KeyType::Public, extractable, usages, }) }, ImportKeyInput::Jwk(jwk) => { if jwk.d.is_some() { check_usages_subset(&usages, &[KeyUsage::Sign])?; } else { check_usages_subset(&usages, &[KeyUsage::Verify])?; } jwk.check_kty("OKP")?; jwk.check_crv("Ed25519")?; jwk.check_alg_oneof(&["Ed25519", "EdDSA"])?; jwk.check_key_ops_and_use(&usages, "sig")?; jwk.check_ext(extractable)?; let x = jwk .x .as_ref() .and_then(|k| base64::decode_config(k, URL_SAFE_FORGIVING).ok()) .and_then(|x| <[u8; 32]>::try_from(x).ok()) .ok_or_else(|| { anyhow::anyhow!(DOMException::new( "invalid key `x`", DOMExceptionName::DataError )) })?; if let Some(d) = jwk.d { let d = base64::decode_config(&d, URL_SAFE_FORGIVING) .ok() .and_then(|d| <[u8; 32]>::try_from(d).ok()) .ok_or_else(|| { anyhow::anyhow!(DOMException::new( "invalid key `d`", DOMExceptionName::DataError )) })?; let keypair = Ed25519KeyPair::from_seed_and_public_key(&d, &x).map_err(|_| { DOMException::new("JWT `d` and `x` do not match", DOMExceptionName::DataError) })?; Ok(CryptoKey { kind: CryptoKeyKind::Ed25519Private { algorithm: Ed25519Algorithm {}, key: Ed25519PrivateKey { keypair }, }, r#type: KeyType::Private, extractable, usages, }) } else { Ok(CryptoKey { kind: CryptoKeyKind::Ed25519Public { algorithm: Ed25519Algorithm {}, key: Ed25519PublicKey { x }, }, r#type: KeyType::Public, extractable, usages, }) } }, } } impl Ed25519PrivateKey { pub(crate) fn export_key(&self, format: KeyFormat) -> anyhow::Result<KeyData> { match format { KeyFormat::Pkcs8 => { // N.B.: WebCrypto spec calls for version 1: // > Let data be an instance of the PrivateKeyInfo ASN.1 structure defined in [RFC5208] with the following properties: // > - Set the version field to 0. // > ... Ok(KeyData::Raw(self.keypair.to_pkcs8v1()?.as_ref().to_vec())) }, KeyFormat::Jwk => { let jwk = JsonWebKey { kty: Some("OKP".to_owned()), alg: Some("Ed25519".to_owned()), crv: Some("Ed25519".to_owned()), x: Some(base64::encode_config( self.keypair.public_key().as_ref(), base64::URL_SAFE_NO_PAD, )), d: Some(base64::encode_config( self.keypair.seed()?.as_be_bytes()?.as_ref(), base64::URL_SAFE_NO_PAD, )), ..Default::default() }; Ok(KeyData::Jwk(jwk)) }, KeyFormat::Raw | KeyFormat::Spki => anyhow::bail!(DOMException::new( "invalid export format for Ed25519 private key", DOMExceptionName::InvalidAccessError )), } } pub(crate) fn sign(&self, data: &[u8]) -> Vec<u8> { self.keypair.sign(data).as_ref().to_vec() } } impl Ed25519PublicKey { pub(crate) fn export_key(&self, format: KeyFormat) -> anyhow::Result<KeyData> { match format { KeyFormat::Spki => { Ok(KeyData::Raw( spki::SubjectPublicKeyInfo { algorithm: spki::AlgorithmIdentifierOwned { // id-Ed25519 oid: ED25519_OID, parameters: None, }, subject_public_key: BitStringRef::from_bytes(&self.x)?, } .to_der()?, )) }, KeyFormat::Jwk => { let jwk = JsonWebKey { kty: Some("OKP".to_owned()), alg: Some("Ed25519".to_owned()), crv: Some("Ed25519".to_owned()), x: Some(base64::encode_config(self.x, base64::URL_SAFE_NO_PAD)), ..Default::default() }; Ok(KeyData::Jwk(jwk)) }, KeyFormat::Raw => Ok(KeyData::Raw(self.x.to_vec())), KeyFormat::Pkcs8 => anyhow::bail!(DOMException::new( "invalid export format for Ed25519 public key", DOMExceptionName::InvalidAccessError )), } } pub fn verify(&self, data: &[u8], signature: &[u8]) -> bool { UnparsedPublicKey::new(&signature::ED25519, self.x) .verify(data, signature) .is_ok() } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server