Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

did-simple: implement ed25519 sign and verify #105

Merged
merged 1 commit into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions crates/did-simple/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ description = "Dead simple DIDs"
publish = true

[features]
default = ["ed25519"]
default = ["ed25519", "random"]
ed25519 = [
"dep:curve25519-dalek",
"dep:ed25519-dalek",
]
random = ["dep:rand_core", "ed25519-dalek?/rand_core"]

# Only applications should enable this! If you use did-simple as a dependency,
# don't enable this feature - let applications set it instead.
Expand All @@ -24,8 +25,9 @@ allow-unsafe = []
bs58 = "0.5.1"
bytes = "1.6.0"
thiserror = "1.0.60"
ed25519-dalek = { version = "2.1.1", optional = true }
ed25519-dalek = { version = "2.1.1", optional = true, features = ["digest"] }
curve25519-dalek = { version = "4.1.2", optional = true }
rand_core = { version = "0.6.4", optional = true, features = ["getrandom"] }

[dev-dependencies]
eyre = "0.6.12"
Expand Down
202 changes: 189 additions & 13 deletions crates/did-simple/src/crypto/ed25519.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
//! Implementation of the ed25519ph signing algorithm.

use curve25519_dalek::edwards::CompressedEdwardsY;
use ed25519_dalek::VerifyingKey;

use crate::key_algos::StaticKeyAlgo as _;
use super::Context;
use crate::key_algos::StaticSigningAlgo as _;

pub use ed25519_dalek::{ed25519::Signature, Digest, Sha512, SignatureError};

/// An ed25519 public key.
#[allow(dead_code)]
pub struct PubKey(VerifyingKey);
/// Re-exported for lower level control
pub use ed25519_dalek;

