simplesamlphp/xml-security

SimpleSAMLphp library for XML Security

v0.3.3 2021-07-27 07:31 UTC

README

Build Status Scrutinizer Code Quality Coverage Status

This library implements XML signatures and encryption. It provides an extensible interface that allows you to use your own signature and encryption implementations, and deals with everything else to sign, verify, encrypt and decrypt your XML objects. It is built on top of the xml-common library, which provides you with a standard API to create PHP objects from their XML representation, as well as producing XML from your objects. The aim of the library is to provide a secure, yet flexible implementation of the xmldsig and xmlenc standards in PHP.

The library provides two main ways to use it, one API for signed XML documents, and another for encrypted ones. Additionally, the lower level APIs are available to implement those operations yourself if needed, although we highly recommend using the main interfaces.

Signature API

The XML signature API consists mainly of two interfaces:

  • SimpleSAML\XMLSecurity\XML\SignableElementInterface
  • SimpleSAML\XMLSecurity\XML\SignedElementInterface

In general, both should be used together. The former signals that an object can be signed (and as such mandates the implementation of a sign() method in the object), while the latter indicates that an object is already signed and allows the verification of its signature by means of verify() method.

Since the signature API is provided via PHP interfaces, your objects need to implement those interfaces. For your convenience, each interface is accompanied by two traits with the actual implementation for the PHP interfaces:

  • SimpleSAML\XMLSecurity\XML\SignableElementTrait
  • SimpleSAML\XMLSecurity\XML\SignedElementTrait

Both declare an abstract getId() method that you will have to implement, since only you know what attribute is declared in your XML objects to act as an xml:id.

The two interfaces mentioned extend from a third one, SimpleSAML\XMLSecurity\XML\CanonicalizableElementInterface. This interface ensures that your XML objects can be properly canonicalized, so that if they were created from an actual XML document, it will be possible to restore that original XML document from your object. Again, a SimpleSAML\XMLSecurity\XML\CanonicalizableElementTrait is provided for your convenience. This trait implements the canonicalization for you, and ensures that your object can be serialized and later unserialized, but in exchange requires you to implement a getOriginalXML() method. This means you will have to keep the original XML that created your object, if any.

In general, your code should implement both main interfaces and use the traits. The bare minimum you will need to do to add XML signature capabilities to your objects will look like the following:

namespace MyNamespace;

use DOMElement;
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
use SimpleSAML\XMLSecurity\XML\SignableElementTrait;
use SimpleSAML\XMLSecurity\XML\SignedElementInterface;
use SimpleSAML\XMLSecurity\XML\SignedElementTrait;

class MyObject implements SignableElementInterface, SignedElementInterface
{
    use SignableElementTrait;
    use SignedElementTrait;
    
    ...
    
    public function getId(): ?string
    {
        // return the ID of your object
    }
    
    
    protected function getOriginalXML(): DOMElement
    {
        // return the original XML, if any, or the XML generated by your object
    }
}

However, we strongly recommend your XML objects to build on top of the API provided by xml-common. That way, you should probably have an abstract class to declare your namespace and namespace prefix:

namespace MyNamespace;

use SimpleSAML\XML\AbstractXMLElement;

abstract class AbstractMyNSElement extends AbstractXMLElement
{
    public const NS = 'my:namespace';
    
    public const NS_PREFIX = 'prefix';
}

Then your object can extend from that:

namespace MyNamespace;

use DOMElement;
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
use SimpleSAML\XMLSecurity\XML\SignableElementTrait;
use SimpleSAML\XMLSecurity\XML\SignedElementInterface;
use SimpleSAML\XMLSecurity\XML\SignedElementTrait;

class MyObject extends AbstractMyNSElement implements SignableElementInterface, SignedElementInterface
{
    use SignableElementTrait;
    use SignedElementTrait;
    
    ...
    
    public function getId(): ?string
    {
        // return the ID of your object
    }
    
    
    protected function getOriginalXML(): DOMElement
    {
        // return the original XML, if any, or the XML generated by your object
    }
    
    
    public static function fromXML(DOMElement $xml): object
    {
        // build an instance of your object based on an XML document representing it
    }
    
    
    public function toXML(DOMElement $parent = null): DOMElement
    {
        // build an XML representation of your object
    }
}

Have a look at the CustomSignable class provided with the tests in this repository to get an idea of how a working implementation could look like.

When dealing with XML signatures, you typically need to provide two things: the signature algorithm you want to use and a key. Depending on the algorithm, one type of key or another would be suitable. For that reason, this library introduces the concept of a SignatureAlgorithm, which is a given instance of an algorithm with a key associated. SignatureAlgorithms can be used then as signers (when signing an object) and verifiers (when used to verify a signature). This interface, together with the ones provided for key material and signature backends, will allow you to sign and verify signatures without much effort.

Signing

