From 26494e11a5ec67e0931b3e49e825100a3db02439 Mon Sep 17 00:00:00 2001 From: 0xZensh Date: Mon, 22 Jan 2024 10:43:49 +0800 Subject: [PATCH] feat: improve COSE Key in ns-inscriber --- README.md | 2 +- crates/ns-inscriber/Cargo.toml | 2 +- crates/ns-inscriber/src/bin/main.rs | 215 ++++++++++++-------- crates/ns-inscriber/src/wallet/cose_key.rs | 189 +++++++++-------- crates/ns-inscriber/src/wallet/ed25519.rs | 206 ++++++++++++++++++- crates/ns-inscriber/src/wallet/encrypt.rs | 55 +++-- crates/ns-inscriber/src/wallet/mod.rs | 21 +- crates/ns-inscriber/src/wallet/secp256k1.rs | 36 +++- crates/ns-inscriber/src/wallet/sign.rs | 73 +++++++ 9 files changed, 585 insertions(+), 214 deletions(-) create mode 100644 crates/ns-inscriber/src/wallet/sign.rs diff --git a/README.md b/README.md index 7e11980..980ca08 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# NS-RS — Inscribing Name service on Bitcoin network +# NS-RS — Inscribing naming trusted database on Bitcoin network Rust implementation of [NS-Protocol](https://github.com/ldclabs/ns-protocol) (Name & Service Protocol) by the LDC Labs diff --git a/crates/ns-inscriber/Cargo.toml b/crates/ns-inscriber/Cargo.toml index abc7aed..344e0a8 100644 --- a/crates/ns-inscriber/Cargo.toml +++ b/crates/ns-inscriber/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ns-inscriber" -version = "0.5.0" +version = "0.6.0" edition = "2021" rust-version = "1.64" description = "Name & Service Protocol inscriber service in Rust" diff --git a/crates/ns-inscriber/src/bin/main.rs b/crates/ns-inscriber/src/bin/main.rs index 4dfc18a..8130fbe 100644 --- a/crates/ns-inscriber/src/bin/main.rs +++ b/crates/ns-inscriber/src/bin/main.rs @@ -2,27 +2,31 @@ use anyhow::anyhow; use bitcoin::{address::NetworkChecked, Address, Amount, Network, PublicKey, ScriptBuf, Txid}; use chrono::{Local, SecondsFormat}; use clap::{Parser, Subcommand}; -use ns_protocol::ns::valid_name; use std::{ path::{Path, PathBuf}, str::FromStr, }; // use sys_locale::get_locale; -use coset::{CoseEncrypt0, TaggedCborSerializable}; +use coset::{CborSerializable, CoseEncrypt0, CoseKey, Label, TaggedCborSerializable}; use terminal_prompt::Terminal; use ns_inscriber::{ bitcoin::BitCoinRPCOptions, inscriber::{Inscriber, InscriberOptions, UnspentTxOut, UnspentTxOutJSON}, wallet::{ - base64_decode, base64_encode, base64url_decode, base64url_encode, ed25519, hash_256, iana, - secp256k1, unwrap_cbor_tag, wrap_cbor_tag, DerivationPath, Encrypt0, Key, + base64_decode, base64url_decode, base64url_encode, decode_sign1, + ed25519::{self, Ed25519Key}, + encode_sign1, hash_256, iana, new_sym, secp256k1, skip_tag, with_tag, DerivationPath, + Encrypt0, KeyHelper, CBOR_TAG, }, }; -use ns_protocol::ns::{Bytes32, Name, Operation, PublicKeyParams, Service, ThresholdLevel, Value}; +use ns_protocol::ns::{ + valid_name, Bytes32, Name, Operation, PublicKeyParams, Service, ThresholdLevel, Value, +}; -const AAD: &[u8; 12] = b"ns-inscriber"; -const TRANSFER_KEY_AAD: &[u8; 20] = b"ns:transfer.cose.key"; +const INS_AAD: &[u8; 12] = b"ns-inscriber"; +const NS_TRANS_KEY_AAD: &[u8; 20] = b"NS:COSE/Transfer.Key"; +const NS_SIGN_MESSAGE_AAD: &[u8; 19] = b"NS:COSE/Sign.Mesage"; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -80,7 +84,10 @@ pub enum Commands { idx: u32, }, /// List keys in keys dir. - ListKeys {}, + ListKeys { + #[arg(short, long, default_value_t = false)] + detail: bool, + }, /// Display secp256k1 addresses Secp256k1Address { /// secp256k key file name, will be combined to "{key}.cose.key" to read secp256k key @@ -106,7 +113,7 @@ pub enum Commands { #[arg(long)] msg: String, /// signature to verify - #[arg(long)] + #[arg(long, default_value = "")] sig: String, /// signature encoding format, default is base64, can be "hex" #[arg(long, default_value = "base64")] @@ -190,14 +197,15 @@ async fn main() -> anyhow::Result<()> { let password = terminal.prompt_sensitive("Enter a password to protect KEK: ")?; let mkek = hash_256(password.as_bytes()); let kid = if alias.is_empty() { - Local::now().to_rfc3339_opts(SecondsFormat::Secs, true) + Value::Text(Local::now().to_rfc3339_opts(SecondsFormat::Secs, true)) } else { - alias.to_owned() + Value::Text(alias.to_owned()) }; - let encryptor = Encrypt0::new(mkek); - let kek = Key::new_sym(iana::Algorithm::A256GCM, kid.as_bytes())?; - let data = encryptor.encrypt(&kek.to_vec()?, AAD, kid.as_bytes())?; - let data = wrap_cbor_tag(&data); + let kid = Some(kid); + let encryptor = Encrypt0::new(mkek, None); + let kek = new_sym(iana::Algorithm::A256GCM, kid.clone())?; + let data = encryptor.encrypt(&kek.to_slice()?, INS_AAD, kid)?; + let data = with_tag(&CBOR_TAG, &data); println!( "Put this new KEK as INSCRIBER_KEK on config file:\n{}", base64url_encode(&data) @@ -213,26 +221,26 @@ async fn main() -> anyhow::Result<()> { } let kek = KekEncryptor::open()?; - let signing_key = ed25519::new_ed25519(); - let address = format!("0x{}", hex::encode(signing_key.verifying_key().to_bytes())); - let key = Key::ed25519_from_secret(signing_key.as_bytes(), address.as_bytes())?; - let key_id = key.key_id(); - kek.save_key(&file, key)?; + let mut key = ed25519::Ed25519Key::new(None)?; + let pk = key.get_public()?; + let kid = Value::Bytes(pk.to_vec()); + key.0.set_kid(kid.clone())?; + kek.save_key(&file, key.0, Some(Value::Bytes(pk.to_vec())))?; println!( - "New seed key: {}, key id: {}", + "New seed key: {}, key id: {:?}", file.display(), - String::from_utf8_lossy(&key_id) + hex::encode(pk) ); return Ok(()); } Some(Commands::ImportSeed { alias }) => { - let kid = if alias.is_empty() { + let alias = if alias.is_empty() { Local::now().to_rfc3339_opts(SecondsFormat::Secs, true) + ".seed" } else { alias.to_owned() }; - let file = keys_path.join(format!("{kid}.cose.key")); + let file = keys_path.join(format!("{alias}.cose.key")); if KekEncryptor::key_exists(&file) { println!("{} exists, skipping key generation", file.display()); return Ok(()); @@ -245,20 +253,16 @@ async fn main() -> anyhow::Result<()> { let password = terminal.prompt_sensitive("Enter the password that protected the seed: ")?; let kek = hash_256(password.as_bytes()); - let decryptor = Encrypt0::new(kek); + let decryptor = Encrypt0::new(kek, None); let ciphertext = base64url_decode(import_key.trim())?; - let key = decryptor.decrypt(unwrap_cbor_tag(&ciphertext), TRANSFER_KEY_AAD)?; - Key::from_slice(&key)? + let key = decryptor.decrypt(skip_tag(&CBOR_TAG, &ciphertext), NS_TRANS_KEY_AAD)?; + Ed25519Key::from_slice(&key)? }; let kek = KekEncryptor::open()?; - let key_id = key.key_id(); - kek.save_key(&file, key)?; - println!( - "Imported seed key: {}, key id: {}", - file.display(), - String::from_utf8_lossy(&key_id) - ); + let kid = key.0.kid(); + kek.save_key(&file, key.0, kid.clone())?; + println!("Imported seed key: {}, key id: {:?}", file.display(), kid); return Ok(()); } @@ -267,13 +271,13 @@ async fn main() -> anyhow::Result<()> { let kek = KekEncryptor::open()?; kek.read_key(&keys_path.join(format!("{seed}.cose.key")))? }; - let key_id = key.key_id(); + let key = Ed25519Key(key); + let kid = key.0.kid(); let mut terminal = Terminal::open()?; let password = terminal.prompt_sensitive("Enter a password to protect the seed: ")?; let kek = hash_256(password.as_bytes()); - let encryptor = Encrypt0::new(kek); - let data = encryptor.encrypt(&key.to_vec()?, TRANSFER_KEY_AAD, &key_id)?; - let data = wrap_cbor_tag(&data); + let encryptor = Encrypt0::new(kek, None); + let data = encryptor.encrypt(&key.to_slice()?, NS_TRANS_KEY_AAD, kid)?; let data = base64url_encode(&data); println!("The exported seed key (base64url encoded):\n\n{data}\n\n"); @@ -283,11 +287,12 @@ async fn main() -> anyhow::Result<()> { Some(Commands::Secp256k1Derive { seed, acc, idx }) => { let kek = KekEncryptor::open()?; let seed_key = kek.read_key(&keys_path.join(format!("{seed}.cose.key")))?; + let seed_key = Ed25519Key(seed_key); let kid = format!("m/44'/0'/{acc}'/1/{idx}"); let path: DerivationPath = kid.parse()?; let secp = secp256k1::Secp256k1::new(); let keypair = - secp256k1::derive_secp256k1(&secp, network, &seed_key.secret_key()?, &path)?; + secp256k1::derive_secp256k1(&secp, network, &seed_key.get_secret()?, &path)?; let address = Address::p2wpkh( &PublicKey { compressed: true, @@ -295,9 +300,11 @@ async fn main() -> anyhow::Result<()> { }, network, )?; - let key = Key::secp256k1_from_keypair(&keypair, kid.as_bytes())?; + let kid = Some(Value::Text(kid)); + let key = + secp256k1::Secp256k1Key::from_secret(keypair.secret_key().as_ref(), kid.clone())?; let file = keys_path.join(format!("{}.cose.key", address)); - kek.save_key(&file, key)?; + kek.save_key(&file, key.0, kid)?; println!("key: {}, address: {}", file.display(), address); return Ok(()); } @@ -305,33 +312,55 @@ async fn main() -> anyhow::Result<()> { Some(Commands::Ed25519Derive { seed, acc, idx }) => { let kek = KekEncryptor::open()?; let seed_key = kek.read_key(&keys_path.join(format!("{seed}.cose.key")))?; + let seed_key = Ed25519Key(seed_key); let kid = format!("m/42'/0'/{acc}'/1/{idx}"); let path: DerivationPath = kid.parse()?; - let signing_key = ed25519::derive_ed25519(&seed_key.secret_key()?, &path); - let address = format!("0x{}", hex::encode(signing_key.verifying_key().to_bytes())); - let key = Key::ed25519_from_secret(signing_key.as_bytes(), kid.as_bytes())?; + let signing_key = ed25519::derive_ed25519(&seed_key.get_secret()?, &path); + let pk = signing_key.verifying_key().to_bytes(); + let address = format!("0x{}", hex::encode(pk)); + let kid = Value::Text(kid); + let key = Ed25519Key::from_secret(signing_key.as_bytes(), Some(kid.clone()))?; let file = keys_path.join(format!("{}.cose.key", address)); - kek.save_key(&file, key)?; + kek.save_key(&file, key.0, Some(kid))?; println!("key: {}, public key: {}", file.display(), address); return Ok(()); } - Some(Commands::ListKeys {}) => { + Some(Commands::ListKeys { detail }) => { + let kek = if *detail { + Some(KekEncryptor::open()?) + } else { + None + }; for entry in std::fs::read_dir(keys_path)? { let path = entry?.path(); if path.is_file() { - let data = std::fs::read(&path)?; let filename = &path .file_name() .expect("should get file name") .to_string_lossy(); - let data = unwrap_cbor_tag(&data); - let e0 = CoseEncrypt0::from_tagged_slice(data).map_err(anyhow::Error::msg)?; - let key_id = String::from_utf8_lossy(&e0.unprotected.key_id); - if key_id.starts_with("m/") { - println!("\nkey file: {}\nkey derived path: {}", filename, key_id); - } else { - println!("\nkey file: {}\nkey id: {}", filename, key_id); + println!("\nkey file: {}", filename); + + match kek { + Some(ref kek) => { + let key = kek.read_key(&path)?; + println!("key id: {}", key.kid_string()); + } + None => { + let data = std::fs::read(&path)?; + let data = skip_tag(&CBOR_TAG, &data); + let e0 = CoseEncrypt0::from_tagged_slice(data) + .map_err(anyhow::Error::msg)?; + let cid = e0 + .unprotected + .rest + .iter() + .find(|&v| v.0 == Label::Text("cid".to_string())) + .map(|v| &v.1); + if let Some(cid) = cid { + println!("key id: {:?}", cid); + } + } } } } @@ -347,7 +376,7 @@ async fn main() -> anyhow::Result<()> { } let secp = secp256k1::Secp256k1::new(); let keypair = - secp256k1::Keypair::from_seckey_slice(&secp, &secp256k1_key.secret_key()?)?; + secp256k1::Keypair::from_seckey_slice(&secp, &secp256k1_key.get_secret()?)?; let (public_key, _parity) = keypair.x_only_public_key(); let script_pubkey = ScriptBuf::new_p2tr(&secp, public_key, None); let address: Address = @@ -382,21 +411,26 @@ async fn main() -> anyhow::Result<()> { if cose_key.is_crv(iana::EllipticCurve::Secp256k1) { let secp = secp256k1::Secp256k1::new(); let keypair = - secp256k1::Keypair::from_seckey_slice(&secp, &cose_key.secret_key()?)?; + secp256k1::Keypair::from_seckey_slice(&secp, &cose_key.get_secret()?)?; let sig = secp256k1::sign_message(&secp, &keypair.secret_key(), msg); if enc == "hex" { println!("signature:\n{}", hex::encode(sig.serialize())); } else { - println!("signature:\n{}", base64_encode(&sig.serialize())); + println!("signature:\n{}", base64url_encode(&sig.serialize())); } } else if cose_key.is_crv(iana::EllipticCurve::Ed25519) { - let signing_key = ed25519::SigningKey::from_bytes(&cose_key.secret_key()?); - let sig = ed25519::sign_message(&signing_key, msg); + let key = ed25519::Ed25519Key(cose_key); + let signer = key.signer()?; + let output = encode_sign1( + signer, + msg.as_bytes().to_vec(), + NS_SIGN_MESSAGE_AAD.as_slice(), + )?; if enc == "hex" { - println!("signature:\n{}", hex::encode(sig.to_bytes())); + println!("signed message:\n{}", hex::encode(&output)); } else { - println!("signature:\n{}", base64_encode(&sig.to_bytes())); + println!("signed message:\n{}", base64url_encode(&output)); } } else { println!("unsupported key type"); @@ -407,23 +441,28 @@ async fn main() -> anyhow::Result<()> { Some(Commands::VerifyMessage { key, msg, sig, enc }) => { let kek = KekEncryptor::open()?; let cose_key = kek.read_key(&keys_path.join(format!("{key}.cose.key")))?; - let sig = if enc == "hex" { - hex::decode(sig)? - } else { - base64_decode(sig)? - }; + if cose_key.is_crv(iana::EllipticCurve::Secp256k1) { + let sig = if enc == "hex" { + hex::decode(sig)? + } else { + base64_decode(sig)? + }; let secp = secp256k1::Secp256k1::new(); - let keypair = - secp256k1::Keypair::from_seckey_slice(&secp, &cose_key.secret_key()?)?; + secp256k1::Keypair::from_seckey_slice(&secp, &cose_key.get_secret()?)?; secp256k1::verify_message(&secp, &keypair.public_key(), msg, &sig)?; println!("signature is valid"); } else if cose_key.is_crv(iana::EllipticCurve::Ed25519) { - let signing_key = ed25519::SigningKey::from_bytes(&cose_key.secret_key()?); - ed25519::verify_message(&signing_key.verifying_key(), msg, &sig)?; - + let msg = if enc == "hex" { + hex::decode(msg)? + } else { + base64_decode(msg)? + }; + let key = ed25519::Ed25519Key(cose_key); + let verifier = key.verifier()?; + decode_sign1(verifier, &msg, NS_SIGN_MESSAGE_AAD.as_slice())?; println!("signature is valid"); } else { println!("unsupported key type"); @@ -452,7 +491,7 @@ async fn main() -> anyhow::Result<()> { } let secp = secp256k1::Secp256k1::new(); let keypair = - secp256k1::Keypair::from_seckey_slice(&secp, &secp256k1_key.secret_key()?)?; + secp256k1::Keypair::from_seckey_slice(&secp, &secp256k1_key.get_secret()?)?; let (p2wpkh_pubkey, p2tr_pubkey) = secp256k1::as_script_pubkey(&secp, &keypair); let inscriber = get_inscriber(network).await?; @@ -504,7 +543,7 @@ async fn main() -> anyhow::Result<()> { if !ed25519_key.is_crv(iana::EllipticCurve::Ed25519) { anyhow::bail!("{} is not a ed25519 key", key); } - let signing_key = ed25519::SigningKey::from_bytes(&ed25519_key.secret_key()?); + let signing_key = ed25519::SigningKey::from_bytes(&ed25519_key.get_secret()?); let params = PublicKeyParams { public_keys: vec![Bytes32(signing_key.verifying_key().to_bytes().to_owned())], threshold: None, @@ -581,7 +620,7 @@ async fn main() -> anyhow::Result<()> { if !ed25519_key.is_crv(iana::EllipticCurve::Ed25519) { anyhow::bail!("{} is not a ed25519 key", key); } - let signing_key = ed25519::SigningKey::from_bytes(&ed25519_key.secret_key()?); + let signing_key = ed25519::SigningKey::from_bytes(&ed25519_key.get_secret()?); let params = PublicKeyParams { public_keys: vec![Bytes32(signing_key.verifying_key().to_bytes().to_owned())], threshold: None, @@ -615,7 +654,7 @@ async fn main() -> anyhow::Result<()> { } let secp = secp256k1::Secp256k1::new(); let keypair = - secp256k1::Keypair::from_seckey_slice(&secp, &secp256k1_key.secret_key()?)?; + secp256k1::Keypair::from_seckey_slice(&secp, &secp256k1_key.get_secret()?)?; let (p2wpkh_pubkey, p2tr_pubkey) = secp256k1::as_script_pubkey(&secp, &keypair); let inscriber = get_inscriber(network).await?; @@ -672,12 +711,12 @@ impl KekEncryptor { let mut terminal = Terminal::open()?; let password = terminal.prompt_sensitive("Enter the password protected KEK: ")?; let mkek = hash_256(password.as_bytes()); - let decryptor = Encrypt0::new(mkek); + let decryptor = Encrypt0::new(mkek, None); let ciphertext = base64url_decode(kek_str.trim())?; - let key = decryptor.decrypt(unwrap_cbor_tag(&ciphertext), AAD)?; - let key = Key::from_slice(&key)?; + let key = decryptor.decrypt(skip_tag(&CBOR_TAG, &ciphertext), INS_AAD)?; + let key = CoseKey::from_slice(&key).map_err(anyhow::Error::msg)?; Ok(KekEncryptor { - encryptor: Encrypt0::new(key.secret_key()?), + encryptor: Encrypt0::new(key.get_secret()?, key.kid()), }) } @@ -685,18 +724,18 @@ impl KekEncryptor { std::fs::read(file).is_ok() } - fn read_key(&self, file: &Path) -> anyhow::Result { + fn read_key(&self, file: &Path) -> anyhow::Result { let data = std::fs::read(file)?; - let key = self.encryptor.decrypt(unwrap_cbor_tag(&data), AAD)?; - Key::from_slice(&key) + let key = self + .encryptor + .decrypt(skip_tag(&CBOR_TAG, &data), INS_AAD)?; + CoseKey::from_slice(&key).map_err(anyhow::Error::msg) } - fn save_key(&self, file: &Path, key: Key) -> anyhow::Result<()> { - let kid = key.key_id(); - let data = self - .encryptor - .encrypt(key.to_vec()?.as_slice(), AAD, &kid)?; - std::fs::write(file, wrap_cbor_tag(&data))?; + fn save_key(&self, file: &Path, key: CoseKey, cid: Option) -> anyhow::Result<()> { + let data = key.to_vec().map_err(anyhow::Error::msg)?; + let data = self.encryptor.encrypt(&data, INS_AAD, cid)?; + std::fs::write(file, with_tag(&CBOR_TAG, &data))?; Ok(()) } } diff --git a/crates/ns-inscriber/src/wallet/cose_key.rs b/crates/ns-inscriber/src/wallet/cose_key.rs index 54badae..ca1aa50 100644 --- a/crates/ns-inscriber/src/wallet/cose_key.rs +++ b/crates/ns-inscriber/src/wallet/cose_key.rs @@ -1,79 +1,106 @@ -use ciborium::Value; use coset::{ iana, CborSerializable, CoseKey, CoseKeyBuilder, KeyType, Label, RegisteredLabelWithPrivate, }; +use ns_protocol::{ + ns::Value, + state::{from_bytes, to_bytes}, +}; use rand_core::{OsRng, RngCore}; -use super::secp256k1::Keypair; +pub trait CoseSigner { + fn alg(&self) -> iana::Algorithm; + fn kid(&self) -> Vec; + fn sign(&self, data: &[u8]) -> Vec; +} + +pub trait CoseVerifier { + fn alg(&self) -> iana::Algorithm; + fn verify(&self, data: &[u8], sig: &[u8]) -> Result<(), anyhow::Error>; +} -const KEY_PARAM_K: Label = Label::Int(iana::SymmetricKeyParameter::K as i64); -const KEY_PARAM_D: Label = Label::Int(iana::OkpKeyParameter::D as i64); +pub trait KeyHelper { + fn to_slice(self) -> anyhow::Result>; + fn kty(&self) -> iana::KeyType; + fn alg(&self) -> iana::Algorithm; + fn kid(&self) -> Option; + fn set_kid(&mut self, kid: Value) -> anyhow::Result<()>; + fn is_crv(&self, crv: iana::EllipticCurve) -> bool; + fn has_param(&self, key_label: &Label) -> bool; + fn get_param(&self, key_label: &Label) -> anyhow::Result<&Value>; + + fn kid_string(&self) -> String { + if let Some(kid) = self.kid() { + match kid { + Value::Text(s) => return s, + Value::Bytes(b) => return hex::encode(b), + Value::Bool(b) => return b.to_string(), + Value::Integer(i) => return i128::from(i).to_string(), + v => { + return format!("{:?}", v); + } + } + } + "".to_string() + } -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Key(pub CoseKey); + fn get_secret(&self) -> anyhow::Result<[u8; 32]> { + let key_label = match self.kty() { + iana::KeyType::Symmetric => Label::Int(iana::SymmetricKeyParameter::K as i64), + iana::KeyType::OKP => Label::Int(iana::OkpKeyParameter::D as i64), + iana::KeyType::EC2 => Label::Int(iana::Ec2KeyParameter::D as i64), + _ => { + return Err(anyhow::Error::msg("unsupport key type")); + } + }; -impl Key { - pub fn new_sym(alg: iana::Algorithm, kid: &[u8]) -> anyhow::Result { + let val = self + .get_param(&key_label)? + .as_bytes() + .filter(|v| v.len() == 32) + .ok_or_else(|| anyhow::Error::msg("invalid secret key"))?; let mut key = [0u8; 32]; - OsRng.fill_bytes(&mut key); + key.copy_from_slice(val); + Ok(key) + } +} - let mut key = CoseKeyBuilder::new_symmetric_key(key.to_vec()).algorithm(alg); - if !kid.is_empty() { - key = key.key_id(kid.to_vec()); - } - Ok(Self(key.build())) +impl KeyHelper for CoseKey { + fn to_slice(self) -> anyhow::Result> { + self.to_vec().map_err(anyhow::Error::msg) } - pub fn ed25519_from_secret(secret: &[u8; 32], kid: &[u8]) -> anyhow::Result { - let mut key = CoseKeyBuilder::new_okp_key() - .algorithm(iana::Algorithm::EdDSA) - .param( - iana::OkpKeyParameter::Crv as i64, - Value::from(iana::EllipticCurve::Ed25519 as i64), - ) - .param( - iana::OkpKeyParameter::D as i64, - Value::Bytes(secret.to_vec()), - ); - - if !kid.is_empty() { - key = key.key_id(kid.to_vec()); + fn kty(&self) -> iana::KeyType { + match self.kty { + KeyType::Assigned(kty) => kty, + _ => iana::KeyType::Reserved, } - Ok(Self(key.build())) } - pub fn secp256k1_from_keypair(keypair: &Keypair, kid: &[u8]) -> anyhow::Result { - let mut key = CoseKey { - kty: KeyType::Assigned(iana::KeyType::EC2), - alg: Some(RegisteredLabelWithPrivate::Assigned( - iana::Algorithm::ES256K, - )), - params: vec![ - ( - Label::Int(iana::Ec2KeyParameter::Crv as i64), - Value::from(iana::EllipticCurve::Secp256k1 as i64), - ), - ( - Label::Int(iana::Ec2KeyParameter::D as i64), - Value::Bytes(keypair.secret_key().as_ref().to_vec()), - ), - ], - ..Default::default() - }; + fn alg(&self) -> iana::Algorithm { + if let Some(RegisteredLabelWithPrivate::Assigned(alg)) = self.alg { + return alg; + } + + iana::Algorithm::Reserved + } - if !kid.is_empty() { - key.key_id.extend_from_slice(kid); + fn kid(&self) -> Option { + if self.key_id.is_empty() { + return None; } - Ok(Self(key)) + + Some(from_bytes(&self.key_id).unwrap_or_else(|_e| Value::Bytes(self.key_id.clone()))) } - pub fn key_id(&self) -> Vec { - self.0.key_id.clone() + fn set_kid(&mut self, kid: Value) -> anyhow::Result<()> { + self.key_id = to_bytes(&kid)?; + Ok(()) } - pub fn is_crv(&self, crv: iana::EllipticCurve) -> bool { - for (label, value) in &self.0.params { - if label == &Label::Int(iana::Ec2KeyParameter::Crv as i64) { + fn is_crv(&self, crv: iana::EllipticCurve) -> bool { + for (label, value) in &self.params { + // https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters + if label == &Label::Int(-1i64) { if let Some(val) = value.as_integer() { return val == (crv as i64).into(); } @@ -82,43 +109,29 @@ impl Key { false } - pub fn to_vec(self) -> anyhow::Result> { - self.0.to_vec().map_err(anyhow::Error::msg) + fn has_param(&self, key_label: &Label) -> bool { + self.params.iter().any(|(label, _)| label == key_label) } - pub fn from_slice(data: &[u8]) -> anyhow::Result { - let key = CoseKey::from_slice(data).map_err(anyhow::Error::msg)?; - Ok(Self(key)) - } - - pub fn secret_key(&self) -> anyhow::Result<[u8; 32]> { - let key_param = match self.0.kty { - KeyType::Assigned(iana::KeyType::Symmetric) => &KEY_PARAM_K, - KeyType::Assigned(iana::KeyType::OKP) => &KEY_PARAM_D, - KeyType::Assigned(iana::KeyType::EC2) => &Label::Int(iana::Ec2KeyParameter::D as i64), - _ => { - return Err(anyhow::Error::msg("unsupport key type")); - } - }; - - for (label, value) in &self.0.params { - if label == key_param { - match value { - Value::Bytes(val) => { - if val.len() != 32 { - return Err(anyhow::Error::msg("invalid key length, expected 32")); - } - let mut key = [0u8; 32]; - key.copy_from_slice(val); - return Ok(key); - } - _ => { - return Err(anyhow::Error::msg("invalid key type")); - } - } + fn get_param(&self, key_label: &Label) -> anyhow::Result<&Value> { + for (label, value) in &self.params { + if label == key_label { + return Ok(value); } } + Err(anyhow::Error::msg(format!("key {:?} not found", key_label))) + } +} + +pub fn new_sym(alg: iana::Algorithm, kid: Option) -> anyhow::Result { + let mut key = [0u8; 32]; + OsRng.fill_bytes(&mut key); - Err(anyhow::Error::msg("invalid key")) + let mut key = CoseKeyBuilder::new_symmetric_key(key.to_vec()) + .algorithm(alg) + .build(); + if let Some(kid) = kid { + key.set_kid(kid)?; } + Ok(key) } diff --git a/crates/ns-inscriber/src/wallet/ed25519.rs b/crates/ns-inscriber/src/wallet/ed25519.rs index 54fa4f7..8f9f696 100644 --- a/crates/ns-inscriber/src/wallet/ed25519.rs +++ b/crates/ns-inscriber/src/wallet/ed25519.rs @@ -1,26 +1,210 @@ use bitcoin::bip32::DerivationPath; +use coset::{iana, CborSerializable, CoseKey, CoseKeyBuilder, Label}; +use ns_protocol::ns::Value; use rand_core::{OsRng, RngCore}; use slip10_ed25519::derive_ed25519_private_key; -pub use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +pub use ed25519_dalek::{SecretKey, Signature, Signer, SigningKey, Verifier, VerifyingKey}; + +use super::{CoseSigner, CoseVerifier, KeyHelper}; + +const KEY_PARAM_X: Label = Label::Int(iana::OkpKeyParameter::X as i64); +const KEY_PARAM_D: Label = Label::Int(iana::OkpKeyParameter::D as i64); pub fn derive_ed25519(seed: &[u8], path: &DerivationPath) -> SigningKey { let secret = derive_ed25519_private_key(seed, &path.to_u32_vec()); SigningKey::from_bytes(&secret) } -pub fn new_ed25519() -> SigningKey { - let mut secret = [0u8; 32]; - OsRng.fill_bytes(&mut secret); - SigningKey::from_bytes(&secret) +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Ed25519Key(pub CoseKey); + +impl Ed25519Key { + pub fn new(kid: Option) -> anyhow::Result { + let mut secret = [0u8; 32]; + OsRng.fill_bytes(&mut secret); + Self::from_secret(&secret, kid) + } + + pub fn from_secret(secret: &[u8; 32], kid: Option) -> anyhow::Result { + let mut key = CoseKeyBuilder::new_okp_key() + .algorithm(iana::Algorithm::EdDSA) + .param( + iana::OkpKeyParameter::Crv as i64, + Value::from(iana::EllipticCurve::Ed25519 as i64), + ) + .param( + iana::OkpKeyParameter::D as i64, + Value::Bytes(secret.to_vec()), + ) + .build(); + + if let Some(kid) = kid { + key.set_kid(kid)?; + } + Ok(Self(key)) + } + + pub fn from_public(public: &[u8; 32], kid: Option) -> anyhow::Result { + let mut key = CoseKeyBuilder::new_okp_key() + .algorithm(iana::Algorithm::EdDSA) + .param( + iana::OkpKeyParameter::Crv as i64, + Value::from(iana::EllipticCurve::Ed25519 as i64), + ) + .param( + iana::OkpKeyParameter::X as i64, + Value::Bytes(public.to_vec()), + ) + .build(); + + if let Some(kid) = kid { + key.set_kid(kid)?; + } + Ok(Self(key)) + } + + pub fn from_slice(data: &[u8]) -> anyhow::Result { + let key = CoseKey::from_slice(data).map_err(anyhow::Error::msg)?; + if key.kty() != iana::KeyType::OKP { + return Err(anyhow::Error::msg("invalid key type")); + } + if key.alg() != iana::Algorithm::EdDSA { + return Err(anyhow::Error::msg("invalid algorithm")); + } + if !key.is_crv(iana::EllipticCurve::Ed25519) { + return Err(anyhow::Error::msg("invalid ed25519 curve")); + } + + // TODO: more checks + Ok(Self(key)) + } + + pub fn to_slice(self) -> anyhow::Result> { + self.0.to_slice() + } + + pub fn public(&self) -> anyhow::Result { + let mut key = self.0.clone(); + if !self.0.has_param(&KEY_PARAM_X) { + if let Some(val) = self + .0 + .get_param(&KEY_PARAM_D)? + .as_bytes() + .filter(|v| v.len() == 32) + { + let mut secret: SecretKey = [0u8; 32]; + secret.copy_from_slice(val); + let public = &SigningKey::from_bytes(&secret).verifying_key(); + key.params + .push((KEY_PARAM_X, Value::Bytes(public.to_bytes().to_vec()))); + } + } + + key.params.retain(|(label, _)| label != &KEY_PARAM_D); + // TODO: more checks + Ok(Self(key)) + } + + pub fn get_secret(&self) -> anyhow::Result { + let val = self + .0 + .get_param(&KEY_PARAM_D)? + .as_bytes() + .filter(|v| v.len() == 32) + .ok_or_else(|| anyhow::Error::msg("invalid ed25519 secret key"))?; + let mut key: SecretKey = [0u8; 32]; + key.copy_from_slice(val); + Ok(key) + } + + pub fn get_public(&self) -> anyhow::Result<[u8; 32]> { + if let Ok(Some(val)) = self + .0 + .get_param(&KEY_PARAM_X) + .map(|v| v.as_bytes().filter(|v| v.len() == 32)) + { + let mut key: [u8; 32] = [0u8; 32]; + key.copy_from_slice(val); + return Ok(key); + } + + let secret: SecretKey = self + .get_secret() + .map_err(|_e| anyhow::Error::msg("invalid ed25519 public key"))?; + Ok(SigningKey::from_bytes(&secret).verifying_key().to_bytes()) + } + + pub fn signer(&self) -> anyhow::Result { + let key = self.get_secret()?; + Ok(Ed25519Signer(self.0.key_id.clone(), key)) + } + + pub fn verifier(&self) -> anyhow::Result { + let key = self.get_public()?; + Ok(Ed25519Verifier(key)) + } } -pub fn sign_message(sk: &SigningKey, msg: &str) -> Signature { - sk.sign(msg.as_bytes()) +pub struct Ed25519Signer(Vec, SecretKey); + +impl CoseSigner for Ed25519Signer { + fn alg(&self) -> iana::Algorithm { + iana::Algorithm::EdDSA + } + + fn kid(&self) -> Vec { + self.0.clone() + } + + fn sign(&self, data: &[u8]) -> Vec { + let sk = SigningKey::from_bytes(&self.1); + sk.sign(data).to_vec() + } } -pub fn verify_message(pk: &VerifyingKey, msg: &str, sig: &[u8]) -> anyhow::Result<()> { - let sig = Signature::from_slice(sig)?; - pk.verify_strict(msg.as_bytes(), &sig)?; - Ok(()) +pub struct Ed25519Verifier([u8; 32]); + +impl CoseVerifier for Ed25519Verifier { + fn alg(&self) -> iana::Algorithm { + iana::Algorithm::EdDSA + } + + fn verify(&self, data: &[u8], sig: &[u8]) -> Result<(), anyhow::Error> { + let pk = VerifyingKey::from_bytes(&self.0)?; + let sig = Signature::from_slice(sig)?; + pk.verify_strict(data, &sig)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ed25519_key_works() { + let msg = b"This is the content."; + let key = Ed25519Key::new(None).unwrap(); + let signer = key.signer().unwrap(); + let sig = signer.sign(msg); + let verifier = key.verifier().unwrap(); + assert!(verifier.verify(msg, &sig).is_ok()); + + let key2 = Ed25519Key::from_secret(&key.get_secret().unwrap(), None).unwrap(); + assert_eq!(key2.signer().unwrap().sign(msg), sig); + assert!(key2.verifier().is_ok()); + + let key2 = Ed25519Key::from_public(&key.get_public().unwrap(), None).unwrap(); + assert!(key2.verifier().unwrap().verify(msg, &sig).is_ok()); + assert!(key2.signer().is_err()); + + let key2 = Ed25519Key::from_slice(&key.to_slice().unwrap()).unwrap(); + assert_eq!(key2.signer().unwrap().sign(msg), sig); + assert!(key2.verifier().is_ok()); + + let key2 = key2.public().unwrap(); + assert!(key2.verifier().unwrap().verify(msg, &sig).is_ok()); + assert!(key2.signer().is_err()); + } } diff --git a/crates/ns-inscriber/src/wallet/encrypt.rs b/crates/ns-inscriber/src/wallet/encrypt.rs index 6516fe7..b124213 100644 --- a/crates/ns-inscriber/src/wallet/encrypt.rs +++ b/crates/ns-inscriber/src/wallet/encrypt.rs @@ -2,33 +2,48 @@ use aes_gcm::{ aead::{AeadCore, KeyInit}, AeadInPlace, Aes256Gcm, Key, Nonce, }; -use coset::{iana, CoseEncrypt0, CoseEncrypt0Builder, HeaderBuilder, TaggedCborSerializable}; +use coset::{iana, CborSerializable, CoseEncrypt0, CoseEncrypt0Builder, HeaderBuilder}; use rand_core::OsRng; +use ns_protocol::{ns::Value, state::to_bytes}; + +use super::{skip_tag, with_tag, ENCRYPT0_TAG}; + pub struct Encrypt0 { + kid: Option, cipher: Aes256Gcm, } impl Encrypt0 { - pub fn new(key: [u8; 32]) -> Self { + pub fn new(key: [u8; 32], kid: Option) -> Self { let key = Key::::from_slice(&key); let cipher = Aes256Gcm::new(key); - Self { cipher } + Self { kid, cipher } } - pub fn encrypt(&self, plaintext: &[u8], aad: &[u8], kid: &[u8]) -> anyhow::Result> { + pub fn encrypt( + &self, + plaintext: &[u8], + aad: &[u8], + cid: Option, + ) -> anyhow::Result> { let protected = HeaderBuilder::new() .algorithm(iana::Algorithm::A256GCM) .build(); let nonce = Aes256Gcm::generate_nonce(&mut OsRng); - let unprotected = HeaderBuilder::new() - .key_id(kid.to_vec()) - .iv(nonce.to_vec()) - .build(); + let mut unprotected = HeaderBuilder::new() + .key_id(to_bytes(&self.kid)?) + .iv(nonce.to_vec()); + if let Some(kid) = self.kid.as_ref() { + unprotected = unprotected.key_id(to_bytes(kid)?); + } + if let Some(cid) = cid { + unprotected = unprotected.text_value("cid".to_string(), cid); + } let e0 = CoseEncrypt0Builder::new() .protected(protected) - .unprotected(unprotected) + .unprotected(unprotected.build()) .create_ciphertext(plaintext, aad, |plain, enc| { let mut buf: Vec = Vec::with_capacity(plain.len() + 16); buf.extend_from_slice(plain); @@ -36,11 +51,15 @@ impl Encrypt0 { buf }) .build(); - e0.to_tagged_vec().map_err(anyhow::Error::msg) + Ok(with_tag( + &ENCRYPT0_TAG, + e0.to_vec().map_err(anyhow::Error::msg)?.as_slice(), + )) } pub fn decrypt(&self, encrypt0_data: &[u8], aad: &[u8]) -> anyhow::Result> { - let e0 = CoseEncrypt0::from_tagged_slice(encrypt0_data).map_err(anyhow::Error::msg)?; + let e0 = CoseEncrypt0::from_slice(skip_tag(&ENCRYPT0_TAG, encrypt0_data)) + .map_err(anyhow::Error::msg)?; if e0.unprotected.iv.len() != 12 { return Err(anyhow::Error::msg("invalid iv length")); } @@ -67,14 +86,18 @@ mod tests { let mut key = [0u8; 32]; OsRng.fill_bytes(&mut key); - let encrypt0 = Encrypt0::new(key); + let encrypt0 = Encrypt0::new(key, None); let plaintext = b"hello world"; - let data = encrypt0.encrypt(plaintext, b"yiwen.ai", b"test").unwrap(); + let data = encrypt0 + .encrypt(plaintext, b"Name & Service Protocol", None) + .unwrap(); // println!("{}", hex_string(&data)); - let res = encrypt0.decrypt(&data, b"yiwen.ai").unwrap(); + let res = encrypt0.decrypt(&data, b"Name & Service Protocol").unwrap(); assert_eq!(res, plaintext); - assert!(encrypt0.decrypt(&data[1..], b"yiwen.ai").is_err()); - assert!(encrypt0.decrypt(&data, b"yiwen").is_err()); + assert!(encrypt0 + .decrypt(&data[2..], b"Name & Service Protocol") + .is_err()); + assert!(encrypt0.decrypt(&data, b"NS").is_err()); } } diff --git a/crates/ns-inscriber/src/wallet/mod.rs b/crates/ns-inscriber/src/wallet/mod.rs index 0945cf4..ab7ce5e 100644 --- a/crates/ns-inscriber/src/wallet/mod.rs +++ b/crates/ns-inscriber/src/wallet/mod.rs @@ -6,14 +6,18 @@ mod cose_key; pub mod ed25519; mod encrypt; pub mod secp256k1; +mod sign; pub use bitcoin::bip32::DerivationPath; -pub use cose_key::Key; +pub use cose_key::{new_sym, CoseSigner, CoseVerifier, KeyHelper}; pub use coset::iana; pub use encrypt::Encrypt0; +pub use sign::{decode_sign1, encode_sign1}; // https://www.rfc-editor.org/rfc/rfc8949.html#name-self-described-cbor pub const CBOR_TAG: [u8; 3] = [0xd9, 0xd9, 0xf7]; +pub const ENCRYPT0_TAG: [u8; 1] = [0xd0]; +pub const SIGN1_TAG: [u8; 1] = [0xd2]; pub fn base64url_encode(data: &[u8]) -> String { general_purpose::URL_SAFE_NO_PAD.encode(data) @@ -35,18 +39,19 @@ pub fn base64_decode(data: &str) -> anyhow::Result> { .map_err(anyhow::Error::msg) } -pub fn wrap_cbor_tag(data: &[u8]) -> Vec { - let mut buf: Vec = Vec::with_capacity(data.len() + 3); - buf.extend_from_slice(&CBOR_TAG); +pub fn with_tag(tag: &[u8], data: &[u8]) -> Vec { + let mut buf: Vec = Vec::with_capacity(data.len() + tag.len()); + buf.extend_from_slice(tag); buf.extend_from_slice(data); buf } -pub fn unwrap_cbor_tag(data: &[u8]) -> &[u8] { - if data.len() > 3 && data[..3] == CBOR_TAG { - return &data[3..]; +pub fn skip_tag<'a>(tag: &'a [u8], data: &'a [u8]) -> &'a [u8] { + if data.starts_with(tag) { + &data[tag.len()..] + } else { + data } - data } pub fn hash_256(data: &[u8]) -> [u8; 32] { diff --git a/crates/ns-inscriber/src/wallet/secp256k1.rs b/crates/ns-inscriber/src/wallet/secp256k1.rs index 99ab66f..c4b5ad9 100644 --- a/crates/ns-inscriber/src/wallet/secp256k1.rs +++ b/crates/ns-inscriber/src/wallet/secp256k1.rs @@ -1,5 +1,5 @@ use bitcoin::Network; -// use bip32::{DerivationPath, XPrv}; +use coset::{iana, CoseKey, KeyType, Label, RegisteredLabelWithPrivate}; use rand_core::OsRng; pub use bitcoin::{ @@ -13,6 +13,10 @@ pub use bitcoin::{ ScriptBuf, }; +use ns_protocol::ns::Value; + +use super::KeyHelper; + pub fn derive_secp256k1( secp: &Secp256k1, network: Network, @@ -77,3 +81,33 @@ where } Ok(()) } + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Secp256k1Key(pub CoseKey); + +impl Secp256k1Key { + pub fn from_secret(secret: &[u8; 32], kid: Option) -> anyhow::Result { + let mut key = CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + alg: Some(RegisteredLabelWithPrivate::Assigned( + iana::Algorithm::ES256K, + )), + params: vec![ + ( + Label::Int(iana::Ec2KeyParameter::Crv as i64), + Value::from(iana::EllipticCurve::Secp256k1 as i64), + ), + ( + Label::Int(iana::Ec2KeyParameter::D as i64), + Value::Bytes(secret.to_vec()), + ), + ], + ..Default::default() + }; + + if let Some(kid) = kid { + key.set_kid(kid)?; + } + Ok(Self(key)) + } +} diff --git a/crates/ns-inscriber/src/wallet/sign.rs b/crates/ns-inscriber/src/wallet/sign.rs new file mode 100644 index 0000000..a5a341e --- /dev/null +++ b/crates/ns-inscriber/src/wallet/sign.rs @@ -0,0 +1,73 @@ +use coset::{CborSerializable, CoseSign1, CoseSign1Builder, HeaderBuilder}; + +use super::{skip_tag, with_tag, CoseSigner, CoseVerifier, SIGN1_TAG}; + +pub fn encode_sign1( + signer: impl CoseSigner, + payload: Vec, + aad: &[u8], +) -> anyhow::Result> { + let protected = HeaderBuilder::new().algorithm(signer.alg()).build(); + let unprotected = HeaderBuilder::new().key_id(signer.kid()).build(); + + let data = CoseSign1Builder::new() + .protected(protected) + .unprotected(unprotected) + .payload(payload) + .create_signature(aad, |data| signer.sign(data)) + .build() + .to_vec() + .map_err(anyhow::Error::msg)?; + Ok(with_tag(&SIGN1_TAG, &data)) +} + +pub fn decode_sign1( + verifier: impl CoseVerifier, + sign1_data: &[u8], + aad: &[u8], +) -> anyhow::Result> { + let msg = + CoseSign1::from_slice(skip_tag(&SIGN1_TAG, sign1_data)).map_err(anyhow::Error::msg)?; + msg.verify_signature(aad, |sig, data| verifier.verify(data, sig))?; + msg.payload + .ok_or_else(|| anyhow::Error::msg("missing payload")) +} + +#[cfg(test)] +mod tests { + use hex_literal::hex; + use ns_protocol::ns::Value; + + use super::*; + + use crate::wallet::cose_key::KeyHelper; + use crate::wallet::ed25519; + + #[test] + fn sign1_works() { + let secret = hex!("57c92077664146e876760c9520d054aa93c3afb04e306705db6090308507b4d3"); + let msg = b"This is the content."; + let aad = hex!("11aa22bb33cc44dd55006699"); + let kid = Value::Text("11".to_string()); + let key = ed25519::Ed25519Key::from_secret(&secret, Some(kid.clone())).unwrap(); + assert_eq!(key.0.kid(), Some(kid.clone())); + + let signer = key.signer().unwrap(); + let output = encode_sign1(signer, msg.to_vec(), &aad).unwrap(); + assert_eq!(output, hex!("d28443a10127a1044362313154546869732069732074686520636f6e74656e742e584011319ba8e8508d613f5cc83bbb64d37e1b310582777ff8a7ec587c12879fb9a83c593167a65438d2e6a8906ea1da4296a8fcb5d1ebed9a6de157f1ba2257070d").to_vec()); + + let verifier = key.verifier().unwrap(); + let msg2 = decode_sign1(verifier, &output, &aad).unwrap(); + assert_eq!(msg2.as_slice(), msg.as_slice()); + + let pk = key.public().unwrap(); + assert_eq!(pk.0.kid(), Some(kid)); + assert_eq!( + pk.get_public().unwrap().as_slice(), + hex!("8373deeba9c0af9880e5c9e976ffda8522db9e3df20fddfe54b3a8c59cfe3c94").as_slice() + ); + let verifier = pk.verifier().unwrap(); + let msg2 = decode_sign1(verifier, &output, &aad).unwrap(); + assert_eq!(msg2.as_slice(), msg.as_slice()); + } +}