acme-0.31.0/0000755000076600000240000000000013427120450012407 5ustar bmwstaff00000000000000acme-0.31.0/PKG-INFO0000644000076600000240000000175713427120450013516 0ustar bmwstaff00000000000000Metadata-Version: 2.1 Name: acme Version: 0.31.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project Author-email: client-dev@letsencrypt.org License: Apache License 2.0 Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Provides-Extra: docs Provides-Extra: dev acme-0.31.0/pytest.ini0000644000076600000240000000007713427120435014447 0ustar bmwstaff00000000000000[pytest] norecursedirs = .* build dist CVS _darcs {arch} *.egg acme-0.31.0/acme/0000755000076600000240000000000013427120450013314 5ustar bmwstaff00000000000000acme-0.31.0/acme/jws.py0000644000076600000240000000413313427120435014475 0ustar bmwstaff00000000000000"""ACME-specific JWS. The JWS implementation in josepy only implements the base JOSE standard. In order to support the new header fields defined in ACME, this module defines some ACME-specific classes that layer on top of josepy. """ import josepy as jose class Header(jose.Header): """ACME-specific JOSE Header. Implements nonce, kid, and url. """ nonce = jose.Field('nonce', omitempty=True, encoder=jose.encode_b64jose) kid = jose.Field('kid', omitempty=True) url = jose.Field('url', omitempty=True) @nonce.decoder def nonce(value): # pylint: disable=missing-docstring,no-self-argument try: return jose.decode_b64jose(value) except jose.DeserializationError as error: # TODO: custom error raise jose.DeserializationError("Invalid nonce: {0}".format(error)) class Signature(jose.Signature): """ACME-specific Signature. Uses ACME-specific Header for customer fields.""" __slots__ = jose.Signature._orig_slots # pylint: disable=no-member # TODO: decoder/encoder should accept cls? Otherwise, subclassing # JSONObjectWithFields is tricky... header_cls = Header header = jose.Field( 'header', omitempty=True, default=header_cls(), decoder=header_cls.from_json) # TODO: decoder should check that nonce is in the protected header class JWS(jose.JWS): """ACME-specific JWS. Includes none, url, and kid in protected header.""" signature_cls = Signature __slots__ = jose.JWS._orig_slots # pylint: disable=no-member @classmethod # pylint: disable=arguments-differ,too-many-arguments def sign(cls, payload, key, alg, nonce, url=None, kid=None): # Per ACME spec, jwk and kid are mutually exclusive, so only include a # jwk field if kid is not provided. include_jwk = kid is None return super(JWS, cls).sign(payload, key=key, alg=alg, protect=frozenset(['nonce', 'url', 'kid', 'jwk', 'alg']), nonce=nonce, url=url, kid=kid, include_jwk=include_jwk) acme-0.31.0/acme/testdata/0000755000076600000240000000000013427120450015125 5ustar bmwstaff00000000000000acme-0.31.0/acme/testdata/csr-100sans.pem0000644000076600000240000000502113427120435017603 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIHNTCCBt8CAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 lUTor4R0T+3C5QIDAQABoIIGFDCCBhAGCSqGSIb3DQEJDjGCBgEwggX9MAkGA1Ud EwQCMAAwCwYDVR0PBAQDAgXgMIIF4QYDVR0RBIIF2DCCBdSCDGV4YW1wbGUxLmNv bYIMZXhhbXBsZTIuY29tggxleGFtcGxlMy5jb22CDGV4YW1wbGU0LmNvbYIMZXhh bXBsZTUuY29tggxleGFtcGxlNi5jb22CDGV4YW1wbGU3LmNvbYIMZXhhbXBsZTgu Y29tggxleGFtcGxlOS5jb22CDWV4YW1wbGUxMC5jb22CDWV4YW1wbGUxMS5jb22C DWV4YW1wbGUxMi5jb22CDWV4YW1wbGUxMy5jb22CDWV4YW1wbGUxNC5jb22CDWV4 YW1wbGUxNS5jb22CDWV4YW1wbGUxNi5jb22CDWV4YW1wbGUxNy5jb22CDWV4YW1w bGUxOC5jb22CDWV4YW1wbGUxOS5jb22CDWV4YW1wbGUyMC5jb22CDWV4YW1wbGUy MS5jb22CDWV4YW1wbGUyMi5jb22CDWV4YW1wbGUyMy5jb22CDWV4YW1wbGUyNC5j b22CDWV4YW1wbGUyNS5jb22CDWV4YW1wbGUyNi5jb22CDWV4YW1wbGUyNy5jb22C DWV4YW1wbGUyOC5jb22CDWV4YW1wbGUyOS5jb22CDWV4YW1wbGUzMC5jb22CDWV4 YW1wbGUzMS5jb22CDWV4YW1wbGUzMi5jb22CDWV4YW1wbGUzMy5jb22CDWV4YW1w bGUzNC5jb22CDWV4YW1wbGUzNS5jb22CDWV4YW1wbGUzNi5jb22CDWV4YW1wbGUz Ny5jb22CDWV4YW1wbGUzOC5jb22CDWV4YW1wbGUzOS5jb22CDWV4YW1wbGU0MC5j b22CDWV4YW1wbGU0MS5jb22CDWV4YW1wbGU0Mi5jb22CDWV4YW1wbGU0My5jb22C DWV4YW1wbGU0NC5jb22CDWV4YW1wbGU0NS5jb22CDWV4YW1wbGU0Ni5jb22CDWV4 YW1wbGU0Ny5jb22CDWV4YW1wbGU0OC5jb22CDWV4YW1wbGU0OS5jb22CDWV4YW1w bGU1MC5jb22CDWV4YW1wbGU1MS5jb22CDWV4YW1wbGU1Mi5jb22CDWV4YW1wbGU1 My5jb22CDWV4YW1wbGU1NC5jb22CDWV4YW1wbGU1NS5jb22CDWV4YW1wbGU1Ni5j b22CDWV4YW1wbGU1Ny5jb22CDWV4YW1wbGU1OC5jb22CDWV4YW1wbGU1OS5jb22C DWV4YW1wbGU2MC5jb22CDWV4YW1wbGU2MS5jb22CDWV4YW1wbGU2Mi5jb22CDWV4 YW1wbGU2My5jb22CDWV4YW1wbGU2NC5jb22CDWV4YW1wbGU2NS5jb22CDWV4YW1w bGU2Ni5jb22CDWV4YW1wbGU2Ny5jb22CDWV4YW1wbGU2OC5jb22CDWV4YW1wbGU2 OS5jb22CDWV4YW1wbGU3MC5jb22CDWV4YW1wbGU3MS5jb22CDWV4YW1wbGU3Mi5j b22CDWV4YW1wbGU3My5jb22CDWV4YW1wbGU3NC5jb22CDWV4YW1wbGU3NS5jb22C DWV4YW1wbGU3Ni5jb22CDWV4YW1wbGU3Ny5jb22CDWV4YW1wbGU3OC5jb22CDWV4 YW1wbGU3OS5jb22CDWV4YW1wbGU4MC5jb22CDWV4YW1wbGU4MS5jb22CDWV4YW1w bGU4Mi5jb22CDWV4YW1wbGU4My5jb22CDWV4YW1wbGU4NC5jb22CDWV4YW1wbGU4 NS5jb22CDWV4YW1wbGU4Ni5jb22CDWV4YW1wbGU4Ny5jb22CDWV4YW1wbGU4OC5j b22CDWV4YW1wbGU4OS5jb22CDWV4YW1wbGU5MC5jb22CDWV4YW1wbGU5MS5jb22C DWV4YW1wbGU5Mi5jb22CDWV4YW1wbGU5My5jb22CDWV4YW1wbGU5NC5jb22CDWV4 YW1wbGU5NS5jb22CDWV4YW1wbGU5Ni5jb22CDWV4YW1wbGU5Ny5jb22CDWV4YW1w bGU5OC5jb22CDWV4YW1wbGU5OS5jb22CDmV4YW1wbGUxMDAuY29tMA0GCSqGSIb3 DQEBCwUAA0EAW05UMFavHn2rkzMyUfzsOvWzVNlm43eO2yHu5h5TzDb23gkDnNEo duUAbQ+CLJHYd+MvRCmPQ+3ZnaPy7l/0Hg== -----END CERTIFICATE REQUEST----- acme-0.31.0/acme/testdata/rsa2048_cert.pem0000644000076600000240000000241613427120435017756 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV BAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do ZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx MzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC V0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy dGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN PSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm 7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn xvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD g7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1 RAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU uQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T RiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx nkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8 kTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+ aonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P AfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf Esg= -----END CERTIFICATE----- acme-0.31.0/acme/testdata/csr-nosans.pem0000644000076600000240000000070413427120435017722 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA= -----END CERTIFICATE REQUEST----- acme-0.31.0/acme/testdata/csr-idnsans.pem0000644000076600000240000000323313427120435020060 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIEpzCCBFECAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 lUTor4R0T+3C5QIDAQABoIIDhjCCA4IGCSqGSIb3DQEJDjGCA3MwggNvMAkGA1Ud EwQCMAAwCwYDVR0PBAQDAgXgMIIDUwYDVR0RBIIDSjCCA0aCYs+Dz4TPhc+Gz4fP iM+Jz4rPi8+Mz43Pjs+Pz5DPkc+Sz5PPlM+Vz5bPl8+Yz5nPms+bz5zPnc+ez5/P oM+hz6LPo8+kz6XPps+nz6jPqc+qz6vPrM+tz67Pry5pbnZhbGlkgmLPsM+xz7LP s8+0z7XPts+3z7jPuc+6z7vPvM+9z77Pv9mB2YLZg9mE2YXZhtmH2YjZidmK2YvZ jNmN2Y7Zj9mQ2ZHZktmT2ZTZldmW2ZfZmNmZ2ZrZm9mc2Z0uaW52YWxpZIJi2Z7Z n9mg2aHZotmj2aTZpdmm2afZqNmp2arZq9ms2a3Zrtmv2bDZsdmy2bPZtNm12bbZ t9m42bnZutm72bzZvdm+2b/agNqB2oLag9qE2oXahtqH2ojaidqKLmludmFsaWSC YtqL2ozajdqO2o/akNqR2pLak9qU2pXaltqX2pjamdqa2pvanNqd2p7an9qg2qHa otqj2qTapdqm2qfaqNqp2qraq9qs2q3artqv2rDasdqy2rPatNq12rbaty5pbnZh bGlkgmLauNq52rrau9q82r3avtq/24DbgduC24PbhNuF24bbh9uI24nbituL24zb jduO24/bkNuR25Lbk9uU25XbltuX25jbmdua25vbnNud257bn9ug26Hbotuj26Qu aW52YWxpZIJ426Xbptun26jbqduq26vbrNut267br9uw27Hbstuz27Tbtdu227fb uNu527rbu+GgoOGgoeGgouGgo+GgpOGgpeGgpuGgp+GgqOGgqeGgquGgq+GgrOGg reGgruGgr+GgsOGgseGgsuGgs+GgtOGgtS5pbnZhbGlkgoGP4aC24aC34aC44aC5 4aC64aC74aC84aC94aC+4aC/4aGA4aGB4aGC4aGD4aGE4aGF4aGG4aGH4aGI4aGJ 4aGK4aGL4aGM4aGN4aGO4aGP4aGQ4aGR4aGS4aGT4aGU4aGV4aGW4aGX4aGY4aGZ 4aGa4aGb4aGc4aGd4aGe4aGf4aGg4aGh4aGiLmludmFsaWSCROGho+GhpOGhpeGh puGhp+GhqOGhqeGhquGhq+GhrOGhreGhruGhr+GhsOGhseGhsuGhs+GhtOGhteGh ti5pbnZhbGlkMA0GCSqGSIb3DQEBCwUAA0EAeNkY0M0+kMnjRo6dEUoGE4dX9fEr dfGrpPUBcwG0P5QBdZJWvZxTfRl14yuPYHbGHULXeGqRdkU6HK5pOlzpng== -----END CERTIFICATE REQUEST----- acme-0.31.0/acme/testdata/rsa256_key.pem0000644000076600000240000000045213427120435017526 0ustar bmwstaff00000000000000-----BEGIN RSA PRIVATE KEY----- MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3 rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt -----END RSA PRIVATE KEY----- acme-0.31.0/acme/testdata/rsa2048_key.pem0000644000076600000240000000325013427120435017606 0ustar bmwstaff00000000000000-----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq r5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo VpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW AEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ MmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W Gju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp v+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3 3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO z7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB o35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK lt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH Vqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a hUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/ IkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/ 9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP n94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj 9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy ftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb f4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3 YEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3 xFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG wifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL Hh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK jUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh QxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6 kmf1UgGFcKrJuXgwEtTVxw== -----END PRIVATE KEY----- acme-0.31.0/acme/testdata/cert-san.pem0000644000076600000240000000142213427120435017346 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE----- MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7 RDjyGMKy5ZgM2w== -----END CERTIFICATE----- acme-0.31.0/acme/testdata/rsa512_key.pem0000644000076600000240000000075513427120435017527 0ustar bmwstaff00000000000000-----BEGIN RSA PRIVATE KEY----- MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79 vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj 8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq 6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/ -----END RSA PRIVATE KEY----- acme-0.31.0/acme/testdata/cert.der0000644000076600000240000000140313427120435016557 0ustar bmwstaff0000000000000000 2WZd0  *H  010U example.com0 151028072441Z 151127072441Z010U example.com0"0  *H 0 |0w3$Gav1.k>=ae\u[DnXߋ":ājP]teMp*8(if3-/e\vui DKlK˷ nal3g3$aLOGL) 3S񪓪̀,˞j s|H"0yc_M=.ۢh5JIh^+WɯFWs;?Gs9_̆eƞT4)kP0N0U6  )+eX70U#06  )+eX70 U00  *H  T2O]J7 ԣN^ x1[IK,=^2k뮫mHG[2-mlhfll@'DpH4C'{[C c60u;d FAzk&eP Ǫa3EuPjW3u ,S~C6Q8YOfs UpM!ȑ)+m,(hE {b KRVhŗ ;lv1acme-0.31.0/acme/testdata/rsa1024_key.pem0000644000076600000240000000156713427120435017610 0ustar bmwstaff00000000000000-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQCaifO0fGlcAcjjcYEAPYcIL0Hf0KiNa9VCJ14RBdlZxLWRrVFi 4tdNCKSKqzKuKrrA8DWd4PHFD7UpLyRrPPXY6GozAyCT+5UFBClGJ2KyNKu+eU6/ w4C1kpO4lpeXs8ptFc1lA9P8V1M/MkWzTE402nPNK0uUmZNo2tsFpGJUSQIDAQAB AoGAFjLWxQhSAhtnhfRZ+XTdHrnbFpFchOQGgDgzdPKIJDLzefeRh0jacIBbUmgB Ia+Vn/1hVkpnsEzvUvkonBbnoYWlYVQdpNTmrrew7SOztf8/1fYCsSkyDAvqGTXc TmHM0PaLS+junoWcKOvQRVb0N3k+43OnBkr2b393Sx30qGECQQDNO2IBWOsYs8cB CZQAZs8zBlbwBFZibqovqpLwXt9adBIsT9XzgagGbJMpzSuoHTUn3QqqJd9uHD8X UTmmoh4NAkEAwMRauo+PlNj8W1lusflko52KL17+E5cmeOERM2xvhZNpO7d3/1ak Co9dxVMicrYSh7jXbcXFNt3xNDTv6Dg8LQJAPuJwMDt/pc0IMCAwMkNOP7M0lkyt 73E7QmnAplhblcq0+tDnnLpgsr84BHnyY4u3iuRm7SW3pXSQPGPOB2nrTQJANBXa HgakWSe4KEal7ljgpITwzZPxOwHgV1EZALgP+hu2l3gfaFLUyDWstKCd8jjYEOwU 6YhCnWyiu+SB3lEzkQJBAJapJpfypFyY8kQNYlYILLBcPu5fmy3QUZKHJ4L3rIVJ c2UTLMeBBgGFHT04CtWntmjwzSv+V6lwiCxKXsIUySc= -----END RSA PRIVATE KEY----- acme-0.31.0/acme/testdata/cert.pem0000644000076600000240000000130513427120435016567 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE----- MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn B/o= -----END CERTIFICATE----- acme-0.31.0/acme/testdata/critical-san.pem0000644000076600000240000000322313427120435020204 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE----- MIIErTCCA5WgAwIBAgIKETb7VQAAAAAdGTANBgkqhkiG9w0BAQsFADCBkTELMAkG A1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5 MRUwEwYDVQQKEwxWZW5hZmksIEluYy4xHzAdBgNVBAsTFkRlbW9uc3RyYXRpb24g U2VydmljZXMxIjAgBgNVBAMTGVZlbmFmaSBFeGFtcGxlIElzc3VpbmcgQ0EwHhcN MTcwNzEwMjMxNjA1WhcNMTcwODA5MjMxNjA1WjAAMIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEA7CU5qRIzCs9hCRiSUvLZ8r81l4zIYbx1V1vZz6x1cS4M 0keNfFJ1wB+zuvx80KaMYkWPYlg4Rsm9Ok3ZapakXDlaWtrfg78lxtHuPw1o7AYV EXDwwPkNugLMJfYw5hWYSr8PCLcOJoY00YQ0fJ44L+kVsUyGjN4UTRRZmOh/yNVU 0W12dTCz4X7BAW01OuY6SxxwewnW3sBEep+APfr2jd/oIx7fgZmVB8aRCDPj4AFl XINWIwxmptOwnKPbwLN/vhCvJRUkO6rA8lpYwQkedFf6fHhqi2Sq/NCEOg4RvMCF fKbMpncOXxz+f4/i43SVLrPz/UyhjNbKGJZ+zFrQowIDAQABo4IBlTCCAZEwPgYD VR0RAQH/BDQwMoIbY2hpY2Fnby1jdWJzLnZlbmFmaS5leGFtcGxlghNjdWJzLnZl bmFmaS5leGFtcGxlMB0GA1UdDgQWBBTgKZXVSFNyPHHtO/phtIALPcCF5DAfBgNV HSMEGDAWgBT/JJ6Wei/pzf+9DRHuv6Wgdk2HsjBSBgNVHR8ESzBJMEegRaBDhkFo dHRwOi8vcGtpLnZlbmFmaS5leGFtcGxlL2NybC9WZW5hZmklMjBFeGFtcGxlJTIw SXNzdWluZyUyMENBLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0 dHA6Ly9wa2kudmVuYWZpLmV4YW1wbGUvb2NzcDAOBgNVHQ8BAf8EBAMCBaAwPQYJ KwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhIDLGYTvsSSEnZ8ehvD5UofP4hMEgobv DIGy4mcCAWQCAQIwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGwYJKwYBBAGCNxUKBA4w DDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA3YW4t1AzxEn384OqdU6L ny8XkMhWpRM0W0Z9ZC3gRZKbVUu49nG/KB5hbVn/de33zdX9HOZJKc0vXzkGZQUs OUCCsKX4VKzV5naGXOuGRbvV4CJh5P0kPlDzyb5t312S49nJdcdBf0Y/uL5Qzhst bXy8qNfFNG3SIKKRAUpqE9OVIl+F+JBwexa+v/4dFtUOqMipfXxB3TaxnDqvU1dS yO34ZTvIMGXJIZ5nn/d/LNc3N3vBg2SHkMpladqw0Hr7mL0bFOe0b+lJgkDP06Be n08fikhz1j2AW4/ZHa9w4DUz7J21+RtHMhh+Vd1On0EAeZ563svDe7Z+yrg6zOVv KA== -----END CERTIFICATE-----acme-0.31.0/acme/testdata/README0000644000076600000240000000100613427120435016005 0ustar bmwstaff00000000000000In order for acme.test_util._guess_loader to work properly, make sure to use appropriate extension for vector filenames: .pem for PEM and .der for DER. The following command has been used to generate test keys: for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done and for the CSR: openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der and for the certificate: openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der acme-0.31.0/acme/testdata/cert-nocn.der0000644000076600000240000000256513427120435017524 0ustar bmwstaff000000000000000q0Yob6^%0  *H  01 0 UGB10UGreater Manchester10USalford10U COMODO CA Limited1<0:U3COMODO RSA Organization Validation Secure Server CA0 170323000000Z 200622235959Z0k1 0 UUS10U California10U Walnut Creek10U  Lucas Garron10U Multi-Domain SSL0"0  *H 0 ³PpX̓\hI"+,v>lK`LL겴rɆ\A]+qK.P7gaABF't R&!slLyUk=HR(۪n`Jwk9$1MSmB|3H"qzl(7Jlr~ZiWD Gt\A"").I '@rٌWSkT@'spiAEU|d?=,é|Lʅ00U#0+ϭO/*HH*B$0U{ :Giq}7?0U0 U00U%0++0PU I0G0; +10+0)+https://secure.comodo.com/CPS0g 0ZUS0Q0OMKIhttp://crl.comodoca.com/COMODORSAOrganizationValidationSecureServerCA.crl0+0}0U+0Ihttp://crt.comodoca.com/COMODORSAOrganizationValidationSecureServerCA.crt0$+0http://ocsp.comodoca.com0$U0no-common-name.badssl.com0  *H  i%Ff4DH:kD{|p5&J;~Zw-J/wUؔ?3Hb5Vh3-fRjEZm؝,QN?lْ-I0q.gd{҅ӣMBi$Kn:ħ)k!8LFÿ2WiKj,F;{facme-0.31.0/acme/testdata/csr-6sans.pem0000644000076600000240000000124413427120435017453 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0 9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG 9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv IvzVBz/nD11drfz/RNuX -----END CERTIFICATE REQUEST----- acme-0.31.0/acme/testdata/cert-idnsans.pem0000644000076600000240000000351213427120435020226 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE----- MIIFNjCCBOCgAwIBAgIJAP4rNqqOKifCMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X DTE2MDEwNjIwMDg1OFoXDTE2MDEwNzIwMDg1OFowZDELMAkGA1UECAwCQ0ExFjAU BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IDczCCA28wCQYDVR0T BAIwADALBgNVHQ8EBAMCBeAwggNTBgNVHREEggNKMIIDRoJiz4PPhM+Fz4bPh8+I z4nPis+Lz4zPjc+Oz4/PkM+Rz5LPk8+Uz5XPls+Xz5jPmc+az5vPnM+dz57Pn8+g z6HPos+jz6TPpc+mz6fPqM+pz6rPq8+sz63Prs+vLmludmFsaWSCYs+wz7HPss+z z7TPtc+2z7fPuM+5z7rPu8+8z73Pvs+/2YHZgtmD2YTZhdmG2YfZiNmJ2YrZi9mM 2Y3ZjtmP2ZDZkdmS2ZPZlNmV2ZbZl9mY2ZnZmtmb2ZzZnS5pbnZhbGlkgmLZntmf 2aDZodmi2aPZpNml2abZp9mo2anZqtmr2azZrdmu2a/ZsNmx2bLZs9m02bXZttm3 2bjZudm62bvZvNm92b7Zv9qA2oHagtqD2oTahdqG2ofaiNqJ2oouaW52YWxpZIJi 2ovajNqN2o7aj9qQ2pHaktqT2pTaldqW2pfamNqZ2pram9qc2p3antqf2qDaodqi 2qPapNql2qbap9qo2qnaqtqr2qzardqu2q/asNqx2rLas9q02rXattq3LmludmFs aWSCYtq42rnautq72rzavdq+2r/bgNuB24Lbg9uE24XbhtuH24jbiduK24vbjNuN 247bj9uQ25HbktuT25TblduW25fbmNuZ25rbm9uc253bntuf26Dbodui26PbpC5p bnZhbGlkgnjbpdum26fbqNup26rbq9us263brtuv27Dbsduy27PbtNu127bbt9u4 27nbutu74aCg4aCh4aCi4aCj4aCk4aCl4aCm4aCn4aCo4aCp4aCq4aCr4aCs4aCt 4aCu4aCv4aCw4aCx4aCy4aCz4aC04aC1LmludmFsaWSCgY/hoLbhoLfhoLjhoLnh oLrhoLvhoLzhoL3hoL7hoL/hoYDhoYHhoYLhoYPhoYThoYXhoYbhoYfhoYjhoYnh oYrhoYvhoYzhoY3hoY7hoY/hoZDhoZHhoZLhoZPhoZThoZXhoZbhoZfhoZjhoZnh oZrhoZvhoZzhoZ3hoZ7hoZ/hoaDhoaHhoaIuaW52YWxpZIJE4aGj4aGk4aGl4aGm 4aGn4aGo4aGp4aGq4aGr4aGs4aGt4aGu4aGv4aGw4aGx4aGy4aGz4aG04aG14aG2 LmludmFsaWQwDQYJKoZIhvcNAQELBQADQQAzOQL/54yXxln87/YvEQbBm9ik9zoT TxEkvnZ4kmTRhDsUPtRjMXhY2FH7LOtXKnJQ7POUB7AsJ2Z6uq2w623G -----END CERTIFICATE----- acme-0.31.0/acme/testdata/csr.der0000644000076600000240000000113713427120435016415 0ustar bmwstaff000000000000000[0C010U example.com0"0  *H 0 |0w3$Gav1.k>=ae\u[DnXߋ":ājP]teMp*8(if3-/e\vui DKlK˷ nal3g3$aLOGL) 3S񪓪̀,˞j s|H"0yc_M=.ۢh5JIh^+WɯFWs;?Gs9_̆eƞT4)k0  *H  r0J0*;s k[-I%I!6gu%i8O}D6>:}_QMKYB4a861p @TEXUBï Pr>h6(.Μhw ٖqaQ_PkN=EКS?E %OdѻK=.gh첶Be+_)uUusZQÍV8s0Qr;`z=FIacme-0.31.0/acme/testdata/csr.pem0000644000076600000240000000104613427120435016423 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G n9XBE1N9W6HCIEut2d8wACg= -----END CERTIFICATE REQUEST----- acme-0.31.0/acme/testdata/cert-100sans.pem0000644000076600000240000000530013427120435017751 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE----- MIIHxDCCB26gAwIBAgIJAOGrG1Un9lHiMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X DTE2MDEwNjE5MDkzN1oXDTE2MDEwNzE5MDkzN1owZDELMAkGA1UECAwCQ0ExFjAU BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IGATCCBf0wCQYDVR0T BAIwADALBgNVHQ8EBAMCBeAwggXhBgNVHREEggXYMIIF1IIMZXhhbXBsZTEuY29t ggxleGFtcGxlMi5jb22CDGV4YW1wbGUzLmNvbYIMZXhhbXBsZTQuY29tggxleGFt cGxlNS5jb22CDGV4YW1wbGU2LmNvbYIMZXhhbXBsZTcuY29tggxleGFtcGxlOC5j b22CDGV4YW1wbGU5LmNvbYINZXhhbXBsZTEwLmNvbYINZXhhbXBsZTExLmNvbYIN ZXhhbXBsZTEyLmNvbYINZXhhbXBsZTEzLmNvbYINZXhhbXBsZTE0LmNvbYINZXhh bXBsZTE1LmNvbYINZXhhbXBsZTE2LmNvbYINZXhhbXBsZTE3LmNvbYINZXhhbXBs ZTE4LmNvbYINZXhhbXBsZTE5LmNvbYINZXhhbXBsZTIwLmNvbYINZXhhbXBsZTIx LmNvbYINZXhhbXBsZTIyLmNvbYINZXhhbXBsZTIzLmNvbYINZXhhbXBsZTI0LmNv bYINZXhhbXBsZTI1LmNvbYINZXhhbXBsZTI2LmNvbYINZXhhbXBsZTI3LmNvbYIN ZXhhbXBsZTI4LmNvbYINZXhhbXBsZTI5LmNvbYINZXhhbXBsZTMwLmNvbYINZXhh bXBsZTMxLmNvbYINZXhhbXBsZTMyLmNvbYINZXhhbXBsZTMzLmNvbYINZXhhbXBs ZTM0LmNvbYINZXhhbXBsZTM1LmNvbYINZXhhbXBsZTM2LmNvbYINZXhhbXBsZTM3 LmNvbYINZXhhbXBsZTM4LmNvbYINZXhhbXBsZTM5LmNvbYINZXhhbXBsZTQwLmNv bYINZXhhbXBsZTQxLmNvbYINZXhhbXBsZTQyLmNvbYINZXhhbXBsZTQzLmNvbYIN ZXhhbXBsZTQ0LmNvbYINZXhhbXBsZTQ1LmNvbYINZXhhbXBsZTQ2LmNvbYINZXhh bXBsZTQ3LmNvbYINZXhhbXBsZTQ4LmNvbYINZXhhbXBsZTQ5LmNvbYINZXhhbXBs ZTUwLmNvbYINZXhhbXBsZTUxLmNvbYINZXhhbXBsZTUyLmNvbYINZXhhbXBsZTUz LmNvbYINZXhhbXBsZTU0LmNvbYINZXhhbXBsZTU1LmNvbYINZXhhbXBsZTU2LmNv bYINZXhhbXBsZTU3LmNvbYINZXhhbXBsZTU4LmNvbYINZXhhbXBsZTU5LmNvbYIN ZXhhbXBsZTYwLmNvbYINZXhhbXBsZTYxLmNvbYINZXhhbXBsZTYyLmNvbYINZXhh bXBsZTYzLmNvbYINZXhhbXBsZTY0LmNvbYINZXhhbXBsZTY1LmNvbYINZXhhbXBs ZTY2LmNvbYINZXhhbXBsZTY3LmNvbYINZXhhbXBsZTY4LmNvbYINZXhhbXBsZTY5 LmNvbYINZXhhbXBsZTcwLmNvbYINZXhhbXBsZTcxLmNvbYINZXhhbXBsZTcyLmNv bYINZXhhbXBsZTczLmNvbYINZXhhbXBsZTc0LmNvbYINZXhhbXBsZTc1LmNvbYIN ZXhhbXBsZTc2LmNvbYINZXhhbXBsZTc3LmNvbYINZXhhbXBsZTc4LmNvbYINZXhh bXBsZTc5LmNvbYINZXhhbXBsZTgwLmNvbYINZXhhbXBsZTgxLmNvbYINZXhhbXBs ZTgyLmNvbYINZXhhbXBsZTgzLmNvbYINZXhhbXBsZTg0LmNvbYINZXhhbXBsZTg1 LmNvbYINZXhhbXBsZTg2LmNvbYINZXhhbXBsZTg3LmNvbYINZXhhbXBsZTg4LmNv bYINZXhhbXBsZTg5LmNvbYINZXhhbXBsZTkwLmNvbYINZXhhbXBsZTkxLmNvbYIN ZXhhbXBsZTkyLmNvbYINZXhhbXBsZTkzLmNvbYINZXhhbXBsZTk0LmNvbYINZXhh bXBsZTk1LmNvbYINZXhhbXBsZTk2LmNvbYINZXhhbXBsZTk3LmNvbYINZXhhbXBs ZTk4LmNvbYINZXhhbXBsZTk5LmNvbYIOZXhhbXBsZTEwMC5jb20wDQYJKoZIhvcN AQELBQADQQBEunJbKUXcyNKTSfA0pKRyWNiKmkoBqYgfZS6eHNrNH/hjFzHtzyDQ XYHHK6kgEWBvHfRXGmqhFvht+b1tQKkG -----END CERTIFICATE----- acme-0.31.0/acme/testdata/csr-san.pem0000644000076600000240000000107613427120435017205 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== -----END CERTIFICATE REQUEST----- acme-0.31.0/acme/testdata/dsa512_key.pem0000644000076600000240000000125413427120435017504 0ustar bmwstaff00000000000000-----BEGIN DSA PARAMETERS----- MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl 41pgNJpgu99YOYqPpS0g7A== -----END DSA PARAMETERS----- -----BEGIN DSA PRIVATE KEY----- MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE mNv063So6E+eYaIN -----END DSA PRIVATE KEY----- acme-0.31.0/acme/messages_test.py0000644000076600000240000004037513427120435016550 0ustar bmwstaff00000000000000"""Tests for acme.messages.""" import unittest import josepy as jose import mock from acme import challenges from acme import test_util from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module CERT = test_util.load_comparable_cert('cert.der') CSR = test_util.load_comparable_csr('csr.der') KEY = test_util.load_rsa_private_key('rsa512_key.pem') class ErrorTest(unittest.TestCase): """Tests for acme.messages.Error.""" def setUp(self): from acme.messages import Error, ERROR_PREFIX self.error = Error( detail='foo', typ=ERROR_PREFIX + 'malformed', title='title') self.jobj = { 'detail': 'foo', 'title': 'some title', 'type': ERROR_PREFIX + 'malformed', } self.error_custom = Error(typ='custom', detail='bar') self.empty_error = Error() self.jobj_custom = {'type': 'custom', 'detail': 'bar'} def test_default_typ(self): from acme.messages import Error self.assertEqual(Error().typ, 'about:blank') def test_from_json_empty(self): from acme.messages import Error self.assertEqual(Error(), Error.from_json('{}')) def test_from_json_hashable(self): from acme.messages import Error hash(Error.from_json(self.error.to_json())) def test_description(self): self.assertEqual( 'The request message was malformed', self.error.description) self.assertTrue(self.error_custom.description is None) def test_code(self): from acme.messages import Error self.assertEqual('malformed', self.error.code) self.assertEqual(None, self.error_custom.code) self.assertEqual(None, Error().code) def test_is_acme_error(self): from acme.messages import is_acme_error self.assertTrue(is_acme_error(self.error)) self.assertFalse(is_acme_error(self.error_custom)) self.assertFalse(is_acme_error(self.empty_error)) self.assertFalse(is_acme_error("must pet all the {dogs|rabbits}")) def test_unicode_error(self): from acme.messages import Error, ERROR_PREFIX, is_acme_error arabic_error = Error( detail=u'\u0639\u062f\u0627\u0644\u0629', typ=ERROR_PREFIX + 'malformed', title='title') self.assertTrue(is_acme_error(arabic_error)) def test_with_code(self): from acme.messages import Error, is_acme_error self.assertTrue(is_acme_error(Error.with_code('badCSR'))) self.assertRaises(ValueError, Error.with_code, 'not an ACME error code') def test_str(self): self.assertEqual( str(self.error), u"{0.typ} :: {0.description} :: {0.detail} :: {0.title}" .format(self.error)) class ConstantTest(unittest.TestCase): """Tests for acme.messages._Constant.""" def setUp(self): from acme.messages import _Constant class MockConstant(_Constant): # pylint: disable=missing-docstring POSSIBLE_NAMES = {} # type: Dict self.MockConstant = MockConstant # pylint: disable=invalid-name self.const_a = MockConstant('a') self.const_b = MockConstant('b') def test_to_partial_json(self): self.assertEqual('a', self.const_a.to_partial_json()) self.assertEqual('b', self.const_b.to_partial_json()) def test_from_json(self): self.assertEqual(self.const_a, self.MockConstant.from_json('a')) self.assertRaises( jose.DeserializationError, self.MockConstant.from_json, 'c') def test_from_json_hashable(self): hash(self.MockConstant.from_json('a')) def test_repr(self): self.assertEqual('MockConstant(a)', repr(self.const_a)) self.assertEqual('MockConstant(b)', repr(self.const_b)) def test_equality(self): const_a_prime = self.MockConstant('a') self.assertFalse(self.const_a == self.const_b) self.assertTrue(self.const_a == const_a_prime) self.assertTrue(self.const_a != self.const_b) self.assertFalse(self.const_a != const_a_prime) class DirectoryTest(unittest.TestCase): """Tests for acme.messages.Directory.""" def setUp(self): from acme.messages import Directory self.dir = Directory({ 'new-reg': 'reg', mock.MagicMock(resource_type='new-cert'): 'cert', 'meta': Directory.Meta( terms_of_service='https://example.com/acme/terms', website='https://www.example.com/', caa_identities=['example.com'], ), }) def test_init_wrong_key_value_success(self): # pylint: disable=no-self-use from acme.messages import Directory Directory({'foo': 'bar'}) def test_getitem(self): self.assertEqual('reg', self.dir['new-reg']) from acme.messages import NewRegistration self.assertEqual('reg', self.dir[NewRegistration]) self.assertEqual('reg', self.dir[NewRegistration()]) def test_getitem_fails_with_key_error(self): self.assertRaises(KeyError, self.dir.__getitem__, 'foo') def test_getattr(self): self.assertEqual('reg', self.dir.new_reg) def test_getattr_fails_with_attribute_error(self): self.assertRaises(AttributeError, self.dir.__getattr__, 'foo') def test_to_json(self): self.assertEqual(self.dir.to_json(), { 'new-reg': 'reg', 'new-cert': 'cert', 'meta': { 'terms-of-service': 'https://example.com/acme/terms', 'website': 'https://www.example.com/', 'caaIdentities': ['example.com'], }, }) def test_from_json_deserialization_unknown_key_success(self): # pylint: disable=no-self-use from acme.messages import Directory Directory.from_json({'foo': 'bar'}) def test_iter_meta(self): result = False for k in self.dir.meta: if k == 'terms_of_service': result = self.dir.meta[k] == 'https://example.com/acme/terms' self.assertTrue(result) class ExternalAccountBindingTest(unittest.TestCase): def setUp(self): from acme.messages import Directory self.key = jose.jwk.JWKRSA(key=KEY.public_key()) self.kid = "kid-for-testing" self.hmac_key = "hmac-key-for-testing" self.dir = Directory({ 'newAccount': 'http://url/acme/new-account', }) def test_from_data(self): from acme.messages import ExternalAccountBinding eab = ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir) self.assertEqual(len(eab), 3) self.assertEqual(sorted(eab.keys()), sorted(['protected', 'payload', 'signature'])) class RegistrationTest(unittest.TestCase): """Tests for acme.messages.Registration.""" def setUp(self): key = jose.jwk.JWKRSA(key=KEY.public_key()) contact = ( 'mailto:admin@foo.com', 'tel:1234', ) agreement = 'https://letsencrypt.org/terms' from acme.messages import Registration self.reg = Registration(key=key, contact=contact, agreement=agreement) self.reg_none = Registration() self.jobj_to = { 'contact': contact, 'agreement': agreement, 'key': key, } self.jobj_from = self.jobj_to.copy() self.jobj_from['key'] = key.to_json() def test_from_data(self): from acme.messages import Registration reg = Registration.from_data(phone='1234', email='admin@foo.com') self.assertEqual(reg.contact, ( 'tel:1234', 'mailto:admin@foo.com', )) def test_new_registration_from_data_with_eab(self): from acme.messages import NewRegistration, ExternalAccountBinding, Directory key = jose.jwk.JWKRSA(key=KEY.public_key()) kid = "kid-for-testing" hmac_key = "hmac-key-for-testing" directory = Directory({ 'newAccount': 'http://url/acme/new-account', }) eab = ExternalAccountBinding.from_data(key, kid, hmac_key, directory) reg = NewRegistration.from_data(email='admin@foo.com', external_account_binding=eab) self.assertEqual(reg.contact, ( 'mailto:admin@foo.com', )) self.assertEqual(sorted(reg.external_account_binding.keys()), sorted(['protected', 'payload', 'signature'])) def test_phones(self): self.assertEqual(('1234',), self.reg.phones) def test_emails(self): self.assertEqual(('admin@foo.com',), self.reg.emails) def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.reg.to_partial_json()) def test_from_json(self): from acme.messages import Registration self.assertEqual(self.reg, Registration.from_json(self.jobj_from)) def test_from_json_hashable(self): from acme.messages import Registration hash(Registration.from_json(self.jobj_from)) class UpdateRegistrationTest(unittest.TestCase): """Tests for acme.messages.UpdateRegistration.""" def test_empty(self): from acme.messages import UpdateRegistration jstring = '{"resource": "reg"}' self.assertEqual(jstring, UpdateRegistration().json_dumps()) self.assertEqual( UpdateRegistration(), UpdateRegistration.json_loads(jstring)) class RegistrationResourceTest(unittest.TestCase): """Tests for acme.messages.RegistrationResource.""" def setUp(self): from acme.messages import RegistrationResource self.regr = RegistrationResource( body=mock.sentinel.body, uri=mock.sentinel.uri, terms_of_service=mock.sentinel.terms_of_service) def test_to_partial_json(self): self.assertEqual(self.regr.to_json(), { 'body': mock.sentinel.body, 'uri': mock.sentinel.uri, 'terms_of_service': mock.sentinel.terms_of_service, }) class ChallengeResourceTest(unittest.TestCase): """Tests for acme.messages.ChallengeResource.""" def test_uri(self): from acme.messages import ChallengeResource self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock( uri='http://challb'), authzr_uri='http://authz').uri) class ChallengeBodyTest(unittest.TestCase): """Tests for acme.messages.ChallengeBody.""" def setUp(self): self.chall = challenges.DNS(token=jose.b64decode( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) from acme.messages import ChallengeBody from acme.messages import Error from acme.messages import STATUS_INVALID self.status = STATUS_INVALID error = Error(typ='urn:ietf:params:acme:error:serverInternal', detail='Unable to communicate with DNS server') self.challb = ChallengeBody( uri='http://challb', chall=self.chall, status=self.status, error=error) self.jobj_to = { 'uri': 'http://challb', 'status': self.status, 'type': 'dns', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', 'error': error, } self.jobj_from = self.jobj_to.copy() self.jobj_from['status'] = 'invalid' self.jobj_from['error'] = { 'type': 'urn:ietf:params:acme:error:serverInternal', 'detail': 'Unable to communicate with DNS server', } def test_encode(self): self.assertEqual(self.challb.encode('uri'), self.challb.uri) def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.challb.to_partial_json()) def test_from_json(self): from acme.messages import ChallengeBody self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from)) def test_from_json_hashable(self): from acme.messages import ChallengeBody hash(ChallengeBody.from_json(self.jobj_from)) def test_proxy(self): self.assertEqual(jose.b64decode( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'), self.challb.token) class AuthorizationTest(unittest.TestCase): """Tests for acme.messages.Authorization.""" def setUp(self): from acme.messages import ChallengeBody from acme.messages import STATUS_VALID self.challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_VALID, chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')), ChallengeBody(uri='http://challb2', status=STATUS_VALID, chall=challenges.DNS( token=b'DGyRejmCefe7v4NfDGDKfA')), ) combinations = ((0,), (1,)) from acme.messages import Authorization from acme.messages import Identifier from acme.messages import IDENTIFIER_FQDN identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') self.authz = Authorization( identifier=identifier, combinations=combinations, challenges=self.challbs) self.jobj_from = { 'identifier': identifier.to_json(), 'challenges': [challb.to_json() for challb in self.challbs], 'combinations': combinations, } def test_from_json(self): from acme.messages import Authorization Authorization.from_json(self.jobj_from) def test_from_json_hashable(self): from acme.messages import Authorization hash(Authorization.from_json(self.jobj_from)) def test_resolved_combinations(self): self.assertEqual(self.authz.resolved_combinations, ( (self.challbs[0],), (self.challbs[1],), )) class AuthorizationResourceTest(unittest.TestCase): """Tests for acme.messages.AuthorizationResource.""" def test_json_de_serializable(self): from acme.messages import AuthorizationResource authzr = AuthorizationResource( uri=mock.sentinel.uri, body=mock.sentinel.body) self.assertTrue(isinstance(authzr, jose.JSONDeSerializable)) class CertificateRequestTest(unittest.TestCase): """Tests for acme.messages.CertificateRequest.""" def setUp(self): from acme.messages import CertificateRequest self.req = CertificateRequest(csr=CSR) def test_json_de_serializable(self): self.assertTrue(isinstance(self.req, jose.JSONDeSerializable)) from acme.messages import CertificateRequest self.assertEqual( self.req, CertificateRequest.from_json(self.req.to_json())) class CertificateResourceTest(unittest.TestCase): """Tests for acme.messages.CertificateResourceTest.""" def setUp(self): from acme.messages import CertificateResource self.certr = CertificateResource( body=CERT, uri=mock.sentinel.uri, authzrs=(), cert_chain_uri=mock.sentinel.cert_chain_uri) def test_json_de_serializable(self): self.assertTrue(isinstance(self.certr, jose.JSONDeSerializable)) from acme.messages import CertificateResource self.assertEqual( self.certr, CertificateResource.from_json(self.certr.to_json())) class RevocationTest(unittest.TestCase): """Tests for acme.messages.RevocationTest.""" def setUp(self): from acme.messages import Revocation self.rev = Revocation(certificate=CERT) def test_from_json_hashable(self): from acme.messages import Revocation hash(Revocation.from_json(self.rev.to_json())) class OrderResourceTest(unittest.TestCase): """Tests for acme.messages.OrderResource.""" def setUp(self): from acme.messages import OrderResource self.regr = OrderResource( body=mock.sentinel.body, uri=mock.sentinel.uri) def test_to_partial_json(self): self.assertEqual(self.regr.to_json(), { 'body': mock.sentinel.body, 'uri': mock.sentinel.uri, 'authorizations': None, }) class NewOrderTest(unittest.TestCase): """Tests for acme.messages.NewOrder.""" def setUp(self): from acme.messages import NewOrder self.reg = NewOrder( identifiers=mock.sentinel.identifiers) def test_to_partial_json(self): self.assertEqual(self.reg.to_json(), { 'identifiers': mock.sentinel.identifiers, }) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.31.0/acme/standalone.py0000644000076600000240000002613213427120435016025 0ustar bmwstaff00000000000000"""Support for standalone client challenge solvers. """ import argparse import collections import functools import logging import os import socket import sys import threading from six.moves import BaseHTTPServer # type: ignore # pylint: disable=import-error from six.moves import http_client # pylint: disable=import-error from six.moves import socketserver # type: ignore # pylint: disable=import-error import OpenSSL from acme import challenges from acme import crypto_util from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module logger = logging.getLogger(__name__) # six.moves.* | pylint: disable=no-member,attribute-defined-outside-init # pylint: disable=too-few-public-methods,no-init class TLSServer(socketserver.TCPServer): """Generic TLS Server.""" def __init__(self, *args, **kwargs): self.ipv6 = kwargs.pop("ipv6", False) if self.ipv6: self.address_family = socket.AF_INET6 else: self.address_family = socket.AF_INET self.certs = kwargs.pop("certs", {}) self.method = kwargs.pop( # pylint: disable=protected-access "method", crypto_util._DEFAULT_TLSSNI01_SSL_METHOD) self.allow_reuse_address = kwargs.pop("allow_reuse_address", True) socketserver.TCPServer.__init__(self, *args, **kwargs) def _wrap_sock(self): self.socket = crypto_util.SSLSocket( self.socket, certs=self.certs, method=self.method) def server_bind(self): # pylint: disable=missing-docstring self._wrap_sock() return socketserver.TCPServer.server_bind(self) class ACMEServerMixin: # pylint: disable=old-style-class """ACME server common settings mixin.""" # TODO: c.f. #858 server_version = "ACME client standalone challenge solver" allow_reuse_address = True class BaseDualNetworkedServers(object): """Base class for a pair of IPv6 and IPv4 servers that tries to do everything it's asked for both servers, but where failures in one server don't affect the other. If two servers are instantiated, they will serve on the same port. """ def __init__(self, ServerClass, server_address, *remaining_args, **kwargs): port = server_address[1] self.threads = [] # type: List[threading.Thread] self.servers = [] # type: List[ACMEServerMixin] # Must try True first. # Ubuntu, for example, will fail to bind to IPv4 if we've already bound # to IPv6. But that's ok, since it will accept IPv4 connections on the IPv6 # socket. On the other hand, FreeBSD will successfully bind to IPv4 on the # same port, which means that server will accept the IPv4 connections. # If Python is compiled without IPv6, we'll error out but (probably) successfully # create the IPv4 server. for ip_version in [True, False]: try: kwargs["ipv6"] = ip_version new_address = (server_address[0],) + (port,) + server_address[2:] new_args = (new_address,) + remaining_args server = ServerClass(*new_args, **kwargs) # pylint: disable=star-args logger.debug( "Successfully bound to %s:%s using %s", new_address[0], new_address[1], "IPv6" if ip_version else "IPv4") except socket.error: if self.servers: # Already bound using IPv6. logger.debug( "Certbot wasn't able to bind to %s:%s using %s, this " + "is often expected due to the dual stack nature of " + "IPv6 socket implementations.", new_address[0], new_address[1], "IPv6" if ip_version else "IPv4") else: logger.debug( "Failed to bind to %s:%s using %s", new_address[0], new_address[1], "IPv6" if ip_version else "IPv4") else: self.servers.append(server) # If two servers are set up and port 0 was passed in, ensure we always # bind to the same port for both servers. port = server.socket.getsockname()[1] if len(self.servers) == 0: raise socket.error("Could not bind to IPv4 or IPv6.") def serve_forever(self): """Wraps socketserver.TCPServer.serve_forever""" for server in self.servers: thread = threading.Thread( # pylint: disable=no-member target=server.serve_forever) thread.start() self.threads.append(thread) def getsocknames(self): """Wraps socketserver.TCPServer.socket.getsockname""" return [server.socket.getsockname() for server in self.servers] def shutdown_and_server_close(self): """Wraps socketserver.TCPServer.shutdown, socketserver.TCPServer.server_close, and threading.Thread.join""" for server in self.servers: server.shutdown() server.server_close() for thread in self.threads: thread.join() self.threads = [] class TLSSNI01Server(TLSServer, ACMEServerMixin): """TLSSNI01 Server.""" def __init__(self, server_address, certs, ipv6=False): TLSServer.__init__( self, server_address, BaseRequestHandlerWithLogging, certs=certs, ipv6=ipv6) class TLSSNI01DualNetworkedServers(BaseDualNetworkedServers): """TLSSNI01Server Wrapper. Tries everything for both. Failures for one don't affect the other.""" def __init__(self, *args, **kwargs): BaseDualNetworkedServers.__init__(self, TLSSNI01Server, *args, **kwargs) class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler): """BaseRequestHandler with logging.""" def log_message(self, format, *args): # pylint: disable=redefined-builtin """Log arbitrary message.""" logger.debug("%s - - %s", self.client_address[0], format % args) def handle(self): """Handle request.""" self.log_message("Incoming request") socketserver.BaseRequestHandler.handle(self) class HTTPServer(BaseHTTPServer.HTTPServer): """Generic HTTP Server.""" def __init__(self, *args, **kwargs): self.ipv6 = kwargs.pop("ipv6", False) if self.ipv6: self.address_family = socket.AF_INET6 else: self.address_family = socket.AF_INET BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs) class HTTP01Server(HTTPServer, ACMEServerMixin): """HTTP01 Server.""" def __init__(self, server_address, resources, ipv6=False): HTTPServer.__init__( self, server_address, HTTP01RequestHandler.partial_init( simple_http_resources=resources), ipv6=ipv6) class HTTP01DualNetworkedServers(BaseDualNetworkedServers): """HTTP01Server Wrapper. Tries everything for both. Failures for one don't affect the other.""" def __init__(self, *args, **kwargs): BaseDualNetworkedServers.__init__(self, HTTP01Server, *args, **kwargs) class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): """HTTP01 challenge handler. Adheres to the stdlib's `socketserver.BaseRequestHandler` interface. :ivar set simple_http_resources: A set of `HTTP01Resource` objects. TODO: better name? """ HTTP01Resource = collections.namedtuple( "HTTP01Resource", "chall response validation") def __init__(self, *args, **kwargs): self.simple_http_resources = kwargs.pop("simple_http_resources", set()) BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kwargs) def log_message(self, format, *args): # pylint: disable=redefined-builtin """Log arbitrary message.""" logger.debug("%s - - %s", self.client_address[0], format % args) def handle(self): """Handle request.""" self.log_message("Incoming request") BaseHTTPServer.BaseHTTPRequestHandler.handle(self) def do_GET(self): # pylint: disable=invalid-name,missing-docstring if self.path == "/": self.handle_index() elif self.path.startswith("/" + challenges.HTTP01.URI_ROOT_PATH): self.handle_simple_http_resource() else: self.handle_404() def handle_index(self): """Handle index page.""" self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(self.server.server_version.encode()) def handle_404(self): """Handler 404 Not Found errors.""" self.send_response(http_client.NOT_FOUND, message="Not Found") self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(b"404") def handle_simple_http_resource(self): """Handle HTTP01 provisioned resources.""" for resource in self.simple_http_resources: if resource.chall.path == self.path: self.log_message("Serving HTTP01 with token %r", resource.chall.encode("token")) self.send_response(http_client.OK) self.end_headers() self.wfile.write(resource.validation.encode()) return else: # pylint: disable=useless-else-on-loop self.log_message("No resources to serve") self.log_message("%s does not correspond to any resource. ignoring", self.path) @classmethod def partial_init(cls, simple_http_resources): """Partially initialize this handler. This is useful because `socketserver.BaseServer` takes uninitialized handler and initializes it with the current request. """ return functools.partial( cls, simple_http_resources=simple_http_resources) def simple_tls_sni_01_server(cli_args, forever=True): """Run simple standalone TLSSNI01 server.""" logging.basicConfig(level=logging.DEBUG) parser = argparse.ArgumentParser() parser.add_argument( "-p", "--port", default=0, help="Port to serve at. By default " "picks random free port.") args = parser.parse_args(cli_args[1:]) certs = {} _, hosts, _ = next(os.walk('.')) # type: ignore # https://github.com/python/mypy/issues/465 for host in hosts: with open(os.path.join(host, "cert.pem")) as cert_file: cert_contents = cert_file.read() with open(os.path.join(host, "key.pem")) as key_file: key_contents = key_file.read() certs[host.encode()] = ( OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, key_contents), OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, cert_contents)) server = TLSSNI01Server(('', int(args.port)), certs=certs) logger.info("Serving at https://%s:%s...", *server.socket.getsockname()[:2]) if forever: # pragma: no cover server.serve_forever() else: server.handle_request() if __name__ == "__main__": sys.exit(simple_tls_sni_01_server(sys.argv)) # pragma: no cover acme-0.31.0/acme/challenges_test.py0000644000076600000240000005043213427120435017041 0ustar bmwstaff00000000000000"""Tests for acme.challenges.""" import unittest import warnings import josepy as jose import mock import OpenSSL import requests from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error from acme import errors from acme import test_util CERT = test_util.load_comparable_cert('cert.pem') KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) class ChallengeTest(unittest.TestCase): def test_from_json_unrecognized(self): from acme.challenges import Challenge from acme.challenges import UnrecognizedChallenge chall = UnrecognizedChallenge({"type": "foo"}) # pylint: disable=no-member self.assertEqual(chall, Challenge.from_json(chall.jobj)) class UnrecognizedChallengeTest(unittest.TestCase): def setUp(self): from acme.challenges import UnrecognizedChallenge self.jobj = {"type": "foo"} self.chall = UnrecognizedChallenge(self.jobj) def test_to_partial_json(self): self.assertEqual(self.jobj, self.chall.to_partial_json()) def test_from_json(self): from acme.challenges import UnrecognizedChallenge self.assertEqual( self.chall, UnrecognizedChallenge.from_json(self.jobj)) class KeyAuthorizationChallengeResponseTest(unittest.TestCase): def setUp(self): def _encode(name): assert name == "token" return "foo" self.chall = mock.Mock() self.chall.encode.side_effect = _encode def test_verify_ok(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') self.assertTrue(response.verify(self.chall, KEY.public_key())) def test_verify_wrong_token(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='bar.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') self.assertFalse(response.verify(self.chall, KEY.public_key())) def test_verify_wrong_thumbprint(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxv') self.assertFalse(response.verify(self.chall, KEY.public_key())) def test_verify_wrong_form(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='.foo.oKGqedy-b-acd5eoybm2f-' 'NVFxvyOoET5CNy3xnv8WY') self.assertFalse(response.verify(self.chall, KEY.public_key())) class DNS01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes def setUp(self): from acme.challenges import DNS01Response self.msg = DNS01Response(key_authorization=u'foo') self.jmsg = { 'resource': 'challenge', 'type': 'dns-01', 'keyAuthorization': u'foo', } from acme.challenges import DNS01 self.chall = DNS01(token=(b'x' * 16)) self.response = self.chall.response(KEY) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNS01Response self.assertEqual(self.msg, DNS01Response.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import DNS01Response hash(DNS01Response.from_json(self.jmsg)) def test_simple_verify_failure(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) public_key = key2.public_key() verified = self.response.simple_verify(self.chall, "local", public_key) self.assertFalse(verified) def test_simple_verify_success(self): public_key = KEY.public_key() verified = self.response.simple_verify(self.chall, "local", public_key) self.assertTrue(verified) class DNS01Test(unittest.TestCase): def setUp(self): from acme.challenges import DNS01 self.msg = DNS01(token=jose.decode_b64jose( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) self.jmsg = { 'type': 'dns-01', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', } def test_validation_domain_name(self): self.assertEqual('_acme-challenge.www.example.com', self.msg.validation_domain_name('www.example.com')) def test_validation(self): self.assertEqual( "rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk", self.msg.validation(KEY)) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNS01 self.assertEqual(self.msg, DNS01.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import DNS01 hash(DNS01.from_json(self.jmsg)) class HTTP01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes def setUp(self): from acme.challenges import HTTP01Response self.msg = HTTP01Response(key_authorization=u'foo') self.jmsg = { 'resource': 'challenge', 'type': 'http-01', 'keyAuthorization': u'foo', } from acme.challenges import HTTP01 self.chall = HTTP01(token=(b'x' * 16)) self.response = self.chall.response(KEY) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import HTTP01Response self.assertEqual( self.msg, HTTP01Response.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import HTTP01Response hash(HTTP01Response.from_json(self.jmsg)) def test_simple_verify_bad_key_authorization(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) self.response.simple_verify(self.chall, "local", key2.public_key()) @mock.patch("acme.challenges.requests.get") def test_simple_verify_good_validation(self, mock_get): validation = self.chall.validation(KEY) mock_get.return_value = mock.MagicMock(text=validation) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_get.assert_called_once_with(self.chall.uri("local")) @mock.patch("acme.challenges.requests.get") def test_simple_verify_bad_validation(self, mock_get): mock_get.return_value = mock.MagicMock(text="!") self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) @mock.patch("acme.challenges.requests.get") def test_simple_verify_whitespace_validation(self, mock_get): from acme.challenges import HTTP01Response mock_get.return_value = mock.MagicMock( text=(self.chall.validation(KEY) + HTTP01Response.WHITESPACE_CUTSET)) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_get.assert_called_once_with(self.chall.uri("local")) @mock.patch("acme.challenges.requests.get") def test_simple_verify_connection_error(self, mock_get): mock_get.side_effect = requests.exceptions.RequestException self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) @mock.patch("acme.challenges.requests.get") def test_simple_verify_port(self, mock_get): self.response.simple_verify( self.chall, domain="local", account_public_key=KEY.public_key(), port=8080) self.assertEqual("local:8080", urllib_parse.urlparse( mock_get.mock_calls[0][1][0]).netloc) class HTTP01Test(unittest.TestCase): def setUp(self): from acme.challenges import HTTP01 self.msg = HTTP01( token=jose.decode_b64jose( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) self.jmsg = { 'type': 'http-01', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', } def test_path(self): self.assertEqual(self.msg.path, '/.well-known/acme-challenge/' 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') def test_uri(self): self.assertEqual( 'http://example.com/.well-known/acme-challenge/' 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', self.msg.uri('example.com')) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import HTTP01 self.assertEqual(self.msg, HTTP01.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import HTTP01 hash(HTTP01.from_json(self.jmsg)) def test_good_token(self): self.assertTrue(self.msg.good_token) self.assertFalse( self.msg.update(token=b'..').good_token) class TLSSNI01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes def setUp(self): from acme.challenges import TLSSNI01 self.chall = TLSSNI01( token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) self.response = self.chall.response(KEY) self.jmsg = { 'resource': 'challenge', 'type': 'tls-sni-01', 'keyAuthorization': self.response.key_authorization, } # pylint: disable=invalid-name label1 = b'dc38d9c3fa1a4fdcc3a5501f2d38583f' label2 = b'b7793728f084394f2a1afd459556bb5c' self.z = label1 + label2 self.z_domain = label1 + b'.' + label2 + b'.acme.invalid' self.domain = 'foo.com' def test_z_and_domain(self): self.assertEqual(self.z, self.response.z) self.assertEqual(self.z_domain, self.response.z_domain) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.response.to_partial_json()) def test_from_json(self): from acme.challenges import TLSSNI01Response self.assertEqual(self.response, TLSSNI01Response.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import TLSSNI01Response hash(TLSSNI01Response.from_json(self.jmsg)) @mock.patch('acme.challenges.socket.gethostbyname') @mock.patch('acme.challenges.crypto_util.probe_sni') def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): mock_gethostbyname.return_value = '127.0.0.1' self.response.probe_cert('foo.com') mock_gethostbyname.assert_called_once_with('foo.com') mock_probe_sni.assert_called_once_with( host='127.0.0.1', port=self.response.PORT, name=self.z_domain) self.response.probe_cert('foo.com', host='8.8.8.8') mock_probe_sni.assert_called_with( host='8.8.8.8', port=mock.ANY, name=mock.ANY) self.response.probe_cert('foo.com', port=1234) mock_probe_sni.assert_called_with( host=mock.ANY, port=1234, name=mock.ANY) self.response.probe_cert('foo.com', bar='baz') mock_probe_sni.assert_called_with( host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz') self.response.probe_cert('foo.com', name=b'xxx') mock_probe_sni.assert_called_with( host=mock.ANY, port=mock.ANY, name=self.z_domain) def test_gen_verify_cert(self): key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') cert, key2 = self.response.gen_cert(key1) self.assertEqual(key1, key2) self.assertTrue(self.response.verify_cert(cert)) def test_gen_verify_cert_gen_key(self): cert, key = self.response.gen_cert() self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) self.assertTrue(self.response.verify_cert(cert)) def test_verify_bad_cert(self): self.assertFalse(self.response.verify_cert( test_util.load_cert('cert.pem'))) def test_simple_verify_bad_key_authorization(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) self.response.simple_verify(self.chall, "local", key2.public_key()) @mock.patch('acme.challenges.TLSSNI01Response.verify_cert', autospec=True) def test_simple_verify(self, mock_verify_cert): mock_verify_cert.return_value = mock.sentinel.verification self.assertEqual( mock.sentinel.verification, self.response.simple_verify( self.chall, self.domain, KEY.public_key(), cert=mock.sentinel.cert)) mock_verify_cert.assert_called_once_with( self.response, mock.sentinel.cert) @mock.patch('acme.challenges.TLSSNI01Response.probe_cert') def test_simple_verify_false_on_probe_error(self, mock_probe_cert): mock_probe_cert.side_effect = errors.Error self.assertFalse(self.response.simple_verify( self.chall, self.domain, KEY.public_key())) class TLSSNI01Test(unittest.TestCase): def setUp(self): self.jmsg = { 'type': 'tls-sni-01', 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', } def _msg(self): from acme.challenges import TLSSNI01 with warnings.catch_warnings(record=True) as warn: warnings.simplefilter("always") msg = TLSSNI01( token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) assert warn is not None # using a raw assert for mypy self.assertTrue(len(warn) == 1) self.assertTrue(issubclass(warn[-1].category, DeprecationWarning)) self.assertTrue('deprecated' in str(warn[-1].message)) return msg def test_to_partial_json(self): self.assertEqual(self.jmsg, self._msg().to_partial_json()) def test_from_json(self): from acme.challenges import TLSSNI01 self.assertEqual(self._msg(), TLSSNI01.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import TLSSNI01 hash(TLSSNI01.from_json(self.jmsg)) def test_from_json_invalid_token_length(self): from acme.challenges import TLSSNI01 self.jmsg['token'] = jose.encode_b64jose(b'abcd') self.assertRaises( jose.DeserializationError, TLSSNI01.from_json, self.jmsg) @mock.patch('acme.challenges.TLSSNI01Response.gen_cert') def test_validation(self, mock_gen_cert): mock_gen_cert.return_value = ('cert', 'key') self.assertEqual(('cert', 'key'), self._msg().validation( KEY, cert_key=mock.sentinel.cert_key)) mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key) class TLSALPN01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes def setUp(self): from acme.challenges import TLSALPN01Response self.msg = TLSALPN01Response(key_authorization=u'foo') self.jmsg = { 'resource': 'challenge', 'type': 'tls-alpn-01', 'keyAuthorization': u'foo', } from acme.challenges import TLSALPN01 self.chall = TLSALPN01(token=(b'x' * 16)) self.response = self.chall.response(KEY) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import TLSALPN01Response self.assertEqual(self.msg, TLSALPN01Response.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import TLSALPN01Response hash(TLSALPN01Response.from_json(self.jmsg)) class TLSALPN01Test(unittest.TestCase): def setUp(self): from acme.challenges import TLSALPN01 self.msg = TLSALPN01( token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) self.jmsg = { 'type': 'tls-alpn-01', 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', } def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import TLSALPN01 self.assertEqual(self.msg, TLSALPN01.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import TLSALPN01 hash(TLSALPN01.from_json(self.jmsg)) def test_from_json_invalid_token_length(self): from acme.challenges import TLSALPN01 self.jmsg['token'] = jose.encode_b64jose(b'abcd') self.assertRaises( jose.DeserializationError, TLSALPN01.from_json, self.jmsg) def test_validation(self): self.assertRaises(NotImplementedError, self.msg.validation, KEY) class DNSTest(unittest.TestCase): def setUp(self): from acme.challenges import DNS self.msg = DNS(token=jose.b64decode( b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) self.jmsg = { 'type': 'dns', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', } def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNS self.assertEqual(self.msg, DNS.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import DNS hash(DNS.from_json(self.jmsg)) def test_gen_check_validation(self): self.assertTrue(self.msg.check_validation( self.msg.gen_validation(KEY), KEY.public_key())) def test_gen_check_validation_wrong_key(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) self.assertFalse(self.msg.check_validation( self.msg.gen_validation(KEY), key2.public_key())) def test_check_validation_wrong_payload(self): validations = tuple( jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY) for payload in (b'', b'{}') ) for validation in validations: self.assertFalse(self.msg.check_validation( validation, KEY.public_key())) def test_check_validation_wrong_fields(self): bad_validation = jose.JWS.sign( payload=self.msg.update( token=b'x' * 20).json_dumps().encode('utf-8'), alg=jose.RS256, key=KEY) self.assertFalse(self.msg.check_validation( bad_validation, KEY.public_key())) def test_gen_response(self): with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen: mock_gen.return_value = mock.sentinel.validation response = self.msg.gen_response(KEY) from acme.challenges import DNSResponse self.assertTrue(isinstance(response, DNSResponse)) self.assertEqual(response.validation, mock.sentinel.validation) def test_validation_domain_name(self): self.assertEqual( '_acme-challenge.le.wtf', self.msg.validation_domain_name('le.wtf')) class DNSResponseTest(unittest.TestCase): def setUp(self): from acme.challenges import DNS self.chall = DNS(token=jose.b64decode( b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA")) self.validation = jose.JWS.sign( payload=self.chall.json_dumps(sort_keys=True).encode(), key=KEY, alg=jose.RS256) from acme.challenges import DNSResponse self.msg = DNSResponse(validation=self.validation) self.jmsg_to = { 'resource': 'challenge', 'type': 'dns', 'validation': self.validation, } self.jmsg_from = { 'resource': 'challenge', 'type': 'dns', 'validation': self.validation.to_json(), } def test_to_partial_json(self): self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNSResponse self.assertEqual(self.msg, DNSResponse.from_json(self.jmsg_from)) def test_from_json_hashable(self): from acme.challenges import DNSResponse hash(DNSResponse.from_json(self.jmsg_from)) def test_check_validation(self): self.assertTrue( self.msg.check_validation(self.chall, KEY.public_key())) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.31.0/acme/errors_test.py0000644000076600000240000000276013427120435016251 0ustar bmwstaff00000000000000"""Tests for acme.errors.""" import unittest import mock class BadNonceTest(unittest.TestCase): """Tests for acme.errors.BadNonce.""" def setUp(self): from acme.errors import BadNonce self.error = BadNonce(nonce="xxx", error="error") def test_str(self): self.assertEqual("Invalid nonce ('xxx'): error", str(self.error)) class MissingNonceTest(unittest.TestCase): """Tests for acme.errors.MissingNonce.""" def setUp(self): from acme.errors import MissingNonce self.response = mock.MagicMock(headers={}) self.response.request.method = 'FOO' self.error = MissingNonce(self.response) def test_str(self): self.assertTrue("FOO" in str(self.error)) self.assertTrue("{}" in str(self.error)) class PollErrorTest(unittest.TestCase): """Tests for acme.errors.PollError.""" def setUp(self): from acme.errors import PollError self.timeout = PollError( exhausted=set([mock.sentinel.AR]), updated={}) self.invalid = PollError(exhausted=set(), updated={ mock.sentinel.AR: mock.sentinel.AR2}) def test_timeout(self): self.assertTrue(self.timeout.timeout) self.assertFalse(self.invalid.timeout) def test_repr(self): self.assertEqual('PollError(exhausted=%s, updated={sentinel.AR: ' 'sentinel.AR2})' % repr(set()), repr(self.invalid)) if __name__ == "__main__": unittest.main() # pragma: no cover acme-0.31.0/acme/magic_typing.py0000644000076600000240000000102613427120435016342 0ustar bmwstaff00000000000000"""Shim class to not have to depend on typing module in prod.""" import sys class TypingClass(object): """Ignore import errors by getting anything""" def __getattr__(self, name): return None try: # mypy doesn't respect modifying sys.modules from typing import * # pylint: disable=wildcard-import, unused-wildcard-import # pylint: disable=unused-import from typing import Collection, IO # type: ignore # pylint: enable=unused-import except ImportError: sys.modules[__name__] = TypingClass() acme-0.31.0/acme/fields.py0000644000076600000240000000332013427120435015135 0ustar bmwstaff00000000000000"""ACME JSON fields.""" import logging import josepy as jose import pyrfc3339 logger = logging.getLogger(__name__) class Fixed(jose.Field): """Fixed field.""" def __init__(self, json_name, value): self.value = value super(Fixed, self).__init__( json_name=json_name, default=value, omitempty=False) def decode(self, value): if value != self.value: raise jose.DeserializationError('Expected {0!r}'.format(self.value)) return self.value def encode(self, value): if value != self.value: logger.warning( 'Overriding fixed field (%s) with %r', self.json_name, value) return value class RFC3339Field(jose.Field): """RFC3339 field encoder/decoder. Handles decoding/encoding between RFC3339 strings and aware (not naive) `datetime.datetime` objects (e.g. ``datetime.datetime.now(pytz.utc)``). """ @classmethod def default_encoder(cls, value): return pyrfc3339.generate(value) @classmethod def default_decoder(cls, value): try: return pyrfc3339.parse(value) except ValueError as error: raise jose.DeserializationError(error) class Resource(jose.Field): """Resource MITM field.""" def __init__(self, resource_type, *args, **kwargs): self.resource_type = resource_type super(Resource, self).__init__( 'resource', default=resource_type, *args, **kwargs) def decode(self, value): if value != self.resource_type: raise jose.DeserializationError( 'Wrong resource type: {0} instead of {1}'.format( value, self.resource_type)) return value acme-0.31.0/acme/jws_test.py0000644000076600000240000000404113427120435015532 0ustar bmwstaff00000000000000"""Tests for acme.jws.""" import unittest import josepy as jose from acme import test_util KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) class HeaderTest(unittest.TestCase): """Tests for acme.jws.Header.""" good_nonce = jose.encode_b64jose(b'foo') wrong_nonce = u'F' # Following just makes sure wrong_nonce is wrong try: jose.b64decode(wrong_nonce) except (ValueError, TypeError): assert True else: assert False # pragma: no cover def test_nonce_decoder(self): from acme.jws import Header nonce_field = Header._fields['nonce'] self.assertRaises( jose.DeserializationError, nonce_field.decode, self.wrong_nonce) self.assertEqual(b'foo', nonce_field.decode(self.good_nonce)) class JWSTest(unittest.TestCase): """Tests for acme.jws.JWS.""" def setUp(self): self.privkey = KEY self.pubkey = self.privkey.public_key() self.nonce = jose.b64encode(b'Nonce') self.url = 'hi' self.kid = 'baaaaa' def test_kid_serialize(self): from acme.jws import JWS jws = JWS.sign(payload=b'foo', key=self.privkey, alg=jose.RS256, nonce=self.nonce, url=self.url, kid=self.kid) self.assertEqual(jws.signature.combined.nonce, self.nonce) self.assertEqual(jws.signature.combined.url, self.url) self.assertEqual(jws.signature.combined.kid, self.kid) self.assertEqual(jws.signature.combined.jwk, None) # TODO: check that nonce is in protected header self.assertEqual(jws, JWS.from_json(jws.to_json())) def test_jwk_serialize(self): from acme.jws import JWS jws = JWS.sign(payload=b'foo', key=self.privkey, alg=jose.RS256, nonce=self.nonce, url=self.url) self.assertEqual(jws.signature.combined.kid, None) self.assertEqual(jws.signature.combined.jwk, self.pubkey) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.31.0/acme/util.py0000644000076600000240000000024613427120435014650 0ustar bmwstaff00000000000000"""ACME utilities.""" import six def map_keys(dikt, func): """Map dictionary keys.""" return dict((func(key), value) for key, value in six.iteritems(dikt)) acme-0.31.0/acme/client.py0000644000076600000240000013202413427120435015151 0ustar bmwstaff00000000000000"""ACME client API.""" import base64 import collections import datetime from email.utils import parsedate_tz import heapq import logging import time import six from six.moves import http_client # pylint: disable=import-error import josepy as jose import OpenSSL import re from requests_toolbelt.adapters.source import SourceAddressAdapter import requests from requests.adapters import HTTPAdapter import sys from acme import crypto_util from acme import errors from acme import jws from acme import messages # pylint: disable=unused-import, no-name-in-module from acme.magic_typing import Dict, List, Set, Text logger = logging.getLogger(__name__) # Prior to Python 2.7.9 the stdlib SSL module did not allow a user to configure # many important security related options. On these platforms we use PyOpenSSL # for SSL, which does allow these options to be configured. # https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning if sys.version_info < (2, 7, 9): # pragma: no cover try: # pylint: disable=no-member requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() # type: ignore except AttributeError: import urllib3.contrib.pyopenssl # pylint: disable=import-error urllib3.contrib.pyopenssl.inject_into_urllib3() DEFAULT_NETWORK_TIMEOUT = 45 DER_CONTENT_TYPE = 'application/pkix-cert' class ClientBase(object): # pylint: disable=too-many-instance-attributes """ACME client base object. :ivar messages.Directory directory: :ivar .ClientNetwork net: Client network. :ivar int acme_version: ACME protocol version. 1 or 2. """ def __init__(self, directory, net, acme_version): """Initialize. :param .messages.Directory directory: Directory Resource :param .ClientNetwork net: Client network. :param int acme_version: ACME protocol version. 1 or 2. """ self.directory = directory self.net = net self.acme_version = acme_version @classmethod def _regr_from_response(cls, response, uri=None, terms_of_service=None): if 'terms-of-service' in response.links: terms_of_service = response.links['terms-of-service']['url'] return messages.RegistrationResource( body=messages.Registration.from_json(response.json()), uri=response.headers.get('Location', uri), terms_of_service=terms_of_service) def _send_recv_regr(self, regr, body): response = self._post(regr.uri, body) # TODO: Boulder returns httplib.ACCEPTED #assert response.status_code == httplib.OK # TODO: Boulder does not set Location or Link on update # (c.f. acme-spec #94) return self._regr_from_response( response, uri=regr.uri, terms_of_service=regr.terms_of_service) def _post(self, *args, **kwargs): """Wrapper around self.net.post that adds the acme_version. """ kwargs.setdefault('acme_version', self.acme_version) if hasattr(self.directory, 'newNonce'): kwargs.setdefault('new_nonce_url', getattr(self.directory, 'newNonce')) return self.net.post(*args, **kwargs) def update_registration(self, regr, update=None): """Update registration. :param messages.RegistrationResource regr: Registration Resource. :param messages.Registration update: Updated body of the resource. If not provided, body will be taken from `regr`. :returns: Updated Registration Resource. :rtype: `.RegistrationResource` """ update = regr.body if update is None else update body = messages.UpdateRegistration(**dict(update)) updated_regr = self._send_recv_regr(regr, body=body) self.net.account = updated_regr return updated_regr def deactivate_registration(self, regr): """Deactivate registration. :param messages.RegistrationResource regr: The Registration Resource to be deactivated. :returns: The Registration resource that was deactivated. :rtype: `.RegistrationResource` """ return self.update_registration(regr, update={'status': 'deactivated'}) def query_registration(self, regr): """Query server about registration. :param messages.RegistrationResource: Existing Registration Resource. """ return self._send_recv_regr(regr, messages.UpdateRegistration()) def _authzr_from_response(self, response, identifier=None, uri=None): authzr = messages.AuthorizationResource( body=messages.Authorization.from_json(response.json()), uri=response.headers.get('Location', uri)) if identifier is not None and authzr.body.identifier != identifier: raise errors.UnexpectedUpdate(authzr) return authzr def answer_challenge(self, challb, response): """Answer challenge. :param challb: Challenge Resource body. :type challb: `.ChallengeBody` :param response: Corresponding Challenge response :type response: `.challenges.ChallengeResponse` :returns: Challenge Resource with updated body. :rtype: `.ChallengeResource` :raises .UnexpectedUpdate: """ response = self._post(challb.uri, response) try: authzr_uri = response.links['up']['url'] except KeyError: raise errors.ClientError('"up" Link header missing') challr = messages.ChallengeResource( authzr_uri=authzr_uri, body=messages.ChallengeBody.from_json(response.json())) # TODO: check that challr.uri == response.headers['Location']? if challr.uri != challb.uri: raise errors.UnexpectedUpdate(challr.uri) return challr @classmethod def retry_after(cls, response, default): """Compute next `poll` time based on response ``Retry-After`` header. Handles integers and various datestring formats per https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37 :param requests.Response response: Response from `poll`. :param int default: Default value (in seconds), used when ``Retry-After`` header is not present or invalid. :returns: Time point when next `poll` should be performed. :rtype: `datetime.datetime` """ retry_after = response.headers.get('Retry-After', str(default)) try: seconds = int(retry_after) except ValueError: # The RFC 2822 parser handles all of RFC 2616's cases in modern # environments (primarily HTTP 1.1+ but also py27+) when = parsedate_tz(retry_after) if when is not None: try: tz_secs = datetime.timedelta(when[-1] if when[-1] else 0) return datetime.datetime(*when[:7]) - tz_secs except (ValueError, OverflowError): pass seconds = default return datetime.datetime.now() + datetime.timedelta(seconds=seconds) def _revoke(self, cert, rsn, url): """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :param int rsn: Reason code for certificate revocation. :param str url: ACME URL to post to :raises .ClientError: If revocation is unsuccessful. """ response = self._post(url, messages.Revocation( certificate=cert, reason=rsn)) if response.status_code != http_client.OK: raise errors.ClientError( 'Successful revocation must return HTTP OK status') class Client(ClientBase): """ACME client for a v1 API. .. todo:: Clean up raised error types hierarchy, document, and handle (wrap) instances of `.DeserializationError` raised in `from_json()`. :ivar messages.Directory directory: :ivar key: `josepy.JWK` (private) :ivar alg: `josepy.JWASignature` :ivar bool verify_ssl: Verify SSL certificates? :ivar .ClientNetwork net: Client network. Useful for testing. If not supplied, it will be initialized using `key`, `alg` and `verify_ssl`. """ def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True, net=None): """Initialize. :param directory: Directory Resource (`.messages.Directory`) or URI from which the resource will be downloaded. """ # pylint: disable=too-many-arguments self.key = key if net is None: net = ClientNetwork(key, alg=alg, verify_ssl=verify_ssl) if isinstance(directory, six.string_types): directory = messages.Directory.from_json( net.get(directory).json()) super(Client, self).__init__(directory=directory, net=net, acme_version=1) def register(self, new_reg=None): """Register. :param .NewRegistration new_reg: :returns: Registration Resource. :rtype: `.RegistrationResource` """ new_reg = messages.NewRegistration() if new_reg is None else new_reg response = self._post(self.directory[new_reg], new_reg) # TODO: handle errors assert response.status_code == http_client.CREATED # "Instance of 'Field' has no key/contact member" bug: # pylint: disable=no-member return self._regr_from_response(response) def agree_to_tos(self, regr): """Agree to the terms-of-service. Agree to the terms-of-service in a Registration Resource. :param regr: Registration Resource. :type regr: `.RegistrationResource` :returns: Updated Registration Resource. :rtype: `.RegistrationResource` """ return self.update_registration( regr.update(body=regr.body.update(agreement=regr.terms_of_service))) def request_challenges(self, identifier, new_authzr_uri=None): """Request challenges. :param .messages.Identifier identifier: Identifier to be challenged. :param str new_authzr_uri: Deprecated. Do not use. :returns: Authorization Resource. :rtype: `.AuthorizationResource` :raises errors.WildcardUnsupportedError: if a wildcard is requested """ if new_authzr_uri is not None: logger.debug("request_challenges with new_authzr_uri deprecated.") if identifier.value.startswith("*"): raise errors.WildcardUnsupportedError( "Requesting an authorization for a wildcard name is" " forbidden by this version of the ACME protocol.") new_authz = messages.NewAuthorization(identifier=identifier) response = self._post(self.directory.new_authz, new_authz) # TODO: handle errors assert response.status_code == http_client.CREATED return self._authzr_from_response(response, identifier) def request_domain_challenges(self, domain, new_authzr_uri=None): """Request challenges for domain names. This is simply a convenience function that wraps around `request_challenges`, but works with domain names instead of generic identifiers. See ``request_challenges`` for more documentation. :param str domain: Domain name to be challenged. :param str new_authzr_uri: Deprecated. Do not use. :returns: Authorization Resource. :rtype: `.AuthorizationResource` :raises errors.WildcardUnsupportedError: if a wildcard is requested """ return self.request_challenges(messages.Identifier( typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri) def request_issuance(self, csr, authzrs): """Request issuance. :param csr: CSR :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` :param authzrs: `list` of `.AuthorizationResource` :returns: Issued certificate :rtype: `.messages.CertificateResource` """ assert authzrs, "Authorizations list is empty" logger.debug("Requesting issuance...") # TODO: assert len(authzrs) == number of SANs req = messages.CertificateRequest(csr=csr) content_type = DER_CONTENT_TYPE # TODO: add 'cert_type 'argument response = self._post( self.directory.new_cert, req, content_type=content_type, headers={'Accept': content_type}) cert_chain_uri = response.links.get('up', {}).get('url') try: uri = response.headers['Location'] except KeyError: raise errors.ClientError('"Location" Header missing') return messages.CertificateResource( uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, body=jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_ASN1, response.content))) def poll(self, authzr): """Poll Authorization Resource for status. :param authzr: Authorization Resource :type authzr: `.AuthorizationResource` :returns: Updated Authorization Resource and HTTP response. :rtype: (`.AuthorizationResource`, `requests.Response`) """ response = self.net.get(authzr.uri) updated_authzr = self._authzr_from_response( response, authzr.body.identifier, authzr.uri) return updated_authzr, response def poll_and_request_issuance( self, csr, authzrs, mintime=5, max_attempts=10): """Poll and request issuance. This function polls all provided Authorization Resource URIs until all challenges are valid, respecting ``Retry-After`` HTTP headers, and then calls `request_issuance`. :param .ComparableX509 csr: CSR (`OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`) :param authzrs: `list` of `.AuthorizationResource` :param int mintime: Minimum time before next attempt, used if ``Retry-After`` is not present in the response. :param int max_attempts: Maximum number of attempts (per authorization) before `PollError` with non-empty ``waiting`` is raised. :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is the issued certificate (`.messages.CertificateResource`), and ``updated_authzrs`` is a `tuple` consisting of updated Authorization Resources (`.AuthorizationResource`) as present in the responses from server, and in the same order as the input ``authzrs``. :rtype: `tuple` :raises PollError: in case of timeout or if some authorization was marked by the CA as invalid """ # pylint: disable=too-many-locals assert max_attempts > 0 attempts = collections.defaultdict(int) # type: Dict[messages.AuthorizationResource, int] exhausted = set() # priority queue with datetime.datetime (based on Retry-After) as key, # and original Authorization Resource as value waiting = [ (datetime.datetime.now(), index, authzr) for index, authzr in enumerate(authzrs) ] heapq.heapify(waiting) # mapping between original Authorization Resource and the most # recently updated one updated = dict((authzr, authzr) for authzr in authzrs) while waiting: # find the smallest Retry-After, and sleep if necessary when, index, authzr = heapq.heappop(waiting) now = datetime.datetime.now() if when > now: seconds = (when - now).seconds logger.debug('Sleeping for %d seconds', seconds) time.sleep(seconds) # Note that we poll with the latest updated Authorization # URI, which might have a different URI than initial one updated_authzr, response = self.poll(updated[authzr]) updated[authzr] = updated_authzr attempts[authzr] += 1 # pylint: disable=no-member if updated_authzr.body.status not in ( messages.STATUS_VALID, messages.STATUS_INVALID): if attempts[authzr] < max_attempts: # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self.retry_after( response, default=mintime), index, authzr)) else: exhausted.add(authzr) if exhausted or any(authzr.body.status == messages.STATUS_INVALID for authzr in six.itervalues(updated)): raise errors.PollError(exhausted, updated) updated_authzrs = tuple(updated[authzr] for authzr in authzrs) return self.request_issuance(csr, updated_authzrs), updated_authzrs def _get_cert(self, uri): """Returns certificate from URI. :param str uri: URI of certificate :returns: tuple of the form (response, :class:`josepy.util.ComparableX509`) :rtype: tuple """ content_type = DER_CONTENT_TYPE # TODO: make it a param response = self.net.get(uri, headers={'Accept': content_type}, content_type=content_type) return response, jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_ASN1, response.content)) def check_cert(self, certr): """Check for new cert. :param certr: Certificate Resource :type certr: `.CertificateResource` :returns: Updated Certificate Resource. :rtype: `.CertificateResource` """ # TODO: acme-spec 5.1 table action should be renamed to # "refresh cert", and this method integrated with self.refresh response, cert = self._get_cert(certr.uri) if 'Location' not in response.headers: raise errors.ClientError('Location header missing') if response.headers['Location'] != certr.uri: raise errors.UnexpectedUpdate(response.text) return certr.update(body=cert) def refresh(self, certr): """Refresh certificate. :param certr: Certificate Resource :type certr: `.CertificateResource` :returns: Updated Certificate Resource. :rtype: `.CertificateResource` """ # TODO: If a client sends a refresh request and the server is # not willing to refresh the certificate, the server MUST # respond with status code 403 (Forbidden) return self.check_cert(certr) def fetch_chain(self, certr, max_length=10): """Fetch chain for certificate. :param .CertificateResource certr: Certificate Resource :param int max_length: Maximum allowed length of the chain. Note that each element in the certificate requires new ``HTTP GET`` request, and the length of the chain is controlled by the ACME CA. :raises errors.Error: if recursion exceeds `max_length` :returns: Certificate chain for the Certificate Resource. It is a list ordered so that the first element is a signer of the certificate from Certificate Resource. Will be empty if ``cert_chain_uri`` is ``None``. :rtype: `list` of `OpenSSL.crypto.X509` wrapped in `.ComparableX509` """ chain = [] # type: List[jose.ComparableX509] uri = certr.cert_chain_uri while uri is not None and len(chain) < max_length: response, cert = self._get_cert(uri) uri = response.links.get('up', {}).get('url') chain.append(cert) if uri is not None: raise errors.Error( "Recursion limit reached. Didn't get {0}".format(uri)) return chain def revoke(self, cert, rsn): """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :param int rsn: Reason code for certificate revocation. :raises .ClientError: If revocation is unsuccessful. """ return self._revoke(cert, rsn, self.directory[messages.Revocation]) class ClientV2(ClientBase): """ACME client for a v2 API. :ivar messages.Directory directory: :ivar .ClientNetwork net: Client network. """ def __init__(self, directory, net): """Initialize. :param .messages.Directory directory: Directory Resource :param .ClientNetwork net: Client network. """ super(ClientV2, self).__init__(directory=directory, net=net, acme_version=2) def new_account(self, new_account): """Register. :param .NewRegistration new_account: :raises .ConflictError: in case the account already exists :returns: Registration Resource. :rtype: `.RegistrationResource` """ response = self._post(self.directory['newAccount'], new_account) # if account already exists if response.status_code == 200 and 'Location' in response.headers: raise errors.ConflictError(response.headers.get('Location')) # "Instance of 'Field' has no key/contact member" bug: # pylint: disable=no-member regr = self._regr_from_response(response) self.net.account = regr return regr def query_registration(self, regr): """Query server about registration. :param messages.RegistrationResource: Existing Registration Resource. """ self.net.account = regr updated_regr = super(ClientV2, self).query_registration(regr) self.net.account = updated_regr return updated_regr def update_registration(self, regr, update=None): """Update registration. :param messages.RegistrationResource regr: Registration Resource. :param messages.Registration update: Updated body of the resource. If not provided, body will be taken from `regr`. :returns: Updated Registration Resource. :rtype: `.RegistrationResource` """ # https://github.com/certbot/certbot/issues/6155 new_regr = self._get_v2_account(regr) return super(ClientV2, self).update_registration(new_regr, update) def _get_v2_account(self, regr): self.net.account = None only_existing_reg = regr.body.update(only_return_existing=True) response = self._post(self.directory['newAccount'], only_existing_reg) updated_uri = response.headers['Location'] new_regr = regr.update(uri=updated_uri) self.net.account = new_regr return new_regr def new_order(self, csr_pem): """Request a new Order object from the server. :param str csr_pem: A CSR in PEM format. :returns: The newly created order. :rtype: OrderResource """ csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem) # pylint: disable=protected-access dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr) identifiers = [] for name in dnsNames: identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=name)) order = messages.NewOrder(identifiers=identifiers) response = self._post(self.directory['newOrder'], order) body = messages.Order.from_json(response.json()) authorizations = [] for url in body.authorizations: authorizations.append(self._authzr_from_response(self._post_as_get(url), uri=url)) return messages.OrderResource( body=body, uri=response.headers.get('Location'), authorizations=authorizations, csr_pem=csr_pem) def poll(self, authzr): """Poll Authorization Resource for status. :param authzr: Authorization Resource :type authzr: `.AuthorizationResource` :returns: Updated Authorization Resource and HTTP response. :rtype: (`.AuthorizationResource`, `requests.Response`) """ response = self._post_as_get(authzr.uri) updated_authzr = self._authzr_from_response( response, authzr.body.identifier, authzr.uri) return updated_authzr, response def poll_and_finalize(self, orderr, deadline=None): """Poll authorizations and finalize the order. If no deadline is provided, this method will timeout after 90 seconds. :param messages.OrderResource orderr: order to finalize :param datetime.datetime deadline: when to stop polling and timeout :returns: finalized order :rtype: messages.OrderResource """ if deadline is None: deadline = datetime.datetime.now() + datetime.timedelta(seconds=90) orderr = self.poll_authorizations(orderr, deadline) return self.finalize_order(orderr, deadline) def poll_authorizations(self, orderr, deadline): """Poll Order Resource for status.""" responses = [] for url in orderr.body.authorizations: while datetime.datetime.now() < deadline: authzr = self._authzr_from_response(self._post_as_get(url), uri=url) if authzr.body.status != messages.STATUS_PENDING: responses.append(authzr) break time.sleep(1) # If we didn't get a response for every authorization, we fell through # the bottom of the loop due to hitting the deadline. if len(responses) < len(orderr.body.authorizations): raise errors.TimeoutError() failed = [] for authzr in responses: if authzr.body.status != messages.STATUS_VALID: for chall in authzr.body.challenges: if chall.error != None: failed.append(authzr) if len(failed) > 0: raise errors.ValidationError(failed) return orderr.update(authorizations=responses) def finalize_order(self, orderr, deadline): """Finalize an order and obtain a certificate. :param messages.OrderResource orderr: order to finalize :param datetime.datetime deadline: when to stop polling and timeout :returns: finalized order :rtype: messages.OrderResource """ csr = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, orderr.csr_pem) wrapped_csr = messages.CertificateRequest(csr=jose.ComparableX509(csr)) self._post(orderr.body.finalize, wrapped_csr) while datetime.datetime.now() < deadline: time.sleep(1) response = self._post_as_get(orderr.uri) body = messages.Order.from_json(response.json()) if body.error is not None: raise errors.IssuanceError(body.error) if body.certificate is not None: certificate_response = self._post_as_get(body.certificate, content_type=DER_CONTENT_TYPE).text return orderr.update(body=body, fullchain_pem=certificate_response) raise errors.TimeoutError() def revoke(self, cert, rsn): """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :param int rsn: Reason code for certificate revocation. :raises .ClientError: If revocation is unsuccessful. """ return self._revoke(cert, rsn, self.directory['revokeCert']) def external_account_required(self): """Checks if ACME server requires External Account Binding authentication.""" if hasattr(self.directory, 'meta') and self.directory.meta.external_account_required: return True else: return False def _post_as_get(self, *args, **kwargs): """ Send GET request using the POST-as-GET protocol if needed. The request will be first issued using POST-as-GET for ACME v2. If the ACME CA servers do not support this yet and return an error, request will be retried using GET. For ACME v1, only GET request will be tried, as POST-as-GET is not supported. :param args: :param kwargs: :return: """ if self.acme_version >= 2: # We add an empty payload for POST-as-GET requests new_args = args[:1] + (None,) + args[1:] try: return self._post(*new_args, **kwargs) # pylint: disable=star-args except messages.Error as error: if error.code == 'malformed': logger.debug('Error during a POST-as-GET request, ' 'your ACME CA may not support it:\n%s', error) logger.debug('Retrying request with GET.') else: # pragma: no cover raise # If POST-as-GET is not supported yet, we use a GET instead. return self.net.get(*args, **kwargs) class BackwardsCompatibleClientV2(object): """ACME client wrapper that tends towards V2-style calls, but supports V1 servers. .. note:: While this class handles the majority of the differences between versions of the ACME protocol, if you need to support an ACME server based on version 3 or older of the IETF ACME draft that uses combinations in authorizations (or lack thereof) to signal that the client needs to complete something other than any single challenge in the authorization to make it valid, the user of this class needs to understand and handle these differences themselves. This does not apply to either of Let's Encrypt's endpoints where successfully completing any challenge in an authorization will make it valid. :ivar int acme_version: 1 or 2, corresponding to the Let's Encrypt endpoint :ivar .ClientBase client: either Client or ClientV2 """ def __init__(self, net, key, server): directory = messages.Directory.from_json(net.get(server).json()) self.acme_version = self._acme_version_from_directory(directory) if self.acme_version == 1: self.client = Client(directory, key=key, net=net) else: self.client = ClientV2(directory, net=net) def __getattr__(self, name): return getattr(self.client, name) def new_account_and_tos(self, regr, check_tos_cb=None): """Combined register and agree_tos for V1, new_account for V2 :param .NewRegistration regr: :param callable check_tos_cb: callback that raises an error if the check does not work """ def _assess_tos(tos): if check_tos_cb is not None: check_tos_cb(tos) if self.acme_version == 1: regr = self.client.register(regr) if regr.terms_of_service is not None: _assess_tos(regr.terms_of_service) return self.client.agree_to_tos(regr) return regr else: if "terms_of_service" in self.client.directory.meta: _assess_tos(self.client.directory.meta.terms_of_service) regr = regr.update(terms_of_service_agreed=True) return self.client.new_account(regr) def new_order(self, csr_pem): """Request a new Order object from the server. If using ACMEv1, returns a dummy OrderResource with only the authorizations field filled in. :param str csr_pem: A CSR in PEM format. :returns: The newly created order. :rtype: OrderResource :raises errors.WildcardUnsupportedError: if a wildcard domain is requested but unsupported by the ACME version """ if self.acme_version == 1: csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem) # pylint: disable=protected-access dnsNames = crypto_util._pyopenssl_cert_or_req_all_names(csr) authorizations = [] for domain in dnsNames: authorizations.append(self.client.request_domain_challenges(domain)) return messages.OrderResource(authorizations=authorizations, csr_pem=csr_pem) else: return self.client.new_order(csr_pem) def finalize_order(self, orderr, deadline): """Finalize an order and obtain a certificate. :param messages.OrderResource orderr: order to finalize :param datetime.datetime deadline: when to stop polling and timeout :returns: finalized order :rtype: messages.OrderResource """ if self.acme_version == 1: csr_pem = orderr.csr_pem certr = self.client.request_issuance( jose.ComparableX509( OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)), orderr.authorizations) chain = None while datetime.datetime.now() < deadline: try: chain = self.client.fetch_chain(certr) break except errors.Error: time.sleep(1) if chain is None: raise errors.TimeoutError( 'Failed to fetch chain. You should not deploy the generated ' 'certificate, please rerun the command for a new one.') cert = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped).decode() chain = crypto_util.dump_pyopenssl_chain(chain).decode() return orderr.update(fullchain_pem=(cert + chain)) else: return self.client.finalize_order(orderr, deadline) def revoke(self, cert, rsn): """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :param int rsn: Reason code for certificate revocation. :raises .ClientError: If revocation is unsuccessful. """ return self.client.revoke(cert, rsn) def _acme_version_from_directory(self, directory): if hasattr(directory, 'newNonce'): return 2 else: return 1 def external_account_required(self): """Checks if the server requires an external account for ACMEv2 servers. Always return False for ACMEv1 servers, as it doesn't use External Account Binding.""" if self.acme_version == 1: return False else: return self.client.external_account_required() class ClientNetwork(object): # pylint: disable=too-many-instance-attributes """Wrapper around requests that signs POSTs for authentication. Also adds user agent, and handles Content-Type. """ JSON_CONTENT_TYPE = 'application/json' JOSE_CONTENT_TYPE = 'application/jose+json' JSON_ERROR_CONTENT_TYPE = 'application/problem+json' REPLAY_NONCE_HEADER = 'Replay-Nonce' """Initialize. :param josepy.JWK key: Account private key :param messages.RegistrationResource account: Account object. Required if you are planning to use .post() with acme_version=2 for anything other than creating a new account; may be set later after registering. :param josepy.JWASignature alg: Algoritm to use in signing JWS. :param bool verify_ssl: Whether to verify certificates on SSL connections. :param str user_agent: String to send as User-Agent header. :param float timeout: Timeout for requests. :param source_address: Optional source address to bind to when making requests. :type source_address: str or tuple(str, int) """ def __init__(self, key, account=None, alg=jose.RS256, verify_ssl=True, user_agent='acme-python', timeout=DEFAULT_NETWORK_TIMEOUT, source_address=None): # pylint: disable=too-many-arguments self.key = key self.account = account self.alg = alg self.verify_ssl = verify_ssl self._nonces = set() # type: Set[Text] self.user_agent = user_agent self.session = requests.Session() self._default_timeout = timeout adapter = HTTPAdapter() if source_address is not None: adapter = SourceAddressAdapter(source_address) self.session.mount("http://", adapter) self.session.mount("https://", adapter) def __del__(self): # Try to close the session, but don't show exceptions to the # user if the call to close() fails. See #4840. try: self.session.close() except Exception: # pylint: disable=broad-except pass def _wrap_in_jws(self, obj, nonce, url, acme_version): """Wrap `JSONDeSerializable` object in JWS. .. todo:: Implement ``acmePath``. :param josepy.JSONDeSerializable obj: :param str url: The URL to which this object will be POSTed :param bytes nonce: :rtype: `josepy.JWS` """ jobj = obj.json_dumps(indent=2).encode() if obj else b'' logger.debug('JWS payload:\n%s', jobj) kwargs = { "alg": self.alg, "nonce": nonce } if acme_version == 2: kwargs["url"] = url # newAccount and revokeCert work without the kid # newAccount must not have kid if self.account is not None: kwargs["kid"] = self.account["uri"] kwargs["key"] = self.key # pylint: disable=star-args return jws.JWS.sign(jobj, **kwargs).json_dumps(indent=2) @classmethod def _check_response(cls, response, content_type=None): """Check response content and its type. .. note:: Checking is not strict: wrong server response ``Content-Type`` HTTP header is ignored if response is an expected JSON object (c.f. Boulder #56). :param str content_type: Expected Content-Type response header. If JSON is expected and not present in server response, this function will raise an error. Otherwise, wrong Content-Type is ignored, but logged. :raises .messages.Error: If server response body carries HTTP Problem (draft-ietf-appsawg-http-problem-00). :raises .ClientError: In case of other networking errors. """ response_ct = response.headers.get('Content-Type') try: # TODO: response.json() is called twice, once here, and # once in _get and _post clients jobj = response.json() except ValueError: jobj = None if response.status_code == 409: raise errors.ConflictError(response.headers.get('Location')) if not response.ok: if jobj is not None: if response_ct != cls.JSON_ERROR_CONTENT_TYPE: logger.debug( 'Ignoring wrong Content-Type (%r) for JSON Error', response_ct) try: raise messages.Error.from_json(jobj) except jose.DeserializationError as error: # Couldn't deserialize JSON object raise errors.ClientError((response, error)) else: # response is not JSON object raise errors.ClientError(response) else: if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: logger.debug( 'Ignoring wrong Content-Type (%r) for JSON decodable ' 'response', response_ct) if content_type == cls.JSON_CONTENT_TYPE and jobj is None: raise errors.ClientError( 'Unexpected response Content-Type: {0}'.format(response_ct)) return response def _send_request(self, method, url, *args, **kwargs): # pylint: disable=too-many-locals """Send HTTP request. Makes sure that `verify_ssl` is respected. Logs request and response (with headers). For allowed parameters please see `requests.request`. :param str method: method for the new `requests.Request` object :param str url: URL for the new `requests.Request` object :raises requests.exceptions.RequestException: in case of any problems :returns: HTTP Response :rtype: `requests.Response` """ if method == "POST": logger.debug('Sending POST request to %s:\n%s', url, kwargs['data']) else: logger.debug('Sending %s request to %s.', method, url) kwargs['verify'] = self.verify_ssl kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('User-Agent', self.user_agent) kwargs.setdefault('timeout', self._default_timeout) try: response = self.session.request(method, url, *args, **kwargs) except requests.exceptions.RequestException as e: # pylint: disable=pointless-string-statement """Requests response parsing The requests library emits exceptions with a lot of extra text. We parse them with a regexp to raise a more readable exceptions. Example: HTTPSConnectionPool(host='acme-v01.api.letsencrypt.org', port=443): Max retries exceeded with url: /directory (Caused by NewConnectionError(' : Failed to establish a new connection: [Errno 65] No route to host',))""" # pylint: disable=line-too-long err_regex = r".*host='(\S*)'.*Max retries exceeded with url\: (\/\w*).*(\[Errno \d+\])([A-Za-z ]*)" m = re.match(err_regex, str(e)) if m is None: raise # pragma: no cover else: host, path, _err_no, err_msg = m.groups() raise ValueError("Requesting {0}{1}:{2}".format(host, path, err_msg)) # If content is DER, log the base64 of it instead of raw bytes, to keep # binary data out of the logs. if response.headers.get("Content-Type") == DER_CONTENT_TYPE: debug_content = base64.b64encode(response.content) else: debug_content = response.content.decode("utf-8") logger.debug('Received response:\nHTTP %d\n%s\n\n%s', response.status_code, "\n".join(["{0}: {1}".format(k, v) for k, v in response.headers.items()]), debug_content) return response def head(self, *args, **kwargs): """Send HEAD request without checking the response. Note, that `_check_response` is not called, as it is expected that status code other than successfully 2xx will be returned, or messages2.Error will be raised by the server. """ return self._send_request('HEAD', *args, **kwargs) def get(self, url, content_type=JSON_CONTENT_TYPE, **kwargs): """Send GET request and check response.""" return self._check_response( self._send_request('GET', url, **kwargs), content_type=content_type) def _add_nonce(self, response): if self.REPLAY_NONCE_HEADER in response.headers: nonce = response.headers[self.REPLAY_NONCE_HEADER] try: decoded_nonce = jws.Header._fields['nonce'].decode(nonce) except jose.DeserializationError as error: raise errors.BadNonce(nonce, error) logger.debug('Storing nonce: %s', nonce) self._nonces.add(decoded_nonce) else: raise errors.MissingNonce(response) def _get_nonce(self, url, new_nonce_url): if not self._nonces: logger.debug('Requesting fresh nonce') if new_nonce_url is None: response = self.head(url) else: # request a new nonce from the acme newNonce endpoint response = self._check_response(self.head(new_nonce_url), content_type=None) self._add_nonce(response) return self._nonces.pop() def post(self, *args, **kwargs): """POST object wrapped in `.JWS` and check response. If the server responded with a badNonce error, the request will be retried once. """ try: return self._post_once(*args, **kwargs) except messages.Error as error: if error.code == 'badNonce': logger.debug('Retrying request after error:\n%s', error) return self._post_once(*args, **kwargs) else: raise def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE, acme_version=1, **kwargs): try: new_nonce_url = kwargs.pop('new_nonce_url') except KeyError: new_nonce_url = None data = self._wrap_in_jws(obj, self._get_nonce(url, new_nonce_url), url, acme_version) kwargs.setdefault('headers', {'Content-Type': content_type}) response = self._send_request('POST', url, data=data, **kwargs) response = self._check_response(response, content_type=content_type) self._add_nonce(response) return response acme-0.31.0/acme/standalone_test.py0000644000076600000240000002504413427120435017065 0ustar bmwstaff00000000000000"""Tests for acme.standalone.""" import os import shutil import socket import threading import tempfile import unittest from six.moves import http_client # pylint: disable=import-error from six.moves import queue # pylint: disable=import-error from six.moves import socketserver # type: ignore # pylint: disable=import-error import josepy as jose import mock import requests from acme import challenges from acme import crypto_util from acme import test_util from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module class TLSServerTest(unittest.TestCase): """Tests for acme.standalone.TLSServer.""" def test_bind(self): # pylint: disable=no-self-use from acme.standalone import TLSServer server = TLSServer( ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True) server.server_close() # pylint: disable=no-member def test_ipv6(self): if socket.has_ipv6: from acme.standalone import TLSServer server = TLSServer( ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True, ipv6=True) server.server_close() # pylint: disable=no-member class TLSSNI01ServerTest(unittest.TestCase): """Test for acme.standalone.TLSSNI01Server.""" def setUp(self): self.certs = {b'localhost': ( test_util.load_pyopenssl_private_key('rsa2048_key.pem'), test_util.load_cert('rsa2048_cert.pem'), )} from acme.standalone import TLSSNI01Server self.server = TLSSNI01Server(('localhost', 0), certs=self.certs) # pylint: disable=no-member self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() def tearDown(self): self.server.shutdown() # pylint: disable=no-member self.thread.join() def test_it(self): host, port = self.server.socket.getsockname()[:2] cert = crypto_util.probe_sni( b'localhost', host=host, port=port, timeout=1) self.assertEqual(jose.ComparableX509(cert), jose.ComparableX509(self.certs[b'localhost'][1])) class HTTP01ServerTest(unittest.TestCase): """Tests for acme.standalone.HTTP01Server.""" def setUp(self): self.account_key = jose.JWK.load( test_util.load_vector('rsa1024_key.pem')) self.resources = set() # type: Set from acme.standalone import HTTP01Server self.server = HTTP01Server(('', 0), resources=self.resources) # pylint: disable=no-member self.port = self.server.socket.getsockname()[1] self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() def tearDown(self): self.server.shutdown() # pylint: disable=no-member self.thread.join() def test_index(self): response = requests.get( 'http://localhost:{0}'.format(self.port), verify=False) self.assertEqual( response.text, 'ACME client standalone challenge solver') self.assertTrue(response.ok) def test_404(self): response = requests.get( 'http://localhost:{0}/foo'.format(self.port), verify=False) self.assertEqual(response.status_code, http_client.NOT_FOUND) def _test_http01(self, add): chall = challenges.HTTP01(token=(b'x' * 16)) response, validation = chall.response_and_validation(self.account_key) from acme.standalone import HTTP01RequestHandler resource = HTTP01RequestHandler.HTTP01Resource( chall=chall, response=response, validation=validation) if add: self.resources.add(resource) return resource.response.simple_verify( resource.chall, 'localhost', self.account_key.public_key(), port=self.port) def test_http01_found(self): self.assertTrue(self._test_http01(add=True)) def test_http01_not_found(self): self.assertFalse(self._test_http01(add=False)) class BaseDualNetworkedServersTest(unittest.TestCase): """Test for acme.standalone.BaseDualNetworkedServers.""" class SingleProtocolServer(socketserver.TCPServer): """Server that only serves on a single protocol. FreeBSD has this behavior for AF_INET6.""" def __init__(self, *args, **kwargs): ipv6 = kwargs.pop("ipv6", False) if ipv6: self.address_family = socket.AF_INET6 kwargs["bind_and_activate"] = False else: self.address_family = socket.AF_INET socketserver.TCPServer.__init__(self, *args, **kwargs) if ipv6: # NB: On Windows, socket.IPPROTO_IPV6 constant may be missing. # We use the corresponding value (41) instead. level = getattr(socket, "IPPROTO_IPV6", 41) # pylint: disable=no-member self.socket.setsockopt(level, socket.IPV6_V6ONLY, 1) try: self.server_bind() self.server_activate() except: self.server_close() raise @mock.patch("socket.socket.bind") def test_fail_to_bind(self, mock_bind): mock_bind.side_effect = socket.error from acme.standalone import BaseDualNetworkedServers self.assertRaises(socket.error, BaseDualNetworkedServers, BaseDualNetworkedServersTest.SingleProtocolServer, ('', 0), socketserver.BaseRequestHandler) def test_ports_equal(self): from acme.standalone import BaseDualNetworkedServers servers = BaseDualNetworkedServers( BaseDualNetworkedServersTest.SingleProtocolServer, ('', 0), socketserver.BaseRequestHandler) socknames = servers.getsocknames() prev_port = None # assert ports are equal for sockname in socknames: port = sockname[1] if prev_port: self.assertEqual(prev_port, port) prev_port = port class TLSSNI01DualNetworkedServersTest(unittest.TestCase): """Test for acme.standalone.TLSSNI01DualNetworkedServers.""" def setUp(self): self.certs = {b'localhost': ( test_util.load_pyopenssl_private_key('rsa2048_key.pem'), test_util.load_cert('rsa2048_cert.pem'), )} from acme.standalone import TLSSNI01DualNetworkedServers self.servers = TLSSNI01DualNetworkedServers(('localhost', 0), certs=self.certs) self.servers.serve_forever() def tearDown(self): self.servers.shutdown_and_server_close() def test_connect(self): socknames = self.servers.getsocknames() # connect to all addresses for sockname in socknames: host, port = sockname[:2] cert = crypto_util.probe_sni( b'localhost', host=host, port=port, timeout=1) self.assertEqual(jose.ComparableX509(cert), jose.ComparableX509(self.certs[b'localhost'][1])) class HTTP01DualNetworkedServersTest(unittest.TestCase): """Tests for acme.standalone.HTTP01DualNetworkedServers.""" def setUp(self): self.account_key = jose.JWK.load( test_util.load_vector('rsa1024_key.pem')) self.resources = set() # type: Set from acme.standalone import HTTP01DualNetworkedServers self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources) # pylint: disable=no-member self.port = self.servers.getsocknames()[0][1] self.servers.serve_forever() def tearDown(self): self.servers.shutdown_and_server_close() def test_index(self): response = requests.get( 'http://localhost:{0}'.format(self.port), verify=False) self.assertEqual( response.text, 'ACME client standalone challenge solver') self.assertTrue(response.ok) def test_404(self): response = requests.get( 'http://localhost:{0}/foo'.format(self.port), verify=False) self.assertEqual(response.status_code, http_client.NOT_FOUND) def _test_http01(self, add): chall = challenges.HTTP01(token=(b'x' * 16)) response, validation = chall.response_and_validation(self.account_key) from acme.standalone import HTTP01RequestHandler resource = HTTP01RequestHandler.HTTP01Resource( chall=chall, response=response, validation=validation) if add: self.resources.add(resource) return resource.response.simple_verify( resource.chall, 'localhost', self.account_key.public_key(), port=self.port) def test_http01_found(self): self.assertTrue(self._test_http01(add=True)) def test_http01_not_found(self): self.assertFalse(self._test_http01(add=False)) @test_util.broken_on_windows class TestSimpleTLSSNI01Server(unittest.TestCase): """Tests for acme.standalone.simple_tls_sni_01_server.""" def setUp(self): # mirror ../examples/standalone self.test_cwd = tempfile.mkdtemp() localhost_dir = os.path.join(self.test_cwd, 'localhost') os.makedirs(localhost_dir) shutil.copy(test_util.vector_path('rsa2048_cert.pem'), os.path.join(localhost_dir, 'cert.pem')) shutil.copy(test_util.vector_path('rsa2048_key.pem'), os.path.join(localhost_dir, 'key.pem')) from acme.standalone import simple_tls_sni_01_server self.thread = threading.Thread( target=simple_tls_sni_01_server, kwargs={ 'cli_args': ('filename',), 'forever': False, }, ) self.old_cwd = os.getcwd() os.chdir(self.test_cwd) def tearDown(self): os.chdir(self.old_cwd) self.thread.join() shutil.rmtree(self.test_cwd) @mock.patch('acme.standalone.logger') def test_it(self, mock_logger): # Use a Queue because mock objects aren't thread safe. q = queue.Queue() # type: queue.Queue[int] # Add port number to the queue. mock_logger.info.side_effect = lambda *args: q.put(args[-1]) self.thread.start() # After the timeout, an exception is raised if the queue is empty. port = q.get(timeout=5) cert = crypto_util.probe_sni(b'localhost', b'0.0.0.0', port) self.assertEqual(jose.ComparableX509(cert), test_util.load_comparable_cert( 'rsa2048_cert.pem')) if __name__ == "__main__": unittest.main() # pragma: no cover acme-0.31.0/acme/__init__.py0000644000076600000240000000132713427120435015433 0ustar bmwstaff00000000000000"""ACME protocol implementation. This module is an implementation of the `ACME protocol`_. .. _`ACME protocol`: https://ietf-wg-acme.github.io/acme """ import sys # This code exists to keep backwards compatibility with people using acme.jose # before it became the standalone josepy package. # # It is based on # https://github.com/requests/requests/blob/1278ecdf71a312dc2268f3bfc0aabfab3c006dcf/requests/packages.py import josepy as jose for mod in list(sys.modules): # This traversal is apparently necessary such that the identities are # preserved (acme.jose.* is josepy.*) if mod == 'josepy' or mod.startswith('josepy.'): sys.modules['acme.' + mod.replace('josepy', 'jose', 1)] = sys.modules[mod] acme-0.31.0/acme/test_util.py0000644000076600000240000000617413427120435015715 0ustar bmwstaff00000000000000"""Test utilities. .. warning:: This module is not part of the public API. """ import os import sys import pkg_resources import unittest from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import josepy as jose from OpenSSL import crypto def vector_path(*names): """Path to a test vector.""" return pkg_resources.resource_filename( __name__, os.path.join('testdata', *names)) def load_vector(*names): """Load contents of a test vector.""" # luckily, resource_string opens file in binary mode return pkg_resources.resource_string( __name__, os.path.join('testdata', *names)) def _guess_loader(filename, loader_pem, loader_der): _, ext = os.path.splitext(filename) if ext.lower() == '.pem': return loader_pem elif ext.lower() == '.der': return loader_der else: # pragma: no cover raise ValueError("Loader could not be recognized based on extension") def load_cert(*names): """Load certificate.""" loader = _guess_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_certificate(loader, load_vector(*names)) def load_comparable_cert(*names): """Load ComparableX509 cert.""" return jose.ComparableX509(load_cert(*names)) def load_csr(*names): """Load certificate request.""" loader = _guess_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_certificate_request(loader, load_vector(*names)) def load_comparable_csr(*names): """Load ComparableX509 certificate request.""" return jose.ComparableX509(load_csr(*names)) def load_rsa_private_key(*names): """Load RSA private key.""" loader = _guess_loader(names[-1], serialization.load_pem_private_key, serialization.load_der_private_key) return jose.ComparableRSAKey(loader( load_vector(*names), password=None, backend=default_backend())) def load_pyopenssl_private_key(*names): """Load pyOpenSSL private key.""" loader = _guess_loader( names[-1], crypto.FILETYPE_PEM, crypto.FILETYPE_ASN1) return crypto.load_privatekey(loader, load_vector(*names)) def skip_unless(condition, reason): # pragma: no cover """Skip tests unless a condition holds. This implements the basic functionality of unittest.skipUnless which is only available on Python 2.7+. :param bool condition: If ``False``, the test will be skipped :param str reason: the reason for skipping the test :rtype: callable :returns: decorator that hides tests unless condition is ``True`` """ if hasattr(unittest, "skipUnless"): return unittest.skipUnless(condition, reason) elif condition: return lambda cls: cls else: return lambda cls: None def broken_on_windows(function): """Decorator to skip temporarily a broken test on Windows.""" reason = 'Test is broken and ignored on windows but should be fixed.' return unittest.skipIf( sys.platform == 'win32' and os.environ.get('SKIP_BROKEN_TESTS_ON_WINDOWS', 'true') == 'true', reason)(function) acme-0.31.0/acme/util_test.py0000644000076600000240000000071013427120435015703 0ustar bmwstaff00000000000000"""Tests for acme.util.""" import unittest class MapKeysTest(unittest.TestCase): """Tests for acme.util.map_keys.""" def test_it(self): from acme.util import map_keys self.assertEqual({'a': 'b', 'c': 'd'}, map_keys({'a': 'b', 'c': 'd'}, lambda key: key)) self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1)) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.31.0/acme/messages.py0000644000076600000240000004616513427120435015514 0ustar bmwstaff00000000000000"""ACME protocol messages.""" import six import json try: from collections.abc import Hashable # pylint: disable=no-name-in-module except ImportError: from collections import Hashable import josepy as jose from acme import challenges from acme import errors from acme import fields from acme import util from acme import jws OLD_ERROR_PREFIX = "urn:acme:error:" ERROR_PREFIX = "urn:ietf:params:acme:error:" ERROR_CODES = { 'badCSR': 'The CSR is unacceptable (e.g., due to a short key)', 'badNonce': 'The client sent an unacceptable anti-replay nonce', 'connection': ('The server could not connect to the client to verify the' ' domain'), 'dnssec': 'The server could not validate a DNSSEC signed domain', # deprecate invalidEmail 'invalidEmail': 'The provided email for a registration was invalid', 'invalidContact': 'The provided contact URI was invalid', 'malformed': 'The request message was malformed', 'rateLimited': 'There were too many requests of a given type', 'serverInternal': 'The server experienced an internal error', 'tls': 'The server experienced a TLS error during domain verification', 'unauthorized': 'The client lacks sufficient authorization', 'unknownHost': 'The server could not resolve a domain name', 'externalAccountRequired': 'The server requires external account binding', } ERROR_TYPE_DESCRIPTIONS = dict( (ERROR_PREFIX + name, desc) for name, desc in ERROR_CODES.items()) ERROR_TYPE_DESCRIPTIONS.update(dict( # add errors with old prefix, deprecate me (OLD_ERROR_PREFIX + name, desc) for name, desc in ERROR_CODES.items())) def is_acme_error(err): """Check if argument is an ACME error.""" if isinstance(err, Error) and (err.typ is not None): return (ERROR_PREFIX in err.typ) or (OLD_ERROR_PREFIX in err.typ) else: return False @six.python_2_unicode_compatible class Error(jose.JSONObjectWithFields, errors.Error): """ACME error. https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 :ivar unicode typ: :ivar unicode title: :ivar unicode detail: """ typ = jose.Field('type', omitempty=True, default='about:blank') title = jose.Field('title', omitempty=True) detail = jose.Field('detail', omitempty=True) @classmethod def with_code(cls, code, **kwargs): """Create an Error instance with an ACME Error code. :unicode code: An ACME error code, like 'dnssec'. :kwargs: kwargs to pass to Error. """ if code not in ERROR_CODES: raise ValueError("The supplied code: %s is not a known ACME error" " code" % code) typ = ERROR_PREFIX + code return cls(typ=typ, **kwargs) @property def description(self): """Hardcoded error description based on its type. :returns: Description if standard ACME error or ``None``. :rtype: unicode """ return ERROR_TYPE_DESCRIPTIONS.get(self.typ) @property def code(self): """ACME error code. Basically self.typ without the ERROR_PREFIX. :returns: error code if standard ACME code or ``None``. :rtype: unicode """ code = str(self.typ).split(':')[-1] if code in ERROR_CODES: return code def __str__(self): return b' :: '.join( part.encode('ascii', 'backslashreplace') for part in (self.typ, self.description, self.detail, self.title) if part is not None).decode() class _Constant(jose.JSONDeSerializable, Hashable): # type: ignore """ACME constant.""" __slots__ = ('name',) POSSIBLE_NAMES = NotImplemented def __init__(self, name): self.POSSIBLE_NAMES[name] = self self.name = name def to_partial_json(self): return self.name @classmethod def from_json(cls, value): if value not in cls.POSSIBLE_NAMES: raise jose.DeserializationError( '{0} not recognized'.format(cls.__name__)) return cls.POSSIBLE_NAMES[value] def __repr__(self): return '{0}({1})'.format(self.__class__.__name__, self.name) def __eq__(self, other): return isinstance(other, type(self)) and other.name == self.name def __hash__(self): return hash((self.__class__, self.name)) def __ne__(self, other): return not self == other class Status(_Constant): """ACME "status" field.""" POSSIBLE_NAMES = {} # type: dict STATUS_UNKNOWN = Status('unknown') STATUS_PENDING = Status('pending') STATUS_PROCESSING = Status('processing') STATUS_VALID = Status('valid') STATUS_INVALID = Status('invalid') STATUS_REVOKED = Status('revoked') STATUS_READY = Status('ready') class IdentifierType(_Constant): """ACME identifier type.""" POSSIBLE_NAMES = {} # type: dict IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder class Identifier(jose.JSONObjectWithFields): """ACME identifier. :ivar IdentifierType typ: :ivar unicode value: """ typ = jose.Field('type', decoder=IdentifierType.from_json) value = jose.Field('value') class Directory(jose.JSONDeSerializable): """Directory.""" _REGISTERED_TYPES = {} # type: dict class Meta(jose.JSONObjectWithFields): """Directory Meta.""" _terms_of_service = jose.Field('terms-of-service', omitempty=True) _terms_of_service_v2 = jose.Field('termsOfService', omitempty=True) website = jose.Field('website', omitempty=True) caa_identities = jose.Field('caaIdentities', omitempty=True) external_account_required = jose.Field('externalAccountRequired', omitempty=True) def __init__(self, **kwargs): kwargs = dict((self._internal_name(k), v) for k, v in kwargs.items()) # pylint: disable=star-args super(Directory.Meta, self).__init__(**kwargs) @property def terms_of_service(self): """URL for the CA TOS""" return self._terms_of_service or self._terms_of_service_v2 def __iter__(self): # When iterating over fields, use the external name 'terms_of_service' instead of # the internal '_terms_of_service'. for name in super(Directory.Meta, self).__iter__(): yield name[1:] if name == '_terms_of_service' else name def _internal_name(self, name): return '_' + name if name == 'terms_of_service' else name @classmethod def _canon_key(cls, key): return getattr(key, 'resource_type', key) @classmethod def register(cls, resource_body_cls): """Register resource.""" resource_type = resource_body_cls.resource_type assert resource_type not in cls._REGISTERED_TYPES cls._REGISTERED_TYPES[resource_type] = resource_body_cls return resource_body_cls def __init__(self, jobj): canon_jobj = util.map_keys(jobj, self._canon_key) # TODO: check that everything is an absolute URL; acme-spec is # not clear on that self._jobj = canon_jobj def __getattr__(self, name): try: return self[name.replace('_', '-')] except KeyError as error: raise AttributeError(str(error) + ': ' + name) def __getitem__(self, name): try: return self._jobj[self._canon_key(name)] except KeyError: raise KeyError('Directory field not found') def to_partial_json(self): return self._jobj @classmethod def from_json(cls, jobj): jobj['meta'] = cls.Meta.from_json(jobj.pop('meta', {})) return cls(jobj) class Resource(jose.JSONObjectWithFields): """ACME Resource. :ivar acme.messages.ResourceBody body: Resource body. """ body = jose.Field('body') class ResourceWithURI(Resource): """ACME Resource with URI. :ivar unicode uri: Location of the resource. """ uri = jose.Field('uri') # no ChallengeResource.uri class ResourceBody(jose.JSONObjectWithFields): """ACME Resource Body.""" class ExternalAccountBinding(object): """ACME External Account Binding""" @classmethod def from_data(cls, account_public_key, kid, hmac_key, directory): """Create External Account Binding Resource from contact details, kid and hmac.""" key_json = json.dumps(account_public_key.to_partial_json()).encode() decoded_hmac_key = jose.b64.b64decode(hmac_key) url = directory["newAccount"] eab = jws.JWS.sign(key_json, jose.jwk.JWKOct(key=decoded_hmac_key), jose.jwa.HS256, None, url, kid) return eab.to_partial_json() class Registration(ResourceBody): """Registration Resource Body. :ivar josepy.jwk.JWK key: Public key. :ivar tuple contact: Contact information following ACME spec, `tuple` of `unicode`. :ivar unicode agreement: """ # on new-reg key server ignores 'key' and populates it based on # JWS.signature.combined.jwk key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) contact = jose.Field('contact', omitempty=True, default=()) agreement = jose.Field('agreement', omitempty=True) status = jose.Field('status', omitempty=True) terms_of_service_agreed = jose.Field('termsOfServiceAgreed', omitempty=True) only_return_existing = jose.Field('onlyReturnExisting', omitempty=True) external_account_binding = jose.Field('externalAccountBinding', omitempty=True) phone_prefix = 'tel:' email_prefix = 'mailto:' @classmethod def from_data(cls, phone=None, email=None, external_account_binding=None, **kwargs): """Create registration resource from contact details.""" details = list(kwargs.pop('contact', ())) if phone is not None: details.append(cls.phone_prefix + phone) if email is not None: details.extend([cls.email_prefix + mail for mail in email.split(',')]) kwargs['contact'] = tuple(details) if external_account_binding: kwargs['external_account_binding'] = external_account_binding return cls(**kwargs) def _filter_contact(self, prefix): return tuple( detail[len(prefix):] for detail in self.contact if detail.startswith(prefix)) @property def phones(self): """All phones found in the ``contact`` field.""" return self._filter_contact(self.phone_prefix) @property def emails(self): """All emails found in the ``contact`` field.""" return self._filter_contact(self.email_prefix) @Directory.register class NewRegistration(Registration): """New registration.""" resource_type = 'new-reg' resource = fields.Resource(resource_type) class UpdateRegistration(Registration): """Update registration.""" resource_type = 'reg' resource = fields.Resource(resource_type) class RegistrationResource(ResourceWithURI): """Registration Resource. :ivar acme.messages.Registration body: :ivar unicode new_authzr_uri: Deprecated. Do not use. :ivar unicode terms_of_service: URL for the CA TOS. """ body = jose.Field('body', decoder=Registration.from_json) new_authzr_uri = jose.Field('new_authzr_uri', omitempty=True) terms_of_service = jose.Field('terms_of_service', omitempty=True) class ChallengeBody(ResourceBody): """Challenge Resource Body. .. todo:: Confusingly, this has a similar name to `.challenges.Challenge`, as well as `.achallenges.AnnotatedChallenge`. Please use names such as ``challb`` to distinguish instances of this class from ``achall``. :ivar acme.challenges.Challenge: Wrapped challenge. Conveniently, all challenge fields are proxied, i.e. you can call ``challb.x`` to get ``challb.chall.x`` contents. :ivar acme.messages.Status status: :ivar datetime.datetime validated: :ivar messages.Error error: """ __slots__ = ('chall',) # ACMEv1 has a "uri" field in challenges. ACMEv2 has a "url" field. This # challenge object supports either one, but should be accessed through the # name "uri". In Client.answer_challenge, whichever one is set will be # used. _uri = jose.Field('uri', omitempty=True, default=None) _url = jose.Field('url', omitempty=True, default=None) status = jose.Field('status', decoder=Status.from_json, omitempty=True, default=STATUS_PENDING) validated = fields.RFC3339Field('validated', omitempty=True) error = jose.Field('error', decoder=Error.from_json, omitempty=True, default=None) def __init__(self, **kwargs): kwargs = dict((self._internal_name(k), v) for k, v in kwargs.items()) # pylint: disable=star-args super(ChallengeBody, self).__init__(**kwargs) def encode(self, name): return super(ChallengeBody, self).encode(self._internal_name(name)) def to_partial_json(self): jobj = super(ChallengeBody, self).to_partial_json() jobj.update(self.chall.to_partial_json()) return jobj @classmethod def fields_from_json(cls, jobj): jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj) jobj_fields['chall'] = challenges.Challenge.from_json(jobj) return jobj_fields @property def uri(self): """The URL of this challenge.""" return self._url or self._uri def __getattr__(self, name): return getattr(self.chall, name) def __iter__(self): # When iterating over fields, use the external name 'uri' instead of # the internal '_uri'. for name in super(ChallengeBody, self).__iter__(): yield name[1:] if name == '_uri' else name def _internal_name(self, name): return '_' + name if name == 'uri' else name class ChallengeResource(Resource): """Challenge Resource. :ivar acme.messages.ChallengeBody body: :ivar unicode authzr_uri: URI found in the 'up' ``Link`` header. """ body = jose.Field('body', decoder=ChallengeBody.from_json) authzr_uri = jose.Field('authzr_uri') @property def uri(self): """The URL of the challenge body.""" # pylint: disable=function-redefined,no-member return self.body.uri class Authorization(ResourceBody): """Authorization Resource Body. :ivar acme.messages.Identifier identifier: :ivar list challenges: `list` of `.ChallengeBody` :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` of `int`, as opposed to `list` of `list` from the spec). :ivar acme.messages.Status status: :ivar datetime.datetime expires: """ identifier = jose.Field('identifier', decoder=Identifier.from_json) challenges = jose.Field('challenges', omitempty=True) combinations = jose.Field('combinations', omitempty=True) status = jose.Field('status', omitempty=True, decoder=Status.from_json) # TODO: 'expires' is allowed for Authorization Resources in # general, but for Key Authorization '[t]he "expires" field MUST # be absent'... then acme-spec gives example with 'expires' # present... That's confusing! expires = fields.RFC3339Field('expires', omitempty=True) wildcard = jose.Field('wildcard', omitempty=True) @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument return tuple(ChallengeBody.from_json(chall) for chall in value) @property def resolved_combinations(self): """Combinations with challenges instead of indices.""" return tuple(tuple(self.challenges[idx] for idx in combo) for combo in self.combinations) @Directory.register class NewAuthorization(Authorization): """New authorization.""" resource_type = 'new-authz' resource = fields.Resource(resource_type) class AuthorizationResource(ResourceWithURI): """Authorization Resource. :ivar acme.messages.Authorization body: :ivar unicode new_cert_uri: Deprecated. Do not use. """ body = jose.Field('body', decoder=Authorization.from_json) new_cert_uri = jose.Field('new_cert_uri', omitempty=True) @Directory.register class CertificateRequest(jose.JSONObjectWithFields): """ACME new-cert request. :ivar josepy.util.ComparableX509 csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` """ resource_type = 'new-cert' resource = fields.Resource(resource_type) csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) class CertificateResource(ResourceWithURI): """Certificate Resource. :ivar josepy.util.ComparableX509 body: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :ivar unicode cert_chain_uri: URI found in the 'up' ``Link`` header :ivar tuple authzrs: `tuple` of `AuthorizationResource`. """ cert_chain_uri = jose.Field('cert_chain_uri') authzrs = jose.Field('authzrs') @Directory.register class Revocation(jose.JSONObjectWithFields): """Revocation message. :ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` """ resource_type = 'revoke-cert' resource = fields.Resource(resource_type) certificate = jose.Field( 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) reason = jose.Field('reason') class Order(ResourceBody): """Order Resource Body. :ivar list of .Identifier: List of identifiers for the certificate. :ivar acme.messages.Status status: :ivar list of str authorizations: URLs of authorizations. :ivar str certificate: URL to download certificate as a fullchain PEM. :ivar str finalize: URL to POST to to request issuance once all authorizations have "valid" status. :ivar datetime.datetime expires: When the order expires. :ivar .Error error: Any error that occurred during finalization, if applicable. """ identifiers = jose.Field('identifiers', omitempty=True) status = jose.Field('status', decoder=Status.from_json, omitempty=True) authorizations = jose.Field('authorizations', omitempty=True) certificate = jose.Field('certificate', omitempty=True) finalize = jose.Field('finalize', omitempty=True) expires = fields.RFC3339Field('expires', omitempty=True) error = jose.Field('error', omitempty=True, decoder=Error.from_json) @identifiers.decoder def identifiers(value): # pylint: disable=missing-docstring,no-self-argument return tuple(Identifier.from_json(identifier) for identifier in value) class OrderResource(ResourceWithURI): """Order Resource. :ivar acme.messages.Order body: :ivar str csr_pem: The CSR this Order will be finalized with. :ivar list of acme.messages.AuthorizationResource authorizations: Fully-fetched AuthorizationResource objects. :ivar str fullchain_pem: The fetched contents of the certificate URL produced once the order was finalized, if it's present. """ body = jose.Field('body', decoder=Order.from_json) csr_pem = jose.Field('csr_pem', omitempty=True) authorizations = jose.Field('authorizations') fullchain_pem = jose.Field('fullchain_pem', omitempty=True) @Directory.register class NewOrder(Order): """New order.""" resource_type = 'new-order' acme-0.31.0/acme/fields_test.py0000644000076600000240000000404213427120435016176 0ustar bmwstaff00000000000000"""Tests for acme.fields.""" import datetime import unittest import josepy as jose import pytz class FixedTest(unittest.TestCase): """Tests for acme.fields.Fixed.""" def setUp(self): from acme.fields import Fixed self.field = Fixed('name', 'x') def test_decode(self): self.assertEqual('x', self.field.decode('x')) def test_decode_bad(self): self.assertRaises(jose.DeserializationError, self.field.decode, 'y') def test_encode(self): self.assertEqual('x', self.field.encode('x')) def test_encode_override(self): self.assertEqual('y', self.field.encode('y')) class RFC3339FieldTest(unittest.TestCase): """Tests for acme.fields.RFC3339Field.""" def setUp(self): self.decoded = datetime.datetime(2015, 3, 27, tzinfo=pytz.utc) self.encoded = '2015-03-27T00:00:00Z' def test_default_encoder(self): from acme.fields import RFC3339Field self.assertEqual( self.encoded, RFC3339Field.default_encoder(self.decoded)) def test_default_encoder_naive_fails(self): from acme.fields import RFC3339Field self.assertRaises( ValueError, RFC3339Field.default_encoder, datetime.datetime.now()) def test_default_decoder(self): from acme.fields import RFC3339Field self.assertEqual( self.decoded, RFC3339Field.default_decoder(self.encoded)) def test_default_decoder_raises_deserialization_error(self): from acme.fields import RFC3339Field self.assertRaises( jose.DeserializationError, RFC3339Field.default_decoder, '') class ResourceTest(unittest.TestCase): """Tests for acme.fields.Resource.""" def setUp(self): from acme.fields import Resource self.field = Resource('x') def test_decode_good(self): self.assertEqual('x', self.field.decode('x')) def test_decode_wrong(self): self.assertRaises(jose.DeserializationError, self.field.decode, 'y') if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.31.0/acme/magic_typing_test.py0000644000076600000240000000266213427120435017410 0ustar bmwstaff00000000000000"""Tests for acme.magic_typing.""" import sys import unittest import mock class MagicTypingTest(unittest.TestCase): """Tests for acme.magic_typing.""" def test_import_success(self): try: import typing as temp_typing except ImportError: # pragma: no cover temp_typing = None # pragma: no cover typing_class_mock = mock.MagicMock() text_mock = mock.MagicMock() typing_class_mock.Text = text_mock sys.modules['typing'] = typing_class_mock if 'acme.magic_typing' in sys.modules: del sys.modules['acme.magic_typing'] # pragma: no cover from acme.magic_typing import Text # pylint: disable=no-name-in-module self.assertEqual(Text, text_mock) del sys.modules['acme.magic_typing'] sys.modules['typing'] = temp_typing def test_import_failure(self): try: import typing as temp_typing except ImportError: # pragma: no cover temp_typing = None # pragma: no cover sys.modules['typing'] = None if 'acme.magic_typing' in sys.modules: del sys.modules['acme.magic_typing'] # pragma: no cover from acme.magic_typing import Text # pylint: disable=no-name-in-module self.assertTrue(Text is None) del sys.modules['acme.magic_typing'] sys.modules['typing'] = temp_typing if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.31.0/acme/errors.py0000644000076600000240000000711113427120435015205 0ustar bmwstaff00000000000000"""ACME errors.""" from josepy import errors as jose_errors class Error(Exception): """Generic ACME error.""" class DependencyError(Error): """Dependency error""" class SchemaValidationError(jose_errors.DeserializationError): """JSON schema ACME object validation error.""" class ClientError(Error): """Network error.""" class UnexpectedUpdate(ClientError): """Unexpected update error.""" class NonceError(ClientError): """Server response nonce error.""" class BadNonce(NonceError): """Bad nonce error.""" def __init__(self, nonce, error, *args, **kwargs): super(BadNonce, self).__init__(*args, **kwargs) self.nonce = nonce self.error = error def __str__(self): return 'Invalid nonce ({0!r}): {1}'.format(self.nonce, self.error) class MissingNonce(NonceError): """Missing nonce error. According to the specification an "ACME server MUST include an Replay-Nonce header field in each successful response to a POST it provides to a client (...)". :ivar requests.Response response: HTTP Response """ def __init__(self, response, *args, **kwargs): super(MissingNonce, self).__init__(*args, **kwargs) self.response = response def __str__(self): return ('Server {0} response did not include a replay ' 'nonce, headers: {1} (This may be a service outage)'.format( self.response.request.method, self.response.headers)) class PollError(ClientError): """Generic error when polling for authorization fails. This might be caused by either timeout (`exhausted` will be non-empty) or by some authorization being invalid. :ivar exhausted: Set of `.AuthorizationResource` that didn't finish within max allowed attempts. :ivar updated: Mapping from original `.AuthorizationResource` to the most recently updated one """ def __init__(self, exhausted, updated): self.exhausted = exhausted self.updated = updated super(PollError, self).__init__() @property def timeout(self): """Was the error caused by timeout?""" return bool(self.exhausted) def __repr__(self): return '{0}(exhausted={1!r}, updated={2!r})'.format( self.__class__.__name__, self.exhausted, self.updated) class ValidationError(Error): """Error for authorization failures. Contains a list of authorization resources, each of which is invalid and should have an error field. """ def __init__(self, failed_authzrs): self.failed_authzrs = failed_authzrs super(ValidationError, self).__init__() class TimeoutError(Error): """Error for when polling an authorization or an order times out.""" class IssuanceError(Error): """Error sent by the server after requesting issuance of a certificate.""" def __init__(self, error): """Initialize. :param messages.Error error: The error provided by the server. """ self.error = error super(IssuanceError, self).__init__() class ConflictError(ClientError): """Error for when the server returns a 409 (Conflict) HTTP status. In the version of ACME implemented by Boulder, this is used to find an account if you only have the private key, but don't know the account URL. Also used in V2 of the ACME client for the same purpose. """ def __init__(self, location): self.location = location super(ConflictError, self).__init__() class WildcardUnsupportedError(Error): """Error for when a wildcard is requested but is unsupported by ACME CA.""" acme-0.31.0/acme/crypto_util_test.py0000644000076600000240000002375713427120435017323 0ustar bmwstaff00000000000000"""Tests for acme.crypto_util.""" import itertools import socket import threading import time import unittest import six from six.moves import socketserver #type: ignore # pylint: disable=import-error import josepy as jose import OpenSSL from acme import errors from acme import test_util from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module class SSLSocketAndProbeSNITest(unittest.TestCase): """Tests for acme.crypto_util.SSLSocket/probe_sni.""" def setUp(self): self.cert = test_util.load_comparable_cert('rsa2048_cert.pem') key = test_util.load_pyopenssl_private_key('rsa2048_key.pem') # pylint: disable=protected-access certs = {b'foo': (key, self.cert.wrapped)} from acme.crypto_util import SSLSocket class _TestServer(socketserver.TCPServer): # pylint: disable=too-few-public-methods # six.moves.* | pylint: disable=attribute-defined-outside-init,no-init def server_bind(self): # pylint: disable=missing-docstring self.socket = SSLSocket(socket.socket(), certs=certs) socketserver.TCPServer.server_bind(self) self.server = _TestServer(('', 0), socketserver.BaseRequestHandler) self.port = self.server.socket.getsockname()[1] self.server_thread = threading.Thread( # pylint: disable=no-member target=self.server.handle_request) def tearDown(self): if self.server_thread.is_alive(): # The thread may have already terminated. self.server_thread.join() # pragma: no cover def _probe(self, name): from acme.crypto_util import probe_sni return jose.ComparableX509(probe_sni( name, host='127.0.0.1', port=self.port)) def _start_server(self): self.server_thread.start() time.sleep(1) # TODO: avoid race conditions in other way def test_probe_ok(self): self._start_server() self.assertEqual(self.cert, self._probe(b'foo')) def test_probe_not_recognized_name(self): self._start_server() self.assertRaises(errors.Error, self._probe, b'bar') def test_probe_connection_error(self): # pylint has a hard time with six self.server.server_close() # pylint: disable=no-member original_timeout = socket.getdefaulttimeout() try: socket.setdefaulttimeout(1) self.assertRaises(errors.Error, self._probe, b'bar') finally: socket.setdefaulttimeout(original_timeout) class PyOpenSSLCertOrReqAllNamesTest(unittest.TestCase): """Test for acme.crypto_util._pyopenssl_cert_or_req_all_names.""" @classmethod def _call(cls, loader, name): # pylint: disable=protected-access from acme.crypto_util import _pyopenssl_cert_or_req_all_names return _pyopenssl_cert_or_req_all_names(loader(name)) def _call_cert(self, name): return self._call(test_util.load_cert, name) def test_cert_one_san_no_common(self): self.assertEqual(self._call_cert('cert-nocn.der'), ['no-common-name.badssl.com']) def test_cert_no_sans_yes_common(self): self.assertEqual(self._call_cert('cert.pem'), ['example.com']) def test_cert_two_sans_yes_common(self): self.assertEqual(self._call_cert('cert-san.pem'), ['example.com', 'www.example.com']) class PyOpenSSLCertOrReqSANTest(unittest.TestCase): """Test for acme.crypto_util._pyopenssl_cert_or_req_san.""" @classmethod def _call(cls, loader, name): # pylint: disable=protected-access from acme.crypto_util import _pyopenssl_cert_or_req_san return _pyopenssl_cert_or_req_san(loader(name)) @classmethod def _get_idn_names(cls): """Returns expected names from '{cert,csr}-idnsans.pem'.""" chars = [six.unichr(i) for i in itertools.chain(range(0x3c3, 0x400), range(0x641, 0x6fc), range(0x1820, 0x1877))] return [''.join(chars[i: i + 45]) + '.invalid' for i in range(0, len(chars), 45)] def _call_cert(self, name): return self._call(test_util.load_cert, name) def _call_csr(self, name): return self._call(test_util.load_csr, name) def test_cert_no_sans(self): self.assertEqual(self._call_cert('cert.pem'), []) def test_cert_two_sans(self): self.assertEqual(self._call_cert('cert-san.pem'), ['example.com', 'www.example.com']) def test_cert_hundred_sans(self): self.assertEqual(self._call_cert('cert-100sans.pem'), ['example{0}.com'.format(i) for i in range(1, 101)]) def test_cert_idn_sans(self): self.assertEqual(self._call_cert('cert-idnsans.pem'), self._get_idn_names()) def test_csr_no_sans(self): self.assertEqual(self._call_csr('csr-nosans.pem'), []) def test_csr_one_san(self): self.assertEqual(self._call_csr('csr.pem'), ['example.com']) def test_csr_two_sans(self): self.assertEqual(self._call_csr('csr-san.pem'), ['example.com', 'www.example.com']) def test_csr_six_sans(self): self.assertEqual(self._call_csr('csr-6sans.pem'), ['example.com', 'example.org', 'example.net', 'example.info', 'subdomain.example.com', 'other.subdomain.example.com']) def test_csr_hundred_sans(self): self.assertEqual(self._call_csr('csr-100sans.pem'), ['example{0}.com'.format(i) for i in range(1, 101)]) def test_csr_idn_sans(self): self.assertEqual(self._call_csr('csr-idnsans.pem'), self._get_idn_names()) def test_critical_san(self): self.assertEqual(self._call_cert('critical-san.pem'), ['chicago-cubs.venafi.example', 'cubs.venafi.example']) class RandomSnTest(unittest.TestCase): """Test for random certificate serial numbers.""" def setUp(self): self.cert_count = 5 self.serial_num = [] # type: List[int] self.key = OpenSSL.crypto.PKey() self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) def test_sn_collisions(self): from acme.crypto_util import gen_ss_cert for _ in range(self.cert_count): cert = gen_ss_cert(self.key, ['dummy'], force_san=True) self.serial_num.append(cert.get_serial_number()) self.assertTrue(len(set(self.serial_num)) > 1) class MakeCSRTest(unittest.TestCase): """Test for standalone functions.""" @classmethod def _call_with_key(cls, *args, **kwargs): privkey = OpenSSL.crypto.PKey() privkey.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) privkey_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey) from acme.crypto_util import make_csr return make_csr(privkey_pem, *args, **kwargs) def test_make_csr(self): csr_pem = self._call_with_key(["a.example", "b.example"]) self.assertTrue(b'--BEGIN CERTIFICATE REQUEST--' in csr_pem) self.assertTrue(b'--END CERTIFICATE REQUEST--' in csr_pem) csr = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, csr_pem) # In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't # have a get_extensions() method, so we skip this test if the method # isn't available. if hasattr(csr, 'get_extensions'): self.assertEqual(len(csr.get_extensions()), 1) self.assertEqual(csr.get_extensions()[0].get_data(), OpenSSL.crypto.X509Extension( b'subjectAltName', critical=False, value=b'DNS:a.example, DNS:b.example', ).get_data(), ) def test_make_csr_must_staple(self): csr_pem = self._call_with_key(["a.example"], must_staple=True) csr = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, csr_pem) # In pyopenssl 0.13 (used with TOXENV=py27-oldest), csr objects don't # have a get_extensions() method, so we skip this test if the method # isn't available. if hasattr(csr, 'get_extensions'): self.assertEqual(len(csr.get_extensions()), 2) # NOTE: Ideally we would filter by the TLS Feature OID, but # OpenSSL.crypto.X509Extension doesn't give us the extension's raw OID, # and the shortname field is just "UNDEF" must_staple_exts = [e for e in csr.get_extensions() if e.get_data() == b"0\x03\x02\x01\x05"] self.assertEqual(len(must_staple_exts), 1, "Expected exactly one Must Staple extension") class DumpPyopensslChainTest(unittest.TestCase): """Test for dump_pyopenssl_chain.""" @classmethod def _call(cls, loaded): # pylint: disable=protected-access from acme.crypto_util import dump_pyopenssl_chain return dump_pyopenssl_chain(loaded) def test_dump_pyopenssl_chain(self): names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem'] loaded = [test_util.load_cert(name) for name in names] length = sum( len(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)) for cert in loaded) self.assertEqual(len(self._call(loaded)), length) def test_dump_pyopenssl_chain_wrapped(self): names = ['cert.pem', 'cert-san.pem', 'cert-idnsans.pem'] loaded = [test_util.load_cert(name) for name in names] wrap_func = jose.ComparableX509 wrapped = [wrap_func(cert) for cert in loaded] dump_func = OpenSSL.crypto.dump_certificate length = sum(len(dump_func(OpenSSL.crypto.FILETYPE_PEM, cert)) for cert in loaded) self.assertEqual(len(self._call(wrapped)), length) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.31.0/acme/challenges.py0000644000076600000240000004576413427120435016016 0ustar bmwstaff00000000000000"""ACME Identifier Validation Challenges.""" import abc import functools import hashlib import logging import socket import warnings from cryptography.hazmat.primitives import hashes # type: ignore import josepy as jose import OpenSSL import requests import six from acme import errors from acme import crypto_util from acme import fields logger = logging.getLogger(__name__) # pylint: disable=too-few-public-methods class Challenge(jose.TypedJSONObjectWithFields): # _fields_to_partial_json | pylint: disable=abstract-method """ACME challenge.""" TYPES = {} # type: dict @classmethod def from_json(cls, jobj): try: return super(Challenge, cls).from_json(jobj) except jose.UnrecognizedTypeError as error: logger.debug(error) return UnrecognizedChallenge.from_json(jobj) class ChallengeResponse(jose.TypedJSONObjectWithFields): # _fields_to_partial_json | pylint: disable=abstract-method """ACME challenge response.""" TYPES = {} # type: dict resource_type = 'challenge' resource = fields.Resource(resource_type) class UnrecognizedChallenge(Challenge): """Unrecognized challenge. ACME specification defines a generic framework for challenges and defines some standard challenges that are implemented in this module. However, other implementations (including peers) might define additional challenge types, which should be ignored if unrecognized. :ivar jobj: Original JSON decoded object. """ def __init__(self, jobj): super(UnrecognizedChallenge, self).__init__() object.__setattr__(self, "jobj", jobj) def to_partial_json(self): # pylint: disable=no-member return self.jobj @classmethod def from_json(cls, jobj): return cls(jobj) class _TokenChallenge(Challenge): """Challenge with token. :ivar bytes token: """ TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec """Minimum size of the :attr:`token` in bytes.""" # TODO: acme-spec doesn't specify token as base64-encoded value token = jose.Field( "token", encoder=jose.encode_b64jose, decoder=functools.partial( jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) # XXX: rename to ~token_good_for_url @property def good_token(self): # XXX: @token.decoder """Is `token` good? .. todo:: acme-spec wants "It MUST NOT contain any non-ASCII characters", but it should also warrant that it doesn't contain ".." or "/"... """ # TODO: check that path combined with uri does not go above # URI_ROOT_PATH! return b'..' not in self.token and b'/' not in self.token class KeyAuthorizationChallengeResponse(ChallengeResponse): """Response to Challenges based on Key Authorization. :param unicode key_authorization: """ key_authorization = jose.Field("keyAuthorization") thumbprint_hash_function = hashes.SHA256 def verify(self, chall, account_public_key): """Verify the key authorization. :param KeyAuthorization chall: Challenge that corresponds to this response. :param JWK account_public_key: :return: ``True`` iff verification of the key authorization was successful. :rtype: bool """ parts = self.key_authorization.split('.') # pylint: disable=no-member if len(parts) != 2: logger.debug("Key authorization (%r) is not well formed", self.key_authorization) return False if parts[0] != chall.encode("token"): logger.debug("Mismatching token in key authorization: " "%r instead of %r", parts[0], chall.encode("token")) return False thumbprint = jose.b64encode(account_public_key.thumbprint( hash_function=self.thumbprint_hash_function)).decode() if parts[1] != thumbprint: logger.debug("Mismatching thumbprint in key authorization: " "%r instead of %r", parts[0], thumbprint) return False return True @six.add_metaclass(abc.ABCMeta) class KeyAuthorizationChallenge(_TokenChallenge): # pylint: disable=abstract-class-little-used,too-many-ancestors """Challenge based on Key Authorization. :param response_cls: Subclass of `KeyAuthorizationChallengeResponse` that will be used to generate `response`. :param str typ: type of the challenge """ typ = NotImplemented response_cls = NotImplemented thumbprint_hash_function = ( KeyAuthorizationChallengeResponse.thumbprint_hash_function) def key_authorization(self, account_key): """Generate Key Authorization. :param JWK account_key: :rtype unicode: """ return self.encode("token") + "." + jose.b64encode( account_key.thumbprint( hash_function=self.thumbprint_hash_function)).decode() def response(self, account_key): """Generate response to the challenge. :param JWK account_key: :returns: Response (initialized `response_cls`) to the challenge. :rtype: KeyAuthorizationChallengeResponse """ return self.response_cls( key_authorization=self.key_authorization(account_key)) @abc.abstractmethod def validation(self, account_key, **kwargs): """Generate validation for the challenge. Subclasses must implement this method, but they are likely to return completely different data structures, depending on what's necessary to complete the challenge. Interpretation of that return value must be known to the caller. :param JWK account_key: :returns: Challenge-specific validation. """ raise NotImplementedError() # pragma: no cover def response_and_validation(self, account_key, *args, **kwargs): """Generate response and validation. Convenience function that return results of `response` and `validation`. :param JWK account_key: :rtype: tuple """ return (self.response(account_key), self.validation(account_key, *args, **kwargs)) @ChallengeResponse.register class DNS01Response(KeyAuthorizationChallengeResponse): """ACME dns-01 challenge response.""" typ = "dns-01" def simple_verify(self, chall, domain, account_public_key): """Simple verify. This method no longer checks DNS records and is a simple wrapper around `KeyAuthorizationChallengeResponse.verify`. :param challenges.DNS01 chall: Corresponding challenge. :param unicode domain: Domain name being verified. :param JWK account_public_key: Public key for the key pair being authorized. :return: ``True`` iff verification of the key authorization was successful. :rtype: bool """ # pylint: disable=unused-argument verified = self.verify(chall, account_public_key) if not verified: logger.debug("Verification of key authorization in response failed") return verified @Challenge.register # pylint: disable=too-many-ancestors class DNS01(KeyAuthorizationChallenge): """ACME dns-01 challenge.""" response_cls = DNS01Response typ = response_cls.typ LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" def validation(self, account_key, **unused_kwargs): """Generate validation. :param JWK account_key: :rtype: unicode """ return jose.b64encode(hashlib.sha256(self.key_authorization( account_key).encode("utf-8")).digest()).decode() def validation_domain_name(self, name): """Domain name for TXT validation record. :param unicode name: Domain name being validated. """ return "{0}.{1}".format(self.LABEL, name) @ChallengeResponse.register class HTTP01Response(KeyAuthorizationChallengeResponse): """ACME http-01 challenge response.""" typ = "http-01" PORT = 80 """Verification port as defined by the protocol. You can override it (e.g. for testing) by passing ``port`` to `simple_verify`. """ WHITESPACE_CUTSET = "\n\r\t " """Whitespace characters which should be ignored at the end of the body.""" def simple_verify(self, chall, domain, account_public_key, port=None): """Simple verify. :param challenges.SimpleHTTP chall: Corresponding challenge. :param unicode domain: Domain name being verified. :param JWK account_public_key: Public key for the key pair being authorized. :param int port: Port used in the validation. :returns: ``True`` iff validation with the files currently served by the HTTP server is successful. :rtype: bool """ if not self.verify(chall, account_public_key): logger.debug("Verification of key authorization in response failed") return False # TODO: ACME specification defines URI template that doesn't # allow to use a custom port... Make sure port is not in the # request URI, if it's standard. if port is not None and port != self.PORT: logger.warning( "Using non-standard port for http-01 verification: %s", port) domain += ":{0}".format(port) uri = chall.uri(domain) logger.debug("Verifying %s at %s...", chall.typ, uri) try: http_response = requests.get(uri) except requests.exceptions.RequestException as error: logger.error("Unable to reach %s: %s", uri, error) return False logger.debug("Received %s: %s. Headers: %s", http_response, http_response.text, http_response.headers) challenge_response = http_response.text.rstrip(self.WHITESPACE_CUTSET) if self.key_authorization != challenge_response: logger.debug("Key authorization from response (%r) doesn't match " "HTTP response (%r)", self.key_authorization, challenge_response) return False return True @Challenge.register # pylint: disable=too-many-ancestors class HTTP01(KeyAuthorizationChallenge): """ACME http-01 challenge.""" response_cls = HTTP01Response typ = response_cls.typ URI_ROOT_PATH = ".well-known/acme-challenge" """URI root path for the server provisioned resource.""" @property def path(self): """Path (starting with '/') for provisioned resource. :rtype: string """ return '/' + self.URI_ROOT_PATH + '/' + self.encode('token') def uri(self, domain): """Create an URI to the provisioned resource. Forms an URI to the HTTPS server provisioned resource (containing :attr:`~SimpleHTTP.token`). :param unicode domain: Domain name being verified. :rtype: string """ return "http://" + domain + self.path def validation(self, account_key, **unused_kwargs): """Generate validation. :param JWK account_key: :rtype: unicode """ return self.key_authorization(account_key) @ChallengeResponse.register class TLSSNI01Response(KeyAuthorizationChallengeResponse): """ACME tls-sni-01 challenge response.""" typ = "tls-sni-01" DOMAIN_SUFFIX = b".acme.invalid" """Domain name suffix.""" PORT = 443 """Verification port as defined by the protocol. You can override it (e.g. for testing) by passing ``port`` to `simple_verify`. """ @property def z(self): # pylint: disable=invalid-name """``z`` value used for verification. :rtype bytes: """ return hashlib.sha256( self.key_authorization.encode("utf-8")).hexdigest().lower().encode() @property def z_domain(self): """Domain name used for verification, generated from `z`. :rtype bytes: """ return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX def gen_cert(self, key=None, bits=2048): """Generate tls-sni-01 certificate. :param OpenSSL.crypto.PKey key: Optional private key used in certificate generation. If not provided (``None``), then fresh key will be generated. :param int bits: Number of bits for newly generated key. :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` """ if key is None: key = OpenSSL.crypto.PKey() key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) return crypto_util.gen_ss_cert(key, [ # z_domain is too big to fit into CN, hence first dummy domain 'dummy', self.z_domain.decode()], force_san=True), key def probe_cert(self, domain, **kwargs): """Probe tls-sni-01 challenge certificate. :param unicode domain: """ # TODO: domain is not necessary if host is provided if "host" not in kwargs: host = socket.gethostbyname(domain) logger.debug('%s resolved to %s', domain, host) kwargs["host"] = host kwargs.setdefault("port", self.PORT) kwargs["name"] = self.z_domain # TODO: try different methods? # pylint: disable=protected-access return crypto_util.probe_sni(**kwargs) def verify_cert(self, cert): """Verify tls-sni-01 challenge certificate. :param OpensSSL.crypto.X509 cert: Challenge certificate. :returns: Whether the certificate was successfully verified. :rtype: bool """ # pylint: disable=protected-access sans = crypto_util._pyopenssl_cert_or_req_san(cert) logger.debug('Certificate %s. SANs: %s', cert.digest('sha256'), sans) return self.z_domain.decode() in sans def simple_verify(self, chall, domain, account_public_key, cert=None, **kwargs): """Simple verify. Verify ``validation`` using ``account_public_key``, optionally probe tls-sni-01 certificate and check using `verify_cert`. :param .challenges.TLSSNI01 chall: Corresponding challenge. :param str domain: Domain name being validated. :param JWK account_public_key: :param OpenSSL.crypto.X509 cert: Optional certificate. If not provided (``None``) certificate will be retrieved using `probe_cert`. :param int port: Port used to probe the certificate. :returns: ``True`` iff client's control of the domain has been verified. :rtype: bool """ if not self.verify(chall, account_public_key): logger.debug("Verification of key authorization in response failed") return False if cert is None: try: cert = self.probe_cert(domain=domain, **kwargs) except errors.Error as error: logger.debug(str(error), exc_info=True) return False return self.verify_cert(cert) @Challenge.register # pylint: disable=too-many-ancestors class TLSSNI01(KeyAuthorizationChallenge): """ACME tls-sni-01 challenge.""" response_cls = TLSSNI01Response typ = response_cls.typ # boulder#962, ietf-wg-acme#22 #n = jose.Field("n", encoder=int, decoder=int) def __init__(self, *args, **kwargs): warnings.warn("TLS-SNI-01 is deprecated, and will stop working soon.", DeprecationWarning, stacklevel=2) super(TLSSNI01, self).__init__(*args, **kwargs) def validation(self, account_key, **kwargs): """Generate validation. :param JWK account_key: :param OpenSSL.crypto.PKey cert_key: Optional private key used in certificate generation. If not provided (``None``), then fresh key will be generated. :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` """ return self.response(account_key).gen_cert(key=kwargs.get('cert_key')) @ChallengeResponse.register class TLSALPN01Response(KeyAuthorizationChallengeResponse): """ACME TLS-ALPN-01 challenge response. This class only allows initiating a TLS-ALPN-01 challenge returned from the CA. Full support for responding to TLS-ALPN-01 challenges by generating and serving the expected response certificate is not currently provided. """ typ = "tls-alpn-01" @Challenge.register # pylint: disable=too-many-ancestors class TLSALPN01(KeyAuthorizationChallenge): """ACME tls-alpn-01 challenge. This class simply allows parsing the TLS-ALPN-01 challenge returned from the CA. Full TLS-ALPN-01 support is not currently provided. """ typ = "tls-alpn-01" response_cls = TLSALPN01Response def validation(self, account_key, **kwargs): """Generate validation for the challenge.""" raise NotImplementedError() @Challenge.register # pylint: disable=too-many-ancestors class DNS(_TokenChallenge): """ACME "dns" challenge.""" typ = "dns" LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" def gen_validation(self, account_key, alg=jose.RS256, **kwargs): """Generate validation. :param .JWK account_key: Private account key. :param .JWA alg: :returns: This challenge wrapped in `.JWS` :rtype: .JWS """ return jose.JWS.sign( payload=self.json_dumps(sort_keys=True).encode('utf-8'), key=account_key, alg=alg, **kwargs) def check_validation(self, validation, account_public_key): """Check validation. :param JWS validation: :param JWK account_public_key: :rtype: bool """ if not validation.verify(key=account_public_key): return False try: return self == self.json_loads( validation.payload.decode('utf-8')) except jose.DeserializationError as error: logger.debug("Checking validation for DNS failed: %s", error) return False def gen_response(self, account_key, **kwargs): """Generate response. :param .JWK account_key: Private account key. :param .JWA alg: :rtype: DNSResponse """ return DNSResponse(validation=self.gen_validation( account_key, **kwargs)) def validation_domain_name(self, name): """Domain name for TXT validation record. :param unicode name: Domain name being validated. """ return "{0}.{1}".format(self.LABEL, name) @ChallengeResponse.register class DNSResponse(ChallengeResponse): """ACME "dns" challenge response. :param JWS validation: """ typ = "dns" validation = jose.Field("validation", decoder=jose.JWS.from_json) def check_validation(self, chall, account_public_key): """Check validation. :param challenges.DNS chall: :param JWK account_public_key: :rtype: bool """ return chall.check_validation(self.validation, account_public_key) acme-0.31.0/acme/client_test.py0000644000076600000240000015670013427120435016217 0ustar bmwstaff00000000000000"""Tests for acme.client.""" # pylint: disable=too-many-lines import copy import datetime import json import unittest from six.moves import http_client # pylint: disable=import-error import josepy as jose import mock import OpenSSL import requests from acme import challenges from acme import errors from acme import jws as acme_jws from acme import messages from acme import messages_test from acme import test_util from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module CERT_DER = test_util.load_vector('cert.der') CERT_SAN_PEM = test_util.load_vector('cert-san.pem') CSR_SAN_PEM = test_util.load_vector('csr-san.pem') KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) KEY2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) DIRECTORY_V1 = messages.Directory({ messages.NewRegistration: 'https://www.letsencrypt-demo.org/acme/new-reg', messages.Revocation: 'https://www.letsencrypt-demo.org/acme/revoke-cert', messages.NewAuthorization: 'https://www.letsencrypt-demo.org/acme/new-authz', messages.CertificateRequest: 'https://www.letsencrypt-demo.org/acme/new-cert', }) DIRECTORY_V2 = messages.Directory({ 'newAccount': 'https://www.letsencrypt-demo.org/acme/new-account', 'newNonce': 'https://www.letsencrypt-demo.org/acme/new-nonce', 'newOrder': 'https://www.letsencrypt-demo.org/acme/new-order', 'revokeCert': 'https://www.letsencrypt-demo.org/acme/revoke-cert', }) class ClientTestBase(unittest.TestCase): """Base for tests in acme.client.""" def setUp(self): self.response = mock.MagicMock( ok=True, status_code=http_client.OK, headers={}, links={}) self.net = mock.MagicMock() self.net.post.return_value = self.response self.net.get.return_value = self.response self.identifier = messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='example.com') # Registration self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') reg = messages.Registration( contact=self.contact, key=KEY.public_key()) the_arg = dict(reg) # type: Dict self.new_reg = messages.NewRegistration(**the_arg) # pylint: disable=star-args self.regr = messages.RegistrationResource( body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1') # Authorization authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' challb = messages.ChallengeBody( uri=(authzr_uri + '/1'), status=messages.STATUS_VALID, chall=challenges.DNS(token=jose.b64decode( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'))) self.challr = messages.ChallengeResource( body=challb, authzr_uri=authzr_uri) self.authz = messages.Authorization( identifier=messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='example.com'), challenges=(challb,), combinations=None) self.authzr = messages.AuthorizationResource( body=self.authz, uri=authzr_uri) # Reason code for revocation self.rsn = 1 class BackwardsCompatibleClientV2Test(ClientTestBase): """Tests for acme.client.BackwardsCompatibleClientV2.""" def setUp(self): super(BackwardsCompatibleClientV2Test, self).setUp() # contains a loaded cert self.certr = messages.CertificateResource( body=messages_test.CERT) loaded = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, CERT_SAN_PEM) wrapped = jose.ComparableX509(loaded) self.chain = [wrapped, wrapped] self.cert_pem = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, messages_test.CERT.wrapped).decode() single_chain = OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_PEM, loaded).decode() self.chain_pem = single_chain + single_chain self.fullchain_pem = self.cert_pem + self.chain_pem self.orderr = messages.OrderResource( csr_pem=CSR_SAN_PEM) def _init(self): uri = 'http://www.letsencrypt-demo.org/directory' from acme.client import BackwardsCompatibleClientV2 return BackwardsCompatibleClientV2(net=self.net, key=KEY, server=uri) def test_init_downloads_directory(self): uri = 'http://www.letsencrypt-demo.org/directory' from acme.client import BackwardsCompatibleClientV2 BackwardsCompatibleClientV2(net=self.net, key=KEY, server=uri) self.net.get.assert_called_once_with(uri) def test_init_acme_version(self): self.response.json.return_value = DIRECTORY_V1.to_json() client = self._init() self.assertEqual(client.acme_version, 1) self.response.json.return_value = DIRECTORY_V2.to_json() client = self._init() self.assertEqual(client.acme_version, 2) def test_query_registration_client_v2(self): self.response.json.return_value = DIRECTORY_V2.to_json() client = self._init() self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, client.query_registration(self.regr)) def test_forwarding(self): self.response.json.return_value = DIRECTORY_V1.to_json() client = self._init() self.assertEqual(client.directory, client.client.directory) self.assertEqual(client.key, KEY) self.assertEqual(client.deactivate_registration, client.client.deactivate_registration) self.assertRaises(AttributeError, client.__getattr__, 'nonexistent') self.assertRaises(AttributeError, client.__getattr__, 'new_account_and_tos') self.assertRaises(AttributeError, client.__getattr__, 'new_account') def test_new_account_and_tos(self): # v2 no tos self.response.json.return_value = DIRECTORY_V2.to_json() with mock.patch('acme.client.ClientV2') as mock_client: client = self._init() client.new_account_and_tos(self.new_reg) mock_client().new_account.assert_called_with(self.new_reg) # v2 tos good with mock.patch('acme.client.ClientV2') as mock_client: mock_client().directory.meta.__contains__.return_value = True client = self._init() client.new_account_and_tos(self.new_reg, lambda x: True) mock_client().new_account.assert_called_with( self.new_reg.update(terms_of_service_agreed=True)) # v2 tos bad with mock.patch('acme.client.ClientV2') as mock_client: mock_client().directory.meta.__contains__.return_value = True client = self._init() def _tos_cb(tos): raise errors.Error self.assertRaises(errors.Error, client.new_account_and_tos, self.new_reg, _tos_cb) mock_client().new_account.assert_not_called() # v1 yes tos self.response.json.return_value = DIRECTORY_V1.to_json() with mock.patch('acme.client.Client') as mock_client: regr = mock.MagicMock(terms_of_service="TOS") mock_client().register.return_value = regr client = self._init() client.new_account_and_tos(self.new_reg) mock_client().register.assert_called_once_with(self.new_reg) mock_client().agree_to_tos.assert_called_once_with(regr) # v1 no tos with mock.patch('acme.client.Client') as mock_client: regr = mock.MagicMock(terms_of_service=None) mock_client().register.return_value = regr client = self._init() client.new_account_and_tos(self.new_reg) mock_client().register.assert_called_once_with(self.new_reg) mock_client().agree_to_tos.assert_not_called() @mock.patch('OpenSSL.crypto.load_certificate_request') @mock.patch('acme.crypto_util._pyopenssl_cert_or_req_all_names') def test_new_order_v1(self, mock__pyopenssl_cert_or_req_all_names, unused_mock_load_certificate_request): self.response.json.return_value = DIRECTORY_V1.to_json() mock__pyopenssl_cert_or_req_all_names.return_value = ['example.com', 'www.example.com'] mock_csr_pem = mock.MagicMock() with mock.patch('acme.client.Client') as mock_client: mock_client().request_domain_challenges.return_value = mock.sentinel.auth client = self._init() orderr = client.new_order(mock_csr_pem) self.assertEqual(orderr.authorizations, [mock.sentinel.auth, mock.sentinel.auth]) def test_new_order_v2(self): self.response.json.return_value = DIRECTORY_V2.to_json() mock_csr_pem = mock.MagicMock() with mock.patch('acme.client.ClientV2') as mock_client: client = self._init() client.new_order(mock_csr_pem) mock_client().new_order.assert_called_once_with(mock_csr_pem) @mock.patch('acme.client.Client') def test_finalize_order_v1_success(self, mock_client): self.response.json.return_value = DIRECTORY_V1.to_json() mock_client().request_issuance.return_value = self.certr mock_client().fetch_chain.return_value = self.chain deadline = datetime.datetime(9999, 9, 9) client = self._init() result = client.finalize_order(self.orderr, deadline) self.assertEqual(result.fullchain_pem, self.fullchain_pem) mock_client().fetch_chain.assert_called_once_with(self.certr) @mock.patch('acme.client.Client') def test_finalize_order_v1_fetch_chain_error(self, mock_client): self.response.json.return_value = DIRECTORY_V1.to_json() mock_client().request_issuance.return_value = self.certr mock_client().fetch_chain.return_value = self.chain mock_client().fetch_chain.side_effect = [errors.Error, self.chain] deadline = datetime.datetime(9999, 9, 9) client = self._init() result = client.finalize_order(self.orderr, deadline) self.assertEqual(result.fullchain_pem, self.fullchain_pem) self.assertEqual(mock_client().fetch_chain.call_count, 2) @mock.patch('acme.client.Client') def test_finalize_order_v1_timeout(self, mock_client): self.response.json.return_value = DIRECTORY_V1.to_json() mock_client().request_issuance.return_value = self.certr deadline = deadline = datetime.datetime.now() - datetime.timedelta(seconds=60) client = self._init() self.assertRaises(errors.TimeoutError, client.finalize_order, self.orderr, deadline) def test_finalize_order_v2(self): self.response.json.return_value = DIRECTORY_V2.to_json() mock_orderr = mock.MagicMock() mock_deadline = mock.MagicMock() with mock.patch('acme.client.ClientV2') as mock_client: client = self._init() client.finalize_order(mock_orderr, mock_deadline) mock_client().finalize_order.assert_called_once_with(mock_orderr, mock_deadline) def test_revoke(self): self.response.json.return_value = DIRECTORY_V1.to_json() with mock.patch('acme.client.Client') as mock_client: client = self._init() client.revoke(messages_test.CERT, self.rsn) mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn) self.response.json.return_value = DIRECTORY_V2.to_json() with mock.patch('acme.client.ClientV2') as mock_client: client = self._init() client.revoke(messages_test.CERT, self.rsn) mock_client().revoke.assert_called_once_with(messages_test.CERT, self.rsn) def test_update_registration(self): self.response.json.return_value = DIRECTORY_V1.to_json() with mock.patch('acme.client.Client') as mock_client: client = self._init() client.update_registration(mock.sentinel.regr, None) mock_client().update_registration.assert_called_once_with(mock.sentinel.regr, None) # newNonce present means it will pick acme_version 2 def test_external_account_required_true(self): self.response.json.return_value = messages.Directory({ 'newNonce': 'http://letsencrypt-test.com/acme/new-nonce', 'meta': messages.Directory.Meta(external_account_required=True), }).to_json() client = self._init() self.assertTrue(client.external_account_required()) # newNonce present means it will pick acme_version 2 def test_external_account_required_false(self): self.response.json.return_value = messages.Directory({ 'newNonce': 'http://letsencrypt-test.com/acme/new-nonce', 'meta': messages.Directory.Meta(external_account_required=False), }).to_json() client = self._init() self.assertFalse(client.external_account_required()) def test_external_account_required_false_v1(self): self.response.json.return_value = messages.Directory({ 'meta': messages.Directory.Meta(external_account_required=False), }).to_json() client = self._init() self.assertFalse(client.external_account_required()) class ClientTest(ClientTestBase): """Tests for acme.client.Client.""" # pylint: disable=too-many-instance-attributes,too-many-public-methods def setUp(self): super(ClientTest, self).setUp() self.directory = DIRECTORY_V1 # Registration self.regr = self.regr.update( terms_of_service='https://www.letsencrypt-demo.org/tos') # Request issuance self.certr = messages.CertificateResource( body=messages_test.CERT, authzrs=(self.authzr,), uri='https://www.letsencrypt-demo.org/acme/cert/1', cert_chain_uri='https://www.letsencrypt-demo.org/ca') from acme.client import Client self.client = Client( directory=self.directory, key=KEY, alg=jose.RS256, net=self.net) def test_init_downloads_directory(self): uri = 'http://www.letsencrypt-demo.org/directory' from acme.client import Client self.client = Client( directory=uri, key=KEY, alg=jose.RS256, net=self.net) self.net.get.assert_called_once_with(uri) @mock.patch('acme.client.ClientNetwork') def test_init_without_net(self, mock_net): mock_net.return_value = mock.sentinel.net alg = jose.RS256 from acme.client import Client self.client = Client( directory=self.directory, key=KEY, alg=alg) mock_net.called_once_with(KEY, alg=alg, verify_ssl=True) self.assertEqual(self.client.net, mock.sentinel.net) def test_register(self): # "Instance of 'Field' has no to_json/update member" bug: # pylint: disable=no-member self.response.status_code = http_client.CREATED self.response.json.return_value = self.regr.body.to_json() self.response.headers['Location'] = self.regr.uri self.response.links.update({ 'terms-of-service': {'url': self.regr.terms_of_service}, }) self.assertEqual(self.regr, self.client.register(self.new_reg)) # TODO: test POST call arguments def test_update_registration(self): # "Instance of 'Field' has no to_json/update member" bug: # pylint: disable=no-member self.response.headers['Location'] = self.regr.uri self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, self.client.update_registration(self.regr)) # TODO: test POST call arguments # TODO: split here and separate test self.response.json.return_value = self.regr.body.update( contact=()).to_json() def test_deactivate_account(self): self.response.headers['Location'] = self.regr.uri self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, self.client.deactivate_registration(self.regr)) def test_query_registration(self): self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, self.client.query_registration(self.regr)) def test_agree_to_tos(self): self.client.update_registration = mock.Mock() self.client.agree_to_tos(self.regr) regr = self.client.update_registration.call_args[0][0] self.assertEqual(self.regr.terms_of_service, regr.body.agreement) def _prepare_response_for_request_challenges(self): self.response.status_code = http_client.CREATED self.response.headers['Location'] = self.authzr.uri self.response.json.return_value = self.authz.to_json() def test_request_challenges(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier) self.net.post.assert_called_once_with( self.directory.new_authz, messages.NewAuthorization(identifier=self.identifier), acme_version=1) def test_request_challenges_deprecated_arg(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier, new_authzr_uri="hi") self.net.post.assert_called_once_with( self.directory.new_authz, messages.NewAuthorization(identifier=self.identifier), acme_version=1) def test_request_challenges_custom_uri(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier) self.net.post.assert_called_once_with( 'https://www.letsencrypt-demo.org/acme/new-authz', mock.ANY, acme_version=1) def test_request_challenges_unexpected_update(self): self._prepare_response_for_request_challenges() self.response.json.return_value = self.authz.update( identifier=self.identifier.update(value='foo')).to_json() self.assertRaises( errors.UnexpectedUpdate, self.client.request_challenges, self.identifier) def test_request_challenges_wildcard(self): wildcard_identifier = messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='*.example.org') self.assertRaises( errors.WildcardUnsupportedError, self.client.request_challenges, wildcard_identifier) def test_request_domain_challenges(self): self.client.request_challenges = mock.MagicMock() self.assertEqual( self.client.request_challenges(self.identifier), self.client.request_domain_challenges('example.com')) def test_answer_challenge(self): self.response.links['up'] = {'url': self.challr.authzr_uri} self.response.json.return_value = self.challr.body.to_json() chall_response = challenges.DNSResponse(validation=None) self.client.answer_challenge(self.challr.body, chall_response) # TODO: split here and separate test self.assertRaises(errors.UnexpectedUpdate, self.client.answer_challenge, self.challr.body.update(uri='foo'), chall_response) def test_answer_challenge_missing_next(self): self.assertRaises( errors.ClientError, self.client.answer_challenge, self.challr.body, challenges.DNSResponse(validation=None)) def test_retry_after_date(self): self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' self.assertEqual( datetime.datetime(1999, 12, 31, 23, 59, 59), self.client.retry_after(response=self.response, default=10)) @mock.patch('acme.client.datetime') def test_retry_after_invalid(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta self.response.headers['Retry-After'] = 'foooo' self.assertEqual( datetime.datetime(2015, 3, 27, 0, 0, 10), self.client.retry_after(response=self.response, default=10)) @mock.patch('acme.client.datetime') def test_retry_after_overflow(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta dt_mock.datetime.side_effect = datetime.datetime self.response.headers['Retry-After'] = "Tue, 116 Feb 2016 11:50:00 MST" self.assertEqual( datetime.datetime(2015, 3, 27, 0, 0, 10), self.client.retry_after(response=self.response, default=10)) @mock.patch('acme.client.datetime') def test_retry_after_seconds(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta self.response.headers['Retry-After'] = '50' self.assertEqual( datetime.datetime(2015, 3, 27, 0, 0, 50), self.client.retry_after(response=self.response, default=10)) @mock.patch('acme.client.datetime') def test_retry_after_missing(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta self.assertEqual( datetime.datetime(2015, 3, 27, 0, 0, 10), self.client.retry_after(response=self.response, default=10)) def test_poll(self): self.response.json.return_value = self.authzr.body.to_json() self.assertEqual((self.authzr, self.response), self.client.poll(self.authzr)) # TODO: split here and separate test self.response.json.return_value = self.authz.update( identifier=self.identifier.update(value='foo')).to_json() self.assertRaises( errors.UnexpectedUpdate, self.client.poll, self.authzr) def test_request_issuance(self): self.response.content = CERT_DER self.response.headers['Location'] = self.certr.uri self.response.links['up'] = {'url': self.certr.cert_chain_uri} self.assertEqual(self.certr, self.client.request_issuance( messages_test.CSR, (self.authzr,))) # TODO: check POST args def test_request_issuance_missing_up(self): self.response.content = CERT_DER self.response.headers['Location'] = self.certr.uri self.assertEqual( self.certr.update(cert_chain_uri=None), self.client.request_issuance(messages_test.CSR, (self.authzr,))) def test_request_issuance_missing_location(self): self.assertRaises( errors.ClientError, self.client.request_issuance, messages_test.CSR, (self.authzr,)) @mock.patch('acme.client.datetime') @mock.patch('acme.client.time') def test_poll_and_request_issuance(self, time_mock, dt_mock): # clock.dt | pylint: disable=no-member clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) def sleep(seconds): """increment clock""" clock.dt += datetime.timedelta(seconds=seconds) time_mock.sleep.side_effect = sleep def now(): """return current clock value""" return clock.dt dt_mock.datetime.now.side_effect = now dt_mock.timedelta = datetime.timedelta def poll(authzr): # pylint: disable=missing-docstring # record poll start time based on the current clock value authzr.times.append(clock.dt) # suppose it takes 2 seconds for server to produce the # result, increment clock clock.dt += datetime.timedelta(seconds=2) if len(authzr.retries) == 1: # no more retries done = mock.MagicMock(uri=authzr.uri, times=authzr.times) done.body.status = authzr.retries[0] return done, [] # response (2nd result tuple element) is reduced to only # Retry-After header contents represented as integer # seconds; authzr.retries is a list of Retry-After # headers, head(retries) is peeled of as a current # Retry-After header, and tail(retries) is persisted for # later poll() calls return (mock.MagicMock(retries=authzr.retries[1:], uri=authzr.uri + '.', times=authzr.times), authzr.retries[0]) self.client.poll = mock.MagicMock(side_effect=poll) mintime = 7 def retry_after(response, default): # pylint: disable=missing-docstring # check that poll_and_request_issuance correctly passes mintime self.assertEqual(default, mintime) return clock.dt + datetime.timedelta(seconds=response) self.client.retry_after = mock.MagicMock(side_effect=retry_after) def request_issuance(csr, authzrs): # pylint: disable=missing-docstring return csr, authzrs self.client.request_issuance = mock.MagicMock( side_effect=request_issuance) csr = mock.MagicMock() authzrs = ( mock.MagicMock(uri='a', times=[], retries=( 8, 20, 30, messages.STATUS_VALID)), mock.MagicMock(uri='b', times=[], retries=( 5, messages.STATUS_VALID)), ) cert, updated_authzrs = self.client.poll_and_request_issuance( csr, authzrs, mintime=mintime, # make sure that max_attempts is per-authorization, rather # than global max_attempts=max(len(authzrs[0].retries), len(authzrs[1].retries))) self.assertTrue(cert[0] is csr) self.assertTrue(cert[1] is updated_authzrs) self.assertEqual(updated_authzrs[0].uri, 'a...') self.assertEqual(updated_authzrs[1].uri, 'b.') self.assertEqual(updated_authzrs[0].times, [ datetime.datetime(2015, 3, 27), # a is scheduled for 10, but b is polling [9..11), so it # will be picked up as soon as b is finished, without # additional sleeping datetime.datetime(2015, 3, 27, 0, 0, 11), datetime.datetime(2015, 3, 27, 0, 0, 33), datetime.datetime(2015, 3, 27, 0, 1, 5), ]) self.assertEqual(updated_authzrs[1].times, [ datetime.datetime(2015, 3, 27, 0, 0, 2), datetime.datetime(2015, 3, 27, 0, 0, 9), ]) self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) # CA sets invalid | TODO: move to a separate test invalid_authzr = mock.MagicMock( times=[], retries=[messages.STATUS_INVALID]) self.assertRaises( errors.PollError, self.client.poll_and_request_issuance, csr, authzrs=(invalid_authzr,), mintime=mintime) # exceeded max_attempts | TODO: move to a separate test self.assertRaises( errors.PollError, self.client.poll_and_request_issuance, csr, authzrs, mintime=mintime, max_attempts=2) def test_check_cert(self): self.response.headers['Location'] = self.certr.uri self.response.content = CERT_DER self.assertEqual(self.certr.update(body=messages_test.CERT), self.client.check_cert(self.certr)) # TODO: split here and separate test self.response.headers['Location'] = 'foo' self.assertRaises( errors.UnexpectedUpdate, self.client.check_cert, self.certr) def test_check_cert_missing_location(self): self.response.content = CERT_DER self.assertRaises( errors.ClientError, self.client.check_cert, self.certr) def test_refresh(self): self.client.check_cert = mock.MagicMock() self.assertEqual( self.client.check_cert(self.certr), self.client.refresh(self.certr)) def test_fetch_chain_no_up_link(self): self.assertEqual([], self.client.fetch_chain(self.certr.update( cert_chain_uri=None))) def test_fetch_chain_single(self): # pylint: disable=protected-access self.client._get_cert = mock.MagicMock() self.client._get_cert.return_value = ( mock.MagicMock(links={}), "certificate") self.assertEqual([self.client._get_cert(self.certr.cert_chain_uri)[1]], self.client.fetch_chain(self.certr)) def test_fetch_chain_max(self): # pylint: disable=protected-access up_response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) noup_response = mock.MagicMock(links={}) self.client._get_cert = mock.MagicMock() self.client._get_cert.side_effect = [ (up_response, "cert")] * 9 + [(noup_response, "last_cert")] chain = self.client.fetch_chain(self.certr, max_length=10) self.assertEqual(chain, ["cert"] * 9 + ["last_cert"]) def test_fetch_chain_too_many(self): # recursive # pylint: disable=protected-access response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) self.client._get_cert = mock.MagicMock() self.client._get_cert.return_value = (response, "certificate") self.assertRaises(errors.Error, self.client.fetch_chain, self.certr) def test_revoke(self): self.client.revoke(self.certr.body, self.rsn) self.net.post.assert_called_once_with( self.directory[messages.Revocation], mock.ANY, acme_version=1) def test_revocation_payload(self): obj = messages.Revocation(certificate=self.certr.body, reason=self.rsn) self.assertTrue('reason' in obj.to_partial_json().keys()) self.assertEqual(self.rsn, obj.to_partial_json()['reason']) def test_revoke_bad_status_raises_error(self): self.response.status_code = http_client.METHOD_NOT_ALLOWED self.assertRaises( errors.ClientError, self.client.revoke, self.certr, self.rsn) class ClientV2Test(ClientTestBase): """Tests for acme.client.ClientV2.""" def setUp(self): super(ClientV2Test, self).setUp() self.directory = DIRECTORY_V2 from acme.client import ClientV2 self.client = ClientV2(self.directory, self.net) self.new_reg = self.new_reg.update(terms_of_service_agreed=True) self.authzr_uri2 = 'https://www.letsencrypt-demo.org/acme/authz/2' self.authz2 = self.authz.update(identifier=messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='www.example.com'), status=messages.STATUS_PENDING) self.authzr2 = messages.AuthorizationResource( body=self.authz2, uri=self.authzr_uri2) self.order = messages.Order( identifiers=(self.authz.identifier, self.authz2.identifier), status=messages.STATUS_PENDING, authorizations=(self.authzr.uri, self.authzr_uri2), finalize='https://www.letsencrypt-demo.org/acme/acct/1/order/1/finalize') self.orderr = messages.OrderResource( body=self.order, uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1', authorizations=[self.authzr, self.authzr2], csr_pem=CSR_SAN_PEM) def test_new_account(self): self.response.status_code = http_client.CREATED self.response.json.return_value = self.regr.body.to_json() self.response.headers['Location'] = self.regr.uri self.assertEqual(self.regr, self.client.new_account(self.new_reg)) def test_new_account_conflict(self): self.response.status_code = http_client.OK self.response.headers['Location'] = self.regr.uri self.assertRaises(errors.ConflictError, self.client.new_account, self.new_reg) def test_new_order(self): order_response = copy.deepcopy(self.response) order_response.status_code = http_client.CREATED order_response.json.return_value = self.order.to_json() order_response.headers['Location'] = self.orderr.uri self.net.post.return_value = order_response authz_response = copy.deepcopy(self.response) authz_response.json.return_value = self.authz.to_json() authz_response.headers['Location'] = self.authzr.uri authz_response2 = self.response authz_response2.json.return_value = self.authz2.to_json() authz_response2.headers['Location'] = self.authzr2.uri with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get: mock_post_as_get.side_effect = (authz_response, authz_response2) self.assertEqual(self.client.new_order(CSR_SAN_PEM), self.orderr) @mock.patch('acme.client.datetime') def test_poll_and_finalize(self, mock_datetime): mock_datetime.datetime.now.return_value = datetime.datetime(2018, 2, 15) mock_datetime.timedelta = datetime.timedelta expected_deadline = mock_datetime.datetime.now() + datetime.timedelta(seconds=90) self.client.poll_authorizations = mock.Mock(return_value=self.orderr) self.client.finalize_order = mock.Mock(return_value=self.orderr) self.assertEqual(self.client.poll_and_finalize(self.orderr), self.orderr) self.client.poll_authorizations.assert_called_once_with(self.orderr, expected_deadline) self.client.finalize_order.assert_called_once_with(self.orderr, expected_deadline) @mock.patch('acme.client.datetime') def test_poll_authorizations_timeout(self, mock_datetime): now_side_effect = [datetime.datetime(2018, 2, 15), datetime.datetime(2018, 2, 16), datetime.datetime(2018, 2, 17)] mock_datetime.datetime.now.side_effect = now_side_effect self.response.json.side_effect = [ self.authz.to_json(), self.authz2.to_json(), self.authz2.to_json()] self.assertRaises( errors.TimeoutError, self.client.poll_authorizations, self.orderr, now_side_effect[1]) def test_poll_authorizations_failure(self): deadline = datetime.datetime(9999, 9, 9) challb = self.challr.body.update(status=messages.STATUS_INVALID, error=messages.Error.with_code('unauthorized')) authz = self.authz.update(status=messages.STATUS_INVALID, challenges=(challb,)) self.response.json.return_value = authz.to_json() self.assertRaises( errors.ValidationError, self.client.poll_authorizations, self.orderr, deadline) def test_poll_authorizations_success(self): deadline = datetime.datetime(9999, 9, 9) updated_authz2 = self.authz2.update(status=messages.STATUS_VALID) updated_authzr2 = messages.AuthorizationResource( body=updated_authz2, uri=self.authzr_uri2) updated_orderr = self.orderr.update(authorizations=[self.authzr, updated_authzr2]) self.response.json.side_effect = ( self.authz.to_json(), self.authz2.to_json(), updated_authz2.to_json()) self.assertEqual(self.client.poll_authorizations(self.orderr, deadline), updated_orderr) def test_finalize_order_success(self): updated_order = self.order.update( certificate='https://www.letsencrypt-demo.org/acme/cert/') updated_orderr = self.orderr.update(body=updated_order, fullchain_pem=CERT_SAN_PEM) self.response.json.return_value = updated_order.to_json() self.response.text = CERT_SAN_PEM deadline = datetime.datetime(9999, 9, 9) self.assertEqual(self.client.finalize_order(self.orderr, deadline), updated_orderr) def test_finalize_order_error(self): updated_order = self.order.update(error=messages.Error.with_code('unauthorized')) self.response.json.return_value = updated_order.to_json() deadline = datetime.datetime(9999, 9, 9) self.assertRaises(errors.IssuanceError, self.client.finalize_order, self.orderr, deadline) def test_finalize_order_timeout(self): deadline = datetime.datetime.now() - datetime.timedelta(seconds=60) self.assertRaises(errors.TimeoutError, self.client.finalize_order, self.orderr, deadline) def test_revoke(self): self.client.revoke(messages_test.CERT, self.rsn) self.net.post.assert_called_once_with( self.directory["revokeCert"], mock.ANY, acme_version=2, new_nonce_url=DIRECTORY_V2['newNonce']) def test_update_registration(self): # "Instance of 'Field' has no to_json/update member" bug: # pylint: disable=no-member self.response.headers['Location'] = self.regr.uri self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, self.client.update_registration(self.regr)) self.assertNotEqual(self.client.net.account, None) self.assertEqual(self.client.net.post.call_count, 2) self.assertTrue(DIRECTORY_V2.newAccount in self.net.post.call_args_list[0][0]) self.response.json.return_value = self.regr.body.update( contact=()).to_json() def test_external_account_required_true(self): self.client.directory = messages.Directory({ 'meta': messages.Directory.Meta(external_account_required=True) }) self.assertTrue(self.client.external_account_required()) def test_external_account_required_false(self): self.client.directory = messages.Directory({ 'meta': messages.Directory.Meta(external_account_required=False) }) self.assertFalse(self.client.external_account_required()) def test_external_account_required_default(self): self.assertFalse(self.client.external_account_required()) def test_post_as_get(self): with mock.patch('acme.client.ClientV2._authzr_from_response') as mock_client: mock_client.return_value = self.authzr2 self.client.poll(self.authzr2) # pylint: disable=protected-access self.client.net.post.assert_called_once_with( self.authzr2.uri, None, acme_version=2, new_nonce_url='https://www.letsencrypt-demo.org/acme/new-nonce') self.client.net.get.assert_not_called() class FakeError(messages.Error): # pylint: disable=too-many-ancestors """Fake error to reproduce a malformed request ACME error""" def __init__(self): # pylint: disable=super-init-not-called pass @property def code(self): return 'malformed' self.client.net.post.side_effect = FakeError() self.client.poll(self.authzr2) # pylint: disable=protected-access self.client.net.get.assert_called_once_with(self.authzr2.uri) class MockJSONDeSerializable(jose.JSONDeSerializable): # pylint: disable=missing-docstring def __init__(self, value): self.value = value def to_partial_json(self): return {'foo': self.value} @classmethod def from_json(cls, value): pass # pragma: no cover class ClientNetworkTest(unittest.TestCase): """Tests for acme.client.ClientNetwork.""" # pylint: disable=too-many-public-methods def setUp(self): self.verify_ssl = mock.MagicMock() self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) from acme.client import ClientNetwork self.net = ClientNetwork( key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl, user_agent='acme-python-test') self.response = mock.MagicMock(ok=True, status_code=http_client.OK) self.response.headers = {} self.response.links = {} def test_init(self): self.assertTrue(self.net.verify_ssl is self.verify_ssl) def test_wrap_in_jws(self): # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( MockJSONDeSerializable('foo'), nonce=b'Tg', url="url", acme_version=1) jws = acme_jws.JWS.json_loads(jws_dump) self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) self.assertEqual(jws.signature.combined.nonce, b'Tg') def test_wrap_in_jws_v2(self): self.net.account = {'uri': 'acct-uri'} # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( MockJSONDeSerializable('foo'), nonce=b'Tg', url="url", acme_version=2) jws = acme_jws.JWS.json_loads(jws_dump) self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) self.assertEqual(jws.signature.combined.nonce, b'Tg') self.assertEqual(jws.signature.combined.kid, u'acct-uri') self.assertEqual(jws.signature.combined.url, u'url') def test_check_response_not_ok_jobj_no_error(self): self.response.ok = False self.response.json.return_value = {} with mock.patch('acme.client.messages.Error.from_json') as from_json: from_json.side_effect = jose.DeserializationError # pylint: disable=protected-access self.assertRaises( errors.ClientError, self.net._check_response, self.response) def test_check_response_not_ok_jobj_error(self): self.response.ok = False self.response.json.return_value = messages.Error( detail='foo', typ='serverInternal', title='some title').to_json() # pylint: disable=protected-access self.assertRaises( messages.Error, self.net._check_response, self.response) def test_check_response_not_ok_no_jobj(self): self.response.ok = False self.response.json.side_effect = ValueError # pylint: disable=protected-access self.assertRaises( errors.ClientError, self.net._check_response, self.response) def test_check_response_ok_no_jobj_ct_required(self): self.response.json.side_effect = ValueError for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: self.response.headers['Content-Type'] = response_ct # pylint: disable=protected-access self.assertRaises( errors.ClientError, self.net._check_response, self.response, content_type=self.net.JSON_CONTENT_TYPE) def test_check_response_ok_no_jobj_no_ct(self): self.response.json.side_effect = ValueError for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: self.response.headers['Content-Type'] = response_ct # pylint: disable=protected-access,no-value-for-parameter self.assertEqual( self.response, self.net._check_response(self.response)) def test_check_response_conflict(self): self.response.ok = False self.response.status_code = 409 # pylint: disable=protected-access self.assertRaises(errors.ConflictError, self.net._check_response, self.response) def test_check_response_jobj(self): self.response.json.return_value = {} for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: self.response.headers['Content-Type'] = response_ct # pylint: disable=protected-access,no-value-for-parameter self.assertEqual( self.response, self.net._check_response(self.response)) def test_send_request(self): self.net.session = mock.MagicMock() self.net.session.request.return_value = self.response # pylint: disable=protected-access self.assertEqual(self.response, self.net._send_request( 'HEAD', 'http://example.com/', 'foo', bar='baz')) self.net.session.request.assert_called_once_with( 'HEAD', 'http://example.com/', 'foo', headers=mock.ANY, verify=mock.ANY, timeout=mock.ANY, bar='baz') @mock.patch('acme.client.logger') def test_send_request_get_der(self, mock_logger): self.net.session = mock.MagicMock() self.net.session.request.return_value = mock.MagicMock( ok=True, status_code=http_client.OK, headers={"Content-Type": "application/pkix-cert"}, content=b"hi") # pylint: disable=protected-access self.net._send_request('HEAD', 'http://example.com/', 'foo', timeout=mock.ANY, bar='baz') mock_logger.debug.assert_called_with( 'Received response:\nHTTP %d\n%s\n\n%s', 200, 'Content-Type: application/pkix-cert', b'aGk=') def test_send_request_post(self): self.net.session = mock.MagicMock() self.net.session.request.return_value = self.response # pylint: disable=protected-access self.assertEqual(self.response, self.net._send_request( 'POST', 'http://example.com/', 'foo', data='qux', bar='baz')) self.net.session.request.assert_called_once_with( 'POST', 'http://example.com/', 'foo', headers=mock.ANY, verify=mock.ANY, timeout=mock.ANY, data='qux', bar='baz') def test_send_request_verify_ssl(self): # pylint: disable=protected-access for verify in True, False: self.net.session = mock.MagicMock() self.net.session.request.return_value = self.response self.net.verify_ssl = verify # pylint: disable=protected-access self.assertEqual( self.response, self.net._send_request('GET', 'http://example.com/')) self.net.session.request.assert_called_once_with( 'GET', 'http://example.com/', verify=verify, timeout=mock.ANY, headers=mock.ANY) def test_send_request_user_agent(self): self.net.session = mock.MagicMock() # pylint: disable=protected-access self.net._send_request('GET', 'http://example.com/', headers={'bar': 'baz'}) self.net.session.request.assert_called_once_with( 'GET', 'http://example.com/', verify=mock.ANY, timeout=mock.ANY, headers={'User-Agent': 'acme-python-test', 'bar': 'baz'}) self.net._send_request('GET', 'http://example.com/', headers={'User-Agent': 'foo2'}) self.net.session.request.assert_called_with( 'GET', 'http://example.com/', verify=mock.ANY, timeout=mock.ANY, headers={'User-Agent': 'foo2'}) def test_send_request_timeout(self): self.net.session = mock.MagicMock() # pylint: disable=protected-access self.net._send_request('GET', 'http://example.com/', headers={'bar': 'baz'}) self.net.session.request.assert_called_once_with( mock.ANY, mock.ANY, verify=mock.ANY, headers=mock.ANY, timeout=45) def test_del(self, close_exception=None): sess = mock.MagicMock() if close_exception is not None: sess.close.side_effect = close_exception self.net.session = sess del self.net sess.close.assert_called_once_with() def test_del_error(self): self.test_del(ReferenceError) @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): mock_requests.exceptions = requests.exceptions mock_requests.request.side_effect = requests.exceptions.RequestException # pylint: disable=protected-access self.assertRaises(requests.exceptions.RequestException, self.net._send_request, 'GET', 'uri') def test_urllib_error(self): # Using a connection error to test a properly formatted error message try: # pylint: disable=protected-access self.net._send_request('GET', "http://localhost:19123/nonexistent.txt") # Value Error Generated Exceptions except ValueError as y: self.assertEqual("Requesting localhost/nonexistent: " "Connection refused", str(y)) # Requests Library Exceptions except requests.exceptions.ConnectionError as z: #pragma: no cover self.assertTrue("'Connection aborted.'" in str(z) or "[WinError 10061]" in str(z)) class ClientNetworkWithMockedResponseTest(unittest.TestCase): """Tests for acme.client.ClientNetwork which mock out response.""" # pylint: disable=too-many-instance-attributes def setUp(self): from acme.client import ClientNetwork self.net = ClientNetwork(key=None, alg=None) self.response = mock.MagicMock(ok=True, status_code=http_client.OK) self.response.headers = {} self.response.links = {} self.response.checked = False self.acmev1_nonce_response = mock.MagicMock(ok=False, status_code=http_client.METHOD_NOT_ALLOWED) self.acmev1_nonce_response.headers = {} self.obj = mock.MagicMock() self.wrapped_obj = mock.MagicMock() self.content_type = mock.sentinel.content_type self.all_nonces = [ jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2'), jose.b64encode(b'Nonce3')] self.available_nonces = self.all_nonces[:] def send_request(*args, **kwargs): # pylint: disable=unused-argument,missing-docstring self.assertFalse("new_nonce_url" in kwargs) method = args[0] uri = args[1] if method == 'HEAD' and uri != "new_nonce_uri": response = self.acmev1_nonce_response else: response = self.response if self.available_nonces: response.headers = { self.net.REPLAY_NONCE_HEADER: self.available_nonces.pop().decode()} else: response.headers = {} return response # pylint: disable=protected-access self.net._send_request = self.send_request = mock.MagicMock( side_effect=send_request) self.net._check_response = self.check_response self.net._wrap_in_jws = mock.MagicMock(return_value=self.wrapped_obj) def check_response(self, response, content_type): # pylint: disable=missing-docstring self.assertEqual(self.response, response) self.assertEqual(self.content_type, content_type) self.assertTrue(self.response.ok) self.response.checked = True return self.response def test_head(self): self.assertEqual(self.acmev1_nonce_response, self.net.head( 'http://example.com/', 'foo', bar='baz')) self.send_request.assert_called_once_with( 'HEAD', 'http://example.com/', 'foo', bar='baz') def test_head_v2(self): self.assertEqual(self.response, self.net.head( 'new_nonce_uri', 'foo', bar='baz')) self.send_request.assert_called_once_with( 'HEAD', 'new_nonce_uri', 'foo', bar='baz') def test_get(self): self.assertEqual(self.response, self.net.get( 'http://example.com/', content_type=self.content_type, bar='baz')) self.assertTrue(self.response.checked) self.send_request.assert_called_once_with( 'GET', 'http://example.com/', bar='baz') def test_post_no_content_type(self): self.content_type = self.net.JOSE_CONTENT_TYPE self.assertEqual(self.response, self.net.post('uri', self.obj)) self.assertTrue(self.response.checked) def test_post(self): # pylint: disable=protected-access self.assertEqual(self.response, self.net.post( 'uri', self.obj, content_type=self.content_type)) self.assertTrue(self.response.checked) self.net._wrap_in_jws.assert_called_once_with( self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1) self.available_nonces = [] self.assertRaises(errors.MissingNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) self.net._wrap_in_jws.assert_called_with( self.obj, jose.b64decode(self.all_nonces.pop()), "uri", 1) def test_post_wrong_initial_nonce(self): # HEAD self.available_nonces = [b'f', jose.b64encode(b'good')] self.assertRaises(errors.BadNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) def test_post_wrong_post_response_nonce(self): self.available_nonces = [jose.b64encode(b'good'), b'f'] self.assertRaises(errors.BadNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) def test_post_failed_retry(self): check_response = mock.MagicMock() check_response.side_effect = messages.Error.with_code('badNonce') # pylint: disable=protected-access self.net._check_response = check_response self.assertRaises(messages.Error, self.net.post, 'uri', self.obj, content_type=self.content_type) def test_post_not_retried(self): check_response = mock.MagicMock() check_response.side_effect = [messages.Error.with_code('malformed'), self.response] # pylint: disable=protected-access self.net._check_response = check_response self.assertRaises(messages.Error, self.net.post, 'uri', self.obj, content_type=self.content_type) def test_post_successful_retry(self): post_once = mock.MagicMock() post_once.side_effect = [messages.Error.with_code('badNonce'), self.response] # pylint: disable=protected-access self.assertEqual(self.response, self.net.post( 'uri', self.obj, content_type=self.content_type)) def test_head_get_post_error_passthrough(self): self.send_request.side_effect = requests.exceptions.RequestException for method in self.net.head, self.net.get: self.assertRaises( requests.exceptions.RequestException, method, 'GET', 'uri') self.assertRaises(requests.exceptions.RequestException, self.net.post, 'uri', obj=self.obj) def test_post_bad_nonce_head(self): # pylint: disable=protected-access # regression test for https://github.com/certbot/certbot/issues/6092 bad_response = mock.MagicMock(ok=False, status_code=http_client.SERVICE_UNAVAILABLE) self.net._send_request = mock.MagicMock() self.net._send_request.return_value = bad_response self.content_type = None check_response = mock.MagicMock() self.net._check_response = check_response self.assertRaises(errors.ClientError, self.net.post, 'uri', self.obj, content_type=self.content_type, acme_version=2, new_nonce_url='new_nonce_uri') self.assertEqual(check_response.call_count, 1) def test_new_nonce_uri_removed(self): self.content_type = None self.net.post('uri', self.obj, content_type=None, acme_version=2, new_nonce_url='new_nonce_uri') class ClientNetworkSourceAddressBindingTest(unittest.TestCase): """Tests that if ClientNetwork has a source IP set manually, the underlying library has used the provided source address.""" def setUp(self): self.source_address = "8.8.8.8" def test_source_address_set(self): from acme.client import ClientNetwork net = ClientNetwork(key=None, alg=None, source_address=self.source_address) for adapter in net.session.adapters.values(): self.assertTrue(self.source_address in adapter.source_address) def test_behavior_assumption(self): """This is a test that guardrails the HTTPAdapter behavior so that if the default for a Session() changes, the assumptions here aren't violated silently.""" from acme.client import ClientNetwork # Source address not specified, so the default adapter type should be bound -- this # test should fail if the default adapter type is changed by requests net = ClientNetwork(key=None, alg=None) session = requests.Session() for scheme in session.adapters.keys(): client_network_adapter = net.session.adapters.get(scheme) default_adapter = session.adapters.get(scheme) self.assertEqual(client_network_adapter.__class__, default_adapter.__class__) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.31.0/acme/crypto_util.py0000644000076600000240000002577213427120435016263 0ustar bmwstaff00000000000000"""Crypto utilities.""" import binascii import contextlib import logging import os import re import socket from OpenSSL import crypto from OpenSSL import SSL # type: ignore # https://github.com/python/typeshed/issues/2052 import josepy as jose from acme import errors # pylint: disable=unused-import, no-name-in-module from acme.magic_typing import Callable, Union, Tuple, Optional # pylint: enable=unused-import, no-name-in-module logger = logging.getLogger(__name__) # TLSSNI01 certificate serving and probing is not affected by SSL # vulnerabilities: prober needs to check certificate for expected # contents anyway. Working SNI is the only thing that's necessary for # the challenge and thus scoping down SSL/TLS method (version) would # cause interoperability issues: TLSv1_METHOD is only compatible with # TLSv1_METHOD, while SSLv23_METHOD is compatible with all other # methods, including TLSv2_METHOD (read more at # https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni # should be changed to use "set_options" to disable SSLv2 and SSLv3, # in case it's used for things other than probing/serving! _DEFAULT_TLSSNI01_SSL_METHOD = SSL.SSLv23_METHOD # type: ignore class SSLSocket(object): # pylint: disable=too-few-public-methods """SSL wrapper for sockets. :ivar socket sock: Original wrapped socket. :ivar dict certs: Mapping from domain names (`bytes`) to `OpenSSL.crypto.X509`. :ivar method: See `OpenSSL.SSL.Context` for allowed values. """ def __init__(self, sock, certs, method=_DEFAULT_TLSSNI01_SSL_METHOD): self.sock = sock self.certs = certs self.method = method def __getattr__(self, name): return getattr(self.sock, name) def _pick_certificate_cb(self, connection): """SNI certificate callback. This method will set a new OpenSSL context object for this connection when an incoming connection provides an SNI name (in order to serve the appropriate certificate, if any). :param connection: The TLS connection object on which the SNI extension was received. :type connection: :class:`OpenSSL.Connection` """ server_name = connection.get_servername() try: key, cert = self.certs[server_name] except KeyError: logger.debug("Server name (%s) not recognized, dropping SSL", server_name) return new_context = SSL.Context(self.method) new_context.set_options(SSL.OP_NO_SSLv2) new_context.set_options(SSL.OP_NO_SSLv3) new_context.use_privatekey(key) new_context.use_certificate(cert) connection.set_context(new_context) class FakeConnection(object): """Fake OpenSSL.SSL.Connection.""" # pylint: disable=too-few-public-methods,missing-docstring def __init__(self, connection): self._wrapped = connection def __getattr__(self, name): return getattr(self._wrapped, name) def shutdown(self, *unused_args): # OpenSSL.SSL.Connection.shutdown doesn't accept any args return self._wrapped.shutdown() def accept(self): # pylint: disable=missing-docstring sock, addr = self.sock.accept() context = SSL.Context(self.method) context.set_options(SSL.OP_NO_SSLv2) context.set_options(SSL.OP_NO_SSLv3) context.set_tlsext_servername_callback(self._pick_certificate_cb) ssl_sock = self.FakeConnection(SSL.Connection(context, sock)) ssl_sock.set_accept_state() logger.debug("Performing handshake with %s", addr) try: ssl_sock.do_handshake() except SSL.Error as error: # _pick_certificate_cb might have returned without # creating SSL context (wrong server name) raise socket.error(error) return ssl_sock, addr def probe_sni(name, host, port=443, timeout=300, method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('', 0)): """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the client hello message. :param bytes host: Host to connect to. :param int port: Port to connect to. :param int timeout: Timeout in seconds. :param method: See `OpenSSL.SSL.Context` for allowed values. :param tuple source_address: Enables multi-path probing (selection of source interface). See `socket.creation_connection` for more info. Available only in Python 2.7+. :raises acme.errors.Error: In case of any problems. :returns: SSL certificate presented by the server. :rtype: OpenSSL.crypto.X509 """ context = SSL.Context(method) context.set_timeout(timeout) socket_kwargs = {'source_address': source_address} try: # pylint: disable=star-args logger.debug( "Attempting to connect to %s:%d%s.", host, port, " from {0}:{1}".format( source_address[0], source_address[1] ) if socket_kwargs else "" ) socket_tuple = (host, port) # type: Tuple[str, int] sock = socket.create_connection(socket_tuple, **socket_kwargs) # type: ignore except socket.error as error: raise errors.Error(error) with contextlib.closing(sock) as client: client_ssl = SSL.Connection(context, client) client_ssl.set_connect_state() client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13 try: client_ssl.do_handshake() client_ssl.shutdown() except SSL.Error as error: raise errors.Error(error) return client_ssl.get_peer_certificate() def make_csr(private_key_pem, domains, must_staple=False): """Generate a CSR containing a list of domains as subjectAltNames. :param buffer private_key_pem: Private key, in PEM PKCS#8 format. :param list domains: List of DNS names to include in subjectAltNames of CSR. :param bool must_staple: Whether to include the TLS Feature extension (aka OCSP Must Staple: https://tools.ietf.org/html/rfc7633). :returns: buffer PEM-encoded Certificate Signing Request. """ private_key = crypto.load_privatekey( crypto.FILETYPE_PEM, private_key_pem) csr = crypto.X509Req() extensions = [ crypto.X509Extension( b'subjectAltName', critical=False, value=', '.join('DNS:' + d for d in domains).encode('ascii') ), ] if must_staple: extensions.append(crypto.X509Extension( b"1.3.6.1.5.5.7.1.24", critical=False, value=b"DER:30:03:02:01:05")) csr.add_extensions(extensions) csr.set_pubkey(private_key) csr.set_version(2) csr.sign(private_key, 'sha256') return crypto.dump_certificate_request( crypto.FILETYPE_PEM, csr) def _pyopenssl_cert_or_req_all_names(loaded_cert_or_req): common_name = loaded_cert_or_req.get_subject().CN sans = _pyopenssl_cert_or_req_san(loaded_cert_or_req) if common_name is None: return sans else: return [common_name] + [d for d in sans if d != common_name] def _pyopenssl_cert_or_req_san(cert_or_req): """Get Subject Alternative Names from certificate or CSR using pyOpenSSL. .. todo:: Implement directly in PyOpenSSL! .. note:: Although this is `acme` internal API, it is used by `letsencrypt`. :param cert_or_req: Certificate or CSR. :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. :returns: A list of Subject Alternative Names. :rtype: `list` of `unicode` """ # This function finds SANs by dumping the certificate/CSR to text and # searching for "X509v3 Subject Alternative Name" in the text. This method # is used to support PyOpenSSL version 0.13 where the # `_subjectAltNameString` and `get_extensions` methods are not available # for CSRs. # constants based on PyOpenSSL certificate/CSR text dump part_separator = ":" parts_separator = ", " prefix = "DNS" + part_separator if isinstance(cert_or_req, crypto.X509): # pylint: disable=line-too-long func = crypto.dump_certificate # type: Union[Callable[[int, crypto.X509Req], bytes], Callable[[int, crypto.X509], bytes]] else: func = crypto.dump_certificate_request text = func(crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8") # WARNING: this function does not support multiple SANs extensions. # Multiple X509v3 extensions of the same type is disallowed by RFC 5280. match = re.search(r"X509v3 Subject Alternative Name:(?: critical)?\s*(.*)", text) # WARNING: this function assumes that no SAN can include # parts_separator, hence the split! sans_parts = [] if match is None else match.group(1).split(parts_separator) return [part.split(part_separator)[1] for part in sans_parts if part.startswith(prefix)] def gen_ss_cert(key, domains, not_before=None, validity=(7 * 24 * 60 * 60), force_san=True): """Generate new self-signed certificate. :type domains: `list` of `unicode` :param OpenSSL.crypto.PKey key: :param bool force_san: If more than one domain is provided, all of the domains are put into ``subjectAltName`` X.509 extension and first domain is set as the subject CN. If only one domain is provided no ``subjectAltName`` extension is used, unless `force_san` is ``True``. """ assert domains, "Must provide one or more hostnames for the cert." cert = crypto.X509() cert.set_serial_number(int(binascii.hexlify(os.urandom(16)), 16)) cert.set_version(2) extensions = [ crypto.X509Extension( b"basicConstraints", True, b"CA:TRUE, pathlen:0"), ] cert.get_subject().CN = domains[0] # TODO: what to put into cert.get_subject()? cert.set_issuer(cert.get_subject()) if force_san or len(domains) > 1: extensions.append(crypto.X509Extension( b"subjectAltName", critical=False, value=b", ".join(b"DNS:" + d.encode() for d in domains) )) cert.add_extensions(extensions) cert.gmtime_adj_notBefore(0 if not_before is None else not_before) cert.gmtime_adj_notAfter(validity) cert.set_pubkey(key) cert.sign(key, "sha256") return cert def dump_pyopenssl_chain(chain, filetype=crypto.FILETYPE_PEM): """Dump certificate chain into a bundle. :param list chain: List of `OpenSSL.crypto.X509` (or wrapped in :class:`josepy.util.ComparableX509`). :returns: certificate chain bundle :rtype: bytes """ # XXX: returns empty string when no chain is available, which # shuts up RenewableCert, but might not be the best solution... def _dump_cert(cert): if isinstance(cert, jose.ComparableX509): # pylint: disable=protected-access cert = cert.wrapped return crypto.dump_certificate(filetype, cert) # assumes that OpenSSL.crypto.dump_certificate includes ending # newline character return b"".join(_dump_cert(cert) for cert in chain) acme-0.31.0/acme/jose_test.py0000644000076600000240000000366213427120435015677 0ustar bmwstaff00000000000000"""Tests for acme.jose shim.""" import importlib import unittest class JoseTest(unittest.TestCase): """Tests for acme.jose shim.""" def _test_it(self, submodule, attribute): if submodule: acme_jose_path = 'acme.jose.' + submodule josepy_path = 'josepy.' + submodule else: acme_jose_path = 'acme.jose' josepy_path = 'josepy' acme_jose_mod = importlib.import_module(acme_jose_path) josepy_mod = importlib.import_module(josepy_path) self.assertIs(acme_jose_mod, josepy_mod) self.assertIs(getattr(acme_jose_mod, attribute), getattr(josepy_mod, attribute)) # We use the imports below with eval, but pylint doesn't # understand that. # pylint: disable=eval-used,unused-variable import acme import josepy acme_jose_mod = eval(acme_jose_path) josepy_mod = eval(josepy_path) self.assertIs(acme_jose_mod, josepy_mod) self.assertIs(getattr(acme_jose_mod, attribute), getattr(josepy_mod, attribute)) def test_top_level(self): self._test_it('', 'RS512') def test_submodules(self): # This test ensures that the modules in josepy that were # available at the time it was moved into its own package are # available under acme.jose. Backwards compatibility with new # modules or testing code is not maintained. mods_and_attrs = [('b64', 'b64decode',), ('errors', 'Error',), ('interfaces', 'JSONDeSerializable',), ('json_util', 'Field',), ('jwa', 'HS256',), ('jwk', 'JWK',), ('jws', 'JWS',), ('util', 'ImmutableMap',),] for mod, attr in mods_and_attrs: self._test_it(mod, attr) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.31.0/acme.egg-info/0000755000076600000240000000000013427120450015006 5ustar bmwstaff00000000000000acme-0.31.0/acme.egg-info/PKG-INFO0000644000076600000240000000175713427120450016115 0ustar bmwstaff00000000000000Metadata-Version: 2.1 Name: acme Version: 0.31.0 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Certbot Project Author-email: client-dev@letsencrypt.org License: Apache License 2.0 Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* Provides-Extra: docs Provides-Extra: dev acme-0.31.0/acme.egg-info/SOURCES.txt0000644000076600000240000000315113427120450016672 0ustar bmwstaff00000000000000LICENSE.txt MANIFEST.in README.rst pytest.ini setup.cfg setup.py acme/__init__.py acme/challenges.py acme/challenges_test.py acme/client.py acme/client_test.py acme/crypto_util.py acme/crypto_util_test.py acme/errors.py acme/errors_test.py acme/fields.py acme/fields_test.py acme/jose_test.py acme/jws.py acme/jws_test.py acme/magic_typing.py acme/magic_typing_test.py acme/messages.py acme/messages_test.py acme/standalone.py acme/standalone_test.py acme/test_util.py acme/util.py acme/util_test.py acme.egg-info/PKG-INFO acme.egg-info/SOURCES.txt acme.egg-info/dependency_links.txt acme.egg-info/requires.txt acme.egg-info/top_level.txt acme/testdata/README acme/testdata/cert-100sans.pem acme/testdata/cert-idnsans.pem acme/testdata/cert-nocn.der acme/testdata/cert-san.pem acme/testdata/cert.der acme/testdata/cert.pem acme/testdata/critical-san.pem acme/testdata/csr-100sans.pem acme/testdata/csr-6sans.pem acme/testdata/csr-idnsans.pem acme/testdata/csr-nosans.pem acme/testdata/csr-san.pem acme/testdata/csr.der acme/testdata/csr.pem acme/testdata/dsa512_key.pem acme/testdata/rsa1024_key.pem acme/testdata/rsa2048_cert.pem acme/testdata/rsa2048_key.pem acme/testdata/rsa256_key.pem acme/testdata/rsa512_key.pem docs/.gitignore docs/Makefile docs/api.rst docs/conf.py docs/index.rst docs/jws-help.txt docs/make.bat docs/_static/.gitignore docs/_templates/.gitignore docs/api/challenges.rst docs/api/client.rst docs/api/errors.rst docs/api/fields.rst docs/api/jose.rst docs/api/messages.rst docs/api/standalone.rst docs/man/jws.rst examples/standalone/README examples/standalone/localhost/cert.pem examples/standalone/localhost/key.pemacme-0.31.0/acme.egg-info/requires.txt0000644000076600000240000000032513427120450017406 0ustar bmwstaff00000000000000cryptography>=1.2.3 josepy>=1.0.0 mock PyOpenSSL>=0.13.1 pyrfc3339 pytz requests[security]>=2.6.0 requests-toolbelt>=0.3.0 setuptools six>=1.9.0 [dev] pytest pytest-xdist tox [docs] Sphinx>=1.0 sphinx_rtd_theme acme-0.31.0/acme.egg-info/top_level.txt0000644000076600000240000000000513427120450017533 0ustar bmwstaff00000000000000acme acme-0.31.0/acme.egg-info/dependency_links.txt0000644000076600000240000000000113427120450021054 0ustar bmwstaff00000000000000 acme-0.31.0/MANIFEST.in0000644000076600000240000000022213427120435014144 0ustar bmwstaff00000000000000include LICENSE.txt include README.rst include pytest.ini recursive-include docs * recursive-include examples * recursive-include acme/testdata * acme-0.31.0/docs/0000755000076600000240000000000013427120450013337 5ustar bmwstaff00000000000000acme-0.31.0/docs/man/0000755000076600000240000000000013427120450014112 5ustar bmwstaff00000000000000acme-0.31.0/docs/man/jws.rst0000644000076600000240000000004413427120435015450 0ustar bmwstaff00000000000000.. literalinclude:: ../jws-help.txt acme-0.31.0/docs/index.rst0000644000076600000240000000073713427120435015212 0ustar bmwstaff00000000000000.. acme-python documentation master file, created by sphinx-quickstart on Sun Oct 18 13:38:06 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to acme-python's documentation! ======================================= Contents: .. toctree:: :maxdepth: 2 api .. automodule:: acme :members: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` acme-0.31.0/docs/_templates/0000755000076600000240000000000013427120450015474 5ustar bmwstaff00000000000000acme-0.31.0/docs/_templates/.gitignore0000644000076600000240000000000013427120435017455 0ustar bmwstaff00000000000000acme-0.31.0/docs/Makefile0000644000076600000240000001640513427120435015010 0ustar bmwstaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/acme-python.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/acme-python.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/acme-python" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/acme-python" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." acme-0.31.0/docs/conf.py0000644000076600000240000002402113427120435014640 0ustar bmwstaff00000000000000# -*- coding: utf-8 -*- # # acme-python documentation build configuration file, created by # sphinx-quickstart on Sun Oct 18 13:38:06 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex here = os.path.abspath(os.path.dirname(__file__)) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', ] autodoc_member_order = 'bysource' autodoc_default_flags = ['show-inheritance', 'private-members'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'acme-python' copyright = u'2015-2015, Let\'s Encrypt Project' author = u'Let\'s Encrypt Project' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0' # The full version, including alpha/beta/rc tags. release = '0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. default_role = 'py:obj' # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs # on_rtd is whether we are on readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # otherwise, readthedocs.org uses their theme by default, so no need to specify it # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'acme-pythondoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'acme-python.tex', u'acme-python Documentation', u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'acme-python', u'acme-python Documentation', [author], 1), ('man/jws', 'jws', u'jws script documentation', [project], 1), ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'acme-python', u'acme-python Documentation', author, 'acme-python', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False intersphinx_mapping = { 'python': ('https://docs.python.org/', None), 'josepy': ('https://josepy.readthedocs.io/en/latest/', None), } acme-0.31.0/docs/_static/0000755000076600000240000000000013427120450014765 5ustar bmwstaff00000000000000acme-0.31.0/docs/_static/.gitignore0000644000076600000240000000000013427120435016746 0ustar bmwstaff00000000000000acme-0.31.0/docs/jws-help.txt0000644000076600000240000000024713427120435015637 0ustar bmwstaff00000000000000usage: jws [-h] [--compact] {sign,verify} ... positional arguments: {sign,verify} optional arguments: -h, --help show this help message and exit --compact acme-0.31.0/docs/make.bat0000644000076600000240000001612613427120435014755 0ustar bmwstaff00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\acme-python.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\acme-python.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end acme-0.31.0/docs/.gitignore0000644000076600000240000000001113427120435015322 0ustar bmwstaff00000000000000/_build/ acme-0.31.0/docs/api/0000755000076600000240000000000013427120450014110 5ustar bmwstaff00000000000000acme-0.31.0/docs/api/jose.rst0000644000076600000240000000024113427120435015602 0ustar bmwstaff00000000000000JOSE ---- The ``acme.jose`` module was moved to its own package "josepy_". Please refer to its documentation there. .. _josepy: https://josepy.readthedocs.io/ acme-0.31.0/docs/api/fields.rst0000644000076600000240000000007013427120435016110 0ustar bmwstaff00000000000000Fields ------ .. automodule:: acme.fields :members: acme-0.31.0/docs/api/errors.rst0000644000076600000240000000007013427120435016156 0ustar bmwstaff00000000000000Errors ------ .. automodule:: acme.errors :members: acme-0.31.0/docs/api/challenges.rst0000644000076600000240000000010413427120435016745 0ustar bmwstaff00000000000000Challenges ---------- .. automodule:: acme.challenges :members: acme-0.31.0/docs/api/messages.rst0000644000076600000240000000007613427120435016457 0ustar bmwstaff00000000000000Messages -------- .. automodule:: acme.messages :members: acme-0.31.0/docs/api/client.rst0000644000076600000240000000007013427120435016120 0ustar bmwstaff00000000000000Client ------ .. automodule:: acme.client :members: acme-0.31.0/docs/api/standalone.rst0000644000076600000240000000010413427120435016770 0ustar bmwstaff00000000000000Standalone ---------- .. automodule:: acme.standalone :members: acme-0.31.0/docs/api.rst0000644000076600000240000000013013427120435014637 0ustar bmwstaff00000000000000================= API Documentation ================= .. toctree:: :glob: api/* acme-0.31.0/setup.py0000644000076600000240000000470513427120437014134 0ustar bmwstaff00000000000000from setuptools import setup from setuptools import find_packages from setuptools.command.test import test as TestCommand import sys version = '0.31.0' # Please update tox.ini when modifying dependency version requirements install_requires = [ # load_pem_private/public_key (>=0.6) # rsa_recover_prime_factors (>=0.8) 'cryptography>=1.2.3', # formerly known as acme.jose: 'josepy>=1.0.0', # Connection.set_tlsext_host_name (>=0.13) 'mock', 'PyOpenSSL>=0.13.1', 'pyrfc3339', 'pytz', 'requests[security]>=2.6.0', # security extras added in 2.4.1 'requests-toolbelt>=0.3.0', 'setuptools', 'six>=1.9.0', # needed for python_2_unicode_compatible ] dev_extras = [ 'pytest', 'pytest-xdist', 'tox', ] docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', ] class PyTest(TestCommand): user_options = [] def initialize_options(self): TestCommand.initialize_options(self) self.pytest_args = '' def run_tests(self): import shlex # import here, cause outside the eggs aren't loaded import pytest errno = pytest.main(shlex.split(self.pytest_args)) sys.exit(errno) setup( name='acme', version=version, description='ACME protocol implementation in Python', url='https://github.com/letsencrypt/letsencrypt', author="Certbot Project", author_email='client-dev@letsencrypt.org', license='Apache License 2.0', python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ], packages=find_packages(), include_package_data=True, install_requires=install_requires, extras_require={ 'dev': dev_extras, 'docs': docs_extras, }, tests_require=["pytest"], test_suite='acme', cmdclass={"test": PyTest}, ) acme-0.31.0/examples/0000755000076600000240000000000013427120450014225 5ustar bmwstaff00000000000000acme-0.31.0/examples/standalone/0000755000076600000240000000000013427120450016355 5ustar bmwstaff00000000000000acme-0.31.0/examples/standalone/README0000644000076600000240000000010113427120435017230 0ustar bmwstaff00000000000000python -m acme.standalone -p 1234 curl -k https://localhost:1234 acme-0.31.0/examples/standalone/localhost/0000755000076600000240000000000013427120450020345 5ustar bmwstaff00000000000000acme-0.31.0/examples/standalone/localhost/key.pem0000644000076600000240000000325013427120435021643 0ustar bmwstaff00000000000000-----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq r5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo VpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW AEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ MmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W Gju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp v+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3 3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO z7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB o35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK lt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH Vqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a hUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/ IkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/ 9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP n94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj 9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy ftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb f4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3 YEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3 xFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG wifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL Hh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK jUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh QxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6 kmf1UgGFcKrJuXgwEtTVxw== -----END PRIVATE KEY----- acme-0.31.0/examples/standalone/localhost/cert.pem0000644000076600000240000000241613427120435022013 0ustar bmwstaff00000000000000-----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV BAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do ZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx MzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC V0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy dGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN PSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm 7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn xvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD g7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1 RAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU uQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T RiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx nkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8 kTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+ aonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P AfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf Esg= -----END CERTIFICATE----- acme-0.31.0/setup.cfg0000644000076600000240000000010313427120450014222 0ustar bmwstaff00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 acme-0.31.0/README.rst0000644000076600000240000000004713427120435014102 0ustar bmwstaff00000000000000ACME protocol implementation in Python acme-0.31.0/LICENSE.txt0000644000076600000240000002504213427120435014240 0ustar bmwstaff00000000000000 Copyright 2015 Electronic Frontier Foundation and others Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS