package dns-certify

  1. Overview
  2. Docs
MirageOS let's encrypt certificate retrieval

Install

Dune Dependency

Authors

Maintainers

Sources

dns-v4.4.1.tbz
sha256=c8b2a49485c7a1bed06c3be47c80c6e865728e3f6658407e179bc5099c4c80a1
sha512=f9997cf9bbe4705ff9d49b3b69300cf5eec12c79bec68e32e20c0356e10fb9e7a150b08e627e429d2eaaf35befcdbabc738dff8535a763e7c57d825d006e21ea

doc/src/dns-certify/dns_certify.ml.html

Source file dns_certify.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
open Dns

let tlsa_is usage sel typ t =
  t.Tlsa.cert_usage = usage &&
  t.Tlsa.selector = sel &&
  t.Tlsa.matching_type = typ

let is_csr t =
  tlsa_is Tlsa.Domain_issued_certificate Tlsa.Private Tlsa.No_hash t

let csr req =
  let data = X509.Signing_request.encode_der req in
  {
    Tlsa.matching_type = Tlsa.No_hash ;
    cert_usage = Tlsa.Domain_issued_certificate ;
    selector = Tlsa.Private ;
    data
  }

let is_certificate t =
  tlsa_is Tlsa.Domain_issued_certificate Tlsa.Full_certificate Tlsa.No_hash t

let certificate cert =
  let data = X509.Certificate.encode_der cert in
  {
    Tlsa.matching_type = Tlsa.No_hash ;
    cert_usage = Tlsa.Domain_issued_certificate ;
    selector = Tlsa.Full_certificate ;
    data
  }

let is_ca_certificate t =
  tlsa_is Tlsa.CA_constraint Tlsa.Full_certificate Tlsa.No_hash t

let ca_certificate data = {
  Tlsa.matching_type = Tlsa.No_hash ;
  cert_usage = Tlsa.CA_constraint ;
  selector = Tlsa.Full_certificate ;
  data
}

let signing_request hostname ?(more_hostnames = []) key =
  let host = Domain_name.to_string hostname in
  let extensions =
    match more_hostnames with
    | [] -> X509.Signing_request.Ext.empty
    | _ ->
      let ext =
        let additional = List.map Domain_name.to_string more_hostnames in
        let gn = X509.General_name.(singleton DNS (host :: additional)) in
        X509.Extension.(singleton Subject_alt_name (false, gn))
      in
      X509.Signing_request.Ext.(singleton Extensions ext)
  in
  X509.(Signing_request.create
          [Distinguished_name.(Relative_distinguished_name.singleton (CN host))]
          ~extensions key)

let dns_header rng =
  let id = Randomconv.int16 rng in
  (id, Packet.Flags.empty)

let le_label = "_letsencrypt"
and p_label = "_tcp"

let is_name name =
  if Domain_name.count_labels name < 2 then
    false
  else
    Domain_name.(equal_label le_label (get_label_exn name 0) &&
                 equal_label p_label (get_label_exn name 1))

let letsencrypt_name name =
  match Domain_name.(prepend_label (raw name) p_label) with
  | Ok name' -> Domain_name.prepend_label name' le_label
  | Error e -> Error e

