示例#1
0
def retrieve_acme_v1_certificate(client, csr_der):
    '''
    Create a new certificate based on the CSR (ACME v1 protocol).
    Return the certificate object as dict
    https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
    '''
    new_cert = {
        "resource": "new-cert",
        "csr": nopad_b64(csr_der),
    }
    result, info = client.send_signed_request(
        client.directory['new-cert'],
        new_cert,
        error_msg='Failed to receive certificate',
        expected_status_codes=[200, 201])
    cert = CertificateChain(info['location'])
    cert.cert = der_to_pem(result)

    def f(link, relation):
        if relation == 'up':
            chain_result, chain_info = client.get_request(
                link, parse_json_result=False)
            if chain_info['status'] in [200, 201]:
                del cert.chain[:]
                cert.chain.append(der_to_pem(chain_result))

    process_links(info, f)
    return cert
    def finalize(self, client, csr_der, wait=True):
        '''
        Create a new certificate based on the csr.
        Return the certificate object as dict
        https://tools.ietf.org/html/rfc8555#section-7.4
        '''
        new_cert = {
            "csr": nopad_b64(csr_der),
        }
        result, info = client.send_signed_request(
            self.finalize_uri,
            new_cert,
            error_msg='Failed to finalizing order',
            expected_status_codes=[200])
        # It is not clear from the RFC whether the finalize call returns the order object or not.
        # Instead of using the result, we call self.refresh(client) below.

        if wait:
            self.wait_for_finalization(client)
        else:
            self.refresh(client)
            if self.status not in ['procesing', 'valid', 'invalid']:
                raise ACMEProtocolException(
                    client.module,
                    'Failed to finalize order; got status "{status}"'.format(
                        status=self.status),
                    info=info,
                    content_json=result)
def create_key_authorization(client, token):
    '''
    Returns the key authorization for the given token
    https://tools.ietf.org/html/rfc8555#section-8.1
    '''
    accountkey_json = json.dumps(client.account_jwk, sort_keys=True, separators=(',', ':'))
    thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
    return "{0}.{1}".format(token, thumbprint)
    def sign_request(self, protected, payload, key_data, encode_payload=True):
        '''
        Signs an ACME request.
        '''
        try:
            if payload is None:
                # POST-as-GET
                payload64 = ''
            else:
                # POST
                if encode_payload:
                    payload = self.module.jsonify(payload).encode('utf8')
                payload64 = nopad_b64(to_bytes(payload))
            protected64 = nopad_b64(
                self.module.jsonify(protected).encode('utf8'))
        except Exception as e:
            raise ModuleFailException(
                "Failed to encode payload / headers as JSON: {0}".format(e))

        return self.backend.sign(payload64, protected64, key_data)
示例#5
0
    def get_validation_data(self, client, identifier_type, identifier):
        token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token)
        key_authorization = create_key_authorization(client, token)

        if self.type == 'http-01':
            # https://tools.ietf.org/html/rfc8555#section-8.3
            return {
                'resource':
                '.well-known/acme-challenge/{token}'.format(token=token),
                'resource_value':
                key_authorization,
            }

        if self.type == 'dns-01':
            if identifier_type != 'dns':
                return None
            # https://tools.ietf.org/html/rfc8555#section-8.4
            resource = '_acme-challenge'
            value = nopad_b64(
                hashlib.sha256(to_bytes(key_authorization)).digest())
            record = (resource + identifier[1:]) if identifier.startswith(
                '*.') else '{0}.{1}'.format(resource, identifier)
            return {
                'resource': resource,
                'resource_value': value,
                'record': record,
            }

        if self.type == 'tls-alpn-01':
            # https://www.rfc-editor.org/rfc/rfc8737.html#section-3
            if identifier_type == 'ip':
                # IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596)
                resource = compat_ipaddress.ip_address(
                    identifier).reverse_pointer
                if not resource.endswith('.'):
                    resource += '.'
            else:
                resource = identifier
            value = base64.b64encode(
                hashlib.sha256(to_bytes(key_authorization)).digest())
            return {
                'resource':
                resource,
                'resource_original':
                combine_identifier(identifier_type, identifier),
                'resource_value':
                value,
            }

        # Unknown challenge type: ignore
        return None
