certifix

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

main.fnl (5189B)


      1 (local { : view} (require :fennel))
      2 
      3 (local server (require :http.server))
      4 (local headers (require :http.headers))
      5 (local htls (require :http.tls))
      6 (local ctx (require :openssl.ssl.context))
      7 (local csr (require :openssl.x509.csr))
      8 (local x509 (require :openssl.x509))
      9 (local extension (require :openssl.x509.extension))
     10 (local pkey (require :openssl.pkey))
     11 (local bignum (require :openssl.bignum))
     12 
     13 ;; ncall is the opposite of pcall: "non-protected call"
     14 (macro ncall [f]
     15   `(case ,f
     16      ok# ok#
     17      (nil err#) (error err#)))
     18 
     19 (fn string->bignum [bytes]
     20   (bignum.new
     21    (string.format
     22     "0x%03x%03x%03x%03x%03x"
     23     (string.unpack "I4I4I4I4I4" bytes))))
     24 
     25 (fn make-serial []
     26   ;; 20 bytes, but luaossl expects it as a bignum
     27   (let [bytes (with-open [f (io.open "/dev/urandom" :r)]
     28                 (f:read 20))]
     29     (string->bignum bytes)))
     30 
     31 (fn make-headers [status attributes]
     32   (let [h (headers.new)]
     33     (h:append ":status" (tostring status))
     34     (each [k v (pairs attributes)]
     35       (h:append k v))
     36     h))
     37 
     38 (fn send-error [out code text]
     39   (out:write_headers
     40    (make-headers code { :content-type "text/plain" })
     41    false)
     42   (out:write_chunk text true))
     43 
     44 (fn slurp [filename]
     45   (with-open [f (ncall (io.open filename "r"))] (f:read "*a")))
     46 
     47 (fn read-line [filename]
     48   (with-open [f (ncall (io.open filename "r"))] (f:read "l")))
     49 
     50 (fn assoc [tbl k v & more]
     51   (tset tbl k v)
     52   (case more
     53     [k v] (assoc tbl k v)
     54     _ tbl))
     55 
     56 (fn parse-args [args]
     57   (match args
     58     ["--ca-certificate" f & rest]
     59     (assoc (parse-args rest) :ca-certificate (slurp f))
     60     ["--ca-private-key" f & rest]
     61     (assoc (parse-args rest) :ca-private-key (slurp f))
     62     ["--server-certificate" f & rest]
     63     (assoc (parse-args rest) :server-certificate (slurp f))
     64     ["--server-private-key" f & rest]
     65     (assoc (parse-args rest) :server-private-key (slurp f))
     66     ["--challenge-password" f & rest]
     67     (assoc (parse-args rest) :challenge-password (read-line f))
     68     ["--sign-file" f & rest]
     69     (assoc (parse-args rest) :sign-and-exit (slurp f))
     70     [bind-address] { : bind-address }
     71     _ {}))
     72 
     73 (local options
     74        (doto
     75            (parse-args arg)
     76          (case
     77              {:sign-and-exit pathname }
     78            true
     79              {: ca-certificate : ca-private-key
     80               : server-certificate : server-private-key
     81               : challenge-password : bind-address}
     82            true
     83            _
     84            (assert nil "missing required command line params"))))
     85 
     86 (local ca-key (pkey.new options.ca-private-key))
     87 (local ca-crt (x509.new options.ca-certificate))
     88 
     89 (fn new-crt [csr]
     90   (let [crt
     91         (doto (x509.new)
     92           (: :setVersion 3)
     93           (: :setSerial (make-serial))
     94           (: :setIssuer (ca-crt:getSubject))
     95           (: :setLifetime (os.time) (+ (* 365 86400) (os.time)))
     96           (: :setSubject (csr:getSubject))
     97           (: :setPublicKey (csr:getPublicKey)))]
     98     (for [i 1 (csr:getRequestedExtensionCount) 1]
     99       (let [ext (csr:getRequestedExtension i)]
    100         (crt:addExtension ext)))
    101     ;; https://www.golinuxcloud.com/add-x509-extensions-to-certificate-openssl/
    102     (when (not (crt:getExtension "extendedKeyUsage"))
    103       (crt:addExtension (extension.new "extendedKeyUsage" "clientAuth")))
    104     (doto crt
    105       (: :addExtension (extension.new "basicConstraints" "critical,CA:FALSE"))
    106       (: :addExtension (extension.new "keyUsage" "digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment")) ;; all of these?
    107       (: :sign ca-key))))
    108 
    109 (fn approve-request? [csr]
    110   (let [challengePassword (csr:getAttribute :challengePassword)]
    111     (when challengePassword
    112       (accumulate [found false
    113                    _ v (ipairs challengePassword)]
    114         (or found (= v options.challenge-password))))))
    115 
    116 (fn handle-sign-csr [out]
    117   (let [req (csr.new (out:get_body_as_string))]
    118     (if (approve-request? req)
    119         (do
    120           (out:write_headers (make-headers 200 { :content-type "text/plain" }) false)
    121           (out:write_chunk (: (new-crt req) :toPEM) true))
    122         (send-error out 400 "missing attributes in CSR"))))
    123 
    124 (fn on-stream [sv out]
    125   (let [hdrs (out:get_headers)
    126         method (hdrs:get ":method")
    127         path (or (hdrs:get ":path") "/")]
    128     (case path
    129       "/sign"
    130       (handle-sign-csr out)
    131       _
    132       (send-error out 404 "not found"))
    133     (out:shutdown)))
    134 
    135 (fn ssl-context []
    136   (doto (htls.new_server_context)
    137     (: :setCertificate (x509.new options.server-certificate))
    138     (: :setPrivateKey (pkey.new options.server-private-key))))
    139 
    140 (fn new-server []
    141   (let [(addr port) (string.match options.bind-address "(.+):(%d+)$")]
    142     (ncall (server.listen
    143            {
    144             :host addr
    145             :port (tonumber port)
    146             :onstream on-stream
    147             :onerror (fn [server ctx op err errno]
    148                        (print (view { : server : ctx : err : errno })))
    149             :tls true
    150             :ctx (ssl-context)
    151             }))))
    152 
    153 (fn run-server []
    154   (let [s (new-server)]
    155     (ncall (s:listen))
    156     (print "server ready")
    157     (ncall (s:loop))))
    158 
    159 (fn sign-text [s]
    160   (let [crt (new-crt (csr.new s))]
    161     (print (crt:toPEM))))
    162 
    163 
    164 (if options.sign-and-exit
    165     (sign-text options.sign-and-exit)
    166     (run-server))