Esempio n. 1
0
def _get_key_consumer_cert(tenant: str, jwe_kid: str) -> str:
    """
    Retrieves the key consumer certificate. This cert is used to wrap
    the cek (content encryption key)
    """
    trace_enter(inspect.currentframe())

    # try to fetch dedicated key consumer cert
    key_consumer_cert_path = config.get_key_consumer_cert(tenant, jwe_kid)

    if not key_consumer_cert_path:
        ret = ''
        logger.error(
            'Cannot find dedicated key consumer certificate'
            'nor backend-wide key consumer certificate for'
            '"%s/%s"', tenant, jwe_kid)

        trace_exit(inspect.currentframe(), ret)
        return ret

    try:
        with open(key_consumer_cert_path) as file:
            cert = file.read().strip()
    except Exception as exc:
        ret = ''

        logger.error('Cannot read key consumer certificate at '
                     '"%s": %s.', key_consumer_cert_path, exc)

        trace_exit(inspect.currentframe(), ret)
        return ret

    trace_exit(inspect.currentframe(), cert)
    return cert
Esempio n. 2
0
def _create_jwe_token_json(jwe_kid: str, b64_protected_header: bytes,
                           b64_cek_ciphertext: bytes, b64_iv: bytes,
                           b64_encrypted_dek: bytes, b64_tag: bytes) -> str:
    """
    Creates JWE token according to:
        https://tools.ietf.org/html/rfc7516#section-3.3

    Compact Serialization representation:
        BASE64URL(UTF8(JWE Protected Header)) || '.' ||
        BASE64URL(JWE Encrypted Key) || '.' ||
        BASE64URL(JWE Initialization Vector) || '.' ||
        BASE64URL(JWE Ciphertext) || '.' ||
        BASE64URL(JWE Authentication Tag)
    """
    trace_enter(inspect.currentframe())

    try:
        jwe = b64_protected_header + b'.' + b64_cek_ciphertext + b'.' + \
            b64_iv + b'.' + b64_encrypted_dek + b'.' + b64_tag

        jwe_token = {'kid': jwe_kid, 'jwe': jwe.decode()}

        json_jwe_token = json.dumps(jwe_token)
    except Exception as exc:
        ret = ''
        logger.error('Failed to create JWE token: %s', exc)
        trace_exit(inspect.currentframe(), ret)
        return ret

    logger.debug('Created JWE token: %s', json_jwe_token)

    trace_exit(inspect.currentframe(), json_jwe_token)
    return json_jwe_token
Esempio n. 3
0
def get_wrapped_key_as_jwe(priv_dek: bytearray,
                           tenant: str,
                           jwe_kid: str,
                           nonce: str = '') -> str:
    """Creates a JWE."""
    trace_enter(inspect.currentframe())

    logger.info('Creating JWE token for request with kid "%s"...', jwe_kid)

    # Generate a 256 bit AES content encryption key (32 bytes * 8).
    try:
        cek = bytearray(get_random_bytes(32))
    except Exception as exc:
        ret = ''
        logger.error('Failed to get random bytes: %s', exc)
        trace_exit(inspect.currentframe(), ret)
        return ret

    if config.get_config_by_keypath('DEV_MODE'):
        logger.debug('Generated cek (BYOK AES key): %s (hex)', cek.hex())

    if not (b64_cek_ciphertext := _encrypt_cek_with_key_consumer_key(
            tenant, jwe_kid, cek)):

        logger.error(
            'Cannot encrypt content encryption key with key consumer '
            'key of %s/%s.', tenant, jwe_kid)

        trace_exit(inspect.currentframe(), '')
        return ''
