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
Beispiel #2
0
    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))
Beispiel #3
0
def test_fetch_x509_svid_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,
                        ),
                    ]
                )
            ]
        )
    )

    svid = WORKLOAD_API_CLIENT.fetch_x509_svid()

    assert svid.spiffe_id() == SpiffeId.parse('spiffe://example.org/service')
    assert len(svid.cert_chain()) == 2
    assert isinstance(svid.leaf(), Certificate)
    assert isinstance(svid.private_key(), ec.EllipticCurvePrivateKey)
Beispiel #4
0
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])
Beispiel #5
0
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')
Beispiel #6
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')
Beispiel #7
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.
            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_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.'
            )
Beispiel #10
0
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_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
Beispiel #13
0
def test_of_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

        path1 = '/Path1' + c
        path2 = '/Path2' + c
        trust_domain = TrustDomain.parse('trustdomain')

        if c in PATH_CHARS:
            spiffe_id = SpiffeId.of(trust_domain, [path1, path2])
            assert str(spiffe_id) == 'spiffe://trustdomain' + path1 + path2
        else:
            with pytest.raises(SpiffeIdError) as exception:
                SpiffeId.of('spiffe://trustdomain', [path1, path2])
            assert (
                str(exception.value) ==
                'Path segment characters are limited to letters, numbers, dots, dashes, and underscores.'
            )
Beispiel #14
0
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():
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_str_when_no_path():
    spiffe_id = SpiffeId.parse('spiffe://domain.test')
    assert str(spiffe_id) == 'spiffe://domain.test'
def test_not_equal_spiffe_ids():
    trust_domain = TrustDomain.parse('trustdomain')
    spiffeid_1 = SpiffeId.from_segments(trust_domain, '/path1')
    spiffeid_2 = SpiffeId.from_segments(trust_domain, '/path2')
    assert spiffeid_1 != spiffeid_2
def test_of_empty_trust_domain():
    with pytest.raises(ArgumentError) as exception:
        SpiffeId.from_segments('', '/path')

    assert str(exception.value) == 'Trust domain is missing.'
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_equal_spiffe_id_with_multiple_paths():
    trust_domain = TrustDomain.parse('trustdomain')
    spiffeid_1 = SpiffeId.from_segments(trust_domain, ['/PATH1', '/PATH2'])
    spiffeid_2 = SpiffeId.from_segments(trust_domain, ['/PATH1', '/PATH2'])
    assert spiffeid_1 == spiffeid_2
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 test_of_trust_domain_and_invalid_segments(trust_domain, path_segments,
                                              expected_error):
    with pytest.raises(SpiffeIdError) as exception:
        SpiffeId.from_segments(trust_domain, path_segments)
    assert str(exception.value) == expected_error
Beispiel #26
0
def test_equal_spiffe_id():
    trust_domain = TrustDomain.parse('trustdomain')
    spiffeid_1 = SpiffeId.of(trust_domain, '/path1')
    spiffeid_2 = SpiffeId.of(trust_domain, '/path1')
    assert spiffeid_1 == spiffeid_2
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])
Beispiel #28
0
def test_trust_domain_none():
    with pytest.raises(ArgumentError) as exception:
        SpiffeId.of(None, '/path')

    assert str(exception.value) == 'Trust domain is missing.'
Beispiel #29
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_of_trust_domain_and_segments(trust_domain, path_segments,
                                      expected_spiffe_id):
    result = SpiffeId.from_segments(trust_domain, path_segments)
    assert str(result) == expected_spiffe_id