def validate_jwt(jwt): """ Check the incoming JWT and verify that it has all of the fields that we require. :param jwt a JSON Web Token as a string :return the JWT string :raise ValidationError if validation fails """ if not jwt: return None # Validate header. header = brkt_jwt.get_header(jwt) expected_fields = ['typ', 'alg', 'kid'] missing_fields = [f for f in expected_fields if f not in header] if missing_fields: raise ValidationError( 'Missing fields in token header: %s. Use the %s command ' 'to generate a valid token.' % (','.join(missing_fields), brkt_jwt.SUBCOMMAND_NAME)) # Validate payload. payload = brkt_jwt.get_payload(jwt) if not payload.get('jti'): raise ValidationError( 'Token payload does not contain the jti field. Use the %s ' 'command to generate a valid token.' % brkt_jwt.SUBCOMMAND_NAME) return jwt
def parse_timestamp(ts_string): """ Return a datetime that represents the given timestamp string. The string can be a Unix timestamp in seconds or an ISO 8601 timestamp. :raise ValidationError if ts_string is malformed """ now = int(time.time()) # Parse integer timestamp. m = re.match('\d+(\.\d+)?$', ts_string) if m: t = float(ts_string) if t < now: raise ValidationError( '%s is earlier than the current timestamp (%s).' % (ts_string, now)) return _timestamp_to_datetime(t) # Parse ISO 8601 timestamp. dt_now = _timestamp_to_datetime(now) try: dt = iso8601.parse_date(ts_string) except iso8601.ParseError: raise ValidationError( 'Timestamp "%s" must either be a Unix timestamp or in iso8601 ' 'format (2016-05-10T19:15:36Z).' % ts_string) if dt < dt_now: raise ValidationError( '%s is earlier than the current timestamp (%s).' % (ts_string, dt_now)) return dt
def _validate_guest_encrypted_ami(aws_svc, ami_id, encryptor_ami_id): """ Validate that this image was encrypted by Bracket by checking tags. :raise: ValidationError if validation fails :return: the Image object """ ami = _validate_ami(aws_svc, ami_id) # Is this encrypted by Bracket? tags = ami.tags expected_tags = (TAG_ENCRYPTOR, TAG_ENCRYPTOR_SESSION_ID, TAG_ENCRYPTOR_AMI) missing_tags = set(expected_tags) - set(tags.keys()) if missing_tags: raise ValidationError('%s is missing tags: %s' % (ami.id, ', '.join(missing_tags))) # See if this image was already encrypted by the given encryptor AMI. original_encryptor_id = tags.get(TAG_ENCRYPTOR_AMI) if original_encryptor_id == encryptor_ami_id: msg = '%s was already encrypted with Bracket Encryptor %s' % ( ami.id, encryptor_ami_id) raise ValidationError(msg) return ami
def read_private_key(pem_path): """ Read a private key from a PEM file. :return a brkt_cli.crypto.Crypto object :raise ValidationError if the file cannot be read or is malformed, or if the PEM does not represent a 384-bit ECDSA private key. """ key_format_err = ( 'Signing key must be a 384-bit ECDSA private key (NIST P-384)') try: with open(pem_path) as f: pem = f.read() if not brkt_cli.crypto.is_private_key(pem): raise ValidationError(key_format_err) password = None if brkt_cli.crypto.is_encrypted_key(pem): password = getpass.getpass('Encrypted private key password: '******'Unable to load signing key from %s', pem_path) raise ValidationError('Unable to load signing key: %s' % e) log.debug('crypto.curve=%s', crypto.curve) if crypto.curve != brkt_cli.crypto.SECP384R1: raise ValidationError(key_format_err) return crypto
def _parse_endpoint(endpoint): host_port_pattern = r'([^:]+):(\d+)$' m = re.match(host_port_pattern, endpoint) if not m: raise ValidationError(error_msg) host = m.group(1) port = int(m.group(2)) if not util.validate_dns_name_ip_address(host): raise ValidationError('Invalid hostname: ' + host) return host, port
def check_args(values, gce_svc): if not gce_svc.network_exists(values.network): raise ValidationError("Network provided does not exist") if values.encryptor_image: if values.bucket != 'prod': raise ValidationError( "Please provided either an encryptor image or an image bucket") if not values.token: raise ValidationError('Must provide a token') brkt_env = brkt_cli.brkt_env_from_values(values) brkt_cli.check_jwt_auth(brkt_env, values.token)
def make_instance_config(values=None, brkt_env=None, mode=INSTANCE_CREATOR_MODE): log.debug('Creating instance config with %s', brkt_env) brkt_config = {} if not values: return InstanceConfig(brkt_config, mode) if brkt_env: add_brkt_env_to_brkt_config(brkt_env, brkt_config) if values.token: brkt_config['identity_token'] = values.token if values.ntp_servers: brkt_config['ntp_servers'] = values.ntp_servers if mode in (INSTANCE_CREATOR_MODE, INSTANCE_UPDATER_MODE): brkt_config['status_port'] = (values.status_port or encryptor_service.ENCRYPTOR_STATUS_PORT) ic = InstanceConfig(brkt_config, mode) # Now handle the args that cause files to be added to brkt-files proxy_config = get_proxy_config(values) if proxy_config: ic.add_brkt_file('proxy.yaml', proxy_config) if 'ca_cert' in values and values.ca_cert: if mode != INSTANCE_CREATOR_MODE: raise ValidationError( 'Can only specify ca-cert for instance in Creator mode') if not values.brkt_env: raise ValidationError( 'Must specify brkt-env when specifying ca-cert.') try: with open(values.ca_cert, 'r') as f: ca_cert_data = f.read() except IOError as e: raise ValidationError(e) try: x509.load_pem_x509_certificate(ca_cert_data, default_backend()) except Exception as e: raise ValidationError('Error validating CA cert: %s' % e) domain = get_domain_from_brkt_env(brkt_env) ca_cert_filename = 'ca_cert.pem.' + domain ic.add_brkt_file(ca_cert_filename, ca_cert_data) return ic
def _validate_ami(aws_svc, ami_id): """ :return the Image object :raise ValidationError if the image doesn't exist """ try: image = aws_svc.get_image(ami_id) except EC2ResponseError, e: if e.error_code.startswith('InvalidAMIID'): raise ValidationError('Could not find ' + ami_id + ': ' + e.error_code) else: raise ValidationError(e.error_message)
def validate_tag_key(key): """ Verify that the key is a valid EC2 tag key. :return: the key if it's valid :raises ValidationError if the key is invalid """ if len(key) > 127: raise ValidationError( 'Tag key cannot be longer than 127 characters' ) if key.startswith('aws:'): raise ValidationError( 'Tag key cannot start with "aws:"' ) return key
def validate_tag_value(value): """ Verify that the value is a valid EC2 tag value. :return: the value if it's valid :raises ValidationError if the value is invalid """ if len(value) > 255: raise ValidationError( 'Tag value cannot be longer than 255 characters' ) if value.startswith('aws:'): raise ValidationError( 'Tag value cannot start with "aws:"' ) return value
def validate_image_name(name): """ Verify that the name is a valid GCE image name. Return the name if it is valid. : raises ValidationError if name is invalid """ if not (name and len(name) <= 63): raise ValidationError('Image name may be at most 63 characters') m = re.match(r'[a-z0-9\-]*[a-z0-9]$', name) if not m: raise ValidationError( "GCE image must be lower case letters, numbers and hyphens " "and must end with a lower case letter or a number") return name
def check_jwt_auth(brkt_env, jwt): """ Authenticate with Yeti using the given JWT and make sure that the associated public key is registered with the account. :param brkt_env a BracketEnvironment object :param jwt a JWT string :raise ValidationError if the token fails auth or the public key is not registered with the given account """ validate_jwt(jwt) uri = 'https://%s:%d/api/v1/customer/self' % (brkt_env.public_api_host, brkt_env.public_api_port) log.debug('Validating token against %s', uri) request = urllib2.Request(uri, headers={'Authorization': 'Bearer %s' % jwt}) try: response = urllib2.urlopen(request, timeout=10.0) log.debug('Server returned %d', response.getcode()) except urllib2.HTTPError as e: if e.code == 401: raise ValidationError('Unauthorized token.') else: # Unexpected server response. Log a warning and continue, so # that we don't unnecessarily interrupt the encryption process. log.debug('Server response: %s', e.msg) log.warn('Unable to validate token. Server returned error %d. ' 'Use --no-validate to disable validation.' % e.code) except IOError: if log.isEnabledFor(logging.DEBUG): log.exception('') log.warn( 'Unable to validate token against %s. Use --no-validate to ' 'disable validation.', uri)
def _get_encryptor_ami(region_name, pv=False): """ Read the list of AMIs from the AMI endpoint and return the AMI ID for the given region. :raise ValidationError if the region is not supported :raise BracketError if the list of AMIs cannot be read """ if pv: bucket_url = PV_ENCRYPTOR_AMIS_URL else: bucket_url = ENCRYPTOR_AMIS_URL log.debug('Getting encryptor AMI list from %s', bucket_url) r = urllib2.urlopen(bucket_url) if r.getcode() not in (200, 201): raise BracketError('Getting %s gave response: %s' % (bucket_url, r.text)) resp_json = json.loads(r.read()) ami = resp_json.get(region_name) if not ami: regions = resp_json.keys() raise ValidationError('Encryptor AMI is only available in %s' % ', '.join(regions)) return ami
def parse_brkt_env(brkt_env_string): """ Parse the --brkt-env value. The value is in the following format: api_host:port,hsmproxy_host:port :return: a BracketEnvironment object :raise: ValidationError if brkt_env is malformed """ error_msg = ('--brkt-env value must be in the following format: ' '<api-host>:<api-port>,<hsm-proxy-host>:<hsm-proxy-port>') endpoints = brkt_env_string.split(',') if len(endpoints) != 2: raise ValidationError(error_msg) def _parse_endpoint(endpoint): host_port_pattern = r'([^:]+):(\d+)$' m = re.match(host_port_pattern, endpoint) if not m: raise ValidationError(error_msg) host = m.group(1) port = int(m.group(2)) if not util.validate_dns_name_ip_address(host): raise ValidationError('Invalid hostname: ' + host) return host, port be = BracketEnvironment() (be.api_host, be.api_port) = _parse_endpoint(endpoints[0]) # set public api host based on the same prefix assumption # service-domain makes. Hopefully we'll remove brkt-env # soon and we can get rid of it be.public_api_host = be.api_host.replace('yetiapi', 'api') (be.hsmproxy_host, be.hsmproxy_port) = _parse_endpoint(endpoints[1]) return be
def _unset_option(self, opt): """Unset the specified option""" try: self.parsed_config.unset_option(opt) except InvalidOptionError: raise ValidationError('Error: unknown option "%s".' % (opt,)) self._write_config() return 0
def _write_file(path, content): try: with open(path, 'w') as f: f.write(content) except IOError as e: if log.isEnabledFor(logging.DEBUG): log.exception('Unable to write to %s', path) raise ValidationError('Unable to write to %s: %s' % (path, e))
def _get_option(self, opt): try: val = self.parsed_config.get_option(opt) except InvalidOptionError: raise ValidationError('Error: unknown option "%s".' % (opt,)) if val: self.stdout.write("%s\n" % (val,)) return 0
def validate_ntp_servers(ntp_servers): if ntp_servers is None: return for server in ntp_servers: if not validate_dns_name_ip_address(server): raise ValidationError( 'Invalid ntp-server %s specified. ' 'Should be either a host name or an IPv4 address' % server)
def command_diag(values): nonce = util.make_nonce() aws_svc = aws_service.AWSService( nonce, retry_timeout=values.retry_timeout, retry_initial_sleep_seconds=values.retry_initial_sleep_seconds) log.debug('Retry timeout=%.02f, initial sleep seconds=%.02f', aws_svc.retry_timeout, aws_svc.retry_initial_sleep_seconds) if values.snapshot_id and values.instance_id: raise ValidationError("Only one of --instance-id or --snapshot-id " "may be specified") if not values.snapshot_id and not values.instance_id: raise ValidationError("--instance-id or --snapshot-id " "must be specified") if values.validate: # Validate the region before connecting. region_names = [r.name for r in aws_svc.get_regions()] if values.region not in region_names: raise ValidationError( 'Invalid region %s. Supported regions: %s.' % (values.region, ', '.join(region_names))) aws_svc.connect(values.region, key_name=values.key_name) default_tags = {} default_tags.update(brkt_cli.parse_tags(values.tags)) aws_svc.default_tags = default_tags if values.validate: if values.key_name: aws_svc.get_key_pair(values.key_name) if values.instance_id: _validate_log_instance(aws_svc, values.instance_id) _validate_subnet_and_security_groups(aws_svc, values.subnet_id, values.security_group_ids) else: log.info('Skipping validation.') diag.diag(aws_svc, instance_id=values.instance_id, snapshot_id=values.snapshot_id, ssh_keypair=values.key_name) return 0
def validate_image_name(name): """ Verify that the name is a valid EC2 image name. Return the name if it's valid. :raises ValidationError if the name is invalid """ if not (name and 3 <= len(name) <= 128): raise ValidationError( 'Image name must be between 3 and 128 characters long') m = re.match(r'[A-Za-z0-9()\[\] ./\-\'@_]+$', name) if not m: raise ValidationError( "Image name may only contain letters, numbers, spaces, " "and the following characters: ()[]./-'@_" ) return name
def _validate_region(aws_svc, region_name): """ Check that the specified region is a valid AWS region. :raise ValidationError if the region is invalid """ region_names = [r.name for r in aws_svc.get_regions()] if region_name not in region_names: raise ValidationError('%s does not exist. AWS regions are %s' % (region_name, ', '.join(region_names)))
def _validate_encryptor_ami(aws_svc, ami_id): """ Validate that the image exists and is a Bracket encryptor image. :raise ValidationError if validation fails """ image = _validate_ami(aws_svc, ami_id) if 'brkt-avatar' not in image.name: raise ValidationError('%s (%s) is not a Bracket Encryptor image' % (ami_id, image.name)) return None
def get_domain_from_brkt_env(brkt_env): """Return the domain string from the api_host in the brkt_env. """ api_host = brkt_env.api_host if not api_host: raise ValidationError('api_host endpoint not in brkt_env: %s' % brkt_env) # Consider the domain to be everything after the first '.' in # the api_host. return api_host.split('.', 1)[1]
def get_header(jwt_string): """ Return all of the headers in the given JWT. :return the headers as a dictionary """ try: return jwt.get_unverified_header(jwt_string) except jwt.InvalidTokenError as e: if log.isEnabledFor(logging.DEBUG): log.exception('') raise ValidationError('Unable to decode token: %s' % e)
def get_payload(jwt_string): """ Return the payload of the given JWT. :return the payload as a dictionary """ try: return jwt.decode(jwt_string, verify=False) except jwt.InvalidTokenError as e: if log.isEnabledFor(logging.DEBUG): log.exception('') raise ValidationError('Unable to decode token: %s' % e)
def validate_images(gce_svc, encrypted_image_name, encryptor, guest_image, image_project=None): # check that image to be updated exists if not gce_svc.image_exists(guest_image, image_project): raise ValidationError('Guest image or image project invalid') # check that encryptor exists if encryptor and not gce_svc.image_exists(encryptor): raise ValidationError( 'Encryptor image %s does not exist. Encryption failed.' % encryptor) # check that there is no existing image named encrypted_image_name if gce_svc.image_exists(encrypted_image_name): raise ValidationError( 'An image already exists with name %s. Encryption Failed.' % encrypted_image_name)
def _parse_proxies(*proxy_host_ports): """ Parse proxies specified on the command line. :param proxy_host_ports: a list of strings in "host:port" format :return: a list of Proxy objects :raise: ValidationError if any of the items are malformed """ proxies = [] for s in proxy_host_ports: m = re.match(r'([^:]+):(\d+)$', s) if not m: raise ValidationError('%s is not in host:port format' % s) host = m.group(1) port = int(m.group(2)) if not util.validate_dns_name_ip_address(host): raise ValidationError('%s is not a valid hostname' % host) proxy = Proxy(host, port) proxies.append(proxy) return proxies
def _base64_decode_json(base64_string): """ Decode the given base64 string, and return the parsed JSON as a dictionary. :raise ValidationError if either the base64 or JSON is malformed """ try: json_string = util.urlsafe_b64decode(base64_string) return json.loads(json_string) except (TypeError, ValueError) as e: raise ValidationError('Unable to decode %s as JSON: %s' % (base64_string, e))
def parse_name_value(name_value): """ Parse a string in NAME=VALUE format. :return: a tuple of name, value :raise: ValidationError if name_value is malformed """ m = re.match(r'([^=]+)=(.+)', name_value) if not m: raise ValidationError('%s is not in the format NAME=VALUE' % name_value) return m.group(1), m.group(2)
def _validate_guest_ami(aws_svc, ami_id): """ Validate that we are able to encrypt this image. :return: the Image object :raise: ValidationError if the AMI id is invalid """ image = _validate_ami(aws_svc, ami_id) if TAG_ENCRYPTOR in image.tags: raise ValidationError('%s is already an encrypted image' % ami_id) # Amazon's API only returns 'windows' or nothing. We're not currently # able to detect individual Linux distros. if image.platform == 'windows': raise ValidationError('Windows is not a supported platform') if image.root_device_type != 'ebs': raise ValidationError('%s does not use EBS storage.' % ami_id) if image.hypervisor != 'xen': raise ValidationError('%s uses hypervisor %s. Only xen is supported' % (ami_id, image.hypervisor)) return image