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 generation support. #416

Draft
wants to merge 134 commits into
base: dnssec-key
Choose a base branch
from
Draft

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.

arya dradjica added 30 commits October 2, 2024 13:42
A private key converted into a 'KeyPair' can be exported in the
conventional DNS format.  This is an important step in implementing
'ldns-keygen' using 'domain'.  It is up to the implementation modules
to provide conversion to and from 'KeyPair'; some impls (e.g. for HSMs)
won't support it at all.
'Sign' is a more generic version of 'sign::key::SigningKey' that does
not provide public key information.  It does not try to abstract over
all the functionality of a keypair, since that can depend on the
underlying cryptographic implementation.
There are probably lots of bugs in this implementation, I'll add some
tests soon.
Also fixes 'cargo clippy' issues, particularly with the MSRV.
I'm going to add a corresponding 'PublicKey' type, at which point it
becomes important to differentiate from the generic representations and
actual cryptographic implementations.
Key generation, for now, will only be provided by the OpenSSL backend
(coming soon).  However, generic keys (for RSA/SHA-256 or Ed25519) can
be imported into the Ring backend and used freely.
The OpenSSL backend supports import from and export to generic secret
keys, making the formatting and parsing machinery for them usable.  The
next step is to implement generation of keys.
There were bugs in the Base64 encoding/decoding that are not worth
trying to debug; there's a perfectly usable Base64 implementation in
the crate already.
I had to swap out the RSA key since 'ring' found it to be too small.
- RSA signatures were being made with an unspecified padding scheme.
- ECDSA signatures were being output in ASN.1 DER format, instead of
  the fixed-size format required by DNSSEC (and output by 'ring').
- Tests for signature failures are now added for both backends.
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.

src/sign/ring.rs Outdated Show resolved Hide resolved
Comment on lines 637 to 638
nsec3_recs: Vec<Record<N, Nsec3<Octets>>>,
nsec3param_rec: Record<N, Nsec3param<Octets>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Just name these records and params?

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 was expecting to revisit the design of these APIs. If it remains as such then yes I agree.

src/sign/ring.rs Outdated Show resolved Hide resolved
@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.

src/sign/records.rs Outdated Show resolved Hide resolved
// 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.

// It will NOT construct the last name as that will be dealt
// with in the next outer loop iteration.
// - a.b.c.mail.example.com
for n in (1..distance_to_apex - 1).rev() {
Copy link
Contributor

Choose a reason for hiding this comment

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

If distance_to_apex is 2, then this loop will never execute. Could the outer condition have been distance_to_apex > 2, then? Is there an off-by-one error somewhere?

Copy link
Member Author

@ximon18 ximon18 Oct 31, 2024

Choose a reason for hiding this comment

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

Thanks! I was planning to add proper test cases but for now have extended my test input zone to cover this missing case and realize now why I had some additional logic in there before which I removed because I couldn't see what value it was adding... 😛 Looking at the comments again I see I even left in a description of the additional logic I removed, i.e. tracking the last non-empty non-terminal label.

src/validate.rs Outdated Show resolved Hide resolved
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.

2 participants