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
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
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 ''
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
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
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
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
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)
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)
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)
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)
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
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
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
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,
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)
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