Skip to content

Commit 5cd7a9c

Browse files
authored
keypair / signature / ed25519: Deprecate dalek types from public API (#107)
* ed25519-program: Deprecate function taking dalek keypair * signature: Add opaque error for signing uses * keypair: Remove dalek crates from API
1 parent c0f9a14 commit 5cd7a9c

File tree

6 files changed

+151
-30
lines changed

6 files changed

+151
-30
lines changed

ed25519-program/src/lib.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub struct Ed25519SignatureOffsets {
3737
/// buffer.
3838
///
3939
/// Note: If the signer for these messages are the same, it is cheaper to concatenate the messages
40-
/// and have the signer sign the single buffer and use [`new_ed25519_instruction`].
40+
/// and have the signer sign the single buffer and use [`new_ed25519_instruction_with_signature`].
4141
pub fn offsets_to_ed25519_instruction(offsets: &[Ed25519SignatureOffsets]) -> Instruction {
4242
let mut instruction_data = Vec::with_capacity(
4343
SIGNATURE_OFFSETS_START
@@ -58,13 +58,18 @@ pub fn offsets_to_ed25519_instruction(offsets: &[Ed25519SignatureOffsets]) -> In
5858
}
5959
}
6060

61+
#[deprecated(since = "2.2.3", note = "Use new_ed25519_instruction_with_signature")]
6162
pub fn new_ed25519_instruction(keypair: &ed25519_dalek::Keypair, message: &[u8]) -> Instruction {
6263
let signature = keypair.sign(message).to_bytes();
6364
let pubkey = keypair.public.to_bytes();
65+
new_ed25519_instruction_with_signature(message, &signature, &pubkey)
66+
}
6467

65-
assert_eq!(pubkey.len(), PUBKEY_SERIALIZED_SIZE);
66-
assert_eq!(signature.len(), SIGNATURE_SERIALIZED_SIZE);
67-
68+
pub fn new_ed25519_instruction_with_signature(
69+
message: &[u8],
70+
signature: &[u8; SIGNATURE_SERIALIZED_SIZE],
71+
pubkey: &[u8; PUBKEY_SERIALIZED_SIZE],
72+
) -> Instruction {
6873
let mut instruction_data = Vec::with_capacity(
6974
DATA_START
7075
.saturating_add(SIGNATURE_SERIALIZED_SIZE)
@@ -94,11 +99,11 @@ pub fn new_ed25519_instruction(keypair: &ed25519_dalek::Keypair, message: &[u8])
9499

95100
debug_assert_eq!(instruction_data.len(), public_key_offset);
96101

97-
instruction_data.extend_from_slice(&pubkey);
102+
instruction_data.extend_from_slice(pubkey);
98103

99104
debug_assert_eq!(instruction_data.len(), signature_offset);
100105

101-
instruction_data.extend_from_slice(&signature);
106+
instruction_data.extend_from_slice(signature);
102107

103108
debug_assert_eq!(instruction_data.len(), message_data_offset);
104109

@@ -444,7 +449,10 @@ pub mod test {
444449

445450
let privkey = ed25519_dalek::Keypair::generate(&mut thread_rng());
446451
let message_arr = b"hello";
447-
let mut instruction = new_ed25519_instruction(&privkey, message_arr);
452+
let signature = privkey.sign(message_arr).to_bytes();
453+
let pubkey = privkey.public.to_bytes();
454+
let mut instruction =
455+
new_ed25519_instruction_with_signature(message_arr, &signature, &pubkey);
448456
let mint_keypair = Keypair::new();
449457
let feature_set = FeatureSet::all_enabled();
450458

keypair/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ solana-derivation-path = { workspace = true, optional = true }
1818
solana-pubkey = { workspace = true }
1919
solana-seed-derivable = { workspace = true, optional = true }
2020
solana-seed-phrase = { workspace = true }
21-
solana-signature = { workspace = true, features = ["verify"] }
21+
solana-signature = { workspace = true, features = ["std", "verify"] }
2222
solana-signer = { workspace = true }
2323

2424
[target.'cfg(target_arch = "wasm32")'.dependencies]

keypair/src/lib.rs

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use {
77
rand0_7::{rngs::OsRng, CryptoRng, RngCore},
88
solana_pubkey::Pubkey,
99
solana_seed_phrase::generate_seed_from_seed_phrase_and_passphrase,
10-
solana_signature::Signature,
10+
solana_signature::{error::Error as SignatureError, Signature},
1111
solana_signer::{EncodableKey, EncodableKeypair, Signer, SignerError},
1212
std::{
1313
error,
@@ -25,6 +25,8 @@ pub mod signable;
2525
#[derive(Debug)]
2626
pub struct Keypair(ed25519_dalek::Keypair);
2727

28+
pub const KEYPAIR_LENGTH: usize = 64;
29+
2830
impl Keypair {
2931
/// Can be used for generating a Keypair without a dependency on `rand` types
3032
pub const SECRET_KEY_LENGTH: usize = 32;
@@ -44,34 +46,21 @@ impl Keypair {
4446
}
4547

4648
/// Recovers a `Keypair` from a byte array
49+
#[deprecated(since = "2.2.2", note = "Use Keypair::try_from(&[u8]) instead")]
4750
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ed25519_dalek::SignatureError> {
48-
if bytes.len() < ed25519_dalek::KEYPAIR_LENGTH {
49-
return Err(ed25519_dalek::SignatureError::from_source(String::from(
50-
"candidate keypair byte array is too short",
51-
)));
52-
}
53-
let secret =
54-
ed25519_dalek::SecretKey::from_bytes(&bytes[..ed25519_dalek::SECRET_KEY_LENGTH])?;
55-
let public =
56-
ed25519_dalek::PublicKey::from_bytes(&bytes[ed25519_dalek::SECRET_KEY_LENGTH..])?;
57-
let expected_public = ed25519_dalek::PublicKey::from(&secret);
58-
(public == expected_public)
59-
.then_some(Self(ed25519_dalek::Keypair { secret, public }))
60-
.ok_or(ed25519_dalek::SignatureError::from_source(String::from(
61-
"keypair bytes do not specify same pubkey as derived from their secret key",
62-
)))
51+
Self::try_from(bytes).map_err(ed25519_dalek::SignatureError::from_source)
6352
}
6453

6554
/// Returns this `Keypair` as a byte array
66-
pub fn to_bytes(&self) -> [u8; 64] {
55+
pub fn to_bytes(&self) -> [u8; KEYPAIR_LENGTH] {
6756
self.0.to_bytes()
6857
}
6958

7059
/// Recovers a `Keypair` from a base58-encoded string
7160
pub fn from_base58_string(s: &str) -> Self {
7261
let mut buf = [0u8; ed25519_dalek::KEYPAIR_LENGTH];
7362
bs58::decode(s).onto(&mut buf).unwrap();
74-
Self::from_bytes(&buf).unwrap()
63+
Self::try_from(&buf[..]).unwrap()
7564
}
7665

7766
/// Returns this `Keypair` as a base58-encoded string
@@ -80,10 +69,16 @@ impl Keypair {
8069
}
8170

8271
/// Gets this `Keypair`'s SecretKey
72+
#[deprecated(since = "2.2.2", note = "Use secret_bytes()")]
8373
pub fn secret(&self) -> &ed25519_dalek::SecretKey {
8474
&self.0.secret
8575
}
8676

77+
/// Gets this `Keypair`'s secret key bytes
78+
pub fn secret_bytes(&self) -> &[u8; Self::SECRET_KEY_LENGTH] {
79+
self.0.secret.as_bytes()
80+
}
81+
8782
/// Allows Keypair cloning
8883
///
8984
/// Note that the `Clone` trait is intentionally unimplemented because making a
@@ -100,6 +95,30 @@ impl Keypair {
10095
}
10196
}
10297

98+
impl TryFrom<&[u8]> for Keypair {
99+
type Error = SignatureError;
100+
101+
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
102+
if bytes.len() < ed25519_dalek::KEYPAIR_LENGTH {
103+
return Err(SignatureError::from_source(String::from(
104+
"candidate keypair byte array is too short",
105+
)));
106+
}
107+
let secret =
108+
ed25519_dalek::SecretKey::from_bytes(&bytes[..ed25519_dalek::SECRET_KEY_LENGTH])
109+
.map_err(SignatureError::from_source)?;
110+
let public =
111+
ed25519_dalek::PublicKey::from_bytes(&bytes[ed25519_dalek::SECRET_KEY_LENGTH..])
112+
.map_err(SignatureError::from_source)?;
113+
let expected_public = ed25519_dalek::PublicKey::from(&secret);
114+
(public == expected_public)
115+
.then_some(Self(ed25519_dalek::Keypair { secret, public }))
116+
.ok_or(SignatureError::from_source(String::from(
117+
"keypair bytes do not specify same pubkey as derived from their secret key",
118+
)))
119+
}
120+
}
121+
103122
#[cfg(target_arch = "wasm32")]
104123
#[allow(non_snake_case)]
105124
#[wasm_bindgen]
@@ -117,7 +136,7 @@ impl Keypair {
117136

118137
/// Recover a `Keypair` from a `Uint8Array`
119138
pub fn fromBytes(bytes: &[u8]) -> Result<Keypair, JsValue> {
120-
Keypair::from_bytes(bytes).map_err(|e| e.to_string().into())
139+
Keypair::try_from(bytes).map_err(|e| e.to_string().into())
121140
}
122141

123142
/// Return the `Pubkey` for this `Keypair`
@@ -128,6 +147,9 @@ impl Keypair {
128147
}
129148
}
130149

150+
// This should also be marked deprecated, but it's not possible to put a
151+
// `#[deprecated]` attribute on a trait implementation.
152+
// Remove during the next major version bump.
131153
impl From<ed25519_dalek::Keypair> for Keypair {
132154
fn from(value: ed25519_dalek::Keypair) -> Self {
133155
Self(value)
@@ -223,7 +245,7 @@ pub fn read_keypair<R: Read>(reader: &mut R) -> Result<Keypair, Box<dyn error::E
223245
let parsed: u8 = element.parse()?;
224246
out[idx] = parsed;
225247
}
226-
Keypair::from_bytes(&out)
248+
Keypair::try_from(&out[..])
227249
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()).into())
228250
}
229251

signature/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,16 @@ solana-short-vec = { workspace = true }
3636
solana-signature = { path = ".", features = ["serde"] }
3737

3838
[features]
39-
default = ["std"]
39+
default = ["std", "alloc"]
40+
alloc = []
4041
frozen-abi = [
4142
"dep:solana-frozen-abi",
4243
"dep:solana-frozen-abi-macro",
4344
"std"
4445
]
4546
rand = ["dep:rand"]
4647
serde = ["dep:serde", "dep:serde_derive", "dep:serde-big-array"]
47-
std = []
48+
std = ["alloc"]
4849
verify = ["dep:ed25519-dalek"]
4950

5051
[package.metadata.docs.rs]

signature/src/error.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//! Signature error copied directly from RustCrypto's opaque signature error at
2+
//! https://github.com/RustCrypto/traits/tree/master/signature
3+
4+
#[cfg(feature = "alloc")]
5+
use alloc::boxed::Box;
6+
use core::fmt::{self, Debug, Display};
7+
8+
/// Signature errors.
9+
///
10+
/// This type is deliberately opaque as to avoid sidechannel leakage which
11+
/// could potentially be used recover signing private keys or forge signatures
12+
/// (e.g. [BB'06]).
13+
///
14+
/// When the `std` feature is enabled, it impls [`std::error::Error`].
15+
///
16+
/// When the `alloc` feature is enabled, it supports an optional
17+
/// [`std::error::Error::source`], which can be used by things like remote
18+
/// signers (e.g. HSM, KMS) to report I/O or auth errors.
19+
///
20+
/// [BB'06]: https://en.wikipedia.org/wiki/Daniel_Bleichenbacher
21+
#[derive(Default)]
22+
#[non_exhaustive]
23+
pub struct Error {
24+
/// Source of the error (if applicable).
25+
#[cfg(feature = "std")]
26+
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
27+
}
28+
29+
impl Error {
30+
/// Create a new error with an associated source.
31+
///
32+
/// **NOTE:** The "source" should **NOT** be used to propagate cryptographic
33+
/// errors e.g. signature parsing or verification errors. The intended use
34+
/// cases are for propagating errors related to external signers, e.g.
35+
/// communication/authentication errors with HSMs, KMS, etc.
36+
#[cfg(feature = "std")]
37+
pub fn from_source(
38+
source: impl Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
39+
) -> Self {
40+
Self {
41+
source: Some(source.into()),
42+
}
43+
}
44+
}
45+
46+
impl Debug for Error {
47+
#[cfg(not(feature = "std"))]
48+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49+
f.write_str("signature::Error {}")
50+
}
51+
52+
#[cfg(feature = "std")]
53+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54+
f.write_str("signature::Error { source: ")?;
55+
56+
if let Some(source) = &self.source {
57+
write!(f, "Some({})", source)?;
58+
} else {
59+
f.write_str("None")?;
60+
}
61+
62+
f.write_str(" }")
63+
}
64+
}
65+
66+
impl Display for Error {
67+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68+
f.write_str("signature error")
69+
}
70+
}
71+
72+
#[cfg(feature = "std")]
73+
impl From<Box<dyn std::error::Error + Send + Sync + 'static>> for Error {
74+
fn from(source: Box<dyn std::error::Error + Send + Sync + 'static>) -> Error {
75+
Self::from_source(source)
76+
}
77+
}
78+
79+
#[cfg(feature = "std")]
80+
impl std::error::Error for Error {
81+
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
82+
self.source
83+
.as_ref()
84+
.map(|source| source.as_ref() as &(dyn core::error::Error + 'static))
85+
}
86+
}

signature/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use core::{
88
fmt,
99
str::{from_utf8, FromStr},
1010
};
11+
#[cfg(feature = "alloc")]
12+
extern crate alloc;
1113
#[cfg(feature = "std")]
1214
extern crate std;
1315
#[cfg(feature = "std")]
@@ -18,6 +20,8 @@ use {
1820
serde_derive::{Deserialize, Serialize},
1921
};
2022

23+
pub mod error;
24+
2125
/// Number of bytes in a signature
2226
pub const SIGNATURE_BYTES: usize = 64;
2327
/// Maximum string length of a base58 encoded signature

0 commit comments

Comments
 (0)