simplesamlphp/xml-security

SimpleSAMLphp 的 XML 安全库

安装数量: 199,320

依赖者: 10

建议者: 0

安全性: 1

星标: 2

关注者: 7

分支: 2

开放性问题: 1

类型:simplesamlphp-xmlprovider


README

Build Status Scrutinizer Code Quality Coverage Status PHPStan Enabled

这个库实现了 XML 签名和加密。它提供了一个可扩展的接口,允许您使用自己的签名和加密实现,并处理所有其他签名、验证、加密和解密 XML 对象的操作。它建立在 xml-common 库之上,该库为您提供了从其 XML 表示形式创建 PHP 对象的标准 API,以及从您的对象生成 XML。该库的目的是在 PHP 中提供对 xmldsig 和 xmlenc 标准的安全且灵活的实现。

该库提供了两种主要的使用方式,一个是用于签名 XML 文档的 API,另一个是用于加密的 API。此外,如果需要,还提供了较低级别的 API 来实现这些操作,尽管我们强烈建议使用主要接口。

签名 API

XML 签名 API 主要由两个接口组成

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

通常,应该同时使用这两个接口。前者表示对象可以被签名(因此强制在对象中实现 sign() 方法),而后者表示对象已经被签名,并允许通过 verify() 方法验证其签名。

由于签名 API 是通过 PHP 接口提供的,因此您的对象需要实现这些接口。为了方便起见,每个接口都附带两个 trait,提供了 PHP 接口的实际实现

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

这两个 trait 都声明了一个抽象的 getId() 方法,您必须实现它,因为只有您知道在您的 XML 对象中声明了哪个属性用作 xml:id

前面提到的两个接口扩展了第三个接口,SimpleSAML\XMLSecurity\XML\CanonicalizableElementInterface。该接口确保您的 XML 对象可以被正确地规范化,因此如果它们是从实际的 XML 文档创建的,那么将有可能从您的对象恢复原始的 XML 文档。再次提供 SimpleSAML\XMLSecurity\XML\CanonicalizableElementTrait 以供方便。此 trait 为您实现了规范化,并确保您的对象可以被序列化和随后反序列化,但作为交换,您需要实现一个 getOriginalXML() 方法。这意味着您将不得不保留创建您的对象时使用的原始 XML,如果有的话。

通常,您的代码应实现这两个主要接口并使用 traits。要向对象添加 XML 签名功能所需的最小操作如下

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
    }
}

但是,我们强烈建议您的 XML 对象基于 xml-common 提供的 API 构建。这样,您可能需要一个抽象类来声明您的命名空间和命名空间前缀

namespace MyNamespace;

use SimpleSAML\XML\AbstractElement;

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

然后您的对象可以扩展该类

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
    }
}

查看此存储库中提供的测试用例中的 CustomSignable 类,以了解一个工作实现可能的样子。

处理XML签名时,通常需要提供两样东西:您想使用的签名算法和一个密钥。根据算法的不同,可能需要使用一种类型的密钥或另一种类型的密钥。因此,这个库引入了SignatureAlgorithm的概念,它是一个带有相关密钥的算法实例。SignatureAlgorithm可以用作签名者(在签名对象时使用)和验证者(在验证签名时使用)。这个接口,连同为密钥材料和签名后端提供的接口,将允许您轻松地进行签名和验证。

签名

如果您想签名表示XML文档的对象,SignableElementTrait提供了一个doSign()方法,您可以使用它来方便地签名。此方法接受您要签名的XML文档,并返回应用所有签名转换后的另一个文档。要使用的签名者实现将从特质的$signer属性中获取,该属性反过来将由它提供的sign()方法设置。XML签名成功后,doSign()不仅将返回签名的版本,还将使用Signature对象填充$signature属性。

如果您正在使用xml-common提供的API,您通常可以像这样实现对签名对象的支持

    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();
    }

请注意,您需要实现一种机制来获取要签名的实际DOMElement。它可以是方法本身,如本例所示,也可以存储在类属性中。

