diff --git a/resources/certificates/ecdsa_p256.crt b/resources/certificates/ecdsa_p256.crt new file mode 100644 index 00000000..21e1e5b0 --- /dev/null +++ b/resources/certificates/ecdsa_p256.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBlTCCATugAwIBAgIUWHvNlHco/kXqoHF43TZjwf4F2QwwCgYIKoZIzj0EAwIw +IDEeMBwGA1UEAwwVWW91ciBTQU1MIEVudGl0eSBOYW1lMB4XDTI2MTAzMDIwNTY0 +NloXDTM2MTAyNzIwNTY0NlowIDEeMBwGA1UEAwwVWW91ciBTQU1MIEVudGl0eSBO +YW1lMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBm8ZNWwOcn0uQC0FygoZh6JD +jDDVQIzWiXtlL7Hh8RdiVxdPGelxSuSxickteg6ARgFkA/ITHy3T3grfgR/pL6NT +MFEwHQYDVR0OBBYEFGcpb3FbXgL0mMVfTS/fvIg9h+ibMB8GA1UdIwQYMBaAFGcp +b3FbXgL0mMVfTS/fvIg9h+ibMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID +SAAwRQIgPPpZwGxAgjE18UnxZuYG9gGRBqVN/ZIws3b6yxaSVGoCIQCwapQL096j ++OfTgLNl3u7qppz6k+gJiPA+lTmPxoEdKg== +-----END CERTIFICATE----- diff --git a/resources/keys/ecdsa_p256.key b/resources/keys/ecdsa_p256.key new file mode 100644 index 00000000..ee529448 --- /dev/null +++ b/resources/keys/ecdsa_p256.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgr4ZWUPx/u4A/pnZA +ys8xMPj8pW2SZt9Cp3rrOK6fKkOhRANCAAQGbxk1bA5yfS5ALQXKChmHokOMMNVA +jNaJe2UvseHxF2JXF08Z6XFK5LGJyS16DoBGAWQD8hMfLdPeCt+BH+kv +-----END PRIVATE KEY----- diff --git a/resources/keys/ecdsa_p256.pub b/resources/keys/ecdsa_p256.pub new file mode 100644 index 00000000..8644db1c --- /dev/null +++ b/resources/keys/ecdsa_p256.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBm8ZNWwOcn0uQC0FygoZh6JDjDDV +QIzWiXtlL7Hh8RdiVxdPGelxSuSxickteg6ARgFkA/ITHy3T3grfgR/pLw== +-----END PUBLIC KEY----- diff --git a/src/Alg/Signature/ECDSA.php b/src/Alg/Signature/ECDSA.php new file mode 100644 index 00000000..dee16a6c --- /dev/null +++ b/src/Alg/Signature/ECDSA.php @@ -0,0 +1,39 @@ +getMaterial(), $this->digest, $this->sig_padding)) { throw new OpenSSLException('Cannot sign data'); } + return $signature; } @@ -308,6 +309,12 @@ public function setOAEPParams(?string $digestAlg = null, ?string $mgf = null): v public function setSignaturePadding(string $algId): void { $padding = match ($algId) { + C::SIG_ECDSA_SHA1 => 0, + C::SIG_ECDSA_SHA224 => 0, + C::SIG_ECDSA_SHA256 => 0, + C::SIG_ECDSA_SHA384 => 0, + C::SIG_ECDSA_SHA512 => 0, + C::SIG_RSA_PSS_SHA1 => OPENSSL_PKCS1_PSS_PADDING, C::SIG_RSA_PSS_SHA224 => OPENSSL_PKCS1_PSS_PADDING, C::SIG_RSA_PSS_SHA256 => OPENSSL_PKCS1_PSS_PADDING, diff --git a/src/Constants.php b/src/Constants.php index 3d68dae1..ca46c8bb 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -116,6 +116,19 @@ class Constants extends \SimpleSAML\XML\Constants public const string SIG_RSA_RIPEMD160 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-ripemd160'; + /** + * ECDSA Signature algorithms + */ + public const string SIG_ECDSA_SHA1 = 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1'; + + public const string SIG_ECDSA_SHA224 = 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224'; + + public const string SIG_ECDSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256'; + + public const string SIG_ECDSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384'; + + public const string SIG_ECDSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512'; + /** * HMAC Signature algorithms */ @@ -240,6 +253,15 @@ class Constants extends \SimpleSAML\XML\Constants self::BLOCK_ENC_AES256_GCM => 16, ]; + /** @var array */ + public static array $ECDSA_DIGESTS = [ + self::SIG_ECDSA_SHA1 => self::DIGEST_SHA1, + self::SIG_ECDSA_SHA224 => self::DIGEST_SHA224, + self::SIG_ECDSA_SHA256 => self::DIGEST_SHA256, + self::SIG_ECDSA_SHA384 => self::DIGEST_SHA384, + self::SIG_ECDSA_SHA512 => self::DIGEST_SHA512, + ]; + /** @var array */ public static array $RSA_DIGESTS = [ self::SIG_RSA_SHA1 => self::DIGEST_SHA1, diff --git a/src/TestUtils/PEMCertificatesMock.php b/src/TestUtils/PEMCertificatesMock.php index 7f602c04..42ee36aa 100644 --- a/src/TestUtils/PEMCertificatesMock.php +++ b/src/TestUtils/PEMCertificatesMock.php @@ -42,6 +42,12 @@ class PEMCertificatesMock public const string SELFSIGNED_PRIVATE_KEY = 'selfsigned.simplesamlphp.org.key'; + public const string ECDSA_CERTIFICATE = 'ecdsa_p256.crt'; + + public const string ECDSA_PUBLIC_KEY = 'ecdsa_p256.pub'; + + public const string ECDSA_PRIVATE_KEY = 'ecdsa_p256.key'; + public const string BROKEN_CERTIFICATE = 'broken.simplesamlphp.org.crt'; public const string BROKEN_PUBLIC_KEY = 'broken.simplesamlphp.org.pub'; diff --git a/src/XML/ds/SignatureMethod.php b/src/XML/ds/SignatureMethod.php index d94eff6e..845b3dda 100644 --- a/src/XML/ds/SignatureMethod.php +++ b/src/XML/ds/SignatureMethod.php @@ -51,6 +51,7 @@ public function __construct( Assert::oneOf( $Algorithm->getValue(), array_merge( + array_keys(C::$ECDSA_DIGESTS), array_keys(C::$RSA_DIGESTS), array_keys(C::$HMAC_DIGESTS), ), diff --git a/tests/XML/SignedElementTest.php b/tests/XML/SignedElementTest.php index 166e07f3..5dd8eefd 100644 --- a/tests/XML/SignedElementTest.php +++ b/tests/XML/SignedElementTest.php @@ -22,9 +22,11 @@ use SimpleSAML\XMLSecurity\XML\ds\X509Certificate as X509; use SimpleSAML\XMLSecurity\XML\ds\X509Data; +use function array_keys; use function base64_encode; use function chunk_split; use function dirname; +use function in_array; use function strval; use function trim; @@ -38,6 +40,9 @@ final class SignedElementTest extends TestCase /** @var \SimpleSAML\XMLSecurity\CryptoEncoding\PEM */ private PEM $certificate; + /** @var \SimpleSAML\XMLSecurity\CryptoEncoding\PEM */ + private PEM $ecdsa_certificate; + /** @var \SimpleSAML\XMLSecurity\CryptoEncoding\PEM */ private PEM $wrong_certificate; @@ -68,6 +73,10 @@ public function setUp(): void PEMCertificatesMock::getPlainCertificate(PEMCertificatesMock::SELFSIGNED_CERTIFICATE), ); + $this->ecdsa_certificate = PEM::fromString( + PEMCertificatesMock::getPlainCertificate(PEMCertificatesMock::ECDSA_CERTIFICATE), + ); + $this->wrong_certificate = PEM::fromString( PEMCertificatesMock::getPlainCertificate(PEMCertificatesMock::OTHER_CERTIFICATE), ); @@ -328,6 +337,41 @@ public function testSuccessfulVerifyingRsaPssSignature(): void } + /** + * Verify an ECDSA signature + */ + public function testSuccessfulVerifyingEcdsaSignature(): void + { + // sign using ECDSA_SHA256 + $customSignable = $this->sign(C::SIG_ECDSA_SHA256); + // No-op to _actually_ sign the element and set the signature-property. + $customSignable = CustomSignable::fromXML($customSignable->toXML()); + + $signature = $customSignable->getSignature(); + + $this->assertEquals( + C::SIG_ECDSA_SHA256, + $signature + ->getSignedInfo() + ->getSignatureMethod() + ->getAlgorithm() + ->getValue(), + ); + + $certificate = new X509Certificate($this->ecdsa_certificate); + + $factory = new SignatureAlgorithmFactory(); + $verifier = $factory->getAlgorithm( + C::SIG_ECDSA_SHA256, + $certificate->getPublicKey(), + ); + + $verified = $customSignable->verify($verifier); + + $this->assertInstanceOf(CustomSignable::class, $verified); + } + + /** * Negative: verify a RSA-PSS signature using PKCS#1 v1.5 */ @@ -382,9 +426,15 @@ private function sign(string $algorithm): CustomSignable $customSignable = CustomSignable::fromXML($xml->documentElement); - $private = PEMCertificatesMock::getPrivateKey( - PEMCertificatesMock::SELFSIGNED_PRIVATE_KEY, - ); + if (in_array($algorithm, array_keys(C::$ECDSA_DIGESTS))) { + $private = PEMCertificatesMock::getPrivateKey( + PEMCertificatesMock::ECDSA_PRIVATE_KEY, + ); + } else { + $private = PEMCertificatesMock::getPrivateKey( + PEMCertificatesMock::SELFSIGNED_PRIVATE_KEY, + ); + } $factory = new SignatureAlgorithmFactory(); $signer = $factory->getAlgorithm(