-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
main.clj
441 lines (387 loc) · 17.8 KB
/
main.clj
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
(ns certifiable.main
(:require
[clojure.java.shell :refer [sh]]
[clojure.java.io :as io]
[clojure.string :as string]
[clojure.tools.cli :refer [parse-opts]]
[clojure.edn :as edn]
[certifiable.sha :refer [sha-signature-short]]
[certifiable.util :as util]
[certifiable.log :as log]
[certifiable.keytool :refer [keytool keytool-cmd]]
[certifiable.macos-trust :as macos-trust]
[certifiable.nss-trust :as nss-trust]
[clojure.pprint])
(:gen-class))
;; this isn't hidden so that users easily add trust manually and dont' have
;; to navigate hidden files from inside a gui file system explorer
(def ^:dynamic *ca-dir* (io/file (System/getProperty "user.home") "_certifiable_certs"))
(def ^:dynamic *password* "password")
(def ^:dynamic *root-keystore-name* "dev-root.jks")
(def ^:dynamic *root-pem-name* "dev-root-trust-this.pem")
(def ^:dynamic *ca-keystore-name* "intermediate-certificate-authority.jks")
(def ^:dynamic *ca-pem-name* "intermediate-certificate-authority.pem")
(def ^:dynamic *server-keystore-name* "dev-server.jks")
(def ^:dynamic *server-pem-name* "dev-server.pem")
(def ^:dynamic *info-name* "info.edn")
(defn dname [{:keys [stable-name]}]
(format "cn=Certifiable dev root (%s)" stable-name))
(defn file-paths [{:keys [stable-name]}]
{:root-keystore-path (io/file *ca-dir* stable-name *root-keystore-name*)
:ca-keystore-path (io/file *ca-dir* stable-name *ca-keystore-name*)
:root-pem-path (io/file *ca-dir* stable-name *root-pem-name*)
:ca-pem-path (io/file *ca-dir* stable-name *ca-pem-name*)
:server-keystore-path (io/file *ca-dir* stable-name *server-keystore-name*)
:server-pem-path (io/file *ca-dir* stable-name *server-pem-name*)})
(defn gen-key-pair [args-map]
(let [base-args (merge {:validity 10000
:keyalg :RSA
:keysize 2048
:keypass *password*
:storepass *password*}
args-map)]
(assert (:alias base-args))
(assert (:dname base-args))
(assert (:keystore base-args))
(keytool :genkeypair base-args)))
(defn delete-directory [f]
(doseq [f (reverse (file-seq f))]
(when (.exists f)
(.delete f))))
(defn clean! []
(delete-directory (io/file *ca-dir*)))
(defn gen-third-party-ca [opts]
(let [{:keys [root-keystore-path
ca-keystore-path
root-pem-path
ca-pem-path]} (file-paths opts)]
(try
(io/make-parents root-keystore-path)
;; gen keypairs
(log/info "Generating root and ca keypairs")
;; keytool -genkeypair -alias root -dname "cn=Local Network - Development" -validity 10000 -keyalg RSA -keysize 2048 -ext bc:c -keystore root.jks -keypass password -storepass password
(gen-key-pair {:alias :root
:dname (dname opts)
:ext "bc:c"
:keystore root-keystore-path})
;; keytool -genkeypair -alias ca -dname "cn=Local Network - Development" -validity 10000 -keyalg RSA -keysize 2048 -ext bc:c -keystore ca.jks -keypass password -storepass password
(gen-key-pair {:alias :ca
:dname (dname opts)
:ext "bc:c"
:keypass *password*
:keystore ca-keystore-path})
(log/info (str "Generating root certificate: " root-pem-path))
;; generate root certificate
;; keytool -exportcert -rfc -keystore root.jks -alias root -storepass password > root.pem
(->>
(keytool :exportcert [:rfc
:keystore root-keystore-path
:alias :root
:storepass *password*])
:out
(spit root-pem-path))
;; generate a certificate for ca signed by root (root -> ca)
;; keytool -keystore ca.jks -storepass password -certreq -alias ca \
;; | keytool -keystore root.jks -storepass password -gencert -alias root -ext bc=0 -ext san=dns:ca -rfc > ca.pem
(log/info "Generating ca certificate signed by root")
(->> (keytool :certreq
{:keystore ca-keystore-path
:storepass *password*
:alias :ca})
:out
(keytool :gencert
[:keystore root-keystore-path
:storepass *password*
:alias :root
:rfc
:ext "bc=0"
:ext "san=dns:ca"])
:out
(spit ca-pem-path))
;; import ca cert chain into ca.jks
(log/info "Importing root and ca chain into ca.jks keystore")
;; keytool -keystore ca.jks -storepass password -importcert -trustcacerts -noprompt -alias root -file root.pem
(keytool :importcert [:keystore ca-keystore-path
:storepass *password*
:trustcacerts
:noprompt
:alias :root
:file root-pem-path])
;; keytool -keystore ca.jks -storepass password -importcert -alias ca -file ca.pem
(keytool :importcert {:keystore ca-keystore-path
:storepass *password*
:alias :ca
:file ca-pem-path})
(finally
(when (.exists root-keystore-path)
(.delete root-keystore-path)
(log/info (str "Deleted trusted root certificate keys: " root-keystore-path)))))))
(defn domains-and-ips->san [{:keys [domains ips]}]
;; "san=dns:localhost,dns:www.localhost,dns:martian.test,ip:127.0.0.1"
(string/join ","
(concat (map #(str "dns:" %) domains)
(map #(str "ip:" %) ips))))
(defn generate-jks-for-domain [{:keys [keystore-path domains ips] :as opts}]
(let [{:keys [ca-keystore-path
server-keystore-path
root-pem-path
ca-pem-path
server-pem-path]} (file-paths opts)]
(try
(assert (.exists root-pem-path))
(assert (.exists ca-keystore-path))
(assert (.exists ca-pem-path))
;; generate private keys (for server)
;; keytool -genkeypair -alias server -dname cn=server -validity 10000 -keyalg RSA -keysize 2048 -keystore my-keystore.jks -keypass password -storepass password
(log/info "Generate private keys for server")
(gen-key-pair {:alias :server
:dname "cn=CertifiableLeafCert"
:keystore server-keystore-path})
;; generate a certificate for server signed by ca (root -> ca -> server)
;; keytool -keystore my-keystore.jks -storepass password -certreq -alias server \
;; | keytool -keystore ca.jks -storepass password -gencert -alias ca -ext ku:c=dig,keyEnc -ext "san=dns:localhost,ip:127.0.0.1" -ext eku=sa,ca -rfc > server.pem
(log/info "Generate a certificate for server signed by ca")
(->> (keytool :certreq {:keystore server-keystore-path
:storepass *password*
:alias :server})
:out
(keytool :gencert
[:alias :ca
:keystore ca-keystore-path
:storepass *password*
:rfc
:ext "ku:c=dig,keyEnc"
:ext (str "san=" (domains-and-ips->san opts))
:ext "eku=sa,ca"])
:out
(spit server-pem-path))
;; import server cert chain into my-keystore.jks
(log/info (str "Importing complete chain into keystore at: " server-keystore-path))
;; keytool -keystore my-keystore.jks -storepass password -importcert -trustcacerts -noprompt -alias root -file root.pem
(keytool :importcert [:keystore server-keystore-path
:storepass *password*
:trustcacerts
:noprompt
:alias :root
:file root-pem-path])
;; keytool -keystore my-keystore.jks -storepass password -importcert -alias ca -file ca.pem
(keytool :importcert {:keystore server-keystore-path
:storepass *password*
:alias :ca
:file ca-pem-path})
;; keytool -keystore my-keystore.jks -storepass password -importcert -alias server -file server.pem
(keytool :importcert {:keystore server-keystore-path
:storepass *password*
:alias :server
:file server-pem-path})
(finally
(when (.exists ca-keystore-path)
(.delete ca-keystore-path))
(when (.exists ca-pem-path)
(.delete ca-pem-path))
(when (.exists server-pem-path)
(.delete server-pem-path))
(log/info (str "Deleted intermediate certificate authority keys: " ca-keystore-path))))
(log/info (str "Generated Java Keystore file: " server-keystore-path))))
(defn stable-name [{:keys [keystore-path domains ips] :as opts}]
(let [domains-ips (concat (sort domains)
(sort ips))
n (first domains-ips)
sig (sha-signature-short (string/join "," domains-ips))]
(str n "-" sig)))
(defn dev-cert-exists? [opts]
(let [{:keys [root-pem-path server-keystore-path]} (file-paths opts)]
(and (.exists root-pem-path)
(.exists server-keystore-path))))
(defn meta-data [{:keys [domains ips stable-name]}]
{:created (java.util.Date.)
:domains domains
:stable-name stable-name
:ips ips})
(defn info-file [opts]
(io/file *ca-dir* (:stable-name opts) *info-name*))
(defn emit-meta-data [opts]
(spit (info-file opts)
(with-out-str (clojure.pprint/pprint (meta-data opts)))))
(defn emit-keystore [{:keys [keystore-path stable-name]}]
(when keystore-path
(log/info "Outputing Java Keystore to:" (str keystore-path))
(io/copy
(io/file *ca-dir* stable-name *server-keystore-name*)
(io/file keystore-path))))
#_(clean!)
#_(create-dev-certificate-jks {})
(defn install-to-trust-stores! [pem-path]
{:system-trust-installed
(when (= :macos (util/os?))
(macos-trust/install-trust! pem-path))
:firefox-trust-installed
(nss-trust/install-trust! pem-path)})
(defn remove-trust [pem-path]
(when (and (= :macos (util/os?))
(macos-trust/has-cert? pem-path))
(log/info "Removing trust from MacOS keychain for " (str pem-path))
(macos-trust/remove-cert pem-path))
(if (nss-trust/has-cert? pem-path)
(log/info "Removing trust from Firefox NSS trust store for" (str pem-path))
(nss-trust/remove-cert pem-path)))
(defn remove-cert [{:keys [stable-name root-pem-path] :as cert-infot}]
(remove-trust root-pem-path)
(delete-directory (io/file *ca-dir* stable-name)))
(declare list-keystores)
(defn remove-all []
(doseq [cert-info (list-keystores *ca-dir*)]
(log/info "Removing:" (:stable-name cert-info))
(remove-cert cert-info)))
(defn final-instructions [{:keys [keystore-path] :as opts} trust-installed]
(let [{:keys [server-keystore-path
root-pem-path]} (file-paths opts)
path (or keystore-path (str server-keystore-path))
{:keys [system-trust-installed
firefox-trust-installed]} trust-installed]
(str
"--------------------------- Setup Instructions ---------------------------\n"
"Local dev Java keystore generated at: " path "\n"
"The keystore type is: \"JKS\"\n"
"The keystore password is: \"" *password* "\"\n"
"The root certificate is: " root-pem-path "\n"
"For System: root certificate " (if system-trust-installed
"is trusted"
"needs to have trust set up") "\n"
"For Firefox: root certificate " (if firefox-trust-installed
"is trusted"
"needs to have trust set up") "\n"
"Example SSL Configuration for ring.jetty.adapter/run-jetty:\n"
(with-out-str (clojure.pprint/pprint
{:ssl? true
:ssl-port 9533
:keystore path
:key-password *password*})))))
(declare keystore-info)
(defn create-dev-certificate-jks [{:keys [keystore-path domains ips print-instructions?] :as opts}]
(let [opts (merge {:domains ["localhost" "www.localhost"]
:ips ["127.0.0.1"]}
opts)
stable-name' (stable-name opts)
opts (assoc opts :stable-name stable-name')]
(if-not (keytool-cmd)
(do
(log/info "ERROR: Can not find keytool Java command.")
(println "Please check your Java installation and ensure that keytool is on your path."))
(do
(if-not (dev-cert-exists? opts)
(do (gen-third-party-ca opts)
(generate-jks-for-domain opts)
(emit-meta-data opts)
(let [trust-info (install-to-trust-stores! (io/file *ca-dir* stable-name' *root-pem-name*))]
(when print-instructions?
(println (final-instructions opts trust-info)))
(emit-keystore opts)
(keystore-info opts)))
(do
(log/info (str "Root certificate and keystore already exists for " (:stable-name opts)))
(let [trust-info (install-to-trust-stores! (io/file *ca-dir* stable-name' *root-pem-name*))]
(when print-instructions?
(println (final-instructions opts trust-info)))
(emit-keystore opts)
(keystore-info opts))))
))))
(def cli-options
[["-d" "--domains DOMAINS" "A comma seperated list of domains to be included in certificate"
:default ["localhost" "www.localhost"]
:default-desc "localhost,www.localhost"
:parse-fn #(mapv string/trim (string/split % #","))]
["-i" "--ips IPS" "A comma seperated list of IP addresses to be included in certificate"
:default ["127.0.0.1"]
:default-desc "127.0.0.1"
:parse-fn #(mapv string/trim (string/split % #","))
:validate [#(every?
(partial re-matches #"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}")
%)
"Must be a comma seperated list of IP address"]]
["-o" "--output FILE" "The path and filename of the jks output file"
:id :keystore-path]
[nil "--reset" "Start from scratch, deletes root and certificate authority certs"]
["-h" "--help"]
["-v" nil "verbose - outputs more info about keytool calls"
:id :verbosity
:default false]])
(defn usage [options-summary]
(->> ["Generates a local developement Java keystore that can be used
to support SSL/HTTPS connections in a Java Server like Jetty."
""
"Usage: clj -m certifiable.main [options]"
""
"Options:"
options-summary]
(string/join \newline)))
(defn list-dirs [dir]
(let [dir (io/file dir)]
(when (.isDirectory dir)
(filter
#(.isDirectory %)
(seq (.listFiles dir))))))
(defn keystore-info [{:keys [stable-name] :as opts}]
(let [info-file (info-file opts)
data (if (.exists info-file)
(try
(edn/read-string (slurp info-file))
(catch Throwable t)))
{:keys [root-pem-path server-keystore-path]} (file-paths opts)]
(assoc data
:password *password*
:root-pem-path (str root-pem-path)
:server-keystore-path (str server-keystore-path))))
(defn list-keystores [ca-dir]
(mapv keystore-info (map #(hash-map :stable-name %)
(sort (map #(.getName %)
(list-dirs *ca-dir*))))))
#_(list-keystores *ca-dir*)
(defn list-command [ca-dir]
(let [keystores (list-keystores ca-dir)]
(if (empty? keystores)
(println "No installed keystores found")
(println "Keystores found in directory: " (str ca-dir)))
(doall
(map-indexed
#(println (format "%d. %s [%s]"
(inc %1) (:stable-name %2)
(string/join ", " (concat (sort (:domains %2))
(sort (:ips %2)))) ))
(list-keystores ca-dir)))))
(defn -main [& args]
(try
(let [options (parse-opts args cli-options)
err? (or (:errors options)
(not-empty (:arguments options)))]
(log/debug (str "Args:\n"
(with-out-str (clojure.pprint/pprint (:options options)))))
(binding [log/*log-level* (if (:verbosity (:options options))
:all
:info)
*out* *err*]
(cond
(= (first (:arguments options)) "list") (list-command *ca-dir*)
:else
(do
(doseq [err (:errors options)]
(println err))
(doseq [arg (:arguments options)]
(println "Unknown option:" arg))
(cond
(or err?
(-> options :options :help))
(println (usage (:summary options)))
(-> options :options :reset)
(do
(log/info "Resetting: Deleting all certificates!")
(remove-all))
:else
(do
(create-dev-certificate-jks (:options options))))))))
(finally
(shutdown-agents))))
;; keytool -importkeystore -srckeystore /Users/bhauman/.certifiable_dev_certs/certificate-authority.jks -destkeystore /Users/bhauman/.certifiable_dev_certs/certificate-authority.jks -deststoretype pkcs12
;; keytool -importkeystore -srckeystore localhost.p12 -srcstoretype pkcs12 -destkeystore dev-localhost.jks -deststoretype jks
;; converting from a
;; keytool -importkeystore -srckeystore localhost.p12 -srcstoretype pkcs12 -destkeystore dev-localhost.jks -deststoretype jks