Skip to content

Commit

Permalink
ssh-key: p521 feature
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tarcieri committed Nov 20, 2023
1 parent af04d97 commit 93279ad
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 16 deletions.
14 changes: 5 additions & 9 deletions .github/workflows/ssh-key.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]
- 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
Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion ssh-key/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"]
Expand All @@ -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"]

Expand Down
2 changes: 1 addition & 1 deletion ssh-key/src/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
20 changes: 20 additions & 0 deletions ssh-key/src/private/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@ impl From<p384::SecretKey> for EcdsaPrivateKey<48> {
}
}

#[cfg(feature = "p521")]
impl From<p521::SecretKey> 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 {
Expand Down Expand Up @@ -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),
}
}
Expand Down
37 changes: 37 additions & 0 deletions ssh-key/src/public/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ impl TryFrom<EcdsaPublicKey> for p384::ecdsa::VerifyingKey {
}
}

#[cfg(feature = "p521")]
impl TryFrom<EcdsaPublicKey> for p521::ecdsa::VerifyingKey {
type Error = Error;

fn try_from(key: EcdsaPublicKey) -> Result<p521::ecdsa::VerifyingKey> {
p521::ecdsa::VerifyingKey::try_from(&key)
}
}

#[cfg(feature = "p256")]
impl TryFrom<&EcdsaPublicKey> for p256::ecdsa::VerifyingKey {
type Error = Error;
Expand Down Expand Up @@ -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<p521::ecdsa::VerifyingKey> {
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<p256::ecdsa::VerifyingKey> for EcdsaPublicKey {
fn from(key: p256::ecdsa::VerifyingKey) -> EcdsaPublicKey {
Expand Down Expand Up @@ -228,3 +251,17 @@ impl From<&p384::ecdsa::VerifyingKey> for EcdsaPublicKey {
EcdsaPublicKey::NistP384(key.to_encoded_point(false))
}
}

#[cfg(feature = "p521")]
impl From<p521::ecdsa::VerifyingKey> 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))
}
}
100 changes: 95 additions & 5 deletions ssh-key/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -280,7 +280,7 @@ impl Signer<Signature> 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),
Expand All @@ -303,7 +303,7 @@ impl Verifier<Signature> 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),
Expand Down Expand Up @@ -477,6 +477,15 @@ 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<Signature> {
Signature::try_from(&signature)
}
}

#[cfg(feature = "p256")]
impl TryFrom<&p256::ecdsa::Signature> for Signature {
type Error = Error;
Expand Down Expand Up @@ -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<Signature> {
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<Signature> for p256::ecdsa::Signature {
type Error = Error;
Expand All @@ -539,6 +570,15 @@ impl TryFrom<Signature> for p384::ecdsa::Signature {
}
}

#[cfg(feature = "p521")]
impl TryFrom<Signature> for p521::ecdsa::Signature {
type Error = Error;

fn try_from(signature: Signature) -> Result<p521::ecdsa::Signature> {
p521::ecdsa::Signature::try_from(&signature)
}
}

#[cfg(feature = "p256")]
impl TryFrom<&Signature> for p256::ecdsa::Signature {
type Error = Error;
Expand Down Expand Up @@ -601,14 +641,47 @@ 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<p521::ecdsa::Signature> {
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<Signature> for EcdsaKeypair {
fn try_sign(&self, message: &[u8]) -> signature::Result<Signature> {
match self {
#[cfg(feature = "p256")]
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()),
}
}
Expand All @@ -632,7 +705,16 @@ impl Signer<Signature> for EcdsaPrivateKey<48> {
}
}

#[cfg(any(feature = "p256", feature = "p384"))]
#[cfg(feature = "p521")]
impl Signer<Signature> for EcdsaPrivateKey<66> {
fn try_sign(&self, message: &[u8]) -> signature::Result<Signature> {
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<Signature> for EcdsaPublicKey {
fn verify(&self, message: &[u8], signature: &Signature) -> signature::Result<()> {
match signature.algorithm {
Expand All @@ -651,6 +733,14 @@ impl Verifier<Signature> 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()),
Expand Down

0 comments on commit 93279ad

Please sign in to comment.