Skip to content

Commit 8caef67

Browse files
committed
Demonstrate the sibling-certificate attack and both of its fixes
WSS4J signs with ec-client, a different certificate the same CA issued. Anchoring the CA accepts it as readily as the real peer; pinning the peer's own certificate refuses it, and so does naming the expected signer with the CA still anchored. A third row covers pinning a CA-issued leaf at all, which OpenSSL cannot do through chain building.
1 parent 7333b2c commit 8caef67

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

tests/Wsse/SignatureInteropTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Soap\Psr18WsseMiddleware\WSSecurity\Exception\SecurityFault;
1616
use Soap\Psr18WsseMiddleware\WSSecurity\Inbound;
1717
use Soap\Psr18WsseMiddleware\KeyStore\Certificate;
18+
use Soap\Psr18WsseMiddleware\KeyStore\TrustedSigner;
1819
use Soap\Psr18WsseMiddleware\WSSecurity\Outbound;
1920
use Soap\Psr18WsseMiddleware\WSSecurity\Part;
2021
use Soap\Psr18WsseMiddleware\WSSecurity\SecurityProfile;
@@ -245,6 +246,67 @@ public function test_xsw_wrapped_wss4j_signed_message_is_rejected_by_php(): void
245246
self::assertPhpRejects($wrapped, [Part::body(), Part::timestamp()]);
246247
}
247248

249+
/**
250+
* Pinning the peer's own CA-issued certificate rather than its issuer. OpenSSL cannot build a path that
251+
* ends at a CA-issued certificate, so this only works because a store entry matching the presented
252+
* certificate is honoured directly.
253+
*/
254+
public function test_wss4j_signed_message_is_accepted_by_php_against_a_pinned_leaf(): void
255+
{
256+
$javaSigned = Oracle::post('/sign', Oracle::sampleEnvelope())['body'];
257+
258+
$this->phpVerify(
259+
$javaSigned,
260+
[Part::body(), Part::timestamp()],
261+
TrustStore::fromCertificates(Certificate::fromFile(Oracle::certPath('java-server.crt'))),
262+
);
263+
}
264+
265+
/**
266+
* The finding a pin exists for: ec-client is a different certificate the same CA issued, so anchoring the
267+
* CA accepts it as readily as the real peer. Pinning the peer's certificate is what tells them apart.
268+
*/
269+
public function test_a_sibling_certificate_from_the_same_ca_is_refused_against_a_pinned_leaf(): void
270+
{
271+
$siblingSigned = Oracle::post('/sign?sigalg=ECDSA_SHA256&sigalias=ec-client', Oracle::sampleEnvelope())['body'];
272+
273+
// Anchoring the CA accepts it: the certificate is validly issued, it is simply not the peer.
274+
$this->phpVerify(
275+
$siblingSigned,
276+
[Part::body(), Part::timestamp()],
277+
TrustStore::fromCertificates(Certificate::fromFile(Oracle::certPath('ca.crt'))),
278+
);
279+
280+
self::assertPhpRejects(
281+
$siblingSigned,
282+
[Part::body(), Part::timestamp()],
283+
TrustStore::fromCertificates(Certificate::fromFile(Oracle::certPath('java-server.crt'))),
284+
);
285+
}
286+
287+
/**
288+
* The same finding closed the other way, with the CA still anchored: the application names the identity it
289+
* expected and the sibling is refused.
290+
*/
291+
public function test_a_sibling_certificate_is_refused_by_an_expected_signer_check(): void
292+
{
293+
$siblingSigned = Oracle::post('/sign?sigalg=ECDSA_SHA256&sigalias=ec-client', Oracle::sampleEnvelope())['body'];
294+
295+
$document = Document::fromXmlString($siblingSigned);
296+
$context = new WsseContext($document, SoapVersion::Soap12, new SecurityProfile());
297+
$block = (new Inbound\VerifySignature(
298+
TrustStore::fromCertificates(Certificate::fromFile(Oracle::certPath('ca.crt'))),
299+
signed: [Part::body(), Part::timestamp()],
300+
))->onTrustedSigner(static function (TrustedSigner $signer): void {
301+
if (!str_contains($signer->subjectDistinguishedName()->toString(), 'java-server')) {
302+
throw new \RuntimeException('not the expected peer');
303+
}
304+
});
305+
306+
$this->expectException(SecurityFault::class);
307+
$block($context);
308+
}
309+
248310
public function test_untrusted_php_signer_is_rejected_by_wss4j(): void
249311
{
250312
$signed = Wsse::sign(clientCertFile: Oracle::certPath('untrusted-client.pem'));

0 commit comments

Comments
 (0)