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

NSEC3 and multiple key signing support. #416

Closed
wants to merge 545 commits into from
Closed

Conversation

ximon18
Copy link
Member

@ximon18 ximon18 commented Oct 15, 2024

Currently lacks collision detection and tests, though has been manually tested using ldns-verify-zone, dnssec-verify and named-checkzone both with and without opt-out and also including both signed and unsigned delegations.

I'm posting this here as a draft to allow for alignment and early feedback from the team working on various pieces of DNSSEC support for domain.

Note: This PR includes the content of #444.

@ximon18 ximon18 requested a review from a team October 15, 2024 14:18
@ximon18 ximon18 changed the title Initial NSEC3 generation support. NSEC3 generation support. Oct 17, 2024
@bal-e bal-e force-pushed the dnssec-key branch 2 times, most recently from a79aac6 to 0f54a8d Compare October 24, 2024 14:22
src/sign/ring.rs Outdated
Comment on lines 272 to 277
pub fn nsec3_hash<N, SaltOcts, HashOcts>(
owner: N,
algorithm: Nsec3HashAlg,
iterations: u16,
salt: &Nsec3Salt<SaltOcts>,
) -> Result<OwnerHash<HashOcts>, Nsec3HashError>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just take the owner name and an Nsec3param?

Copy link
Contributor

Choose a reason for hiding this comment

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

nsec3_default_hash can then be replaced by calling this function with default() for the second argument.

Copy link
Member Author

Choose a reason for hiding this comment

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

Why not just take the owner name and an Nsec3param?

I had that but took it out. There was a reason. I'll see if I can remember why.

Copy link
Member Author

@ximon18 ximon18 Oct 30, 2024

Choose a reason for hiding this comment

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

Ah I know why. The nsec3s() function takes an Nsec3Param struct which it uses to create the NSEC3PARAM RR at the apex of the zone. For the NSEC3PARAM RR RFC 5155 says that the opt-out flag in the flags field MUST be zero, so for RFC compliance the Nsec3Param struct passed to nsec3s() should have the opt-out flag set to zero.

Honestly I think this is a bit of a foot-gun and perhaps best not to pass an Nsec3Param to nsec3s() but instead only the other fields (algorithm, iterations, salt), however MAYBE in future it will be legal to set some of the other flag bits in the flags field and a user would want to have those set in the created NSEC3PARAM RR... so for that reason nsec3s() currently takes an Nsec3Param as input.

When generating NSEC3 RRs, and when opt-out is enabled, the flags value in the given Nsec3Param cannot be used as-is because the opt-out flag must be set to 1 (but NOT in the NSEC3PARAM RR), and rather than copy the given Nsec3Param or modify it and then pass it to nsec3_hash() I felt it was better to just pass only the values actually needed for hashing in, as NSEC3 hashing doesn't need the flags field at all, and also that way users don't have to think about what value to set the unused Nsec3Param::flags field to when invoking nsec3_hash() directly (as dnst nsec3-hash does).

