keyfork-derive-openpgp: initial proof of concept
This commit is contained in:
parent
ea611906ab
commit
92f15489a4
File diff suppressed because it is too large
Load Diff
|
@ -5,6 +5,7 @@ members = [
|
|||
"keyfork",
|
||||
"keyfork-derive-util",
|
||||
"keyfork-derive-key",
|
||||
"keyfork-derive-openpgp",
|
||||
"keyfork-entropy",
|
||||
"keyfork-frame",
|
||||
"keyfork-mnemonic-from-seed",
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "keyfork-derive-openpgp"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ed25519-dalek = "2.0.0"
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false, features = ["ed25519"] }
|
||||
keyforkd-client = { version = "0.1.0", path = "../keyforkd-client", default-features = false, features = ["ed25519"] }
|
||||
sequoia-openpgp = { version = "1.16.1", features = ["ed25519"] }
|
||||
thiserror = "1.0.49"
|
|
@ -0,0 +1,207 @@
|
|||
use std::{
|
||||
env,
|
||||
str::FromStr,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
use keyfork_derive_util::{
|
||||
request::{DerivationAlgorithm, DerivationRequest},
|
||||
DerivationIndex, DerivationPath, ExtendedPrivateKey, PrivateKey,
|
||||
};
|
||||
use keyforkd_client::Client;
|
||||
use sequoia_openpgp::{
|
||||
packet::{
|
||||
key::{Key4, PrimaryRole, SubordinateRole},
|
||||
signature::SignatureBuilder,
|
||||
Key, UserID,
|
||||
},
|
||||
types::{KeyFlags, SignatureType},
|
||||
Cert, Packet,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum Error {
|
||||
#[error("Bad character: {0}")]
|
||||
BadChar(char),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct KeyType {
|
||||
_inner: KeyFlags,
|
||||
}
|
||||
|
||||
impl Default for KeyType {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
_inner: KeyFlags::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyType {
|
||||
fn certify(mut self) -> Self {
|
||||
self._inner = self._inner.set_certification();
|
||||
self
|
||||
}
|
||||
|
||||
fn sign(mut self) -> Self {
|
||||
self._inner = self._inner.set_signing();
|
||||
self
|
||||
}
|
||||
|
||||
fn encrypt(mut self) -> Self {
|
||||
self._inner = self._inner.set_transport_encryption();
|
||||
self._inner = self._inner.set_storage_encryption();
|
||||
self
|
||||
}
|
||||
|
||||
fn authenticate(mut self) -> Self {
|
||||
self._inner = self._inner.set_authentication();
|
||||
self
|
||||
}
|
||||
|
||||
fn inner(&self) -> &KeyFlags {
|
||||
&self._inner
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for KeyType {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
s.chars().try_fold(Self::default(), |s, ch| match ch {
|
||||
'C' | 'c' => Ok(s.certify()),
|
||||
'S' | 's' => Ok(s.sign()),
|
||||
'E' | 'e' => Ok(s.encrypt()),
|
||||
'A' | 'a' => Ok(s.authenticate()),
|
||||
ch => Err(Error::BadChar(ch)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn validate(
|
||||
path: &str,
|
||||
subkey_format: &str,
|
||||
default_userid: &str,
|
||||
) -> Result<(DerivationPath, Vec<KeyType>, UserID), Box<dyn std::error::Error>> {
|
||||
let mut pgp_u32 = [0u8; 4];
|
||||
pgp_u32[1..].copy_from_slice(&"pgp".bytes().collect::<Vec<u8>>());
|
||||
let index = DerivationIndex::new(u32::from_be_bytes(pgp_u32), true)?;
|
||||
|
||||
let path = DerivationPath::from_str(path)?;
|
||||
assert_eq!(2, path.len(), "Expected path of m/{index}/account_id'");
|
||||
|
||||
let given_index = path.iter().next().expect("checked .len() above");
|
||||
assert_eq!(
|
||||
&index, given_index,
|
||||
"Expected derivation path starting with m/{index}, got: {given_index}",
|
||||
);
|
||||
|
||||
let subkey_format = subkey_format
|
||||
.split(',')
|
||||
.map(KeyType::from_str)
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
assert!(
|
||||
subkey_format[0].inner().for_certification(),
|
||||
"First key must be able to certify"
|
||||
);
|
||||
|
||||
Ok((path, subkey_format, UserID::from(default_userid)))
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = env::args();
|
||||
let program_name = args.next().expect("program name");
|
||||
let args = args.collect::<Vec<_>>();
|
||||
let (path, subkey_format, default_userid) = match args.as_slice() {
|
||||
[path, subkey_format, default_userid] => validate(path, subkey_format, default_userid)?,
|
||||
_ => panic!("Usage: {program_name} path subkey_format default_userid"),
|
||||
};
|
||||
|
||||
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &path);
|
||||
let derived_data = Client::discover_socket()?.request(&request)?;
|
||||
let xprv = ExtendedPrivateKey::<SigningKey>::try_from(&derived_data)?;
|
||||
|
||||
let derived_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?;
|
||||
let epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
|
||||
let one_day = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
|
||||
let primary_key = Key::from(
|
||||
Key4::<_, PrimaryRole>::import_secret_ed25519(
|
||||
&PrivateKey::to_bytes(derived_key.private_key()),
|
||||
epoch,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let cert =
|
||||
Cert::from_packets(vec![Packet::SecretKey(primary_key.clone())].into_iter()).unwrap();
|
||||
let builder = SignatureBuilder::new(SignatureType::PositiveCertification)
|
||||
.set_signature_creation_time(epoch)?
|
||||
.set_key_flags(subkey_format[0].inner().clone())?;
|
||||
let binding = default_userid.bind(&mut primary_key.clone().into_keypair()?, &cert, builder)?;
|
||||
let cert = cert.insert_packets(vec![Packet::from(default_userid), binding.into()])?;
|
||||
|
||||
let policy = sequoia_openpgp::policy::StandardPolicy::new();
|
||||
let mut keypair = cert
|
||||
.primary_key()
|
||||
.key()
|
||||
.clone()
|
||||
.parts_into_secret()?
|
||||
.into_keypair()?;
|
||||
let signatures = cert.set_expiration_time(&policy, None, &mut keypair, Some(one_day))?;
|
||||
let mut cert = cert.insert_packets(signatures)?;
|
||||
|
||||
for (index, subkey_flags) in subkey_format.iter().enumerate().skip(1) {
|
||||
let index = u32::try_from(index)?;
|
||||
let derived_key = xprv.derive_child(&DerivationIndex::new(index, true)?)?;
|
||||
let subkey = Key::from(
|
||||
Key4::<_, SubordinateRole>::import_secret_ed25519(
|
||||
&PrivateKey::to_bytes(derived_key.private_key()),
|
||||
epoch,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let builder = if subkey_flags.inner().for_signing() {
|
||||
SignatureBuilder::new(SignatureType::SubkeyBinding)
|
||||
.set_key_flags(subkey_flags.inner().clone())?
|
||||
.set_signature_creation_time(epoch)?
|
||||
.set_key_validity_period(one_day.duration_since(epoch)?)?
|
||||
.set_embedded_signature(
|
||||
SignatureBuilder::new(SignatureType::PrimaryKeyBinding)
|
||||
.set_signature_creation_time(epoch)?
|
||||
.sign_primary_key_binding(
|
||||
&mut subkey.clone().into_keypair()?,
|
||||
&primary_key,
|
||||
&subkey,
|
||||
)?,
|
||||
)?
|
||||
} else {
|
||||
SignatureBuilder::new(SignatureType::SubkeyBinding)
|
||||
.set_key_flags(subkey_flags.inner().clone())?
|
||||
.set_signature_creation_time(epoch)?
|
||||
.set_key_validity_period(one_day.duration_since(epoch)?)?
|
||||
};
|
||||
let binding =
|
||||
builder.sign_subkey_binding(&mut primary_key.clone().into_keypair()?, None, &subkey)?;
|
||||
cert = cert.insert_packets(vec![
|
||||
Packet::SecretSubkey(subkey.clone()),
|
||||
Packet::Signature(binding),
|
||||
])?;
|
||||
}
|
||||
|
||||
use sequoia_openpgp::{
|
||||
armor::{Kind, Writer},
|
||||
serialize::Marshal,
|
||||
};
|
||||
|
||||
let mut w = Writer::new(std::io::stdout(), Kind::Message)?;
|
||||
|
||||
for packet in cert.into_packets() {
|
||||
packet.serialize(&mut w)?;
|
||||
}
|
||||
|
||||
w.finalize()?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -16,7 +16,7 @@ pub enum Error {
|
|||
type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
/// Index for a given extended private key.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct DerivationIndex(pub(crate) u32);
|
||||
|
||||
impl DerivationIndex {
|
||||
|
|
|
@ -24,7 +24,7 @@ type Result<T, E = Error> = std::result::Result<T, E>;
|
|||
const PREFIX: &str = "m";
|
||||
|
||||
/// A fully qualified path to derive a key.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub struct DerivationPath {
|
||||
pub(crate) path: Vec<DerivationIndex>,
|
||||
}
|
||||
|
@ -71,6 +71,26 @@ impl std::fmt::Display for DerivationPath {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<DerivationIndex> for DerivationPath {
|
||||
type Output = DerivationPath;
|
||||
|
||||
fn add(self, rhs: DerivationIndex) -> Self::Output {
|
||||
let mut output = self.clone();
|
||||
output.path.push(rhs);
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<&[DerivationIndex]> for DerivationPath {
|
||||
type Output = DerivationPath;
|
||||
|
||||
fn add(self, rhs: &[DerivationIndex]) -> Self::Output {
|
||||
let mut output = self.clone();
|
||||
output.path.extend(rhs.iter().cloned());
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -90,4 +110,26 @@ mod tests {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = DerivationPath::from_str("m")?;
|
||||
let path = path + DerivationIndex::new(72, true)?;
|
||||
let path = path + DerivationIndex::new(47, false)?;
|
||||
let path = path + DerivationIndex::new((i32::MAX) as u32, false)?;
|
||||
assert_eq!(path, DerivationPath::from_str("m/72'/47/2147483647")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn add_vec() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = DerivationPath::from_str("m")?;
|
||||
let other_path = [DerivationIndex::new(72, true)?, DerivationIndex::new(47, false)?, DerivationIndex::new((i32::MAX) as u32, false)?];
|
||||
let path = path + &other_path[..];
|
||||
assert_eq!(path, DerivationPath::from_str("m/72'/47/2147483647")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,10 +54,6 @@ pub enum PrivateKeyError {
|
|||
/// For the given algorithm, the private key must be nonzero.
|
||||
#[error("The provided private key must be nonzero, but is not")]
|
||||
NonZero,
|
||||
|
||||
/// Unable to convert a point to a key.
|
||||
#[error("Unable to convert point to key")]
|
||||
PointToKey(#[from] k256::elliptic_curve::Error),
|
||||
}
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
|
|
|
@ -47,10 +47,6 @@ pub enum PublicKeyError {
|
|||
#[error("The provided public key must be nonzero, but is not")]
|
||||
NonZero,
|
||||
|
||||
/// Unable to convert a point to a key.
|
||||
#[error("Unable to convert point to key")]
|
||||
PointToKey(#[from] k256::elliptic_curve::Error),
|
||||
|
||||
/// Public key derivation is unsupported for this algorithm.
|
||||
#[error("Public key derivation is unsupported for this algorithm")]
|
||||
DerivationUnsupported,
|
||||
|
@ -87,7 +83,7 @@ impl PublicKey for k256::PublicKey {
|
|||
.expect("Should have been able to get a NonZeroScalar");
|
||||
|
||||
let point = self.to_projective() + (AffinePoint::generator() * *scalar);
|
||||
Self::from_affine(point.into()).map_err(From::from)
|
||||
Ok(Self::from_affine(point.into()).expect("Could not from_affine after scalar arithmetic"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,10 @@ pub struct DerivationRequest {
|
|||
|
||||
impl DerivationRequest {
|
||||
pub fn new(algorithm: DerivationAlgorithm, path: &DerivationPath) -> Self {
|
||||
Self { algorithm, path: path.clone() }
|
||||
Self {
|
||||
algorithm,
|
||||
path: path.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &DerivationPath {
|
||||
|
@ -105,3 +108,58 @@ impl DerivationResponse {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TryFromDerivationResponseError {
|
||||
#[error("incorrect algorithm provided")]
|
||||
Algorithm,
|
||||
|
||||
#[error("{0}")]
|
||||
ExtendedPrivateKey(#[from] XPrvError),
|
||||
}
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<k256::SecretKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
fn try_from(value: &DerivationResponse) -> std::result::Result<Self, Self::Error> {
|
||||
match value.algorithm {
|
||||
DerivationAlgorithm::Secp256k1 => {
|
||||
Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from)
|
||||
}
|
||||
_ => Err(Self::Error::Algorithm),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<k256::SecretKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
fn try_from(value: DerivationResponse) -> std::result::Result<Self, Self::Error> {
|
||||
ExtendedPrivateKey::<k256::SecretKey>::try_from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ed25519")]
|
||||
impl TryFrom<&DerivationResponse> for ExtendedPrivateKey<ed25519_dalek::SigningKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
fn try_from(value: &DerivationResponse) -> std::result::Result<Self, Self::Error> {
|
||||
match value.algorithm {
|
||||
DerivationAlgorithm::Ed25519 => {
|
||||
Self::new_from_parts(&value.data, value.depth, value.chain_code).map_err(From::from)
|
||||
}
|
||||
_ => Err(Self::Error::Algorithm),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ed25519")]
|
||||
impl TryFrom<DerivationResponse> for ExtendedPrivateKey<ed25519_dalek::SigningKey> {
|
||||
type Error = TryFromDerivationResponseError;
|
||||
|
||||
fn try_from(value: DerivationResponse) -> std::result::Result<Self, Self::Error> {
|
||||
ExtendedPrivateKey::<ed25519_dalek::SigningKey>::try_from(&value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,14 @@ edition = "2021"
|
|||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["ed25519", "secp256k1"]
|
||||
ed25519 = ["keyfork-derive-util/ed25519"]
|
||||
secp256k1 = ["keyfork-derive-util/secp256k1"]
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.3"
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util" }
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util", default-features = false }
|
||||
keyfork-frame = { version = "0.1.0", path = "../keyfork-frame" }
|
||||
thiserror = "1.0.49"
|
||||
|
||||
|
|
Loading…
Reference in New Issue