Skip to content

Commit dc6b63e

Browse files
(PE-37347) add bulk signing function
This adds a new function that given a set of csr names, the CA settings and a function to report activity will attempt to sign all the csrs that are valid. The function returns a map that contains the names of the csrs that were signed, and those that weren't signed. The function validates alt-names, authorization-extensions, and signatures. Locking is done across the group to prevent lock contention. In local testing, it took ~5 seconds to process ~500 records, of which 100 were valid.
1 parent 83f6be3 commit dc6b63e

File tree

2 files changed

+311
-84
lines changed

2 files changed

+311
-84
lines changed

src/clj/puppetlabs/puppetserver/certificate_authority.clj

+151-52
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
(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)
320
(java.nio CharBuffer)
421
(java.nio.file Files)
522
(java.nio.file.attribute FileAttribute PosixFilePermissions)
623
(java.security PrivateKey PublicKey)
7-
(java.security.cert X509Certificate CRLException CertPathValidatorException X509CRL)
24+
(java.security.cert CRLException CertPathValidatorException X509CRL X509Certificate)
825
(java.text SimpleDateFormat)
926
(java.time LocalDateTime ZoneId)
1027
(java.util Date)
1128
(java.util.concurrent.locks ReentrantReadWriteLock)
1229
(org.apache.commons.io IOUtils)
1330
(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)))
3232

3333
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
3434
;;; Public utilities
@@ -718,8 +718,32 @@
718718
(log/trace (i18n/trs "Finish append to serial file. ")))]
719719
(ks-file/atomic-write infra-node-serials-path stream-content-fn)))))))
720720

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+
721745
(schema/defn ^:always-validate
722-
write-cert-to-inventory!
746+
write-cert-to-inventory-unlocked!
723747
"Writes an entry into Puppet's inventory file for a given certificate.
724748
The location of this file is defined by Puppet's 'cert_inventory' setting.
725749
The inventory is a text file where each line represents a certificate in the
@@ -733,11 +757,11 @@
733757
* $NA = The 'not after' field of the cert, as a date/timestamp in UTC.
734758
* $S = The distinguished name of the cert's subject."
735759
[cert :- Certificate
736-
{:keys [cert-inventory inventory-lock inventory-lock-timeout-seconds] :as settings} :- CaSettings]
760+
{:keys [cert-inventory] :as settings} :- CaSettings]
737761
(let [serial-number (.getSerialNumber cert)
738762
formatted-serial-number (->> serial-number
739-
(format-serial-number)
740-
(str "0x"))
763+
(format-serial-number)
764+
(str "0x"))
741765
not-before (-> cert
742766
(.getNotBefore)
743767
(format-date-time))
@@ -746,32 +770,30 @@
746770
(format-date-time))
747771
subject (utils/get-subject-from-x509-certificate cert)
748772
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]
771795
(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)))
775797

776798
(schema/defn is-subject-in-inventory-row? :- schema/Bool
777799
[cn-subject :- utils/ValidX500Name
@@ -797,7 +819,6 @@
797819
;; lack of an end date means we can't tell if it is expired or not, so assume it isn't.
798820
false))
799821

800-
801822
(defn extract-inventory-row-contents
802823
[row]
803824
(str/split row #" "))
@@ -1776,7 +1797,6 @@
17761797
(validate-csr-signature! csr)
17771798
(autosign-certificate-request! subject csr settings report-activity)))))
17781799

1779-
17801800
(schema/defn ^:always-validate
17811801
get-certificate-revocation-list :- schema/Str
17821802
"Given the value of the 'cacrl' setting from Puppet,
@@ -2400,4 +2420,83 @@
24002420
(when (and enable-infra-crl (fs/exists? infra-crl-path))
24012421
(when (crl-expires-in-n-days? infra-crl-path settings crl-expiration-window-days)
24022422
(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

Comments
 (0)