Skip to content

Commit 784e774

Browse files
Support RSA remote network identity keys (paritytech#423)
Support remote RSA network identity keys to handle connections to nodes with such keys. The support is gated behind `rsa` feature flag and deliberately doesn't enable the use of RSA keys as a local network identity keys. This preserves backward compatibility and allows for the use of lighter `x509-parser` dependency supporting only X.509 parsing and not serialization.
1 parent 077d435 commit 784e774

File tree

7 files changed

+126
-39
lines changed

7 files changed

+126
-39
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ fuzz = ["serde/derive", "serde/rc", "bytes/serde", "dep:serde_millis", "cid/serd
103103
# They are not yet suitable for production use-cases and should be used with caution.
104104
quic = ["dep:webpki", "dep:quinn", "dep:rustls", "dep:ring", "dep:rcgen"]
105105
webrtc = ["dep:str0m"]
106+
rsa = ["dep:ring"]
106107

107108
[profile.release]
108109
debug = true

src/crypto/mod.rs

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
use crate::{error::ParseError, peer_id::*};
2525

2626
pub mod ed25519;
27+
#[cfg(feature = "rsa")]
28+
pub mod rsa;
29+
2730
pub(crate) mod noise;
2831
#[cfg(feature = "quic")]
2932
pub(crate) mod tls;
@@ -39,18 +42,6 @@ pub enum PublicKey {
3942
}
4043

4144
impl PublicKey {
42-
/// Verify a signature for a message using this public key, i.e. check
43-
/// that the signature has been produced by the corresponding
44-
/// private key (authenticity), and that the message has not been
45-
/// tampered with (integrity).
46-
#[must_use]
47-
pub fn verify(&self, msg: &[u8], sig: &[u8]) -> bool {
48-
use PublicKey::*;
49-
match self {
50-
Ed25519(pk) => pk.verify(msg, sig),
51-
}
52-
}
53-
5445
/// Encode the public key into a protobuf structure for storage or
5546
/// exchange with other nodes.
5647
pub fn to_protobuf_encoding(&self) -> Vec<u8> {
@@ -63,16 +54,6 @@ impl PublicKey {
6354
buf
6455
}
6556

66-
/// Decode a public key from a protobuf structure, e.g. read from storage
67-
/// or received from another node.
68-
pub fn from_protobuf_encoding(bytes: &[u8]) -> Result<PublicKey, ParseError> {
69-
use prost::Message;
70-
71-
let pubkey = keys_proto::PublicKey::decode(bytes)?;
72-
73-
pubkey.try_into()
74-
}
75-
7657
/// Convert the `PublicKey` into the corresponding `PeerId`.
7758
pub fn to_peer_id(&self) -> PeerId {
7859
self.into()
@@ -110,3 +91,57 @@ impl From<ed25519::PublicKey> for PublicKey {
11091
PublicKey::Ed25519(public_key)
11192
}
11293
}
94+
95+
/// The public key of a remote node's identity keypair. Supports RSA keys additionally to ed25519.
96+
#[derive(Clone, Debug, PartialEq, Eq)]
97+
pub(crate) enum RemotePublicKey {
98+
/// A public Ed25519 key.
99+
Ed25519(ed25519::PublicKey),
100+
/// A public RSA key.
101+
#[cfg(feature = "rsa")]
102+
Rsa(rsa::PublicKey),
103+
}
104+
105+
impl RemotePublicKey {
106+
/// Verify a signature for a message using this public key, i.e. check
107+
/// that the signature has been produced by the corresponding
108+
/// private key (authenticity), and that the message has not been
109+
/// tampered with (integrity).
110+
#[must_use]
111+
pub fn verify(&self, msg: &[u8], sig: &[u8]) -> bool {
112+
use RemotePublicKey::*;
113+
match self {
114+
Ed25519(pk) => pk.verify(msg, sig),
115+
#[cfg(feature = "rsa")]
116+
Rsa(pk) => pk.verify(msg, sig),
117+
}
118+
}
119+
120+
/// Decode a public key from a protobuf structure, e.g. read from storage
121+
/// or received from another node.
122+
pub fn from_protobuf_encoding(bytes: &[u8]) -> Result<RemotePublicKey, ParseError> {
123+
use prost::Message;
124+
125+
let pubkey = keys_proto::PublicKey::decode(bytes)?;
126+
127+
pubkey.try_into()
128+
}
129+
}
130+
131+
impl TryFrom<keys_proto::PublicKey> for RemotePublicKey {
132+
type Error = ParseError;
133+
134+
fn try_from(pubkey: keys_proto::PublicKey) -> Result<Self, Self::Error> {
135+
let key_type = keys_proto::KeyType::try_from(pubkey.r#type)
136+
.map_err(|_| ParseError::UnknownKeyType(pubkey.r#type))?;
137+
138+
match key_type {
139+
keys_proto::KeyType::Ed25519 =>
140+
ed25519::PublicKey::try_from_bytes(&pubkey.data).map(RemotePublicKey::Ed25519),
141+
#[cfg(feature = "rsa")]
142+
keys_proto::KeyType::Rsa =>
143+
rsa::PublicKey::try_decode_x509(&pubkey.data).map(RemotePublicKey::Rsa),
144+
_ => Err(ParseError::UnknownKeyType(key_type as i32)),
145+
}
146+
}
147+
}

src/crypto/noise/mod.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
2424
use crate::{
2525
config::Role,
26-
crypto::{ed25519::Keypair, PublicKey},
26+
crypto::{ed25519::Keypair, PublicKey, RemotePublicKey},
2727
error::{NegotiationError, ParseError},
2828
PeerId,
2929
};
@@ -161,9 +161,9 @@ impl NoiseContext {
161161
Self::assemble(noise, keypair, id_keys, Role::Dialer)
162162
}
163163

164-
/// Get remote public key from the received Noise payload.
164+
/// Get remote peer ID from the received Noise payload.
165165
#[cfg(feature = "webrtc")]
166-
pub fn get_remote_public_key(&mut self, reply: &[u8]) -> Result<PublicKey, NegotiationError> {
166+
pub fn get_remote_peer_id(&mut self, reply: &[u8]) -> Result<PeerId, NegotiationError> {
167167
if reply.len() < 2 {
168168
tracing::error!(target: LOG_TARGET, "reply too short to contain length prefix");
169169
return Err(NegotiationError::ParseError(ParseError::InvalidReplyLength));
@@ -191,7 +191,7 @@ impl NoiseContext {
191191
.map_err(|err| NegotiationError::ParseError(err.into()))?;
192192

193193
let identity = payload.identity_key.ok_or(NegotiationError::PeerIdMissing)?;
194-
PublicKey::from_protobuf_encoding(&identity).map_err(|err| err.into())
194+
Ok(PeerId::from_public_key_protobuf(&identity))
195195
}
196196

197197
/// Get first message.
@@ -745,13 +745,13 @@ fn parse_and_verify_peer_id(
745745
dh_remote_pubkey: &[u8],
746746
) -> Result<PeerId, NegotiationError> {
747747
let identity = payload.identity_key.ok_or(NegotiationError::PeerIdMissing)?;
748-
let remote_public_key = PublicKey::from_protobuf_encoding(&identity)?;
748+
let remote_public_key = RemotePublicKey::from_protobuf_encoding(&identity)?;
749749
let remote_key_signature =
750750
payload.identity_sig.ok_or(NegotiationError::BadSignature).inspect_err(|_err| {
751751
tracing::debug!(target: LOG_TARGET, "payload without signature");
752752
})?;
753753

754-
let peer_id = PeerId::from_public_key(&remote_public_key);
754+
let peer_id = PeerId::from_public_key_protobuf(&identity);
755755

756756
if !remote_public_key.verify(
757757
&[STATIC_KEY_DOMAIN.as_bytes(), dh_remote_pubkey].concat(),

src/crypto/rsa.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2025 litep2p developers
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the "Software"),
5+
// to deal in the Software without restriction, including without limitation
6+
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
7+
// and/or sell copies of the Software, and to permit persons to whom the
8+
// Software is furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19+
// DEALINGS IN THE SOFTWARE.
20+
21+
//! RSA public key.
22+
23+
use crate::error::ParseError;
24+
use ring::signature::{UnparsedPublicKey, RSA_PKCS1_2048_8192_SHA256};
25+
use x509_parser::{prelude::FromDer, x509::SubjectPublicKeyInfo};
26+
27+
/// An RSA public key.
28+
#[derive(Clone, Debug, PartialEq, Eq)]
29+
pub struct PublicKey(Vec<u8>);
30+
31+
impl PublicKey {
32+
/// Decode an RSA public key from a DER-encoded X.509 SubjectPublicKeyInfo structure.
33+
pub fn try_decode_x509(spki: &[u8]) -> Result<Self, ParseError> {
34+
SubjectPublicKeyInfo::from_der(spki)
35+
.map(|(_, spki)| Self(spki.subject_public_key.as_ref().to_vec()))
36+
.map_err(|_| ParseError::InvalidPublicKey)
37+
}
38+
39+
/// Verify the RSA signature on a message using the public key.
40+
pub fn verify(&self, msg: &[u8], sig: &[u8]) -> bool {
41+
let key = UnparsedPublicKey::new(&RSA_PKCS1_2048_8192_SHA256, &self.0);
42+
key.verify(msg, sig).is_ok()
43+
}
44+
}

src/crypto/tls/certificate.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
//! This module handles generation, signing, and verification of certificates.
2424
2525
use crate::{
26-
crypto::{ed25519::Keypair, PublicKey},
26+
crypto::{ed25519::Keypair, RemotePublicKey},
2727
PeerId,
2828
};
2929

@@ -102,10 +102,13 @@ pub struct P2pCertificate<'a> {
102102
/// The contents of the specific libp2p extension, containing the public host key
103103
/// and a signature performed using the private host key.
104104
pub struct P2pExtension {
105-
public_key: PublicKey,
105+
public_key: RemotePublicKey,
106106
/// This signature provides cryptographic proof that the peer was
107107
/// in possession of the private host key at the time the certificate was signed.
108108
signature: Vec<u8>,
109+
/// PeerId derived from the public key. While not being part of the extension, we store it to
110+
/// avoid the need to serialize the public key back to protobuf.
111+
peer_id: PeerId,
109112
}
110113

111114
#[derive(Debug, thiserror::Error)]
@@ -148,7 +151,7 @@ fn parse_unverified(der_input: &[u8]) -> Result<P2pCertificate, webpki::Error> {
148151
// publicKey OCTET STRING,
149152
// signature OCTET STRING
150153
// }
151-
let (public_key, signature): (Vec<u8>, Vec<u8>) =
154+
let (public_key_protobuf, signature): (Vec<u8>, Vec<u8>) =
152155
yasna::decode_der(ext.value).map_err(|_| webpki::Error::ExtensionValueInvalid)?;
153156
// The publicKey field of SignedKey contains the public host key
154157
// of the endpoint, encoded using the following protobuf:
@@ -162,11 +165,13 @@ fn parse_unverified(der_input: &[u8]) -> Result<P2pCertificate, webpki::Error> {
162165
// required KeyType Type = 1;
163166
// required bytes Data = 2;
164167
// }
165-
let public_key = PublicKey::from_protobuf_encoding(&public_key)
168+
let public_key = RemotePublicKey::from_protobuf_encoding(&public_key_protobuf)
166169
.map_err(|_| webpki::Error::UnknownIssuer)?;
170+
let peer_id = PeerId::from_public_key_protobuf(&public_key_protobuf);
167171
let ext = P2pExtension {
168172
public_key,
169173
signature,
174+
peer_id,
170175
};
171176
libp2p_extension = Some(ext);
172177
continue;
@@ -231,7 +236,7 @@ fn make_libp2p_extension(
231236
impl P2pCertificate<'_> {
232237
/// The [`PeerId`] of the remote peer.
233238
pub fn peer_id(&self) -> PeerId {
234-
self.extension.public_key.to_peer_id()
239+
self.extension.peer_id
235240
}
236241

237242
/// Verify the `signature` of the `message` signed by the private key corresponding to the
@@ -453,7 +458,7 @@ mod tests {
453458

454459
assert!(parsed_cert.verify().is_ok());
455460
assert_eq!(
456-
crate::crypto::PublicKey::Ed25519(keypair.public()),
461+
crate::crypto::RemotePublicKey::Ed25519(keypair.public()),
457462
parsed_cert.extension.public_key
458463
);
459464
}

src/peer_id.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,18 @@ impl fmt::Display for PeerId {
5959
impl PeerId {
6060
/// Builds a `PeerId` from a public key.
6161
pub fn from_public_key(key: &PublicKey) -> PeerId {
62-
let key_enc = key.to_protobuf_encoding();
62+
Self::from_public_key_protobuf(&key.to_protobuf_encoding())
63+
}
6364

65+
/// Builds a `PeerId` from a public key in protobuf encoding.
66+
pub fn from_public_key_protobuf(key_enc: &[u8]) -> PeerId {
6467
let hash_algorithm = if key_enc.len() <= MAX_INLINE_KEY_LENGTH {
6568
Code::Identity
6669
} else {
6770
Code::Sha2_256
6871
};
6972

70-
let multihash = hash_algorithm.digest(&key_enc);
73+
let multihash = hash_algorithm.digest(key_enc);
7174

7275
PeerId { multihash }
7376
}

src/transport/webrtc/opening.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,7 @@ impl OpeningWebRtcConnection {
254254
};
255255

256256
let message = WebRtcMessage::decode(&data)?.payload.ok_or(Error::InvalidData)?;
257-
let public_key = context.get_remote_public_key(&message)?;
258-
let remote_peer_id = PeerId::from_public_key(&public_key);
257+
let remote_peer_id = context.get_remote_peer_id(&message)?;
259258

260259
tracing::trace!(
261260
target: LOG_TARGET,
@@ -282,7 +281,7 @@ impl OpeningWebRtcConnection {
282281
.with(Protocol::Udp(self.peer_address.port()))
283282
.with(Protocol::WebRTC)
284283
.with(Protocol::Certhash(certificate))
285-
.with(Protocol::P2p(PeerId::from(public_key).into()));
284+
.with(Protocol::P2p(remote_peer_id.into()));
286285

287286
Ok(WebRtcEvent::ConnectionOpened {
288287
peer: remote_peer_id,

0 commit comments

Comments
 (0)