到目前为止,您的对象已准备好进行签名。您只需创建一个签名者,将其传递给sign(),然后通过调用toXML()创建XML表示(这将执行实际的签名)。

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

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

就这样,您已经签了第一个对象!

现在,您可以像您想要的那样自定义您的签名。例如,您可以将与您的私钥对应的X509证书添加到它中,并指定要使用的规范化算法。

use SimpleSAML\XMLSecurity\Constants as C;
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,
    C::C14N_EXCLUSIVE_WITHOUT_COMMENTS,
    $keyInfo
);

...

如果您计划将签名的对象嵌入到更大的XML文档中,请确保给它一个唯一的标识符。您的对象需要生成一个带有ID属性(类型为xml:id)的XML,该属性包含元素的标识符,并且getId()方法必须返回该标识符。

验证

为了验证签名的对象,SignedElementInterface提供了以下方法

  • getId():获取对象的唯一标识符。
  • getSignature():获取对象的签名,作为SimpleSAML\XMLSecurity\XML\ds\Signature对象。
  • getValidatingKey():获取验证签名的密钥。
  • isSigned():判断对象是否已签名。
  • verify():验证对象的签名。

如果您的类已实现对其对象的支持签名,并且您正在实现SignedElementInterface并使用SignedElementTrait,那么验证签名的支持将直接提供。

验证签名的过程与创建签名的过程类似。您需要实例化一个签名验证器,包含一些密钥材料和签名算法,并使用它来验证签名本身。

use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
use SimpleSAML\XMLSecurity\Key\PublicKey;

$verifier = (new SignatureAlgorithmFactory())->getAlgorithm(
    $myObject->getSignature()->getSignedInfo()->getSignatureMethod()->getAlgorithm(),
    PublicKey::fromFile('/path/to/public-key.pem')
);
$verified = $myObject->verify($verifier);

⚠️ 警告

请注意由 verify() 返回的 $verified 变量。此方法不会返回一个 boolean 值来告诉您签名是否已验证。相反,如果验证失败,将抛出异常。其返回值是一个与您原始对象($myObject)相同类别的对象,只是它是基于已验证签名的XML文档构建的。**非常重要,您只能使用基于已验证签名的对象**。否则,签名过程中可能出现的任何问题都可能导致您获得一个被篡改的对象,其签名实际上无法验证。

验证签名还有另一种方法。如果签名本身包含我们可以用来验证它的密钥(即X509证书),那么我们可以不传递验证器直接调用 verify(),并检查用于验证签名的密钥是否与我们预期的相匹配

use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;

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

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

这种最后的使用模式更为方便,因为您不必创建 验证器,尽管这强制您 记住您需要检查用于验证签名的密钥

加密API

XML加密API与它的签名对应物类似,同样由两个主要接口组成

  • SimpleSAML\XMLSecurity\XML\EncryptableElementInterface
  • SimpleSAML\XMLSecurity\XML\EncryptedElementInterface

就像在签名API中一样,前者表示一个对象可以被加密(因此需要实现一个 encrypt() 方法),而后者表示一个对象已经被加密(因此需要实现一个 decrypt() 方法)。但是与签名API有一个实质性的不同:您需要实现两个不同的类,一个用于您的对象本身,另一个用于您的加密对象。前者将实现 EncryptableElementInterface,而后者将是实现 EncryptedElementInterface 的那个。

同样,该库提供了一些特性,以便最大限度地减少您需要编写的代码量。这些特性包括

  • SimpleSAML\XMLSecurity\XML\EncryptableElementTrait
  • SimpleSAML\XMLSecurity\XML\EncryptedElementTrait

这两个特性在某种程度上是不对称的,因为 EncryptableElementTrait 实现了 encrypt() 方法,而 EncryptedElementTrait 没有实现其对应的 decrypt() 方法。这是因为对象的加密方式可能会有很大的不同,应用程序本身将是唯一知道应该如何进行加密的那个。尽管如此,我们提供了一个基本的默认实现,应该可以涵盖大多数用例。