If you want to sign an object representing an XML document, the SignableElementTrait provides you with a doSign() method that you can use for your convenience. This method takes the XML document you want to sign, and returns another document result of applying all signature transforms to the input. The signer implementation to use will be obtained from the $signer property of the trait, which in turn will be set by the sign() method it provides as well. After the XML is signed successfully, doSign() will not only return the signed version of it, but also populate the $signature property with a Signature object.

If you are using the API provided by xml-common, you would typically implement support for signing your objects like this:

    public function toXML(DOMElement $parent = null): DOMElement
    {
        if ($this->signer !== null) {
            $signedXML = $this->doSign($this->getMyXML());
            $signedXML->insertBefore($this->signature->toXML($signedXML), $signedXML->firstChild);
            return $signedXML;
        }

        return $this->getMyXML();
    }

Note that you will need to implement a mechanism to obtain the actual DOMElement to sign. It could be a method itself, as depicted in this example, or it could be stored in a class property.

At this point, your object is ready to be signed. You just need to create a signer, pass it to sign(), and create the XML representation (which will do the actual signing) by calling toXML():

use SimpleSAML\XMLSecurity\Constants;
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
use SimpleSAML\XMLSecurity\Key\PrivateKey;

$key = PrivateKey::fromFile('/path/to/key.pem');
$signer = (new SignatureAlgorithmFactory())->getAlgorithm(Constants::SIG_RSA_SHA256, $key);
$myObject->sign($signer);
$signedXML = $myObject->toXML();

That's it, you have signed your first object!

Now, you can customize your signatures as much as you want. For example, you can add the X509 certificate corresponding your private key to it, and specify the canonicalization algorithm to use:

use SimpleSAML\XMLSecurity\XML\ds\KeyInfo;
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
use SimpleSAML\XMLSecurity\XML\ds\X509Data

...

$keyInfo = new KeyInfo(
    [
        new X509Data(
            [
                new X509Certificate($base64EncodedCertificateData)
            ]
        )
    ]
);
$customSignable->sign($signer, Constants::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo);

...

If you are planning on embedding your signed object inside a larger XML document, make sure to give it an unique identifier. Your object will need to generate an XML with an ID attribute (of type xml:id) holding the identifier of the element, and the getId() method must return that very same identifier.

Verifying

In order to verify signed objects, the SignedElementInterface provides you with the following methods:

  • getId(): retrieves the unique identifier of the object.
  • getSignature(): retrieves the signature of the object as a SimpleSAML\XMLSecurity\XML\ds\Signature object.
  • getValidatingKey(): retrieves the key the signature has been verified with.
  • isSigned(): tells whether the object is in fact signed or not.
  • verify(): verifies the signature of the object.

If your class has implemented support for signing its objects, and you are implementing the SignedElementInterface and using the SignedElementTrait, support for verifying the signatures comes out of the box.

The process for verifying a signature is similar to the one of creating one. You will need to instantiate a signature verifier with some key material and a signature algorithm, and use it to verify the signature itself:

use SimpleSAML\XMLSecurity\Constants;
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;

$verifier = (new SignatureAlgorithmFactory())->getAlgorithm(
    $myObject->getSignature()->getSignedInfo()->getSignatureMethod()->getAlgorithm(),
    new X509Certificate($pemEncodedCertificate) 
);
$verified = $myObject->verify($verifier);

⚠️ WARNING

Note the $verified variable returned by verify(). The method does not return a boolean value to tell you if the signature was verified or not. Instead, if it fails to verify, an exception will be thrown. Its return value then is an object of the same class of your original object ($myObject), only that it is built based on the XML document whose signature has been verified. It is very important that you use only objects built based on a verified signature. Otherwise, any possible issue during the signature process could leave you with a tampered object whose signature doesn't really verify.

There is one alternative way to verify signatures. If the signature itself contains the key we can use to verify it (namely, an X509 certificate), then we can call verify() without passing a verifier to it, and check that the key used to verify the signature matches the one we expect:

use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;

$trustedCertificate = new X509Certificate($pemEncodedCertificate);
$verified = $myObject->verify();

if ($verified->getValidatingKey() === $trustedCertificate) {
    // signature verified with a trusted certificate
}

This last usage pattern is more convenient since you don't have to create a verifier, although it forces you to remember that you need to check the key used to verify the signature.

Encryption API

Not available yet.

Extending the library

Not available yet.

Keys for testing purposes

All encrypted keys use '1234' as passphrase.

The following keys are available:

  • signed - A CA-signed certificate
  • other - Another CA-signed certificate
  • selfsigned - A self-signed certificate
  • broken - A file with a broken PEM-structure (all spaces are removed from the headers)
  • corrupted - This looks like a proper certificate (every first & last character of every line has been swapped)
  • expired - This CA-signed certificate expires the moment it is generated