Skip to content

Commit

Permalink
Add Secp256k1 keys (#3345)
Browse files Browse the repository at this point in the history
## Motivation

We want to support secp256k1 signatures - meaning we need public/pivate
keys for them.

## Proposal

Adds `Secp256k1SecretKey` and `Secp256k1PublicKey` structs. Creates a
static `SECP256K1` context for reuse, instead of initializing new
context for every operation. Note that the same effect could be used by
using `global-context` feature of the `secp256k1` crate but that is
mutually exclusive with other features (like `alloc`) that we might want
to use in the future.

## Test Plan

N/A

## Release Plan

- [reviewer
checklist](https://github.com/linera-io/linera-protocol/blob/main/CONTRIBUTING.md#reviewer-checklist)
  • Loading branch information
deuszx authored Feb 20, 2025
1 parent 840430a commit e4d3b92
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 30 deletions.
4 changes: 2 additions & 2 deletions CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ A Byzantine-fault tolerant sidechain with low-latency finality and high throughp
* `create-application` — Create an application
* `publish-and-create` — Create an application, and publish the required bytecode
* `request-application` — Request an application from another chain, so it can be used on this one
* `keygen` — Create an unassigned key-pair
* `keygen` — Create an unassigned key pair
* `assign` — Link an owner with a key pair in the wallet to a chain that was created for that owner
* `retry-pending-block` — Retry a block we unsuccessfully tried to propose earlier
* `wallet` — Show the contents of the wallet
Expand Down Expand Up @@ -700,7 +700,7 @@ Request an application from another chain, so it can be used on this one

## `linera keygen`

Create an unassigned key-pair
Create an unassigned key pair

**Usage:** `linera keygen`

Expand Down
4 changes: 2 additions & 2 deletions examples/Cargo.lock

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

12 changes: 6 additions & 6 deletions linera-base/src/crypto/ed25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};

use super::{
le_bytes_to_u64_array, u64_array_to_le_bytes, BcsHashable, BcsSignable, CryptoError,
CryptoHash, HasTypeName, Hashable, ValidatorPublicKey, ValidatorSignature,
CryptoHash, HasTypeName, Hashable,
};
use crate::{doc_scalar, identifiers::Owner};

Expand All @@ -32,25 +32,25 @@ pub struct Ed25519Signature(pub dalek::Signature);

impl Ed25519SecretKey {
#[cfg(all(with_getrandom, with_testing))]
/// Generates a new key-pair.
/// Generates a new key pair.
pub fn generate() -> Self {
let mut rng = rand::rngs::OsRng;
Self::generate_from(&mut rng)
}

#[cfg(with_getrandom)]
/// Generates a new key-pair from the given RNG. Use with care.
/// Generates a new key pair from the given RNG. Use with care.
pub fn generate_from<R: super::CryptoRng>(rng: &mut R) -> Self {
let keypair = dalek::SigningKey::generate(rng);
Ed25519SecretKey(keypair)
}

/// Obtains the public key of a key-pair.
/// Obtains the public key of a key pair.
pub fn public(&self) -> Ed25519PublicKey {
Ed25519PublicKey(self.0.verifying_key().to_bytes())
}

/// Copies the key-pair, **including the secret key**.
/// Copies the key pair, **including the secret key**.
///
/// The `Clone` and `Copy` traits are deliberately not implemented for `KeyPair` to prevent
/// accidental copies of secret keys.
Expand Down Expand Up @@ -330,7 +330,7 @@ impl Ed25519Signature {
pub fn verify_batch<'a, 'de, T, I>(value: &'a T, votes: I) -> Result<(), CryptoError>
where
T: BcsSignable<'de>,
I: IntoIterator<Item = (&'a ValidatorPublicKey, &'a ValidatorSignature)>,
I: IntoIterator<Item = (&'a Ed25519PublicKey, &'a Ed25519Signature)>,
{
Ed25519Signature::verify_batch_internal(value, votes).map_err(|error| {
CryptoError::InvalidSignature {
Expand Down
2 changes: 2 additions & 0 deletions linera-base/src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ pub enum CryptoError {
IncorrectPublicKeySize(usize),
#[error("Could not parse integer: {0}")]
ParseIntError(#[from] ParseIntError),
#[error("secp256k1 error: {0}")]
Secp256k1Error(::secp256k1::Error),
}

#[cfg(with_getrandom)]
Expand Down
152 changes: 136 additions & 16 deletions linera-base/src/crypto/secp256k1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,156 @@

//! Defines secp256k1 signature primitives used by the Linera protocol.
use std::fmt;
use std::{fmt, str::FromStr, sync::LazyLock};

use secp256k1::{self, Message};
use secp256k1::{self, All, Message, Secp256k1};
use serde::{Deserialize, Serialize};

use super::{BcsSignable, CryptoError, CryptoHash, HasTypeName};
use crate::doc_scalar;

/// Static secp256k1 context for reuse.
pub static SECP256K1: LazyLock<Secp256k1<All>> = LazyLock::new(secp256k1::Secp256k1::new);

/// A secp256k1 secret key.
#[derive(Eq, PartialEq)]
pub struct Secp256k1SecretKey(pub secp256k1::SecretKey);

/// A secp256k1 public key.
#[derive(Eq, PartialEq, Copy, Clone)]
pub struct Secp256k1PublicKey(pub secp256k1::PublicKey);

/// Secp256k1 public/secret key pair.
#[derive(Debug, PartialEq, Eq)]
pub struct Secp256k1KeyPair {
/// Secret key.
pub secret_key: Secp256k1SecretKey,
/// Public key.
pub public_key: Secp256k1PublicKey,
}

/// A secp256k1 signature.
#[derive(Eq, PartialEq, Copy, Clone)]
pub struct Secp256k1Signature(pub secp256k1::ecdsa::Signature);

impl fmt::Debug for Secp256k1SecretKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<redacted for Secp256k1 secret key>")
}
}

impl Serialize for Secp256k1PublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
if serializer.is_human_readable() {
serializer.serialize_str(&hex::encode(self.0.serialize()))
} else {
serializer.serialize_newtype_struct("Secp256k1PublicKey", &self.0)
}
}
}

impl<'de> Deserialize<'de> for Secp256k1PublicKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
if deserializer.is_human_readable() {
let s = String::deserialize(deserializer)?;
let value = hex::decode(s).map_err(serde::de::Error::custom)?;
let pk = secp256k1::PublicKey::from_slice(&value).map_err(serde::de::Error::custom)?;
Ok(Secp256k1PublicKey(pk))
} else {
#[derive(Deserialize)]
#[serde(rename = "Secp256k1PublicKey")]
struct Foo(secp256k1::PublicKey);

let value = Foo::deserialize(deserializer)?;
Ok(Self(value.0))
}
}
}

impl FromStr for Secp256k1PublicKey {
type Err = CryptoError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let pk = secp256k1::PublicKey::from_str(s).map_err(CryptoError::Secp256k1Error)?;
Ok(Secp256k1PublicKey(pk))
}
}

impl TryFrom<&[u8]> for Secp256k1PublicKey {
type Error = CryptoError;

fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
let pk = secp256k1::PublicKey::from_slice(value).map_err(CryptoError::Secp256k1Error)?;
Ok(Secp256k1PublicKey(pk))
}
}

impl fmt::Display for Secp256k1PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = hex::encode(self.0.serialize());
write!(f, "{}", s)
}
}

