Merge branch 'keyset' into keyset-signer

This commit is contained in:
Philip Homburg
2025-09-05 16:20:07 +02:00
25 changed files with 10230 additions and 2617 deletions
+402 -59
View File
@@ -1,83 +1,426 @@
name: ci
on: [push, pull_request]
env:
RUSTFLAGS: "-D warnings"
# ======================================================
# Continuous Integration: making sure the codebase works
# ======================================================
#
# This workflow tests modifications to 'dnst', ensuring that 'dnst' can be
# used by others successfully. It verifies certain aspects of the codebase,
# such as the formatting and feature flag combinations, and runs the full test
# suite. It runs on Ubuntu, Mac OS, and Windows.
#
# Based on https://github.com/NLnetLabs/domain/blob/main/.github/workflows/ci.yml
name: CI
# When the worflow runs
# ---------------------
on:
# Execute when a pull request is (re-) opened or its head changes (e.g. new
# commits are added or the commit history is rewritten) ... but only if
# build-related files change.
pull_request:
paths:
- '**.rs'
- 'Cargo.{toml,lock}'
- '.github/workflows/ci.yml'
# If a pull request is merged, at least one commit is added to the target
# branch. If the target is another pull request, it will be caught by the
# above event. We miss PRs that merge to a non-PR branch, except for the
# 'main' branch.
# Execute when a commit is pushed to 'main' (including merged PRs) or to a
# release tag ... but only if build-related files change.
push:
branches:
- 'main'
- 'releases/**'
paths:
- '**.rs'
- 'Cargo.{toml,lock}'
- '.github/workflows/ci.yml'
# Rebuild 'main' every week. This will account for changes to dependencies
# and to Rust, either of which can trigger new failures. Rust releases are
# every 6 weeks, on a Thursday; this event runs every Friday.
schedule:
- cron: '0 10 * * FRI'
# Jobs
# ----------------------------------------------------------------------------
jobs:
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [1.82.0, stable, beta, nightly]
# Check Formatting
# ----------------
#
# NOTE: This job is run even if no '.rs' files have changed. Inserting such
# a check would require using a separate workflow file or using third-party
# actions. Most commits do change '.rs' files, and 'cargo-fmt' is pretty
# fast, so optimizing this is not necessary.
check-fmt:
name: Check formatting
runs-on: ubuntu-latest
steps:
# Load the repository.
- name: Checkout repository
uses: actions/checkout@v4
# Set up the Rust toolchain.
#
# Disable the cache since it's not relevant for formatting.
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
components: rustfmt
cache: false
# Do the actual formatting check.
- name: Check formatting
run: cargo fmt --all -- --check
# Determine MSRV
# --------------
#
# The MSRV needs to be determined as we will test 'dnst' against the Rust
# compiler at that version.
determine-msrv:
name: Determine MSRV
runs-on: ubuntu-latest
outputs:
msrv: ${{ steps.determine-msrv.outputs.msrv }}
steps:
# Load the repository.
- name: Checkout repository
uses: actions/checkout@v4
# Determine the MSRV.
- name: Determine MSRV
id: determine-msrv
run: |
msrv=`cargo metadata --no-deps --format-version 1 | jq -r '.packages[]|select(.name=="dnst")|.rust_version'`
echo "msrv=$msrv" >> "$GITHUB_OUTPUT"
# Check Feature Flags
# -------------------
#
# Rust does not provide any way to check that all possible feature flag
# combinations will succeed, so we need to try them manually here. We will
# assume this choice is not influenced by the OS or Rust version.
check-feature-flags:
name: Check feature flags
runs-on: ubuntu-latest
steps:
# Load the repository.
- name: Checkout repository
uses: actions/checkout@v4
# Set up the Rust toolchain.
- name: Set up Rust
id: setup-rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: false
- name: Install OpenSSL
run: sudo apt-get install -y libssl-dev
# Restore a cache of dependencies and 'target'.
- name: Restore a dependency cache
id: cache-restore
uses: actions/cache/restore@v4
with:
path: |
~/.cargo
target/
# Cache by OS and Rust version.
key: ${{ runner.os }}-${{ steps.setup-rust.outputs.cachekey }}
# Do the actual feature flag checks.
# (--all-features is done in check-minimal-versions)
#
# NOTE: This does not benefit from the 'target' folder cached by a 'cargo
# check --all-features --all-targets' execution. Due to the minimal
# dependency set, it still runs fairly quickly.
# The empty feature set (as it is done in domain) is not allowed in dnst.
- name: Check default feature set
run: cargo check --all-targets
- name: Check openssl feature
run: cargo check --all-targets --no-default-features -F openssl
- name: Check ring feature
run: cargo check --all-targets --no-default-features -F ring
# Check Minimal Versions
# ----------------------
#
# Ensure that 'dnst' compiles with the oldest compatible versions of all
# packages, even those 'dnst' depends upon indirectly.
check-minimal-versions:
name: Check minimal versions
runs-on: ubuntu-latest
needs: determine-msrv
env:
RUSTFLAGS: "-D warnings"
steps:
# Load the repository.
- name: Checkout repository
uses: actions/checkout@v4
# Set up the Rust toolchain.
- name: Set up Rust nightly
id: setup-rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ needs.determine-msrv.outputs.msrv }},nightly
cache: false
- name: Install OpenSSL
run: sudo apt-get install -y libssl-dev
# TODO: Cache minimal-version dependencies?
# Lock all dependencies to their minimal versions.
- name: Lock dependencies to minimal versions
run: cargo +nightly update -Z minimal-versions
# Check that 'dnst' compiles.
#
# NOTE: This does not benefit from the 'target' folder cached by a 'cargo
# check --all-features --all-targets' execution. It may be worthwhile to
# cache this 'target' folder separately (TODO).
- name: Check
run: cargo check --all-targets --all-features --locked
# Clippy
# ------
#
# We run Clippy separately, and only on nightly Rust because it offers a
# superset of the lints.
#
# 'cargo clippy' and 'cargo build' can share some state for fast execution,
# but it's faster to execute them in parallel than to establish an ordering
# between them.
clippy:
name: Clippy
runs-on: ubuntu-latest
env:
RUSTFLAGS: "-D warnings"
steps:
# Load the repository.
- name: Checkout repository
uses: actions/checkout@v4
# Set up the Rust toolchain.
- name: Set up Rust nightly
id: setup-rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
components: clippy
cache: false
# Restore a cache of dependencies and 'target'.
- name: Restore from cache
id: cache-restore
uses: actions/cache/restore@v4
with:
path: |
~/.cargo
target/
# Cache by OS and Rust version.
key: ${{ runner.os }}-${{ steps.setup-rust.outputs.cachekey }}
# Do the actually Clippy run.
- name: Check Clippy
run: cargo +nightly clippy --all-targets --all-features
# Test
# ----
#
# Ensure that 'dnst' compiles and its test suite passes, on a large number
# of operating systems and Rust versions.
test:
name: Test
needs: determine-msrv
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
rust: ["${{ needs.determine-msrv.outputs.msrv }}", stable, beta, nightly]
runs-on: ${{ matrix.os }}
env:
RUSTFLAGS: "-D warnings"
DNST_FEATURES: "--all-features"
# We use 'vcpkg' to install OpenSSL on Windows.
VCPKG_ROOT: "${{ github.workspace }}\\vcpkg"
VCPKGRS_TRIPLET: x64-windows-release
# Ensure that OpenSSL is dynamically linked.
VCPKGRS_DYNAMIC: 1
steps:
# Load the repository.
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Rust
uses: hecrj/setup-rust-action@v2
with:
rust-version: ${{ matrix.rust }}
- if: matrix.os == 'ubuntu-latest'
run: sudo apt-get install -y libssl-dev
- if: matrix.os == 'windows-latest'
uses: actions/checkout@v4
# Prepare the environment on Windows
- name: Prepare Windows environment
if: matrix.os == 'windows-latest'
id: vcpkg
uses: johnwason/vcpkg-action@v6
uses: johnwason/vcpkg-action@v7
with:
pkgs: openssl
triplet: ${{ env.VCPKGRS_TRIPLET }}
token: ${{ github.token }}
github-binarycache: true
- if: matrix.rust == 'stable'
run: rustup component add clippy
- if: matrix.rust == 'stable'
run: cargo clippy --all-features --all-targets -- -D warnings
- if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest'
run: cargo fmt --all -- --check
- run: cargo check --no-default-features -F ring --all-targets
- run: cargo check --no-default-features -F openssl --all-targets
- run: cargo test --all-features
minimal-versions:
name: Check minimal versions
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Rust
uses: hecrj/setup-rust-action@v2
with:
rust-version: "1.82.0"
- name: Install OpenSSL
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get install -y libssl-dev
- name: Install nightly Rust
run: rustup install nightly
- name: Check with minimal-versions
run: |
cargo +nightly update -Z minimal-versions
cargo check --all-features --all-targets --locked
# Set up the Rust toolchain.
- name: Set up Rust ${{ matrix.rust }}
id: setup-rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ matrix.rust }}
cache: false
# Restore a cache of dependencies and 'target'.
- name: Restore from cache
id: cache-restore
uses: actions/cache/restore@v4
with:
path: |
~/.cargo
target/
# Cache by OS and Rust version.
key: ${{ runner.os }}-${{ steps.setup-rust.outputs.cachekey }}
# Build and run the test suite.
- name: Test
run: cargo test --all-targets $DNST_FEATURES
# Additional (ignored) Tests
# --------------------------
#
# Ensure that the extended test suit passes.
# (only on ubuntu-latest and rust stable)
extra-tests:
name: Extra tests
runs-on: ubuntu-latest
env:
RUSTFLAGS: "-D warnings"
DNST_FEATURES: "--all-features"
steps:
# Load the repository.
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Rust
uses: hecrj/setup-rust-action@v2
- name: Install supporting tools/libraries
run: |
# The tests compare their output to that of LDNS tools, so we need to
# install them. Some tests work with DNSSEC keys for which the OpenSSL
# library must be compiled against which requires C build programs and
# pkg-config. Install everything we need.
sudo apt-get update
sudo apt-get install -y build-essential ldnsutils libssl-dev pkg-config
- name: Run tests that are normally ignored
run: cargo test --all-features --all-targets -- --ignored
uses: actions/checkout@v4
- name: Install supporting tools and libraries
# The tests compare their output to that of LDNS tools, so we need to
# install them. Some tests work with DNSSEC keys for which the OpenSSL
# library must be compiled against which requires C build programs and
# pkg-config. Install everything we need.
run: sudo apt-get install -y build-essential ldnsutils libssl-dev pkg-config
# Set up the Rust toolchain.
- name: Set up Rust stable
id: setup-rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: false
# Restore a cache of dependencies and 'target'.
- name: Restore from cache
id: cache-restore
uses: actions/cache/restore@v4
with:
path: |
~/.cargo
target/
# Cache by OS and Rust version.
key: ${{ runner.os }}-${{ steps.setup-rust.outputs.cachekey }}
# Build and run the test suite.
- name: Test ignored
run: cargo test --all-targets $DNST_FEATURES -- --ignored
# Build Cache
# -----------
#
# Prepare a cache for checking and building 'dnst', on 'main'.
cache:
name: Cache
needs: determine-msrv
strategy:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
rust: ["${{ needs.determine-msrv.outputs.msrv }}", stable, beta, nightly]
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/main'
env:
RUSTFLAGS: "-D warnings"
DNST_FEATURES: "--all-features"
# We use 'vcpkg' to install OpenSSL on Windows.
VCPKG_ROOT: "${{ github.workspace }}\\vcpkg"
VCPKGRS_TRIPLET: x64-windows-release
# Ensure that OpenSSL is dynamically linked.
VCPKGRS_DYNAMIC: 1
steps:
# Load the repository.
- name: Checkout repository
uses: actions/checkout@v4
# Prepare the environment on Windows
- name: Prepare Windows environment
if: matrix.os == 'windows-latest'
id: vcpkg
uses: johnwason/vcpkg-action@v7
with:
pkgs: openssl
triplet: ${{ env.VCPKGRS_TRIPLET }}
token: ${{ github.token }}
- name: Install OpenSSL
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get install -y libssl-dev
# Set up the Rust toolchain.
- name: Set up Rust ${{ matrix.rust }}
id: setup-rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ matrix.rust }}
cache: false
# Restore a cache of dependencies and 'target'.
- name: Restore from cache
uses: actions/cache/restore@v4
with:
path: |
~/.cargo
target/
# Cache by OS and Rust version.
key: ${{ runner.os }}-${{ steps.setup-rust.outputs.cachekey }}
# Build all of 'dnst'.
- name: Build
run: cargo build --all-targets $DNST_FEATURES
# Save to the cache.
- name: Save to the cache
uses: actions/cache/save@v4
with:
path: |
~/.cargo
target/
# Cache by OS and Rust version.
key: ${{ runner.os }}-${{ steps.setup-rust.outputs.cachekey }}
+2 -2
View File
@@ -12,7 +12,7 @@ on:
jobs:
package:
uses: NLnetLabs/ploutos/.github/workflows/pkg-rust.yml@v7
uses: NLnetLabs/ploutos/.github/workflows/pkg-rust.yml@v8
# secrets:
# DOCKER_HUB_ID: ${{ secrets.DOCKER_HUB_ID }}
# DOCKER_HUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
@@ -30,4 +30,4 @@ jobs:
deb_extra_build_packages: libssl-dev
rpm_extra_build_packages: make openssl-devel
rpm_scriptlets_path: pkg/rpm/scriptlets.toml
rpm_scriptlets_path: pkg/rpm/scriptlets.toml
Generated
+488 -166
View File
File diff suppressed because it is too large Load Diff
+15 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "dnst"
version = "0.1.1-dev"
version = "0.1.0-rc2"
edition = "2021"
default-run = "dnst"
readme = "README.md"
@@ -17,17 +17,22 @@ name = "ldns"
path = "src/bin/ldns.rs"
[features]
default = ["openssl", "ring"]
default = ["kmip", "openssl", "ring"]
# Cryptographic backends
kmip = ["domain/kmip", "dep:indenter", "dep:rand"]
openssl = ["domain/openssl"]
ring = ["domain/ring"]
# For building in a cargo cross container that lacks openssl-dev so cannot
# successfully compile the Rust OpenSSL crate.
static-openssl = ["openssl/vendored"]
[dependencies]
bytes = "1.8.0"
chrono = "0.4.38"
clap = { version = "4.3.4", features = ["cargo", "derive"] }
domain = { git = "https://github.com/NLnetLabs/domain.git", branch = "crypto-and-keyset-fixes", features = [
clap = { version = "4.3.4", features = ["cargo", "derive", "wrap_help"] }
domain = { git = "https://github.com/NLnetLabs/domain.git", branch = "patches-for-nameshed-prototype", features = [
"bytes",
"net",
"resolv",
@@ -36,13 +41,16 @@ domain = { git = "https://github.com/NLnetLabs/domain.git", branch = "crypto-and
"unstable-client-transport",
"unstable-sign",
"unstable-validator",
"unstable-zonetree",
"unstable-zonetree"
] }
indenter = { version = "0.3.4", optional = true }
lexopt = "0.3.0"
rayon = "1.10.0"
octseq = "0.5.2"
rand = { version = "0.9.2", optional = true }
ring = "0.17.8"
tokio = "1.40.0"
openssl = { version = "*", features = ["vendored"], optional = true }
# LDNS-xxx mode specific dependencies.
# TODO: put these behind a feature gate?
@@ -58,13 +66,14 @@ smallvec = "1.13.2"
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
url = "2.5.4"
futures = "0.3.31"
[dev-dependencies]
const_format = " 0.2.33"
test_bin = "0.4.0"
tempfile = "3.20.0"
regex = "1.11.1"
domain = { git = "https://github.com/NLnetLabs/domain.git", branch = "crypto-and-keyset-fixes", features = [
domain = { git = "https://github.com/NLnetLabs/domain.git", branch = "patches-for-nameshed-prototype", features = [
"unstable-stelline",
] }
pretty_assertions = "1.4.1"
-4
View File
@@ -46,7 +46,3 @@ Options
Print the help text (short summary with ``-h``, long help with
``--help``).
.. option:: -V, --version
Print the version.
@@ -40,7 +40,3 @@ Options
Print the help text (short summary with ``-h``, long help with
``--help``).
.. option:: -V, --version
Print the version.
-4
View File
@@ -140,10 +140,6 @@ NSEC3 options
The following options can be used with ``-n`` to override the default NSEC3
settings used.
.. option:: -a <ALGORITHM NUMBER OR MNEMONIC>
Specify the hashing algorithm. Defaults to SHA-1.
.. option:: -s <STRING>
Specify the salt as a hex string. Defaults to ``-``, meaning empty salt.
+127 -25
View File
@@ -4,50 +4,152 @@ dnst update
Synopsis
--------
:program:`dnst update` ``<DOMAIN NAME>`` ``[ZONE]`` ``<IP>``
``[<TSIG KEY NAME> <TSIG ALGORITHM> <TSIG KEY DATA>]``
:program:`dnst update` ``[OPTIONS]`` ``<DOMAIN NAME>`` ``<COMMAND>``
:program:`dnst update` ``[OPTIONS]`` ``<DOMAIN NAME>`` :subcmd:`add` ``<RRTYPE>`` ``[RDATA]...``
:program:`dnst update` ``[OPTIONS]`` ``<DOMAIN NAME>`` :subcmd:`delete` ``<RRTYPE>`` ``[RDATA]...``
:program:`dnst update` ``[OPTIONS]`` ``<DOMAIN NAME>`` :subcmd:`clear` ``<RRTYPE>``
Description
-----------
**dnst update** sends an RFC 2136 Dynamic Update message to the name servers
for a zone to update an IP address (or delete all existing IP addresses) for a
domain name.
for a zone to add, update, or delete arbitrary Resource Records for a domain
name.
The message to be sent can be optionally authenticated using a given TSIG key.
**dnst update [...] add** adds the given RRs to the domain.
**dnst update [...] delete** deletes the given RRs from the domain. It can be
used to delete individual RRs or a whole RRset.
**dnst update [...] clear** clears (deletes) all RRs of any type from the
domain name.
Arguments
---------
.. option:: <DOMAIN NAME>
The domain name to update the IP address of.
The domain name of the RR(s) to update.
.. option:: <ZONE>
.. option:: <COMMAND>
The zone to send the update to (if omitted, derived from SOA record).
.. option:: <IP>
The IP address to update the domain with (``none`` to remove any
existing IP addresses)
.. option:: <TSIG KEY NAME>
TSIG key name.
.. option:: <TSIG ALGORITHM>
TSIG algorithm (e.g. "hmac-sha256").
.. option:: <TSIG KEY DATA>
Base64 encoded TSIG key data.
Which action to take: add, delete, or clear.
Options:
--------
.. option:: -c, --class <CLASS>
Class
Defaults to IN.
.. option:: -t, --ttl <TTL>
TTL in seconds or with unit suffix (s, m, h, d).
Is only used by the :subcmd:`add` command and is otherwise ignored.
Defaults to 3600.
.. option:: -s, --server <IP>
The name server to send the update to.
By default, the update will be sent to the list of name servers fetched
from the zone's NS RRset as per RFC 2136.
.. option:: -z, --zone <ZONE>
The zone the domain name belongs to (to skip a SOA query)
.. option:: -y, --tsig <NAME:KEY[:ALGO]>
TSIG credentials for the UPDATE packet
.. option:: --rrset-exists <DOMAIN_NAME_AND_TYPE>
Require that at least one RR with the given NAME and TYPE exists.
This option can be provided multiple times, with format ``<DOMAIN_NAME>
<TYPE>`` each, to build up a list of RRs.
If the domain name is relative, it will be relative to the zone's apex.
[aliases: --rrset]
.. option:: --rrset-exists-exact <RESOURCE_RECORD>
Require that an RRset exists and contains exactly the RRs with the given
NAME, TYPE, and RDATA. This option can be provided multiple times, each
with one RR in zonefile format, to build up one or more RRsets that is
required to exist. CLASS and TTL can be omitted.
If the domain name is relative, it will be relative to the zone's apex.
[aliases: --rrset-exact]
.. option:: --rrset-non-existent <DOMAIN_NAME_AND_TYPE>
RRset does not exist. This option can be provided multiple times, with
format ``<DOMAIN_NAME> <TYPE>`` each, to build up a list of RRs that
specify that no RRs with a specified NAME and TYPE can exist.
If the domain name is relative, it will be relative to the zone's apex.
[aliases: --rrset-empty]
.. option:: --name-in-use <DOMAIN_NAME>
Name is in use. This option can be provided multiple times, with format
``<DOMAIN_NAME>`` each, to collect a list of NAMEs that must own at least
one RR.
Note that this prerequisite is NOT satisfied by empty nonterminals.
If the domain name is relative, it will be relative to the zone's apex.
[aliases: --name-used]
.. option:: --name-not-in-use <DOMAIN_NAME>
Name is not in use. This option can be provided multiple times, with
format ``<DOMAIN_NAME>`` each, to collect a list of NAMEs that must NOT
own any RRs.
Note that this prerequisite IS satisfied by empty nonterminals.
If the domain name is relative, it will be relative to the zone's apex.
[aliases: --name-unused]
.. option:: -h, --help
Print the help text (short summary with ``-h``, long help with
``--help``).
``--help``). Can also be used on the individual sub commands.
Arguments for :subcmd:`add` and :subcmd:`delete`
------------------------------------------------------
.. option:: <RRTYPE>
The RR type to add or delete.
.. option:: [RDATA]...
One or more RDATA arguments for :subcmd:`add`, and zero or more for
:subcmd:`delete`.
Each argument corresponds to a single RR's RDATA, so beware of (shell and
DNS) quoting rules.
Each RDATA argument will be parsed as if it was read from a zone file.
| Examples:
| :code:`dnst update some.example.com add AAAA ::1 2001:db8::`
| :code:`dnst update some.example.com add TXT '"Spacious String" "Another
string for the same TXT record"' '"This is another TXT RR"'`
+19 -2
View File
@@ -17,8 +17,25 @@ managing DNS servers and DNS zones.
Please consult the manual pages for these individual commands for more
information.
dnst Commands
-------------
Options
-------
.. option:: -v, --verbosity <LEVEL>
Set the verbosity to 0-5 or a level name (``off``, ``error``, ``warn``,
``info``, ``debug`` or ``trace``). Defaults to ``warn``.
.. option:: -h, --help
Print the help text (short summary with ``-h``, long help with
``--help``).
.. option:: -V, --version
Print the version.
Commands
--------
.. glossary::
+4 -2
View File
@@ -10,8 +10,10 @@ image:
- "debian:buster" # debian/10
- "debian:bullseye" # debian/11
- "debian:bookworm" # debian/12
- 'rockylinux:8' # compatible with EOL centos:8
- 'rockylinux:9'
- "debian:trixie" # debian/13
- 'almalinux:8' # compatible with EOL centos:8
- 'almalinux:9'
- 'almalinux:10'
target:
- 'x86_64'
include:
+19 -5
View File
@@ -6,9 +6,11 @@ pkg:
image:
- "ubuntu:focal" # ubuntu/20.04
- "ubuntu:jammy" # ubuntu/22.04
- "ubuntu:noble" # ubuntu/24.04
- "debian:buster" # debian/10
- "debian:bullseye" # debian/11
- "debian:bookworm" # debian/12
- "debian:trixie" # debian/13
published_pkg:
- 'ldnsutils' # correct for Ubuntu/Debian
target:
@@ -26,24 +28,36 @@ test-mode:
- 'upgrade-from-published'
include:
- pkg: 'dnst'
image: 'rockylinux:8'
image: 'almalinux:8'
target: 'x86_64'
test-mode: 'fresh-install'
- pkg: 'dnst'
image: 'rockylinux:8'
image: 'almalinux:8'
target: 'x86_64'
test-mode: 'upgrade-from-published'
published_pkg: 'ldns-utils'
rpm_yum_extra_args: --enablerepo powertools
- pkg: 'dnst'
image: 'rockylinux:9'
image: 'almalinux:9'
target: 'x86_64'
test-mode: 'fresh-install'
- pkg: 'dnst'
image: 'rockylinux:9'
image: 'almalinux:9'
target: 'x86_64'
test-mode: 'upgrade-from-published'
published_pkg: 'ldns-utils'
rpm_yum_extra_args: --enablerepo crb
- pkg: 'dnst'
image: 'almalinux:10'
target: 'x86_64'
test-mode: 'fresh-install'
- pkg: 'dnst'
image: 'almalinux:10'
target: 'x86_64'
test-mode: 'upgrade-from-published'
published_pkg: 'ldns-utils'
@@ -57,4 +71,4 @@ include:
# mode: 'upgrade-from-published'
# - pkg: 'routinator'
# image: 'debian:bookworm'
# mode: 'upgrade-from-published'
# mode: 'upgrade-from-published'
+14 -1
View File
@@ -4,12 +4,22 @@ use super::commands::Command;
use super::error::Error;
use clap::Parser;
use tracing::level_filters::LevelFilter;
#[derive(Clone, Debug, Parser)]
#[command(version, disable_help_subcommand = true)]
pub struct Args {
#[command(subcommand)]
pub command: Command,
/// Verbosity: 0-5 or a level name ("off", "error", "warn", "info", "debug" or "trace")
#[arg(
short = 'v',
long = "verbosity",
value_name = "level",
default_value_t = LevelFilter::from_level(tracing::Level::WARN),
)]
pub verbosity: LevelFilter,
}
impl Args {
@@ -20,6 +30,9 @@ impl Args {
impl From<Command> for Args {
fn from(value: Command) -> Self {
Args { command: value }
Args {
command: value,
verbosity: LevelFilter::from_level(tracing::Level::WARN),
}
}
}
+25 -9
View File
@@ -6,21 +6,37 @@
use std::process::ExitCode;
use dnst::env::Env;
use dnst::log::LogFormatter;
use dnst::try_ldns_compatibility;
use tracing::level_filters::LevelFilter;
fn main() -> ExitCode {
let env = dnst::env::RealEnv;
run(env)
}
fn run(env: impl Env) -> ExitCode {
let mut args = env.args_os();
let argv0 = args.next().unwrap();
let stderr = env.stderr();
let subscriber = tracing_subscriber::FmtSubscriber::builder()
.with_ansi(stderr.is_terminal())
.with_writer(stderr)
.with_max_level(LevelFilter::WARN)
.event_format(LogFormatter {
program: argv0.to_string_lossy().to_string(),
});
let mut args = std::env::args_os();
args.next().unwrap();
let args =
try_ldns_compatibility(args).map(|args| args.expect("ldns commmand lacks ldns- prefix"));
match args.and_then(|args| args.execute(&env)) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
err.pretty_print(env);
ExitCode::FAILURE
tracing::subscriber::with_default(subscriber.finish(), || {
match args.and_then(|args| args.execute(&env)) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
err.pretty_print(env);
ExitCode::FAILURE
}
}
}
})
}
+122 -20
View File
@@ -1,4 +1,5 @@
use std::ffi::OsString;
use std::fmt::Display;
use std::fs::File;
use std::io::{self, Write as _};
use std::path::PathBuf;
@@ -6,8 +7,8 @@ use std::path::PathBuf;
use clap::builder::ValueParser;
use clap::Parser;
use domain::base::iana::{DigestAlgorithm, SecurityAlgorithm};
use domain::base::zonefile_fmt::{DisplayKind, ZonefileFmt};
use domain::base::Record;
use domain::base::zonefile_fmt::ZonefileFmt;
use domain::base::{Record, RecordData, ToName};
use domain::dnssec::validator::base::DnskeyExt;
use domain::rdata::Ds;
use domain::zonefile::inplace::{Entry, ScannedRecordData};
@@ -15,12 +16,11 @@ use lexopt::Arg;
use crate::env::Env;
use crate::error::Error;
use crate::Args;
use crate::{Args, DISPLAY_KIND};
use super::{Command, LdnsCommand};
#[derive(Clone, Debug, Parser, PartialEq, Eq)]
#[command(version)]
pub struct Key2ds {
/// ignore SEP flag (i.e. make DS records for any key)
#[arg(long = "ignore-sep")]
@@ -45,6 +45,13 @@ pub struct Key2ds {
/// Keyfile to read
#[arg()]
keyfile: PathBuf,
// -----------------------------------------------------------------------
// Non-command line argument fields:
// -----------------------------------------------------------------------
/// Whether or not we were invoked as `ldns-key2ds`.
#[arg(skip = false)]
invoked_as_ldns: bool,
}
pub fn parse_digest_alg(arg: &str) -> Result<DigestAlgorithm, Error> {
@@ -103,7 +110,6 @@ impl LdnsCommand for Key2ds {
}
keyfile = Some(val);
}
Arg::Short('v') => return Ok(Self::report_version()),
Arg::Short(x) => return Err(format!("Invalid short option: -{x}").into()),
Arg::Long(x) => {
return Err(format!("Long options are not supported, but `--{x}` given").into())
@@ -123,6 +129,7 @@ impl LdnsCommand for Key2ds {
// present in the ldns version of this command.
force_overwrite: true,
keyfile: keyfile.into(),
invoked_as_ldns: true,
})))
}
}
@@ -166,9 +173,13 @@ impl Key2ds {
let key_tag = dnskey.key_tag();
let sec_alg = dnskey.algorithm();
let digest_alg = self
.algorithm
.unwrap_or_else(|| determine_hash_from_sec_alg(sec_alg));
let digest_alg = if let Some(alg) = self.algorithm {
alg
} else if self.invoked_as_ldns {
determine_hash_from_sec_alg(sec_alg).unwrap_or(DigestAlgorithm::SHA1)
} else {
determine_hash_from_sec_alg(sec_alg)?
};
if digest_alg == DigestAlgorithm::GOST {
return Err("Error: the GOST algorithm is deprecated and must not be used. Try a different algorithm.".into());
@@ -185,7 +196,11 @@ impl Key2ds {
let rr = Record::new(owner, class, ttl, ds);
if self.write_to_stdout {
writeln!(env.stdout(), "{}", rr.display_zonefile(DisplayKind::Simple));
if self.invoked_as_ldns {
let _ = write_rr_as_ldns(&env.stdout(), &rr);
} else {
writeln!(env.stdout(), "{}", rr.display_zonefile(DISPLAY_KIND));
}
} else {
let owner = owner.fmt_with_dot();
let sec_alg = sec_alg.to_int();
@@ -215,8 +230,13 @@ impl Key2ds {
let mut out_file =
res.map_err(|e| format!("Could not create file \"{filename}\": {e}"))?;
writeln!(out_file, "{}", rr.display_zonefile(DisplayKind::Simple))
.map_err(|e| format!("Could not write to file \"{filename}\": {e}"))?;
if self.invoked_as_ldns {
write_rr_as_ldns(out_file, &rr)
.map_err(|e| format!("Could not write to file \"{filename}\": {e}"))?;
} else {
writeln!(out_file, "{}", rr.display_zonefile(DISPLAY_KIND))
.map_err(|e| format!("Could not write to file \"{filename}\": {e}"))?;
}
writeln!(env.stdout(), "{keyname}");
}
@@ -226,19 +246,38 @@ impl Key2ds {
}
}
fn determine_hash_from_sec_alg(sec_alg: SecurityAlgorithm) -> DigestAlgorithm {
fn determine_hash_from_sec_alg(sec_alg: SecurityAlgorithm) -> Result<DigestAlgorithm, Error> {
match sec_alg {
SecurityAlgorithm::RSASHA256
| SecurityAlgorithm::RSASHA512
| SecurityAlgorithm::ED25519
| SecurityAlgorithm::ED448
| SecurityAlgorithm::ECDSAP256SHA256 => DigestAlgorithm::SHA256,
SecurityAlgorithm::ECDSAP384SHA384 => DigestAlgorithm::SHA384,
SecurityAlgorithm::ECC_GOST => DigestAlgorithm::GOST,
_ => DigestAlgorithm::SHA1,
| SecurityAlgorithm::ECDSAP256SHA256 => Ok(DigestAlgorithm::SHA256),
SecurityAlgorithm::ECDSAP384SHA384 => Ok(DigestAlgorithm::SHA384),
SecurityAlgorithm::ECC_GOST => Ok(DigestAlgorithm::GOST),
_ => Err(concat!(
"Unable to derive digest algorithm from used key algorithm. ",
"Please select a digest algorithm using --algorithm."
)
.into()),
}
}
fn write_rr_as_ldns<W: io::Write, N: ToName, D: RecordData + Display>(
mut w: W,
rr: &Record<N, D>,
) -> Result<(), io::Error> {
writeln!(
w,
"{}\t{}\t{}\t{}\t{}",
rr.owner().fmt_with_dot(),
rr.ttl().as_secs(),
rr.class(),
rr.rtype(),
rr.data()
)
}
#[cfg(test)]
mod test {
use domain::base::iana::DigestAlgorithm;
@@ -276,6 +315,7 @@ mod test {
force_overwrite: false,
algorithm: None,
keyfile: PathBuf::from("keyfile1.key"),
invoked_as_ldns: false,
};
// Check the defaults
@@ -365,6 +405,7 @@ mod test {
force_overwrite: true, // note that this is true
algorithm: None,
keyfile: PathBuf::from("keyfile1.key"),
invoked_as_ldns: true,
};
// Check the defaults
@@ -426,6 +467,11 @@ mod test {
",
)
.unwrap();
let mut file = File::create(dir.path().join("key3.key")).unwrap();
// DSA Key for fallback testing
file
.write_all(b". IN DNSKEY 256 3 3 CJ7RxFssAV8F41ftNWyGNW6eo49FhiDQpuR1Nop1dIzcFVD15Do76z1mZaLoheVttGMtE3jah0gFThjyUeBI65pmofV1pALXzrfbyDsoKJM2cjdaqICr9Zg/OcQ4yqkkJpTt1+e+w8xv6YrBOrvSwYwOIUGkj2ci/84HXC90W8I8ph6MHk1vaYmGoOS9wM+S9x2RO6sxD/UtB4QUGFp1qDlFc+C4dc1kB+rs868C004mRGoHe+PHHeDXdfKwr5zVpdgPc85TkBJAY82XYfzIE7rUpXFI4JzRE1Lz1Mu7rXZnwNve6bVykghJ8uOAaDO1KFLdIgThqJh1N8XMSinNKhPwU2qPMX7dsC5hh82BHg04fc3O2hzUG3A1z49i6Hbqa+zgvJe70AQZdqiFSaOpfgC6a8wRfdyGSTYBxsi2YhG2G1/x6qr8sToEeZjq9awPRY+bXT8kJijg9AH9/adrjvF77m+NJASepFTre40I79+Ymo3fnjFU1oSnsGTB91tQbbSNV55fd5Jsqndj9AJkhYJUpbVN ;{id = 15147 (zsk), size = 1024b}\n")
.unwrap();
dir
}
@@ -441,7 +487,7 @@ mod test {
assert_eq!(res.stderr, "");
let out = std::fs::read_to_string(dir.path().join("Kexample.test.+015+60136.ds")).unwrap();
assert_eq!(out, "example.test. 3600 IN DS 60136 15 2 52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n");
assert_eq!(out, "example.test.\t3600\tIN\tDS\t60136 15 2 52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n");
}
#[test]
@@ -455,10 +501,10 @@ mod test {
assert_eq!(res.stderr, "");
let out = std::fs::read_to_string(dir.path().join("Kone.test.+015+38429.ds")).unwrap();
assert_eq!(out, "one.test. 3600 IN DS 38429 15 2 B85F7D27C48A7B84D633C7A41C3022EA0F7FC80896227B61AE7BFC59BF5F0256\n");
assert_eq!(out, "one.test.\t3600\tIN\tDS\t38429 15 2 B85F7D27C48A7B84D633C7A41C3022EA0F7FC80896227B61AE7BFC59BF5F0256\n");
let out = std::fs::read_to_string(dir.path().join("Ktwo.test.+015+00425.ds")).unwrap();
assert_eq!(out, "two.test. 3600 IN DS 425 15 2 AA2030287A7C5C56CB3C0E9C64BE55616729C0C78DE2B83613D03B10C0F1EA93\n");
assert_eq!(out, "two.test.\t3600\tIN\tDS\t425 15 2 AA2030287A7C5C56CB3C0E9C64BE55616729C0C78DE2B83613D03B10C0F1EA93\n");
}
#[test]
@@ -472,7 +518,7 @@ mod test {
assert_eq!(res.exit_code, 0);
assert_eq!(
res.stdout,
"example.test. 3600 IN DS 60136 15 2 52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n"
"example.test.\t3600\tIN\tDS\t60136 15 2 52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n"
);
assert_eq!(res.stderr, "");
}
@@ -500,4 +546,60 @@ mod test {
assert_eq!(res.stdout, "Kexample.test.+015+60136\n");
assert_eq!(res.stderr, "");
}
#[test]
fn ldns_lowercase_digest() {
let dir = run_setup();
let res = FakeCmd::new(["ldns-key2ds", "-n", "key1.key"])
.cwd(&dir)
.run();
assert_eq!(res.exit_code, 0);
assert_eq!(
res.stdout,
"example.test.\t3600\tIN\tDS\t60136 15 2 52bd3bf40c8220bf1a3e2a3751c423bc4b69bcd7f328d38c4cd021a85de65ad4\n"
);
assert_eq!(res.stderr, "");
let res = FakeCmd::new(["ldns-key2ds", "key1.key"]).cwd(&dir).run();
assert_eq!(res.exit_code, 0, "{res:?}");
assert_eq!(res.stdout, "Kexample.test.+015+60136\n");
assert_eq!(res.stderr, "");
let out = std::fs::read_to_string(dir.path().join("Kexample.test.+015+60136.ds")).unwrap();
assert_eq!(out, "example.test.\t3600\tIN\tDS\t60136 15 2 52bd3bf40c8220bf1a3e2a3751c423bc4b69bcd7f328d38c4cd021a85de65ad4\n");
}
#[test]
fn ldns_algorithm_fallback() {
let dir = run_setup();
let res = FakeCmd::new(["ldns-key2ds", "-nf", "key3.key"])
.cwd(&dir)
.run();
assert_eq!(res.exit_code, 0);
assert_eq!(
res.stdout,
".\t3600\tIN\tDS\t15147 3 1 dbccc2cc359d4661ab39c72898ef58e9cdcd27ab\n"
);
assert_eq!(res.stderr, "");
}
#[test]
fn dnst_algorithm_fallback_err() {
let dir = run_setup();
let res = FakeCmd::new(["dnst", "key2ds", "-n", "--ignore-sep", "key3.key"])
.cwd(&dir)
.run();
assert_eq!(res.exit_code, 1);
assert_eq!(res.stdout, "");
assert!(res.stderr.contains(
"Unable to derive digest algorithm from used key algorithm. Please select a digest algorithm using --algorithm."
));
}
}
+29 -11
View File
@@ -16,7 +16,7 @@ use lexopt::Arg;
use crate::env::Env;
use crate::error::{Context, Error};
use crate::parse::parse_name;
use crate::{util, Args};
use crate::{util, Args, DISPLAY_KIND};
use super::{parse_os, parse_os_with, Command, LdnsCommand};
@@ -36,6 +36,8 @@ pub struct Keygen {
feature = "openssl",
doc = " - ED448: An Ed448 key (algorithm 16)"
)]
///
/// Tip: Using the algorithm number instead of the name is also supported.
#[allow(rustdoc::invalid_html_tags)]
#[arg(
short = 'a',
@@ -268,11 +270,11 @@ impl From<Keygen> for Command {
impl Keygen {
fn parse_algorithm(value: &str) -> Result<GenerateParams, clap::Error> {
match value {
"RSASHA256" => return Ok(GenerateParams::RsaSha256 { bits: 2048 }),
"ECDSAP256SHA256" => return Ok(GenerateParams::EcdsaP256Sha256),
"ECDSAP384SHA384" => return Ok(GenerateParams::EcdsaP384Sha384),
"ED25519" => return Ok(GenerateParams::Ed25519),
"ED448" => return Ok(GenerateParams::Ed448),
"RSASHA256" | "8" => return Ok(GenerateParams::RsaSha256 { bits: 2048 }),
"ECDSAP256SHA256" | "13" => return Ok(GenerateParams::EcdsaP256Sha256),
"ECDSAP384SHA384" | "14" => return Ok(GenerateParams::EcdsaP384Sha384),
"ED25519" | "15" => return Ok(GenerateParams::Ed25519),
"ED448" | "16" => return Ok(GenerateParams::Ed448),
_ => {}
}
@@ -281,7 +283,7 @@ impl Keygen {
if let Some((name, params)) = value.split_once(':') {
#[allow(clippy::single_match)]
match name {
"RSASHA256" => {
"RSASHA256" | "8" => {
let bits: u32 = params.parse().map_err(|err| {
clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
@@ -357,7 +359,7 @@ impl Keygen {
let algorithm = public_key.algorithm();
let secret_key = secret_key.display_as_bind().to_string();
let public_key = format!(
"{} IN DNSKEY {}",
"{} IN DNSKEY {}\n",
self.name.fmt_with_dot(),
public_key.display_zonefile(DisplayKind::Simple)
);
@@ -367,7 +369,7 @@ impl Keygen {
self.name.fmt_with_dot(),
Ds::new(key_tag, algorithm, digest_alg, digest)
.expect("we generated the digest, so don't expect it to be too long")
.display_zonefile(DisplayKind::Simple)
.display_zonefile(DISPLAY_KIND)
)
});
@@ -475,6 +477,22 @@ mod test {
..base.clone()
}
);
// - Specifying the algorithm by number
assert_eq!(
parse(cmd.args(["-a", "8", "example.org"])),
Keygen {
algorithm: GenerateParams::RsaSha256 { bits: 2048 },
..base.clone()
}
);
// - Specifying the algorithm by number incl. keysize
assert_eq!(
parse(cmd.args(["-a", "8:1024", "example.org"])),
Keygen {
algorithm: GenerateParams::RsaSha256 { bits: 1024 },
..base.clone()
}
);
// Test 'make_ksk':
assert_eq!(
@@ -612,7 +630,7 @@ mod test {
let name_regex = Regex::new(r"^Kexample\.org\.\+015\+[0-9]{5}$").unwrap();
let public_key_regex =
Regex::new(r"^example.org. IN DNSKEY 256 3 15 [A-Za-z0-9/+=]+").unwrap();
Regex::new(r"^example.org. IN DNSKEY 256 3 15 [A-Za-z0-9/+=]+\n$").unwrap();
let secret_key_regex = Regex::new(
r"^Private-key-format: v1\.2\nAlgorithm: 15 \(ED25519\)\nPrivateKey: [A-Za-z0-9/+=]+\n$",
)
@@ -644,7 +662,7 @@ mod test {
let name_regex = Regex::new(r"^Kexample\.org\.\+015\+[0-9]{5}$").unwrap();
let public_key_regex =
Regex::new(r"^example.org. IN DNSKEY 257 3 15 [A-Za-z0-9/+=]+").unwrap();
Regex::new(r"^example.org. IN DNSKEY 257 3 15 [A-Za-z0-9/+=]+\n$").unwrap();
let digest_key_regex =
Regex::new(r"^example.org. IN DS [0-9]+ 15 2 [0-9a-fA-F]+\n$").unwrap();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
pub mod cmd;
#[cfg(feature = "kmip")]
pub mod kmip;
pub use cmd::*;
+13 -8
View File
@@ -48,6 +48,10 @@ pub enum Command {
#[command(name = "keygen", verbatim_doc_comment)]
Keygen(self::keygen::Keygen),
/// Maintain a set of DNSSEC keys. EXPERIMENTAL.
#[command(name = "keyset")]
Keyset(self::keyset::Keyset),
/// Generate a DS RR from the DNSKEYS in keyfile
///
/// The following file will be created for each key:
@@ -80,12 +84,12 @@ pub enum Command {
#[command(name = "update")]
Update(self::update::Update),
/// Maintain a set of DNSSEC keys
#[command(name = "keyset")]
Keyset(self::keyset::Keyset),
/// Show the manual pages
Help(self::help::Help),
/// Send an UPDATE packet ldns compatibility variant
///
/// This variant is not a dnst command and only used to provide
/// a separate implementation from the dnst update command.
#[command(skip)]
LdnsUpdate(self::update::LdnsUpdate),
/// Report a string to stdout
///
@@ -100,13 +104,14 @@ impl Command {
match self {
Self::Key2ds(key2ds) => key2ds.execute(env),
Self::Keygen(keygen) => keygen.execute(env),
Self::Keyset(keyset) => keyset.execute(env),
Self::Nsec3Hash(nsec3hash) => nsec3hash.execute(env),
Self::Notify(notify) => notify.execute(env),
Self::Signer(signer) => signer.execute(env),
Self::SignZone(signzone) => signzone.execute(env),
Self::Update(update) => update.execute(env),
Self::Keyset(keyset) => keyset.execute(env),
Self::Help(help) => help.execute(),
// Self::Help(help) => help.execute(env),
Self::LdnsUpdate(ldnsupdate) => ldnsupdate.execute(env),
Self::Report(s) => {
writeln!(env.stdout(), "{s}");
Ok(())
+131 -39
View File
@@ -99,7 +99,7 @@ pub struct SignZone {
///
/// Using this flag enables -O and -R automatically.
#[arg(
help_heading = Some("OUTPUT FORMATTING"),
help_heading = Some("Output Formatting"),
short = 'b',
default_value_t = false
)]
@@ -186,14 +186,7 @@ pub struct SignZone {
allow_zonemd_without_signing: bool,
/// Hashing algorithm.
#[arg(
help_heading = Some("NSEC3 (when using '-n')"),
short = 'a',
value_name = "algorithm",
default_value = "SHA-1",
value_parser = ValueParser::new(Nsec3Hash::parse_nsec3_alg),
requires = "nsec3"
)]
#[arg(skip = Nsec3HashAlgorithm::SHA1)]
algorithm: Nsec3HashAlgorithm,
/// Number of hash iterations.
@@ -252,7 +245,7 @@ pub struct SignZone {
///
/// Requires -n.
#[arg(
help_heading = Some("OUTPUT FORMATTING"),
help_heading = Some("Output Formatting"),
short = 'L',
default_value_t = false,
requires = "nsec3"
@@ -263,7 +256,7 @@ pub struct SignZone {
///
/// Enabled automatically by -b.
#[arg(
help_heading = Some("OUTPUT FORMATTING"),
help_heading = Some("Output Formatting"),
short = 'O',
default_value_t = false,
default_value_if("extra_comments", "true", Some("true")),
@@ -275,7 +268,7 @@ pub struct SignZone {
///
/// Enabled automatically by -b.
#[arg(
help_heading = Some("OUTPUT FORMATTING"),
help_heading = Some("Output Formatting"),
short = 'R',
default_value_t = false,
default_value_if("extra_comments", "true", Some("true")),
@@ -286,7 +279,7 @@ pub struct SignZone {
///
/// Cannot be used with -Z or -H.
#[arg(
help_heading = Some("OUTPUT FORMATTING"),
help_heading = Some("Output Formatting"),
short = 'T',
default_value_t = false,
conflicts_with_all = ["allow_zonemd_without_signing", "hash_only"],
@@ -505,8 +498,8 @@ impl LdnsCommand for SignZone {
hash_only: false,
use_yyyymmddhhmmss_rrsig_format: true,
preceed_zone_with_hash_list,
order_rrsigs_after_the_rtype_they_cover: extra_comments,
order_nsec3_rrs_by_unhashed_owner_name: extra_comments,
order_rrsigs_after_the_rtype_they_cover: true,
order_nsec3_rrs_by_unhashed_owner_name: true,
zonefile_path,
key_paths,
invoked_as_ldns: true,
@@ -977,7 +970,11 @@ impl SignZone {
let mut nsec3_hashes: Option<Nsec3HashMap> = None;
if self.use_nsec3 && (self.extra_comments || self.preceed_zone_with_hash_list) {
if self.use_nsec3
&& (self.extra_comments
|| self.preceed_zone_with_hash_list
|| self.order_nsec3_rrs_by_unhashed_owner_name)
{
// Create a collection of NSEC3 hashes that can later be used for
// debug output.
let mut hash_provider = Nsec3HashMap::new();
@@ -1160,9 +1157,6 @@ impl SignZone {
// compatibility with ldns-signzone, re-order them to be in canonical
// order by unhashed owner name and so that hashed names come after
// equivalent unhashed names.
//
// INCOMAPATIBILITY WARNING: Unlike ldns-signzone, we only apply this
// ordering if `-b` is specified.
let mut owner_rrs;
let owner_rrs_iter: AnyOwnerRrsIter =
if self.order_nsec3_rrs_by_unhashed_owner_name && nsec3_hashes.is_some() {
@@ -1255,12 +1249,22 @@ impl SignZone {
// we skip that, and we skip RRSIGs as they are output only after
// the RRset that they cover.
if self.order_rrsigs_after_the_rtype_they_cover {
for rrset in owner_rrs
.rrsets()
.filter(|rrset| !matches!(rrset.rtype(), Rtype::SOA | Rtype::RRSIG))
{
for rrset in owner_rrs.rrsets().filter(|rrset| {
!(matches!(rrset.rtype(), Rtype::SOA | Rtype::RRSIG)
// If run as ldns-signzone we want to list the NSEC RR
// at the end of the RRset of the apex. By default,
// the NSEC RR would preceed the DNSKEY RRset, so we
// need to filter it out here to manually reinsert it
// later. This is only necessary for the NSEC RR at
// the apex, as the ordering issue doesn't appear at
// other locations than the apex.
|| (self.invoked_as_ldns
&& rrset.rtype() == Rtype::NSEC
&& rrset.owner() == apex))
}) {
for rr in rrset.iter() {
self.write_rr(&mut writer, rr)?;
match rr.data() {
ZoneRecordData::Nsec3(nsec3) if self.extra_comments => {
nsec3.comment(&mut writer, rr, nsec3_cs)?
@@ -1283,13 +1287,52 @@ impl SignZone {
for covering_rrsigs in owner_rrs
.rrsets()
.filter(|this_rrset| this_rrset.rtype() == Rtype::RRSIG)
.map(|this_rrset| this_rrset.iter().filter(|rr| matches!(rr.data(), ZoneRecordData::Rrsig(rrsig) if rrsig.type_covered() == rrset.rtype())))
.map(|this_rrset| {
this_rrset.iter().filter(|rr| {
matches!(rr.data(), ZoneRecordData::Rrsig(rrsig)
if rrsig.type_covered() == rrset.rtype()
&& if self.invoked_as_ldns && rr.owner() == apex {
// Withhold an RRSIG that covers the NSEC of the apex
// as we' reinserting them at the end of the apex' RRsets
rrsig.type_covered() != Rtype::NSEC
} else { true }
)
})
})
{
for covering_rrsig_rr in covering_rrsigs {
self.writeln_rr(&mut writer, covering_rrsig_rr)?;
}
}
}
// If running as ldns-signzone, we've been withholding the NSEC and NSEC's RRSIG at
// the apex above to reinsert them after all other RRsets at the apex. By default,
// the DNSKEY RRset and it's RRSIG would take the rear of the RRsets at the apex.
// This doesn't apply, if we're using NSEC3. Additionally, the NSEC RRs at other
// places than the apex do not have the ordering issue.
if self.invoked_as_ldns && !self.use_nsec3 && owner_rrs.owner() == apex {
if let Some(nsec_rrset) = owner_rrs
.rrsets()
.find(|this_rrset| this_rrset.rtype() == Rtype::NSEC)
{
self.writeln_rr(&mut writer, nsec_rrset.first())?;
}
if let Some(rrsig_rrset) = owner_rrs
.rrsets()
.find(|this_rrset| this_rrset.rtype() == Rtype::RRSIG)
{
for rr in rrsig_rrset.iter() {
if matches!(rr.data(), ZoneRecordData::Rrsig(rrsig) if rrsig.type_covered() == Rtype::NSEC)
{
self.writeln_rr(&mut writer, rr)?;
break;
}
}
}
}
if self.extra_comments {
writer.write_str(";\n")?;
}
@@ -1334,6 +1377,13 @@ impl SignZone {
}
}
if self.invoked_as_ldns {
if let ZoneRecordData::Nsec3(nsec3) = rr.data() {
let rr = Record::new(rr.owner(), rr.class(), rr.ttl(), LdnsNsec3(nsec3));
return writer.write_fmt(format_args!("{}", rr.display_zonefile(DISPLAY_KIND)));
}
}
writer.write_fmt(format_args!("{}", rr.display_zonefile(DISPLAY_KIND)))
}
@@ -1562,12 +1612,8 @@ impl SignZone {
Self::write_iterations_warning(env, "NSEC3 iterations larger than 0 increases performance cost while providing only moderate protection!");
}
fn write_iterations_warning(env: &impl Env, text: &str) {
warn!("{text}");
writeln!(
env.stderr(),
"See: https://www.rfc-editor.org/rfc/rfc9276.html"
);
fn write_iterations_warning(_env: &impl Env, text: &str) {
warn!("{text}\nSee: https://www.rfc-editor.org/rfc/rfc9276.html");
}
/// Create the ZONEMD digest for the SIMPLE scheme.
@@ -2033,6 +2079,52 @@ impl<O, N> RecordData for YyyyMmDdHhMMSsRrsig<'_, O, N> {
}
}
//------------ LdnsNsec3 -----------------------------------------------------
/// A wrapper around Nsec3 to print the Nsec3 data in the exact format used by
/// ldns-signzone with all its quirks.
struct LdnsNsec3<'a, O>(&'a Nsec3<O>);
impl<O: AsRef<[u8]>> ZonefileFmt for LdnsNsec3<'_, O> {
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
// This block of code was copied from the `domain` crate impl of
// `Zonefilefmt` for domain::rdata::nsec3::Nsec3 and adapted for
// ldns output format.
p.block(|p| {
p.write_show(self.0.hash_algorithm())?;
p.write_token(self.0.flags())?;
p.write_comment(format_args!(
"flags: {}",
if self.0.opt_out() {
"opt-out"
} else {
"<none>"
}
))?;
p.write_token(self.0.iterations())?;
p.write_comment("iterations")?;
p.write_show(self.0.salt())?;
p.write_token(format!(
" {}",
domain::utils::base32::encode_display_hex(&self.0.next_owner())
.to_string()
.to_lowercase()
))?;
p.write_show(self.0.types())?;
// ldns-signzone ends its NSEC3 rtype bitmap with a trailing
// space. Adding an empty token, because the formatter will add
// a space as a delimiter.
p.write_token("")
})
}
}
impl<O> RecordData for LdnsNsec3<'_, O> {
fn rtype(&self) -> Rtype {
Rtype::NSEC3
}
}
//-------------- Nsec3HashMap ------------------------------------------------
#[derive(Debug)]
@@ -2427,8 +2519,8 @@ mod test {
hash_only: false,
use_yyyymmddhhmmss_rrsig_format: true,
preceed_zone_with_hash_list: false,
order_rrsigs_after_the_rtype_they_cover: false,
order_nsec3_rrs_by_unhashed_owner_name: false,
order_rrsigs_after_the_rtype_they_cover: true,
order_nsec3_rrs_by_unhashed_owner_name: true,
zonefile_path: PathBuf::from("example.org.zone"),
key_paths: Vec::from([PathBuf::from("anykey")]),
invoked_as_ldns: true,
@@ -2967,14 +3059,14 @@ m.root-servers.net.\t3600000\tIN\tAAAA\t2001:dc3::35
// (dnst) ldns-signzone -np -f - -e 20241127162422 -i 20241127162422 nsec3_optout1_example.org.zone ksk1 | grep NSEC3
let ldns_dnst_output_stripped: &str = "\
example.org.\t3600\tIN\tRRSIG\tNSEC3PARAM 15 2 3600 20241127162422 20241127162422 38873 example.org. 0XdDm1l2Mm8dyhtzbyQb91CmyNONs8lc9d22FUGvpjfqo8T2h0xs04x5MIfP0DjmiVnNqIyPK6sipnDqf6tCDg==\n\
example.org.\t3600\tIN\tNSEC3PARAM\t1 1 1 -\n\
example.org.\t3600\tIN\tRRSIG\tNSEC3PARAM 15 2 3600 20241127162422 20241127162422 38873 example.org. 0XdDm1l2Mm8dyhtzbyQb91CmyNONs8lc9d22FUGvpjfqo8T2h0xs04x5MIfP0DjmiVnNqIyPK6sipnDqf6tCDg==\n\
93u63bg57ppj6649al2n31l92iedkjd6.example.org.\t240\tIN\tNSEC3\t1 1 1 - k71ku6aicr5jpdjoe9j7cdnlk6d5c3ue A NS SOA RRSIG DNSKEY NSEC3PARAM \n\
93u63bg57ppj6649al2n31l92iedkjd6.example.org.\t240\tIN\tRRSIG\tNSEC3 15 3 240 20241127162422 20241127162422 38873 example.org. z4ceUmbSZiSnluFj8CDJ7B9fukCR2flTWgca4GE2xrw48+fiieH/04xCKhJmDRJUJTVkKtIYpB4p0Q4m60M1Cg==\n\
93u63bg57ppj6649al2n31l92iedkjd6.example.org.\t240\tIN\tNSEC3\t1 1 1 - K71KU6AICR5JPDJOE9J7CDNLK6D5C3UE A NS SOA RRSIG DNSKEY NSEC3PARAM\n\
k71ku6aicr5jpdjoe9j7cdnlk6d5c3ue.example.org.\t240\tIN\tNSEC3\t1 1 1 - ojicmhri4vp8po7h2kvej99sklqnj5p2 NS \n\
k71ku6aicr5jpdjoe9j7cdnlk6d5c3ue.example.org.\t240\tIN\tRRSIG\tNSEC3 15 3 240 20241127162422 20241127162422 38873 example.org. HUrf7tOm3simXqpZj1oZeKX/P3eWoTTKc3fsyqfuLD6sGssXrBfpv1/LINBR9eEBjJ9rFbQXILgweS6huBL/Ag==\n\
k71ku6aicr5jpdjoe9j7cdnlk6d5c3ue.example.org.\t240\tIN\tNSEC3\t1 1 1 - OJICMHRI4VP8PO7H2KVEJ99SKLQNJ5P2 NS\n\
ojicmhri4vp8po7h2kvej99sklqnj5p2.example.org.\t240\tIN\tNSEC3\t1 1 1 - 93u63bg57ppj6649al2n31l92iedkjd6 NS DS RRSIG \n\
ojicmhri4vp8po7h2kvej99sklqnj5p2.example.org.\t240\tIN\tRRSIG\tNSEC3 15 3 240 20241127162422 20241127162422 38873 example.org. NG/8jk3UHht1ZYNEjUZ4swaEHea1amF4l3jZ893oARi95oxtPVLKoinVbBbfVuoanicOgeZxUPpKWHMBR12XDA==\n\
ojicmhri4vp8po7h2kvej99sklqnj5p2.example.org.\t240\tIN\tNSEC3\t1 1 1 - 93U63BG57PPJ6649AL2N31L92IEDKJD6 NS DS RRSIG\n\
";
let res = FakeCmd::new([
@@ -3003,14 +3095,14 @@ m.root-servers.net.\t3600000\tIN\tAAAA\t2001:dc3::35
fn ldns_signzone_disables_minus_b_when_output_is_to_stdout() {
let expected_output = r###"example.org.\t239\tIN\tSOA\texample.net. hostmaster.example.net. 1234567890 28800 7200 604800 238
example.org.\t239\tIN\tRRSIG\tSOA 8 2 239 20241127162422 20241127162422 51331 example.org. XD5+Exk0KLfvLYA7y+Qs6jhF+JeESFONqZAjkSvznXdjod80W6cv9C77XeHqqod+5glGHlw9bXmVhuJ/5n056BbnDcMWF+AV4taFc/RrDcZb5A0tS6LnRWbpO9puKeLVK10FeAChCygct6/+GNiE12DDLnzKJFuyMuu+nLa2p88=
example.org.\t238\tIN\tRRSIG\tNSEC 8 2 238 20241127162422 20241127162422 51331 example.org. AT4PDLEolpApcrYi7mcTXrqCQ6psXeZNdmFub08m6BJRs2jeW07fM11Amft53FXKgqbT23WILkEM7Raai8E8qPJoSdDCys6zYXW/NCU9Cf/oXIKdD4nxQXXWbnX4GCMN4XJy382dYnxTDssQK6lNIKKi4OvGYIxVUPthaLKJFU0=
example.org.\t239\tIN\tDNSKEY\t257 3 8 AwEAAckp/oMmocs+pv4KsCkCciazIl2+SohAZ2/bH2viAMg3tHAPjw5YfPNErUBqMGvN4c23iBCnt9TktT5bVoQdpXyCJ+ZwmWrFxlXvXIqG8rpkwHi1xFoXWVZLrG9XYCqLVMq2cB+FgMIaX504XMGk7WQydtV1LAqLgP3B8JA2Fc1j ;{id = 51331 (ksk), size = 1024b}
example.org.\t239\tIN\tRRSIG\tDNSKEY 8 2 239 20241127162422 20241127162422 51331 example.org. rLwqlu9fYkzAy0jM9crtw5du4rUaDVH9PI4m06lRwjSKhu1VQ1AHjRhlKy1OgUee/5LovXSRGcgNZi4wiTS5ZULTJw7UQTBRXaaNhVACENX/MoVw9SmYuDSTyvQboChmFmYSMch3Q/02VhgN+BT8F7+OdDVgsWqZUEKPVNixk/0=
example.org.\t238\tIN\tNSEC\tsome.example.org. SOA RRSIG NSEC DNSKEY
example.org.\t239\tIN\tDNSKEY\t257 3 8 AwEAAckp/oMmocs+pv4KsCkCciazIl2+SohAZ2/bH2viAMg3tHAPjw5YfPNErUBqMGvN4c23iBCnt9TktT5bVoQdpXyCJ+ZwmWrFxlXvXIqG8rpkwHi1xFoXWVZLrG9XYCqLVMq2cB+FgMIaX504XMGk7WQydtV1LAqLgP3B8JA2Fc1j ;{id = 51331 (ksk), size = 1024b}
example.org.\t238\tIN\tRRSIG\tNSEC 8 2 238 20241127162422 20241127162422 51331 example.org. AT4PDLEolpApcrYi7mcTXrqCQ6psXeZNdmFub08m6BJRs2jeW07fM11Amft53FXKgqbT23WILkEM7Raai8E8qPJoSdDCys6zYXW/NCU9Cf/oXIKdD4nxQXXWbnX4GCMN4XJy382dYnxTDssQK6lNIKKi4OvGYIxVUPthaLKJFU0=
some.example.org.\t240\tIN\tA\t1.2.3.4
some.example.org.\t240\tIN\tRRSIG\tA 8 3 240 20241127162422 20241127162422 51331 example.org. xdVbhbaMXEyMySCOKy2yYQgU2URAOnu+jLU5py+4R8R3yVVvdl6yMjzdUD3vyxprHitJ+xLrXU/wHSQvtjSwmxVL53ztu+9wrnrhQm6nqXGLW+iw58LepdLVRlppz2WlV0CJAlLIQPJ8rw4hND3NYLJojnO8OdrgpHL89ajD4II=
some.example.org.\t238\tIN\tRRSIG\tNSEC 8 3 238 20241127162422 20241127162422 51331 example.org. PP4tH4Y6JNymWSJebPd3zjvDrjyZXVBF8QTKxKAmbmtPacbWyIcRuI0L8+8Z1folAN2U5cUZmCaIbt5Ylaj6ab4UAYHiy0BrcF/zbNIeLRSTz4hOteencIooTDvqIqYuI9/xTVXcfJ+gVzzlIh2dJK2GW5O4+B1xR+CINLNJ/j8=
some.example.org.\t238\tIN\tNSEC\texample.org. A RRSIG NSEC
some.example.org.\t238\tIN\tRRSIG\tNSEC 8 3 238 20241127162422 20241127162422 51331 example.org. PP4tH4Y6JNymWSJebPd3zjvDrjyZXVBF8QTKxKAmbmtPacbWyIcRuI0L8+8Z1folAN2U5cUZmCaIbt5Ylaj6ab4UAYHiy0BrcF/zbNIeLRSTz4hOteencIooTDvqIqYuI9/xTVXcfJ+gVzzlIh2dJK2GW5O4+B1xR+CINLNJ/j8=
"###.replace("\\t", "\t");
let zone_file_path =
+1441 -184
View File
File diff suppressed because it is too large Load Diff
+14 -11
View File
@@ -96,17 +96,20 @@ impl<T: io::Write> io::Write for &Stream<T> {
impl<T: io::Write> Stream<T> {
pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) {
// This unwrap is not _really_ safe, but we are using this as stdout.
// The `println` macro also ignores errors and `push_str` of the
// fake stream also does not return an error. If this fails, it means
// we can't write to stdout anymore so a graceful exit will be very
// hard anyway.
self.writer
.lock()
.unwrap()
.deref_mut()
.write_fmt(args)
.unwrap();
if let Err(e) = self.writer.lock().unwrap().deref_mut().write_fmt(args) {
// This can happen when output is piped to a command that
// terminates before we do. Note: If stderr is also broken this
// will panc, but then neither the panic nor this error would be
// printed out.
let prog = std::env::args_os()
.next()
.unwrap()
.to_string_lossy()
.to_string();
eprintln!("{prog}: failed printing to stdout: {e}");
// using exit(101) to replicate rust's default error exit code
std::process::exit(101);
}
}
pub fn is_terminal(&self) -> bool {
+3 -3
View File
@@ -74,11 +74,11 @@ impl Error {
PrimaryError::Other(error) => error,
};
error!("{msg}");
let mut err = env.stderr();
let mut buf = String::new();
for context in &self.0.context {
writeln!(err, "... while {context}");
buf.push_str(&format!("... while {context}\n"));
}
error!("{msg}\n{buf}");
}
pub fn exit_code(&self) -> u8 {
+16 -15
View File
@@ -7,13 +7,13 @@ use commands::keygen::Keygen;
use commands::notify::Notify;
use commands::nsec3hash::Nsec3Hash;
use commands::signzone::SignZone;
use commands::update::Update;
use commands::update::LdnsUpdate;
use commands::LdnsCommand;
use domain::base::zonefile_fmt::DisplayKind;
use env::Env;
use error::Error;
use log::LogFormatter;
use domain::base::zonefile_fmt::DisplayKind;
use tracing::level_filters::LevelFilter;
pub use self::args::Args;
@@ -49,7 +49,7 @@ pub fn try_ldns_compatibility<I: IntoIterator<Item = OsString>>(
"keygen" => Keygen::parse_ldns_args(args_iter),
"nsec3-hash" => Nsec3Hash::parse_ldns_args(args_iter),
"signzone" => SignZone::parse_ldns_args(args_iter),
"update" => Update::parse_ldns_args(args_iter),
"update" => LdnsUpdate::parse_ldns_args(args_iter),
_ => Err(format!("Unrecognized ldns command 'ldns-{binary_name}'").into()),
}?;
@@ -100,22 +100,23 @@ fn parse_args(env: impl Env) -> Result<Args, Error> {
pub fn run(env: impl Env) -> u8 {
let stderr = env.stderr();
let subscriber = tracing_subscriber::FmtSubscriber::builder()
let mut subscriber = tracing_subscriber::FmtSubscriber::builder()
.with_ansi(stderr.is_terminal())
.with_writer(stderr)
.with_max_level(LevelFilter::WARN)
.event_format(LogFormatter {
program: env.args_os().next().unwrap().to_string_lossy().to_string(),
})
.finish();
tracing::subscriber::with_default(subscriber, || {
let res = parse_args(&env).and_then(|args| args.execute(&env));
match res {
Ok(()) => 0,
Err(err) => {
});
let res = parse_args(&env);
if let Ok(args) = &res {
subscriber = subscriber.with_max_level(args.verbosity);
}
tracing::subscriber::with_default(subscriber.finish(), || {
res.and_then(|args| args.execute(&env))
.map(|()| 0)
.unwrap_or_else(|err| {
err.pretty_print(&env);
err.exit_code()
}
}
})
})
}