就像数字签名一样,我们提供了演示加密功能的类。您可以查看测试中提供的 CustomSignable 类,以了解如何将加密添加到您的对象中,然后 EncryptedCustom 类将演示如何处理已经加密的对象。

解密对象

在XML加密中,当您有一个加密对象时,您通常会将其包裹在一个特定的元素中,该元素表示该对象是另一个对象的加密版本。您可以在加密对象中包含自己的元素和逻辑,但绝对最小要求是在其中包含一个 xenc:EncryptedData 元素。这意味着您将需要为您的加密对象创建类,并且它们必须实现 EncryptedElementInterface

然后最简单的方法是利用 EncryptedElementTrait,并且我们再次建议利用 simplesamlphp/xml-common 提供的XML对象框架。您只需要实现 decrypt() 方法以及特性所需的一些getter方法。

use SimpleSAML\XML\AbstractElement;
use SimpleSAML\XML\ElementInterface;
use SimpleSAML\XMLSecurity\Alg\Encryption\EncryptionAlgorithmInterface;
use SimpleSAML\XMLSecurity\Backend\EncryptionBackend;
use SimpleSAML\XMLSecurity\XML\EncryptedElementInterface;

class MyEncryptedObject extends AbstractElement
  implements EncryptedElementInterface
{
    use EncryptedElementTrait;
    
    
    public function getBlacklistedAlgorithms(): ?array
    {
        // return an array with the algorithms you don't want to allow to be used
    }
    
    
    public function getEncryptionBackend(): ?EncryptionBackend
    {
        // return the encryption backend you want to use,
        // or null if you are fine with the default
    }
    
    
    public function decrypt(EncryptionAlgorithmInterface $decryptor): MyObject 
    {
        // implement the actual decryption here with help from the library
    }
}

请注意,这里decrypt()返回的值是您自己的MyObject类。这意味着MyObject需要扩展SimpleSAML\XML\ElementInterface,这也是为什么decrypt()的实现留给了应用的原因之一。

当然,这个库的目的是为了使您的生活更轻松,因此您实际上不需要自己实现解密。以下decrypt()的实现将适用于大多数场景。

    public function decrypt(EncryptionAlgorithmInterface $decryptor): MyObject
    {
        return MyObject::fromXML(
            \SimpleSAML\XML\DOMDocumentFactory::fromString(
                $this->decryptData($decryptor)
            )->documentElement
        );
    }

那么,这里到底发生了什么?MyObject需要实现ElementInterface,对吗?这意味着它必须实现一个fromXML()静态方法,该方法根据传递给它的DOMElement对象创建一个新的类实例。这个DOMElement本身是通过DOMDocumentFactory类创建的,该类又使用了特性中提供的decryptData()方法的字符串结果。就这样,这可能就是您需要解密加密对象的所有内容!

但请记住,这只是一个最基本的使用场景。您的加密对象将需要如下所示:

<MyEncryptedObject>
  <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
    <xenc:EncryptionMethod Algorithm="..."/>
    <xenc:CipherData>
      <xenc:CipherValue>...</xenc:CipherValue>
    </xenc:CipherData>
  </xenc:EncryptedData>
</MyEncryptedObject>

如果您需要在内部添加更多元素、根元素的属性或其他任何内容,您将不得不调整实现以适应这些需求。在这种情况下,您可能需要为加密对象定义不同的构造函数,而不是特性提供的构造函数。您可以在利用特性中构造函数的同时定义自己的构造函数,通过重命名后者来实现。

use SimpleSAML\XML\AbstractElement;
use SimpleSAML\XMLSecurity\XML\EncryptedElementInterface;
use SimpleSAML\XMLSecurity\XML\xenc\EncryptedData;

