Skip to content

Commit 0bbd523

Browse files
committed
Cover a certification-path BinarySecurityToken in both directions
The PHP side can now advertise its whole path as a #X509PKIPathv1 token, and the inbound half had only ever been read back by our own writer. The PHP->Java row is what settles the byte order the middleware emits. Our reader derives the end-entity from issuer linkage and therefore accepts either direction, so it cannot arbitrate; WSS4J can, and it refuses a leaf-first path with "CA key usage check failed: keyCertSign bit is not set", having read the first certificate as the issuer. Anchor first is accepted, which is how ITU-T X.509 defines a PkiPath. The Java->PHP row needs the oracle to emit one, which is WSS4J's setUseSingleCertificate(false), wired as signature.certificatePath and the ?certpath= query parameter. It writes the same order, CA then leaf, and includes the anchor rather than stopping at the intermediates.
1 parent 7e03c42 commit 0bbd523

5 files changed

Lines changed: 55 additions & 2 deletions

File tree

oracle/src/main/java/org/phpsoap/interop/OracleServer.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ private static String attachJson(Attachments.ReceiveResult result) {
233233
/**
234234
* Builds a per-request {@link ScenarioConfig} from query parameters, defaulting to the matrix happy
235235
* flow. Recognised params mirror the CLI config keys, e.g.
236-
* {@code ?keyref=SubjectKeyIdentifier&sigalg=RSA_SHA512&encdata=AES256_CBC&enckey=RSA_OAEP&oaep=SHA256
236+
* {@code ?keyref=SubjectKeyIdentifier&sigalg=RSA_SHA512&certpath=true&encdata=AES256_CBC&enckey=RSA_OAEP&oaep=SHA256
237237
* &c14n=INCLUSIVE&disableBsp=true&ts=false}.
238238
*/
239239
private static ScenarioConfig configFrom(URI uri) {
@@ -245,6 +245,9 @@ private static ScenarioConfig configFrom(URI uri) {
245245
if (q.containsKey("sigalg")) {
246246
config.signatureAlgorithm = q.get("sigalg");
247247
}
248+
if (q.containsKey("certpath")) {
249+
config.signatureCertificatePath = Boolean.parseBoolean(q.get("certpath"));
250+
}
248251
if (q.containsKey("sigalias")) {
249252
config.signatureKeyAlias = q.get("sigalias");
250253
}

oracle/src/main/java/org/phpsoap/interop/ScenarioConfig.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ public final class ScenarioConfig {
3434
/** XML-DSig signature algorithm: RSA_SHA256 (default), RSA_SHA512 or ECDSA_SHA256. */
3535
public String signatureAlgorithm = "RSA_SHA256";
3636

37+
/**
38+
* When true the signer puts the whole certification path in the wsse:BinarySecurityToken as an
39+
* ASN.1 SEQUENCE OF Certificate (#X509PKIPathv1) instead of the leaf certificate alone
40+
* (#X509v3), which is WSS4J's setUseSingleCertificate(false). Mirrors the PHP
41+
* Outbound\Signature::withCertificatePath() opt-in so both emitters can be cross-tested.
42+
*/
43+
public boolean signatureCertificatePath = false;
44+
3745
/**
3846
* Keystore alias the signer uses. Defaults to the RSA java-server key; the ECDSA-SHA256 rows select the
3947
* EC leaf (ec-client) so the signature algorithm and key type agree.
@@ -100,6 +108,8 @@ public static ScenarioConfig fromProperties(Properties props) {
100108
props.getProperty("signature.keyReference", config.signatureKeyReference).trim();
101109
config.signatureAlgorithm =
102110
props.getProperty("signature.algorithm", config.signatureAlgorithm).trim();
111+
config.signatureCertificatePath =
112+
boolProp(props, "signature.certificatePath", config.signatureCertificatePath);
103113
config.canonicalization =
104114
props.getProperty("signature.canonicalization", config.canonicalization).trim();
105115
config.dataEncryptionAlgorithm =

oracle/src/main/java/org/phpsoap/interop/Signer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ String sign(String xml) throws Exception {
7272
signature.setSignatureAlgorithm(signatureAlgorithm(config.signatureAlgorithm));
7373
signature.setDigestAlgo(WSConstants.SHA256);
7474
signature.setSigCanonicalization(canonicalizationUri(config.canonicalization));
75+
// A path token carries the leaf plus the CA certificates from the keystore entry, so a peer that
76+
// will not complete the chain itself gets the intermediates handed to it.
77+
signature.setUseSingleCertificate(!config.signatureCertificatePath);
7578

7679
signature.getParts().add(
7780
new WSEncryptionPart(WSConstants.ELEM_BODY, soapNamespace(document), "Content"));

tests/Support/Wsse.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Soap\Psr18WsseMiddleware\Algorithm\SignatureMethod;
1010
use Soap\Psr18WsseMiddleware\KeyStore\Certificate;
1111
use Soap\Psr18WsseMiddleware\KeyStore\ClientCertificate;
12+
use Soap\Psr18WsseMiddleware\KeyStore\Pkcs12Bundle;
1213
use Soap\Psr18WsseMiddleware\WSSecurity\Outbound;
1314
use Soap\Psr18WsseMiddleware\WSSecurity\Part;
1415
use Soap\Psr18WsseMiddleware\WSSecurity\SecurityProfile;
@@ -28,6 +29,7 @@ final class Wsse
2829
* Sign the sample with the given knobs and return the signed XML.
2930
*
3031
* @param list<Part>|null $parts override the signed parts (default Body + Timestamp)
32+
* @param bool $certificatePath advertise the whole certification path as a #X509PKIPathv1 token
3133
*/
3234
public static function sign(
3335
?SoapVersion $soapVersion = null,
@@ -39,11 +41,17 @@ public static function sign(
3941
?string $inputXml = null,
4042
int $timestampTtl = 300,
4143
bool $inclusivePrefixes = false,
44+
bool $certificatePath = false,
4245
): string {
4346
$soapVersion ??= SoapVersion::Soap12;
4447
$document = Document::fromXmlString($inputXml ?? Oracle::sampleEnvelope());
4548
$context = new WsseContext($document, $soapVersion, new SecurityProfile());
46-
$clientCertificate = ClientCertificate::fromFile($clientCertFile ?? Oracle::certPath('php-client.pem'));
49+
// A certificate path comes from the PKCS#12 bundle, which is the only shipped material that carries the
50+
// CA alongside the leaf; the PEM signing identity has no chain to offer.
51+
$bundle = $certificatePath ? Pkcs12Bundle::fromFile(Oracle::certPath('php-client.p12'), 'changeit') : null;
52+
$clientCertificate = $bundle !== null
53+
? ClientCertificate::fromPkcs12($bundle)
54+
: ClientCertificate::fromFile($clientCertFile ?? Oracle::certPath('php-client.pem'));
4755

4856
(new Outbound\Timestamp($timestampTtl))($context);
4957

@@ -60,6 +68,9 @@ public static function sign(
6068
if ($inclusivePrefixes) {
6169
$signature = $signature->withInclusivePrefixes();
6270
}
71+
if ($bundle !== null) {
72+
$signature = $signature->withCertificatePath($bundle->chain);
73+
}
6374
$signature($context);
6475

6576
return $document->toXmlString();

tests/Wsse/SignatureInteropTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ public function test_php_signed_happy_flow_is_accepted_by_wss4j(): void
4343
self::assertJsonStringEqualsJsonString('{"valid":true}', $response['body']);
4444
}
4545

46+
public function test_php_signed_certificate_path_is_accepted_by_wss4j(): void
47+
{
48+
// The BinarySecurityToken carries the leaf and the CA as one ASN.1 SEQUENCE OF Certificate
49+
// (#X509PKIPathv1) instead of the leaf alone. This row is what settles the byte order: our own reader
50+
// derives the end-entity from issuer linkage and so accepts either direction, while WSS4J refuses a
51+
// leaf-first path outright ("CA key usage check failed: keyCertSign bit is not set" -- it reads the
52+
// first certificate as the issuer). Anchor first, as ITU-T X.509 defines a PkiPath, is what passes.
53+
$signed = Wsse::sign(certificatePath: true);
54+
55+
$response = Oracle::post('/verify', $signed);
56+
57+
self::assertValid($response, 'WSS4J should verify a PHP signature advertising a certification path');
58+
}
59+
4660
public function test_php_signed_soap11_is_accepted_by_wss4j(): void
4761
{
4862
$signed = Wsse::sign(
@@ -129,6 +143,18 @@ public function test_wss4j_signed_ecdsa_sha256_is_accepted_by_php(): void
129143
$this->phpVerify($javaSigned, [Part::body(), Part::timestamp()]);
130144
}
131145

146+
public function test_wss4j_signed_certificate_path_is_accepted_by_php(): void
147+
{
148+
// WSS4J's own path emitter (setUseSingleCertificate(false)), so the inbound half is validated against a
149+
// real peer's encoder rather than only against ours. It writes the order we write: the CA first, then
150+
// the java-server leaf.
151+
$javaSigned = Oracle::post('/sign?certpath=true', Oracle::sampleEnvelope())['body'];
152+
153+
$this->phpVerify($javaSigned, [Part::body(), Part::timestamp()]);
154+
155+
self::assertStringContainsString('X509PKIPathv1', $javaSigned);
156+
}
157+
132158
/**
133159
* @return iterable<string, array{string}>
134160
*/

0 commit comments

Comments
 (0)