Beispiel #1
0
    def sign_assertion(self, key_file: str, cert_file: str,
                       signature_method: str, digest_method: str) -> bool:
        """
        Sign the SAML assertion.

        :return: `True` if the assertion element is present and has been signed, `False` otherwise.
        :raise SecurityError: If the assertion or response signature is already present.
        """
        assertion = self.document.getroot().find('./{}'.format(
            Q_NAMES['saml2:Assertion']))
        if assertion is None:
            # Failure responses don't contain <Assertion>.
            return False

        if self.assertion_signature is not None:
            raise SecurityError('The assertion signature is already present.')

        if self.response_signature is not None:
            # Signing assertion would invalidate the response signature.
            raise SecurityError(
                'Cannot sign assertion because response signature is already present.'
            )

        sign_xml_node(assertion, key_file, cert_file, signature_method,
                      digest_method,
                      int(self._assertion_issuer_element is not None))
        return True
Beispiel #2
0
    def get_light_token(self,
                        parameter_name: str,
                        issuer: str,
                        hash_algorithm: str,
                        secret: str,
                        lifetime: Optional[int] = None) -> LightToken:
        """
        Retrieve and verify a light token according to token settings.

        :param parameter_name: The name of HTTP POST parameter to get the token from.
        :param issuer: Token issuer.
        :param hash_algorithm: A hashlib hash algorithm.
        :param secret: A secret shared between communication parties.
        :param lifetime: Lifetime of the token (in minutes) until its expiration.
        :return: A decoded LightToken.
        :raise ParseError: If the token is malformed and cannot be decoded.
        :raise ValidationError: If the token can be decoded but model validation fails.
        :raise SecurityError: If the token digest or issuer is invalid or the token has expired.
        """
        encoded_token = self.request.POST.get(parameter_name,
                                              '').encode('utf-8')
        LOGGER.info('[#%r] Received encoded light token: %r', self.log_id,
                    encoded_token)
        token = LightToken.decode(encoded_token, hash_algorithm, secret)
        LOGGER.info('[#%r] Decoded light token: id=%r, issuer=%r', self.log_id,
                    token.id, token.issuer)
        if token.issuer != issuer:
            raise SecurityError('Invalid token issuer.')
        if lifetime and token.created + timedelta(
                minutes=lifetime) < datetime.now():
            raise SecurityError('Token has expired.')
        return token
def verify_xml_signatures(node: Element,
                          cert_file: str) -> List[SignatureInfo]:
    """
    Verify all XML signatures from the provided node.

    :param node: A XML subtree.
    :param cert_file: A path to the certificate file.
    :return: A list of signature details if there are any signatures, an empty list otherwise.
    :raise SecurityError: If any of the element references or signatures are invalid.
    """
    signature_info = []
    signatures = node.findall(".//{}".format(
        QName(XML_SIG_NAMESPACE, 'Signature')))
    if signatures:
        key = xmlsec.Key.from_file(cert_file,
                                   xmlsec.constants.KeyDataFormatCertPem)
        for sig_num, signature in enumerate(signatures, 1):
            # Find and register referenced elements
            referenced_elements = []
            ctx = xmlsec.SignatureContext()
            refs = signature.findall('./{}/{}'.format(
                QName(XML_SIG_NAMESPACE, 'SignedInfo'),
                QName(XML_SIG_NAMESPACE, 'Reference')))
            for ref_num, ref in enumerate(refs, 1):
                # ID is referenced in the URI attribute and prefixed with a hash.
                ref_id = ref.get(XML_ATTRIBUTE_URI)
                if ref_id is None or len(ref_id) < 2 or ref_id[0] != '#':
                    raise SecurityError(
                        'Signature {}, reference {}: Invalid id {!r}.'.format(
                            sig_num, ref_num, ref_id))
                ref_id = ref_id[1:]
                ref_elms = node.xpath('//*[@{}=\'{}\']'.format(
                    XML_ATTRIBUTE_ID, ref_id))
                if not ref_elms:
                    raise SecurityError(
                        'Signature {}, reference {}: Element with id {!r} not found.'
                        .format(sig_num, ref_num, ref_id))
                if len(ref_elms) > 1:
                    raise SecurityError(
                        'Signature {}, reference {}: Element with id {!r} occurs more than once.'
                        .format(sig_num, ref_num, ref_id))
                referenced_elements.append(ref_elms[0])
                # Unlike HTML, XML doesn't have a single standardized id so we need to tell xmlsec about our id.
                ctx.register_id(ref_elms[0], XML_ATTRIBUTE_ID, None)

            # Verify the signature
            try:
                ctx.key = key
                ctx.verify(signature)
            except xmlsec.Error:
                raise SecurityError('Signature {} is invalid.'.format(sig_num))

            signature_info.append(
                SignatureInfo(signature, tuple(referenced_elements)))
    return signature_info
