It is required that clients digitally sign their price requests. Missing digital signatures or signatures which cannot be validated
by the service lead to a fault response. The
Organization for the Advancement of Structured Information Standards
has specified a standard adressing Web Service Security, see
OASIS Web Services Security (WS-Security). The indicated specification
uses, for example, the W3C XML Signature recommendation to achieve
security goals like message content integrity. It is however completely natural to avoid such heavy-weight specifications
like WS-Security and to use directly
XML Signature since the Java (and .NET) platform provides an
implementation of this W3C recommendation.
By doing so the service interoperability is somewhat reduced because signing client stubs cannot be generated automatically from our service
definition because the service definition
doesn't include policies about the application of digital signatures. Instead, we are relying on so called SOAPHandler
s
to create a XML signature of the message payload. Thus we have to implement and to add a SignatureHandler
to the process chain of
the client and an appropriate VerificationHandler
to the process chain of the service. On the other hand our approach
follows the separation of concerns principle and makes it easier to reason about the security of the applied digital signature.
There exists several attacks, especially so called XML signature wrapping attacks, on WS-Security (and naively applied XML Signatures). See the
Analysis of Signature Wrapping Attacks and Countermeasures
and
XML Signature Element Wrapping Attacks and Countermeasures
for some details.
XML Signatures may be applied to certain portions of XML documents. These to be signed XML resources
must be referenced by Unique Resource Identifiers
(URI
s).
The XML Signature Processor will compute
cryptographic message digests over these referenced resources. The references (together with the computed digests), the specified message digest
and signature algorithms as well as some
additional required transformations like Canonicalization are parts of the SignedInfo
element.
The actual SignatureValue
will be created over this SignedInfo
element.
The top-level Signature
element consists of these SignedInfo
and
SignatureValue
elements.
That is the signature generation is a two step process. First the message digests of the referenced resources will be computed and then
the actual signature will be generated over these digests.
Additionally, a KeyInfo
can be added to the
Signature
element. This optional KeyInfo
element may contain information
to retrieve the public key needed to validate the digital signature. See this
overview and subsequent
example
from the official
XML Signature recommendation.
The Java Development Kit provides an implementation of the XML Signature recommendation.
See the Java XML Digital Signature API Specification
and The XML Digital Signature API Reference and Tutorial
for further information. In the next sections, I will present some code pieces used within the SignatureHandler
and VerificationHandler
.
[Top]
First we have to generate a key pair for the web service client. We will be using
keytool, the Key and Certificate Management Tool
of the JDK.
However, this distribution as provided within the Downloads section comes with two preconfigured keystores. The keystore
of the client, mykeystore.jks
, contains the key pair whereas certificates.jks
contains
the certificate authenticating the linked public key of the client.
Note that the following shown keytool commands assume that the bin-directory of the JDK is referenced by the PATH
environment
variable.
$ keytool -genkeypair -dname "cn=Christof Reichardt, L=Rodgau, ST=Hessen, c=DE" -alias Tester -keyalg RSA -keypass changeit -keystore mykeystore.jks -storepass changeit -validity 180
The preceding command creates a key pair with the specified X.500 Distinguished Name "cn=Christof Reichardt, L=Rodgau, ST=Hessen, c=DE" identified by the alias "Tester" and adds it
to the mykeystore.jks
. If the file mykeystore.jks
doesn't exist, it will be
created. The self-signed certificate should be considered valid for 180 days. The following command exports the certificate linked with the
key entry identified by the alias "Tester" from the mykeystore.jks
into a file named
certificate.bin
$ keytool -exportcert -alias Tester -file certificate.bin -keystore mykeystore.jks -storepass changeit
Next we have to import this certificate into the keystore used by the service to authenticate the price requests:
$ keytool -importcert -alias Tester -file certificate.bin -keystore certificates.jks -keypass changeit -storepass changeit -v
The mykeystore.jks
remains on the client side whereas the certificates.jks
must be accessible by the service. Within a real world scenario the certificate of the client might be authenticated by a certificate
authority (CA) like DigiCert. By doing so the validity of the client's certificate
can be verified by the public key of the CA.
[Top]
The SignatureHandler
Handlers can be added programmatically on the client side by accessing the handler chain via the BindingProvider
.
In order to get such a BindingProvider
we have to instantiate the service interface at first:
1 |
URL url = new URL("http://127.0.1.1:8080/SecurityPriceService-server-0.0.1-SNAPSHOT/securitypriceservice?wsdl"); |
2 |
QName qName = new QName("http://securitypriceservice.jaxws.christofreichardt.de/", "SecurityPriceService"); |
3 |
SecurityPriceService securityPriceService = new SecurityPriceService(url, qName); |
4 |
SecurityPriceServicePortType securityPriceServicePort = securityPriceService.getPort(SecurityPriceServicePortType.class); |
The SecurityPriceServicePortType
instance can be casted to the desired BindingProvider
which in
turn is used to add the SignatureHandler
to the handler chain:
5 |
BindingProvider bindingProvider = (BindingProvider) securityPriceServicePort; |
6 |
List<Handler> handlerChain = bindingProvider.getBinding().getHandlerChain(); |
7 |
handlerChain.add(new SignatureHandler()); |
8 |
bindingProvider.getBinding().setHandlerChain(handlerChain); |
Such a (client) handler will be typically called twice during a request/response cycle. The first invocation takes place when the SOAP message is
outbound and the second invocation takes place when it is inbound.
In order to add functionality to the process chain we must implement the
boolean handleMessage(C context)
method. A certain property can be used to determine the message direction:
1 |
public boolean handleMessage(SOAPMessageContext soapMessageContext) { |
2 |
boolean outbound = (boolean) soapMessageContext.get(SOAPMessageContext.MESSAGE_OUTBOUND_PROPERTY); |
3 |
if (outbound) { |
4 |
SOAPMessage soapMessage = soapMessageContext.getMessage(); |
5 |
try { |
6 |
SOAPHeader soapHeader = soapMessage.getSOAPHeader(); |
7 |
addSignature(soapHeader); |
8 |
} |
9 |
catch ( ... ) { |
10 |
... |
11 |
} |
12 |
} |
13 |
return true; |
14 |
} |
Next we can explore how the actual signature will be created and appended to the SOAP header:
1 |
private void addSignature(Element soapHeader) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, MarshalException, XMLSignatureException { |
2 |
XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance(); |
3 |
DigestMethod digestMethod = xmlSignatureFactory.newDigestMethod(DigestMethod.SHA256, null); |
4 |
Reference reference = xmlSignatureFactory.newReference("#xpointer(/S:Envelope/S:Body)", digestMethod); |
5 |
CanonicalizationMethod canonicalizationMethod = xmlSignatureFactory.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS, (C14NMethodParameterSpec) null); |
6 |
SignatureMethod signatureMethod = xmlSignatureFactory.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null); |
7 |
SignedInfo signedInfo = xmlSignatureFactory.newSignedInfo(canonicalizationMethod, signatureMethod, Collections.singletonList(reference)); |
8 |
XMLSignature xmlSignature = xmlSignatureFactory.newXMLSignature(signedInfo, null); |
9 |
DOMSignContext domSignContext = new DOMSignContext(this.privateKeyEntry.getPrivateKey(), soapHeader); |
10 |
domSignContext.setURIDereferencer(new SoapBodyDereferencer()); |
11 |
domSignContext.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE); |
12 |
xmlSignature.sign(domSignContext); |
13 |
List<?> references = xmlSignature.getSignedInfo().getReferences(); |
14 |
traceReferences(references); |
15 |
} |
We get a XMLSignatureFactory
instance in line two. A XMLSignatureFactory
is needed to
create and to assemble the several objects which make up a XMLSignature
. First we use the
XMLSignatureFactory
to request the SHA-256 digest algorithm required for the checksum calculation of the
referenced resources. Since we want to sign the whole SOAP body only one such reference is created in line four. The given
XPointer fragment #xpointer(/S:Envelope/S:Body)
denotes a so called same document reference.
Note that the enclosed XPath expression is merely a hint for third parties because we will make use of a custom URI resolver (line 10).
This SoapBodyDereferencer
doesn't really evaluate the given XPointer fragment but validates only its presence
and resolves thereupon the SOAP body. A canonicalization algorithm is chosen in line five. Canonicalization is needed because logical
equivalent XML documents may have different DOM representations. E.g. intermediary node processors may change the DOM representation
but maintain the logical equivalence. The algorithms to compute the actual signature over the digested reference are requested in line 6
(RSA with SHA-256). The SignedInfo
object is assembled in line seven. The actual XMLSignature
is put together in line eight (we leave out any KeyInfo
). The DOMSignContext
specifies the private key and the location in the DOM tree where an XMLSignature
object is to be marshalled when
generating the signature (line nine). We demand the caching of the references in line eleven. This enables the tracing of the referenced input streams after the
signature generation which is useful for debugging purposes. The actual signature generation is requested in line twelve.
How does an actual XMLSignature
over a SOAP body might look like? Consider this
SecurityPriceServiceRequest (I don't give a textual representation to avoid
Canonicalization issues). The Signed SecurityPriceServiceRequest contains a so called
detached signature within its SOAP header (contrary to enveloped or enveloping signatures).
[Top]
The VerificationHandler
The handler chain on the server side cannot
be accessed programmatically unlike the handler chain on the client side. Someone can use an annotation on the service implementation class
to indicate a XML resource file containing the handler chain definition. That is the handler chain is fixed at runtime.
1 |
<?xml version="1.0" encoding="UTF-8"?> |
2 |
<handler-chains xmlns="http://java.sun.com/xml/ns/javaee"> |
3 |
<handler-chain> |
4 |
<handler> |
5 |
<handler-name>VerificationHandler</handler-name> |
6 |
<handler-class>de.christofreichardt.jaxws.securitypriceservice.VerificationHandler</handler-class> |
7 |
</handler> |
8 |
<handler> |
9 |
<handler-name>SchemaValidationHandler</handler-name> |
10 |
<handler-class>de.christofreichardt.jaxws.securitypriceservice.SchemaValidationHandler</handler-class> |
11 |
</handler> |
12 |
</handler-chain> |
13 |
</handler-chains> |
Besides the VerificationHandler
we are seeing a second handler responsible for the validation of the incoming
message against the XML schema.
Handlers on the server side will be called twice too. The first invocation takes place when the message
is inbound. At that time we are required to verify the signature.
Missing or invalid signatures as well as missing certificates for
the given user id lead to a ProtocolException
.
1 |
public boolean handleMessage(SOAPMessageContext soapMessageContext) { |
2 |
boolean outbound = (boolean) soapMessageContext.get(SOAPMessageContext.MESSAGE_OUTBOUND_PROPERTY); |
3 |
SOAPMessage soapMessage = soapMessageContext.getMessage(); |
4 |
if (!outbound) { |
5 |
KeyStore keyStore = retrieveKeystore(soapMessageContext); |
6 |
try { |
7 |
SOAPHeader soapHeader = soapMessage.getSOAPHeader(); |
8 |
Node signature = soapHeader.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature").item(0); |
9 |
if (signature == null) |
10 |
throw new ProtocolException("No Signature element found."); |
11 |
PublicKey publicKey = fetchPublicKey(soapMessage, keyStore); |
12 |
XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance(); |
13 |
DOMValidateContext domValidateContext = new DOMValidateContext(publicKey, signature); |
14 |
domValidateContext.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE); |
15 |
domValidateContext.setURIDereferencer(new ServerSoapBodyDereferencer()); |
16 |
XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); |
17 |
boolean validated = xmlSignature.validate(domValidateContext); |
18 |
traceReferences(xmlSignature.getSignedInfo().getReferences(), domValidateContext); |
19 |
if (!validated) |
20 |
throw new ProtocolException("Invalid Signature."); |
21 |
} |
22 |
catch (...) { |
23 |
... |
24 |
} |
25 |
} |
26 |
return true; |
27 |
} |
[Top]