Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for NIST P-256 curve #185

Merged
merged 6 commits into from
Feb 17, 2025
Merged

Conversation

AlfioEmanueleFresta
Copy link
Contributor

@AlfioEmanueleFresta AlfioEmanueleFresta commented Sep 28, 2024

Changes

Motivation

This curve is used for the Noise handshake in CTAP2 Hybrid Transport. This is the protocol used for browsers to access FIDO2 credentials (aka Passkeys) on roaming authenticators connected over the internet, such as phones running Android and iOS.

I'm using snow in my implementation of CTAP2, aimed at bringing Passkeys to the Linux desktop, see xdg-credentials-portal.

Tests

All commands below succeed:

$ cargo build --all-targets
$ cargo build --all-targets --features p256
$ cargo test --all-targets
$ cargo test --all-targets --features p256

@AlfioEmanueleFresta AlfioEmanueleFresta changed the title Add support for p256 Add support for NIST P-256 curve Sep 28, 2024
AlfioEmanueleFresta added a commit to linux-credentials/libwebauthn that referenced this pull request Jan 27, 2025
…nt) (#51)

An initial rough implementation of hybrid transport - formerly known as Cloud Assisted Bluetooth Low Energy, or caBLE.

## Includes

* Generates QR code for QR-initiated transactions
* Adds WebAuthn example which prints QR code to terminal (`examples/webauthn_cable.rs`)
* Adds submodule fork of `snow` (noise protocol framework) with NIST P-256 curve implementation, as required by CTAP spec (see mcginty/snow#185)

## Limitations and follow ups

* Unable to discover Android devices (created issue #52)
* Does not yet support state-assisted transactions (part of #31)
* Clean up (part of #31)
@AlfioEmanueleFresta
Copy link
Contributor Author

Friendy ping on this @mcginty - I would need this change to be upstreamed so we can begin publishing linux-credentials/libwebauthn to crates.io. TIA!

@mcginty
Copy link
Owner

mcginty commented Feb 13, 2025

@AlfioEmanueleFresta Wow, I'm so sorry I dropped the ball on this! Reviewing now.

Copy link
Owner

@mcginty mcginty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! This is looking pretty good, I just have some small comments/questions.

src/constants.rs Outdated
@@ -4,7 +4,7 @@ pub const TAGLEN: usize = 16;

pub const MAXHASHLEN: usize = 64;
pub const MAXBLOCKLEN: usize = 128;
pub const MAXDHLEN: usize = 56;
pub const MAXDHLEN: usize = 65;
Copy link
Owner

@mcginty mcginty Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should feature-gate this as well so we don't change any constants in the default state (although really our MAXDHLEN should be 32 by default here, since we don't support Curve448 as of yet, but that's another fix outside of this PR :)

Suggested change
pub const MAXDHLEN: usize = 65;
// P-256 uncompressed SEC-1 encodings are 65 bytes long, larger than the `MAXDHLEN` in the official Noise spec.
#[cfg(feature = "p256")]
pub const MAXDHLEN: usize = 65;
// Curve448 keys are the largest in the official Noise spec.
#[cfg(not(feature = "p256"))]
pub const MAXDHLEN: usize = 56;

#[cfg(feature = "p256")]
#[derive(Default)]
struct P256 {
privkey: [u8; 32],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to store privkey as a p256::SecretKey here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave this a try again, as I couldn't remember why - it seems it's not trivial:

error[E0515]: cannot return reference to temporary value
   --> src/resolvers/default.rs:241:9
    |
241 |         &self.pubkey.to_sec1_bytes()
    |         ^---------------------------
    |         ||
    |         |temporary value created here
    |         returns a reference to data owned by the current function

The issue is that the Dh trait requires exporting slices:

pub trait Dh: Send + Sync {
    fn pubkey(&self) -> &[u8];
    fn privkey(&self) -> &[u8];

However, p256::SecretKey and p256::PublicKey only return owned types (eg. FieldBytes<...>, or GenericArray<u8, ...>).

Pre-serializing the keys to their byte representation allows keeping the trait unchanged.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also took a quick look and agree this isn't a simple drop-in; the key types available only provide copied-array access to the inner bytes because they are stored as a "generic" field representation.

FWIW I have run into similar issues before with the use of slices for everything in the Dh implementations. It was when attempting to add support for ECDH directly in the ring provider because those also don't (at all) provide access to a slice representation of private keys.

Copy link
Owner

@mcginty mcginty Feb 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it -- that makes sense to keep it as-is then!

I can address this more holistically in a large rework of this trait later. It doesn't make sense to keep these slices if they're holding us back in the long run.

Comment on lines 693 to 707
let scalar =
Vec::<u8>::from_hex("58c77a30bb0fa177286346d18f59678ac1b8d3637ee65f1bd88a8f52e49ef189")
.unwrap();
keypair.set(&scalar);
let public = Vec::<u8>::from_hex(
"042009fddefeed3b342696b11683b423db8ede2ef5cd66af9b7db2772f7deaf3d\
f1a69a4648d990ae2a4e5928f156f32e15fa08ba4465df8cb17838dc2afb719d2",
)
.unwrap();
let mut output = [0u8; 32];
keypair.dh(&public, &mut output).unwrap();
assert_eq!(
hex::encode(output),
"07505b308650d07e6ead11dd36bbf6f24bf99fc6479649fadd3939faa33ddeb3"
);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did you get these test constants from? It would be good to have a citation to a known-good example to derive trust for this implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly do not recall, so I've replaced these with the test vectors from RFC 5903, and added some comments to this effect.

Copy link
Owner

@mcginty mcginty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! This is looking pretty good, I just have some small comments/questions.

@complexspaces
Copy link
Contributor

complexspaces commented Feb 13, 2025

As a drive-by comment, which might be helpful context: I asked Trevor at RWC one year what his thoughts were on CTAP Hybrid being specified with a non-standard Noise curve, as the designer of Noise because I was curious, and the answer was that it wasn't a big deal these days and that maybe future versions of Noise would be more curve agnostic anyway.

Tl;dr I think this change is worthwhile for sure :)

@mcginty
Copy link
Owner

mcginty commented Feb 13, 2025

@complexspaces thanks for the context! Yeah, generally I'm open to snow supporting "unofficial" general-use Noise modes, as long as the default snow only exposes the officially supported modes and unofficial modifications are feature-gated, just as this PR does.

@AlfioEmanueleFresta
Copy link
Contributor Author

Thanks for the review @mcginty! Addressed your comments.

@complexspaces that's interesting! FWIW, here are some reasons P-256 might have been chosen for the FIDO specs: fido-alliance/discussions#2 (comment)

@complexspaces
Copy link
Contributor

@AlfioEmanueleFresta Yeah it makes a lot of sense why they picked P-256, including the reasons listed in your link. I don't think it was done harmfully to be clear, just out of necessity for what algorithms existing key APIs supported across OSes.

@AlfioEmanueleFresta AlfioEmanueleFresta marked this pull request as draft February 17, 2025 20:54
@AlfioEmanueleFresta AlfioEmanueleFresta marked this pull request as ready for review February 17, 2025 20:55
Copy link
Owner

@mcginty mcginty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thank you very much for making the change and patience in needing to ping me to review it :).

@mcginty mcginty merged commit 656a56f into mcginty:main Feb 17, 2025
3 checks passed
mcginty pushed a commit that referenced this pull request Feb 18, 2025
* Adds support for NIST P-256, aka secp256r1, using the p256 crate from RustCrypto. This is a pure-Rust implementation.
* As per feedback in previous abandoned PR try to add secp256k1_support #38 (comment), the code is feature gated, as this is a non-standard curve.
* Includes PR Update Cargo.toml #181 which updates curve25519-dalek, which unbreaks the build for me.
AlfioEmanueleFresta added a commit to linux-credentials/libwebauthn that referenced this pull request Feb 19, 2025
PR mcginty/snow#185 was merged into snow
v0.10.0-alpha.1, adding support for P-256. This is required by the CTAP
Hybrid Transport spec.

This PR deprecates the snow fork, in favour of the new upstream version
containing my changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants