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
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
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)
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.')
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
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
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))
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))
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
def test_str(self): self.assertEqual(str(SecurityError(self.ERROR)), 'Signature does not match.')
def test_repr(self): self.assertEqual(repr(SecurityError(self.ERROR)), "SecurityError('Signature does not match.')")
def test_error_attribute(self): self.assertIs(SecurityError(self.ERROR).error, self.ERROR)