Skip to content

Commit cdcea47

Browse files
authored
Merge pull request #5 from veewee/soapui-style
Make it possible to configure WSSE like you do in SoapUI
2 parents a027b4d + 56964ef commit cdcea47

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3400
-203
lines changed

.github/workflows/analyzers.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
strategy:
88
matrix:
99
operating-system: [ubuntu-latest]
10-
php-versions: ['8.0', '8.1']
10+
php-versions: ['8.1']
1111
fail-fast: false
1212
name: PHP ${{ matrix.php-versions }} @ ${{ matrix.operating-system }}
1313
steps:

.github/workflows/code-style.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
strategy:
88
matrix:
99
operating-system: [ubuntu-latest]
10-
php-versions: ['8.0', '8.1']
10+
php-versions: ['8.1']
1111
fail-fast: false
1212
name: PHP ${{ matrix.php-versions }} @ ${{ matrix.operating-system }}
1313
steps:

.github/workflows/tests.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
strategy:
88
matrix:
99
operating-system: [ubuntu-latest]
10-
php-versions: ['8.0', '8.1']
10+
php-versions: ['8.1']
1111
fail-fast: false
1212
name: PHP ${{ matrix.php-versions }} @ ${{ matrix.operating-system }}
1313
steps:

.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">
3-
<phar name="psalm" version="^4.13.1" installed="4.13.1" location="./tools/psalm.phar" copy="true"/>
3+
<phar name="psalm" version="^4.13.1" installed="4.22.0" location="./tools/psalm.phar" copy="true"/>
44
<phar name="php-cs-fixer" version="^3.3.2" installed="3.3.2" location="./tools/php-cs-fixer.phar" copy="true"/>
55
</phive>

README.md