impl fmt::Debug for Secp256k1PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", hex::encode(&self.0.serialize()[0..9]))
}
}

impl Secp256k1KeyPair {
/// Generates a new key pair.
#[cfg(all(with_getrandom, with_testing))]
pub fn generate() -> Self {
let mut rng = rand::rngs::OsRng;
Self::generate_from(&mut rng)
}

/// Generates a new key pair from the given RNG. Use with care.
#[cfg(with_getrandom)]
pub fn generate_from<R: super::CryptoRng>(rng: &mut R) -> Self {
let (sk, pk) = SECP256K1.generate_keypair(rng);
Secp256k1KeyPair {
secret_key: Secp256k1SecretKey(sk),
public_key: Secp256k1PublicKey(pk),
}
}
}

impl Secp256k1SecretKey {
/// Returns a public key for the given secret key.
pub fn to_public(&self) -> Secp256k1PublicKey {
Secp256k1PublicKey(self.0.public_key(&SECP256K1))
}
}

impl Secp256k1Signature {
/// Computes a secp256k1 signature for `value` using the given `secret`.
/// It first serializes the `T` type and then creates the `CryptoHash` from the serialized bytes.
pub fn new<'de, T>(value: &T, secret: &secp256k1::SecretKey) -> Self
pub fn new<'de, T>(value: &T, secret: &Secp256k1SecretKey) -> Self
where
T: BcsSignable<'de>,
{
let secp = secp256k1::Secp256k1::new();
let secp = secp256k1::Secp256k1::signing_only();
let message = Message::from_digest(CryptoHash::new(value).as_bytes().0);
let signature = secp.sign_ecdsa(&message, secret);
let signature = secp.sign_ecdsa(&message, &secret.0);
Secp256k1Signature(signature)
}

/// Checks a signature.
pub fn check<'de, T>(&self, value: &T, author: &secp256k1::PublicKey) -> Result<(), CryptoError>
pub fn check<'de, T>(&self, value: &T, author: &Secp256k1PublicKey) -> Result<(), CryptoError>
where
T: BcsSignable<'de> + fmt::Debug,
{
let secp = secp256k1::Secp256k1::new();
let message = Message::from_digest(CryptoHash::new(value).as_bytes().0);
secp.verify_ecdsa(&message, &self.0, author)
SECP256K1
.verify_ecdsa(&message, &self.0, &author.0)
.map_err(|error| CryptoError::InvalidSignature {
error: error.to_string(),
type_name: T::type_name().to_string(),
Expand Down Expand Up @@ -100,24 +217,27 @@ mod secp256k1_tests {
fn test_signatures() {
use serde::{Deserialize, Serialize};

use crate::crypto::{secp256k1::Secp256k1Signature, BcsSignable, TestString};
use crate::crypto::{
secp256k1::{Secp256k1KeyPair, Secp256k1Signature},
BcsSignable, TestString,
};

#[derive(Debug, Serialize, Deserialize)]
struct Foo(String);

impl<'de> BcsSignable<'de> for Foo {}

let (sk1, pk1) = secp256k1::Secp256k1::new().generate_keypair(&mut rand::thread_rng());
let (_sk2, pk2) = secp256k1::Secp256k1::new().generate_keypair(&mut rand::thread_rng());
let keypair1 = Secp256k1KeyPair::generate();
let keypair2 = Secp256k1KeyPair::generate();

let ts = TestString("hello".into());
let tsx = TestString("hellox".into());
let foo = Foo("hello".into());

let s = Secp256k1Signature::new(&ts, &sk1);
assert!(s.check(&ts, &pk1).is_ok());
assert!(s.check(&ts, &pk2).is_err());
assert!(s.check(&tsx, &pk1).is_err());
assert!(s.check(&foo, &pk1).is_err());
let s = Secp256k1Signature::new(&ts, &keypair1.secret_key);
assert!(s.check(&ts, &keypair1.public_key).is_ok());
assert!(s.check(&ts, &keypair2.public_key).is_err());
assert!(s.check(&tsx, &keypair1.public_key).is_err());
assert!(s.check(&foo, &keypair1.public_key).is_err());
}
}
4 changes: 1 addition & 3 deletions linera-chain/src/unit_tests/data_types_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,8 @@ fn test_certificates() {
let key1 = ValidatorSecretKey::generate();
let key2 = ValidatorSecretKey::generate();
let key3 = ValidatorSecretKey::generate();
let validator1_pk = key1.public();
let validator2_pk = key2.public();

let committee = Committee::make_simple(vec![validator1_pk, validator2_pk]);
let committee = Committee::make_simple(vec![key1.public(), key2.public()]);

let block =
make_first_block(ChainId::root(1)).with_simple_transfer(ChainId::root(1), Amount::ONE);
Expand Down
2 changes: 1 addition & 1 deletion linera-client/src/client_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,7 +875,7 @@ pub enum ClientCommand {
requester_chain_id: Option<ChainId>,
},

/// Create an unassigned key-pair.
/// Create an unassigned key pair.
Keygen,

/// Link an owner with a key pair in the wallet to a chain that was created for that owner.
Expand Down

0 comments on commit e4d3b92

Please sign in to comment.