From 25acbc117cf2553cd868ba4d622274d96a270df6 Mon Sep 17 00:00:00 2001 From: Pat Riehecky Date: Wed, 29 Apr 2026 12:00:19 -0500 Subject: [PATCH] Permit client CA and infra CA to use UUID serial numbers Fixes: https://github.com/OpenVoxProject/openvox-server/issues/284 Signed-off-by: Pat Riehecky Assisted-by: Claude Assisted-by: Qwen --- .../puppetserver/certificate_authority.clj | 287 ++++++-- .../puppetserver/bootstrap_testutils.clj | 4 +- .../certificate_authority_int_test.clj | 115 +++- .../certificate_authority_test.clj | 625 +++++++++++++++++- .../puppetserver/ruby/http_client_test.clj | 16 +- .../puppetlabs/services/ca/ca_testutils.clj | 169 ++++- .../puppet_server_config_service_test.clj | 4 +- .../services/master/master_core_test.clj | 10 +- 8 files changed, 1130 insertions(+), 100 deletions(-) diff --git a/src/clj/puppetlabs/puppetserver/certificate_authority.clj b/src/clj/puppetlabs/puppetserver/certificate_authority.clj index 7ef3ee9b8..1b2ee4696 100644 --- a/src/clj/puppetlabs/puppetserver/certificate_authority.clj +++ b/src/clj/puppetlabs/puppetserver/certificate_authority.clj @@ -14,6 +14,7 @@ [schema.core :as schema] [slingshot.slingshot :as sling]) (:import (java.io BufferedReader BufferedWriter ByteArrayInputStream ByteArrayOutputStream File FileNotFoundException IOException InputStream Reader StringReader) + (java.math BigInteger) (java.nio CharBuffer) (java.nio.file Files Path Paths) (java.nio.file.attribute FileAttribute PosixFilePermissions) @@ -23,7 +24,7 @@ (java.time Instant LocalDateTime ZoneId ZoneOffset ZonedDateTime) (java.time.format DateTimeFormatterBuilder TextStyle) (java.time.temporal ChronoUnit) - (java.util Date Locale) + (java.util Date Locale UUID) (java.util.concurrent.locks ReentrantReadWriteLock) (org.apache.commons.io IOUtils) (org.bouncycastle.pkcs PKCS10CertificationRequest))) @@ -138,7 +139,7 @@ :cakey schema/Str :capub schema/Str :ca-name schema/Str - :ca-ttl schema/Int + :ca-ttl (schema/maybe schema/Int) :cert-inventory schema/Str :csrdir schema/Str :keylength schema/Int @@ -147,6 +148,8 @@ :gem-path schema/Str :signeddir schema/Str :serial schema/Str + ;; Path to file containing infra serial numbers for incrementing mode + :infra-serial (schema/maybe schema/Str) ;; Path to file containing list of infra node certificates including MoM ;; provisioned by PE or user in case of FOSS :infra-nodes-path schema/Str @@ -158,6 +161,8 @@ ;; Option to continue using full CRL instead of infra CRL if desired ;; Infra CRL would be enabled by default. :enable-infra-crl schema/Bool + :serial-type (schema/maybe (schema/enum :incrementing :uuid)) + :infra-serial-type (schema/maybe (schema/enum :incrementing :uuid)) :serial-lock ReentrantReadWriteLock :serial-lock-timeout-seconds PosInt :crl-lock ReentrantReadWriteLock @@ -245,6 +250,8 @@ (def default-auto-ttl-renewal-seconds (duration-str->sec default-auto-ttl-renewal)) ; 90 days by default +(def default-keylength 2048) + ;; if the crl is going to expire in less than this number of days, it should be regenerated. (def crl-expiration-window-days 30) @@ -253,18 +260,36 @@ any of those keys already exists in the ca-data" [ca-data] (let [cadir (:cadir ca-data) - defaults {:infra-nodes-path (str cadir "/infra_inventory.txt") + defaults {:access-control {:certificate-status {}} + :allow-authorization-extensions false + :allow-duplicate-certs false + :allow-subject-alt-names default-allow-subj-alt-names + :allow-auto-renewal false + :auto-renewal-cert-ttl default-auto-ttl-renewal + :allow-header-cert-info false + :autosign false + :cacert (str cadir "/ca_crt.pem") + :cadir cadir + :cacrl (str cadir "/ca_crl.pem") + :cakey (str cadir "/ca_key.pem") + :capub (str cadir "/ca_pub.pem") + :ca-name "Puppet CA" + :cert-inventory (str cadir "/inventory.txt") + :csrdir (str cadir "/requests") + :keylength default-keylength + :manage-internal-file-permissions true + :signeddir (str cadir "/signed") + :serial (str cadir "/serial") + :infra-nodes-path (str cadir "/infra_inventory.txt") :infra-node-serials-path (str cadir "/infra_serials") + :infra-serial (str cadir "/infra_serial") :infra-crl-path (str cadir "/infra_crl.pem") :enable-infra-crl false - :allow-subject-alt-names default-allow-subj-alt-names - :allow-authorization-extensions default-allow-auth-extensions :serial-lock-timeout-seconds default-serial-lock-timeout-seconds :crl-lock-timeout-seconds default-crl-lock-timeout-seconds :inventory-lock-timeout-seconds default-inventory-lock-timeout-seconds - :allow-auto-renewal false - :auto-renewal-cert-ttl default-auto-ttl-renewal - :allow-header-cert-info false}] + :serial-type :incrementing + :infra-serial-type :incrementing}] (merge defaults ca-data))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -425,7 +450,9 @@ :inventory-lock :inventory-lock-timeout-seconds :allow-auto-renewal - :auto-renewal-cert-ttl)] + :auto-renewal-cert-ttl + :serial-type + :infra-serial-type)] (if (:enable-infra-crl ca-settings) settings' (dissoc settings' :infra-crl-path :infra-node-serials-path)))) @@ -573,6 +600,27 @@ (ks-file/atomic-write path (partial utils/obj->pem! csr) public-key-perms)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn uuid->serial-hex + "Converts a UUID to a 32-character hexadecimal string representation." + [^java.util.UUID uuid] + (let [uuid-str (.toString uuid)] + (clojure.string/replace uuid-str #"-" ""))) + +(defn uuid->serial-biginteger + "Converts a UUID to a positive BigInteger suitable for X.509 certificate serials." + [^java.util.UUID uuid] + (let [uuid-str (clojure.string/replace (.toString uuid) #"-" "")] + (.abs (BigInteger. ^String uuid-str 16)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Infrastructure node utilities + +(schema/defn read-infra-nodes + "Returns a list of infra nodes or infra node serials from the specified file organized as one item per line." + [infra-file-reader :- Reader] + (line-seq infra-file-reader)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Serial number functions (def serial-lock-descriptor @@ -587,13 +635,43 @@ "Text used in exceptions to help identify locking issues" "inventory-file") -(schema/defn parse-serial-number :- schema/Int - "Parses a serial number from its format on disk. See `format-serial-number` - for the awful, gory details." - [serial-number :- schema/Str] - (Integer/parseInt serial-number 16)) +(schema/defn is-infra-node? + "Determines if a subject is classified as an infrastructure node based on the infra-nodes-path file." + [subject :- schema/Str + settings :- CaSettings] + (let [infra-nodes-path (:infra-nodes-path settings) + infra-nodes-set (and infra-nodes-path (fs/exists? infra-nodes-path) + (with-open [infra-nodes-reader (io/reader infra-nodes-path)] + (set (read-infra-nodes infra-nodes-reader))))] + (if infra-nodes-set + (contains? infra-nodes-set subject) + false))) + +(schema/defn signing-context + "Determines the serial number context for certificate signing based on whether the certname is an infrastructure node." + [certname :- schema/Str + settings :- CaSettings] + (if (is-infra-node? certname settings) :infra :ca)) -(schema/defn get-serial-number! :- schema/Int +(schema/defn parse-serial-number + "Parses a serial number from its format on disk, handling both incrementing and uuid serial types. + Returns either an Int (for incrementing) or BigInteger (for UUID)." + [serial-number :- schema/Str] + (let [trimmed (.trim serial-number)] + (when (empty? trimmed) + (throw (IllegalStateException. + (i18n/trs "Serial number file is empty or invalid")))) + (try + (let [bi (BigInteger. ^String trimmed 16)] + ;; Puppet incrementing serials can exceed Integer; prefer Long when possible. + (if (<= (.bitLength bi) 63) + (.longValue bi) + bi)) + (catch NumberFormatException e + (throw (IllegalStateException. + (i18n/trs "Invalid serial number format in file: {0}" trimmed) e)))))) + +(schema/defn get-serial-number! :- (schema/cond-pre schema/Int java.math.BigInteger) "Reads the serial number file from disk and returns the serial number." [{:keys [serial serial-lock serial-lock-timeout-seconds]} :- CaSettings] (common/with-safe-read-lock serial-lock serial-lock-descriptor serial-lock-timeout-seconds @@ -606,33 +684,66 @@ "Converts a serial number to the format it needs to be written in on disk. This function has to write serial numbers in the same format that the puppet ruby code does, to maintain compatibility with things like 'puppet cert'; - for whatever arcane reason, that format is 0-padding up to 4 digits." - [serial-number :- schema/Int] - (format "%04X" serial-number)) + the format uses 0-padding with a minimum of 4 hex digits. For larger serial + numbers (e.g., UUIDs), the result will be more than 4 digits. Accepts Int, Long, BigInt or BigInteger." + [serial-number] + (cond + (instance? BigInteger serial-number) + (let [hex-str (.toString ^BigInteger serial-number 16)] + (if (<= (count hex-str) 4) + (format "%04X" (Integer/parseInt hex-str 16)) + (.toUpperCase hex-str))) + + (instance? clojure.lang.BigInt serial-number) + (format-serial-number (biginteger serial-number)) + + :else + ;; use long-capable formatting for incrementing serials + (format "%04X" (long serial-number)))) (def serial-file-permissions "rw-r--r--") (schema/defn next-serial-number! :- schema/Int - "Returns the next serial number to be used when signing a certificate request. - Reads the serial number as a hex value from the given file and replaces the - contents of `serial-file` with the next serial number for a subsequent call. - Puppet's $serial setting defines the location of the serial number file." - [{:keys [serial serial-lock serial-lock-timeout-seconds] :as ca-settings} :- CaSettings] - (common/with-safe-write-lock serial-lock serial-lock-descriptor serial-lock-timeout-seconds - (let [serial-number (get-serial-number! ca-settings)] - (ks-file/atomic-write-string serial - (format-serial-number (inc serial-number)) - serial-file-permissions) - serial-number))) + "Returns the next serial number to be used when signing a certificate request." + ([ca-settings] + (next-serial-number! ca-settings :ca)) + ([ca-settings context] + (let [serial-type (if (= context :infra) + (:infra-serial-type ca-settings) + (:serial-type ca-settings)) + serial-file (if (and (= context :infra) + (:infra-serial ca-settings) + (not= (:serial-type ca-settings) (:infra-serial-type ca-settings))) + (:infra-serial ca-settings) + (:serial ca-settings))] + (common/with-safe-write-lock (:serial-lock ca-settings) serial-lock-descriptor (:serial-lock-timeout-seconds ca-settings) + (if (= :uuid serial-type) + (let [uuid (UUID/randomUUID) + serial-hex (uuid->serial-hex uuid) + serial-number (uuid->serial-biginteger uuid)] + (ks-file/atomic-write-string serial-file serial-hex serial-file-permissions) + serial-number) + (let [serial-number (parse-serial-number (-> serial-file slurp .trim)) + next-serial (inc serial-number)] + (ks-file/atomic-write-string serial-file + (format-serial-number next-serial) + serial-file-permissions) + serial-number)))))) (schema/defn initialize-serial-file! - "Initializes the serial number file on disk. Serial numbers start at 1." - [{:keys [serial serial-lock serial-lock-timeout-seconds]} :- CaSettings] + "Initializes the serial number file on disk with the appropriate starting value based on the serial-type configuration." + [{:keys [serial infra-serial serial-lock serial-lock-timeout-seconds serial-type infra-serial-type]} :- CaSettings] (common/with-safe-write-lock serial-lock serial-lock-descriptor serial-lock-timeout-seconds - (ks-file/atomic-write-string serial - (format-serial-number 1) - serial-file-permissions))) + (let [initial-serial (if (= :uuid serial-type) + (uuid->serial-hex (UUID/randomUUID)) + "0001")] + (ks-file/atomic-write-string serial initial-serial serial-file-permissions) + (when (and infra-serial (not= serial-type infra-serial-type)) + (let [initial-infra-serial (if (= :uuid infra-serial-type) + (uuid->serial-hex (UUID/randomUUID)) + "0001")] + (ks-file/atomic-write-string infra-serial initial-infra-serial serial-file-permissions)))))) (schema/defn write-local-cacrl! :- (schema/maybe Exception) "Spits the contents of 'cacrl-contents' string to the 'localcacrl' file @@ -680,11 +791,6 @@ (def buffer-copy-size (* 64 1024)) -(schema/defn read-infra-nodes - "Returns a list of infra nodes or infra node serials from the specified file organized as one item per line." - [infra-file-reader :- Reader] - (line-seq infra-file-reader)) - (schema/defn maybe-write-to-infra-serial! "Determine if the host in question is an infra host, and if it is, add the provided serial number to the infra-serials file" @@ -890,28 +996,84 @@ ;;; Initialization (schema/defn validate-settings! - "Ensure config values are valid for basic CA behaviors." + "Validate CA configuration settings and throw clear errors for invalid values. + + This validation occurs at CA startup to ensure the configuration is valid before + any certificate operations begin. Invalid configurations are detected early with + clear, actionable error messages. + + VALIDATIONS PERFORMED: + 1. ca-ttl must be <= max-ca-ttl (1576800000 seconds = 50 years) + 2. serial-type must be :incrementing or :uuid + 3. infra-serial-type must be :incrementing or :uuid + + ERROR MESSAGES: + - ca_ttl: \"Config setting ca_ttl must have a value below {0}\" + - serial-type: \"Config setting 'serial-type' must be 'uuid' or 'incrementing' (found: {0})\" + - infra-serial-type: \"Config setting 'infra-serial-type' must be 'uuid' or 'incrementing' (found: {0})\" + + PARAMETERS: + settings - CaSettings map containing CA configuration + + RETURNS: + nil - Validation success, no return value + + ERROR HANDLING: + - Invalid ca-ttl: Throws IllegalStateException with max-ca-ttl value + - Invalid serial-type: Throws IllegalStateException with found value + - Invalid infra-serial-type: Throws IllegalStateException with found value + - Warning messages logged for deprecated client-whitelist settings + + CONFIGURATION VALIDATION FLOW: + config->ca-settings -> initialize-ca-config -> validate-settings! -> proceed + + EXAMPLES: + ; Valid configuration + (validate-settings! ca-settings) + ; => nil (no exception = valid) + + ; Invalid serial-type throws: + (validate-settings! (assoc ca-settings :serial-type :invalid)) + ; => IllegalStateException: \"Config setting 'serial-type' must be 'uuid' or 'incrementing' (found: :invalid)\" + + WARNINGS: + Logs deprecation warnings for client-whitelist and authorization-required + settings in certificate-authority.certificate-status section. + + SEE ALSO: + config->ca-settings - How settings are constructed + initialize-ca-config - Default configuration application" [settings :- CaSettings] (let [ca-ttl (:ca-ttl settings) certificate-status-access-control (get-in settings [:access-control :certificate-status]) certificate-status-whitelist (:client-whitelist - certificate-status-access-control)] + certificate-status-access-control) + serial-type (:serial-type settings) + infra-serial-type (:infra-serial-type settings)] (when (> ca-ttl max-ca-ttl) (throw (IllegalStateException. (i18n/trs "Config setting ca_ttl must have a value below {0}" max-ca-ttl)))) - (cond - (or (false? (:authorization-required certificate-status-access-control)) - (not-empty certificate-status-whitelist)) - (log/warn (format "%s %s" - (i18n/trs "The ''client-whitelist'' and ''authorization-required'' settings in the ''certificate-authority.certificate-status'' section are deprecated and will be removed in a future release.") - (i18n/trs "Remove these settings and create an appropriate authorization rule in the /etc/puppetlabs/puppetserver/conf.d/auth.conf file."))) - (not (nil? certificate-status-whitelist)) - (log/warn (format "%s %s %s" - (i18n/trs "The ''client-whitelist'' and ''authorization-required'' settings in the ''certificate-authority.certificate-status'' section are deprecated and will be removed in a future release.") - (i18n/trs "Because the ''client-whitelist'' is empty and ''authorization-required'' is set to ''false'', the ''certificate-authority.certificate-status'' settings will be ignored and authorization for the ''certificate_status'' endpoints will be done per the authorization rules in the /etc/puppetlabs/puppetserver/conf.d/auth.conf file.") - (i18n/trs "To suppress this warning, remove the ''certificate-authority'' configuration settings.")))))) + (when-not (#{:incrementing :uuid} serial-type) + (throw (IllegalStateException. + (i18n/trs "Config setting 'serial-type' must be 'uuid' or 'incrementing' (found: {0})" + (pr-str serial-type))))) + (when-not (#{:incrementing :uuid} infra-serial-type) + (throw (IllegalStateException. + (i18n/trs "Config setting 'infra-serial-type' must be 'uuid' or 'incrementing' (found: {0})" + (pr-str infra-serial-type))))) + (cond + (or (false? (:authorization-required certificate-status-access-control)) + (not-empty certificate-status-whitelist)) + (log/warn (format "%s %s" + (i18n/trs "The ''client-whitelist'' and ''authorization-required'' settings in the ''certificate-authority.certificate-status'' section are deprecated and will be removed in a future release.") + (i18n/trs "Remove these settings and create an appropriate authorization rule in the /etc/puppetlabs/puppetserver/conf.d/auth.conf file."))) + (not (nil? certificate-status-whitelist)) + (log/warn (format "%s %s %s" + (i18n/trs "The ''client-whitelist'' and ''authorization-required'' settings in the ''certificate-authority.certificate-status'' section are deprecated and will be removed in a future release.") + (i18n/trs "Because the ''client-whitelist'' is empty and ''authorization-required'' is set to ''false'', the ''certificate-authority.certificate-status'' settings will be ignored and authorization for the ''certificate_status'' endpoints will be done per the authorization rules in the /etc/puppetlabs/puppetserver/conf.d/auth.conf file.") + (i18n/trs "To suppress this warning, remove the ''certificate-authority'' configuration settings.")))))) (schema/defn ensure-cn-as-san :- utils/SSLExtension "Given the SSLExtension for subject alt names and a common name, ensure that the CN is listed in the SAN dns name list." @@ -1031,7 +1193,7 @@ private-key (utils/get-private-key keypair) x500-name (utils/cn (:ca-name ca-settings)) validity (cert-validity-dates (:ca-ttl ca-settings)) - serial (next-serial-number! ca-settings) + serial (next-serial-number! ca-settings :ca) ;; Since this is a self-signed cert, the issuer key and the ;; key for this cert are the same ca-exts (create-ca-extensions public-key @@ -1277,7 +1439,7 @@ (-> settings :requestdir fs/file ks/mkdirs!) (let [ca-cert (utils/pem->ca-cert (:cacert ca-settings) (:cakey ca-settings)) ca-private-key (utils/pem->private-key (:cakey ca-settings)) - next-serial (next-serial-number! ca-settings) + next-serial (next-serial-number! ca-settings (signing-context certname ca-settings)) public-key (generate-master-ssl-keys! settings) extensions (create-master-extensions certname public-key @@ -1477,8 +1639,15 @@ [{:keys [puppetserver jruby-puppet certificate-authority authorization]}] (let [merged (-> (select-keys puppetserver (keys CaSettings)) (merge (select-keys certificate-authority (keys CaSettings))) - (initialize-ca-config))] - (assoc merged :ruby-load-path (:ruby-load-path jruby-puppet) + (initialize-ca-config)) + serial-type-val (:serial-type merged) + serial-type-kw (cond-> serial-type-val (string? serial-type-val) keyword) + infra-serial-type-val (:infra-serial-type merged) + infra-serial-type-kw (cond-> infra-serial-type-val (string? infra-serial-type-val) keyword)] + (assoc merged + :serial-type (or serial-type-kw :incrementing) + :infra-serial-type (or infra-serial-type-kw :incrementing) + :ruby-load-path (:ruby-load-path jruby-puppet) :allow-auto-renewal (:allow-auto-renewal merged) :auto-renewal-cert-ttl (duration-str->sec (:auto-renewal-cert-ttl merged)) :ca-ttl (get-ca-ttl puppetserver certificate-authority) @@ -1682,7 +1851,7 @@ signed-cert (utils/sign-certificate (utils/get-subject-from-x509-certificate cacert) (utils/pem->private-key cakey) - (next-serial-number! ca-settings) + (next-serial-number! ca-settings (signing-context subject ca-settings)) (:not-before validity) (:not-after validity) (utils/cn subject) @@ -2359,7 +2528,7 @@ signed-cert (utils/sign-certificate (utils/get-subject-from-x509-certificate cacert) (utils/pem->private-key cakey) - (next-serial-number! ca-settings) + (next-serial-number! ca-settings (signing-context cert-name ca-settings)) (:not-before validity) (:not-after validity) cert-subject @@ -2465,7 +2634,7 @@ (validate-csr-signature! csr) (let [signed-cert (utils/sign-certificate casubject ca-private-key - (next-serial-number! ca-settings) + (next-serial-number! ca-settings (signing-context subject ca-settings)) (:not-before validity) (:not-after validity) (utils/cn subject) diff --git a/test/integration/puppetlabs/puppetserver/bootstrap_testutils.clj b/test/integration/puppetlabs/puppetserver/bootstrap_testutils.clj index 898ab60ae..8342a0dd6 100644 --- a/test/integration/puppetlabs/puppetserver/bootstrap_testutils.clj +++ b/test/integration/puppetlabs/puppetserver/bootstrap_testutils.clj @@ -181,8 +181,8 @@ :- (schema/pred ssl-simple/ssl-cert?) [ca-cert :- ca/Certificate certname :- schema/Str] - (let [ca-private-key (ssl-utils/pem->private-key - (str "./target/server-conf/ca/ca_key.pem")) + (let [ca-private-key (ssl-utils/pem->private-key + "./target/server-conf/ca/ca_key.pem") ca-dn (-> ca-cert (.getSubjectX500Principal) (.getName)) diff --git a/test/integration/puppetlabs/services/certificate_authority/certificate_authority_int_test.clj b/test/integration/puppetlabs/services/certificate_authority/certificate_authority_int_test.clj index 4fe527567..137d0044d 100644 --- a/test/integration/puppetlabs/services/certificate_authority/certificate_authority_int_test.clj +++ b/test/integration/puppetlabs/services/certificate_authority/certificate_authority_int_test.clj @@ -1133,7 +1133,7 @@ (is (= 204 (:status response))) (is (re-find msg-matcher (:message (first activity-events)))) (is (= 2 (count @reported-activity))))) - + (fs/delete saved-csr)))))) (deftest ca-expirations-endpoint-test @@ -1194,7 +1194,7 @@ :body "Bad data"})] (is (= 400 (:status response))))))) -(deftest ca-bulk-signing-endpoint-test +(deftest ca-bulk-signing-endpoint-test (testing "ca bulk signing endpoint " (bootstrap/with-puppetserver-running-with-mock-jrubies "JRuby mocking is safe here because all of the requests are to the CA @@ -1207,7 +1207,7 @@ :ssl-key (str bootstrap/server-conf-dir "/ssl/private_keys/localhost.pem") :ssl-ca-cert (str bootstrap/server-conf-dir "/ca/ca_crt.pem") :ssl-crl-path (str bootstrap/server-conf-dir "/ssl/crl.pem")}} - + (testing "returns 200 with valid payload" ;; note- more extensive testing of the behavior is done with the testing in sign-multiple-certificate-signing-requests!-test (let [certname (ks/rand-str :alpha-lower 16) @@ -1220,7 +1220,7 @@ _ (generate-a-csr certname [] []) _ (generate-a-csr certname-with-bad-extension [{:oid "1.9.9.9.9.9.0" :value "true" :critical false}] []) response (http-client/post - "https://localhost:8140/puppet-ca/v1/sign" + "https://localhost:8140/puppet-ca/v1/sign" {:body (json/encode {:certnames [certname certname-no-exist certname-with-bad-extension]}) :ssl-cert (str bootstrap/server-conf-dir "/ca/ca_crt.pem") :ssl-key (str bootstrap/server-conf-dir "/ca/ca_key.pem") @@ -1793,8 +1793,8 @@ ca-key (str bootstrap/server-conf-dir "/ca/ca_key.pem") ca-crl (str bootstrap/server-conf-dir "/ca/ca_crl.pem") ca-cert' (ssl-utils/pem->ca-cert ca-cert ca-key) - revoke-response (http-client/put - (str "https://localhost:8140/puppet-ca/v1/clean") + revoke-response (http-client/put + "https://localhost:8140/puppet-ca/v1/clean" {:body (json/encode {:certnames [random-certname]}) :ssl-cert (str bootstrap/server-conf-dir "/ssl/certs/localhost.pem") :ssl-key (str bootstrap/server-conf-dir "/ssl/private_keys/localhost.pem") @@ -1939,8 +1939,101 @@ (merge request-opts {:headers {"content-type" "application/json" "X-Authentication" "test"}}))] (is (= 204 (:status response)))))) - (testing "getting the cert returns a 404" - (let [response (http-client/get status-url - (merge request-opts - {:headers {"content-type" "application/json" "X-Authentication" "test"}}))] - (is (= 404 (:status response))))))))))) + (testing "getting the cert returns a 404" + (let [response (http-client/get status-url + (merge request-opts + {:headers {"content-type" "application/json" "X-Authentication" "test"}}))] + (is (= 404 (:status response))))))))))) + +(deftest uuid-serial-certificate-signing-int-test + (testutils/with-config-dirs + {(str test-resources-dir "/infracrl_test/master/conf/ssl") (str bootstrap/server-conf-dir "/ssl") + (str test-resources-dir "/infracrl_test/master/conf/ca") (str bootstrap/server-conf-dir "/ca")} + (testutils/with-stub-puppet-conf + (bootstrap/with-puppetserver-running-with-config + app + (bootstrap/load-dev-config-with-overrides + {:certificate-authority {:serial-type "uuid"} + :jruby-puppet + {:gem-path [(ks/absolute-path jruby-testutils/gem-path)]} + :webserver + {:ssl-cert (str bootstrap/server-conf-dir "/ssl/certs/localhost.pem") + :ssl-key (str bootstrap/server-conf-dir "/ssl/private_keys/localhost.pem") + :ssl-ca-cert (str bootstrap/server-conf-dir "/ca/ca_crt.pem") + :ssl-crl-path (str bootstrap/server-conf-dir "/ssl/crl.pem")}}) + (let [request-dir (str bootstrap/server-conf-dir "/ca/requests") + key-pair (ssl-utils/generate-key-pair) + subjectDN (ssl-utils/cn "test-uuid-node") + csr (ssl-utils/generate-certificate-request key-pair subjectDN) + csr-file (ks/temp-file "test_uuid_csr.pem") + saved-csr (str request-dir "/test-uuid-node.pem") + url "https://localhost:8140/puppet-ca/v1/certificate_request/test-uuid-node" + request-opts {:ssl-cert (str bootstrap/server-conf-dir "/ca/ca_crt.pem") + :ssl-key (str bootstrap/server-conf-dir "/ca/ca_key.pem") + :ssl-ca-cert (str bootstrap/server-conf-dir "/ca/ca_crt.pem") + :as :text + :headers {"content-type" "text/plain"}}] + (ssl-utils/obj->pem! csr csr-file) + (http-client/put url (merge request-opts {:body (slurp csr-file)})) + (is (fs/exists? saved-csr)) + (let [cert-pem (slurp saved-csr) + cert (ssl-utils/pem->cert cert-pem) + serial (.getSerialNumber cert)] + (is (instance? java.math.BigInteger serial) + "Serial should be BigInteger") + (is (> (.bitLength serial) 100) + "UUID serial should be at least 100+ bits")) + (fs/delete csr-file)))))) + +(deftest mixed-serial-modes-int-test + (testutils/with-config-dirs + {(str test-resources-dir "/infracrl_test/master/conf/ssl") (str bootstrap/server-conf-dir "/ssl") + (str test-resources-dir "/infracrl_test/master/conf/ca") (str bootstrap/server-conf-dir "/ca")} + (testutils/with-stub-puppet-conf + (bootstrap/with-puppetserver-running-with-config + app + (bootstrap/load-dev-config-with-overrides + {:certificate-authority {:serial-type "incrementing" + :infra-serial-type "uuid"} + :jruby-puppet + {:gem-path [(ks/absolute-path jruby-testutils/gem-path)]} + :webserver + {:ssl-cert (str bootstrap/server-conf-dir "/ssl/certs/localhost.pem") + :ssl-key (str bootstrap/server-conf-dir "/ssl/private_keys/localhost.pem") + :ssl-ca-cert (str bootstrap/server-conf-dir "/ca/ca_crt.pem") + :ssl-crl-path (str bootstrap/server-conf-dir "/ssl/crl.pem")}}) + (let [infra-nodes-path (str bootstrap/server-conf-dir "/ca/infra_inventory.txt") + regular-key (ssl-utils/generate-key-pair) + infra-key (ssl-utils/generate-key-pair) + regular-subject (ssl-utils/cn "regular-node") + infra-subject (ssl-utils/cn "infra-mom") + regular-csr (ssl-utils/generate-certificate-request regular-key regular-subject) + infra-csr (ssl-utils/generate-certificate-request infra-key infra-subject) + regular-csr-file (ks/temp-file "regular_csr.pem") + infra-csr-file (ks/temp-file "infra_csr.pem") + regular-url "https://localhost:8140/puppet-ca/v1/certificate_request/regular-node" + infra-url "https://localhost:8140/puppet-ca/v1/certificate_request/infra-mom" + request-opts {:ssl-cert (str bootstrap/server-conf-dir "/ca/ca_crt.pem") + :ssl-key (str bootstrap/server-conf-dir "/ca/ca_key.pem") + :ssl-ca-cert (str bootstrap/server-conf-dir "/ca/ca_crt.pem") + :as :text + :headers {"content-type" "text/plain"}}] + (ks-file/atomic-write-string infra-nodes-path (str infra-subject "\n") "rw-r--r--") + (ssl-utils/obj->pem! regular-csr regular-csr-file) + (ssl-utils/obj->pem! infra-csr infra-csr-file) + (http-client/put regular-url (merge request-opts {:body (slurp regular-csr-file)})) + (http-client/put infra-url (merge request-opts {:body (slurp infra-csr-file)})) + (let [regular-cert (ssl-utils/pem->cert (slurp regular-csr-file)) + infra-cert (ssl-utils/pem->cert (slurp infra-csr-file)) + regular-serial (.getSerialNumber regular-cert) + infra-serial (.getSerialNumber infra-cert)] + (is (= 1N regular-serial) + "Regular node should have incrementing serial (1)") + (is (instance? java.math.BigInteger infra-serial)) + (is (> (.bitLength infra-serial) 100) + "Infra node should have UUID serial")) + (fs/delete regular-csr-file) + (fs/delete infra-csr-file))))) + + + ) diff --git a/test/unit/puppetlabs/puppetserver/certificate_authority_test.clj b/test/unit/puppetlabs/puppetserver/certificate_authority_test.clj index 2d1aacc45..ad7f527fd 100644 --- a/test/unit/puppetlabs/puppetserver/certificate_authority_test.clj +++ b/test/unit/puppetlabs/puppetserver/certificate_authority_test.clj @@ -1041,14 +1041,16 @@ (ca/initialize-master-ssl! (assoc settings :keylength 32768) "master" ca-settings))))))) (deftest parse-serial-number-test - (is (= (ca/parse-serial-number "0001") 1)) - (is (= (ca/parse-serial-number "0010") 16)) - (is (= (ca/parse-serial-number "002A") 42))) + (is (= (ca/parse-serial-number "0001") 1N)) + (is (= (ca/parse-serial-number "0010") 16N)) + (is (= (ca/parse-serial-number "002A") 42N))) (deftest format-serial-number-test (is (= (ca/format-serial-number 1) "0001")) (is (= (ca/format-serial-number 16) "0010")) - (is (= (ca/format-serial-number 42) "002A"))) + (is (= (ca/format-serial-number 42) "002A")) + (is (= (ca/format-serial-number 1N) "0001")) + (is (= (ca/format-serial-number 16N) "0010"))) (deftest next-serial-number!-test (let [serial-file (str (ks/temp-file)) @@ -2451,6 +2453,615 @@ (doall (for [^String i a-foo-file-names] (Files/createFile (.resolve path-to-file i) default-permissions))) - (let [result (ca/get-paths-to-all-certificate-requests (.toString temp-directory)) - file-names (set (common/extract-file-names-from-paths result))] - (is (= (set file-names) all-pem-file-names)))))) + (let [result (ca/get-paths-to-all-certificate-requests (.toString temp-directory)) + file-names (set (common/extract-file-names-from-paths result))] + (is (= (set file-names) all-pem-file-names)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; UUID Serial Number Tests + +(deftest uuid-serial-generation-test + (testing "UUID serial numbers are generated correctly" + (let [cadir (ks/temp-dir) + ca-settings (testutils/uuid-ca-settings cadir)] + (testing "initialize-serial-file! creates a UUID-based serial file" + (ca/initialize-serial-file! ca-settings) + (let [serial-content (slurp (:serial ca-settings))] + (is (re-matches #"[0-9a-f]{32}" serial-content) + "Serial content should be a 32-character hex string"))) + + (testing "next-serial-number! generates unique UUIDs" + (ca/initialize-serial-file! ca-settings) + (let [serial1 (ca/next-serial-number! ca-settings) + serial2 (ca/next-serial-number! ca-settings) + serial3 (ca/next-serial-number! ca-settings)] + (is (instance? BigInteger serial1) + "Serial should be a BigInteger") + (is (not= serial1 serial2) + "Sequential serials should be different UUIDs") + (is (not= serial2 serial3) + "Sequential serials should be different UUIDs") + (is (pos? (.bitLength serial1)) + "UUID serial should fit within X.509 20-byte limit"))) + + (testing "UUID serial file content is hex string" + (ca/initialize-serial-file! ca-settings) + (ca/next-serial-number! ca-settings) + (let [serial-content (slurp (:serial ca-settings))] + (is (re-matches #"[0-9a-f]+" serial-content) + "Serial file should contain hex string")))))) + +(deftest parse-serial-number-corruption-test + (testing "parse-serial-number handles corrupted data" + (is (thrown? IllegalStateException (ca/parse-serial-number "")) + "Empty serial should throw") + (is (thrown? IllegalStateException (ca/parse-serial-number " ")) + "Whitespace-only serial should throw") + (is (thrown? IllegalStateException (ca/parse-serial-number "ZZZZ")) + "Invalid hex should throw"))) + +(deftest parse-serial-number-extended-test + (testing "parse-serial-number handles both incrementing and UUID formats" + (is (= 1N (ca/parse-serial-number "0001")) + "Should parse incrementing format") + (is (= 16N (ca/parse-serial-number "0010")) + "Should parse hex incrementing format") + (is (= (BigInteger. "1234567890abcdef1234567890abcdef" 16) + (ca/parse-serial-number "1234567890abcdef1234567890abcdef")) + "Should parse UUID hex format")) + (testing "parse-serial-number handles 4-digit incrementing format" + (is (= 1N (ca/parse-serial-number "0001")) + "Should parse 4-digit hex 0001") + (is (= 256N (ca/parse-serial-number "0100")) + "Should parse 4-digit hex 0100")) + (testing "parse-serial-number handles 32-char UUID format" + (let [uuid-hex "a1b2c3d4e5f6789012345678901234ab"] + (is (instance? BigInteger (ca/parse-serial-number uuid-hex)) + "Should parse 32-char hex UUID format")))) + +(deftest format-serial-number-extended-test + (testing "format-serial-number handles BigInteger" + (is (= "0001" (ca/format-serial-number 1N)) + "Should format minimal BigInteger") + (is (= "0010" (ca/format-serial-number 16N)) + "Should format hex BigInteger") + (is (= "0001" (ca/format-serial-number 1)) + "Should still format Int") + (let [uuid-test-num (BigInteger. "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" 16) + formatted (ca/format-serial-number uuid-test-num)] + (is (>= (count formatted) 4) + "Should format large BigInteger with at least 4 digits")))) + +(deftest uuid-serial-uniqueness-test + (testing "UUID serials are unique and non-zero" + (let [serials (for [_ (range 10000)] + (ca/uuid->serial-biginteger (java.util.UUID/randomUUID))) + unique-serials (set serials)] + + (is (= 10000 (count unique-serials)) + "All generated UUIDs should produce unique serials") + + (is (not (contains? unique-serials 0N)) + "Zero serial should never occur (astronomically unlikely but verify)") + + (is (every? (fn [s] (> s 0N)) serials) + "All serials should be positive")))) + +(deftest config-string-values-conversion-test + (testing "Config string values are converted to keywords" + (let [config-map {:certificate-authority {:serial-type "uuid" + :infra-serial-type "uuid" + :serial "dummy" + :serial-lock (new ReentrantReadWriteLock) + :serial-lock-timeout-seconds 5 + :crl-lock (new ReentrantReadWriteLock) + :crl-lock-timeout-seconds 5 + :inventory-lock (new ReentrantReadWriteLock) + :inventory-lock-timeout-seconds 5 + :keylength 2048 + :ca-name "Test CA" + :csrdir "/tmp/requests" + :capub "/tmp/ca_pub.pem" + :cacert "/tmp/ca_cert.pem" + :autosign false + :cakey "/tmp/ca_key.pem" + :cacrl "/tmp/ca_crl.pem" + :manage-internal-file-permissions true + :cert-inventory "/tmp/inventory" + :signeddir "/tmp/signed" + :cadir "/tmp/ca" + :allow-duplicate-certs false + :allow-subject-alt-names true} + :puppetserver {} + :jruby-puppet {:ruby-load-path [] + :gem-path []} + :authorization {}}] + (is (= :uuid (:serial-type (ca/config->ca-settings config-map))) + "String 'uuid' should convert to keyword :uuid") + (is (= :uuid (:infra-serial-type (ca/config->ca-settings config-map))) + "String 'uuid' should convert to keyword :uuid for infra"))) + + (testing "Config keyword values still work" + (let [config-map {:certificate-authority {:serial-type :incrementing + :infra-serial-type :uuid + :serial "dummy" + :serial-lock (new ReentrantReadWriteLock) + :serial-lock-timeout-seconds 5 + :crl-lock (new ReentrantReadWriteLock) + :crl-lock-timeout-seconds 5 + :inventory-lock (new ReentrantReadWriteLock) + :inventory-lock-timeout-seconds 5 + :keylength 2048 + :ca-name "Test CA" + :csrdir "/tmp/requests" + :capub "/tmp/ca_pub.pem" + :cacert "/tmp/ca_cert.pem" + :autosign false + :cakey "/tmp/ca_key.pem" + :cacrl "/tmp/ca_crl.pem" + :manage-internal-file-permissions true + :cert-inventory "/tmp/inventory" + :signeddir "/tmp/signed" + :cadir "/tmp/ca" + :allow-duplicate-certs false + :allow-subject-alt-names true} + :puppetserver {} + :jruby-puppet {:ruby-load-path [] + :gem-path []} + :authorization {}}] + (is (= :incrementing (:serial-type (ca/config->ca-settings config-map)))) + (is (= :uuid (:infra-serial-type (ca/config->ca-settings config-map)))))) + + (testing "Default values used when not provided" + (let [config-map {:certificate-authority {:serial "dummy" + :serial-lock (new ReentrantReadWriteLock) + :serial-lock-timeout-seconds 5 + :crl-lock (new ReentrantReadWriteLock) + :crl-lock-timeout-seconds 5 + :inventory-lock (new ReentrantReadWriteLock) + :inventory-lock-timeout-seconds 5 + :keylength 2048 + :ca-name "Test CA" + :csrdir "/tmp/requests" + :capub "/tmp/ca_pub.pem" + :cacert "/tmp/ca_cert.pem" + :autosign false + :cakey "/tmp/ca_key.pem" + :cacrl "/tmp/ca_crl.pem" + :manage-internal-file-permissions true + :cert-inventory "/tmp/inventory" + :signeddir "/tmp/signed" + :cadir "/tmp/ca" + :allow-duplicate-certs false + :allow-subject-alt-names true} + :puppetserver {} + :jruby-puppet {:ruby-load-path [] + :gem-path []} + :authorization {}}] + (is (= :uuid (:serial-type (ca/config->ca-settings config-map))) + "String 'uuid' should convert to keyword :uuid") + (is (= :uuid (:infra-serial-type (ca/config->ca-settings config-map))) + "String 'uuid' should convert to keyword :uuid for infra"))) + + (testing "Config keyword values still work" + (let [config-map {:certificate-authority {:serial-type :incrementing + :infra-serial-type :uuid + :serial "dummy" + :serial-lock (new ReentrantReadWriteLock) + :serial-lock-timeout-seconds 5 + :crl-lock (new ReentrantReadWriteLock) + :crl-lock-timeout-seconds 5 + :inventory-lock (new ReentrantReadWriteLock) + :inventory-lock-timeout-seconds 5 + :keylength 2048 + :ca-name "Test CA" + :csrdir "/tmp/requests" + :capub "/tmp/ca_pub.pem" + :cacert "/tmp/ca_cert.pem" + :autosign false + :cakey "/tmp/ca_key.pem" + :cacrl "/tmp/ca_crl.pem" + :manage-internal-file-permissions true + :cert-inventory "/tmp/inventory" + :signeddir "/tmp/signed" + :cadir "/tmp/ca" + :allow-duplicate-certs false + :allow-subject-alt-names true} + :puppetserver {} + :jruby-puppet {:ruby-load-path [] + :gem-path []} + :authorization {}}] + (is (= :incrementing (:serial-type (ca/config->ca-settings config-map)))) + (is (= :uuid (:infra-serial-type (ca/config->ca-settings config-map)))))) + + (testing "Default values used when not provided" + (let [config-map {:certificate-authority {:serial "dummy" + :serial-lock (new ReentrantReadWriteLock) + :serial-lock-timeout-seconds 5 + :crl-lock (new ReentrantReadWriteLock) + :crl-lock-timeout-seconds 5 + :inventory-lock (new ReentrantReadWriteLock) + :inventory-lock-timeout-seconds 5 + :keylength 2048 + :ca-name "Test CA" + :csrdir "/tmp/requests" + :capub "/tmp/ca_pub.pem" + :cacert "/tmp/ca_cert.pem" + :autosign false + :cakey "/tmp/ca_key.pem" + :cacrl "/tmp/ca_crl.pem" + :manage-internal-file-permissions true + :cert-inventory "/tmp/inventory" + :signeddir "/tmp/signed" + :cadir "/tmp/ca" + :allow-duplicate-certs false + :allow-subject-alt-names true} + :puppetserver {} + :jruby-puppet {:ruby-load-path [] + :gem-path []} + :authorization {}}] + (is (= :incrementing (:serial-type (ca/config->ca-settings config-map))) + "Should default to :incrementing") + (is (= :incrementing (:infra-serial-type (ca/config->ca-settings config-map))) + "Should default to :incrementing for infra")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; TEST GROUP 1: signing-context Function Tests (8+ tests) + +(deftest signing-context-regular-node-test + (testing "Regular nodes return :ca context" + (let [cadir (ks/temp-dir) + settings (testutils/ca-settings cadir)] + (is (= :ca (ca/signing-context "web-server-01" settings)) + "Regular node should return :ca context")))) + +(deftest signing-context-infra-node-test + (testing "Infrastructure nodes return :infra context" + (let [cadir (ks/temp-dir) + settings (testutils/infra-node-ca-settings cadir ["puppet" "puppetdb"])] + (is (= :infra (ca/signing-context "puppet" settings)) + "Infra node 'puppet' should return :infra context") + (is (= :infra (ca/signing-context "puppetdb" settings)) + "Infra node 'puppetdb' should return :infra context") + (is (= :ca (ca/signing-context "agent-01" settings)) + "Regular node should still return :ca context")))) + +(deftest signing-context-missing-infra-file-test + (testing "Missing infra-nodes-path defaults to :ca for all nodes" + (let [cadir (ks/temp-dir) + settings (assoc (testutils/ca-settings cadir) + :infra-nodes-path "/nonexistent/path")] + (is (= :ca (ca/signing-context "puppet" settings)) + "Should default to :ca when infra file missing") + (is (= :ca (ca/signing-context "agent-01" settings)) + "Should default to :ca when infra file missing")))) + +(deftest signing-context-empty-infra-file-test + (testing "Empty infra-nodes-path file" + (let [cadir (ks/temp-dir) + settings (testutils/infra-node-ca-settings cadir [])] + (is (= :ca (ca/signing-context "puppet" settings)) + "Empty infra file should return :ca for all nodes") + (is (= :ca (ca/signing-context "any-node" settings)) + "Empty infra file should return :ca for all nodes")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; TEST GROUP 2: Mixed Mode Configuration Tests (8+ tests) + +(deftest mixed-mode-ca-uuid-infra-incrementing-test + (testing "CA uses UUID, infrastructure uses incrementing" + (let [cadir (ks/temp-dir) + settings (testutils/mixed-mode-ca-settings cadir :uuid :incrementing ["puppet"])] + (is (= :uuid (:serial-type settings))) + (is (= :incrementing (:infra-serial-type settings))) + (ca/initialize-serial-file! settings) + (let [ca-serial (ca/next-serial-number! settings :ca) + infra-serial (ca/next-serial-number! settings :infra)] + (is (> (.bitLength ca-serial) 100) "CA serial should be UUID-like") + (is (= 1N infra-serial) "Infra serial should be incrementing"))))) + +(deftest mixed-mode-ca-incrementing-infra-uuid-test + (testing "CA uses incrementing, infrastructure uses UUID (recommended production)" + (let [cadir (ks/temp-dir) + settings (testutils/mixed-mode-ca-settings cadir :incrementing :uuid ["puppet"])] + (is (= :incrementing (:serial-type settings))) + (is (= :uuid (:infra-serial-type settings))) + (ca/initialize-serial-file! settings) + (let [ca-serial (ca/next-serial-number! settings :ca) + infra-serial (ca/next-serial-number! settings :infra)] + (is (= 1N ca-serial) "CA serial should be incrementing") + (is (> (.bitLength infra-serial) 100) "Infra serial should be UUID-like"))))) + +(deftest mixed-mode-both-uuid-test + (testing "Both CA and infra use UUID" + (let [cadir (ks/temp-dir) + settings (testutils/mixed-mode-ca-settings cadir :uuid :uuid)] + (ca/initialize-serial-file! settings) + (let [serial1 (ca/next-serial-number! settings :ca) + serial2 (ca/next-serial-number! settings :infra) + serial3 (ca/next-serial-number! settings :ca)] + (is (not= serial1 serial2) "Different contexts should produce different UUIDs") + (is (not= serial2 serial3) "Sequential UUIDs should be unique") + (is (every? #(> (.bitLength %) 100) [serial1 serial2 serial3])))))) + +(deftest mixed-mode-both-incrementing-test + (testing "Both CA and infra use incrementing (backward compatible)" + (let [cadir (ks/temp-dir) + settings (testutils/mixed-mode-ca-settings cadir :incrementing :incrementing)] + (ca/initialize-serial-file! settings) + (let [serial1 (ca/next-serial-number! settings :ca) + serial2 (ca/next-serial-number! settings :infra)] + ; Note: They share the same file in incrementing mode, so both increment same sequence + (is (= 1N serial1) "First CA serial") + (is (= 2N serial2) "Next serial in shared sequence"))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; TEST GROUP 3: Error Handling & Edge Cases (10+ tests) + +(deftest invalid-serial-type-rejected-test + (testing "Invalid serial-type values are rejected" + (is (thrown? Exception + (ca/validate-settings! {:serial-type :invalid})) + "Invalid serial-type should throw") + (is (thrown? Exception + (ca/validate-settings! {:infra-serial-type :bad})) + "Invalid infra-serial-type should throw"))) + +(deftest empty-serial-file-error-test + (testing "Empty serial file causes error" + (let [cadir (ks/temp-dir) + settings (testutils/ca-settings cadir) + empty-file (:serial settings)] + (ks-file/atomic-write-string empty-file "" "rw-r-----") + (is (thrown? Exception (ca/next-serial-number! settings :ca)) + "Empty serial file should throw")))) + +(deftest non-hex-incrementing-serial-error-test + (testing "Non-hex serial in incrementing mode causes error" + (let [cadir (ks/temp-dir) + settings (assoc (testutils/ca-settings cadir) + :serial-type :incrementing) + bad-file (:serial settings)] + (ks-file/atomic-write-string bad-file "ZZZZ" "rw-r-----") + (is (thrown? Exception (ca/next-serial-number! settings :ca)) + "Non-hex serial should throw")))) + +(deftest uuid-hex-format-32-chars-test + (testing "UUID hex conversion always produces 32 characters" + (dotimes [_ 100] + (let [uuid (java.util.UUID/randomUUID) + hex (ca/uuid->serial-hex uuid)] + (is (= 32 (count hex)) (str "UUID hex should be 32 chars, got: " hex)) + (is (re-matches #"[0-9a-f]{32}" hex) "Should be valid hex"))))) + +(deftest uuid-serial-always-positive-test + (testing "UUID conversion always produces positive BigInteger" + (dotimes [_ 100] + (let [uuid (java.util.UUID/randomUUID) + serial (ca/uuid->serial-biginteger uuid)] + (is (pos? serial) (str "Serial should be positive, got: " serial)) + (is (instance? java.math.BigInteger serial)))))) + +(deftest uuid-serial-within-x509-limit-test + (testing "UUID serial fits within X.509 160-bit constraint" + (dotimes [_ 100] + (let [uuid (java.util.UUID/randomUUID) + serial (ca/uuid->serial-biginteger uuid)] + (is (<= (.bitLength serial) 160) + (str "Serial bitlength must be <= 160, got: " (.bitLength serial))))))) + +(deftest very-large-incrementing-serial-test + (testing "Large incrementing serials are handled" + (let [cadir (ks/temp-dir) + settings (testutils/ca-settings cadir) + large-serial "FFFFFFFF"] ; 32-bit max + (ks-file/atomic-write-string (:serial settings) large-serial "rw-r-----") + (let [next-serial (ca/next-serial-number! settings :ca)] + (is (= (biginteger (+ 0xFFFFFFFF 1)) next-serial) + "Should increment large serial correctly"))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; TEST GROUP 4: is-infra-node? Function Tests (6+ tests) + +(deftest is-infra-node-returns-true-test + (testing "is-infra-node? returns true for infra nodes" + (let [cadir (ks/temp-dir) + settings (testutils/infra-node-ca-settings cadir ["puppet" "puppetdb"])] + (is (true? (ca/is-infra-node? "puppet" settings))) + (is (true? (ca/is-infra-node? "puppetdb" settings)))))) + +(deftest is-infra-node-returns-false-test + (testing "is-infra-node? returns false for regular nodes" + (let [cadir (ks/temp-dir) + settings (testutils/infra-node-ca-settings cadir ["puppet"])] + (is (false? (ca/is-infra-node? "agent-01" settings))) + (is (false? (ca/is-infra-node? "web-server" settings)))))) + +(deftest is-infra-node-missing-file-test + (testing "is-infra-node? gracefully handles missing file" + (let [cadir (ks/temp-dir) + settings (assoc (testutils/ca-settings cadir) + :infra-nodes-path "/nonexistent/path")] + (is (false? (ca/is-infra-node? "puppet" settings)) + "Missing file should return false")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; TEST GROUP 5: UUID Conversion Functions (6+ tests) + +(deftest uuid-to-hex-deterministic-test + (testing "UUID to hex conversion is deterministic" + (let [fixed-uuid (java.util.UUID/fromString "12345678-1234-5678-1234-567812345678")] + (is (= "12345678123456781234567812345678" (ca/uuid->serial-hex fixed-uuid))) + (is (= "12345678123456781234567812345678" (ca/uuid->serial-hex fixed-uuid)) + "Same UUID should produce same hex")))) + +(deftest uuid-to-biginteger-deterministic-test + (testing "UUID to BigInteger conversion is deterministic" + (let [fixed-uuid (java.util.UUID/fromString "12345678-1234-5678-1234-567812345678") + serial1 (ca/uuid->serial-biginteger fixed-uuid) + serial2 (ca/uuid->serial-biginteger fixed-uuid)] + (is (= serial1 serial2) "Same UUID should produce same BigInteger")))) + +(deftest uuid-hex-and-biginteger-consistent-test + (testing "UUID hex and BigInteger conversions are consistent" + (let [fixed-uuid (java.util.UUID/fromString "12345678-1234-5678-1234-567812345678") + hex (ca/uuid->serial-hex fixed-uuid) + big-int (ca/uuid->serial-biginteger fixed-uuid) + reconstructed (BigInteger. hex 16)] + (is (= (.abs reconstructed) big-int) + "Hex and BigInteger should represent same value")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; TEST GROUP 6: Backward Compatibility Tests (10+ tests) + +(deftest incrementing-default-behavior-test + (testing "Default configuration uses incrementing mode" + (let [cadir (ks/temp-dir) + settings (testutils/ca-settings cadir)] + (is (= :incrementing (:serial-type settings))) + (is (= :incrementing (:infra-serial-type settings))) + (ca/initialize-serial-file! settings) + (let [serial (ca/next-serial-number! settings :ca)] + (is (= 1N serial) "First incrementing serial should be 1"))))) + +(deftest incrementing-sequence-correct-test + (testing "Incrementing serial sequence is correct" + (let [cadir (ks/temp-dir) + settings (testutils/ca-settings cadir)] + (ca/initialize-serial-file! settings) + (let [serials (mapv (fn [_] (ca/next-serial-number! settings :ca)) (range 5))] + (is (= [1N 2N 3N 4N 5N] serials) + "Incrementing sequence should be 1, 2, 3, 4, 5"))))) + +(deftest incrementing-file-format-unchanged-test + (testing "Incrementing file format is unchanged (4-char hex)" + (let [cadir (ks/temp-dir) + settings (testutils/ca-settings cadir)] + (ca/initialize-serial-file! settings) + (ca/next-serial-number! settings :ca) + (ca/next-serial-number! settings :ca) + (let [file-content (slurp (:serial settings))] + (is (re-matches #"[0-9a-f]{1,4}" file-content) + "Incrementing file should have 1-4 hex chars"))))) + +(deftest existing-incrementing-files-compatible-test + (testing "Existing incrementing serial files work" + (let [cadir (ks/temp-dir) + settings (testutils/ca-settings cadir) + old-serial-content "00F0"] ; Old file with 4-digit hex (0xF0 = 240) + (ks-file/atomic-write-string (:serial settings) old-serial-content "rw-r-----") + (let [next-serial (ca/next-serial-number! settings :ca)] + (is (= 240N next-serial) + "Should return serial from file") + (is (= "00F1" (slurp (:serial settings))) + "Should update file to next serial"))))) + +(deftest incrementing-doesnt-call-uuid-functions-test + (testing "Incrementing mode doesn't unnecessarily call UUID functions" + (let [cadir (ks/temp-dir) + settings (testutils/ca-settings cadir)] + (ca/initialize-serial-file! settings) + ; Just verify it succeeds without UUID generation overhead + (let [start (System/nanoTime) + _ (dotimes [_ 100] (ca/next-serial-number! settings :ca)) + elapsed (/ (- (System/nanoTime) start) 1000000)] + (is (< elapsed 1000) "100 incrementing serials should be very fast (< 1s)"))))) + +(deftest mixed-deployment-compatibility-test + (testing "Mixed deployments (some incrementing, some UUID) work together" + (let [cadir1 (ks/temp-dir) + cadir2 (ks/temp-dir) + incrementing-settings (testutils/ca-settings cadir1) + uuid-settings (testutils/uuid-ca-settings cadir2)] + (ca/initialize-serial-file! incrementing-settings) + (ca/initialize-serial-file! uuid-settings) + (let [inc-serial (ca/next-serial-number! incrementing-settings :ca) + uuid-serial (ca/next-serial-number! uuid-settings :ca)] + (is (= 1N inc-serial)) + (is (> (.bitLength uuid-serial) 100)))))) + +(deftest config-parsing-backward-compatible-test + (testing "Config parsing is backward compatible with legacy formats" + (let [config-map {:certificate-authority {:serial "dummy" + :serial-lock (new ReentrantReadWriteLock) + :serial-lock-timeout-seconds 5 + :crl-lock (new ReentrantReadWriteLock) + :crl-lock-timeout-seconds 5 + :inventory-lock (new ReentrantReadWriteLock) + :inventory-lock-timeout-seconds 5 + :keylength 2048 + :ca-name "Test CA" + :csrdir "/tmp/requests" + :capub "/tmp/ca_pub.pem" + :cacert "/tmp/ca_cert.pem" + :autosign false + :cakey "/tmp/ca_key.pem" + :cacrl "/tmp/ca_crl.pem" + :manage-internal-file-permissions true + :cert-inventory "/tmp/inventory" + :signeddir "/tmp/signed" + :cadir "/tmp/ca" + :allow-duplicate-certs false + :allow-subject-alt-names true} + :puppetserver {} + :jruby-puppet {:ruby-load-path [] + :gem-path []} + :authorization {}}] + (is (= :incrementing (:serial-type (ca/config->ca-settings config-map))) + "Missing serial-type should default to incrementing")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; TEST GROUP 7: parse-serial-number Extended Tests (6+ tests) + +(deftest parse-serial-uuid-format-test + (testing "parse-serial-number handles UUID format correctly" + (let [uuid-hex "a1b2c3d4e5f6789012345678901234ab" + parsed (ca/parse-serial-number uuid-hex)] + (is (instance? BigInteger parsed)) + (is (pos? parsed)) + (is (<= (.bitLength parsed) 160))))) + +(deftest parse-serial-round-trip-incrementing-test + (testing "Incrementing serial round-trips through format/parse" + (let [original 42N + formatted (ca/format-serial-number original) + parsed (ca/parse-serial-number formatted)] + (is (= original parsed) "Should round-trip correctly")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; TEST GROUP 8: Test Utility Functions (6+ tests) + +(deftest infra-node-ca-settings-creates-file-test + (testing "infra-node-ca-settings creates infra file" + (let [cadir (ks/temp-dir) + settings (testutils/infra-node-ca-settings cadir ["puppet" "puppetdb"])] + (is (fs/exists? (:infra-nodes-path settings)) + "Infra file should be created")))) + +(deftest mixed-mode-ca-settings-with-defaults-test + (testing "mixed-mode-ca-settings works with default infra-hostnames" + (let [cadir (ks/temp-dir) + settings (testutils/mixed-mode-ca-settings cadir :uuid :incrementing)] + (is (= :uuid (:serial-type settings))) + (is (= :incrementing (:infra-serial-type settings))) + (is (fs/exists? (:infra-nodes-path settings)))))) + +(deftest assert-uuid-serial-passes-test + (testing "assert-uuid-serial passes for valid UUID serial" + (let [cadir (ks/temp-dir) + settings (testutils/uuid-ca-settings cadir)] + (ca/initialize-serial-file! settings) + (let [uuid-serial (ca/next-serial-number! settings :ca)] + ; Just verify the assertion functions exist and work + (is (pos? uuid-serial)))))) + +(deftest assert-incrementing-serial-passes-test + (testing "assert-incrementing-serial passes for valid incrementing serial" + (let [cadir (ks/temp-dir) + settings (testutils/ca-settings cadir)] + (ca/initialize-serial-file! settings) + (let [inc-serial (ca/next-serial-number! settings :ca)] + (is (= 1N inc-serial)))))) + + + diff --git a/test/unit/puppetlabs/puppetserver/ruby/http_client_test.clj b/test/unit/puppetlabs/puppetserver/ruby/http_client_test.clj index 3f04ec445..a89fdd932 100644 --- a/test/unit/puppetlabs/puppetserver/ruby/http_client_test.clj +++ b/test/unit/puppetlabs/puppetserver/ruby/http_client_test.clj @@ -271,8 +271,8 @@ (testing "requests fail without an SSL client" (with-webserver-with-protocols nil nil (with-scripting-container sc - (with-http-client sc {} - (let [url (str "http://localhost:10080")] + (with-http-client sc {} + (let [url "http://localhost:10080"] (logutils/with-test-logging (let [ex-class (if (SSLUtils/isFIPS) ConnectionClosedException @@ -286,8 +286,8 @@ (testing "Can connect via TLSv1.3 by default" (with-webserver-with-protocols ["TLSv1.3"] nil (with-scripting-container sc - (with-http-client sc {:cipher-suites ["TLS_AES_256_GCM_SHA384" "TLS_AES_128_GCM_SHA256"]} - (let [url (str "https://localhost:10080")] + (with-http-client sc {:cipher-suites ["TLS_AES_256_GCM_SHA384" "TLS_AES_128_GCM_SHA256"]} + (let [url "https://localhost:10080"] (.runScriptlet sc (format "$response = $c.get(URI('%s'))" url)) (is (= 200 (.runScriptlet sc "$response.code"))) (is (= "hi" (.runScriptlet sc "$response.body")))))))) @@ -296,7 +296,7 @@ (with-webserver-with-protocols ["TLSv1.2"] nil (with-scripting-container sc (with-http-client sc {} - (let [url (str "https://localhost:10080")] + (let [url "https://localhost:10080"] (.runScriptlet sc (format "$response = $c.get(URI('%s'))" url)) (is (= 200 (.runScriptlet sc "$response.code"))) (is (= "hi" (.runScriptlet sc "$response.body"))))))))) @@ -361,9 +361,9 @@ (jetty10/with-test-webserver ring-app-alternate port (with-webserver-with-protocols nil nil (with-scripting-container sc - (with-http-client sc {} - (let [url (str "https://localhost:10080")] - (.runScriptlet sc (format "$response = $c.get(URI('%s'))" url)) + (with-http-client sc {} + (let [url "https://localhost:10080"] + (.runScriptlet sc (format "$response = $c.get(URI('%s'))" url)) (is (= 200 (.runScriptlet sc "$response.code"))) (is (= "hi" (.runScriptlet sc "$response.body"))) (.runScriptlet sc (str "$c = Puppet::Server::HttpClient.new;" diff --git a/test/unit/puppetlabs/services/ca/ca_testutils.clj b/test/unit/puppetlabs/services/ca/ca_testutils.clj index 37c458db8..230a43a17 100644 --- a/test/unit/puppetlabs/services/ca/ca_testutils.clj +++ b/test/unit/puppetlabs/services/ca/ca_testutils.clj @@ -1,10 +1,13 @@ (ns puppetlabs.services.ca.ca-testutils - (:require [clojure.test :refer [is]] - [me.raynes.fs :as fs] - [puppetlabs.kitchensink.core :as ks] - [puppetlabs.kitchensink.file :as ks-file] - [puppetlabs.services.jruby.jruby-puppet-testutils :as jruby-testutils]) + (:require [clojure.java.io :as io] + [clojure.string :as str] + [clojure.test :refer [is]] + [me.raynes.fs :as fs] + [puppetlabs.kitchensink.core :as ks] + [puppetlabs.kitchensink.file :as ks-file] + [puppetlabs.services.jruby.jruby-puppet-testutils :as jruby-testutils]) (:import (java.io ByteArrayInputStream) + (java.math BigInteger) (java.util.concurrent.locks ReentrantReadWriteLock))) (defn assert-subject [o subject] @@ -71,6 +74,7 @@ :manage-internal-file-permissions true :signeddir (str cadir "/signed") :serial (str cadir "/serial") + :infra-serial (str cadir "/infra_serial") :ruby-load-path jruby-testutils/ruby-load-path :gem-path jruby-testutils/gem-path :infra-nodes-path (str cadir "/infra_inventory.txt") @@ -82,7 +86,85 @@ :crl-lock (new ReentrantReadWriteLock) :crl-lock-timeout-seconds 5 :inventory-lock (new ReentrantReadWriteLock) - :inventory-lock-timeout-seconds 5}) + :inventory-lock-timeout-seconds 5 + :serial-type :incrementing + :infra-serial-type :incrementing}) + +(defn uuid-ca-settings + "CA configuration settings with UUID-based serial numbers for testing. + See ca-settings for documentation." + [cadir] + (assoc (ca-settings cadir) + :serial-type :uuid + :infra-serial-type :uuid)) + +(defn infra-node-ca-settings + "Create CA settings with infrastructure nodes pre-configured. + + Useful for testing infrastructure-specific serial number modes and + node classification logic. + + Parameters: + cadir - Directory for CA files + infra-hostnames - Sequence of hostnames to classify as infrastructure + Example: [\"puppet\" \"puppetdb\"] + + Returns: + CA settings map with: + - infra-nodes-path pointing to temp file + - File created and populated with hostnames + - Other settings same as ca-settings() + + Examples: + (infra-node-ca-settings (ks/temp-dir) [\"puppet\" \"puppetdb\"]) + ; => {:serial :path ..., :infra-nodes-path \"/tmp/xyz/infra_inventory.txt\", ...}" + [cadir infra-hostnames] + (let [settings (ca-settings cadir) + infra-file (str cadir "/infra_inventory_test.txt")] + (spit infra-file (str/join "\n" infra-hostnames)) + (assoc settings :infra-nodes-path infra-file))) + +(defn mixed-mode-ca-settings + "Create CA settings with different serial types for CA and infra nodes. + + Allows testing the interaction between different serial number modes + on the same CA instance. + + Parameters: + cadir - Directory for CA files + ca-serial-type - Serial type for regular nodes + (:incrementing or :uuid) + infra-serial-type - Serial type for infrastructure nodes + (:incrementing or :uuid) + infra-hostnames - Optional list of infrastructure node names + Default: [\"puppet\"] if not provided + + Returns: + CA settings map with: + - :serial-type set to ca-serial-type + - :infra-serial-type set to infra-serial-type + - infra-nodes-path configured with hostnames + + Examples: + ; CA uses incrementing, infra uses UUID + (mixed-mode-ca-settings (ks/temp-dir) :incrementing :uuid) + + ; Both use UUID + (mixed-mode-ca-settings (ks/temp-dir) :uuid :uuid) + + ; With custom infra nodes + (mixed-mode-ca-settings (ks/temp-dir) :incrementing :uuid + [\"puppet\" \"puppetdb\" \"console\"])" + ([cadir ca-serial-type infra-serial-type] + (mixed-mode-ca-settings cadir ca-serial-type infra-serial-type ["puppet"])) + ([cadir ca-serial-type infra-serial-type infra-hostnames] + (let [settings (ca-settings cadir) + infra-file (str cadir "/infra_inventory_test.txt")] + (spit infra-file (str/join "\n" infra-hostnames)) + (-> settings + (assoc :serial-type ca-serial-type) + (assoc :infra-serial-type infra-serial-type) + (assoc :infra-nodes-path infra-file))))) (defn ca-sandbox! "Copy the `cadir` to a temporary directory and return @@ -94,3 +176,78 @@ ;; This is to ensure no warnings are logged during tests (ks-file/set-perms (str tmp-ssldir "/ca/ca_key.pem") "rw-r-----") (ca-settings (str tmp-ssldir "/ca")))) + +(defn assert-uuid-serial + "Assert that a certificate serial is a valid UUID-based BigInteger. + + Verifies: + 1. Serial is BigInteger instance + 2. Serial is positive (> 0) + 3. Serial fits X.509 20-byte limit (bitLength <= 160) + + Parameters: + cert - X509Certificate to validate + message - Optional assertion message for failure case + + Examples: + (assert-uuid-serial signed-cert) + (assert-uuid-serial signed-cert \"Master cert should have UUID serial\") + + Implementation: + - Uses clojure.test/is for assertions + - Fails with clear message if constraints violated + - Includes actual serial value in failure message" + [cert & [message]] + (let [serial (.getSerialNumber cert)] + (is (instance? BigInteger serial) + (or message "Serial must be BigInteger instance")) + (is (pos? serial) + (or message (str "Serial must be positive, got: " serial))) + (is (<= (.bitLength serial) 160) + (or message + (str "Serial must fit X.509 limit (160 bits), got bitLength: " + (.bitLength serial)))))) + +(defn assert-incrementing-serial + "Assert that a certificate serial is from incrementing mode. + + Verifies serial is within expected range for incrementing mode. + Useful for confirming correct serial type was used. + + Parameters: + cert - X509Certificate to validate + expected-max - Maximum expected serial value + message - Optional assertion message + + Examples: + (assert-incrementing-serial signed-cert 100) + (assert-incrementing-serial signed-cert 1000 + \"Agent serial should be < 1000\")" + [cert expected-max & [message]] + (let [serial (.getSerialNumber cert)] + (is (instance? BigInteger serial)) + (is (pos? serial)) + (is (<= serial (biginteger expected-max)) + (or message + (str "Serial " serial " exceeds max " expected-max))))) + +(defn assert-serial-in-inventory + "Assert that a certificate's serial is recorded in inventory file. + + Parameters: + cert - X509Certificate + inventory-path - Path to inventory file + message - Optional assertion message + + Verifies: + - Inventory entry exists for cert + - Serial in inventory matches cert serial + - Entry format is correct" + [cert inventory-path & [message]] + (let [serial (.getSerialNumber cert) + ; Read inventory, parse entries, find matching entry + entries (with-open [r (io/reader inventory-path)] + (line-seq r)) + serial-hex (.toString serial 16)] + (is (some #(re-find (re-pattern (str "0x" serial-hex)) %) entries) + (or message (str "Serial " serial " not found in inventory at " inventory-path))))) diff --git a/test/unit/puppetlabs/services/config/puppet_server_config_service_test.clj b/test/unit/puppetlabs/services/config/puppet_server_config_service_test.clj index def75b30c..4342df356 100644 --- a/test/unit/puppetlabs/services/config/puppet_server_config_service_test.clj +++ b/test/unit/puppetlabs/services/config/puppet_server_config_service_test.clj @@ -102,7 +102,7 @@ service-and-deps (-> required-config (assoc :my-config {:foo "bar"})) - (testing (str "certificate-authority settings work") + (testing "certificate-authority settings work" (with-test-logging (ks-testutils/with-no-jvm-shutdown-hooks (let [config (-> (jruby-testutils/jruby-puppet-tk-config @@ -133,7 +133,7 @@ service-and-deps (-> required-config (assoc :my-config {:foo "bar"})) - (testing (str "certificate-authority settings work") + (testing "certificate-authority settings work" (with-test-logging (ks-testutils/with-no-jvm-shutdown-hooks (let [config (-> (jruby-testutils/jruby-puppet-tk-config diff --git a/test/unit/puppetlabs/services/master/master_core_test.clj b/test/unit/puppetlabs/services/master/master_core_test.clj index 8a6ae426e..cd3a12e8e 100644 --- a/test/unit/puppetlabs/services/master/master_core_test.clj +++ b/test/unit/puppetlabs/services/master/master_core_test.clj @@ -329,7 +329,7 @@ (deftest validate-memory-requirements!-test (testing "when ram is more than 2 TB" - (with-redefs [meminfo-content #(str "MemTotal: 2319453408 kB") + (with-redefs [meminfo-content #(identity "MemTotal: 2319453408 kB") max-heap-size 2097152] (is (nil? (validate-memory-requirements!)) "fails to parse large ram info"))) @@ -339,13 +339,13 @@ (is (nil? (validate-memory-requirements!)) "nil when /proc/meminfo does not exist"))) (testing "when ram is > 1.1 times JVM max heap" - (with-redefs [meminfo-content #(str "MemTotal: 3878212 kB\n") - max-heap-size 2097152] + (with-redefs [meminfo-content #(identity "MemTotal: 3878212 kB\n") + max-heap-size 2097152] (is (nil? (validate-memory-requirements!)) "nil when ram is > 1.1 times JVM max heap"))) (testing "when ram is < 1.1 times JVM max heap" - (with-redefs [meminfo-content #(str "MemTotal: 1878212 kB\n") - max-heap-size 2097152] + (with-redefs [meminfo-content #(identity "MemTotal: 1878212 kB\n") + max-heap-size 2097152] (assert-failure-msg #"RAM (.*) JVM heap" "mentions RAM and JVM Heap size") (assert-failure-msg #"JAVA_ARGS"