Esempio n. 4
0
def _encrypt_dek_with_cek(priv_cek: bytearray, initialization_vector: bytes,
                          priv_dek: bytearray,
                          ascii_b64_protected_header: bytes) \
        -> Tuple[bytes, bytes]:
    """
    Wrap dek with cek:
    - Perform authenticated encryption on dek with the AES GCM algorithm.
    - Use cek as encryption key, the initialization vector,
      and the protected header as Additional Authenticated Data value.
    - Request a 128-bit Authentication Tag output.
    """
    trace_enter(inspect.currentframe())

    try:
        # mac_len=16 bytes: 128 bit authentication tag
        dek_cipher = AES.new(priv_cek,
                             AES.MODE_GCM,
                             nonce=initialization_vector,
                             mac_len=16)

        # add additional authenticated data (aad)
        dek_cipher.update(ascii_b64_protected_header)

        # TODO: Autom. padding helpful? Might replace pycryptodome anyway.
        # from Cryptodome.Util.Padding import pad
        # encrypted_dek, tag = \
        #   dek_cipher.encrypt_and_digest(pad(dek, AES.block_size))
        encrypted_dek, tag = dek_cipher.encrypt_and_digest(priv_dek)

        # Remove sensitive data from memory
        del priv_dek[:]
        del priv_cek[:]

        b64_encrypted_dek = base64.urlsafe_b64encode(encrypted_dek)
        b64_tag = base64.urlsafe_b64encode(tag)
    except Exception as exc:
        ret = (b'', b'')
        logger.error('Failed to encrypt dek: %s', exc)
        trace_exit(inspect.currentframe(), ret)
        return ret

    if config.get_config_by_keypath('DEV_MODE'):
        logger.debug('Additional authenticated data (aad): '
                     '%s', ascii_b64_protected_header.decode())
        logger.debug('Encrypted dek: "%s" (hex), '
                     'tag :"%s" (hex).', encrypted_dek.hex(), tag.hex())

    trace_exit(inspect.currentframe(), (b64_encrypted_dek, b64_tag))
    return b64_encrypted_dek, b64_tag
Esempio n. 5
0
def _encrypt_cek_with_key_consumer_key(tenant: str, jwe_kid: str,
                                       priv_cek: bytearray) -> bytes:
    trace_enter(inspect.currentframe())

    # Encrypt cek with public key from key consumer using RSAES-OAEP
    # (BASE64URL(JWE Encrypted CEK Key))
    if not (key_consumer_cert := _get_key_consumer_cert(tenant, jwe_kid)):
        ret = b''

        logger.error(
            'Cannot get key consumer certificate for tenant '
            '"%s" with JWE kid "%s".', tenant, jwe_kid)

        trace_exit(inspect.currentframe(), ret)
        return ret
Esempio n. 6
0
def get_kid_from_jwt(priv_token: str) -> str:
    """
    Extracts KID from JWT token. Only use this method after JWT token
    has been validated.
    """
    trace_enter(inspect.currentframe())

    try:
        protected_header_unverified = jwt.get_unverified_header(priv_token)
    except jwt.DecodeError as exc:
        ret = ''
        logger.error('Cannot decode JWT to get kid: %s', exc)
        logger.debug('JWT: %s', priv_token)
        trace_exit(inspect.currentframe(), ret)
        return ret

    ret = protected_header_unverified.get('kid', '')
    trace_exit(inspect.currentframe(), ret)
    return ret
Esempio n. 7
0
def _get_jwe_protected_header(jwe_kid: str, nonce: str) -> bytes:
    """Creates JWE protected header (BASE64URL(UTF8(JWE Protected Header)))."""
    trace_enter(inspect.currentframe())

    protected_header = {'alg': 'RSA-OAEP', 'enc': 'A256GCM', 'kid': jwe_kid}

    if nonce:
        protected_header['jti'] = nonce

    try:
        b64_protected_header = base64.urlsafe_b64encode(
            json.dumps(protected_header).encode('utf-8'))
    except Exception as exc:
        ret = b''
        logger.error('Failed to create protected header: %s', exc)
        trace_exit(inspect.currentframe(), ret)
        return ret

    trace_exit(inspect.currentframe(), b64_protected_header)
    return b64_protected_header
Esempio n. 8
0
def __handle_request_parsing_error(
        validation_error: typeValidationError,
        request: werkzeug.local.LocalProxy,
        schema: SchemaABC,
        error_status_code: int = None,
        error_headers: Mapping[str, str] = None) -> None:
    """Handles errors, or raised exceptions respectively."""
    trace_enter(inspect.currentframe())

    input_data = validation_error.__dict__['data']

    logger.error('Input validation failed with error "%s". '
                 'Input: "%s".', validation_error, input_data)

    resp = Response(response=json.dumps(validation_error.__dict__['messages']),
                    status=422,
                    content_type='application/json; charset=utf-8')

    trace_exit(inspect.currentframe(), resp)
    abort(resp)