示例#6
0
    def sign(self, payload64, protected64, key_data):
        sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
        if key_data['type'] == 'hmac':
            hex_key = to_native(
                binascii.hexlify(base64.urlsafe_b64decode(
                    key_data['jwk']['k'])))
            cmd_postfix = [
                "-mac", "hmac", "-macopt", "hexkey:{0}".format(hex_key),
                "-binary"
            ]
        else:
            cmd_postfix = ["-sign", key_data['key_file']]
        openssl_sign_cmd = [
            self.openssl_binary, "dgst", "-{0}".format(key_data['hash'])
        ] + cmd_postfix

        dummy, out, dummy = self.module.run_command(
            openssl_sign_cmd,
            data=sign_payload,
            check_rc=True,
            binary_data=True,
            environ_update=_OPENSSL_ENVIRONMENT_UPDATE)

        if key_data['type'] == 'ec':
            dummy, der_out, dummy = self.module.run_command(
                [self.openssl_binary, "asn1parse", "-inform", "DER"],
                data=out,
                binary_data=True,
                environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
            expected_len = 2 * key_data['point_size']
            sig = re.findall(
                r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len,
                to_text(der_out, errors='surrogate_or_strict'))
            if len(sig) != 2:
                raise BackendException(
                    "failed to generate Elliptic Curve signature; cannot parse DER output: {0}"
                    .format(to_text(der_out, errors='surrogate_or_strict')))
            sig[0] = (expected_len - len(sig[0])) * '0' + sig[0]
            sig[1] = (expected_len - len(sig[1])) * '0' + sig[1]
            out = binascii.unhexlify(sig[0]) + binascii.unhexlify(sig[1])

        return {
            "protected": protected64,
            "payload": payload64,
            "signature": nopad_b64(to_bytes(out)),
        }
示例#7
0
    def sign(self, payload64, protected64, key_data):
        sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
        if 'mac_obj' in key_data:
            mac = key_data['mac_obj']()
            mac.update(sign_payload)
            signature = mac.finalize()
        elif isinstance(
                key_data['key_obj'],
                cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
            padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15(
            )
            hashalg = cryptography.hazmat.primitives.hashes.SHA256
            signature = key_data['key_obj'].sign(sign_payload, padding,
                                                 hashalg())
        elif isinstance(
                key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.
                ec.EllipticCurvePrivateKey):
            if key_data['hash'] == 'sha256':
                hashalg = cryptography.hazmat.primitives.hashes.SHA256
            elif key_data['hash'] == 'sha384':
                hashalg = cryptography.hazmat.primitives.hashes.SHA384
            elif key_data['hash'] == 'sha512':
                hashalg = cryptography.hazmat.primitives.hashes.SHA512
            ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(
                hashalg())
            r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(
                key_data['key_obj'].sign(sign_payload, ecdsa))
            rr = _pad_hex(r, 2 * key_data['point_size'])
            ss = _pad_hex(s, 2 * key_data['point_size'])
            signature = binascii.unhexlify(rr) + binascii.unhexlify(ss)

        return {
            "protected": protected64,
            "payload": payload64,
            "signature": nopad_b64(signature),
        }
示例#8
0
 def parse_key(self, key_file=None, key_content=None, passphrase=None):
     '''
     Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
     Raises KeyParsingError in case of errors.
     '''
     # If key_content isn't given, read key_file
     if key_content is None:
         key_content = read_file(key_file)
     else:
         key_content = to_bytes(key_content)
     # Parse key
     try:
         key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
             key_content,
             password=to_bytes(passphrase)
             if passphrase is not None else None,
             backend=_cryptography_backend)
     except Exception as e:
         raise KeyParsingError('error while loading key: {0}'.format(e))
     if isinstance(
             key,
             cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
         pk = key.public_key().public_numbers()
         return {
             'key_obj': key,
             'type': 'rsa',
             'alg': 'RS256',
             'jwk': {
                 "kty":
                 "RSA",
                 "e":
                 nopad_b64(_convert_int_to_bytes(_count_bytes(pk.e), pk.e)),
                 "n":
                 nopad_b64(_convert_int_to_bytes(_count_bytes(pk.n), pk.n)),
             },
             'hash': 'sha256',
         }
     elif isinstance(
             key, cryptography.hazmat.primitives.asymmetric.ec.
             EllipticCurvePrivateKey):
         pk = key.public_key().public_numbers()
         if pk.curve.name == 'secp256r1':
             bits = 256
             alg = 'ES256'
             hashalg = 'sha256'
             point_size = 32
             curve = 'P-256'
         elif pk.curve.name == 'secp384r1':
             bits = 384
             alg = 'ES384'
             hashalg = 'sha384'
             point_size = 48
             curve = 'P-384'
         elif pk.curve.name == 'secp521r1':
             # Not yet supported on Let's Encrypt side, see
             # https://github.com/letsencrypt/boulder/issues/2217
             bits = 521
             alg = 'ES512'
             hashalg = 'sha512'
             point_size = 66
             curve = 'P-521'
         else:
             raise KeyParsingError('unknown elliptic curve: {0}'.format(
                 pk.curve.name))
         num_bytes = (bits + 7) // 8
         return {
             'key_obj': key,
             'type': 'ec',
             'alg': alg,
             'jwk': {
                 "kty": "EC",
                 "crv": curve,
                 "x": nopad_b64(_convert_int_to_bytes(num_bytes, pk.x)),
                 "y": nopad_b64(_convert_int_to_bytes(num_bytes, pk.y)),
             },
             'hash': hashalg,
             'point_size': point_size,
         }
     else:
         raise KeyParsingError('unknown key type "{0}"'.format(type(key)))
示例#9
0
def main():
    argument_spec = get_default_argspec()
    argument_spec.update(
        dict(
            private_key_src=dict(type='path'),
            private_key_content=dict(type='str', no_log=True),
            private_key_passphrase=dict(type='str', no_log=True),
            certificate=dict(type='path', required=True),
            revoke_reason=dict(type='int'),
        ))
    module = AnsibleModule(
        argument_spec=argument_spec,
        required_one_of=([
            'account_key_src', 'account_key_content', 'private_key_src',
            'private_key_content'
        ], ),
        mutually_exclusive=([
            'account_key_src', 'account_key_content', 'private_key_src',
            'private_key_content'
        ], ),
        supports_check_mode=False,
    )
    backend = create_backend(module, False)

    try:
        client = ACMEClient(module, backend)
        account = ACMEAccount(client)
        # Load certificate
        certificate = pem_to_der(module.params.get('certificate'))
        certificate = nopad_b64(certificate)
        # Construct payload
        payload = {'certificate': certificate}
        if module.params.get('revoke_reason') is not None:
            payload['reason'] = module.params.get('revoke_reason')
        # Determine endpoint
        if module.params.get('acme_version') == 1:
            endpoint = client.directory['revoke-cert']
            payload['resource'] = 'revoke-cert'
        else:
            endpoint = client.directory['revokeCert']
        # Get hold of private key (if available) and make sure it comes from disk
        private_key = module.params.get('private_key_src')
        private_key_content = module.params.get('private_key_content')
        # Revoke certificate
        if private_key or private_key_content:
            passphrase = module.params['private_key_passphrase']
            # Step 1: load and parse private key
            try:
                private_key_data = client.parse_key(private_key,
                                                    private_key_content,
                                                    passphrase=passphrase)
            except KeyParsingError as e:
                raise ModuleFailException(
                    "Error while parsing private key: {msg}".format(msg=e.msg))
            # Step 2: sign revokation request with private key
            jws_header = {
                "alg": private_key_data['alg'],
                "jwk": private_key_data['jwk'],
            }
            result, info = client.send_signed_request(
                endpoint,
                payload,
                key_data=private_key_data,
                jws_header=jws_header,
                fail_on_error=False)
        else:
            # Step 1: get hold of account URI
            created, account_data = account.setup_account(allow_creation=False)
            if created:
                raise AssertionError('Unwanted account creation')
            if account_data is None:
                raise ModuleFailException(
                    msg='Account does not exist or is deactivated.')
            # Step 2: sign revokation request with account key
            result, info = client.send_signed_request(endpoint,
                                                      payload,
                                                      fail_on_error=False)
        if info['status'] != 200:
            already_revoked = False
            # Standardized error from draft 14 on (https://tools.ietf.org/html/rfc8555#section-7.6)
            if result.get(
                    'type') == 'urn:ietf:params:acme:error:alreadyRevoked':
                already_revoked = True
            else:
                # Hack for Boulder errors
                if module.params.get('acme_version') == 1:
                    error_type = 'urn:acme:error:malformed'
                else:
                    error_type = 'urn:ietf:params:acme:error:malformed'
                if result.get('type') == error_type and result.get(
                        'detail') == 'Certificate already revoked':
                    # Fallback: boulder returns this in case the certificate was already revoked.
                    already_revoked = True
            # If we know the certificate was already revoked, we don't fail,
            # but successfully terminate while indicating no change
            if already_revoked:
                module.exit_json(changed=False)
            raise ACMEProtocolException('Failed to revoke certificate',
                                        info=info,
                                        content_json=result)
        module.exit_json(changed=True)
    except ModuleFailException as e:
        e.do_fail(module)
示例#10
0
    def parse_key(self, key_file=None, key_content=None, passphrase=None):
        '''
        Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
        Raises KeyParsingError in case of errors.
        '''
        if passphrase is not None:
            raise KeyParsingError(
                'openssl backend does not support key passphrases')
        # If key_file isn't given, but key_content, write that to a temporary file
        if key_file is None:
            fd, tmpsrc = tempfile.mkstemp()
            self.module.add_cleanup_file(
                tmpsrc)  # Ansible will delete the file on exit
            f = os.fdopen(fd, 'wb')
            try:
                f.write(key_content.encode('utf-8'))
                key_file = tmpsrc
            except Exception as err:
                try:
                    f.close()
                except Exception as dummy:
                    pass
                raise KeyParsingError(
                    "failed to create temporary content file: %s" %
                    to_native(err),
                    exception=traceback.format_exc())
            f.close()
        # Parse key
        account_key_type = None
        with open(key_file, "rt") as f:
            for line in f:
                m = re.match(
                    r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$",
                    line)
                if m is not None:
                    account_key_type = m.group(1).lower()
                    break
        if account_key_type is None:
            # This happens for example if openssl_privatekey created this key
            # (as opposed to the OpenSSL binary). For now, we assume this is
            # an RSA key.
            # FIXME: add some kind of auto-detection
            account_key_type = "rsa"
        if account_key_type not in ("rsa", "ec"):
            raise KeyParsingError('unknown key type "%s"' % account_key_type)

        openssl_keydump_cmd = [
            self.openssl_binary, account_key_type, "-in", key_file, "-noout",
            "-text"
        ]
        dummy, out, dummy = self.module.run_command(
            openssl_keydump_cmd,
            check_rc=True,
            environ_update=_OPENSSL_ENVIRONMENT_UPDATE)

        if account_key_type == 'rsa':
            pub_hex, pub_exp = re.search(
                r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)",
                to_text(out, errors='surrogate_or_strict'),
                re.MULTILINE | re.DOTALL).groups()
            pub_exp = "{0:x}".format(int(pub_exp))
            if len(pub_exp) % 2:
                pub_exp = "0{0}".format(pub_exp)

            return {
                'key_file': key_file,
                'type': 'rsa',
                'alg': 'RS256',
                'jwk': {
                    "kty":
                    "RSA",
                    "e":
                    nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
                    "n":
                    nopad_b64(
                        binascii.unhexlify(
                            re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
                },
                'hash': 'sha256',
            }
        elif account_key_type == 'ec':
            pub_data = re.search(
                r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?",
                to_text(out, errors='surrogate_or_strict'),
                re.MULTILINE | re.DOTALL)
            if pub_data is None:
                raise KeyParsingError('cannot parse elliptic curve key')
            pub_hex = binascii.unhexlify(
                re.sub(r"(\s|:)", "", pub_data.group(1)).encode("utf-8"))
            asn1_oid_curve = pub_data.group(2).lower()
            nist_curve = pub_data.group(3).lower() if pub_data.group(
                3) else None
            if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256':
                bits = 256
                alg = 'ES256'
                hashalg = 'sha256'
                point_size = 32
                curve = 'P-256'
            elif asn1_oid_curve == 'secp384r1' or nist_curve == 'p-384':
                bits = 384
                alg = 'ES384'
                hashalg = 'sha384'
                point_size = 48
                curve = 'P-384'
            elif asn1_oid_curve == 'secp521r1' or nist_curve == 'p-521':
                # Not yet supported on Let's Encrypt side, see
                # https://github.com/letsencrypt/boulder/issues/2217
                bits = 521
                alg = 'ES512'
                hashalg = 'sha512'
                point_size = 66
                curve = 'P-521'
            else:
                raise KeyParsingError('unknown elliptic curve: %s / %s' %
                                      (asn1_oid_curve, nist_curve))
            num_bytes = (bits + 7) // 8
            if len(pub_hex) != 2 * num_bytes:
                raise KeyParsingError('bad elliptic curve point (%s / %s)' %
                                      (asn1_oid_curve, nist_curve))
            return {
                'key_file': key_file,
                'type': 'ec',
                'alg': alg,
                'jwk': {
                    "kty": "EC",
                    "crv": curve,
                    "x": nopad_b64(pub_hex[:num_bytes]),
                    "y": nopad_b64(pub_hex[num_bytes:]),
                },
                'hash': hashalg,
                'point_size': point_size,
            }
示例#11
0
def test_nopad_b64(value, result):
    assert nopad_b64(value.encode('utf-8')) == result