def test_exceeds_maximum_length(): path = 'a' * 2028 with pytest.raises(ArgumentError) as exception: SpiffeId.parse('spiffe://example.org/{}'.format(path)) assert str(exception.value) == 'SPIFFE ID: maximum length is 2048 bytes.'
def test_fetch_x509_svids_success(mocker): WORKLOAD_API_CLIENT._spiffe_workload_api_stub.FetchX509SVID = mocker.Mock( return_value=iter([ workload_pb2.X509SVIDResponse(svids=[ workload_pb2.X509SVID( spiffe_id='spiffe://example.org/service', x509_svid=CHAIN1, x509_svid_key=KEY1, ), workload_pb2.X509SVID( spiffe_id='spiffe://example.org/service2', x509_svid=CHAIN2, x509_svid_key=KEY2, ), ]) ])) svids = WORKLOAD_API_CLIENT.fetch_x509_svids() assert len(svids) == 2 svid1 = svids[0] assert svid1.spiffe_id() == SpiffeId.parse('spiffe://example.org/service') assert len(svid1.cert_chain()) == 2 assert isinstance(svid1.leaf(), Certificate) assert isinstance(svid1.private_key(), ec.EllipticCurvePrivateKey) svid2 = svids[1] assert svid2.spiffe_id() == SpiffeId.parse('spiffe://example.org/service2') assert len(svid2.cert_chain()) == 1 assert isinstance(svid2.leaf(), Certificate) assert isinstance(svid2.private_key(), ec.EllipticCurvePrivateKey)
def test_fetch_x509_context_success(mocker): federated_bundles = dict() federated_bundles['domain.test'] = _FEDERATED_BUNDLE WORKLOAD_API_CLIENT._spiffe_workload_api_stub.FetchX509SVID = mocker.Mock( return_value=iter( [ workload_pb2.X509SVIDResponse( svids=[ workload_pb2.X509SVID( spiffe_id='spiffe://example.org/service', x509_svid=_CHAIN1, x509_svid_key=_KEY1, bundle=_BUNDLE, ), workload_pb2.X509SVID( spiffe_id='spiffe://example.org/service2', x509_svid=_CHAIN2, x509_svid_key=_KEY2, bundle=_BUNDLE, ), ], federated_bundles=federated_bundles, ) ] ) ) x509_context = WORKLOAD_API_CLIENT.fetch_x509_context() svids = x509_context.x509_svids() bundle_set = x509_context.x509_bundle_set() assert len(svids) == 2 svid1 = x509_context.default_svid() assert svid1.spiffe_id() == SpiffeId.parse('spiffe://example.org/service') assert len(svid1.cert_chain()) == 2 assert isinstance(svid1.leaf(), Certificate) assert isinstance(svid1.private_key(), ec.EllipticCurvePrivateKey) svid2 = x509_context.x509_svids()[1] assert svid2.spiffe_id() == SpiffeId.parse('spiffe://example.org/service2') assert len(svid2.cert_chain()) == 1 assert isinstance(svid2.leaf(), Certificate) assert isinstance(svid2.private_key(), ec.EllipticCurvePrivateKey) bundle = bundle_set.get_x509_bundle_for_trust_domain(TrustDomain('example.org')) assert bundle assert len(bundle.x509_authorities()) == 1 federated_bundle = bundle_set.get_x509_bundle_for_trust_domain( TrustDomain('domain.test') ) assert federated_bundle assert len(federated_bundle.x509_authorities()) == 1
def test_watch_x509_context_success(mocker): federated_bundles = {'domain.test': FEDERATED_BUNDLE} WORKLOAD_API_CLIENT._spiffe_workload_api_stub.FetchX509SVID = mocker.Mock( return_value=iter([ workload_pb2.X509SVIDResponse( svids=[ workload_pb2.X509SVID( spiffe_id='spiffe://example.org/service', x509_svid=CHAIN1, x509_svid_key=KEY1, bundle=BUNDLE, ), workload_pb2.X509SVID( spiffe_id='spiffe://example.org/service2', x509_svid=CHAIN2, x509_svid_key=KEY2, bundle=BUNDLE, ), ], federated_bundles=federated_bundles, ) ])) done = threading.Event() response_holder = ResponseHolder() WORKLOAD_API_CLIENT.watch_x509_context( lambda r: handle_success(r, response_holder, done), lambda e: handle_error(e, response_holder, done), retry_connect=True, ) done.wait(5) # add timeout to prevent test from hanging assert not response_holder.error x509_context = response_holder.success svid1 = x509_context.default_svid() assert svid1.spiffe_id() == SpiffeId.parse('spiffe://example.org/service') assert len(svid1.cert_chain()) == 2 assert isinstance(svid1.leaf(), Certificate) assert isinstance(svid1.private_key(), ec.EllipticCurvePrivateKey) svid2 = x509_context.x509_svids()[1] assert svid2.spiffe_id() == SpiffeId.parse('spiffe://example.org/service2') assert len(svid2.cert_chain()) == 1 assert isinstance(svid2.leaf(), Certificate) assert isinstance(svid2.private_key(), ec.EllipticCurvePrivateKey) bundle_set = x509_context.x509_bundle_set() bundle = bundle_set.get_x509_bundle_for_trust_domain( TrustDomain.parse('example.org')) assert bundle assert len(bundle.x509_authorities()) == 1
def test_parse_chain_and_ec_key(): chain_bytes = read_bytes('2-chain.pem') key_bytes = read_bytes('2-key.pem') x509_svid = X509Svid.parse(chain_bytes, key_bytes) expected_spiffe_id = SpiffeId.parse('spiffe://example.org/service') assert x509_svid.spiffe_id() == SpiffeId.parse( 'spiffe://example.org/service') assert len(x509_svid.cert_chain()) == 2 assert isinstance(x509_svid.leaf(), Certificate) assert isinstance(x509_svid.cert_chain()[1], Certificate) assert isinstance(x509_svid.private_key(), ec.EllipticCurvePrivateKey) assert _extract_spiffe_id(x509_svid.leaf()) == expected_spiffe_id
def parse_insecure(cls, token: str, expected_audience: List) -> 'JwtSvid': """Parses and validates a JWT-SVID token and returns an instance of a JwtSvid with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry from 'exp' claim. The JWT-SVID signature is not verified. Args: token: A token as a string that is parsed and validated. audience: Audience as a list of strings used to validate the 'aud' claim. Returns: An instance of JwtSvid with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry from 'exp' claim. Raises: ValueError: When the token is blank or cannot be parsed, or in case header is not specified or in case expected_audience is empty or if the SPIFFE ID in the 'sub' claim doesn't comply with the SPIFFE standard. InvalidAlgorithmError: In case specified 'alg' is not supported as specified by the SPIFFE standard. InvalidTypeError: If 'typ' is present in header but is not set to 'JWT' or 'JOSE'. InvalidClaimError: If a required claim ('exp', 'aud', 'sub') is not present in payload or expected_audience is not a subset of audience_claim. TokenExpiredError: If token is expired. InvalidTokenError: If token is malformed and fails to decode. """ if not token: raise ValueError(INVALID_INPUT_ERROR.format('token cannot be empty')) try: token_header = jwt.get_unverified_header(token) validator = JwtSvidValidator() validator.validate_header(token_header) claims = jwt.decode(token, options={'verify_signature': False}) validator.validate_claims(claims, expected_audience) spiffe_ID = SpiffeId.parse(claims['sub']) return JwtSvid(spiffe_ID, claims['aud'], claims['exp'], claims, token) except PyJWTError as err: raise InvalidTokenError(str(err))
def test_x509_source_get_default_x509_svid(mocker): mock_client_return_multiple_svids(mocker) x509_source = DefaultX509Source(WORKLOAD_API_CLIENT) x509_svid = x509_source.get_x509_svid() assert x509_svid.spiffe_id() == SpiffeId.parse( 'spiffe://example.org/service')
def _extract_spiffe_id(cert: Certificate) -> SpiffeId: ext = cert.extensions.get_extension_for_oid( x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) sans = ext.value.get_values_for_type(x509.UniformResourceIdentifier) if len(sans) == 0: raise InvalidLeafCertificateError( 'Certificate does not contain a SPIFFE ID in the URI SAN') return SpiffeId.parse(sans[0])
def test_x509_source_get_x509_svid_with_picker(mocker): mock_client_return_multiple_svids(mocker) x509_source = DefaultX509Source(WORKLOAD_API_CLIENT, picker=lambda svids: svids[1]) x509_svid = x509_source.get_x509_svid() assert x509_svid.spiffe_id() == SpiffeId.parse( 'spiffe://example.org/service2')
def parse_and_validate( cls, token: str, jwt_bundle: JwtBundle, audience: List[str] ) -> 'JwtSvid': """Parses and validates a JWT-SVID token and returns an instance of JwtSvid. The JWT-SVID signature is verified using the JWT bundle source. Args: token: A token as a string that is parsed and validated. jwt_bundle: An instance of JwtBundle that provides the JWT authorities to verify the signature. audience: A list of strings used to validate the 'aud' claim. Returns: An instance of JwtSvid with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry from 'exp' claim. Raises: JwtSvidError: When the token expired or the expiration claim is missing, when the algorithm is not supported, when the header 'kid' is missing, when the signature cannot be verified, or when the 'aud' claim has an audience that is not in the audience list provided as parameter. ValueError: When the token is blank or cannot be parsed. BundleNotFoundError: If the bundle for the trust domain of the spiffe id from the 'sub' cannot be found the jwt_bundle_source. AuthorityNotFoundError: If the authority cannot be found in the bundle using the value from the 'kid' header. InvalidTokenError: In case token is malformed and fails to decode. """ if not token: raise ValueError(INVALID_INPUT_ERROR.format('token cannot be empty')) if not jwt_bundle: raise ValueError(INVALID_INPUT_ERROR.format('jwt_bundle cannot be empty')) try: token_header = jwt.get_unverified_header(token) validator = JwtSvidValidator() validator.validate_header(token_header) signing_key = jwt_bundle.find_jwt_authority(token_header['kid']) claims = jwt.decode( token, algorithms=token_header['alg'], key=signing_key, audience=audience, options={ 'verify_signature': True, 'verify_aud': True, 'verify_exp': True, }, ) # TODO:validate required claims spiffe_ID = SpiffeId.parse(claims['sub']) return JwtSvid(spiffe_ID, claims['aud'], claims['exp'], claims, token) except PyJWTError as err: raise InvalidTokenError(str(err))
def test_parse_leaf_only_and_rsa_key(): chain_bytes = read_bytes('3-good-leaf-only.pem') key_bytes = read_bytes('3-key-pkcs8-rsa.pem') x509_svid = X509Svid.parse(chain_bytes, key_bytes) expected_spiffe_id = SpiffeId.parse('spiffe://example.org/workload-1') assert x509_svid.spiffe_id() == expected_spiffe_id assert len(x509_svid.cert_chain()) == 1 assert isinstance(x509_svid.leaf(), Certificate) assert isinstance(x509_svid.private_key(), rsa.RSAPrivateKey) assert _extract_spiffe_id(x509_svid.leaf()) == expected_spiffe_id
def test_watch_x509_context_raise_retryable_grpc_error_and_then_ok_response( mocker): mock_error_iter = mocker.MagicMock() mock_error_iter.__iter__.side_effect = ( yield_grpc_error_and_then_correct_x509_svid_response()) WORKLOAD_API_CLIENT._spiffe_workload_api_stub.FetchX509SVID = mocker.Mock( return_value=mock_error_iter) expected_error = FetchX509SvidError('StatusCode.DEADLINE_EXCEEDED') done = threading.Event() response_holder = ResponseHolder() WORKLOAD_API_CLIENT.watch_x509_context( lambda r: handle_success(r, response_holder, done), lambda e: assert_error(e, expected_error), True, ) done.wait(5) # add timeout to prevent test from hanging x509_context = response_holder.success svid1 = x509_context.default_svid() assert svid1.spiffe_id() == SpiffeId.parse('spiffe://example.org/service') assert len(svid1.cert_chain()) == 2 assert isinstance(svid1.leaf(), Certificate) assert isinstance(svid1.private_key(), ec.EllipticCurvePrivateKey) svid2 = x509_context.x509_svids()[1] assert svid2.spiffe_id() == SpiffeId.parse('spiffe://example.org/service2') assert len(svid2.cert_chain()) == 1 assert isinstance(svid2.leaf(), Certificate) assert isinstance(svid2.private_key(), ec.EllipticCurvePrivateKey) bundle_set = x509_context.x509_bundle_set() bundle = bundle_set.get_x509_bundle_for_trust_domain( TrustDomain.parse('example.org')) assert bundle assert len(bundle.x509_authorities()) == 1
def test_parse_with_all_chars(): # Go all the way through 255, which ensures we reject UTF-8 appropriately for i in range(0, 255): c = chr(i) # Don't test '/' since it is the delimiter between path segments if c == '/': continue path = '/path' + c if c in PATH_CHARS: spiffe_id = SpiffeId.parse('spiffe://trustdomain' + path) assert str(spiffe_id) == 'spiffe://trustdomain' + path else: with pytest.raises(SpiffeIdError) as exception: SpiffeId.parse('spiffe://trustdomain' + path) assert ( str(exception.value) == 'Path segment characters are limited to letters, numbers, dots, dashes, and underscores.' ) td = 'spiffe://trustdomain' + c if c in TD_CHARS: spiffe_id = SpiffeId.parse(td) assert str(spiffe_id) == td else: with pytest.raises(SpiffeIdError) as exception: SpiffeId.parse(td) assert ( str(exception.value) == 'Trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores.' )
def test_load_from_der_files(): chain_path = _TEST_CERTS_PATH.format('1-chain.der') key_path = _TEST_CERTS_PATH.format('1-key.der') x509_svid = X509Svid.load(chain_path, key_path, serialization.Encoding.DER) expected_spiffe_id = SpiffeId.parse('spiffe://example.org/service') assert x509_svid.spiffe_id() == expected_spiffe_id assert len(x509_svid.cert_chain()) == 2 assert isinstance(x509_svid.leaf(), Certificate) assert isinstance(x509_svid.cert_chain()[1], Certificate) assert isinstance(x509_svid.private_key(), ec.EllipticCurvePrivateKey) assert _extract_spiffe_id(x509_svid.leaf()) == expected_spiffe_id
def test_fetch_jwt_svid_aud(mocker): spiffe_id = 'spiffe://test.com/my_service' jwt_svid = create_jwt(spiffe_id=spiffe_id) WORKLOAD_API_CLIENT._spiffe_workload_api_stub.FetchJWTSVID = mocker.Mock( return_value=workload_pb2.JWTSVIDResponse( svids=[workload_pb2.JWTSVID(svid=jwt_svid, )])) svid = WORKLOAD_API_CLIENT.fetch_jwt_svid(audiences=DEFAULT_AUDIENCE) utc_time = timegm(datetime.datetime.utcnow().utctimetuple()) assert svid.spiffe_id == SpiffeId.parse(spiffe_id) assert svid.token == jwt_svid assert svid.claims['aud'] == DEFAULT_AUDIENCE assert int(svid.expiry) > utc_time
def test_validate_jwt_svid(mocker): audience = 'spire' spiffe_id = 'spiffe://test.com/my_service' jwt_svid = create_jwt(audience=[audience], spiffe_id=spiffe_id) WORKLOAD_API_CLIENT._spiffe_workload_api_stub.ValidateJWTSVID = mocker.Mock( return_value=workload_pb2.ValidateJWTSVIDResponse(spiffe_id=spiffe_id, )) svid = WORKLOAD_API_CLIENT.validate_jwt_svid(token=jwt_svid, audience=audience) assert svid.spiffe_id == SpiffeId.parse(spiffe_id) assert svid.token == jwt_svid assert svid.claims['aud'] == [audience] assert svid.audience == [audience]
def test_save_chain_and_rsa_key_as_der(tmpdir): chain_bytes = read_bytes('3-good-leaf-only.pem') key_bytes = read_bytes('3-key-pkcs8-rsa.pem') # create the X509Svid to be saved x509_svid = X509Svid.parse(chain_bytes, key_bytes) # temp files to store the certs and private_key chain_der_file = tmpdir.join('chain.der') key_der_file = tmpdir.join('key.der') x509_svid.save(chain_der_file, key_der_file, serialization.Encoding.DER) # now load the saved svid, and check that everything was stored correctly saved_svid = X509Svid.load(chain_der_file, key_der_file, serialization.Encoding.DER) expected_spiffe_id = SpiffeId.parse('spiffe://example.org/workload-1') assert saved_svid.spiffe_id() == expected_spiffe_id assert len(saved_svid.cert_chain()) == 1 assert isinstance(saved_svid.leaf(), Certificate) assert isinstance(saved_svid.private_key(), rsa.RSAPrivateKey) assert _extract_spiffe_id(saved_svid.leaf()) == expected_spiffe_id
def test_save_chain_and_ec_key_as_pem(tmpdir): chain_bytes = read_bytes('2-chain.pem') key_bytes = read_bytes('2-key.pem') # create the X509Svid to be saved x509_svid = X509Svid.parse(chain_bytes, key_bytes) # temp files to store the certs and private_key chain_pem_file = tmpdir.join('chain.pem') key_pem_file = tmpdir.join('key.pem') x509_svid.save(chain_pem_file, key_pem_file, serialization.Encoding.PEM) # now load the saved svid, and check that everything was stored correctly saved_svid = X509Svid.load(chain_pem_file, key_pem_file, serialization.Encoding.PEM) expected_spiffe_id = SpiffeId.parse('spiffe://example.org/service') assert saved_svid.spiffe_id() == expected_spiffe_id assert len(saved_svid.cert_chain()) == 2 assert isinstance(saved_svid.leaf(), Certificate) assert isinstance(x509_svid.cert_chain()[1], Certificate) assert isinstance(saved_svid.private_key(), ec.EllipticCurvePrivateKey) assert _extract_spiffe_id(saved_svid.leaf()) == expected_spiffe_id
import pytest from cryptography import x509 from cryptography.hazmat.primitives import serialization from cryptography.x509 import Certificate from pyspiffe.spiffe_id.spiffe_id import SpiffeId from pyspiffe.utils.certificate_utils import ( parse_pem_certificates, parse_der_certificates, load_certificates_bytes_from_file, write_certificate_to_file, serialize_certificate, ) from pyspiffe.utils.exceptions import X509CertificateError _EXPECTED_SPIFFE_ID = SpiffeId.parse('spiffe://example.org/service') _TEST_CERTS_PATH = 'test/svid/x509svid/certs/{}' def test_parse_der_certificates(): certs_bytes = _read_bytes('1-chain.der') certs = parse_der_certificates(certs_bytes) assert len(certs) == 2 assert isinstance(certs[0], Certificate) assert isinstance(certs[1], Certificate) assert _extract_spiffe_id(certs[0]) == _EXPECTED_SPIFFE_ID def test_parse_pem_certificates():
import pytest from test.svid.test_utils import create_jwt, DEFAULT_AUDIENCE from pyspiffe.spiffe_id.spiffe_id import TrustDomain from pyspiffe.proto.spiffe import workload_pb2 from pyspiffe.spiffe_id.spiffe_id import SpiffeId from pyspiffe.workloadapi.default_jwt_source import DefaultJwtSource from pyspiffe.workloadapi.exceptions import JwtSourceError, FetchJwtSvidError from test.workloadapi.test_default_workload_api_client import WORKLOAD_API_CLIENT from pyspiffe.exceptions import ArgumentError from test.utils.utils import ( JWKS_1_EC_KEY, JWKS_2_EC_1_RSA_KEYS, ) SPIFFE_ID = SpiffeId.parse('spiffe://example.org/my_service') def mock_client_get_jwt_svid(mocker): jwt_svid = create_jwt(spiffe_id=str(SPIFFE_ID)) WORKLOAD_API_CLIENT._spiffe_workload_api_stub.FetchJWTSVID = mocker.Mock( return_value=workload_pb2.JWTSVIDResponse(svids=[ workload_pb2.JWTSVID( spiffe_id=str(SPIFFE_ID), svid=jwt_svid, ) ])) def mock_client_fetch_jwt_bundles(mocker):
def test_str_when_no_path(): spiffe_id = SpiffeId.parse('spiffe://domain.test') assert str(spiffe_id) == 'spiffe://domain.test'
def test_is_not_member_of(): spiffe_id = SpiffeId.parse('spiffe://domain.test/path/element') trust_domain = TrustDomain.parse('other.test') assert not spiffe_id.is_member_of(trust_domain)
def test_parse_spiffe_id_from_invalid(spiffe_id_str, expected): with pytest.raises(ArgumentError) as exception: SpiffeId.parse(spiffe_id_str) assert str(exception.value) == expected
def test_parse_spiffe_id_valid(spiffe_id_str, expected_trust_domain, expected_path): spiffe_id = SpiffeId.parse(spiffe_id_str) assert spiffe_id.trust_domain() == expected_trust_domain assert spiffe_id.path() == expected_path
def _extract_spiffe_id(cert: Certificate) -> SpiffeId: ext = cert.extensions.get_extension_for_oid( x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME) sans = ext.value.get_values_for_type(x509.UniformResourceIdentifier) return SpiffeId.parse(sans[0])
def parse_and_validate(cls, token: str, jwt_bundle: JwtBundle, audience: List[str]) -> 'JwtSvid': """Parses and validates a JWT-SVID token and returns an instance of JwtSvid. The JWT-SVID signature is verified using the JWT bundle source. Args: token: A token as a string that is parsed and validated. jwt_bundle: An instance of JwtBundle that provides the JWT authorities to verify the signature. audience: A list of strings used to validate the 'aud' claim. Returns: An instance of JwtSvid with a SPIFFE ID parsed from the 'sub', audience from 'aud', and expiry from 'exp' claim. Raises: JwtSvidError: When the token expired or the expiration claim is missing, when the algorithm is not supported, when the header 'kid' is missing, when the signature cannot be verified, or when the 'aud' claim has an audience that is not in the audience list provided as parameter. ArgumentError: When the token is blank or cannot be parsed. BundleNotFoundError: If the bundle for the trust domain of the spiffe id from the 'sub' cannot be found the jwt_bundle_source. AuthorityNotFoundError: If the authority cannot be found in the bundle using the value from the 'kid' header. InvalidTokenError: In case token is malformed and fails to decode. """ if not token: raise ArgumentError( INVALID_INPUT_ERROR.format('token cannot be empty')) if not jwt_bundle: raise ArgumentError( INVALID_INPUT_ERROR.format('jwt_bundle cannot be empty')) try: header_params = jwt.get_unverified_header(token) validator = JwtSvidValidator() validator.validate_header(header_params) key_id = header_params.get('kid') signing_key = jwt_bundle.get_jwt_authority(key_id) if not signing_key: raise AuthorityNotFoundError(key_id) public_key_pem = signing_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ).decode('UTF-8') claims = jwt.decode( token, algorithms=header_params.get('alg'), key=public_key_pem, audience=audience, options={ 'verify_signature': True, 'verify_aud': True, 'verify_exp': True, }, ) spiffe_id = SpiffeId.parse(claims.get('sub', None)) return JwtSvid(spiffe_id, claims['aud'], claims['exp'], claims, token) except PyJWTError as err: raise InvalidTokenError(str(err)) except ArgumentError as value_err: raise InvalidTokenError(str(value_err))
def test_maximum_length(): path = 'a' * 2027 spiffe_id = SpiffeId.parse('spiffe://example.org/{}'.format(path)) assert spiffe_id.trust_domain() == TrustDomain('example.org') assert spiffe_id.path() == '/' + path