Esempio n. 9
0
def __x_real_ip_validator(x_real_ip: str) -> None:
    """Validates the X-Real-IP header."""
    trace_enter(inspect.currentframe())

    if not 6 < len(x_real_ip) < 16:
        err_msg = 'X-Real-Ip must be between 7 and 15 characters long.'
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)

    parts = x_real_ip.split('.')

    if len(parts) != 4:
        err_msg = ('X-Real-Ip format does not match: '
                   'digits.digits.digits.digits.')
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)

    for part in parts:
        if not 0 < len(part) < 4:
            err_msg = ('X-Real-Ip format does not match: '
                       'x.x.x.x-xxx.xxx.xxx.xxx')
            logger.error(err_msg)
            trace_exit(inspect.currentframe(), err_msg)
            raise ValidationError(err_msg, status_code=422)
Esempio n. 10
0
def __user_agent_validator(user_agent: str) -> None:
    """
    Validates the user agent header.

    User agent specs: https://developer.mozilla.org/en-US/docs/Web/
        HTTP/Headers/User-Agent

    Enforce a minimum pattern of "uname/version".
    """
    trace_enter(inspect.currentframe())

    if len(user_agent) > 150:
        err_msg = 'User agent contains more than 150 characters.'
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)

    parts = user_agent.split('/')

    err_msg = 'User agent pattern does not match "name/version"'
    if len(parts) < 2:
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)

    if (len(parts[0]) < 1) or (len(parts[1]) < 1):
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)
Esempio n. 11
0
def __request_id_validator(request_id: str) -> None:
    # Replay attack detection specs of Salesforce's cache-only service:
    # https://developer.salesforce.com/docs/atlas.en-us.securityImplGuide.meta/
    #   securityImplGuide/security_pe_byok_cache_replay.htm
    trace_enter(inspect.currentframe())

    request_id_length = 32

    if len(request_id) != request_id_length:
        err_msg = ('requestId/nonce length must be %s alphanummeric '
                   'chars.' % request_id_length)
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)

    result = re.match('^[a-zA-Z0-9]+$', request_id)

    if not result:
        err_msg = ('requestId/nonce must consist of alphanummeric chars only.')
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)
Esempio n. 12
0
def __authenticate_vault_client(client: hvac.Client, tenant: str,
                                priv_jwt_token: str) -> hvac.Client:

    trace_enter(inspect.currentframe())

    vault_auth_jwt_path = config.get_vault_auth_jwt_path(tenant)

    if not vault_auth_jwt_path:
        logger.error('Failed to load auth jwt path for tenant "%s"', tenant)
        return None

    if config.get_config_by_keypath('DEV_MODE'):
        logger.debug('Attempting to authenticate against Vault using JWT: %s',
                     priv_jwt_token)

    cache_id = utils.get_vault_token_cache_id(tenant, priv_jwt_token)

    if cache_id in __VAULT_TOKEN_CACHE:
        logger.debug('Cache hit: Found token for "%s".', cache_id)
        client.token = __VAULT_TOKEN_CACHE[cache_id]
    else:
        logger.debug('Cache miss: Token for "%s" not found.', cache_id)

        token = __get_vault_token(client, tenant, priv_jwt_token,
                                  vault_auth_jwt_path)

        if not token:
            ret = None
            logger.error('Failed to get Vault token.')
            trace_exit(inspect.currentframe(), ret)
            return ret

        client.token = token
        __VAULT_TOKEN_CACHE[cache_id] = token

    if not client.is_authenticated():
        # token might be invalid/has expired
        del __VAULT_TOKEN_CACHE[cache_id]

        ret = None

        logger.error('Failed to validate Vault client. '
                     'Review configuration (config/config.json). '
                     'Retry as token might have expired.')

        trace_exit(inspect.currentframe(), ret)
        return ret

    logger.debug('Successfully authenticated Vault client '
                 'for tenant "%s"', tenant)

    trace_exit(inspect.currentframe(), client)
    return client