+192-26
Original file line numberDiff line numberDiff line change
@@ -38,43 +38,209 @@ $transport = Psr18Transport::createForClient(
3838

3939
### WsseMiddleware
4040

41-
If you ever had to implement Web Service Security (WSS / WSSE) manually, you know that it is a lot of work to get this one working.
42-
Luckily for you we created an opinionated WSSE middleware that can be used to sign your SOAP requests.
41+
Oh boy ... WS-Security ... can be a real pain !
42+
This package aims for being as flexible as possible and provides you the tools you need to correctly configure Web Service Security.
43+
The components are shaped based on the [WS-Security UI inside SoapUI](https://www.soapui.org/docs/soapui-projects/ws-security/).
44+
This enables you to configure everything the way your SOAP server wants you to!
45+
If you have a working config on SoapUI, you can transform it to PHP code by following the entries and their configurations.
46+
47+
*Usage:*
4348

44-
**Usage**
4549
```php
4650
use Http\Client\Common\PluginClient;
4751
use Soap\Psr18Transport\Psr18Transport;
4852
use Soap\Psr18WsseMiddleware\WsseMiddleware;
4953

50-
// Simple:
51-
$wsse = new WsseMiddleware('privatekey.pem', 'publickey.pyb');
54+
$transport = Psr18Transport::createForClient(
55+
new PluginClient($yourPsr18Client, [
56+
new WsseMiddleware([$entries])
57+
])
58+
);
59+
```
5260

53-
// With signed headers. E.g: in combination with WSA:
54-
$wsse = (new WsseMiddleware('privatekey.pem', 'publickey.pyb'))
55-
->withAllHeadersSigned();
61+
The WSSE middleware can be built out of multiple configurable entries:
5662

57-
// With configurable timestamp expiration:
58-
$wsse = (new WsseMiddleware('privatekey.pem', 'publickey.pyb'))
59-
->withTimestamp(3600);
63+
* BinarySecurityToken
64+
* Decryption
65+
* Encryption
66+
* SamlAssertion
67+
* Signature
68+
* Timestamp
69+
* Username
6070

61-
// With plain user token:
62-
$wsse = (new WsseMiddleware('privatekey.pem', 'publickey.pyb'))
63-
->withUserToken('username', 'password', false);
71+
Underneath, there are some common examples on how to configure the `$wsseMiddleware`.
6472

65-
// With digest user token:
66-
$wsse = (new WsseMiddleware('privatekey.pem', 'publickey.pyb'))
67-
->withUserToken('username', 'password', true);
73+
#### Adding a username and password
6874

69-
// With end-to-end encryption enabled:
70-
$wsse = (new WsseMiddleware('privatekey.pem', 'publickey.pyb'))
71-
->withEncryption('client-x509.pem')
72-
->withServerCertificateHasSubjectKeyIdentifier(true);
75+
Some services require you to add a username and optionally a password.
76+
This can be done with following middleware.
7377

74-
// Configure your PSR18 client:
75-
$transport = Psr18Transport::createForClient(
76-
new PluginClient($yourPsr18Client, [
77-
$wsse
78-
])
78+
```php
79+
use Soap\Psr18WsseMiddleware\WsseMiddleware;
80+
use Soap\Psr18WsseMiddleware\WSSecurity\Entry;
81+
82+
$wsseMiddleware = new WsseMiddleware(
83+
outgoing: [
84+
(new Entry\Username($user))
85+
->withPassword('xxx')
86+
->withDigest(false),
87+
]
7988
);
8089
```
90+
91+
#### Signing a SOAP request with PKCS12 or X509 certificate.
92+
93+
This is one of the most common implementation of WSS out there.
94+
You are granted a certificate by the soap service with which you need to fetch data.
95+
96+
In case of a p12 certificate: convert it to a private key and public X509 certificate first:
97+
98+
```bash
99+
openssl pkcs12 -in your.p12 -out security_token.pub -clcerts -nokeys
100+
openssl pkcs12 -in your.p12 -out security_token.priv -nocerts -nodes
101+
```
102+
103+
Next, you can configure the middleware like this:
104+
105+
```php
106+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Certificate;
107+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Key;
108+
use Soap\Psr18WsseMiddleware\WSSecurity\SignatureMethod;
109+
use Soap\Psr18WsseMiddleware\WSSecurity\DigestMethod;
110+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyIdentifier;
111+
use Soap\Psr18WsseMiddleware\WsseMiddleware;
112+
use Soap\Psr18WsseMiddleware\WSSecurity\Entry;
113+
114+
$privKey = Key::fromFile('security_token.priv')->withPassphrase('xxx'); // Regular private key (not wrapped in X509)
115+
$pubKey = Certificate::fromFile('security_token.pub'); // Public X509 cert
116+
117+
$wsseMiddleware = new WsseMiddleware(
118+
outgoing: [
119+
new Entry\Timestamp(60),
120+
new Entry\BinarySecurityToken($pubKey),
121+
(new Entry\Signature(
122+
$privKey,
123+
new KeyIdentifier\BinarySecurityTokenIdentifier()
124+
))
125+
->withSignatureMethod(SignatureMethod::RSA_SHA256)
126+
->withDigestMethod(DigestMethod::SHA256)
127+
->withSignAllHeaders(true)
128+
->withSignBody(true)
129+
]
130+
);
131+
```
132+
133+
This example can also be used in combination with signing and username authentication.
134+
135+
#### Authorize a SOAP request with a SAML assertion
136+
137+
Another common implementation is authentication through a WS-Trust compliant STS instance.
138+
In this case, you first have to fetch a SAML assertion from the STS service.
139+
Most of them require you to sign the request with a X509 certificate.
140+
This can be done with the middleware above.
141+
142+
Once you received back your SAML assertion, you have to pass it to the webservice you want to contact.
143+
A common configuration for passing the SAML assertion might look like this:
144+
145+
```php
146+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Certificate;
147+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Key;
148+
use Soap\Psr18WsseMiddleware\WSSecurity\SignatureMethod;
149+
use Soap\Psr18WsseMiddleware\WSSecurity\DigestMethod;
150+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyIdentifier;
151+
use Soap\Psr18WsseMiddleware\WsseMiddleware;
152+
use Soap\Psr18WsseMiddleware\WSSecurity\Entry;
153+
use VeeWee\Xml\Dom\Document;
154+
use function VeeWee\Xml\Dom\Locator\document_element;
155+
156+
$privKey = Key::fromFile('security_token.priv')->withPassphrase('xxx'); // Regular private key (not wrapped in X509)
157+
158+
// These are provided through the STS service.
159+
$samlAssertion = Document::fromXmlString(<<<EOXML
160+
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" AssertionID="xxxx" />
161+
EOXML
162+
);
163+
$samlAssertionId = $samlAssertion->locate(document_element())->getAttribute('AssertionID');
164+
165+
$wsseMiddleware = new WsseMiddleware(
166+
outgoing: [
167+
new Entry\Timestamp(60),
168+
(new Entry\Signature(
169+
$privKey,
170+
new KeyIdentifier\SamlKeyIdentifier($samlAssertionId)
171+
))
172+
->withSignatureMethod(SignatureMethod::RSA_SHA256)
173+
->withDigestMethod(DigestMethod::SHA256)
174+
->withSignAllHeaders(true)
175+
->withSignBody(true)
176+
->withInsertBefore(false),
177+
new Entry\SamlAssertion($samlAssertion),
178+
]
179+
);
180+
```
181+
182+
#### Encrypt sensitive data
183+
184+
Some services require you to encrypt sensitive parts of the request and decrypt sensitive parts of the response.
185+
In this case, you can add your public key to the request, encrypt the payload and send it over the wire.
186+
Incoming responses will be encrypted with your public key and kan be decrypted by using your private key.
187+
188+
189+
Encryption contains a [known bug](https://github.com/robrichards/wse-php/pull/67) in the underlying [robrichards/wse-php](https://github.com/robrichards/wse-php) library.
190+
Since a fix has not been merged yet, you can apply a patch like this:
191+
192+
```bash
193+
composer require --dev cweagans/composer-patches
194+
```
195+
196+
```json
197+
{
198+
"extra": {
199+
"patches": {
200+
"robrichards/wse-php": {
201+
"Fix encryption bug": "https://patch-diff.githubusercontent.com/raw/robrichards/wse-php/pull/67.diff"
202+
}
203+
}
204+
}
205+
}
206+
```
207+
208+
The configuration for encryption looks like this:
209+
210+
```php
211+
use Soap\Psr18WsseMiddleware\WSSecurity\DataEncryptionMethod;
212+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyEncryptionMethod;
213+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Certificate;
214+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyStore\Key;
215+
use Soap\Psr18WsseMiddleware\WSSecurity\SignatureMethod;
216+
use Soap\Psr18WsseMiddleware\WSSecurity\DigestMethod;
217+
use Soap\Psr18WsseMiddleware\WSSecurity\KeyIdentifier;
218+
use Soap\Psr18WsseMiddleware\WsseMiddleware;
219+
use Soap\Psr18WsseMiddleware\WSSecurity\Entry;
220+
221+
$privKey = Key::fromFile('security_token.priv')->withPassphrase('xxx'); // Regular private key (not wrapped in X509)
222+
$pubKey = Certificate::fromFile('security_token.pub'); // Public X509 cert
223+
$signKey = Certificate::fromFile('sign-key.pem'); // X509 cert for signing. Could be the same as $pubKey.
224+
225+
$wsseMiddleware = new WsseMiddleware(
226+
outgoing: [
227+
new Entry\Timestamp(60),
228+
new Entry\BinarySecurityToken($pubKey),
229+
(new Entry\Signature(
230+
$privKey,
231+
new KeyIdentifier\BinarySecurityTokenIdentifier()
232+
))
233+
(new Entry\Encryption(
234+
$signKey,
235+
new KeyIdentifier\X509SubjectKeyIdentifier($signKey)
236+
))
237+
->withKeyEncryptionMethod(KeyEncryptionMethod::RSA_OAEP_MGF1P)
238+
->withDataEncryptionMethod(DataEncryptionMethod::AES256_CBC)
239+
],
240+
incoming: [
241+
new Entry\Decryption($privKey)
242+
]
243+
);
244+
```
245+
246+
Note: Encryption only can also be done without adding a signature.

composer.json

+22-5
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,35 @@
1919
}
2020
],
2121
"require": {
22-
"php": "^8.0",
23-
"php-soap/psr18-transport": "^1.0",
22+
"php": "^8.1",
23+
"ext-dom": "*",
24+
"ext-openssl": "*",
25+
"azjezz/psl": "^1.9",
26+
"paragonie/hidden-string": "^2.0",
27+
"php-soap/psr18-transport": "^1.2.1",
2428
"php-soap/engine": "^1.0",
25-
"php-soap/xml": "^1.0",
29+
"php-soap/xml": "^1.3.1",
2630
"php-http/client-common": "^2.3",
2731
"robrichards/wse-php": "^2.0",
28-
"veewee/xml": "^1.0"
32+
"veewee/xml": "^1.5"
2933
},
3034
"require-dev": {
3135
"nyholm/psr7": "^1.3",
3236
"php-http/mock-client": "^1.4",
3337
"symfony/http-client": "^5.3",
34-
"phpunit/phpunit": "^9.5"
38+
"phpunit/phpunit": "^9.5",
39+
"cweagans/composer-patches": "^1.7"
40+
},
41+
"extra": {
42+
"patches": {
43+
"robrichards/wse-php": {
44+
"Fix encryption bug": "https://patch-diff.githubusercontent.com/raw/robrichards/wse-php/pull/67.diff"
45+
}
46+
}
47+
},
48+
"config": {
49+
"allow-plugins": {
50+
"cweagans/composer-patches": true
51+
}
3552
}
3653
}

psalm.xml

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
<?xml version="1.0"?>
22
<psalm
3-
errorLevel="2"
3+
errorLevel="1"
44
resolveFromConfigFile="true"
5-
forbidEcho="true"
65
strictBinaryOperands="true"
7-
phpVersion="8.0"
6+
phpVersion="8.1"
87
allowStringToStandInForClass="true"
98
rememberPropertyAssignmentsAfterCall="false"
109
skipChecksOnUnresolvableIncludes="false"

0 commit comments

Comments
 (0)