liminix/pkgs/tangc/tangc.fnl

188 lines
6.8 KiB
Fennel

(local json (require :json))
(local http (require :fetch))
(local { : base64 : %%} (require :anoia))
(local { : spawn } (require :anoia.process))
(local { : find-executable } (require :anoia.fs))
(local ll (require :lualinux))
(local CLEVIS_DEFAULT_THP_LEN 43) ; Length of SHA-256 thumbprint.
(local thumbprint-algs ["S256" "S1"])
(fn jose [params inputstr]
(var buf inputstr)
(let [env (ll.environ)
argv (doto params (table.insert 1 "jose"))
output []
exitstatus
(spawn
(find-executable "jose" (os.getenv "PATH")) argv envp
(fn [stream fd]
(match stream
:out (let [b (ll.read fd)]
(if (> (# b) 0)
(do (table.insert output b) true)
(do (ll.close fd) false)))
:in (let [b (string.sub buf (+ 1 (ll.write fd buf)) -1)]
(if (> (# b) 0)
(do (set buf b) true)
(do (ll.close fd) false))))))]
(values exitstatus (table.concat output))))
(fn jose! [params inputstr]
(let [(exitcode out) (jose params inputstr)]
(if (= exitcode 0)
(json.decode out)
(error (%% "jose %q failed (exit=%d): %q"
(table.concat params " ") exitcode out)))))
(fn josep! [params inputstr]
(let [(exitcode out) (jose params inputstr)]
(if (= exitcode 0)
out
(error (%% "jose %q failed (exit=%d): %q"
(table.concat params " ") exitcode out)))))
(fn has-key? [keys kid alg]
(jose! ["jwk" "thp" "-i-" "-f" kid "-a" alg] (json.encode keys)))
(fn search-key [keys kid]
(accumulate [ret nil
_ alg (ipairs thumbprint-algs)
&until ret]
(or ret (has-key? keys kid alg))))
(fn jwk-generate [crv]
(jose! ["jwk" "gen" "-i" (%% "{\"alg\":\"ECMR\",\"crv\":%q}" crv)] ""))
(fn jwk-pub [response]
(jose! ["jwk" "pub" "-i-"] (json.encode response)))
(fn jwk-exc-noi [clt eph]
(jose! ["jwk" "exc" "-l-" "-r-"]
(.. (json.encode clt) " " (json.encode eph))))
(fn jwk-exc [clt eph]
(jose! ["jwk" "exc" "-i" "{\"alg\":\"ECMR\"}" "-l-" "-r-"]
(.. (json.encode clt) " " (json.encode eph))))
(fn jwe-dec [jwk ph undigested]
;; sometimes jose jwe dec decrypts the file and exits
;; non-zero anyway. FIXME find out why
(let [inputstr (.. (json.encode jwk) ph "." undigested)
(exitcode out) (jose ["jwe" "dec" "-k-" "-i-"] inputstr)]
(if (> exitcode 0)
(: io.stderr :write (%% "jose jwe dec exited %d\n" exitcode)))
(if (not (= out ""))
out
(error (%% "jose jwe dec produced no output, exited %d" exitcode)))))
(fn parse-jwe [jwe]
(assert (= jwe.clevis.pin "tang") "invalid clevis.pin")
(assert jwe.clevis.tang.adv "no advertised keys")
(assert (>= (# jwe.kid) CLEVIS_DEFAULT_THP_LEN)
"tang using a deprecated hash for the JWK thumbprints")
(let [srv (search-key jwe.clevis.tang.adv jwe.kid)]
{
:kid jwe.kid
:clt (assert jwe.epk)
:crv (assert jwe.epk.crv "Unable to determine EPK's curve!")
:url (assert jwe.clevis.tang.url "no tang url")
:srv (assert srv
"JWE header validation of 'clevis.tang.adv' failed: key thumbprint does not match")
}))
(fn http-post [url body]
(match
(http.request "POST" url
"" 0
"application/x-www-form-urlencoded"
body)
s (json.decode s)
(nil code msg) (error (.. "Error " code " POST " url ": " msg))))
(fn http-get [url body]
(match
(http.fetch url)
s (json.decode s)
(nil code msg) (error (.. "Error " code " GET " url ": " msg))))
(fn decrypt []
(let [b64 (base64 :url)
raw (: (io.input) :read "*a")
(_ _ ph undigested) (string.find raw "(.-)%.(.+)")
jwe (json.decode (b64:decode ph))
{ : srv : crv : clt : kid : url} (parse-jwe jwe)
eph (jwk-generate crv)
xfr (jwk-exc clt eph)
response (http-post (.. url "/rec/" kid) (json.encode xfr))]
(assert (and (= response.kty "EC") (= response.crv crv))
"Received invalid server reply!")
(let [tmp (jwk-exc eph srv)
rep (jwk-pub response)
jwk (jwk-exc-noi rep tmp)]
(print (jwe-dec jwk ph undigested)))))
(fn perform-encryption [jwks url input]
(let [enc (jose! [:jwk :use "-i-" "-r" "-u" "deriveKey" "-o-"]
(json.encode jwks))
;; adding a -s to jwk use will "Always output a JWKSet" which
; ;presumably would make the following line redundant
enc_ (if enc.keys enc {:keys [enc]})]
(assert (= (# enc_.keys) 1)
(.. "Expected one exchange key, got " (# enc_.keys)))
(let [jwk (doto (. enc_.keys 1) (tset :key_ops nil) (tset :alg nil))
kid (josep! [:jwk :thp "-i-" "-a" (. thumbprint-algs 1)]
(json.encode jwk))
jwe {:protected {
:alg "ECDH-ES"
:enc "A256GCM"
:kid kid
:clevis {:pin "tang"
:tang {:url url :adv jwks }}}}]
(josep! [:jwe :enc "-i-" "-k-" "-I-" "-c"]
(.. (json.encode jwe) (json.encode jwk) input)))))
(fn usage []
(print "tangc\n=====\n")
(print "tangc decrypt < filename.enc # decrypt")
(print (%% "tangc encrypt %q # print available keys"
(json.encode {:url "http://tang.local"})))
(print (%% "tangc encrypt %q < plaintext > filename.enc # encrypt"
(json.encode {:thp "idGFpbiBhIHByZWJ1aWx0IGRhdGFiYXNlIGZyb20gaH"
:url "http://tang.local"})))
(os.exit 1))
(fn encrypt [cfg]
(let [{ : url : thp : adv } cfg
_ (or url (usage))
raw-input (: (io.input) :read "*a")
b64 (base64 :url)
adv (or adv (http-get (.. url "/adv/" (or thp ""))))]
(assert adv.payload "advertisement is malformed")
(let [jwks (json.decode (b64:decode adv.payload))
ver (jose! [:jwk :use "-i-" "-r" "-u" "verify" "-o-"]
(json.encode jwks))]
(match
(josep! [:jws :ver "-i" (json.encode adv) "-k-" "-a"] (json.encode ver))
"" nil
str (error "jws verify of advertised keys failed: " str))
(if (and thp (search-key ver thp))
(: (io.output) :write (perform-encryption jwks url raw-input))
;; the command line options are currently the same as clevis
;; but unless I can greatly improve this wording, that's gonna change
(print (.. "\ntangc: Thumbprints of advertised keys are listed below. Rerun this command\nproviding the thp attribute to specify the preferred key\n\n"
(josep! [:jwk :thp "-i-" "-a" (. thumbprint-algs 1)] (json.encode ver))))))))
(fn run []
(case arg
["decrypt"] (decrypt)
["encrypt" cfg] (encrypt (json.decode cfg))
_ (usage)))
{ : run }