Beispiel #4
0
    def verify_request(self, cert_file: str) -> None:
        """Verify XML signature of the whole request."""
        signature = self.request_signature
        if signature is None:
            raise SecurityError('Signature does not exist.')

        # We need to check not only that a valid signature exists but it must also reference the correct element.
        for valid_signature, references in verify_xml_signatures(self.document.getroot(), cert_file):
            if valid_signature is signature:
                if signature.getparent() not in references:
                    raise SecurityError('Signature does not reference parent element.')
                break
        else:
            raise SecurityError('Signature not found.')
def encrypt_xml_node(node: Element, cert_file: str, cipher: XmlBlockCipher, key_transport: XmlKeyTransport) -> None:
    """
    Encrypt a XML node.

    The node is removed from the parent element and replaced with <EncryptedData> element.

    :param node: A XML subtree.
    :param cert_file: A path to the certificate file.
    :param cipher: Encryption algorithm to use.
    :param key_transport: Key transport algorithm to use.
    """
    # Create a container without any XML namespace to force namespace declarations in the encrypted node.
    # The decrypted element may then exist as an independent XML document.
    container = Element('container')
    parent = node.getparent()
    node_index = parent.index(node)
    container.append(node)

    # Create a template for encryption. xmlsec.template functions don't cover all libxmlsec1 features yet.
    enc_data = SubElement(container,
                          '{%s}EncryptedData' % XML_ENC_NAMESPACE,
                          {'Type': xmlsec.constants.TypeEncElement},
                          nsmap={'xmlenc': XML_ENC_NAMESPACE})
    SubElement(enc_data, '{%s}EncryptionMethod' % XML_ENC_NAMESPACE, {'Algorithm': cipher.value})
    SubElement(enc_data, '{%s}CipherData' % XML_ENC_NAMESPACE)
    xmlsec.template.encrypted_data_ensure_cipher_value(enc_data)

    # Info about the generated encryption key.
    key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_data, ns='ds')
    enc_key = SubElement(key_info, '{%s}EncryptedKey' % XML_ENC_NAMESPACE)
    SubElement(enc_key, '{%s}EncryptionMethod' % XML_ENC_NAMESPACE, {'Algorithm': key_transport.value})
    SubElement(enc_key, '{%s}CipherData' % XML_ENC_NAMESPACE)
    xmlsec.template.encrypted_data_ensure_cipher_value(enc_key)

    # Info about the certificate.
    key_info = xmlsec.template.encrypted_data_ensure_key_info(enc_key, ns='ds')
    x509_data = xmlsec.template.add_x509_data(key_info)
    xmlsec.template.x509_data_add_certificate(x509_data)
    xmlsec.template.x509_data_add_issuer_serial(x509_data)

    # xmlsec library adds unnecessary newlines to the signature template.
    remove_extra_xml_whitespace(enc_data)

    # Encrypt with a newly generated key
    key_type, key_length = XML_KEY_INFO[cipher]
    manager = xmlsec.KeysManager()
    manager.add_key(xmlsec.Key.from_file(cert_file, xmlsec.constants.KeyDataFormatCertPem))
    ctx = xmlsec.EncryptionContext(manager)
    ctx.key = xmlsec.Key.generate(key_type, key_length, xmlsec.constants.KeyDataTypeSession)

    try:
        ctx.encrypt_xml(enc_data, node)
    except xmlsec.Error:
        raise SecurityError('XML encryption failed. Invalid certificate, invalid or unsupported encryption method.')

    # xmlsec library adds unnecessary tail newlines again, so we remove them.
    remove_extra_xml_whitespace(enc_data)

    # Insert the encrypted data in the position of the original node.
    parent.insert(node_index, enc_data)
Beispiel #6
0
    def _verify_and_remove_signature(self, signature: Optional[Element], cert_file: str) -> None:
        """Verify signature and remove it from document."""
        if signature is None:
            raise SecurityError('Signature does not exist.')

        # We need to check not only that a valid signature exists but it must also reference the correct element.
        for valid_signature, references in verify_xml_signatures(self.document.getroot(), cert_file):
            if valid_signature is signature:
                if signature.getparent() not in references:
                    raise SecurityError('Signature does not reference parent element.')

                # Remove the signature as further document manipulations might invalidate it.
                # E.g., decrypting an encrypted assertion invalidates signature of the whole response.
                signature.getparent().remove(signature)
                break
        else:
            raise SecurityError('Signature not found.')