impl PubKey {
/// An ed25519 public key. Used for verifying messages.
///
/// We recommend deserializing bytes into this type using
/// [`Self::try_from_bytes()`]. Then you can either use this type, which has
/// simpler function signatures, or you can call [`Self::into_inner()`] and use
/// the lower level [`ed25519_dalek`] crate directly, which is slightly less
/// opinionated and has more customization of options made available.
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct VerifyingKey(ed25519_dalek::VerifyingKey);

impl VerifyingKey {
pub const LEN: usize = Self::key_len();

/// Instantiates `PubKey` from some bytes. Performs all necessary validation
/// that the key is valid and of sufficient strength.
///
/// Note that we will reject any keys that are too weak (aka low order).
pub fn try_from(bytes: &[u8; Self::LEN]) -> Result<Self, TryFromBytesError> {
pub fn try_from_bytes(bytes: &[u8; Self::LEN]) -> Result<Self, TryFromBytesError> {
let compressed_edwards = CompressedEdwardsY(bytes.to_owned());
let Some(edwards) = compressed_edwards.decompress() else {
return Err(TryFromBytesError::NotOnCurve);
};
let key = VerifyingKey::from(edwards);
let key = ed25519_dalek::VerifyingKey::from(edwards);
if key.is_weak() {
return Err(TryFromBytesError::WeakKey);
}
Expand All @@ -28,10 +41,175 @@ impl PubKey {

// TODO: Turn this into inline const when that feature stabilizes
const fn key_len() -> usize {
let len = crate::key_algos::Ed25519::PUB_KEY_LEN;
let len = crate::key_algos::Ed25519::VERIFYING_KEY_LEN;
assert!(len == ed25519_dalek::PUBLIC_KEY_LENGTH);
len
}

pub fn into_inner(self) -> ed25519_dalek::VerifyingKey {
self.0
}

/// Verifies `message` using the ed25519ph algorithm.
///
/// # Example
/// ```
/// use did_simple::crypto::{Context, ed25519::{SigningKey, VerifyingKey}};
///
/// let signing_key = SigningKey::random();
/// let verifying_key = signing_key.verifying_key();
/// const CTX: Context = Context::from_bytes("MySuperCoolProtocol".as_bytes());
///
/// let msg = "everyone can read and verify this message";
/// let sig = signing_key.sign(msg, CTX);
///
/// assert!(verifying_key.verify(msg, CTX, &sig).is_ok());
/// ```
pub fn verify(
&self,
message: impl AsRef<[u8]>,
context: Context,
signature: &Signature,
) -> Result<(), SignatureError> {
let digest = Sha512::new().chain_update(message);
self.verify_digest(digest, context, signature)
}

/// Same as `verify`, but allows you to populate `message_digest` separately
/// from signing.
///
/// This can be useful if for example, it is undesirable to buffer the message
/// into a single slice, or the message is being streamed asynchronously.
/// You can instead update the digest chunk by chunk, and pass the digest
/// in after you are done reading all the data.
///
/// # Example
///
/// ```
/// use did_simple::crypto::{Context, ed25519::{Sha512, Digest, SigningKey, VerifyingKey}};
///
/// let signing_key = SigningKey::random();
/// let verifying_key = signing_key.verifying_key();
/// const CTX: Context = Context::from_bytes("MySuperCoolProtocol".as_bytes());
///
/// let sig = signing_key.sign("this is my message", CTX);
///
/// let mut digest = Sha512::new();
/// digest.update("this is ");
/// digest.update("my message");
/// assert!(verifying_key.verify_digest(digest, CTX, &sig).is_ok());
pub fn verify_digest(
&self,
message_digest: Sha512,
context: Context,
signature: &Signature,
) -> Result<(), SignatureError> {
self.0
.verify_prehashed_strict(message_digest, Some(context.0), signature)
}
}

impl TryFrom<ed25519_dalek::VerifyingKey> for VerifyingKey {
type Error = TryFromBytesError;

fn try_from(value: ed25519_dalek::VerifyingKey) -> Result<Self, Self::Error> {
Self::try_from_bytes(value.as_bytes())
}
}

/// An ed25519 private key. Used for signing messages.
#[derive(Debug)]
pub struct SigningKey(ed25519_dalek::SigningKey);

impl SigningKey {
pub const LEN: usize = Self::key_len();

pub fn from_bytes(bytes: &[u8; Self::LEN]) -> Self {
let signing = ed25519_dalek::SigningKey::from_bytes(bytes);
let _pub = VerifyingKey::try_from(signing.verifying_key())
.expect("this should never fail. if it does, please open an issue");
Self(signing)
}

// TODO: Turn this into inline const when that feature stabilizes
const fn key_len() -> usize {
let len = crate::key_algos::Ed25519::SIGNING_KEY_LEN;
assert!(len == ed25519_dalek::SECRET_KEY_LENGTH);
len
}

pub fn into_inner(self) -> ed25519_dalek::SigningKey {
self.0
}

/// Generates a new random [`SigningKey`].
#[cfg(feature = "random")]
pub fn random() -> Self {
let mut csprng = rand_core::OsRng;
Self(ed25519_dalek::SigningKey::generate(&mut csprng))
}

/// Generates a new random [`SigningKey`] from the given RNG.
#[cfg(feature = "random")]
pub fn random_from_rng<R: rand_core::CryptoRngCore + ?Sized>(rng: &mut R) -> Self {
Self(ed25519_dalek::SigningKey::generate(rng))
}

/// Gets the public [`VerifyingKey`] that corresponds to this private [`SigningKey`].
pub fn verifying_key(&self) -> VerifyingKey {
VerifyingKey::try_from(self.0.verifying_key())
.expect("this should never fail. if it does, please open an issue")
}

/// Signs `message` using the ed25519ph algorithm.
///
/// # Example
///
/// ```
/// use did_simple::crypto::{Context, ed25519::{SigningKey, VerifyingKey}};
///
/// let signing_key = SigningKey::random();
/// let verifying_key = signing_key.verifying_key();
/// const CTX: Context = Context::from_bytes("MySuperCoolProtocol".as_bytes());
///
/// let msg = "everyone can read and verify this message";
/// let sig = signing_key.sign(msg, CTX);
///
/// assert!(verifying_key.verify(msg, CTX, &sig).is_ok());
/// ```
pub fn sign(&self, message: impl AsRef<[u8]>, context: Context) -> Signature {
let digest = Sha512::new().chain_update(message);
self.sign_digest(digest, context)
}

/// Same as `sign`, but allows you to populate `message_digest` separately
/// from signing.
///
/// This can be useful if for example, it is undesirable to buffer the message
/// into a single slice, or the message is being streamed asynchronously.
/// You can instead update the digest chunk by chunk, and pass the digest
/// in after you are done reading all the data.
///
/// # Example
///
/// ```
/// use did_simple::crypto::{Context, ed25519::{Sha512, Digest, SigningKey, VerifyingKey}};
///
/// let signing_key = SigningKey::random();
/// let verifying_key = signing_key.verifying_key();
/// const CTX: Context = Context::from_bytes("MySuperCoolProtocol".as_bytes());
///
/// let mut digest = Sha512::new();
/// digest.update("this is ");
/// digest.update("my message");
/// let sig = signing_key.sign_digest(digest, CTX);
///
/// assert!(verifying_key.verify("this is my message", CTX, &sig).is_ok());
pub fn sign_digest(&self, message_digest: Sha512, context: Context) -> Signature {
self.0
.sign_prehashed(message_digest, Some(context.0))
.expect("this should never fail. if it does, please open an issue")
}
}

#[derive(thiserror::Error, Debug)]
Expand All @@ -44,7 +222,5 @@ pub enum TryFromBytesError {
WeakKey,
}

/// Errors which may occur while processing signatures and keypairs.
#[derive(thiserror::Error, Debug)]
#[error("invalid signature")]
pub struct SignatureError(#[from] ed25519_dalek::SignatureError);
#[cfg(test)]
mod test {}
Loading
Loading