From 03e1145a5220c56560aaab6ce51068c6ed4a4d20 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Mon, 20 Nov 2023 15:38:29 -0700 Subject: [PATCH] ssh-key: `p521` feature (#180) Adds initial integration with the `p521` crate, including support for converting public and private keys to `p521` keys, as well as signing and verifying NIST P-521 signatures. Note that there's a lot of redundancy/boilerplate repeated for `p256`, `p384`, and `p521` it would be nice to eventually DRY out, possibly using macros to write the impls. --- .github/workflows/ssh-key.yml | 14 ++--- Cargo.lock | 15 +++++ ssh-key/Cargo.toml | 5 +- ssh-key/src/private.rs | 2 +- ssh-key/src/private/ecdsa.rs | 20 +++++++ ssh-key/src/public/ecdsa.rs | 37 +++++++++++++ ssh-key/src/signature.rs | 100 ++++++++++++++++++++++++++++++++-- 7 files changed, 177 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ssh-key.yml b/.github/workflows/ssh-key.yml index f402cd9..d3f07dc 100644 --- a/.github/workflows/ssh-key.yml +++ b/.github/workflows/ssh-key.yml @@ -49,14 +49,10 @@ jobs: - run: cargo build --target ${{ matrix.target }} --release --all-features minimal-versions: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@nightly - - run: cargo +nightly update -Z minimal-versions - - uses: dtolnay/rust-toolchain@1.65.0 - - run: rm ../Cargo.toml - - run: cargo test --all-features --release + uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master + with: + stable-cmd: cargo test --all-features --release + working-directory: ${{ github.workflow }} no_std: runs-on: ubuntu-latest @@ -75,7 +71,7 @@ jobs: toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} - uses: RustCrypto/actions/cargo-hack-install@master - - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features crypto,default,getrandom,std --release + - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features crypto,default,getrandom,p521,std --release test: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index e87b7cc..8ab665d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -499,6 +499,20 @@ dependencies = [ "sha2", ] +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -801,6 +815,7 @@ dependencies = [ "num-bigint-dig", "p256", "p384", + "p521", "rand_chacha", "rand_core", "rsa", diff --git a/ssh-key/Cargo.toml b/ssh-key/Cargo.toml index 2780bda..af39069 100644 --- a/ssh-key/Cargo.toml +++ b/ssh-key/Cargo.toml @@ -31,6 +31,7 @@ dsa = { version = "0.6", optional = true, default-features = false } ed25519-dalek = { version = "2", optional = true, default-features = false } p256 = { version = "0.13", optional = true, default-features = false, features = ["ecdsa"] } p384 = { version = "0.13", optional = true, default-features = false, features = ["ecdsa"] } +p521 = { version = "0.13.3", optional = true, default-features = false, features = ["ecdsa", "getrandom"] } # TODO(tarcieri): RFC6979 rand_core = { version = "0.6.4", optional = true, default-features = false } rsa = { version = "0.9", optional = true, default-features = false, features = ["sha2"] } sec1 = { version = "0.7.3", optional = true, default-features = false, features = ["point"] } @@ -53,12 +54,13 @@ std = [ "encoding/std", "p256?/std", "p384?/std", + "p521?/std", "rsa?/std", "sec1?/std", "signature/std" ] -crypto = ["ed25519", "p256", "p384", "rsa"] # NOTE: `dsa` is obsolete/weak +crypto = ["ed25519", "p256", "p384", "p521", "rsa"] # NOTE: `dsa` is obsolete/weak dsa = ["dep:bigint", "dep:dsa", "dep:sha1", "alloc", "signature/rand_core"] ecdsa = ["dep:sec1"] ed25519 = ["dep:ed25519-dalek", "rand_core"] @@ -74,6 +76,7 @@ encryption = [ getrandom = ["rand_core/getrandom"] p256 = ["dep:p256", "ecdsa"] p384 = ["dep:p384", "ecdsa"] +p521 = ["dep:p521", "ecdsa"] rsa = ["dep:bigint", "dep:rsa", "alloc", "rand_core"] tdes = ["cipher/tdes", "encryption"] diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index c672226..311a1fe 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -490,7 +490,7 @@ impl PrivateKey { let key_data = match algorithm { #[cfg(feature = "dsa")] Algorithm::Dsa => KeypairData::from(DsaKeypair::random(rng)?), - #[cfg(any(feature = "p256", feature = "p384"))] + #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] Algorithm::Ecdsa { curve } => KeypairData::from(EcdsaKeypair::random(rng, curve)?), #[cfg(feature = "ed25519")] Algorithm::Ed25519 => KeypairData::from(Ed25519Keypair::random(rng)), diff --git a/ssh-key/src/private/ecdsa.rs b/ssh-key/src/private/ecdsa.rs index 19e490d..33d196b 100644 --- a/ssh-key/src/private/ecdsa.rs +++ b/ssh-key/src/private/ecdsa.rs @@ -141,6 +141,16 @@ impl From for EcdsaPrivateKey<48> { } } +#[cfg(feature = "p521")] +impl From for EcdsaPrivateKey<66> { + fn from(sk: p521::SecretKey) -> EcdsaPrivateKey<66> { + // TODO(tarcieri): clean this up when migrating to hybrid-array + let mut bytes = [0u8; 66]; + bytes.copy_from_slice(&sk.to_bytes()); + EcdsaPrivateKey { bytes } + } +} + /// Elliptic Curve Digital Signature Algorithm (ECDSA) private/public keypair. #[derive(Clone, Debug)] pub enum EcdsaKeypair { @@ -196,6 +206,16 @@ impl EcdsaKeypair { public: public.into(), }) } + #[cfg(feature = "p521")] + EcdsaCurve::NistP521 => { + let private = p521::SecretKey::random(rng); + let public = private.public_key(); + Ok(EcdsaKeypair::NistP521 { + private: private.into(), + public: public.into(), + }) + } + #[cfg(not(all(feature = "p256", feature = "p384", feature = "p521")))] _ => Err(Error::AlgorithmUnknown), } } diff --git a/ssh-key/src/public/ecdsa.rs b/ssh-key/src/public/ecdsa.rs index 997274c..6675ca4 100644 --- a/ssh-key/src/public/ecdsa.rs +++ b/ssh-key/src/public/ecdsa.rs @@ -173,6 +173,15 @@ impl TryFrom for p384::ecdsa::VerifyingKey { } } +#[cfg(feature = "p521")] +impl TryFrom for p521::ecdsa::VerifyingKey { + type Error = Error; + + fn try_from(key: EcdsaPublicKey) -> Result { + p521::ecdsa::VerifyingKey::try_from(&key) + } +} + #[cfg(feature = "p256")] impl TryFrom<&EcdsaPublicKey> for p256::ecdsa::VerifyingKey { type Error = Error; @@ -201,6 +210,20 @@ impl TryFrom<&EcdsaPublicKey> for p384::ecdsa::VerifyingKey { } } +#[cfg(feature = "p521")] +impl TryFrom<&EcdsaPublicKey> for p521::ecdsa::VerifyingKey { + type Error = Error; + + fn try_from(public_key: &EcdsaPublicKey) -> Result { + match public_key { + EcdsaPublicKey::NistP521(key) => { + p521::ecdsa::VerifyingKey::from_encoded_point(key).map_err(|_| Error::Crypto) + } + _ => Err(Error::AlgorithmUnknown), + } + } +} + #[cfg(feature = "p256")] impl From for EcdsaPublicKey { fn from(key: p256::ecdsa::VerifyingKey) -> EcdsaPublicKey { @@ -228,3 +251,17 @@ impl From<&p384::ecdsa::VerifyingKey> for EcdsaPublicKey { EcdsaPublicKey::NistP384(key.to_encoded_point(false)) } } + +#[cfg(feature = "p521")] +impl From for EcdsaPublicKey { + fn from(key: p521::ecdsa::VerifyingKey) -> EcdsaPublicKey { + EcdsaPublicKey::from(&key) + } +} + +#[cfg(feature = "p521")] +impl From<&p521::ecdsa::VerifyingKey> for EcdsaPublicKey { + fn from(key: &p521::ecdsa::VerifyingKey) -> EcdsaPublicKey { + EcdsaPublicKey::NistP521(key.to_encoded_point(false)) + } +} diff --git a/ssh-key/src/signature.rs b/ssh-key/src/signature.rs index 72b80ad..9557574 100644 --- a/ssh-key/src/signature.rs +++ b/ssh-key/src/signature.rs @@ -17,7 +17,7 @@ use { signature::{DigestSigner, DigestVerifier}, }; -#[cfg(any(feature = "p256", feature = "p384"))] +#[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] use crate::{ private::{EcdsaKeypair, EcdsaPrivateKey}, public::EcdsaPublicKey, @@ -280,7 +280,7 @@ impl Signer for private::KeypairData { match self { #[cfg(feature = "dsa")] Self::Dsa(keypair) => keypair.try_sign(message), - #[cfg(any(feature = "p256", feature = "p384"))] + #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] Self::Ecdsa(keypair) => keypair.try_sign(message), #[cfg(feature = "ed25519")] Self::Ed25519(keypair) => keypair.try_sign(message), @@ -303,7 +303,7 @@ impl Verifier for public::KeyData { match self { #[cfg(feature = "dsa")] Self::Dsa(pk) => pk.verify(message, signature), - #[cfg(any(feature = "p256", feature = "p384"))] + #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] Self::Ecdsa(pk) => pk.verify(message, signature), #[cfg(feature = "ed25519")] Self::Ed25519(pk) => pk.verify(message, signature), @@ -477,6 +477,15 @@ impl TryFrom for Signature { } } +#[cfg(feature = "p521")] +impl TryFrom for Signature { + type Error = Error; + + fn try_from(signature: p521::ecdsa::Signature) -> Result { + Signature::try_from(&signature) + } +} + #[cfg(feature = "p256")] impl TryFrom<&p256::ecdsa::Signature> for Signature { type Error = Error; @@ -521,6 +530,28 @@ impl TryFrom<&p384::ecdsa::Signature> for Signature { } } +#[cfg(feature = "p521")] +impl TryFrom<&p521::ecdsa::Signature> for Signature { + type Error = Error; + + fn try_from(signature: &p521::ecdsa::Signature) -> Result { + let (r, s) = signature.split_bytes(); + + #[allow(clippy::arithmetic_side_effects)] + let mut data = Vec::with_capacity(48 * 2 + 4 * 2 + 2); + + Mpint::from_positive_bytes(&r)?.encode(&mut data)?; + Mpint::from_positive_bytes(&s)?.encode(&mut data)?; + + Ok(Signature { + algorithm: Algorithm::Ecdsa { + curve: EcdsaCurve::NistP521, + }, + data, + }) + } +} + #[cfg(feature = "p256")] impl TryFrom for p256::ecdsa::Signature { type Error = Error; @@ -539,6 +570,15 @@ impl TryFrom for p384::ecdsa::Signature { } } +#[cfg(feature = "p521")] +impl TryFrom for p521::ecdsa::Signature { + type Error = Error; + + fn try_from(signature: Signature) -> Result { + p521::ecdsa::Signature::try_from(&signature) + } +} + #[cfg(feature = "p256")] impl TryFrom<&Signature> for p256::ecdsa::Signature { type Error = Error; @@ -601,7 +641,37 @@ impl TryFrom<&Signature> for p384::ecdsa::Signature { } } -#[cfg(any(feature = "p256", feature = "p384"))] +#[cfg(feature = "p521")] +impl TryFrom<&Signature> for p521::ecdsa::Signature { + type Error = Error; + + fn try_from(signature: &Signature) -> Result { + const FIELD_SIZE: usize = 48; + + match signature.algorithm { + Algorithm::Ecdsa { + curve: EcdsaCurve::NistP256, + } => { + let reader = &mut signature.as_bytes(); + let r = Mpint::decode(reader)?; + let s = Mpint::decode(reader)?; + + match (r.as_positive_bytes(), s.as_positive_bytes()) { + (Some(r), Some(s)) if r.len() == FIELD_SIZE && s.len() == FIELD_SIZE => { + Ok(p521::ecdsa::Signature::from_scalars( + *p521::FieldBytes::from_slice(r), + *p521::FieldBytes::from_slice(s), + )?) + } + _ => Err(Error::Crypto), + } + } + _ => Err(signature.algorithm.clone().unsupported_error()), + } + } +} + +#[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] impl Signer for EcdsaKeypair { fn try_sign(&self, message: &[u8]) -> signature::Result { match self { @@ -609,6 +679,9 @@ impl Signer for EcdsaKeypair { Self::NistP256 { private, .. } => private.try_sign(message), #[cfg(feature = "p384")] Self::NistP384 { private, .. } => private.try_sign(message), + #[cfg(feature = "p521")] + Self::NistP521 { private, .. } => private.try_sign(message), + #[cfg(not(all(feature = "p256", feature = "p384", feature = "p521")))] _ => Err(self.algorithm().unsupported_error().into()), } } @@ -632,7 +705,16 @@ impl Signer for EcdsaPrivateKey<48> { } } -#[cfg(any(feature = "p256", feature = "p384"))] +#[cfg(feature = "p521")] +impl Signer for EcdsaPrivateKey<66> { + fn try_sign(&self, message: &[u8]) -> signature::Result { + let signing_key = p521::ecdsa::SigningKey::from_slice(self.as_ref())?; + let signature: p521::ecdsa::Signature = signing_key.try_sign(message)?; + Ok(signature.try_into()?) + } +} + +#[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] impl Verifier for EcdsaPublicKey { fn verify(&self, message: &[u8], signature: &Signature) -> signature::Result<()> { match signature.algorithm { @@ -651,6 +733,14 @@ impl Verifier for EcdsaPublicKey { verifying_key.verify(message, &signature) } + #[cfg(feature = "p521")] + EcdsaCurve::NistP521 => { + let verifying_key = p521::ecdsa::VerifyingKey::try_from(self)?; + let signature = p521::ecdsa::Signature::try_from(signature)?; + verifying_key.verify(message, &signature) + } + + #[cfg(not(all(feature = "p256", feature = "p384", feature = "p521")))] _ => Err(signature.algorithm().unsupported_error().into()), }, _ => Err(signature.algorithm().unsupported_error().into()),