Esempio n. 13
0
def __get_vault_client(tenant: str) -> hvac.Client:
    vault_mtls_client_cert = config.get_vault_mtls_client_cert(tenant)
    vault_mtls_client_key = config.get_vault_mtls_client_key(tenant)
    vault_url = config.get_vault_url(tenant)
    vault_ns = config.get_vault_namespace(tenant)
    vault_ca_cert = config.get_vault_ca_cert(tenant)

    verify: Union[bool, str]

    if not vault_mtls_client_cert:
        logger.error('Failed to load Vault mTLS client cert.')
        return None
    if not vault_mtls_client_key:
        logger.error('Failed to load Vault mTLS client key.')
        return None
    if not vault_url:
        logger.error('Failed to load Vault url.')
        return None
    if not vault_ns:
        logger.error('Failed to load Vault namespace')
        return None
    if not vault_ca_cert:
        logger.warning('Failed to load Vault CA cert.')
        verify = True
    else:
        verify = vault_ca_cert

    if vault_ns == 'root':
        vault_ns = ''

    mtls_auth = (vault_mtls_client_cert, vault_mtls_client_key)

    # hvac.Client() never raises exceptions, regardless of the parameters
    client = hvac.Client(cert=mtls_auth,
                         url=vault_url,
                         namespace=vault_ns,
                         verify=verify)

    trace_exit(inspect.currentframe(), client)
    return client
Esempio n. 14
0
def __get_vault_token(client: hvac.Client, tenant: str, priv_jwt_token: str,
                      vault_auth_jwt_path: str) -> str:

    trace_enter(inspect.currentframe())

    default_role = config.get_vault_default_role(tenant)

    if not default_role:
        logger.error('Failed to load Vault default role for tenant "%s"',
                     tenant)
        return ''

    try:
        response = client.auth.jwt.jwt_login(role=default_role,
                                             jwt=priv_jwt_token,
                                             path=vault_auth_jwt_path)
    except Exception as exc:
        ret = ''
        logger.error('Failed to authenticate against Vault: %s', exc)
        trace_exit(inspect.currentframe(), ret)
        return ret

    if config.get_config_by_keypath('DEV_MODE'):
        logger.debug('Vault login response: %s', response)

    try:
        vault_token = response['auth']['client_token']
    except KeyError as exc:
        ret = ''

        logger.error(
            'Failed to access the Vault token from auth response: '
            'KeyError on key %s. '
            'This is most likely a permission issue.', exc)

        trace_exit(inspect.currentframe(), ret)
        return ret

    if config.get_config_by_keypath('DEV_MODE'):
        logger.debug('Vault client token returned: %s', vault_token)

    logger.debug('Retrieved new Vault token.')

    trace_exit(inspect.currentframe(), CAMOUFLAGE_SIGN)
    return vault_token
Esempio n. 15
0
    try:
        # SHA1 is outdated and broken. However, Salesforce's cache-only key
        # service mandates it.
        rsa_cert = RSA.importKey(key_consumer_cert)
        cek_cipher = PKCS1_OAEP.new(rsa_cert, hashAlgo=SHA1)
        cek_ciphertext = cek_cipher.encrypt(priv_cek)
        b64_cek_ciphertext = base64.urlsafe_b64encode(cek_ciphertext)
    except ValueError as exc:
        # Check RSAES-OAEP encryption boundaries:
        # The asymmetric encryption system RSAES-OAEP cannot encrypt plaintext
        # of arbitrary length and is bound to (n-2)-2|H|, where n represents
        # the RSA modulus (in bytes) and |H| the output size (in bytes) of the
        # chosen hashing algorithm. Thus, this check should be implemented.
        ret = b''

        logger.error('Failed to encrypt cek, encryption boundary violated: %s',
                     exc)

        trace_exit(inspect.currentframe(), ret)
        return ret
    except Exception as exc:
        ret = b''
        logger.error('Failed to encrypt cek: %s', exc)
        trace_exit(inspect.currentframe(), ret)
        return ret

    trace_exit(inspect.currentframe(), b64_cek_ciphertext)
    return b64_cek_ciphertext


