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))