Skip to content

Commit 4ef7477

Browse files
authored
Merge pull request #14 from veewee/client-certificates
Add shortcut key-store for combined PEM client certificates
2 parents 1d3c693 + fd48f14 commit 4ef7477

11 files changed

+305
-8
lines changed

.phive/phars.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<phive xmlns="https://phar.io/phive">
33
<phar name="psalm" version="^5.9.0" installed="5.9.0" location="./tools/psalm.phar" copy="true"/>
4-
<phar name="php-cs-fixer" version="^3.13.0" installed="3.13.0" location="./tools/php-cs-fixer.phar" copy="true"/>
4+
<phar name="php-cs-fixer" version="^3.30.0" installed="3.39.0" location="./tools/php-cs-fixer.phar" copy="true"/>
55
</phive>

README.md

+32-7
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,30 @@ $wsseMiddleware = new WsseMiddleware(
9797
);
9898
```
9999

100-
#### Signing a SOAP request with PKCS12 or X509 certificate.
100+
### Key stores
101101

102-
This is one of the most common implementation of WSS out there.
103-
You are granted a certificate by the soap service with which you need to fetch data.
102+
This package provides a couple of `Key` wrappers that can be used to pass private / public keys:
103+
104+
* `KeyStore\Certificate`: Contains a public X.509 certificate in PEM format.
105+
* `KeyStore\Key`: Contains a PKCS_8 private key in PEM format.
106+
* `KeyStore\ClientCertificate`: Contains both a public X.509 certificate and PKCS_8 private key in PEM format.
107+
108+
Example:
109+
110+
```php
111+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Certificate;
112+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\ClientCertificate;
113+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Key;
114+
115+
$privKey = Key::fromFile('security_token.priv')->withPassphrase('xxx'); // Regular private key (not wrapped in X509)
116+
$pubKey = Certificate::fromFile('security_token.pub'); // Public X509 cert
117+
118+
// or:
119+
120+
$bundle = ClientCertificate::fromFile('client-certificate.pem')->withPassphrase('xxx');
121+
$privKey = $bunlde->privateKey();
122+
$pubKey = $bunlde->publicCertificate();
123+
```
104124

105125
In case of a p12 certificate: convert it to a private key and public X509 certificate first:
106126

@@ -109,6 +129,11 @@ openssl pkcs12 -in your.p12 -out security_token.pub -clcerts -nokeys
109129
openssl pkcs12 -in your.p12 -out security_token.priv -nocerts -nodes
110130
```
111131

132+
#### Signing a SOAP request with PKCS12 or X509 certificate.
133+
134+
This is one of the most common implementation of WSS out there.
135+
You are granted a certificate by the soap service with which you need to fetch data.
136+
112137
Next, you can configure the middleware like this:
113138