def _encrypt_dek_with_cek(priv_cek: bytearray, initialization_vector: bytes,
                          priv_dek: bytearray,
Esempio n. 16
0
def __jwt_validator(priv_jwt: str) -> None:
    """Validates the Authorization header and the JWT."""
    trace_enter(inspect.currentframe())

    parts = priv_jwt.split()

    if parts[0].lower() != 'bearer':
        err_msg = 'Authorization header must start with "Bearer"'
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)

    if len(parts) == 1:
        err_msg = 'Token not found'
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)

    if len(parts) > 2:
        err_msg = 'Authorization header must be "Bearer token".'
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)

    token = parts[1]

    token_parts = token.split('.')

    if len(token_parts) != 3:
        err_msg = 'JWT token does not match format "header.payload.signature".'
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)

    b64_header = token_parts[0]
    payload = token_parts[1]

    # TODO: how to validate?
    # signature = token_parts[2]

    try:
        # fix padding required by python base64 module: + '==='
        b64_header = b64_header + '==='

        header = base64.b64decode(b64_header).decode()
        header = json.loads(header)
    except Exception as exc:
        err_msg = f'JWT protected header must be base64 encoded json: {exc}.'
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422) from exc

    if ('typ' not in header) or ('alg' not in header) or ('kid' not in header):
        err_msg = 'JWT protected header must include "typ", "alg" and "kid".'
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)

    try:
        # fix padding required by python base64 module: + '==='
        payload = payload + '==='

        payload = base64.b64decode(payload).decode()
        payload = json.loads(payload)
    except Exception as exc:
        err_msg = f'JWT payload must be base64 encoded json: {exc}.'
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422) from exc

    if ('sub' not in payload) or ('iss' not in payload) or \
            ('aud' not in payload):
        err_msg = 'JWT payload must include "sub", "iss" & "aud" claim.'
        logger.error(err_msg)
        trace_exit(inspect.currentframe(), err_msg)
        raise ValidationError(err_msg, status_code=422)
Esempio n. 17
0
def get_dynamic_secret(tenant: str, key: str, key_version: str,
                       priv_jwt_token: str) -> bytearray:
    """Fetches dynamic secret from Vault."""

    trace_enter(inspect.currentframe())

    vault_transit_path = config.get_vault_transit_path(tenant)

    if not vault_transit_path:
        logger.error('Failed to load Vault transit path for tenant "%s"',
                     tenant)
        return bytearray()

    client = __get_vault_client(tenant)
    if not client:
        ret = bytearray()
        logger.error('Failed to get Vault client.')
        trace_exit(inspect.currentframe(), ret)
        return ret

    client = __authenticate_vault_client(client, tenant, priv_jwt_token)
    if not client:
        ret = bytearray()
        logger.error('Failed to authenticate Vault client.')
        trace_exit(inspect.currentframe(), ret)
        return ret

    # fetch most recent key version of key
    try:
        response = client.secrets.transit.read_key(
            name=key, mount_point=vault_transit_path)
    except hvac.exceptions.Forbidden as exc:
        ret = bytearray()
        logger.error('Insufficient permissions to access secret: %s', exc)
        trace_exit(inspect.currentframe(), ret)
        return ret

    if key_version == 'latest':
        try:
            key_version = response['data']['latest_version']
        except KeyError as exc:
            ret = bytearray()

            logger.error(
                'Failed to access key version in Vault key read '
                'response: %s', exc)

            trace_exit(inspect.currentframe(), ret)
            return ret

    # fetch key
    try:
        response = client.secrets.transit.export_key(
            name=key,
            key_type='encryption-key',
            version=key_version,
            mount_point=vault_transit_path)
    except Exception as exc:
        ret = bytearray()
        logger.error('Failed to export key: %s ', exc)
        trace_exit(inspect.currentframe(), ret)
        return ret

    try:
        bytearray_b64_key = bytearray(
            response['data']['keys'][str(key_version)].encode())

        bytearray_key = bytearray(base64.b64decode(bytearray_b64_key))
    except Exception as exc:
        ret = bytearray()

        logger.error('Failed to get key material from response: %s', exc)

        trace_exit(inspect.currentframe(), ret)
        return ret

    # delete sensitive data from memory
    del bytearray_b64_key[:]

    trace_exit(inspect.currentframe(), CAMOUFLAGE_SIGN)
    return bytearray_key