Beispiel #7
0
    def get_light_request(self) -> LightRequest:
        """
        Get a light request.

        :return: A light request.
        :raise SecurityError: If the request is not found.
        """
        request = self.storage.pop_light_request(self.light_token.id)
        if request is None:
            raise SecurityError('Request not found in light storage.')
        return request
Beispiel #8
0
    def get_light_response(self) -> LightResponse:
        """
        Get a light response.

        :return: A light response.
        :raise SecurityError: If the response is not found.
        """
        response = self.storage.pop_light_response(self.light_token.id)
        if response is None:
            raise SecurityError('Response not found in light storage.')
        return response
Beispiel #9
0
    def sign_response(self, key_file: str, cert_file: str, signature_method: str, digest_method: str) -> None:
        """
        Sign the whole SAML response.

        :raise SecurityError: If the response signature is already present.
        """
        if self.response_signature is not None:
            raise SecurityError('The response signature is already present.')

        sign_xml_node(self.document.getroot(), key_file, cert_file, signature_method, digest_method,
                      int(self._response_issuer_element is not None))
Beispiel #10
0
    def sign_request(self, key_file: str, cert_file: str, signature_method: str, digest_method: str) -> None:
        """
        Sign the whole SAML request.

        :raise SecurityError: If the signature already exists.
        """
        if self.request_signature is not None:
            raise SecurityError('Request signature already exists.')

        sign_xml_node(self.document.getroot(), key_file, cert_file, signature_method, digest_method,
                      int(self._issuer_element is not None))
Beispiel #11
0
    def create_light_request(self, saml_issuer: str,
                             light_issuer: str) -> LightRequest:
        """
        Create a light request from a SAML request.

        :param saml_issuer: The expected issuer of the SAML request.
        :param light_issuer: The issuer of the light request.
        :return: A light request.
        """
        request = self.saml_request.create_light_request()
        # Verify the original issuer of the request.
        if not request.issuer or not hmac.compare_digest(
                request.issuer, saml_issuer):
            raise SecurityError('Invalid SAML request issuer: {!r}'.format(
                request.issuer))
        # Use our issuer specified in the generic eIDAS Node configuration.
        request.issuer = light_issuer
        LOGGER.info(
            '[#%r] Created light request: id=%r, issuer=%r, citizen_country=%r, origin_country=%r.',
            self.log_id, request.id, request.issuer,
            request.citizen_country_code, request.sp_country_code)
        return request
    def decode(cls,
               encoded_token: bytes,
               hash_algorithm: str,
               secret: str,
               max_size: int = 1024) -> 'LightToken':
        """
        Decode encoded token and check the validity and digest.

        :param encoded_token:  Base64 encoded token.
        :param hash_algorithm: One of hashlib hash algorithms.
        :param secret: The secret shared between the communicating parties.
        :param max_size: The maximal size of the encoded token.
        :return: Decoded and validated token.
        :raise ParseError: If the token is malformed and cannot be decoded.
        :raise ValidationError: If the token can be decoded but model validation fails.
        :raise SecurityError: If the token digest is invalid.
        """
        if max_size and len(encoded_token) > max_size:
            raise ParseError('Maximal token size exceeded.')
        data = b64decode(encoded_token, validate=True).decode('utf-8')
        try:
            issuer, token_id, timestamp, digest_base64 = data.split('|')
        except ValueError as e:
            raise ParseError('Token has wrong number of parts: {}.'.format(
                e.args[0]))

        token = LightToken(issuer=issuer,
                           id=token_id,
                           created=parse_eidas_timestamp(timestamp))
        token.validate()

        provided_digest = b64decode(digest_base64.encode('ascii'))
        valid_digest = token.digest(hash_algorithm, secret)
        if not hmac.compare_digest(valid_digest, provided_digest):
            raise SecurityError('Light token has invalid digest.')
        return token
Beispiel #13
0
 def test_str(self):
     self.assertEqual(str(SecurityError(self.ERROR)), 'Signature does not match.')
Beispiel #14
0
 def test_repr(self):
     self.assertEqual(repr(SecurityError(self.ERROR)), "SecurityError('Signature does not match.')")
Beispiel #15
0
 def test_error_attribute(self):
     self.assertIs(SecurityError(self.ERROR).error, self.ERROR)