@@ -43,6 +43,11 @@ def help
4343 * --target CERTNAME
4444 Clean the specified device certificate instead of this host's certificate.
4545
46+ * --if-expiring-in DURATION
47+ When renewing a certificate only renew if the certificate is valid for
48+ less than this amount of time. Duration can be specified as a time
49+ interval, such as 30s, 5m, 1h.
50+
4651 ACTIONS
4752 -------
4853
@@ -71,6 +76,11 @@ def help
7176 for subsequent requests. If there is already an existing certificate, it
7277 will be overwritten.
7378
79+ * renew_cert
80+ Renew an existing and non-expired client certificate. When
81+ `--if-expiring-in` option is specified, then renew the certificate only
82+ if it's going to expire in the amount of time given.
83+
7484 * verify:
7585 Verify the private key and certificate are present and match, verify the
7686 certificate is issued by a trusted CA, and check revocation status.
@@ -98,6 +108,28 @@ def help
98108 option ( '--localca' )
99109 option ( '--verbose' , '-v' )
100110 option ( '--debug' , '-d' )
111+ option ( '--if-expiring-in DURATION' ) do |arg |
112+ options [ :expiring_in_sec ] = parse_duration ( arg )
113+ end
114+
115+ def parse_duration ( value )
116+ unit_map = {
117+ "y" => 365 * 24 * 60 * 60 ,
118+ "d" => 24 * 60 * 60 ,
119+ "h" => 60 * 60 ,
120+ "m" => 60 ,
121+ "s" => 1
122+ }
123+ format = /^(\d +)(y|d|h|m|s)?$/
124+
125+ v = ( value . is_a? ( Integer ) ? "#{ value } s" : value )
126+
127+ if v =~ format
128+ Regexp . last_match ( 1 ) . to_i * unit_map [ ::Regexp . last_match ( 2 ) || 's' ]
129+ else
130+ raise ArgumentError , "Invalid duration format: #{ value } "
131+ end
132+ end
101133
102134 def initialize ( command_line = Puppet ::Util ::CommandLine . new )
103135 super ( command_line )
@@ -148,6 +180,8 @@ def main
148180 unless cert
149181 raise Puppet ::Error , _ ( "The certificate for '%{name}' has not yet been signed" ) % { name : certname }
150182 end
183+ when 'renew_cert'
184+ renew_cert ( certname , options [ :expiring_in_sec ] )
151185 when 'generate_request'
152186 generate_request ( certname )
153187 when 'verify'
@@ -248,6 +282,37 @@ def download_cert(ssl_context)
248282 raise Puppet ::Error . new ( _ ( "Failed to download certificate: %{message}" ) % { message : e . message } , e )
249283 end
250284
285+ def renew_cert ( certname , expiring_in_sec_maybe )
286+ ssl_context = @ssl_provider . load_context ( certname : certname )
287+
288+ if expiring_in_sec_maybe && ( ssl_context [ :client_cert ] . not_after - Time . now ) > expiring_in_sec_maybe
289+ Puppet . info _ ( "Certificate '%{name}' is still valid until %{date}" ) % { name : certname , date : ssl_context [ :client_cert ] . not_after }
290+ return ssl_context [ :client_cert ]
291+ end
292+
293+ Puppet . debug _ ( "Renewing certificate '%{name}'" ) % { name : certname }
294+ route = create_route ( ssl_context )
295+ _ , x509 = route . post_certificate_renewal ( ssl_context )
296+ cert = OpenSSL ::X509 ::Certificate . new ( x509 )
297+ Puppet . notice _ ( "Downloaded certificate '%{name}' with fingerprint %{fingerprint}" ) % { name : certname , fingerprint : fingerprint ( cert ) }
298+
299+ # verify client cert before saving
300+ @ssl_provider . create_context (
301+ cacerts : ssl_context . cacerts , crls : ssl_context . crls , private_key : ssl_context . private_key , client_cert : cert
302+ )
303+ @cert_provider . save_client_cert ( certname , cert )
304+ @cert_provider . delete_request ( certname )
305+ cert
306+ rescue Puppet ::HTTP ::ResponseError => e
307+ if e . response . code == 404
308+ nil
309+ else
310+ raise Puppet ::Error . new ( _ ( "Failed to download certificate: %{message}" ) % { message : e . message } , e )
311+ end
312+ rescue => e
313+ raise Puppet ::Error . new ( _ ( "Failed to download certificate: %{message}" ) % { message : e . message } , e )
314+ end
315+
251316 def verify ( certname )
252317 password = @cert_provider . load_private_key_password
253318 ssl_context = @ssl_provider . load_context ( certname : certname , password : password )
0 commit comments