type u_err = [ `Tsig of Dns_tsig.e | `Bad_reply of Packet.mismatch * Packet.t | `Unexpected_reply of Packet.reply ]

let pp_u_err ppf = function
  | `Tsig e -> Fmt.pf ppf "tsig error %a" Dns_tsig.pp_e e
  | `Bad_reply (e, res) -> Fmt.pf ppf "bad reply %a: %a" Packet.pp_mismatch e Packet.pp res
  | `Unexpected_reply r -> Fmt.pf ppf "unexpected reply %a" Packet.pp_reply r

let nsupdate rng now ~host ~keyname ~zone dnskey request =
  match letsencrypt_name host with
  | Error e -> Error e
  | Ok host ->
    let tlsa = csr request in
    let zone = Packet.Question.create zone Soa
    and update =
      let up =
        Domain_name.Map.singleton host
          [
            Packet.Update.Remove (K Tlsa) ;
            Packet.Update.Add (B (Tlsa, (3600l, Rr_map.Tlsa_set.singleton tlsa)))
          ]
      in
      (Domain_name.Map.empty, up)
    and header = dns_header rng
    in
    let packet = Packet.create header zone (`Update update) in
    let now = now () in
    match Dns_tsig.encode_and_sign ~proto:`Tcp packet now dnskey keyname with
    | Error e -> Error (`Msg (Fmt.to_to_string Dns_tsig.pp_s e))
    | Ok (data, mac) ->
      Ok (data, (fun data ->
          match Dns_tsig.decode_and_verify now dnskey keyname ~mac data with
          | Error e -> Error (`Tsig e)
          | Ok (res, _, _) ->
            match Packet.reply_matches_request ~request:packet res with
            | Ok `Update_ack -> Ok ()
            | Ok r -> Error (`Unexpected_reply r)
            | Error e -> Error (`Bad_reply (e, res))))

type q_err = [
  | `Decode of Packet.err
  | `Bad_reply of Packet.mismatch * Packet.t
  | `Unexpected_reply of Packet.reply
  | `No_tlsa
]

let pp_q_err ppf = function
  | `Decode err -> Fmt.pf ppf "decoding failed %a" Packet.pp_err err
  | `Bad_reply (e, res) -> Fmt.pf ppf "bad reply %a: %a" Packet.pp_mismatch e Packet.pp res
  | `Unexpected_reply r -> Fmt.pf ppf "unexpected reply %a" Packet.pp_reply r
  | `No_tlsa -> Fmt.pf ppf "No TLSA record found"

let tlsas_to_certchain host public_key tlsas =
  let certificates, ca_certificates =
    Rr_map.Tlsa_set.fold (fun tlsa (certs, cacerts as acc) ->
        if is_certificate tlsa || is_ca_certificate tlsa then
          match X509.Certificate.decode_der tlsa.Tlsa.data with
          | Error (`Msg msg) ->
            Logs.warn (fun m -> m "couldn't decode tlsa record %a: %s (%a)"
                          Domain_name.pp host msg
                          Cstruct.hexdump_pp tlsa.Tlsa.data);
            acc
          | Ok cert ->
            match is_certificate tlsa, is_ca_certificate tlsa with
            | true, _ -> (cert :: certs, cacerts)
            | _, true -> (certs, cert :: cacerts)
            | _ -> acc
        else acc)
      tlsas ([], [])
  in
  let matches_public_key cert =
    let key = X509.Certificate.public_key cert in
    Cstruct.equal (X509.Public_key.id key) (X509.Public_key.id public_key)
  in
  match List.find_opt matches_public_key certificates with
  | None -> Error `No_tlsa
  | Some server_cert ->
    match List.rev (X509.Validation.build_paths server_cert ca_certificates) with
    | (_server :: chain) :: _ -> Ok (server_cert, chain)
    | _ -> Ok (server_cert, []) (* build_paths always returns the server_cert *)

let query rng public_key host =
  match letsencrypt_name host with
  | Error e -> Error e
  | Ok host ->
    let header = dns_header rng
    and question = Packet.Question.create host Tlsa
    in
    let request = Packet.create header question `Query in
    let out, _ = Packet.encode `Tcp request
    and react data =
      match Packet.decode data with
      | Error e -> Error (`Decode e)
      | Ok reply ->
        match Packet.reply_matches_request ~request reply with
        | Ok (`Answer (answer, _)) ->
          begin match Name_rr_map.find host Tlsa answer with
            | None -> Error `No_tlsa
            | Some (_, tlsas) -> tlsas_to_certchain host public_key tlsas
          end
        | Ok (`Rcode_error (Rcode.NXDomain, Opcode.Query, _)) -> Error `No_tlsa
        | Ok reply -> Error (`Unexpected_reply reply)
        | Error e -> Error (`Bad_reply (e, reply))
    in
    Ok (out, react)
OCaml

Innovation. Community. Security.