TOC
Introduction
The first article
of these series explained how to use custom security tokens, adding username
identification / password validation to ensure authentication. Now let's handle
the next possibility to authenticate one's messages: using certificates by
adding binary security tokens. This article extends the example built during the
previous article.
Before we go into implementation details let's have a look at some
fundamental security concepts. Using certificates and thus the technique of
public/private keys can be used for different purposes, but most probably for
signing messages or encrypting (parts of) them. And these topics, signing and
encryption of messages, build the content of this article.
The next few
paragraphs give a short introduction to encryption, signatures and
certificates.
Encryption
Encryption technologies can be used to assure data confidentiality.
Passing data through a network is very delicate regarding security issues. So
encrypting the data before it passes the network and decrypting it afterwards
can prevent eavesdroppers from reading the data during its transmittal.
The
process of plaintext being coded to create ciphertext is called encryption, and
the process of decoding ciphertext to restore the plaintext is called
decryption.
There are two types of encryption: symmetric key encryption and
asymmetric (public) key encryption, which can be even used in
conjunction.
Symmetric Key Encryption
means the same key is used to both encrypt and decrypt a message. The key is
a secret key because it is kept as a shared secret between the two
involved entities (sender and receiver). Not keeping it secret would lead to the
confidentiality of the encrypted data being compromised. Symmetric key
encryption is often much faster then public encryption, up to 1000 times,
because public encryption needs higher computational efforts.
Asymmetric (Public) Key Encryption
is based on two different keys for encryption and decryption: a private
key (known only to its owner) and a public key (available and known
to other entities on the network). A public key is typically used to encrypt a
message, guaranteeing that only the person with the corresponding private key
can decrypt the message.
The other way, the message can be encrypted using the private key and then
decrypted with the public key.
Encryption is very important to Web Service security since SOAP messages are
plain text by default, so being readable by any recipient and eavesdropper. The
WSE supports both symmetric and asymmetric encryption. Using the former, the Web
Service and the client share a secret key outside the SOAP message
communication, the client encrypts the message using this key and the Web
Service decrypts the message using the same shared key. Using asymmetric
encryption, a client uses the public key (of an X.509 certificate) to encrypt
his message and only the owner of the private key (of that X.509 certificate)
can decrypt the message.
Signatures
Public keys can also be used to create and validate digital signatures. The
goal is to achieve message integrity, that is ensuring that the data an
recipient receives has not been altered during transit.
The basis here is that the data to be signed is combined with the private key
and then transformed by some algorithm. Taken this, only someone possessing the
private key could have created the digital signature, but anyone that has access
to the corresponding public key can verify the digital signature. The important
part: any changes made on the signed data invalidate the whole signature. So
digital signatures allow a recipient to cryptographically verify that a message
has not been altered since it was signed. To ease the use of the signature
mechanism, the signatures themselves can be sent together with the data signed,
so that the receiver can easily verify the message' origin.
The WSE provides mechanism to digitally sign SOAP messages using a
UsernameToken, X.509 certificates or custom binary security tokens. When the WSE
is configured on the SOAP message recipient's computer, the WSE automatically
verifies the signature.
Certificates
So how to pass public or symmetric keys? Passing one's public key (the way it
is most commonly needed) face-to-face is a possibility, but obviously not very
practicable in every situation. Often it will be necessary to obtain a public
key using some public directory, a less secure mechanism than face-to-face. So
it's important for a user accessing a key from such a directory to have the
certainty that this key really is the key it claims to be from. A standardized
way to do so is to use certificates. A certificate is a digitally signed
statement that contains information about the owner and its public key, binding
these two pieces of information together.
Certificates are issued by entities or services called certificate
authority (CA). A CA guarantees the validity of the binding between the
certificate owner and its public key. Trusting a CA means trusting that
any certificate created and issued for its owner by that CA identifies the owner
of the certificate. Therefore the private key that corresponds to the public key
in the certificate is deemed to be known only by the specified owner.
Certificates can contain different type of data. A standard format representing
certificates is X.509. Below, the members of this certificate format are
listed.
Field |
Description |
Version |
of the certificate format, f.i. 3 |
Certificate Serial Number |
unique serial number assigned by the issuing CA |
Certificate Algorithm Identifier and Parameters |
public key cryptography and message digest algorithms used by the issuing CA
to digitally sign the certificate |
Issuer |
name of issuing CA |
Validity Period |
start and expiration date |
Subject |
name of person or entity that is owner of the certificate |
Subject Public-Key Information incl. Algorithm Identifier & Public-Key
Value |
- the public key and a list of public key cryptography algorithms -
information specifying the cryptography operations for which the public and
private key can be used |
Extensional Optional Fields |
|
Certification Authority's Digital Signature |
CA's digital signature |
Certificate Stores
All certificates are stored in certificate stores, where several default
stores are available:
CURRENT_USER\MY\
Personal certificates store for current logged-in
user, not visible to other users logged in
LOCAL_MACHINE\MY\
Personal certificates store common for all
users
CURRENT_USER\Root\
Trusted Root Certification Authorities and
contains certificates for user-trusted root CAs. Certificates with a
certification path to a root CA certificate are trusted by the current user for
all valid purposes of the certificate.
LOCAL_MACHINE\Root\
as above, but trusted by all users
There are root CAs that are trusted by default, such as Verisign. Though our
examples use a trial Verisign certificate, these are issued by the Verisign Test
Authority, which is not trusted by default.
Certificate Management and Creation
It's time to start the actual implementation of our example using X.509
certificates. In fact, the step we take from the example of the last article to
X.509 is not very big - it's only some lines of code we have to add to enable
X.509 certificate authentication. But additional, some efforts will have to be
made for the actual certificate management. Creation and Management of
certificates can be done using two tools provided with your Visual Studio
installation.
These tools are Certmgr.exe and Makecert.exe.
Certmgr
Certmgr.exe is the ECM Certificate Manager and handles tasks as
certificate import/export and management. In detail, it has the following
functions (see MSDN)
- Displays certificates, CTLs, and CRLs to the console.
- Adds certificates, CTLs, and CRLs to a certificate store.
- Deletes certificates, CTLs, and CRLs from a certificate store.
- Saves an X.509 certificate, CTL, or CRL from a certificate store to a file.
MSDN describes Certmgr as follows:
"Certmgr.exe works with two types of certificate stores: StoreFile and system
store. It is not necessary to specify the type of certificate store; Certmgr.exe
can identify the store type and perform the appropriate operations. Running
Certmgr.exe without specifying any options launches a GUI that helps with the
certificate management tasks that are also available from the command line. The
GUI provides an import wizard, which copies certificates, CTLs, and CRLs from
your disk to a certificate store."
Summarized, it's just a tool enabling us to handle installation, deletion and
property management of certificates on our local system.
Makecert
The second tool, Makecert, can generate X.509 certificates for testing
purposes only.
Let's again quote MSDN:
"[�] creates a public and private key pair for digital signatures and stores
it in a certificate file. This tool also associates the key pair with a
specified publisher's name and creates an X.509 certificate that binds a
user-specified name to the public part of the key pair. "
For a detailed explanation of the supported commands have a look at the
documentation.
For public distribution, one needs to purchase a certificate from a
Certification Authority. So don't use the test certificates we create here for
real world scenarios.
For our purposes it's sufficient to create a simple test certificate.
Typing the command
makecert -sk myNewKey -r -n "CN=Test" -ss root myNew.cer
creates such a test certificate, with a publisher's name "Test", and saved to
a file called myNew.cer.
The syntax is as follows (taken from MSDN again - "Digital
Code Signing Step-by-Step Guide"):
- -sk subject key
The location of the subject's key container
which holds the private key. If a key container does not exist, one is created.
If neither of the -sk or -sv options is used, a key container called JoeSoft
will be created by default.
- -r
Creates a self-signed certificate.
- -n name
Name for the publisher's certificate. This name
must conform to the X.500 standard. The simplest method is to use "CN=MyName"
format. For example: -n "CN=Test".
- -ss subjectCertStoreName
Name of the subject's certificate
store where the generated certificate will be stored.
- myNew.cer
Save the certificate to a file and name the file as
saveCertificate.cer.
The certificate is created using the default test root. A key store named
myNewKey is created, and the file is output both to a store and a file.
Adding X.509 Certificates to a SOAP
Message
Now that we have created our own certificated we can start to do the coding.
What we need for our application is a way to open certificate stores and access
the certificates included. Luckily WSE provides us with the means to do that
without any problems.
But before we start the practical part, let's have
just a short look at ASP .NET security.
May I ... Permissions
In our example we want to access the current users root store. To do so, the
application demanding access to this store must have the according rights.
Typically, only the owner and the SYSTEM account are allowed to access the
according files. Since your application could run under any user specific to
your system, it's possible that you need to adapt the access right of the
certificate-related file. If the sample application will be run as SYSTEM or the
files owner, you can skip the next steps.
But otherwise we have some steps
to do.
Typically, a process run by APS.NET will be done so using the ASPNET account,
a low privilege user account. Due to security reasons, it will lack the rights
to access the local machine's root store.
Now you have two choices: either specify the account used by ASP .NET in the
machine.config file, at the key
/configuration/system.web/processModel, changing the attributes userName
and password. The default values are machine for userName and AutoGenerate for
password. Machine means you are running as ASPNET. Change these values to a user
with more the required rights - but note that this could open some doors to
security-relevant parts of your system. So be aware of what you do�
The second option is my favourite, taken that it also works for applications
not run as an ASP.NET process. This option involves adapting the rights of the
files needed to be accessed by your application. These files can be found in the
folder C:\Documents and Settings\All Users\Application
Data\Microsoft\Crypto\RSA\MachineKeys. So perform the following steps to
grant the user your app is executed as the required rights:
- Open Windows Explorer.
- Navigate to the C:\Documents and Settings\All Users\Application
Data\Microsoft\Crypto\RSA\MachineKeys folder.
- Select the needed files, and right-click them to open a menu from where to
select Properties.
- Open the Security tab, add the ASPNET account (or the account you need) and
select the Full Control option.
Now you should be able to go on.
For more information on ASP.NET security
read on at An
Introductory Guide to Building and Deploying More Secure Sites with ASP.NET and
IIS.
The new XML Header Element
How is an X.509 certificate added to and transmitted with a SOAP message? As
we already know, all security information related to the WSE are added to the
<security>
header, introduced in my previous article. While
we there added a <UsernameToken>
element to enable
username/password validation, this time we add a
<BinarySecurityToken>
element. This element will contain the
public version of the certificate, with the certificate itself sent along as
base64 encoded data.
The <BinarySecurityToken>
attributes we need to use are:
ValueType
defines what type of value is included, where
three values are defined:
X509v3
An X.509 version 3 certificate
Kerberosv5ST
A ticket granting ticket as defined by section
5.3.1 of the Kerberos specification
Kerberosv5TGT
A service ticket as defined by section 5.3.1
of the Kerberos specification
EncodingTpye
how the value is encoded
wsse:Base64Binary
wsse:HexBinary
Id
a unique identifier
Taken this, a <BinarySecurityToken>
element passing an
X.509 certificate could look like this:
<wsse:BinarySecurityToken
ValueType="wsse:X509v3"
EncodingType="wsse:Base64Binary"
Id="SecurityToken-c7ff1a4e-276d-4af3-8837-1264ab9f69b3"
>
MIIBtzCCAWGgAwIBAgIQz4ySuWmd/opBywS0LK...
</wsse:BinarySecurityToken>
So addding a security token using the WSE ends up having a
<BinarySecurityToken>
element added to the WSE security
header.
Sending a Certificate
First action to do is to retrieve a certificate that will be added to our
SOAP message.
We start with creating a new store object representing the store we want to
open - in our case the CURRENT_USER\Root store. Typically, a client accesses the
current user store, while the Web Service refers to the local machine store.
X509CertificateStore *store = X509CertificateStore::CurrentUserStore(
X509CertificateStore::RootStore->
The function CurrentUserStore(String* storeName)
creates a new
X509CertificateStore
using the specified store name, here
represented by RootStore->ToString()
. RootStore
is
a constant field representing the predefined system certificate store "Root".
ToString()
should simply return "Root". I encountered some problems
doing it this way, because RootStore
contained a string different
from "Root", thus leading the call CurrentUserStore(�)
to fail. So
try simply typing "Root" if you have any problems.
Now open the store for
reading purposes
store->OpenRead();
and find the certificate you'd like to use. Take one out of the following
functions that fits best to your
needs:
FindCertificateBySubjectString(String*
subjectsubstring)
FindCertificateByHash(unsigned char certHash
__gc[])
FindCertificateByKeyIdentifier(unsigned char
keyIdentifier __gc[])
FindCertificateBySubjectName(String*
subjectstring).
I think the function names are self-explaining. I chose the first method to
find my certificate.
X509CertificateCollection *col = (X509CertificateCollection*)store
->FindCertificateBySubjectString("Hendrik");
All functions return an X509CertificateCollection
object, so we
simply take the first certificate inside the collection for our further
steps.
X509Certificate *cert = col->get_Item(0);
Now having a certificate object, it may be added to our SOAP message and be
used for all security purposes. So simply create a new
X509SecurityToken
object, representing the binary security
token,
X509SecurityToken *x509st = new X509SecurityToken(cert);
and add it to the Tokens
member of the requests SOAP security
header - the same collection we added our UsernameToken
in the
previous article to.
ws->RequestSoapContext->Security->Tokens->Add(x509st);
Here the whole method:
void TestX509Certification()
{
SimpleWebService *ws = new SimpleWebService();
X509CertificateCollection *col = 0;
try {
X509CertificateStore *store = X509CertificateStore::
LocalMachineStore( X509CertificateStore::RootStore );
bool bRead = store->OpenRead();
col = (X509CertificateCollection*)store->
FindCertificateBySubjectString(S"Hendrik");
}
catch( System::InvalidOperationException *e) {
throw new ApplicationException(S"Could not read certificate.");
}
if( col->Count == 0 )
return;
X509Certificate* cert = col->get_Item(0);
if( cert->SupportsDigitalSignature && cert->Key != 0 ) {
X509SecurityToken *x509st = new X509SecurityToken(cert);
ws->RequestSoapContext->Security->Tokens->Add(x509st);
ws->RequestSoapContext->Security->Elements->Add(new Signature(x509st));
}
ws->RequestSoapContext->Timestamp->Ttl = 60000;
Console::WriteLine(ws->HelloWorldX509());
}
Now all steps needed to add a X.509 certificate to a SOAP message are done.
But this alone isn't much a way of authenticating anything. We currently just
have a SOAP message containing a certificate that can be received and verified
by the Web Service. But before we come to actually using that certificate, we
implement the Web Service part of the sample.
Validating received Certificates
The entry point to the service' security part is just the same as in the last
article - retrieving the message's SOAP context and looking for a security
token, now being of type X509SecurityToken
instead of
UsernameToken
. We keep the principal function of our sample Web
Service, returning a simple string. I just called it HelloWorldX509
to clarify its purpose.
SoapContext* sc = HttpSoapContext::RequestContext;
if( sc == 0 )
throw new ApplicationException(S"Only SOAP-requests allowed!");
bool valid = false;
SecurityToken *st = 0;
Enumerator *ie = sc->Security->Tokens->GetEnumerator();
while( ie->MoveNext() ) {
st = (SecurityToken *)ie->get_Current();
if( st != 0 && st->GetType()->Equals(__typeof(X509SecurityToken)) )
{
}
}
Signing a message using an X.509 Certificate
So now we have all prerequisites to finally secure our message, first using
digital signing. Using the WSE, adding signatures to a SOAP message is fairly
easy. You already know the SoapContext::Security::Tokens
element,
and now we make use of the SoapContext::Security::Elements
property. This is a collection of security elements to be added to the message,
like signatures and encryption keys. And here adding a signature is what we're
going to do.
Adding a signature ends up with a new element added to the
<Security>
header. It is called
<Signature>
, and may appear zero or multiple times. The
related namespace is http://www.w3.org/2000/09/xmldsig#.
On the server site, the WSE automatically validates signatures received. The
validation fails if one of the following circumstances occur:
- the syntax of the signature entry content does not conform to the XML
signature specification
- the core validation of the specification fails
These two possibilities are only cryptographic verification related. So
another reason for the validation to fail is if the application, applying its
own trust policy, rejects the message. A cause could be the signature created by
an untrusted key.
The Client Part
On the client side, signing a message just means adding one new line after
the part where we added our binary security token. Just create a new Signature
object, passing our security token as an argument, and add this object to the
Elements
collection.
...
X509SecurityToken *x509st = new X509SecurityToken(cert);
ws->RequestSoapContext->Security->Tokens->Add(x509st);
ws->RequestSoapContext->Security->Elements->Add(new Signature(x509st));
...
Adding a signature is now already done.
Now let's have a look at the snippet of a SOAP message that has been signed
as shown above.
<soap:Envelope>
<soap:Header>
<wsse:Security>
<wsse:BinarySecurityToken ValueType="wsse:X509v3"
EncodingType="wsse:Base64Binary"
xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility"
wsu:Id="SecurityToken-f7997210-3be2-4588-9606-de24cba035ee"
>
MIIBtzCCAWGgAwIBAgIQz4ySuWmd/opByw...
</wsse:BinarySecurityToken>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm=
"http://www.w3.org/2001/10/xml-exc-c14n#" />
<SignatureMethod Algorithm=
"http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<Reference URI="#Id-abf496e3-37e8-4fbb-9e62-b3b456b19d0b">
<Transforms>
<Transform Algorithm=
"http://www.w3.org/2001/10/xml-exc-c14n#" />
</Transforms>
<DigestMethod Algorithm=
"http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>LIBpNIQph1yxuRuWmGXXNy9lEKA=</DigestValue>
</Reference>
<Reference>
...
</Reference>
...
</SignedInfo>
<SignatureValue>ZqEf473ru9847a2TNbbInx1D...</SignatureValue>
<KeyInfo>
<wsse:SecurityTokenReference>
<wsse:Reference
URI="#SecurityToken-f7997210-3be2-4588-9606-de24cba035ee" />
</wsse:SecurityTokenReference>
</KeyInfo>
</Signature>
</wsse:Security>
</soap:Header>
<soap:Body
wsu:Id="Id-abf496e3-37e8-4fbb-9e62-b3b456b19d0b"
xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility"
>
<HelloWorldX509 xmlns="http://tempuri.org/" />
</soap:Body>
</soap:Envelope>
Notice the new element part of the security header, called
<Signature>
. It contains three important sub-elements:
SignedInfo
, SignatureValue
and KeyInfo
.
SignedInfo
defines the data that is being signed in this message,
how it is canonicalized and what algorithm is used for computing the signature.
Inside SignedInfo
you'll see a list of reference elements, which
actually define the elements of the message that are signed. The
URI
attribute now corresponds to the ID
attribute of
the element that is signed. In above snippet, the URI
of the
reference is in fact similar to the body element's ID
, thus the
body is included in the signature.
What you can't see in this snippet is that
most elements of the header are signed by default. In detail these are
(namespaces not included):
- the
Body
element
- the
Created
and Expires elements of the WS-Timestamp SOAP
header
- the
Action
, To
, Id
and
From
elements of the WS-Routing SOAP header
The Web Service
On the server side, it's some more lines of code involved. Remember that we
left our HelloWorldX509
having found a valid X.509 certificate. Now
we have to find a digital signature and verify its conformance with this
certificate. So first gain access to the Elements
property of the
Security
element and start iterating over it.
Object *obj = 0;
IEnumerator *iter = sc->Security->Elements->GetEnumerator();
while( iter->MoveNext() ) {
obj = iter->get_Current();
For every element check whether it's of type Signature
, what we
are of course looking for.
if( obj && obj->GetType()->Equals(__typeof(Signature)) ) {
If successful, we only want to accept the message if the body is signed. To
determine which parts of a message are signed check the
SignatureOptions
property of the Signature
class. The
SignatureOptions
enumeration can be a combination of the following
values:
IncludeNone |
Specifies that no part of the message is signed |
IncludePath |
Specifies that the path element in the routing header should be
signed |
IncludePathAction |
Specifies that the action sub-element of the path element in the routing
header should be signed |
IncludePathFrom |
Specifies that the from sub-element of the path element in the routing
header should be signed |
IncludePathId |
Specifies that the id sub-element of the path element in the routing header
should be signed. |
IncludePathTo |
Specifies that the to sub-element of the path element in the routing header
should be signed |
IncludeSoapBody |
Specifies that the SOAP body should be signed |
IncludeTimestamp |
Specifies that the timestamp should be signed. |
IncludeTimestampCreated |
Specifies that the Created element of a timestamp should be
signed. |
IncludeTimestampExpires |
Specifies that the Expires element of a timestamp should be
signed. |
Since we want the body to be signed, check for the
IncludeSoapBody
value:
Signature *sig = (Signature*)obj;
if( (sig->SignatureOptions & SignatureOptions::IncludeSoapBody) != 0 ) {
Finally ensure that the message body was signed using a X.509 certificate.
Here, you can also check for other security tokens like
UsernameTokens
. If all conditions are met, we know who sent the
message and can be sure that the message arrived unaltered.
if( sig->SecurityToken->GetType()->Equals(__typeof(X509SecurityToken)) )
valid = true;
The complete function body:
String __gc* SimpleWebService::HelloWorldX509()
{
SoapContext* sc = HttpSoapContext::RequestContext;
if( sc == 0 )
throw new ApplicationException(S"Only SOAP-requests allowed!");
bool valid = false;
SecurityToken *st = 0;
IEnumerator *ie = sc->Security->Tokens->GetEnumerator();
while( ie->MoveNext() ) {
st = (SecurityToken *)ie->get_Current();
if( st != 0 && st->GetType()->Equals(__typeof(X509SecurityToken)) ) {
Object *obj = 0;
IEnumerator *iter = sc->Security->Elements->GetEnumerator();
while( iter->MoveNext() ) {
obj = iter->get_Current();
if( obj && obj->GetType()->Equals(__typeof(Signature)) ) {
Signature *sig = (Signature*)obj;
if( (sig->SignatureOptions &
SignatureOptions::IncludeSoapBody) != 0 ) {
if( sig->SecurityToken->GetType()->Equals(
__typeof(X509SecurityToken)) ) {
valid = true;
break;
}
}
}
}
if( valid )
break;
}
}
if( valid == false )
throw new ApplicationException(S"Invalid or missing security token");
return S"Hello! X.509 certificate received.";
}
Now we had a look at a first use of X.509 certificates, signing messages. But
note that you don't need a certificate to sign a message - it can be even done
using user name and password. This means that we can simply extend our previous
article sample, using UsernameTokens
for authentication, to sign
the message.
Having an instance of UsernameToken
called
ut
, simply type
ws->RequestSoapContext->Security->Elements->Add(new Signature(ut));
instead of
ws->RequestSoapContext->Security->Elements->Add(new Signature(x509st));
And viola, the message is signed using a user name. On the server side,
validation is done similar to our validation process - only difference is that
you'd now check for the signature's security token being of type
UsernameToken
instead of X509SecurityToken
.
Signing Parts of a Message
In the previous scenario, we signed the whole body of the SOAP message. But
in some cases, this may be unnecessary or even unwanted. Just remember what I
mentioned at the beginning: "any changes made on the signed data invalidate the
whole signature.". So taken the case an intermediary wants to modify parts of
the body, our whole signature would become invalid. The solution is to sign just
the parts of the message required to be signed.
To sign only a part of a message, specify an Id attribute for every element
that you want to sign and add a reference, as automatically done for the header
elements, to those elements. This is the way WSE creates partial signatures for
the various options that the SignatureOptions
enumeration offers.
Beside that you can create the Id
attribute yourself and manually
add a reference to the SignedInfo
part of the signature. This
allows you to sign only certain parts of the body or the SOAP header.
The Web Service again ...
To have our example use partial signing of messages, we add a custom SOAP
header that will be signed to our Web Service. The header is just simple,
representing kind of a timestamp. Below the class implementing our header:
public __gc class MyHeader :
public System::Web::Services::Protocols::SoapHeader
{
public:
[System::Xml::Serialization::XmlAttributeAttribute("Id",
Namespace="http://schemas.xmlsoap.org/ws/2002/07/utility")]
String *Id;
DateTime Created;
DateTime Expires;
};
As you can see, the header contains just 3 members. Created
and
Expires
are DateTime
values indicating when the header
was created and when it will be expired. That's similar to what the
Timestamp
header does. The third property is a String
called Id
. Its value is the Id of the header element which will be
referenced by the SignedInfo/Reference
element. The Id
must be a message-wide unique identifier that follows the rules of the
xsd:Id type defined at http://www.w3.org/2001/XMLSchema. If
the rules are not followed for the xsd:Id type, the WSE throws an
exception with a message text of "Malformed reference". The namespace of the
attribute must be http://schemas.xmlsoap.org/ws/2002/07/utility, and this
we pass to the XmlAttributeAttribute
constructor.
Now add the custom header to the Web Service by inserting a member of type
MyHeader
.
MyHeader* myHeader;
To be accessible during a function call, you must specify which functions are
using that header. This can be easily done by adding the following line just
above the function declaration:
...
[SoapHeaderAttribute("myHeader", Direction=SoapHeaderDirection::In)]
String __gc* HelloWorld_UsingMyHeader();
...
myHeader
is the attribute representing our header, and
SoapHeaderDirection::In
means that the header is to be used only
for incoming messages. Other possible values would be
SoapHeaderDirection::InOut
and
SoapHeaderDirection::Out
. Now, our new header will be accessible
inside the corresponding functions through the myHeader
member
variable.
To ensure that our header is being signed by the person we expect, we have to
check it by examining all Signature
elements of the security header
again. Having found a Signature
object as before, we now aren't
interested in whether the body is signed. Instead, we retrieve all references
the Signature
contains and check if one of these references to our
header element.
IEnumerator* refIter = sig->SignedInfo->References->GetEnumerator();
while( refIter->MoveNext() ) {
ref = (Reference*)refIter->get_Current();
Finding our header to be signed is a simple comparison of the header
Id
, accessible through myHeader->Id
, and the
reference's Uri
member, which actually points to this
Id
.
if( ref->Uri->Equals(String::Format("#{0}", myHeader->Id)) ) {
...
}
If this line is successful, we know that our header has been signed using the
senders certificate, and that it has not been altered. To make the changes
visible to our client, rebuild the project and include the new Dll in your IIS'
Web Service folder.
Client Changes
The client part is not more difficult. To add the new header to our message,
we first create a MyHeader
object.
MyHeader* header = new MyHeader();
Having created a unique id by help of the Guid
class, we can
assign our Id
member of the header.
Guid refId = Guid::NewGuid();
header->Id = String::Format(S"Id-{0}", refId.ToString());
Then just initialize the Created
and Expires
values
and add the header to our Web Service. If you followed my naming conventions,
wsdl.exe will have created a member called MyHeaderValue
to
your Web Service proxy that then will carry our header object.
header->Created = DateTime::UtcNow;
header->Expires = header->Created.AddMinutes(5);
ws->MyHeaderValue = header;
Last thing to do is to manually add a reference to our header to the
signature we use.
Signature* sig = new Signature(x509st);
Reference* ref = new Reference(String::Format(S"#{0}", header->Id));
sig->AddReference(ref);
As you can see, the reference is initialized with a string equal to the
header's id, only with a # prefix, indicating the reference.
Now any call to the Web Service method expecting the MyHeader
element and lacking it will be rejected by WSE. Additionally, we can reject the
message if the header is not signed or if the signature is due to whatever
reason invalid.
Using Certificates to encrypt Messages
To get the sample work, we again need at least a test X.509 certificate. The
WSE encrypts a message using the public key of the receiver's certificate, and
decrypts the message using the receiver's private key. So since our client will
encrypt the message, a vaild public key of the receiver's certificate needs to
reside inside the Personal certificate store for the current user
account. Additionally, for the server side, the private key for this certificate
must be present in the Personal key store for the local machine account.
And in addition, one of the certificates in the CA certificate chain must be
present in the server's Trusted store so that the WSE knows to trust the
incoming certificate. For test certificate, this will be typically the Root
Agency certificate.
Having handled the mechanisms of signing messages, encryption is a small step
to go. Encrypting messages (or parts of) and the process of decryption are very
similar to the steps we took to sign a message. It's again based on an X.509
certificate which is used to actually encrypt the content. To be used for
encryption, the Key Usage property of the certificate must include
Data Encipherment. Whether a certificate is suitable can be easily found
out by checking the SupportsDataEncryption
property of the
X509Certificate
class.
So let's first handle the client side again.
Sending an encrypted Message
Taking our method from the Signing chapter, all we have to add are the
following lines:
if( cert->SupportsDataEncryption ) {
EncryptedData* ed = new EncryptedData(x509st);
ws->RequestSoapContext->Security->Elements->Add(ed);
}
It's obvious that the process of encrypting a message is completely similar
to signing it. Just create a new instance of EncryptedData
, passing
the security token to use, and add it to the security header elements.
So how do these lines change our SOAP message? First, an
<EncryptedKey>
element will be added to the security header.
Second, an <EncryptedData>
element replaces the original data
contained in the body tag. In detail, every content to be encrypted has to be
replaced by an <EncryptedData>
element. The
<EncryptedKey>
should in turn contain a reference to every
data encrypted according to the information provided there.
With these new elements, an encrypted SOAP message's security header includes
the following part:
<xenc:EncryptedKey
Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
>
<xenc:EncryptionMethod Algorithm=
"http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<wsse:SecurityTokenReference>
<wsse:KeyIdentifier ValueType="wsse:X509v3">
xnps3T2iiIgwG7adOhG9liCyxI4=</wsse:KeyIdentifier>
</wsse:SecurityTokenReference>
</KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>usAVvHo+pm2cL...</xenc:CipherValue>
</xenc:CipherData>
<xenc:ReferenceList>
<xenc:DataReference
URI="#EncryptedContent-163b4068-5237-47ee-b1cb-a3fb655149e9" />
</xenc:ReferenceList>
</xenc:EncryptedKey>
We recognize the new element, <EncryptedKey>
, and the
sub-elements <EncryptionMethod>
,
<KeyInfo>
, <CipherData>
and
<ReferenceList>
. <KeyInfo is>
used for
carrying the key information, allows different key types and provides
extensibility. The <ReferenceList>
again contains references
to the elements encrypted using that key.
The only encrypted element in this example is the body, originally containing
just one element:
<HelloWorldX509_Extended xmlns="http://tempuri.org/" />
Note that whenever you don't specify anything else, the body is the only
element encrypted.
Thats the call to the Web Service Method. Now being encrypted, it is replaced
by an EncryptedData
element, as follows:
<xenc:EncryptedData
Id="EncryptedContent-163b4068-5237-47ee-b1cb-a3fb655149e9"
Type="http://www.w3.org/2001/04/xmlenc#Content"
xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"
>
<xenc:EncryptionMethod Algorithm=
"http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
<xenc:CipherData>
<xenc:CipherValue>EVb5U61VrXSawk...</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
Receiving encrypted Messages
The server side doesn't require any coding action of us. The WSE, if
installed and configured properly, automatically recieves the certificate given
and checks its validity. If successful, the message is decrypted and proceeded.
Otherwise, it is rejected and a failure code will be sent back to the sender of
the message.
The first thing needed for decryption to work is the following entry in the
Web.config file:
<configuration>
<system.web>
<webServices>
<soapExtensionTypes>
<add type="Microsoft.Web.Services.WebServicesExtension,
Microsoft.Web.Services,Version=1.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35"
priority="1" group="0"/>
</soapExtensionTypes>
</webServices>
</system.web>
</configuration>
It enables the Web Service to use the WSE. But since we already did that as
part of the previous article, there is nothing to do for us here.
Now we only need to tell the WSE where to look for the private key of the
certificate, which is needed for the decryption. Since we put it in the
Personal store for the local machine account, this is where we point to.
This is done by adding an <x509>
element to
/configuration/microsoft.web.services/security in Web.config. The
associated value is storeLocation
, which we will assign the value
LocalMachine
for the local machine account. The other possible
value would be CurrentUser
, but this is more practible for clients
receiving encrypted messages.
Second value to set is verifyTrust
, which must be set to
false
to allow test certificates to be trusted. Never allow this in
a productive environment, but for test purposes only.
<configuration>
<microsoft.web.services>
<security>
<x509 storeLocation="LocalMachine" verifyTrust="false"
allowTestRoot="true" />
</security>
</microsoft.web.services>
</configuration>
Specifying Parts to be encrypted
Encrypting only parts of a SOAP messages is similar to partial signing of
messages. We again need the id of the element to be encrypted. In case we want
to encrypt our custom header, the id has to be created the way we already did.
Then, we create a EncryptedData
object, with the security token as
first parameter. So the only difference to encrypting the whole body only is to
add a second parameter, the reference to the Id
.
String *ref = String::Format(S"#{0}", header->Id);
EncryptedData* ed = new EncryptedData(x509st, ref);
Then add the element to the security elements:
ws->RequestSoapContext->Security->Elements->Add(ed);
Regarding the client, determining the encrypted parts is again similar to
partial signatures. You need to iterate over the Elements
collection of the Security
property, and to determine whether the
current object is of type EncryptedData
. For each element in the
SOAP message that is encrypted, there will be such an EncryptedData
element in the Elements
collection.
A code snippet would look like follows.
Object *obj = 0;
IEnumerator *iter = sc->Security->Elements->GetEnumerator();
while( iter->MoveNext() ) {
obj = iter->get_Current();
if( obj && obj->GetType()->Equals(__typeof(EncryptedData)) ) {
}
}
I didn't implement these things in my example since the technique doesn't
differ from signing parts of a message. But I'm sure you won't have any problems
implementing such a scenario.
Testing the Example
Installing the example is similar to the previous article, so please have a
look there regarding any questions.
Now the client contains three different functions:
TestAuthentication()
- uses username/password authentication, and is just the function of the
previous article
- calls the Web Service's
HelloWorld()
function
TestX509Certification()
- uses an X.509 certificate to authenticate, and the message is signed and
encrypted with this certificate
- calls
HelloWorldX509()
on the server side
TestX509Certification_Extended()
- encrypts the messge using an X.509 certificate, additionaly sending and
encryptin a custom message header
- the Web Service's counterpart is
HelloWorldX509_Extended()
For testing just call one of the functions above.
Read on ...
... at
Perspective
Now having the first insight on encryption and signatures, the next article
will continue handling encryption using a shared secret. Also, I'm going to
introduce custom binary security tokens.