To
validate an XML signature, you must provide an implementation of
the IURIDereferencer interface. The implementation is responsible
for resolving the URIs within the Reference elements of an XML signature
document and returning the data so that the digest can be computed.
The computed digest is compared with the digest in the signature
to determine if the referenced data has been altered since the signature
was created.
Note:
HTML-based AIR applications must import a SWF library containing
an ActionScript implementation in order to validate XML signatures.
The IURIDereferencer interface cannot be implemented in JavaScript.
The IURIDerefencer interface has a single method,
dereference(uri:String)
,
that must be implemented. The XMLSignatureValidator object calls
this method for each reference in the signature. The method must
return the data in a ByteArray object.
In most cases, you will also need to add properties or methods
that allow your dereferencer object to locate the referenced data.
For example, if the signed data is located in the same document
as the signature, you could add a member variable that provides
a reference to the XML document. The
dereference()
method
can then use this variable, along with the URI, to locate the referenced data.
Likewise, if the signed data is located in a directory of the local
file system, the
dereference()
method might need
a property providing the path to that directory in order to resolve
the referenced files.
The XMLSignatureValidator relies entirely on the dereferencer
for interpreting URI strings. The standard rules for dereferencing
URIs are given in the section 4.3.3 of the W3C Recommendation for
XML Signature Syntax and Processing.
Dereferencing URIs in enveloped signatures
When an enveloped XML signature is generated, the signature elements
are inserted into the signed data. For example, if you signed the
following message using an enveloped signature structure:
<message>
<data>...</data>
</message>
The resulting signed document will look like this:
<message>
<data>...</data>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>yv6...Z0Y=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>cCY...LQ==</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>MII...4e</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
</message>
Notice that the signature contains a single Reference element
with an empty string as its URI. An empty string in this context
refers to the root of the document.
Also notice that the transform algorithm specifies that an enveloped
signature transform has been applied. When an enveloped signature
transform has been applied, the XMLSignatureValidator automatically
removes the signature from the document before computing the digest.
This means that the dereferencer does not need to remove the Signature
element when returning the data.
The following example illustrates a dereferencer for enveloped
signatures:
package
{
import flash.events.ErrorEvent;
import flash.events.EventDispatcher;
import flash.security.IURIDereferencer;
import flash.utils.ByteArray;
import flash.utils.IDataInput;
public class EnvelopedDereferencer
extends EventDispatcher implements IURIDereferencer
{
private var signedMessage:XML;
public function EnvelopedDereferencer( signedMessage:XML )
{
this.signedMessage = signedMessage;
}
public function dereference( uri:String ):IDataInput
{
try
{
if( uri.length != 0 )
{
throw( new Error("Unsupported signature type.") );
}
var data:ByteArray = new ByteArray();
data.writeUTFBytes( signedMessage.toXMLString() );
data.position = 0;
}
catch (e:Error)
{
var error:ErrorEvent =
new ErrorEvent("Ref error " + uri + " ", false, false, e.message);
this.dispatchEvent(error);
data = null;
throw new Error("Reference not resolvable: " + uri + ", " + e.message);
}
finally
{
return data;
}
}
}
}
This dereferencer class uses a constructor function with a parameter,
signedMessage
,
to make the enveloped signature document available to the
dereference()
method.
Since the reference in an enveloped signature always refers to the
root of the signed data, the
dereferencer()
method writes
the document into a byte array and returns it.
Dereferencing URIs in enveloping and detached signatures
When the signed data is located in the same document as the signature
itself, the URIs in the references typically use XPath or XPointer
syntax to address the elements that are signed. The W3C Recommendation
for XML Signature Syntax and Processing only recommends this syntax,
so you should base your implementation on the signatures you expect
to encounter (and add sufficient error checking to gracefully handle
unsupported syntax).
The signature of an AIR application is an example of an enveloping
signature. The files in the application are listed in a Manifest
element. The Manifest element is addressed in the Reference URI
attribute using the string, “#PackageContents”, which refers to
the Id of the Manifest element:
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="PackageSignature">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/TR/xmldsig-core#rsa-sha1"/>
<Reference URI="#PackageContents">
<Transforms>
<Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>ZMGqQdaRKQc1HirIRsDpeBDlaElS+pPotdziIAyAYDk=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue Id="PackageSignatureValue">cQK...7Zg==</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>MII...T4e</X509Certificate>
</X509Data>
</KeyInfo>
<Object>
<Manifest Id="PackageContents">
<Reference URI="mimetype">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256">
</DigestMethod>
<DigestValue>0/oCb84THKMagtI0Dy0KogEu92TegdesqRr/clXct1c=</DigestValue>
</Reference>
<Reference URI="META-INF/AIR/application.xml">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256">
</DigestMethod>
<DigestValue>P9MqtqSdqcqnFgeoHCJysLQu4PmbUW2JdAnc1WLq8h4=</DigestValue>
</Reference>
<Reference URI="XMLSignatureValidation.swf">
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256">
</DigestMethod>
<DigestValue>OliRHRAgc9qt3Dk0m0Bi53Ur5ur3fAweIFwju74rFgE=</DigestValue>
</Reference>
</Manifest>
</Object>
</Signature>
A dereferencer for validating this signature must take the URI
string containing,
"#PackageContents"
from the
Reference element, and return the Manifest element in a ByteArray
object. The “#” symbol refers to the value of an element Id attribute.
The following example implements a dereferencer for validating
AIR application signatures. The implementation is kept simple by
relying on the known structure of an AIR signature. A general-purpose
dereferencer could be significantly more complex.
package
{
import flash.events.ErrorEvent;
import flash.security.IURIDereferencer;
import flash.utils.ByteArray;
import flash.utils.IDataInput;
public class AIRSignatureDereferencer implements IURIDereferencer {
private const XML_SIG_NS:Namespace =
new Namespace( "http://www.w3.org/2000/09/xmldsig#" );
private var airSignature:XML;
public function AIRSignatureDereferencer( airSignature:XML ) {
this.airSignature = airSignature;
}
public function dereference( uri:String ):IDataInput {
var data:ByteArray = null;
try
{
if( uri != "#PackageContents" )
{
throw( new Error("Unsupported signature type.") );
}
var manifest:XMLList =
airSignature.XML_SIG_NS::Object.XML_SIG_NS::Manifest;
data = new ByteArray();
data.writeUTFBytes( manifest.toXMLString());
data.position = 0;
}
catch (e:Error)
{
data = null;
throw new Error("Reference not resolvable: " + uri + ", " + e.message);
}
finally
{
return data;
}
}
}
}
When you verify this type of signature, only the data in the
Manifest element is validated. The actual files in the package are
not checked at all. To check the package files for tampering, you
must read the files, compute the SHA256 digest and compare the result
to digest recorded in the manifest. The XMLSignatureValidator does
not automatically check such secondary references.
Note:
This example is provided only to illustrate the signature
validation process. There is little use in an AIR application validating
its own signature. If the application has already been tampered
with, the tampering agent could simply remove the validation check.
Computing digest values for external resources
AIR does not include built-in functions for computing SHA256
digests, but the Flex SDK does include a SHA256 utility class. The
SDK also includes the Base64 encoder utility class that is helpful
for comparing the computed digest to the digest stored in a signature.
The following example function reads and validates the files
in an AIR package manifest:
import mx.utils.Base64Encoder;
import mx.utils.SHA256;
private function verifyManifest( sigFile:File, manifest:XML ):Boolean
{
var result:Boolean = true;
var message:String = '';
var nameSpace:Namespace = manifest.namespace();
if( manifest.nameSpace::Reference.length() <= 0 )
{
result = false;
message = "Nothing to validate.";
}
for each (var reference:XML in manifest.nameSpace::Reference)
{
var file:File = sigFile.parent.parent.resolvePath( reference.@URI );
var stream:FileStream = new FileStream();
stream.open(file, FileMode.READ);
var fileData:ByteArray = new ByteArray();
stream.readBytes( fileData, 0, stream.bytesAvailable );
var digestHex:String = SHA256.computeDigest( fileData );
//Convert hexidecimal string to byte array
var digest:ByteArray = new ByteArray();
for( var c:int = 0; c < digestHex.length; c += 2 ){
var byteChar:String = digestHex.charAt(c) + digestHex.charAt(c+1);
digest.writeByte( parseInt( byteChar, 16 ));
}
digest.position = 0;
var base64Encoder:Base64Encoder = new Base64Encoder();
base64Encoder.insertNewLines = false;
base64Encoder.encodeBytes( digest, 0, digest.bytesAvailable );
var digestBase64:String = base64Encoder.toString();
if( digestBase64 == reference.nameSpace::DigestValue )
{
result = result && true;
message += " " + reference.@URI + " verified.\n";
}
else
{
result = false;
message += " ---- " + reference.@URI + " has been modified!\n";
}
base64Encoder.reset();
}
trace( message );
return result;
}
The function loops through all the references in the Manifest
element. For each reference, the SHA256 digest is computed, encoded
in base64 format, and compared to the digest in the manifest. The
URIs in an AIR package refer to paths relative to the application
directory. The paths are resolved based on the location of the signature
file, which is always in the META-INF subdirectory within the application
directory. Note that the Flex SHA256 class returns the digest as
a string of hexadecimal characters. This string must be converted
into a ByteArray containing the bytes represented by the hexadecimal
string.
To use the mx.utils.SHA256
and Base64Encoder classes in Flash CS4, you can either locate and
copy these classes into your application development directory or
compile a library SWF containing the classes using the Flex SDK.
To use
the mx.utils.SHA256 and Base64Encoder classes in an HTML-based AIR
application, you can compile a library SWF containing the classes
using the Flex SDK and add the SWF to your HTML page using a
<script>
tag.
Dereferencing URIs in detached signatures referencing external data
When a URI refers to an external resource, the data must be accessed
and loaded into a ByteArray object. If the URI contains an absolute
URL, then it is simply a matter of reading a file or requesting
a URL. If, as is probably the more common case, the URI contains
to a relative path, then your IURIDereferencer implementation must
include a way to resolve the paths to the signed files.
The following example uses a File object initialized when the
dereferencer instance is constructed as the base for resolving signed
files.
package
{
import flash.events.ErrorEvent;
import flash.events.EventDispatcher;
import flash.filesystem.File;
import flash.filesystem.FileMode;
import flash.filesystem.FileStream;
import flash.security.IURIDereferencer;
import flash.utils.ByteArray;
import flash.utils.IDataInput;
public class RelativeFileDereferencer
extends EventDispatcher implements IURIDereferencer
{
private var base:File;
public function RelativeFileDereferencer( base:File )
{
this.base = base;
}
public function dereference( uri:String ):IDataInput
{
var data:ByteArray = null;
try{
var referent:File = this.base.resolvePath( uri );
var refStream:FileStream = new FileStream();
data = new ByteArray();
refStream.open( referent, FileMode.READ );
refStream.readBytes( data, 0, data.bytesAvailable );
} catch ( e:Error ) {
data = null;
throw new Error("Reference not resolvable: " + referent.nativePath + ", " + e.message );
} finally {
return data;
}
}
}
}
The
dereference()
function simply locates the
file addressed by the reference URI, loads the file contents into
a byte array, and returns the ByteArray object.
Note:
Before validating remote external references, consider whether
your application could be vulnerable to a “phone home” or similar
type of attack by a maliciously constructed signature document.
|
|
|