class MyEncryptedObject extends AbstractElement
  implements EncryptedElementInterface
{
    use EncryptedElementTrait {
        __construct as constructor;
    }
    
    
    public function __construct(EncryptedData $encryptedData, ...)
    {
        $this->constructor($encryptedData);
        
        ...
    }
}

同样,如果您的加密方案与默认支持的两个方案都不兼容,您也将需要自己实现它。默认支持的两种加密方案是:

  • 共享密钥加密:双方共享一个秘密密钥,并使用它来加密和解密对象。这意味着<xenc:EncryptionMethod>元素将在Algorithm属性中指定一个块加密算法。然后,将创建一个针对该特定块加密的$decryptor对象,使用的密钥将是一个包含共享秘密作为密钥材料的SimpleSAML\XMLSecurity\Key\SymmetricKey对象。

  • 非对称加密:在这种情况下,使用公钥加密来加密对象。然而,公钥加密在计算上非常昂贵,因此与数字签名类似,我们生成一个随机的秘密或会话密钥,该密钥将用于使用块加密加密对象本身,然后我们将使用接收者的公钥加密该密钥。

    在这种情况下,$decryptor将实现一个密钥传输算法(这本身就是一个类似于RSA的非对称加密算法),而附加到它上的密钥将是一个包含接收者私钥的SimpleSAML\XMLSecurity\Key\PrivateKey对象。

    当使用非对称加密时,您的加密XML对象将类似于以下内容:

    <MyEncryptedObject>
      <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
        <xenc:EncryptionMethod Algorithm="BLOCK CIPHER ALGORITHM IDENTIFIER"/>
        <dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
          <xenc:EncryptedKey>
            <xenc:EncryptionMethod Algorithm="KEY TRANSPORT ALGORITHM IDENTIFIER"/>
            <xenc:CipherData>
              <xenc:CipherValue>...</xenc:CipherValue>
            </xenc:CipherData>
          </xenc:EncryptedKey>
        </dsig:KeyInfo>
        <xenc:CipherData>
          <xenc:CipherValue>...</xenc:CipherValue>
        </xenc:CipherData>
      </xenc:EncryptedData>
    </MyEncryptedObject> 

    最内层的<xenc:CipherValue>将包含加密的会话密钥,而最外层将包含加密的对象本身。

SimpleSAML\XMLSecurity\XML\EncryptedElementTrait::decryptData()方法能够处理这两种加密方案。如果您的应用使用其中任何一种,您可以根据前面的解释传递适当的decryptor来使用该方法。如果您使用共享密钥加密,则可以执行以下操作:

use SimpleSAML\XMLSecurity\Alg\Encryption\EncryptionAlgorithmFactory;
use SimpleSAML\XMLSecurity\Key\SymmetricKey;

$decryptor = (new EncryptionAlgorithmFactory())->getAlgorithm(
    $myEncryptedObject->getEncryptedData()->getEncryptionMethod()->getAlgorithm(),
    new SymmetricKey('MY SHARED SECRET')
);
$myObject = $myEncryptedObject->decrypt($decryptor);

⚠️ 警告

始终确保在<xenc:EncryptionMethod>元素中指定的算法是块加密算法。只有在那种情况下,库才会尝试使用共享密钥加密方案进行解密。SimpleSAML\XMLSecurity\Constants::$BLOCK_CIPHER_ALGORITHMS关联数组包含了这个库支持的块加密算法的所有标识符作为键。

或者,如果您的应用程序使用非对称加密,您将需要使用一个适当的解密器,该解密器是用您的私钥实例化的,以便解密您的对象

use SimpleSAML\XMLSecurity\Alg\KeyTransport\KeyTransportAlgorithmFactory;
use SimpleSAML\XMLSecurity\Key\PrivateKey;

$decryptor = (new KeyTransportAlgorithmFactory())->getAlgorithm(
    $myEncryptedObject->getEncryptedKey()->getEncryptionMethod()->getAlgorithm(),
    PrivateKey::fromFile('/path/to/private-key.pem')
);
$myObject = $myEncryptedObject->decrypt($decryptor);

