1
1
(ns puppetlabs.puppetserver.certificate-authority
2
- (:import (java.io BufferedReader BufferedWriter FileNotFoundException InputStream ByteArrayOutputStream ByteArrayInputStream File Reader StringReader IOException)
2
+ (:require [clj-time.coerce :as time-coerce]
3
+ [clj-time.core :as time]
4
+ [clj-time.format :as time-format]
5
+ [clojure.java.io :as io]
6
+ [clojure.set :as set]
7
+ [clojure.string :as str]
8
+ [clojure.tools.logging :as log]
9
+ [me.raynes.fs :as fs]
10
+ [puppetlabs.i18n.core :as i18n]
11
+ [puppetlabs.kitchensink.core :as ks]
12
+ [puppetlabs.kitchensink.file :as ks-file]
13
+ [puppetlabs.puppetserver.common :as common]
14
+ [puppetlabs.puppetserver.ringutils :as ringutils]
15
+ [puppetlabs.puppetserver.shell-utils :as shell-utils]
16
+ [puppetlabs.ssl-utils.core :as utils]
17
+ [schema.core :as schema]
18
+ [slingshot.slingshot :as sling])
19
+ (:import (java.io BufferedReader BufferedWriter ByteArrayInputStream ByteArrayOutputStream File FileNotFoundException IOException InputStream Reader StringReader)
3
20
(java.nio CharBuffer)
4
21
(java.nio.file Files)
5
22
(java.nio.file.attribute FileAttribute PosixFilePermissions)
6
23
(java.security PrivateKey PublicKey)
7
- (java.security.cert X509Certificate CRLException CertPathValidatorException X509CRL)
24
+ (java.security.cert CRLException CertPathValidatorException X509CRL X509Certificate )
8
25
(java.text SimpleDateFormat)
9
26
(java.time LocalDateTime ZoneId)
10
27
(java.util Date)
11
28
(java.util.concurrent.locks ReentrantReadWriteLock)
12
29
(org.apache.commons.io IOUtils)
13
30
(org.bouncycastle.pkcs PKCS10CertificationRequest)
14
- (org.joda.time DateTime))
15
- (:require [me.raynes.fs :as fs]
16
- [schema.core :as schema]
17
- [clojure.string :as str]
18
- [clojure.set :as set]
19
- [clojure.java.io :as io]
20
- [clojure.tools.logging :as log]
21
- [clj-time.core :as time]
22
- [clj-time.format :as time-format]
23
- [clj-time.coerce :as time-coerce]
24
- [slingshot.slingshot :as sling]
25
- [puppetlabs.kitchensink.core :as ks]
26
- [puppetlabs.kitchensink.file :as ks-file]
27
- [puppetlabs.puppetserver.common :as common]
28
- [puppetlabs.puppetserver.ringutils :as ringutils]
29
- [puppetlabs.ssl-utils.core :as utils]
30
- [puppetlabs.puppetserver.shell-utils :as shell-utils]
31
- [puppetlabs.i18n.core :as i18n]))
31
+ (org.joda.time DateTime)))
32
32
33
33
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
34
34
; ;; Public utilities
718
718
(log/trace (i18n/trs " Finish append to serial file. " )))]
719
719
(ks-file/atomic-write infra-node-serials-path stream-content-fn)))))))
720
720
721
+ (defn stream-content-to-file
722
+ [^String cert-inventory ^String entry ^BufferedWriter writer]
723
+ (log/trace (i18n/trs " Begin append to inventory file." ))
724
+ (let [copy-buffer (CharBuffer/allocate buffer-copy-size)]
725
+ (try
726
+ (with-open [^BufferedReader reader (io/reader cert-inventory)]
727
+ ; ; copy all the existing content
728
+ (loop [read-length (.read reader copy-buffer)]
729
+ ; ; theoretically read can return 0, which means try again
730
+ (when (<= 0 read-length)
731
+ (when (pos? read-length)
732
+ (.write writer (.array copy-buffer) 0 read-length))
733
+ (.clear copy-buffer)
734
+ (recur (.read reader copy-buffer)))))
735
+ (catch FileNotFoundException _e
736
+ (log/trace (i18n/trs " Inventory file not found. Assume empty." )))
737
+ (catch Throwable e
738
+ (log/error e (i18n/trs " Error while appending to inventory file." ))
739
+ (throw e))))
740
+ (.write writer entry)
741
+ (.flush writer)
742
+ (log/trace (i18n/trs " Finish append to inventory file. " )))
743
+
744
+
721
745
(schema/defn ^:always-validate
722
- write-cert-to-inventory!
746
+ write-cert-to-inventory-unlocked !
723
747
" Writes an entry into Puppet's inventory file for a given certificate.
724
748
The location of this file is defined by Puppet's 'cert_inventory' setting.
725
749
The inventory is a text file where each line represents a certificate in the
733
757
* $NA = The 'not after' field of the cert, as a date/timestamp in UTC.
734
758
* $S = The distinguished name of the cert's subject."
735
759
[cert :- Certificate
736
- {:keys [cert-inventory inventory-lock inventory-lock-timeout-seconds ] :as settings} :- CaSettings]
760
+ {:keys [cert-inventory] :as settings} :- CaSettings]
737
761
(let [serial-number (.getSerialNumber cert)
738
762
formatted-serial-number (->> serial-number
739
- (format-serial-number )
740
- (str " 0x" ))
763
+ (format-serial-number )
764
+ (str " 0x" ))
741
765
not-before (-> cert
742
766
(.getNotBefore )
743
767
(format-date-time ))
746
770
(format-date-time ))
747
771
subject (utils/get-subject-from-x509-certificate cert)
748
772
cert-name (utils/x500-name->CN subject)
749
- entry (str formatted-serial-number " " not-before " " not-after " /" subject " \n " )
750
- stream-content-fn ( fn [^BufferedWriter writer]
751
- ( log/trace ( i18n/trs " Begin append to inventory file. " ))
752
- ( let [copy-buffer ( CharBuffer/allocate buffer-copy-size)]
753
- ( try
754
- ( with-open [^BufferedReader reader ( io/reader cert-inventory)]
755
- ; ; copy all the existing content
756
- ( loop [read-length ( .read reader copy-buffer)]
757
- ; ; theoretically read can return 0, which means try again
758
- ( when ( <= 0 read-length)
759
- ( when ( pos? read-length)
760
- ( .write writer ( .array copy-buffer) 0 read-length))
761
- ( .clear copy-buffer)
762
- ( recur ( .read reader copy-buffer)))))
763
- ( catch FileNotFoundException _e
764
- ( log/trace ( i18n/trs " Inventory file not found. Assume empty. " )))
765
- ( catch Throwable e
766
- ( log/error e ( i18n/trs " Error while appending to inventory file. " ))
767
- ( throw e))))
768
- ( .write writer entry)
769
- ( .flush writer)
770
- ( log/trace ( i18n/trs " Finish append to inventory file. " ))) ]
773
+ entry (str formatted-serial-number " " not-before " " not-after " /" subject " \n " )]
774
+ ( log/debug ( i18n/trs " Append \" {1} \" to inventory file {0} " cert-inventory entry))
775
+ ( ks-file/atomic-write cert-inventory ( partial stream-content-to-file cert- inventory entry ))
776
+ ( maybe-write-to-infra-serial! serial-number cert-name settings)))
777
+
778
+ ( schema/defn ^:always-validate
779
+ write-cert-to-inventory!
780
+ " Same behavior as `write-cert-to-inventory-unlocked! but acquires the inventory lock prior to doing the work.
781
+ Writes an entry into Puppet's inventory file for a given certificate.
782
+ The location of this file is defined by Puppet's 'cert_inventory' setting.
783
+ The inventory is a text file where each line represents a certificate in the
784
+ following format:
785
+ $SN $NB $NA /$S
786
+ where:
787
+ * $SN = The serial number of the cert. The serial number is formatted as a
788
+ hexadecimal number, with a leading 0x, and zero-padded up to four
789
+ digits, eg. 0x002f.
790
+ * $NB = The 'not before' field of the cert, as a date/timestamp in UTC.
791
+ * $NA = The 'not after' field of the cert, as a date/timestamp in UTC.
792
+ * $S = The distinguished name of the cert's subject. "
793
+ [cert :- Certificate
794
+ { :keys [inventory-lock inventory-lock-timeout-seconds] :as settings} :- CaSettings ]
771
795
(common/with-safe-write-lock inventory-lock inventory-lock-descriptor inventory-lock-timeout-seconds
772
- (log/debug (i18n/trs " Append \" {1}\" to inventory file {0}" cert-inventory entry))
773
- (ks-file/atomic-write cert-inventory stream-content-fn)
774
- (maybe-write-to-infra-serial! serial-number cert-name settings))))
796
+ (write-cert-to-inventory-unlocked! cert settings)))
775
797
776
798
(schema/defn is-subject-in-inventory-row? :- schema /Bool
777
799
[cn-subject :- utils/ValidX500Name
797
819
; ; lack of an end date means we can't tell if it is expired or not, so assume it isn't.
798
820
false ))
799
821
800
-
801
822
(defn extract-inventory-row-contents
802
823
[row]
803
824
(str/split row #" " ))
1776
1797
(validate-csr-signature! csr)
1777
1798
(autosign-certificate-request! subject csr settings report-activity)))))
1778
1799
1779
-
1780
1800
(schema/defn ^:always-validate
1781
1801
get-certificate-revocation-list :- schema /Str
1782
1802
" Given the value of the 'cacrl' setting from Puppet,
2400
2420
(when (and enable-infra-crl (fs/exists? infra-crl-path))
2401
2421
(when (crl-expires-in-n-days? infra-crl-path settings crl-expiration-window-days)
2402
2422
(log/info (i18n/trs " infra crl expiring within 30 days, updating." ))
2403
- (update-and-sign-crl! infra-crl-path settings))))
2423
+ (update-and-sign-crl! infra-crl-path settings))))
2424
+
2425
+ (schema/defn maybe-sign-one :- (schema/enum :signed :not-signed )
2426
+ [subject :- schema/Str
2427
+ csr-path :- schema/Str
2428
+ cacert :- Certificate
2429
+ casubject :- schema/Str
2430
+ ca-private-key :- PrivateKey
2431
+ {:keys [signeddir ca-ttl allow-auto-renewal allow-subject-alt-names
2432
+ allow-authorization-extensions auto-renewal-cert-ttl] :as ca-settings} :- CaSettings]
2433
+ (try
2434
+ (let [csr (utils/pem->csr csr-path)
2435
+ renewal-ttl (if (and allow-auto-renewal (supports-auto-renewal? csr))
2436
+ auto-renewal-cert-ttl
2437
+ ca-ttl)
2438
+ _ (log/debug (i18n/trs " Calculating validity dates from ttl of {0} " renewal-ttl))
2439
+ validity (cert-validity-dates renewal-ttl)]
2440
+ ; ; these ensure/validate functions throw exceptions if the criteria isn't met
2441
+ (ensure-subject-alt-names-allowed! csr allow-subject-alt-names)
2442
+ (ensure-no-authorization-extensions! csr allow-authorization-extensions)
2443
+ (validate-extensions! (utils/get-extensions csr))
2444
+ (validate-csr-signature! csr)
2445
+ (let [signed-cert (utils/sign-certificate casubject
2446
+ ca-private-key
2447
+ (next-serial-number! ca-settings)
2448
+ (:not-before validity)
2449
+ (:not-after validity)
2450
+ (utils/cn subject)
2451
+ (utils/get-public-key csr)
2452
+ (create-agent-extensions csr cacert))]
2453
+ (write-cert-to-inventory-unlocked! signed-cert ca-settings)
2454
+ (write-cert signed-cert (path-to-cert signeddir subject))
2455
+ (delete-certificate-request! ca-settings subject)
2456
+ ; ; success case, add the host to the set of signed results
2457
+ :signed ))
2458
+ (catch Throwable e
2459
+ (log/debug e (i18n/trs " Failed in bulk signing for entry {0}" subject))
2460
+ ; ; failure case, add the host to the set of not signed results
2461
+ :not-signed )))
2462
+
2463
+ (schema/defn ^:always-validate
2464
+ sign-multiple-certificate-signing-requests! :- {:signed [schema/Str]
2465
+ :not-signed [schema/Str]}
2466
+ [subjects :- [schema/Str]
2467
+ {:keys [cacert cakey csrdir
2468
+ inventory-lock inventory-lock-timeout-seconds
2469
+ serial-lock serial-lock-timeout-seconds] :as ca-settings} :- CaSettings
2470
+ report-activity]
2471
+ (let [; ; if part of a CA bundle, the intermediate CA will be first in the chain
2472
+ cacert (utils/pem->ca-cert cacert cakey)
2473
+ casubject (utils/get-subject-from-x509-certificate cacert)
2474
+ ca-private-key (utils/pem->private-key cakey)]
2475
+ (when-not (empty? subjects)
2476
+ ; ; since we are going to be manipulating the serial file and the inventory file for multiple entries,
2477
+ ; ; acquire the locks to prevent lock thrashing
2478
+ (common/with-safe-write-lock serial-lock serial-lock-descriptor serial-lock-timeout-seconds
2479
+ (common/with-safe-write-lock inventory-lock inventory-lock-descriptor inventory-lock-timeout-seconds
2480
+ (let [results
2481
+ ; ; loop through the subjects, one at a time, and collect the results for success or failure.
2482
+ (loop [s subjects
2483
+ result {:signed []
2484
+ :not-signed []}]
2485
+ (if-not (empty? s)
2486
+ (let [subject (first s)
2487
+ csr-path (path-to-cert-request csrdir subject)]
2488
+ (if (fs/exists? csr-path)
2489
+ (let [_ (log/trace (i18n/trs " File exists at {0}" csr-path))
2490
+ one-result (maybe-sign-one subject csr-path cacert casubject ca-private-key ca-settings)]
2491
+ ; ; one-result is either :signed or :not-signed
2492
+ (recur (rest s)
2493
+ (update result one-result conj subject)))
2494
+ (do
2495
+ (log/trace (i18n/trs " File does not exist at {0}" csr-path))
2496
+ (recur (rest s)
2497
+ (update result :not-signed conj subject)))))
2498
+ result))]
2499
+ ; ; submit the signing activity as one entry for all the hosts.
2500
+ (when-not (empty? (:signed results))
2501
+ (report-activity (:signed results) " signed" ))
2502
+ results))))))
0 commit comments