Skip to content

Commit

Permalink
feat ability to load pkcs8 key from file
Browse files Browse the repository at this point in the history
  • Loading branch information
Joonas Koivunen committed Dec 13, 2019
1 parent b78ad78 commit dd61207
Showing 1 changed file with 156 additions and 21 deletions.
177 changes: 156 additions & 21 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use libp2p::{Multiaddr, PeerId};
use libp2p::multiaddr::Protocol;
use libp2p::identity::Keypair;
use libp2p::identity::{Keypair, PublicKey};
use rand::{Rng, rngs::EntropyRng};
use serde_derive::{Serialize, Deserialize};
use std::fs;
Expand All @@ -18,39 +18,139 @@ const BOOTSTRAP_NODES: &[&'static str] = &[
"/ip6/2a03:b0c0:0:1010::23:1001/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd",
];

/// See test cases for examples how to write such file.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConfigFile {
private_key: [u8; 32],
#[serde(flatten)]
key: KeyMaterial,
bootstrap: Vec<Multiaddr>,
}

/// KeyMaterial is an additional abstraction as the identity::Keypair does not support Clone or
/// Debug nor is there a clearcut way to serialize and deserialize such yet at least.
#[derive(Clone, Serialize, Deserialize)]
#[serde(untagged)]
enum KeyMaterial {
Ed25519 {
private_key: [u8; 32],
#[serde(skip)]
keypair: Option<libp2p::identity::ed25519::Keypair>,
},
RsaPkcs8File {
#[serde(rename = "rsa_pkcs8_filename")]
filename: String,
#[serde(skip)]
keypair: Option<libp2p::identity::rsa::Keypair>,
},
}

use std::fmt;

impl fmt::Debug for KeyMaterial {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match *self {
KeyMaterial::Ed25519 { ref keypair, .. } =>
if let Some(kp) = keypair.as_ref() {
write!(fmt, "{:?}", kp)
} else {
write!(fmt, "Ed25519(not loaded)")
},
KeyMaterial::RsaPkcs8File { ref keypair, ref filename } =>
if let Some(kp) = keypair.as_ref() {
write!(fmt, "{:?}", kp.public())
} else {
write!(fmt, "Rsa(not loaded: {:?})", filename)
},
}
}
}

#[derive(Debug)]
enum KeyMaterialLoadingFailure {
Io(std::io::Error),
RsaDecoding(libp2p::identity::error::DecodingError),
}

impl KeyMaterial {

fn clone_keypair(&self) -> Keypair {
match *self {
KeyMaterial::Ed25519 { ref keypair, .. } => keypair.clone().map(Keypair::Ed25519),
KeyMaterial::RsaPkcs8File { ref keypair, .. } => keypair.clone().map(Keypair::Rsa),
}.expect("KeyMaterial needs to be loaded before accessing the keypair")
}

fn public(&self) -> PublicKey {
self.clone_keypair().public()
}

fn load(&mut self) -> Result<(), KeyMaterialLoadingFailure> {
match self {
&mut KeyMaterial::Ed25519 { ref private_key, ref mut keypair } if keypair.is_none() => {
let mut cloned = private_key.clone();
let sk = libp2p::identity::ed25519::SecretKey::from_bytes(&mut cloned)
.expect("Failed to extract ed25519::SecretKey");

let kp = libp2p::identity::ed25519::Keypair::from(sk);

*keypair = Some(kp);
},
&mut KeyMaterial::RsaPkcs8File { ref filename, ref mut keypair } if keypair.is_none() => {
let mut bytes = std::fs::read(filename)
.map_err(|e| KeyMaterialLoadingFailure::Io(e))?;
let kp = libp2p::identity::rsa::Keypair::from_pkcs8(&mut bytes)
.map_err(|e| KeyMaterialLoadingFailure::RsaDecoding(e))?;
*keypair = Some(kp);
}
_ => { /* all set */ }
}

Ok(())
}

fn into_loaded(mut self) -> Result<Self, KeyMaterialLoadingFailure> {
self.load()?;
Ok(self)
}
}

impl ConfigFile {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
fs::read_to_string(&path).map(|content| {
serde_json::from_str(&content).unwrap()
}).unwrap_or_else(|_| {
let config = ConfigFile::default();
let string = serde_json::to_string_pretty(&config).unwrap();
fs::write(path, string).unwrap();
config
})
fs::read_to_string(&path)
.map(|content| Self::parse(&content))
.map(|parsed| parsed.into_loaded().unwrap())
.unwrap_or_else(|_| {
let config = ConfigFile::default();
config.store_at(path).unwrap();

config.into_loaded().unwrap()
})
}

pub fn secio_key_pair(&self) -> Keypair {
// FIXME: the keypair should be extract at load time and only cloned here
// there could also some (de)serialization support in libp2p like there is for rsa and
// secp256k1?
let mut cloned = self.private_key.clone();
let sk = libp2p::identity::ed25519::SecretKey::from_bytes(&mut cloned)
.expect("Failed to extract ed25519::SecretKey");
fn load(&mut self) -> Result<(), KeyMaterialLoadingFailure> {
self.key.load()
}

fn into_loaded(mut self) -> Result<Self, KeyMaterialLoadingFailure> {
self.load()?;
Ok(self)
}

fn parse(s: &str) -> Self {
serde_json::from_str(s).unwrap()
}

let kp = libp2p::identity::ed25519::Keypair::from(sk);
pub fn store_at<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
let string = serde_json::to_string_pretty(self).unwrap();
fs::write(path, string)
}

Keypair::Ed25519(kp)
pub fn secio_key_pair(&self) -> Keypair {
self.key.clone_keypair()
}

pub fn peer_id(&self) -> PeerId {
self.secio_key_pair().public().into_peer_id()
self.key.public().into_peer_id()
}

pub fn bootstrap(&self) -> Vec<(Multiaddr, PeerId)> {
Expand All @@ -69,13 +169,48 @@ impl ConfigFile {

impl Default for ConfigFile {
fn default() -> Self {
// the ed25519 has no chance of working with go-ipfs as of now because of
// https://github.com/libp2p/specs/issues/138 and
// https://github.com/libp2p/go-libp2p-core/blob/dc718fa4dab1866476fd9f379718fdd619455a4f/peer/peer.go#L23-L34
//
// and on the other hand:
// https://github.com/libp2p/rust-libp2p/blob/eb7b7bd919b93e6acf00847c19d1a76c09016120/core/src/peer_id.rs#L62-L74
let private_key: [u8; 32] = EntropyRng::new().gen();

let bootstrap = BOOTSTRAP_NODES.iter().map(|node| {
node.parse().unwrap()
}).collect();
ConfigFile {
private_key,
key: KeyMaterial::Ed25519 { private_key, keypair: None }.into_loaded().unwrap(),
bootstrap,
}
}
}

#[cfg(test)]
mod tests {

use super::ConfigFile;

#[test]
fn supports_older_v1_and_ed25519_v2() {
let input = r#"{"private_key":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"bootstrap":[]}"#;

let actual = ConfigFile::parse(input);

let roundtrip = serde_json::to_string(&actual).unwrap();

assert_eq!(input, roundtrip);
}

#[test]
fn supports_v2() {
let input = r#"{"rsa_pkcs8_filename":"foobar.pk8","bootstrap":[]}"#;

let actual = ConfigFile::parse(input);

let roundtrip = serde_json::to_string(&actual).unwrap();

assert_eq!(input, roundtrip);
}
}

0 comments on commit dd61207

Please sign in to comment.