src/sign/ring.rs Outdated
Comment on lines 325 to 326
let owner_hash = OwnerHash::from_octets(hash)
.map_err(|_| Nsec3HashError::OwnerHashError)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this should return an OwnerHashError. There are two failure cases for from_octets: if the hash is more than 255 bytes (impossible since NSEC3 doesn't support any such digest algorithms) or if memory could not be allocated (in which case we should return AppendError).

Copy link
Member Author

Choose a reason for hiding this comment

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

It's not clear to me from the docs on OwnerHashError that it only fails relating to length, it just says "The hashing process produced an invalid owner hash" and I have no way of knowing when that error might occur or why.

@ximon18
Copy link
Member Author

ximon18 commented Oct 30, 2024

@bal-e: I realize that I moved the nsec3_hash() function from validator/nsec.rs to sign/ring.rs I think because that more clearly showed that it was Ring powered/dependent. However, we were discussing yesterday that lots of functionality is shared between signing and validating and that right now you are putting everything under validation, while I proposed a common module shared by both signing and validating. Either way, if you are currently putting everything under validation, this move to sign is inconsistent and perhaps you think I should move it back?

@bal-e
Copy link
Contributor

bal-e commented Oct 30, 2024

@ximon18: yeah, I think this should be moved under validate for now. We'll gather up the shared functionality in that module for now, then decide how to distribute it.

@ximon18
Copy link
Member Author

ximon18 commented Oct 30, 2024

@ximon18: yeah, I think this should be moved under validate for now. We'll gather up the shared functionality in that module for now, then decide how to distribute it.

Done.

// the apex and the original owner name."
let distance_to_root = name.owner().iter_labels().count();
let distance_to_apex = distance_to_root - apex_label_count;
if distance_to_apex > 1 {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think the if statement is necessary, the for loop will run for zero iterations if this condition is not true.

Copy link
Member Author

Choose a reason for hiding this comment

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

True, but the if statement matches nicely with the RFC text and makes it clear that if we enter this block it is because of the condition identified by the RFC text.

@ximon18 ximon18 changed the base branch from dnssec-key to main November 8, 2024 12:13
@tertsdiepraam tertsdiepraam force-pushed the initial-nsec3-generation branch from cf2f450 to e1c1db8 Compare November 22, 2024 14:22
…m the NSEC unit test suite with the changes required for the NSEC3 case.
…enerate_nsecs().

- Added a unit test verifying compliance with the RFC 5155 Appendix A signed zone example.

- Imroved and added comments.

- Rewrote cryptic slice based NSEC3 next owner hashing loop with initial (untested!) type bit map merging and collision detection support, and removed no longer needed (but added in this branch) SortedRecords::as_mut_slice().

- Removed confusing duplicate storage of NSEC3 opt-out info in GenerateNsec3Config.

- Added missing (though not breaking) trailing periods in test data.
Copy link
Contributor

@bal-e bal-e left a comment

Choose a reason for hiding this comment

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

This is a massive PR, @ximon18, well done on getting all this functionality together into a cohesive unit!

  • My primary concern is with the large number of new traits added. I'm worried that they're trying to provide extensibility in a manner that won't actually be helpful for our target use cases. In some places, they can be replaced with simpler concrete data types, and I'd strongly prefer that.

  • A lot of public methods (e.g. in sign::records) are still missing documentation. Perhaps a Clippy lint can help catch everything?

  • There are a number of similarly named types and traits (e.g. DesignatedSigningKey, DnssecSigningKey, and SigningKey). Can some of these be combined together to reduce ambiguity in the public API? I'm worried users will get lost in the number of types and traits they might have to think about.

src/validate.rs Outdated
Comment on lines 1768 to 1781
match self {
Nsec3HashError::UnsupportedAlgorithm => {
f.write_str("Unsupported algorithm")
}
Nsec3HashError::AppendError => {
f.write_str("Append error: out of memory?")
}
Nsec3HashError::OwnerHashError => {
f.write_str("Hashing produced an invalid owner hash")
}
Nsec3HashError::CollisionDetected => {
f.write_str("Hash collision detected")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Factor the f.write_str() out of the match expression.

Cargo.toml Outdated
Comment on lines 87 to 100
itertools = "0.13.0"
lazy_static = { version = "1.4.0" }
pretty_assertions = "1.4.1"
rstest = "0.19.0"
rustls-pemfile = { version = "2.1.2" }
serde_test = "1.0.130"
serde_json = "1.0.113"
serde_yaml = "0.9"
socket2 = { version = "0.5.5" }
tokio = { version = "1.37", features = ["rt-multi-thread", "io-util", "net", "test-util"] }
tokio-rustls = { version = "0.26", default-features = false, features = [ "ring", "logging", "tls12" ] }
tokio-test = "0.4"
tokio-tfo = { version = "0.2.0" }
webpki-roots = { version = "0.26" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you make this more consistent between the { version = "..." } and "..." cases?

/// the [ZONEMD].
///
/// For the currently registered values see the [IANA registration]. This
/// type is complete as of 2024-11-29.
Copy link
Contributor

Choose a reason for hiding this comment

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

Reword "complete" to "up-to-date"? "Complete" makes it sound like no more additions are expected.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this was copied from existing domain code, i.e. aimed to be consistent.

Comment on lines +42 to +46
/// Specifies that the SHA-384 algorithm is used.
(SHA384 => 1, "SHA384")

/// Specifies that the SHA-512 algorithm is used.
(SHA512 => 2, "SHA512")
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps the doc comments should be phrased as "Use the SHA-xxx algorithm." That would be consistent with other documentation (e.g. we document what a function does, not what it is).

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this was copied from existing domain code, i.e. aimed to be consistent.

Comment on lines +512 to +537
impl<Octs> Default for Nsec3param<Octs>
where
Octs: From<&'static [u8]>,
{
/// Best practice default values for NSEC3 hashing.
///
/// Per [RFC 9276] section 3.1:
///
/// - _SHA-1, no extra iterations, empty salt._
///
/// Per [RFC 5155] section 4.1.2:
///
/// - _The Opt-Out flag is not used and is set to zero._
/// - _All other flags are reserved for future use, and must be zero._
///
/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html
/// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html
fn default() -> Self {
Self {
hash_algorithm: Nsec3HashAlg::SHA1,
flags: 0,
iterations: 0,
salt: Nsec3Salt::empty(),
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not entirely comfortable with the "best-practice" value being default(). If the best practices for NSEC3 ever change, we'd have to change the default() implementation or (to avoid breaking changes) introduce a new fn new_best_practices() -> Self, and end up with a confusing interface. I'd rather have a fn best_practices() method from the beginning, and no Default impl. This also makes it more clear to end users that they're opting into a best-practices value rather than a default inherent to the type. They're also more likely to find and read the documentation, rather than just calling Default::default() and assuming that will work.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think you make a good point.

My intent was to provide good defaults.

src/validate.rs Outdated
Comment on lines 1763 to 1764
/// The hash provider did not provide a hash for the given owner name.
MissingHash,
Copy link
Contributor

Choose a reason for hiding this comment

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

In what context could such an error occur? Can't the hash provider interface just guarantee that a hash will always be provided?

src/validate.rs Outdated
Comment on lines 1755 to 1758
/// The hashing process produced an invalid owner hash.
///
/// See: [OwnerHashError](crate::rdata::nsec3::OwnerHashError)
OwnerHashError,
Copy link
Contributor

Choose a reason for hiding this comment

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

There isn't a good way for an end user to respond to this error. Can we find a way to guarantee that it can't happen? I'm pretty sure that any SHA-256 or SHA-384 hash will satisfy the requirements of OwnerHash.


pub rrsig_validity_period_strategy: ValidityStrat,

_phantom: PhantomData<(Inner, KeyStrat, Sort)>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Sort doesn't have to be included here, it's already used in DenialConfig.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I tried that and it didn't compile.

Comment on lines 769 to 779
pub fn fixed(ttl: Ttl) -> Self {
Self::Fixed(ttl)
}

pub fn soa() -> Self {
Self::Soa
}

pub fn soa_minimum() -> Self {
Self::SoaMinimum
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do these exist? Self::Fixed, Self::Soa, and Self::SoaMinimum are exactly equivalent.

Copy link
Member Author

Choose a reason for hiding this comment

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

No they are not.

The RFCs don't state how the TTL of the NSEC3PARAM RR should be chosen and different tools do it at least these three different ways.

///
/// [`sign_zone()`]: SignableZone::sign_zone
pub trait SignableZone<N, Octs, Sort>:
Deref<Target = [Record<N, ZoneRecordData<Octs, N>>]>
Copy link
Contributor

Choose a reason for hiding this comment

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

This bound is incredibly restrictive -- it prevents the use of this trait with any zone representation beyond those already provided (e.g. an efficient zone representation where records are stored in a B-tree). In order to be useful, these traits need to be loosened significantly, or should not be expressed as traits at all.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ideally there would be no reference to slices or arrays at all, only iterators. This evolved out of the existing code but isn't a desirable end state. Currently it is whatever compiles and works.

@ximon18
Copy link
Member Author

ximon18 commented Feb 3, 2025

The missing docs are just not written yet.

@ximon18
Copy link
Member Author

ximon18 commented Feb 3, 2025

Re strategy traits, maybe. However, we use them in dnst. They exist at points where there is no one right way to do it, but a default is provided by us.

@ximon18
Copy link
Member Author

ximon18 commented Feb 3, 2025

Re similarly named types, yes it would be good to see how to improve this.

Philip-NLnetLabs and others added 4 commits February 26, 2025 15:30
* The great rename.

* Most of crypto requires ring.

* Introduce unstable-sign and make sign conditional on ring.

* Make cypto depend on unstable-sign for now.

* Get rid of the apex_owner parameter to sign_rrset_in. Take the name from the key.

* Fix tests.

* Remove NsecToNsec3TransitionState, Nsec3ToNsecTransitionState,
TransitioningNsecToNsec3, and TransitioningNsec3ToNsec.

* Remove RrsigValidityPeriodStrategy.

* Fix tests.

* No longer generate and sign the DNSKEY RRset.

* Fix documentation example.

* Introduce crypto::common::DigestContext, move nsec3_hash to dnssec::common.

* Add SHA-256 and use DigestContext in the signature cache.

* Move #[allow(unreachable_code)] to a different place.

* Move DnskeyExt from crypto::validate to dnssec::validator::base.

* Add crypto::common::PublicKey. Move RrsigExt to dnssec::validator::base

* Removed Key because it is mostly redundant.

Move parse_from_bind, format_as_bind, and display_as_bind to dnssec::common.
Move key_size to dnssec::validator::base.

* Remove PublicKeyBytes.

* Bump ring version to 0.17.2 to get AsRef<[u8]> for UnparsedPublicKey.

* Clippy.

* Fmt

* crypto is now independent of dnssec::sign.

* Fix some feature test issues.

* More feature issues.

* Remove hard dependency on ring for DNSSEC signing and validation.

* Fix dependencies for the keyset example.

* Introduce crypto::sign, get rid of crypto::misc.

* Improve documentation for the crypto module.

* Clippy found places where documentation was missing.

* Cleanup documentation for the dnssec module.

* Spell feature flags correctly.

* Improve feature tests.

* Remove Nsec3HashProvider.

* Fix example.

* Rename Default to Rsa.

* Add support for ED448 to from_dnskey.

* Add support for MessageDigest::sha1 and MessageDigest::sha512 to dnskey.

* Use unreachable for some match cases that cannot happen.

* Some simplifications.

* Spelling correction.

* Typo correction.

* Update src/dnssec/sign/denial/config.rs

Co-authored-by: Ximon Eighteen <[email protected]>

* Update Cargo.toml

Co-authored-by: Ximon Eighteen <[email protected]>

* Remove previous unstable-sign feature line.

* Restore stelline.

* Small doc fix.

* remove superfluous map_err.

* Rename DigestContext to DigestBuilder.

* Also rename in doc and test.

* Move doc string.

* Use SecAlg instead of pointers.

* Extract common RSA encoding code.

* Some comments.

* Move tests around.

* [crypto] Make minor changes

Following PR comments (#488).

---------

Co-authored-by: Ximon Eighteen <[email protected]>
Co-authored-by: arya dradjica <[email protected]>
Philip-NLnetLabs added a commit that referenced this pull request Apr 1, 2025
…ng (#416)

---------

Co-authored-by: arya dradjica <[email protected]>
Co-authored-by: Jannik Peters <[email protected]>
Co-authored-by: Martin Hoffmann <[email protected]>
Co-authored-by: Ximon Eighteen <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants