Packages
An Erlang DNS message library that supports most common record types, TSIG authenticated messages, EDNS0 and DNSSEC.
Current section
Files
Jump to
Current section
Files
src/dnssec.erl
%% -------------------------------------------------------------------
%%
%% Copyright (c) 2011 Andrew Tunnell-Jones. All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
-module(dnssec).
-moduledoc """
The `dnssec` module exports functions used for generating NSEC/NSEC3 records,
signing and verifying RRSIGs, and adding keytags to DNSKEY records.
For example, the `sign_rr/6` function can be given a collection of resource records,
the signer name, keytag, signing algorithm, private key, and a collection of options
and it will return a list of RRSIG records. Supported signing algorithms include
DSA, RSA (SHA1/SHA256/SHA512), ECDSA (P-256/P-384), Ed25519, and Ed448.
""".
%% API
-export([gen_nsec/1, gen_nsec/3, gen_nsec/4]).
-export([gen_nsec3/1, gen_nsec3/2]).
-export([sign_rr/5, sign_rr/6]).
-export([sign_rrset/5, sign_rrset/6]).
-export([verify_rrsig/4]).
-export([add_keytag_to_dnskey/1, add_keytag_to_cdnskey/1]).
-export([canonical_rrdata_form/1]).
-export([ih/4]).
-include_lib("dns_erlang/include/dns.hrl").
-include_lib("public_key/include/public_key.hrl").
-export_type([
sigalg/0,
nsec3_hashalg/0,
nsec3_hashalg_fun/0,
nsec3_salt/0,
nsec3_iterations/0,
gen_nsec_opts/0,
gen_nsec3_opts/0,
sign_rr_opts/0,
verify_rrsig_opts/0,
keytag/0,
key/0,
ds/0,
dnskey/0,
rrsig/0,
nsec/0,
nsec3/0
]).
-doc "DS (Delegation Signer) resource record data.".
-type ds() :: #dns_rrdata_ds{}.
-doc "DNSKEY resource record data.".
-type dnskey() :: #dns_rrdata_dnskey{}.
-doc "RRSIG resource record data.".
-type rrsig() :: #dns_rrdata_rrsig{}.
-doc "NSEC resource record data.".
-type nsec() :: #dns_rrdata_nsec{}.
-doc "NSEC3 resource record data.".
-type nsec3() :: #dns_rrdata_nsec3{}.
-doc """
DNSSEC signing algorithm identifier.
Unlike `t:dns:alg/0`, this type is restricted to algorithms valid for zone signing.
""".
-type sigalg() ::
?DNS_ALG_DSA
| ?DNS_ALG_NSEC3DSA
| ?DNS_ALG_RSASHA1
| ?DNS_ALG_NSEC3RSASHA1
| ?DNS_ALG_RSASHA256
| ?DNS_ALG_RSASHA512
| ?DNS_ALG_ECDSAP256SHA256
| ?DNS_ALG_ECDSAP384SHA384
| ?DNS_ALG_ED25519
| ?DNS_ALG_ED448.
-doc "NSEC3 hash algorithm identifier (currently only SHA-1).".
-type nsec3_hashalg() :: ?DNSSEC_NSEC3_ALG_SHA1.
-doc "Custom hash function for use with `ih/4`.".
-type nsec3_hashalg_fun() :: fun((iodata()) -> binary()).
-doc "NSEC3 salt value.".
-type nsec3_salt() :: binary().
-doc "NSEC3 iteration count.".
-type nsec3_iterations() :: non_neg_integer().
-doc "Options for `gen_nsec/4`.".
-type gen_nsec_opts() :: #{base_types => [dns:type()]}.
-doc "Options for `gen_nsec3/2`.".
-type gen_nsec3_opts() :: gen_nsec_opts().
-doc "DNSKEY key tag (RFC 4034 Appendix B).".
-type keytag() :: integer().
-doc "Cryptographic key material for signing or verification.".
-type key() :: [binary()] | binary().
-doc "Options for `sign_rr/6` and `sign_rrset/6`.".
-type sign_rr_opts() :: #{inception => dns:unix_time(), expiration => dns:unix_time()}.
-doc "Options for `verify_rrsig/4`.".
-type verify_rrsig_opts() :: #{now => dns:unix_time()}.
-define(RSASHA1_PREFIX,
<<16#30, 16#21, 16#30, 16#09, 16#06, 16#05, 16#2B, 16#0E, 16#03, 16#02, 16#1A, 16#05, 16#00,
16#04, 16#14>>
).
-define(RSASHA256_PREFIX,
<<16#30, 16#31, 16#30, 16#0d, 16#06, 16#09, 16#60, 16#86, 16#48, 16#01, 16#65, 16#03, 16#04,
16#02, 16#01, 16#05, 16#00, 16#04, 16#20>>
).
-define(RSASHA512_PREFIX,
<<16#30, 16#51, 16#30, 16#0d, 16#06, 16#09, 16#60, 16#86, 16#48, 16#01, 16#65, 16#03, 16#04,
16#02, 16#03, 16#05, 16#00, 16#04, 16#40>>
).
-define(SECS_IN_YEAR, (365 * 24 * 60 * 60)).
-doc """
Generate NSEC records from a list of `t:dns:rr/0`.
The list must contain a SOA `t:dns:rr/0` which is used to determine zone name and TTL.
""".
-spec gen_nsec([dns:rr()]) -> [dns:rr()].
gen_nsec(RR) ->
case lists:keyfind(?DNS_TYPE_SOA, #dns_rr.type, RR) of
false ->
erlang:error(badarg);
#dns_rr{name = ZoneName} = Soa ->
TTL = minimum_soa_ttl(Soa),
gen_nsec(ZoneName, RR, TTL)
end.
-doc #{equiv => gen_nsec(ZoneName, RR, TTL, #{})}.
-spec gen_nsec(dns:dname(), [dns:rr()], dns:ttl()) -> [dns:rr()].
gen_nsec(ZoneName, RR, TTL) ->
gen_nsec(ZoneName, RR, TTL, #{}).
-doc "Generate NSEC records.".
-spec gen_nsec(dns:dname(), [dns:rr()], dns:ttl(), gen_nsec_opts()) -> [dns:rr()].
gen_nsec(ZoneNameM, RR, TTL, Opts) ->
ZoneName = dns_domain:to_lower(ZoneNameM),
BaseTypes = maps:get(base_types, Opts, [?DNS_TYPE_NSEC, ?DNS_TYPE_RRSIG]),
Map = build_rrmap_gbt(RR, BaseTypes),
Unsorted = [
#dns_rr{
name = Name,
class = Class,
type = ?DNS_TYPE_NSEC,
ttl = TTL,
data = #dns_rrdata_nsec{next_dname = Name, types = Types}
}
|| {Name, Class} := Types <- Map
],
Sorted = name_order(Unsorted),
add_next_dname([], Sorted, ZoneName).
-spec add_next_dname([dns:rr()], [dns:rr(), ...], binary()) -> [dns:rr(), ...].
add_next_dname(Added, [#dns_rr{data = Data} = RR | [#dns_rr{name = Next} | _] = ToAdd], ZoneName) ->
NewRR = RR#dns_rr{data = Data#dns_rrdata_nsec{next_dname = Next}},
NewAdded = [NewRR | Added],
add_next_dname(NewAdded, ToAdd, ZoneName);
add_next_dname(Added, [#dns_rr{type = ?DNS_TYPE_NSEC, data = Data} = RR], ZoneName) ->
NewRR = RR#dns_rr{data = Data#dns_rrdata_nsec{next_dname = ZoneName}},
lists:reverse([NewRR | Added]).
-doc #{equiv => gen_nsec3(RRs, #{})}.
-spec gen_nsec3([dns:rr()]) -> [dns:rr()].
gen_nsec3(RRs) ->
gen_nsec3(RRs, #{}).
-doc """
Generate NSEC3 records from a list of `t:dns:rr/0`.
The list must contain a SOA `t:dns:rr/0` to source the zone name and
TTL from as well as as an NSEC3Param `t:dns:rr/0` to source the
hash algorithm, iterations and salt from.
""".
-spec gen_nsec3([dns:rr()], gen_nsec3_opts()) -> [dns:rr()].
gen_nsec3(RRs, Opts) ->
case lists:keyfind(?DNS_TYPE_SOA, #dns_rr.type, RRs) of
false ->
erlang:error(badarg);
#dns_rr{name = ZoneName} = Soa ->
case lists:keyfind(?DNS_TYPE_NSEC3PARAM, #dns_rr.type, RRs) of
false ->
erlang:error(badarg);
#dns_rr{
class = Class,
data = #dns_rrdata_nsec3param{
hash_alg = HashAlg,
iterations = Iter,
salt = Salt
}
} ->
TTL = minimum_soa_ttl(Soa),
gen_nsec3(RRs, ZoneName, HashAlg, Salt, Iter, TTL, Class, Opts)
end
end.
-spec gen_nsec3(
[dns:rr()],
dns:dname(),
nsec3_hashalg(),
nsec3_salt(),
nsec3_iterations(),
dns:ttl(),
dns:class(),
gen_nsec3_opts()
) -> [dns:rr()].
gen_nsec3(RRs, ZoneName, Alg, Salt, Iterations, TTL, Class, Opts) ->
BaseTypes = maps:get(base_types, Opts, [?DNS_TYPE_RRSIG]),
Map = build_rrmap(RRs, BaseTypes, ZoneName),
Unsorted = maps:fold(
fun
({Name, SClass}, Types, Acc) when SClass =:= Class ->
DName = dns_domain:to_wire(Name),
HashedName = ih(Alg, Salt, DName, Iterations),
HexdHashName = base32:encode(HashedName, [hex, nopad]),
NewName = <<HexdHashName/binary, $., ZoneName/binary>>,
Data = #dns_rrdata_nsec3{
hash_alg = Alg,
opt_out = false,
iterations = Iterations,
salt = Salt,
hash = HashedName,
types = Types
},
NewRR = #dns_rr{
name = NewName,
class = Class,
type = ?DNS_TYPE_NSEC3,
ttl = TTL,
data = Data
},
[{HashedName, NewRR} | Acc];
(_, _, Acc) ->
Acc
end,
[],
Map
),
Sorted = [RR || {_, RR} <- lists:keysort(1, Unsorted)],
add_next_hash(Sorted).
-spec minimum_soa_ttl(dns:rr()) -> dns:ttl().
minimum_soa_ttl(#dns_rr{type = ?DNS_TYPE_SOA, ttl = Rec, data = #dns_rrdata_soa{minimum = Min}}) ->
erlang:min(Min, Rec).
-doc "NSEC3 iterative hash function.".
-spec ih(nsec3_hashalg() | nsec3_hashalg_fun(), nsec3_salt(), binary(), nsec3_iterations()) ->
binary().
ih(?DNSSEC_NSEC3_ALG_SHA1, Salt, X, I) when is_binary(Salt), is_binary(X), is_integer(I), 0 =< I ->
ih_nsec3(Salt, X, I);
ih(H, Salt, X, I) when is_function(H, 1), is_binary(Salt), is_binary(X), is_integer(I), 0 =< I ->
ih_nsec3_custom(H, Salt, X, I).
%% Optimise for the common case
-spec ih_nsec3(nsec3_salt(), binary(), nsec3_iterations()) -> binary().
ih_nsec3(Salt, X, 0) ->
crypto:hash(sha, [X, Salt]);
ih_nsec3(Salt, X, I) ->
ih_nsec3(Salt, crypto:hash(sha, [X, Salt]), I - 1).
-spec ih_nsec3_custom(fun((iodata()) -> binary()), nsec3_salt(), binary(), nsec3_iterations()) ->
binary().
ih_nsec3_custom(H, Salt, X, 0) ->
H([X, Salt]);
ih_nsec3_custom(H, Salt, X, I) ->
ih_nsec3_custom(H, Salt, H([X, Salt]), I - 1).
-spec add_next_hash([dns:rr(), ...]) -> [dns:rr(), ...].
add_next_hash([#dns_rr{data = #dns_rrdata_nsec3{hash = First}} | _] = Hashes) ->
add_next_hash(Hashes, [], First).
-spec add_next_hash([dns:rr(), ...], [dns:rr()], _) -> [dns:rr(), ...].
add_next_hash([#dns_rr{data = Data} = RR], RRs, FirstHash) ->
NewRR = RR#dns_rr{data = Data#dns_rrdata_nsec3{hash = FirstHash}},
lists:reverse([NewRR | RRs]);
add_next_hash(
[
#dns_rr{data = Data} = RR
| [#dns_rr{data = #dns_rrdata_nsec3{hash = NextHash}} | _] = Hashes
],
RRs,
FirstHash
) ->
NewRR = RR#dns_rr{data = Data#dns_rrdata_nsec3{hash = NextHash}},
add_next_hash(Hashes, [NewRR | RRs], FirstHash).
-spec normalise_rr(dns:rr()) -> dns:rr().
normalise_rr(#dns_rr{name = Name} = RR) ->
RR#dns_rr{name = dns_domain:to_lower(Name)}.
-type rrtype_map() :: #{{dns:dname(), dns:class()} => [integer()]}.
-spec build_rrmap([dns:rr()], [integer()], dns:dname()) -> rrtype_map().
build_rrmap(RR, BaseTypes, ZoneName) ->
Base = build_rrmap_gbt(RR, BaseTypes),
build_rrmap_nonterm(ZoneName, maps:keys(Base), Base).
-spec build_rrmap_nonterm(dns:dname(), [{dns:dname(), dns:class()} | binary()], rrtype_map()) ->
rrtype_map().
build_rrmap_nonterm(_, [], Map) ->
Map;
build_rrmap_nonterm(ZoneName, [{Name, Class} | Rest], Map) when is_binary(ZoneName) ->
NameAncs = name_ancestors(Name, ZoneName),
NewMap = add_nonterm_ancestors(Class, NameAncs, Map),
build_rrmap_nonterm(ZoneName, Rest, NewMap).
-spec add_nonterm_ancestors(dns:class(), [dns:dname()], rrtype_map()) -> rrtype_map().
add_nonterm_ancestors(_, [], Map) ->
Map;
add_nonterm_ancestors(Class, [Name | Rest], Map) ->
Key = {Name, Class},
NewMap = maps:update_with(Key, fun(V) -> V end, [], Map),
add_nonterm_ancestors(Class, Rest, NewMap).
-spec build_rrmap_gbt([dns:rr()], [dns:type()]) -> rrtype_map().
build_rrmap_gbt(RR, BaseTypes) ->
build_rrmap_gbt(RR, BaseTypes, #{}).
-spec build_rrmap_gbt([dns:rr()], [dns:type()], rrtype_map()) -> rrtype_map().
build_rrmap_gbt([], _BaseTypes, Map) ->
Map;
build_rrmap_gbt([#dns_rr{} = RR | Rest], BaseTypes, Map) ->
#dns_rr{name = Name, class = Class, type = Type} = normalise_rr(RR),
Key = {Name, Class},
NewMap = maps:update_with(
Key,
fun(Types) ->
case lists:member(Type, Types) of
true -> Types;
false -> [Type | Types]
end
end,
[Type | BaseTypes],
Map
),
build_rrmap_gbt(Rest, BaseTypes, NewMap).
-type tree_key() :: {dns:dname(), dns:class(), dns:type()}.
-spec rrs_to_rrsets([dns:rr()]) -> [[dns:rr()]].
rrs_to_rrsets(RR) when is_list(RR) ->
rrs_to_rrsets(RR, #{}, #{}).
-spec rrs_to_rrsets([dns:rr()], #{tree_key() => dns:ttl()}, #{tree_key() => [dns:rrdata()]}) ->
[[dns:rr()]].
rrs_to_rrsets([], TTLMap, RRSets) ->
[finalise_rrs_to_rrsets(TTLMap, Key, Datas) || Key := Datas <- RRSets];
rrs_to_rrsets([#dns_rr{} = RR | RRs], TTLMap, RRSets) ->
#dns_rr{
name = Name,
class = Class,
type = Type,
ttl = TTL,
data = Data
} = normalise_rr(RR),
Key = {Name, Class, Type},
NewTTLMap = maps:update_with(Key, fun(OldTTL) -> max(OldTTL, TTL) end, TTL, TTLMap),
NewRRSets = maps:update_with(Key, fun(OldData) -> [Data | OldData] end, [Data], RRSets),
rrs_to_rrsets(RRs, NewTTLMap, NewRRSets).
-spec finalise_rrs_to_rrsets(#{tree_key() => dns:ttl()}, tree_key(), [dns:rrdata()]) ->
[dns:rr()].
finalise_rrs_to_rrsets(TTLMap, {Name, Class, Type} = Key, Datas) ->
TTL = maps:get(Key, TTLMap),
[
#dns_rr{
name = Name,
class = Class,
type = Type,
ttl = TTL,
data = Data
}
|| Data <- Datas
].
-doc #{equiv => sign_rr(RR, SignerName, KeyTag, Alg, Key, [])}.
-spec sign_rr([dns:rr()], dns:dname(), keytag(), sigalg(), key()) -> [dns:rr()].
sign_rr(RR, SignerName, KeyTag, Alg, Key) ->
sign_rr(RR, SignerName, KeyTag, Alg, Key, #{}).
-doc "Signs a list of `t:dns:rr/0`.".
-spec sign_rr([dns:rr()], dns:dname(), keytag(), sigalg(), key(), sign_rr_opts()) -> [dns:rr()].
sign_rr(RR, SignerName, KeyTag, Alg, Key, Opts) when is_map(Opts) ->
RRSets = rrs_to_rrsets(RR),
[
sign_rrset(RRSet, SignerName, KeyTag, Alg, Key, Opts)
|| RRSet <- RRSets
].
-doc #{equiv => sign_rrset(RRSet, SignerName, KeyTag, Alg, Key, [])}.
-spec sign_rrset([dns:rr(), ...], dns:dname(), keytag(), sigalg(), key()) ->
dns:rr().
sign_rrset(RRSet, SignerName, KeyTag, Alg, Key) ->
sign_rrset(RRSet, SignerName, KeyTag, Alg, Key, #{}).
-doc "Signs a list of `t:dns:rr/0` of the same class and type.".
-spec sign_rrset([dns:rr(), ...], dns:dname(), keytag(), sigalg(), key(), sign_rr_opts()) ->
dns:rr().
sign_rrset(
[#dns_rr{name = Name, class = Class, ttl = TTL} | _] = RRs,
SignersName,
KeyTag,
Alg,
Key,
Opts
) when is_integer(Alg) ->
Now = erlang:system_time(second),
Incept = maps:get(inception, Opts, Now),
%% 1 year
Expire = maps:get(expiration, Opts, Now + ?SECS_IN_YEAR),
{Data0, BaseSigInput} = build_sig_input(RRs, SignersName, KeyTag, Alg, Incept, Expire),
Signature = sign(Alg, BaseSigInput, Key),
Data = Data0#dns_rrdata_rrsig{signature = Signature},
#dns_rr{
name = Name,
type = ?DNS_TYPE_RRSIG,
class = Class,
ttl = TTL,
data = Data
}.
-spec sign(sigalg(), binary(), key()) -> binary().
sign(Alg, BaseSigInput, Key) when Alg =:= ?DNS_ALG_DSA orelse Alg =:= ?DNS_ALG_NSEC3DSA ->
%% RFC2536: Key must be [P, Q, G, X] as integers (crypto key format).
DerSig = crypto:sign(dss, sha, BaseSigInput, Key),
#'Dss-Sig-Value'{r = R, s = S} = public_key:der_decode('Dss-Sig-Value', DerSig),
[P, _Q, _G, _Y] = Key,
M = byte_size(binary:encode_unsigned(P)),
T = (M - 64) div 8,
<<T, R:20/unit:8, S:20/unit:8>>;
sign(Alg, BaseSigInput, Key) when
Alg =:= ?DNS_ALG_NSEC3RSASHA1 orelse
Alg =:= ?DNS_ALG_RSASHA1 orelse
Alg =:= ?DNS_ALG_RSASHA256 orelse
Alg =:= ?DNS_ALG_RSASHA512
->
crypto:sign(
rsa,
none,
BaseSigInput,
Key,
[{rsa_padding, rsa_pkcs1_padding}]
);
sign(?DNS_ALG_ECDSAP256SHA256, BaseSigInput, Key) ->
DerSig = crypto:sign(ecdsa, sha256, BaseSigInput, [Key, secp256r1]),
%% RFC6605: keys must be r||s of 32 octets each
ecdsa_der_to_wire(DerSig, 32);
sign(?DNS_ALG_ECDSAP384SHA384, BaseSigInput, Key) ->
DerSig = crypto:sign(ecdsa, sha384, BaseSigInput, [Key, secp384r1]),
%% RFC6605: keys must be r||s of 48 octets each
ecdsa_der_to_wire(DerSig, 48);
sign(?DNS_ALG_ED25519, BaseSigInput, Key) ->
crypto:sign(eddsa, none, BaseSigInput, [Key, ed25519]);
sign(?DNS_ALG_ED448, BaseSigInput, Key) ->
crypto:sign(eddsa, none, BaseSigInput, [Key, ed448]).
-doc "Provides primitive verification of an RR set.".
-spec verify_rrsig(dns:rr(), [dns:rr()], [dns:rr()], verify_rrsig_opts()) -> boolean().
verify_rrsig(#dns_rr{type = ?DNS_TYPE_RRSIG, data = Data}, RRs, RRDNSKey, Opts) ->
Now = maps:get(now, Opts, erlang:system_time(second)),
#dns_rrdata_rrsig{
original_ttl = OTTL,
keytag = SigKeyTag,
alg = SigAlg,
inception = Incept,
expiration = Expire,
signers_name = SignersName,
signature = Signature
} = Data,
Keys0 = [
{KeyTag, Alg, PubKey}
|| #dns_rr{
name = Name,
type = ?DNS_TYPE_DNSKEY,
data = #dns_rrdata_dnskey{
protocol = 3,
alg = Alg,
keytag = KeyTag,
public_key = PubKey
}
} <- RRDNSKey,
Alg =:= SigAlg,
dns_domain:are_equal(Name, SignersName)
],
Keys =
case lists:keytake(SigKeyTag, 1, Keys0) of
false -> Keys0;
{value, Match, RemKeys} -> [Match | RemKeys]
end,
case Now of
Now when Now < Incept; Expire < Now -> false;
Now ->
{_SigTuple, SigInput} = build_sig_input(
RRs,
SignersName,
SigKeyTag,
SigAlg,
Incept,
Expire,
OTTL
),
lists:any(fun({_KeyTag, Alg, Key}) -> verify(Alg, Key, Signature, SigInput) end, Keys)
end.
-spec verify(sigalg(), key(), binary(), binary()) -> boolean().
verify(Alg, Key, Signature, SigInput) when Alg =:= ?DNS_ALG_DSA orelse Alg =:= ?DNS_ALG_NSEC3DSA ->
<<_T, R:20/unit:8, S:20/unit:8>> = Signature,
AsnSig = public_key:der_encode('Dss-Sig-Value', #'Dss-Sig-Value'{r = R, s = S}),
%% DNSKEY public_key from decode is [P, Q, G, Y] as binaries; normalize to integers for crypto.
KeyInts = dsa_pubkey_to_integers(Key),
crypto:verify(dss, sha, SigInput, AsnSig, KeyInts);
verify(Alg, Key, Signature, SigInput) when
Alg =:= ?DNS_ALG_NSEC3RSASHA1 orelse
Alg =:= ?DNS_ALG_RSASHA1 orelse
Alg =:= ?DNS_ALG_RSASHA256 orelse
Alg =:= ?DNS_ALG_RSASHA512
->
try
crypto:verify(
rsa,
none,
SigInput,
Signature,
Key,
[{rsa_padding, rsa_pkcs1_padding}]
)
catch
error:decrypt_failed -> false
end;
verify(?DNS_ALG_ECDSAP256SHA256, Key, Signature, SigInput) ->
DerSig = ecdsa_wire_to_der(Signature, 32),
crypto:verify(ecdsa, sha256, SigInput, DerSig, [<<4, Key/binary>>, secp256r1]);
verify(?DNS_ALG_ECDSAP384SHA384, Key, Signature, SigInput) ->
DerSig = ecdsa_wire_to_der(Signature, 48),
crypto:verify(ecdsa, sha384, SigInput, DerSig, [<<4, Key/binary>>, secp384r1]);
verify(?DNS_ALG_ED25519, Key, Signature, SigInput) ->
crypto:verify(eddsa, none, SigInput, Signature, [Key, ed25519]);
verify(?DNS_ALG_ED448, Key, Signature, SigInput) ->
crypto:verify(eddsa, none, SigInput, Signature, [Key, ed448]).
-spec build_sig_input([dns:rr(), ...], dns:dname(), keytag(), dns:alg(), integer(), integer()) ->
{dns:rrdata_rrsig(), binary()}.
build_sig_input(
[#dns_rr{ttl = TTL} | _] = RRs,
SignersName,
KeyTag,
Alg,
Incept,
Expire
) ->
build_sig_input(RRs, SignersName, KeyTag, Alg, Incept, Expire, TTL).
-spec build_sig_input(
[dns:rr(), ...], dns:dname(), keytag(), dns:alg(), integer(), integer(), non_neg_integer()
) ->
{dns:rrdata_rrsig(), binary()}.
build_sig_input(
[#dns_rr{name = Name, class = Class, type = Type, ttl = TTL} | _] = RRs,
SignersName,
KeyTag,
Alg,
Incept,
Expire,
TTL
) when is_integer(Alg) ->
LName = dns_domain:to_lower(Name),
Datas = lists:sort([canonical_rrdata_bin(RR) || RR <- RRs]),
LLabels = dns_domain:split(LName),
NameBin = dns_domain:to_wire(LName),
RecordBase = <<NameBin/binary, Type:16, Class:16, TTL:32>>,
RRSetBin = [<<RecordBase/binary, (byte_size(Data)):16, Data/binary>> || Data <- Datas],
RRSigData = #dns_rrdata_rrsig{
type_covered = Type,
alg = Alg,
labels = count_labels(LLabels),
original_ttl = TTL,
inception = Incept,
expiration = Expire,
keytag = KeyTag,
signers_name = SignersName
},
RRSigRDataBin = rrsig_to_digestable(RRSigData),
SigInput0 = [RRSigRDataBin | RRSetBin],
SigInput = preprocess_sig_input(Alg, SigInput0),
{RRSigData, SigInput}.
-spec preprocess_sig_input(sigalg(), [binary()]) -> binary().
preprocess_sig_input(Alg, SigInput) when Alg =:= ?DNS_ALG_DSA orelse Alg =:= ?DNS_ALG_NSEC3DSA ->
NewSigInput = iolist_to_binary(SigInput),
NewSigInputSize = byte_size(NewSigInput),
<<NewSigInputSize:32, NewSigInput/binary>>;
preprocess_sig_input(Alg, SigInput) when
Alg =:= ?DNS_ALG_NSEC3RSASHA1 orelse
Alg =:= ?DNS_ALG_RSASHA1 orelse
Alg =:= ?DNS_ALG_RSASHA256 orelse
Alg =:= ?DNS_ALG_RSASHA512
->
{Prefix, HashType} = choose_sha_prefix_and_type(Alg),
Hash = crypto:hash(HashType, SigInput),
<<Prefix/binary, Hash/binary>>;
preprocess_sig_input(?DNS_ALG_ECDSAP256SHA256, SigInput) ->
crypto:hash(sha256, SigInput);
preprocess_sig_input(?DNS_ALG_ECDSAP384SHA384, SigInput) ->
crypto:hash(sha384, SigInput);
preprocess_sig_input(Alg, SigInput) when Alg =:= ?DNS_ALG_ED25519 orelse Alg =:= ?DNS_ALG_ED448 ->
iolist_to_binary(SigInput).
-spec choose_sha_prefix_and_type(sigalg()) -> {binary(), sha | sha256 | sha512}.
choose_sha_prefix_and_type(?DNS_ALG_RSASHA1) ->
{?RSASHA1_PREFIX, sha};
choose_sha_prefix_and_type(?DNS_ALG_NSEC3RSASHA1) ->
{?RSASHA1_PREFIX, sha};
choose_sha_prefix_and_type(?DNS_ALG_RSASHA256) ->
{?RSASHA256_PREFIX, sha256};
choose_sha_prefix_and_type(?DNS_ALG_RSASHA512) ->
{?RSASHA512_PREFIX, sha512}.
-doc "Generates and appends a DNS Key records key tag.".
-spec add_keytag_to_dnskey(dns:rr()) -> dns:rr().
add_keytag_to_dnskey(
#dns_rr{type = ?DNS_TYPE_DNSKEY, data = #dns_rrdata_dnskey{} = Data} = RR
) ->
add_keytag_to_key_rr(RR, Data, ?DNS_TYPE_DNSKEY).
-doc "Generates and appends a CDNSKEY records key tag.".
-spec add_keytag_to_cdnskey(dns:rr()) -> dns:rr().
add_keytag_to_cdnskey(
#dns_rr{type = ?DNS_TYPE_CDNSKEY, data = #dns_rrdata_cdnskey{} = Data} = RR
) ->
add_keytag_to_key_rr(RR, Data, ?DNS_TYPE_CDNSKEY).
-spec add_keytag_to_key_rr(dns:rr(), dns:rrdata(), dns:type()) -> dns:rr().
add_keytag_to_key_rr(RR, Data, Type) ->
KeyBin = dns_encode:encode_rrdata(?DNS_CLASS_IN, Data),
NewData = dns_decode:decode_rrdata(KeyBin, ?DNS_CLASS_IN, Type),
RR#dns_rr{data = NewData}.
-spec rrsig_to_digestable(dns:rrdata_rrsig()) -> binary().
rrsig_to_digestable(#dns_rrdata_rrsig{} = Data) ->
dns_encode:encode_rrdata(?DNS_CLASS_IN, Data#dns_rrdata_rrsig{signature = <<>>}).
-spec canonical_rrdata_bin(dns:rr()) -> binary().
canonical_rrdata_bin(#dns_rr{class = Class, data = Data0}) ->
dns_encode:encode_rrdata(Class, canonical_rrdata_form(Data0)).
-doc "Converts a resource record data record to DNSSEC canonical form.".
-spec canonical_rrdata_form(dns:rrdata()) -> dns:rrdata().
canonical_rrdata_form(#dns_rrdata_afsdb{hostname = Hostname} = Data) ->
Data#dns_rrdata_afsdb{hostname = dns_domain:to_lower(Hostname)};
canonical_rrdata_form(#dns_rrdata_cname{dname = DName} = Data) ->
Data#dns_rrdata_cname{dname = dns_domain:to_lower(DName)};
canonical_rrdata_form(#dns_rrdata_dname{dname = DName} = Data) ->
Data#dns_rrdata_dname{dname = dns_domain:to_lower(DName)};
canonical_rrdata_form(#dns_rrdata_dsync{target = Target} = Data) ->
Data#dns_rrdata_dsync{target = dns_domain:to_lower(Target)};
canonical_rrdata_form(#dns_rrdata_ipseckey{gateway = Gateway} = Data) when is_binary(Gateway) ->
Data#dns_rrdata_ipseckey{gateway = dns_domain:to_lower(Gateway)};
canonical_rrdata_form(#dns_rrdata_kx{exchange = Exchange} = Data) ->
Data#dns_rrdata_kx{exchange = dns_domain:to_lower(Exchange)};
canonical_rrdata_form(#dns_rrdata_mb{madname = MaDname} = Data) ->
Data#dns_rrdata_mb{madname = dns_domain:to_lower(MaDname)};
canonical_rrdata_form(#dns_rrdata_mg{madname = MaDname} = Data) ->
Data#dns_rrdata_mg{madname = dns_domain:to_lower(MaDname)};
canonical_rrdata_form(#dns_rrdata_minfo{rmailbx = RmailBx, emailbx = EmailBx} = Data) ->
Data#dns_rrdata_minfo{
rmailbx = dns_domain:to_lower(RmailBx),
emailbx = dns_domain:to_lower(EmailBx)
};
canonical_rrdata_form(#dns_rrdata_mr{newname = NewName} = Data) ->
Data#dns_rrdata_mr{newname = dns_domain:to_lower(NewName)};
canonical_rrdata_form(#dns_rrdata_mx{exchange = Exchange} = Data) ->
Data#dns_rrdata_mx{exchange = dns_domain:to_lower(Exchange)};
canonical_rrdata_form(#dns_rrdata_naptr{replacement = Replacement} = Data) ->
Data#dns_rrdata_naptr{replacement = dns_domain:to_lower(Replacement)};
canonical_rrdata_form(#dns_rrdata_ns{dname = DName} = Data) ->
Data#dns_rrdata_ns{dname = dns_domain:to_lower(DName)};
canonical_rrdata_form(#dns_rrdata_nsec{next_dname = NextDname} = Data) ->
Data#dns_rrdata_nsec{next_dname = dns_domain:to_lower(NextDname)};
canonical_rrdata_form(#dns_rrdata_nxt{dname = DName} = Data) ->
Data#dns_rrdata_nxt{dname = dns_domain:to_lower(DName)};
canonical_rrdata_form(#dns_rrdata_ptr{dname = DName} = Data) ->
Data#dns_rrdata_ptr{dname = dns_domain:to_lower(DName)};
canonical_rrdata_form(#dns_rrdata_rp{mbox = Mbox, txt = Txt} = Data) ->
Data#dns_rrdata_rp{
mbox = dns_domain:to_lower(Mbox),
txt = dns_domain:to_lower(Txt)
};
canonical_rrdata_form(#dns_rrdata_rrsig{signers_name = SignersName} = Data) ->
Data#dns_rrdata_rrsig{signers_name = dns_domain:to_lower(SignersName)};
canonical_rrdata_form(#dns_rrdata_rt{host = Host} = Data) ->
Data#dns_rrdata_rt{host = dns_domain:to_lower(Host)};
canonical_rrdata_form(#dns_rrdata_soa{mname = Mname, rname = Rname} = Data) ->
Data#dns_rrdata_soa{
mname = dns_domain:to_lower(Mname),
rname = dns_domain:to_lower(Rname)
};
canonical_rrdata_form(#dns_rrdata_srv{target = Target} = Data) ->
Data#dns_rrdata_srv{target = dns_domain:to_lower(Target)};
canonical_rrdata_form(#dns_rrdata_svcb{target_name = TargetName} = Data) ->
Data#dns_rrdata_svcb{target_name = dns_domain:to_lower(TargetName)};
canonical_rrdata_form(#dns_rrdata_https{target_name = TargetName} = Data) ->
Data#dns_rrdata_https{target_name = dns_domain:to_lower(TargetName)};
canonical_rrdata_form(X) ->
X.
-spec name_ancestors(dns:dname(), dns:dname()) -> [binary()].
name_ancestors(Name, ZoneName) ->
NameLwr = dns_domain:to_lower(Name),
ZoneNameLwr = dns_domain:to_lower(ZoneName),
gen_name_ancestors(NameLwr, ZoneNameLwr).
-spec gen_name_ancestors(binary(), binary()) -> [binary()].
gen_name_ancestors(ZoneName, ZoneName) ->
[];
gen_name_ancestors(Name, ZoneName) when byte_size(Name) > byte_size(ZoneName) + 1 ->
Offset = byte_size(Name) - byte_size(ZoneName) - 1,
case Name of
<<RelName:Offset/binary, $., ZoneName/binary>> ->
case dns_domain:split(RelName) of
[_] ->
[];
[_ | Labels0] ->
[FirstLabel | Labels] = lists:reverse(Labels0),
gen_name_ancestors_labels(Labels, [<<FirstLabel/binary, $., ZoneName/binary>>])
end;
_ ->
erlang:error(name_mismatch)
end.
gen_name_ancestors_labels([], Anc) ->
Anc;
gen_name_ancestors_labels([Label | Labels], [Parent | _] = Asc) ->
NewName = <<Label/binary, $., Parent/binary>>,
gen_name_ancestors_labels(Labels, [NewName | Asc]).
-spec name_order([dns:rr(), ...]) -> [dns:rr(), ...].
name_order(RRs) when is_list(RRs) ->
lists:sort(fun name_order/2, RRs).
-spec name_order(_, _) -> boolean().
name_order(X, X) ->
true;
name_order(#dns_rr{name = X}, #dns_rr{name = X}) ->
true;
name_order(#dns_rr{name = A}, #dns_rr{name = B}) ->
LabelsA = lists:reverse(dns_domain:split(A)),
LabelsB = lists:reverse(dns_domain:split(B)),
name_order_labels(LabelsA, LabelsB).
name_order_labels([X | A], [X | B]) ->
name_order_labels(A, B);
name_order_labels([], [_ | _]) ->
true;
name_order_labels([_ | _], []) ->
false;
name_order_labels([X | _], [Y | _]) ->
X < Y.
-spec count_labels([binary()]) -> non_neg_integer().
count_labels([~"*" | Labels]) ->
length(Labels);
count_labels(List) when is_list(List) ->
length(List).
%% Convert DNSKEY public_key [P, Q, G, Y] (binaries from decode) to integers for crypto:verify.
-spec dsa_pubkey_to_integers([binary() | integer()]) -> [integer()].
dsa_pubkey_to_integers([P, Q, G, Y]) ->
[dsa_elem_to_int(X) || X <- [P, Q, G, Y]].
-spec dsa_elem_to_int(binary() | integer()) -> integer().
dsa_elem_to_int(B) when is_binary(B) ->
binary:decode_unsigned(B);
dsa_elem_to_int(I) when is_integer(I) ->
I.
%% RFC 6605 ยง4: ECDSA signatures on the wire are r || s, each padded to FieldSize bytes.
%% crypto:sign returns DER-encoded ASN.1; crypto:verify expects DER-encoded ASN.1.
-spec ecdsa_der_to_wire(binary(), pos_integer()) -> binary().
ecdsa_der_to_wire(DerSig, FieldSize) ->
#'ECDSA-Sig-Value'{r = R, s = S} = public_key:der_decode('ECDSA-Sig-Value', DerSig),
RBin = pad_unsigned(binary:encode_unsigned(R), FieldSize),
SBin = pad_unsigned(binary:encode_unsigned(S), FieldSize),
<<RBin/binary, SBin/binary>>.
-spec ecdsa_wire_to_der(binary(), pos_integer()) -> binary().
ecdsa_wire_to_der(WireSig, FieldSize) ->
<<RBin:FieldSize/binary, SBin:FieldSize/binary>> = WireSig,
R = binary:decode_unsigned(RBin),
S = binary:decode_unsigned(SBin),
public_key:der_encode('ECDSA-Sig-Value', #'ECDSA-Sig-Value'{r = R, s = S}).
-spec pad_unsigned(binary(), pos_integer()) -> binary().
pad_unsigned(Bin, Size) when byte_size(Bin) >= Size ->
Bin;
pad_unsigned(Bin, Size) ->
Pad = Size - byte_size(Bin),
<<0:(Pad * 8), Bin/binary>>.