195 lines
7.5 KiB
OCaml
195 lines
7.5 KiB
OCaml
|
|
(* (c) 2017, 2018 Hannes Mehnert, all rights reserved *)
|
||
|
|
|
||
|
|
open Dns
|
||
|
|
|
||
|
|
let src = Logs.Src.create "dns_tsig" ~doc:"DNS tsig"
|
||
|
|
module Log = (val Logs.src_log src : Logs.LOG)
|
||
|
|
|
||
|
|
let algorithm_to_nc = function
|
||
|
|
| Tsig.SHA1 -> `SHA1
|
||
|
|
| Tsig.SHA224 -> `SHA224
|
||
|
|
| Tsig.SHA256 -> `SHA256
|
||
|
|
| Tsig.SHA384 -> `SHA384
|
||
|
|
| Tsig.SHA512 -> `SHA512
|
||
|
|
|
||
|
|
let compute_tsig name tsig ~key buf =
|
||
|
|
let raw_name = Domain_name.raw name in
|
||
|
|
let h = (algorithm_to_nc tsig.Tsig.algorithm :> Digestif.hash')
|
||
|
|
and data = Tsig.encode_raw raw_name tsig
|
||
|
|
in
|
||
|
|
let module H = (val (Digestif.module_of_hash' h)) in
|
||
|
|
H.hmac_string ~key (buf ^ data) |> H.to_raw_string
|
||
|
|
|
||
|
|
let ( let* ) = Result.bind
|
||
|
|
|
||
|
|
let guard p err = if p then Ok () else Error err
|
||
|
|
|
||
|
|
(* TODO: should name compression be done? atm it's convenient not to do it *)
|
||
|
|
let add_tsig ?max_size name tsig str =
|
||
|
|
let buf = Bytes.of_string str in
|
||
|
|
Bytes.set_uint16_be buf 10 (succ (Bytes.get_uint16_be buf 10)) ;
|
||
|
|
let tsig = Tsig.encode_full name tsig in
|
||
|
|
match max_size with
|
||
|
|
| Some x when x - Bytes.length buf < String.length tsig -> None
|
||
|
|
| _ -> Some (Bytes.unsafe_to_string buf ^ tsig)
|
||
|
|
|
||
|
|
let mac_to_prep = function
|
||
|
|
| None -> ""
|
||
|
|
| Some mac ->
|
||
|
|
let l = Bytes.create 2 in
|
||
|
|
Bytes.set_uint16_be l 0 (String.length mac) ;
|
||
|
|
Bytes.unsafe_to_string l ^ mac
|
||
|
|
|
||
|
|
let sign ?mac ?max_size name tsig ~key p str =
|
||
|
|
match Base64.decode key.Dnskey.key with
|
||
|
|
| Error _ -> None
|
||
|
|
| Ok key ->
|
||
|
|
let prep = mac_to_prep mac in
|
||
|
|
let mac = compute_tsig name tsig ~key (prep ^ str) in
|
||
|
|
let tsig = Tsig.with_mac tsig mac in
|
||
|
|
(* RFC2845 Sec 3.1: if TSIG leads to truncation, alter message:
|
||
|
|
- header stays (truncated = true)!
|
||
|
|
- only question is preserved
|
||
|
|
- _one_ additional, the TSIG itself *)
|
||
|
|
match add_tsig ?max_size name tsig str with
|
||
|
|
| Some out -> Some (out, mac)
|
||
|
|
| None ->
|
||
|
|
match p.Packet.data with
|
||
|
|
| #Packet.request ->
|
||
|
|
Log.err (fun m -> m "dns_tsig sign: truncated, is a request, not doing anything") ;
|
||
|
|
None
|
||
|
|
| #Packet.reply as r ->
|
||
|
|
Log.err (fun m -> m "dns_tsig sign: truncated reply %a, sending tsig error"
|
||
|
|
Packet.pp_reply r) ;
|
||
|
|
let header =
|
||
|
|
fst p.header, Packet.Flags.add `Truncation (snd p.header)
|
||
|
|
in
|
||
|
|
let rc = Packet.rcode_data r
|
||
|
|
and op = Packet.opcode_data r
|
||
|
|
in
|
||
|
|
let p' = Packet.create header p.question (`Rcode_error (rc, op, None)) in
|
||
|
|
let new_buf, off = Packet.encode `Udp p' in
|
||
|
|
let tbs = String.sub new_buf 0 off in
|
||
|
|
let mac = compute_tsig name tsig ~key (prep ^ tbs) in
|
||
|
|
let tsig = Tsig.with_mac tsig mac in
|
||
|
|
match add_tsig name tsig new_buf with
|
||
|
|
| None ->
|
||
|
|
Log.err (fun m -> m "dns_tsig sign failed query %a with tsig %a too big (max_size %a) truncated packet %a:@.%a"
|
||
|
|
Packet.pp p Tsig.pp tsig Packet.pp p'
|
||
|
|
Fmt.(option ~none:(any "none") int) max_size
|
||
|
|
Ohex.pp new_buf) ;
|
||
|
|
None
|
||
|
|
| Some out -> Some (out, mac)
|
||
|
|
|
||
|
|
let verify_raw ?mac now name ~key tsig tbs =
|
||
|
|
let name = Domain_name.raw name in
|
||
|
|
let* priv =
|
||
|
|
Result.map_error
|
||
|
|
(fun _ -> `Bad_key (name, tsig))
|
||
|
|
(Base64.decode key.Dnskey.key)
|
||
|
|
in
|
||
|
|
let ac = String.get_int16_be tbs 10 in
|
||
|
|
let tbs = Bytes.of_string tbs in
|
||
|
|
Bytes.set_int16_be tbs 10 (pred ac) ;
|
||
|
|
let tbs = Bytes.unsafe_to_string tbs in
|
||
|
|
let prep = mac_to_prep mac in
|
||
|
|
let computed = compute_tsig name tsig ~key:priv (prep ^ tbs) in
|
||
|
|
let mac = tsig.Tsig.mac in
|
||
|
|
let* () = guard (String.length mac = String.length computed) (`Bad_truncation (name, tsig)) in
|
||
|
|
let* () = guard (String.equal computed mac) (* Eqaf? *) (`Invalid_mac (name, tsig)) in
|
||
|
|
let* () = guard (Tsig.valid_time now tsig) (`Bad_timestamp (name, tsig, key)) in
|
||
|
|
let* tsig =
|
||
|
|
Option.to_result ~none:(`Bad_timestamp (name, tsig, key))
|
||
|
|
(Tsig.with_signed tsig now)
|
||
|
|
in
|
||
|
|
Ok (tsig, mac)
|
||
|
|
|
||
|
|
let verify ?mac now p name ?key tsig tbs =
|
||
|
|
let raw_name = Domain_name.raw name in
|
||
|
|
match
|
||
|
|
let* key =
|
||
|
|
Option.to_result ~none:(`Bad_key (raw_name, tsig)) key
|
||
|
|
in
|
||
|
|
let* tsig, mac = verify_raw ?mac now raw_name ~key tsig tbs in
|
||
|
|
Ok (tsig, mac, key)
|
||
|
|
with
|
||
|
|
| Ok x -> Ok x
|
||
|
|
| Error e ->
|
||
|
|
Log.err (fun m -> m "error %a while verifying %a" Tsig_op.pp_e e Packet.pp p);
|
||
|
|
let answer : string option = match p.Packet.data with
|
||
|
|
| #Packet.reply -> None
|
||
|
|
| #Packet.request as r ->
|
||
|
|
(* now we prepare a reply for the request! *)
|
||
|
|
(* TODO not clear which flags to preserve *)
|
||
|
|
let header = fst p.Packet.header, Packet.Flags.empty
|
||
|
|
and opcode = Packet.opcode_data r
|
||
|
|
in
|
||
|
|
(* TODO: edns *)
|
||
|
|
let answer = Packet.create header p.question (`Rcode_error (Rcode.NotAuth, opcode, None)) in
|
||
|
|
let err, max_size = Packet.encode `Udp answer in
|
||
|
|
let or_err f err = match f err with None -> Some err | Some x -> Some x in
|
||
|
|
match e with
|
||
|
|
| `Bad_key (name, tsig) ->
|
||
|
|
let tsig = Tsig.with_error (Tsig.with_mac tsig String.empty) Rcode.BadKey in
|
||
|
|
or_err (add_tsig ~max_size name tsig) err
|
||
|
|
| `Invalid_mac (name, tsig) ->
|
||
|
|
let tsig = Tsig.with_error (Tsig.with_mac tsig String.empty) Rcode.BadVersOrSig in
|
||
|
|
or_err (add_tsig ~max_size name tsig) err
|
||
|
|
| `Bad_truncation (name, tsig) ->
|
||
|
|
let tsig = Tsig.with_error (Tsig.with_mac tsig String.empty) Rcode.BadTrunc in
|
||
|
|
or_err (add_tsig ~max_size name tsig) err
|
||
|
|
| `Bad_timestamp (name, tsig, key) ->
|
||
|
|
let tsig = Tsig.with_error tsig Rcode.BadTime in
|
||
|
|
match Tsig.with_other tsig (Some now) with
|
||
|
|
| None -> Some err
|
||
|
|
| Some tsig ->
|
||
|
|
match sign ~max_size ~mac:tsig.Tsig.mac name tsig ~key answer err with
|
||
|
|
| None -> Some err
|
||
|
|
| Some (buf, _) -> Some buf
|
||
|
|
in
|
||
|
|
Error (e, answer)
|
||
|
|
|
||
|
|
type s = [ `Key_algorithm of Dnskey.t | `Tsig_creation | `Sign ]
|
||
|
|
|
||
|
|
let pp_s ppf = function
|
||
|
|
| `Key_algorithm key -> Fmt.pf ppf "algorithm %a not supported for tsig" Dnskey.pp key
|
||
|
|
| `Tsig_creation -> Fmt.pf ppf "failed to create tsig"
|
||
|
|
| `Sign -> Fmt.pf ppf "failed to sign"
|
||
|
|
|
||
|
|
let encode_and_sign ?(proto = `Udp) ?mac p now key keyname =
|
||
|
|
let b, _ = Packet.encode proto p in
|
||
|
|
match Tsig.dnskey_to_tsig_algo key with
|
||
|
|
| Error _ -> Error (`Key_algorithm key)
|
||
|
|
| Ok algorithm -> match Tsig.tsig ~algorithm ~signed:now () with
|
||
|
|
| None -> Error `Tsig_creation
|
||
|
|
| Some tsig -> match sign ?mac (Domain_name.raw keyname) ~key tsig p b with
|
||
|
|
| None -> Error `Sign
|
||
|
|
| Some r -> Ok r
|
||
|
|
|
||
|
|
type e = [
|
||
|
|
| `Decode of Packet.err
|
||
|
|
| `Unsigned of Packet.t
|
||
|
|
| `Crypto of Tsig_op.e
|
||
|
|
| `Invalid_key of [ `raw ] Domain_name.t * [ `raw ] Domain_name.t
|
||
|
|
]
|
||
|
|
|
||
|
|
let pp_e ppf = function
|
||
|
|
| `Decode err -> Fmt.pf ppf "decode %a" Packet.pp_err err
|
||
|
|
| `Unsigned res -> Fmt.pf ppf "unsigned %a" Packet.pp res
|
||
|
|
| `Crypto c -> Fmt.pf ppf "crypto %a" Tsig_op.pp_e c
|
||
|
|
| `Invalid_key (key, used) ->
|
||
|
|
Fmt.pf ppf "invalid key, expected %a, but %a was used"
|
||
|
|
Domain_name.pp key Domain_name.pp used
|
||
|
|
|
||
|
|
let decode_and_verify now key keyname ?mac buf =
|
||
|
|
let raw_keyname = Domain_name.raw keyname in
|
||
|
|
match Packet.decode buf with
|
||
|
|
| Error e -> Error (`Decode e)
|
||
|
|
| Ok ({ Packet.tsig = None ; _ } as res) -> Error (`Unsigned res)
|
||
|
|
| Ok ({ Packet.tsig = Some (name, tsig, tsig_off) ; _ } as res) when Domain_name.equal keyname name ->
|
||
|
|
begin match verify_raw ?mac now raw_keyname ~key tsig (String.sub buf 0 tsig_off) with
|
||
|
|
| Ok (_, mac) -> Ok (res, tsig, mac)
|
||
|
|
| Error e -> Error (`Crypto e)
|
||
|
|
end
|
||
|
|
| Ok { Packet.tsig = Some (name, _, _) ; _ } -> Error (`Invalid_key (raw_keyname, name))
|