114139
```php
@@ -120,8 +145,8 @@ use Soap\Psr18WsseMiddleware\WSSecurity\KeyIdentifier;
120145
use Soap\Psr18WsseMiddleware\WsseMiddleware;
121146
use Soap\Psr18WsseMiddleware\WSSecurity\Entry;
122147

123-
$privKey = Key::fromFile('security_token.priv')->withPassphrase('xxx'); // Regular private key (not wrapped in X509)
124-
$pubKey = Certificate::fromFile('security_token.pub'); // Public X509 cert
148+
$privKey = Key::fromFile('security_token.priv')->withPassphrase('xxx');
149+
$pubKey = Certificate::fromFile('security_token.pub');
125150

126151
$wsseMiddleware = new WsseMiddleware(
127152
outgoing: [
@@ -162,7 +187,7 @@ use Soap\Psr18WsseMiddleware\WSSecurity\Entry;
162187
use VeeWee\Xml\Dom\Document;
163188
use function VeeWee\Xml\Dom\Locator\document_element;
164189

165-
$privKey = Key::fromFile('security_token.priv')->withPassphrase('xxx'); // Regular private key (not wrapped in X509)
190+
$privKey = Key::fromFile('security_token.priv')->withPassphrase('xxx');
166191

167192
// These are provided through the STS service.
168193
$samlAssertion = Document::fromXmlString(<<<EOXML
@@ -227,7 +252,7 @@ use Soap\Psr18WsseMiddleware\WSSecurity\KeyIdentifier;
227252
use Soap\Psr18WsseMiddleware\WsseMiddleware;
228253
use Soap\Psr18WsseMiddleware\WSSecurity\Entry;
229254

230-
$privKey = Key::fromFile('security_token.priv')->withPassphrase('xxx'); // Regular private key (not wrapped in X509)
255+
$privKey = Key::fromFile('security_token.priv')->withPassphrase('xxx'); // Private key
231256
$pubKey = Certificate::fromFile('security_token.pub'); // Public X509 cert
232257
$signKey = Certificate::fromFile('sign-key.pem'); // X509 cert for signing. Could be the same as $pubKey.
233258

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Psr18WsseMiddleware\OpenSSL\Exception;
5+
6+
use RuntimeException;
7+
8+
final class InvalidKeyException extends RuntimeException
9+
{
10+
public static function unableToReadPrivateKey(): self
11+
{
12+
return new self('Unable to read the format of the provided private key.');
13+
}
14+
15+
public static function unableToReadPublicKey(): self
16+
{
17+
return new self('Unable to read the format of the provided public key.');
18+
}
19+
}
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Psr18WsseMiddleware\OpenSSL\Parser;
5+
6+
use ParagonIE\HiddenString\HiddenString;
7+
use Soap\Psr18WsseMiddleware\OpenSSL\Exception\InvalidKeyException;
8+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Key;
9+
10+
final class PrivateKeyParser
11+
{
12+
public function __invoke(HiddenString $privateKey, ?HiddenString $password = null): Key
13+
{
14+
$parsed = '';
15+
$key = @openssl_pkey_get_private($privateKey->getString(), $password?->getString() ?: null);
16+
if (!$key) {
17+
throw InvalidKeyException::unableToReadPrivateKey();
18+
}
19+
20+
$result = @openssl_pkey_export($key, $parsed, $password?->getString() ?: null);
21+
if (!$result) {
22+
throw InvalidKeyException::unableToReadPrivateKey();
23+
}
24+
25+
return (new Key($parsed))->withPassphrase($password?->getString() ?? '');
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Psr18WsseMiddleware\OpenSSL\Parser;
5+
6+
use ParagonIE\HiddenString\HiddenString;
7+
use Soap\Psr18WsseMiddleware\OpenSSL\Exception\InvalidKeyException;
8+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Certificate;
9+
10+
final class X509PublicCertificateParser
11+
{
12+
public function __invoke(HiddenString $publicKey): Certificate
13+
{
14+
$parsed = '';
15+
$key = @openssl_x509_read($publicKey->getString());
16+
if (!$key) {
17+
throw InvalidKeyException::unableToReadPublicKey();
18+
}
19+
20+
$result = @openssl_x509_export($key, $parsed);
21+
if (!$result) {
22+
throw InvalidKeyException::unableToReadPublicKey();
23+
}
24+
25+
return new Certificate($parsed);
26+
}
27+
}

src/WSSecurity/KeyStore/Certificate.php

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
use ParagonIE\HiddenString\HiddenString;
77
use function Psl\File\read;
88

9+
/**
10+
* Contains a PEM representation of a public X.509 Certificate.
11+
*/
912
final class Certificate implements KeyInterface
1013
{
1114
private HiddenString $key;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Soap\Psr18WsseMiddleware\WSSecurity\KeyStore;
5+
6+
use ParagonIE\HiddenString\HiddenString;
7+
use Soap\Psr18WsseMiddleware\OpenSSL\Parser\PrivateKeyParser;
8+
use Soap\Psr18WsseMiddleware\OpenSSL\Parser\X509PublicCertificateParser;
9+
use function Psl\File\read;
10+
11+
/**
12+
* Contains a PEM bundle of both public X.509 Certificate and an (un)encrypted private key PKCS_8.
13+
*/
14+
final class ClientCertificate implements KeyInterface
15+
{
16+
private HiddenString $key;
17+
private HiddenString $passphrase;
18+
19+
public function __construct(string $key)
20+
{
21+
$this->key = new HiddenString($key);
22+
$this->passphrase = new HiddenString('');
23+
}
24+
25+
/**
26+
* @param non-empty-string $file
27+
*/
28+
public static function fromFile(string $file): self
29+
{
30+
return new self(read($file));
31+
}
32+
33+
/**
34+
* Parse out the private part of the bundled X509 certificate.
35+
*/
36+
public function privateKey(): Key
37+
{
38+
return (new PrivateKeyParser())($this->key, $this->passphrase);
39+
}
40+
41+
/**
42+
* Parse out the public part of the bundled X509 certificate.
43+
*/
44+
public function publicCertificate(): Certificate
45+
{
46+
return (new X509PublicCertificateParser())($this->key);
47+
}
48+
49+
/**
50+
* Provides the full content of the bundled pem certificate.
51+
*/
52+
public function contents(): string
53+
{
54+
return $this->key->getString();
55+
}
56+
57+
public function passphrase(): string
58+
{
59+
return $this->passphrase->getString();
60+
}
61+
62+
public function isCertificate(): bool
63+
{
64+
return true;
65+
}
66+
67+
public function withPassphrase(string $passphrase): self
68+
{
69+
$new = clone $this;
70+
$new->passphrase = new HiddenString($passphrase);
71+
72+
return $new;
73+
}
74+
}

src/WSSecurity/KeyStore/Key.php

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
use ParagonIE\HiddenString\HiddenString;
77
use function Psl\File\read;
88

9+
/**
10+
* Contains a PEM representation of an (un)encrypted private key (PKCS_8).
11+
*/
912
final class Key implements KeyInterface
1013
{
1114
private HiddenString $key;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace SoapTest\Psr18WsseMiddleware\Unit\OpenSSL\Parser;
5+
6+
use ParagonIE\HiddenString\HiddenString;
7+
use PHPUnit\Framework\TestCase;
8+
use Soap\Psr18WsseMiddleware\OpenSSL\Exception\InvalidKeyException;
9+
use Soap\Psr18WsseMiddleware\OpenSSL\Parser\PrivateKeyParser;
10+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Key;
11+
use function Psl\File\read;
12+
13+
final class PrivateKeyParserTest extends TestCase
14+
{
15+
16+
public function test_it_can_read_private_key(): void
17+
{
18+
$key = $this->createPrivateKey();
19+
$parser = new PrivateKeyParser();
20+
21+
$actual = $parser(new HiddenString($key));
22+
23+
static::assertInstanceOf(Key::class, $actual);
24+
static::assertSame($key, $actual->contents());
25+
}
26+
27+
public function test_it_can_read_encrypted_private_key(): void
28+
{
29+
$key = $this->createPrivateKey($passPhrase = 'password');
30+
$parser = new PrivateKeyParser();
31+
32+
static::assertStringContainsString('ENCRYPTED PRIVATE KEY', $key);
33+
34+
$actual = $parser(new HiddenString($key), new HiddenString($passPhrase));
35+
36+
static::assertInstanceOf(Key::class, $actual);
37+
static::assertSame($passPhrase, $actual->passphrase());
38+
static::assertStringContainsString('ENCRYPTED PRIVATE KEY', $actual->contents());
39+
}
40+
41+
public function test_it_can_read_from_bundle(): void
42+
{
43+
$bundle = FIXTURE_DIR . '/certificates/wsse-client-x509.pem';
44+
$parser = new PrivateKeyParser();
45+
46+
$actual = $parser(new HiddenString(read($bundle)));
47+
48+
static::assertInstanceOf(Key::class, $actual);
49+
static::assertSame('', $actual->passphrase());
50+
static::assertStringContainsString('PRIVATE KEY', $actual->contents());
51+
}
52+
53+
public function test_it_can_not_read_invalid_private_key(): void
54+
{
55+
$key = 'notavalidkey';
56+
$parser = new PrivateKeyParser();
57+
58+
$this->expectException(InvalidKeyException::class);
59+
$parser(new HiddenString($key));
60+
}
61+
62+
private function createPrivateKey(?string $passPhrase = null): string
63+
{
64+
$key = openssl_pkey_new();
65+
static::assertNotFalse($key);
66+
67+
$parsed = '';
68+
$result = openssl_pkey_export($key, $parsed, $passPhrase);
69+
static::assertNotFalse($result);
70+
71+
return $parsed;
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace SoapTest\Psr18WsseMiddleware\Unit\OpenSSL\Parser;
5+
6+
use ParagonIE\HiddenString\HiddenString;
7+
use PHPUnit\Framework\TestCase;
8+
use Soap\Psr18WsseMiddleware\OpenSSL\Exception\InvalidKeyException;
9+
use Soap\Psr18WsseMiddleware\OpenSSL\Parser\X509PublicCertificateParser;
10+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Certificate;
11+
use function Psl\File\read;
12+
13+
final class X509PublicCertificateParserTest extends TestCase
14+
{
15+
16+
public function test_it_can_read_public_x509_key(): void
17+
{
18+
$parser = new X509PublicCertificateParser();
19+
$file = FIXTURE_DIR . '/certificates/wsse-server-x509.crt';
20+
21+
$actual = $parser(new HiddenString(read($file)));
22+
23+
static::assertInstanceOf(Certificate::class, $actual);
24+
static::assertStringEqualsFile($file, $actual->contents());
25+
}
26+
27+
public function test_it_can_read_from_bundle(): void
28+
{
29+
$bundle = FIXTURE_DIR . '/certificates/wsse-client-x509.pem';
30+
$parser = new X509PublicCertificateParser();
31+
32+
$actual = $parser(new HiddenString(read($bundle)));
33+
34+
static::assertInstanceOf(Certificate::class, $actual);
35+
static::assertStringContainsString('CERTIFICATE', $actual->contents());
36+
}
37+
38+
public function test_it_can_not_read_invalid_certificate(): void
39+
{
40+
$key = 'notavalidkey';
41+
$parser = new X509PublicCertificateParser();
42+
43+
$this->expectException(InvalidKeyException::class);
44+
$parser(new HiddenString($key));
45+
}
46+
}

tools/php-cs-fixer.phar

75.9 KB
Binary file not shown.

0 commit comments

Comments
 (0)