107107 type: list
108108 elements: raw
109109 version_added: 2.21.0
110+ get_certificate_chain:
111+ description:
112+ - If set to V(true), will obtain the certificate chain next to the certificate itself.
113+ - The chain as returned by the server can be found in RV(unverified_chain), and the chain that passed validation
114+ in RV(verified_chain).
115+ - B(Note) that this needs B(Python 3.10 or newer). Also note that only Python 3.13 or newer officially supports this.
116+ The module uses internal APIs of Python 3.10, 3.11, and 3.12 to achieve the same. It can be that future versions of
117+ Python 3.10, 3.11, or 3.12 break this.
118+ type: bool
119+ default: false
120+ version_added: 2.21.0
110121
111122notes:
112123 - When using ca_cert on OS X it has been reported that in some conditions the validate will always succeed.
113124
114125requirements:
115- - "Python >= 2.7 when using O(proxy_host)"
126+ - "Python >= 2.7 when using O(proxy_host), and Python >= 3.10 when O(get_certificate_chain=true) "
116127 - "cryptography >= 1.6"
117128
118129seealso:
189200 description: The version number of the certificate.
190201 returned: success
191202 type: str
203+ verified_chain:
204+ description:
205+ - The verified certificate chain retrieved from the port.
206+ - The first entry is always RV(cert).
207+ - The last certificate the root certificate the chain is traced to. If O(ca_cert) is provided this certificate is part of that store;
208+ otherwise it is part of the store used by default by Python.
209+ - Note that RV(unverified_chain) generally does not contain the root certificate, and might contain other certificates that are not part
210+ of the validated chain.
211+ returned: success and O(get_certificate_chain=true)
212+ type: list
213+ elements: str
214+ version_added: 2.21.0
215+ unverified_chain:
216+ description:
217+ - The certificate chain retrieved from the port.
218+ - The first entry is always RV(cert).
219+ returned: success and O(get_certificate_chain=true)
220+ type: list
221+ elements: str
222+ version_added: 2.21.0
192223'''
193224
194225EXAMPLES = '''
240271import base64
241272import traceback
242273import ssl
274+ import sys
243275
244276from os .path import isfile
245277from socket import create_connection , setdefaulttimeout , socket
@@ -317,6 +349,7 @@ def main():
317349 ciphers = dict (type = 'list' , elements = 'str' ),
318350 asn1_base64 = dict (type = 'bool' ),
319351 tls_ctx_options = dict (type = 'list' , elements = 'raw' ),
352+ get_certificate_chain = dict (type = 'bool' , default = False ),
320353 ),
321354 )
322355
@@ -330,7 +363,9 @@ def main():
330363 start_tls_server_type = module .params .get ('starttls' )
331364 ciphers = module .params .get ('ciphers' )
332365 asn1_base64 = module .params ['asn1_base64' ]
333- tls_ctx_options = module .params .get ('tls_ctx_options' )
366+ tls_ctx_options = module .params ['tls_ctx_options' ]
367+ get_certificate_chain = module .params ['get_certificate_chain' ]
368+
334369 if asn1_base64 is None :
335370 module .deprecate (
336371 'The default value `false` for asn1_base64 is deprecated and will change to `true` in '
@@ -341,6 +376,12 @@ def main():
341376 )
342377 asn1_base64 = False
343378
379+ if get_certificate_chain and sys .version_info < (3 , 10 ):
380+ module .fail_json (
381+ msg = 'get_certificate_chain=true can only be used with Python 3.10 (Python 3.13+ officially supports this). '
382+ 'The Python version used to run the get_certificate module is %s' % sys .version
383+ )
384+
344385 backend = module .params .get ('select_crypto_backend' )
345386 if backend == 'auto' :
346387 # Detection what is possible
@@ -371,6 +412,9 @@ def main():
371412 if not isfile (ca_cert ):
372413 module .fail_json (msg = "ca_cert file does not exist" )
373414
415+ verified_chain = None
416+ unverified_chain = None
417+
374418 if not HAS_CREATE_DEFAULT_CONTEXT :
375419 # Python < 2.7.9
376420 if proxy_host :
@@ -450,8 +494,43 @@ def main():
450494 except Exception as e :
451495 module .fail_json (msg = "Failed to add {0} to CTX options" .format (tls_ctx_option_str or tls_ctx_option_int ))
452496
453- cert = ctx .wrap_socket (sock , server_hostname = server_name or host ).getpeercert (True )
497+ tls_sock = ctx .wrap_socket (sock , server_hostname = server_name or host )
498+ cert = tls_sock .getpeercert (True )
454499 cert = DER_cert_to_PEM_cert (cert )
500+
501+ if get_certificate_chain :
502+ if sys .version_info < (3 , 13 ):
503+ # The official way to access this has been added in https://github.com/python/cpython/pull/109113/files.
504+ # We're basically doing the same for older Python versions. The internal API needed for this was added
505+ # in https://github.com/python/cpython/commit/666991fc598bc312d72aff0078ecb553f0a968f1, which was first
506+ # released in Python 3.10.0.
507+ def _convert_chain (chain ):
508+ if not chain :
509+ return []
510+ return [c .public_bytes (ssl ._ssl .ENCODING_DER ) for c in chain ]
511+
512+ ssl_obj = tls_sock ._sslobj # This is of type ssl._ssl._SSLSocket
513+ verified_der_chain = _convert_chain (ssl_obj .get_verified_chain ())
514+ unverified_der_chain = _convert_chain (ssl_obj .get_unverified_chain ())
515+ else :
516+ # This works with Python 3.13+
517+
518+ # Unfortunately due to a bug (https://github.com/python/cpython/issues/118658) some early pre-releases of
519+ # Python 3.13 do not return lists of byte strings, but lists of _ssl.Certificate objects. This is going to
520+ # be fixed by https://github.com/python/cpython/pull/118669. For now we convert the certificates ourselves
521+ # if they are not byte strings to work around this.
522+ def _convert_chain (chain ):
523+ return [
524+ c if isinstance (c , bytes ) else c .public_bytes (ssl ._ssl .ENCODING_DER )
525+ for c in chain
526+ ]
527+
528+ verified_der_chain = _convert_chain (tls_sock .get_verified_chain ())
529+ unverified_der_chain = _convert_chain (tls_sock .get_unverified_chain ())
530+
531+ verified_chain = [DER_cert_to_PEM_cert (c ) for c in verified_der_chain ]
532+ unverified_chain = [DER_cert_to_PEM_cert (c ) for c in unverified_der_chain ]
533+
455534 except Exception as e :
456535 if proxy_host :
457536 module .fail_json (msg = "Failed to get cert via proxy {0}:{1} from {2}:{3}, error: {4}" .format (
@@ -499,6 +578,11 @@ def main():
499578 else :
500579 result ['version' ] = "unknown"
501580
581+ if verified_chain is not None :
582+ result ['verified_chain' ] = verified_chain
583+ if unverified_chain is not None :
584+ result ['unverified_chain' ] = unverified_chain
585+
502586 module .exit_json (** result )
503587
504588
0 commit comments