最后一点:您可能已经注意到了,在使用EncryptedElementTrait时,您需要实现getBlacklistedAlgorithms()getEncryptionBackend()方法。这些方法是因为非对称加密支持所需的。由于库必须使用会话密钥创建一个块加密解密器,用户无法控制该解密器,因此无法直接指定要禁止的算法或要使用的加密后端。因此需要这两个方法,这将允许特质修改它将要构建的解密器中的任何参数。如果您只想使用默认值,只需实现它们以返回null即可。然而,如果您想要自定义接受的算法和/或要使用的后端,那么您必须在这些方法中返回所需的值。

加密对象

如果您想支持解密对象,那么您很可能首先想加密它们。这样做就像实现SimpleSAML\XMLSecurity\XML\EncryptableElementInterface一样简单。

use SimpleSAML\XML\AbstractElement;
use SimpleSAML\XMLSecurity\XML\EncryptableElementInterface;
use SimpleSAML\XMLSecurity\XML\EncryptableElementTrait;

class MyObject extends AbstractElement
  implements EncryptableElementInterface
{
    use EncryptableElementTrait;


    public function getBlacklistedAlgorithms(): ?array
    {
        // return an array with the algorithms you don't want to allow to be used
    }
    
    
    public function getEncryptionBackend(): ?EncryptionBackend
    {
        // return the encryption backend you want to use,
        // or null if you are fine with the default
    }
}

就是这样。简单,不是吗?在这种情况下,encrypt()方法是由SimpleSAML\XMLSecurity\XML\EncryptableElementTrait直接提供的,因为它的返回值始终是SimpleSAML\XMLSecurity\XML\xenc\EncryptedData对象。再次强调,您必须实现特质要求的一些抽象方法,以便告诉它支持的算法和在使用非对称加密时应该使用什么后端。

现在,我们只需要实际加密我们的对象。如果我们的应用程序使用共享密钥加密,我们只需要使用对称密钥创建一个适当的加密器。

use SimpleSAML\XMLSecurity\Constants as C;
use SimpleSAML\XMLSecurity\Alg\Encryption\EncryptionAlgorithmFactory;
use SimpleSAML\XMLSecurity\Key\SymmetricKey;

$encryptor = (new EncryptionAlgorithmFactory())->getAlgorithm(
    C::BLOCK_ENC_...,
    new SymmetricKey('MY SHARED SECRET')
);
$myEncryptedObject = $myObject->encrypt($encryptor)

相反,如果我们想使用非对称加密方案,我们的加密器需要实现一个密钥传输算法,并使用一个公钥。

use SimpleSAML\XMLSecurity\Constants as C;
use SimpleSAML\XMLSecurity\Alg\KeyTransport\KeyTransportAlgorithmFactory;
use SimpleSAML\XMLSecurity\Key\PublicKey;

$encryptor = (new KeyTransportAlgorithmFactory())->getAlgorithm(
    C::KEY_TRANSPORT_...,
    PublicKey::fromFile('/path/to/public-key.pem')
);
$myEncryptedObject = $myObject->encrypt($encryptor);

这将涵盖大多数需求。一般来说,非对称加密将适用于大多数应用程序,因为密钥管理是一个难以解决的问题。如果您需要实现这里支持的两种以外的不同加密方案,您必须自己实现encrypt()方法。

扩展库

目前不可用。

测试目的的密钥

所有加密密钥都使用'1234'作为密码短语。

以下密钥可用

  • 已签名 - 一个CA签名的证书
  • 其他 - 另一个CA签名的证书
  • 自签名 - 一个自签名证书
  • 损坏 - 一个具有损坏的PEM结构的文件(所有标题中的所有空格都已删除)
  • 损坏 - 这看起来像是一个合适的证书(每一行的第一个和最后一个字符已被交换)
  • 已过期 - 这份CA签名的证书在生成时即过期