by Joe Ward

Joe Ward

Created

10 June 2010

Requirements
Prerequisite knowledge

General experience of building applications
with Flash CS4 or ActionScript is suggested.
For more details on getting started with
this Quick Start, refer to Building the Quick
Start sample applications with Flash.

Required products

Sample files

User level

Intermediate
 
Adobe AIR allows you to load and execute downloaded SWF and HTML content in the application security sandbox. When you do so, however, you take on the responsibility to safeguard your application—and your users' computers—from the execution of malicious code. One precaution that you can take is to verify that any downloaded resources are, in fact, your content and that they have not been modified since leaving your hands. You can use XML signatures to solve this problem by signing downloadable content and verifying the signature before loading it into your application. This article discusses how to use the XMLSignatureValidator class to help you validate the integrity and authenticity of signed messages and downloaded resources.
 
There aren't many off-the-shelf XML signing tools available, so this article also discusses how you can use the AIR ADT tool to create signed resource packages. Alternately, you can use the Java XML Digital Signature API. The AIR XMLSignatureValidator class can validate a subset of possible XML signatures as defined in the W3C Recommendation for XML-Signature Syntax and Processing.
 
This article discusses:
 
  • Using the XMLSignatureValidator and related classes
  • Interpreting validation results
  • Validating external resources
  • Implementing the IURIDereferencer interface
  • Creating a signed UCF package with ADT
  • Signing an XML document with Java
The XMLSignatureValidation example application on Microsoft Windows.
Figure 1. The XMLSignatureValidation example application on Microsoft Windows.
 
Note: This is an example application provided, as is, for instructional purposes.
 

 
Understanding the code

The XMLSignatureValidation application described in this article lets you validate the signature of installed AIR applications. The logic for verifying an AIR signature is divided into two steps. First, the signature itself is verified. Next, the files listed in the package manifest are verified. The validate() method creates an XMLSignatureValidator object, sets the validation properties and validates the signature. If validation completes successfully, the XMLSignatureValidator object dispatches a complete event. The complete event handler displays the results and, if the signature is valid, calls the verifyManifest() method to check the files in the package. The application is also able to validate enveloped XML signatures.
 
To validate an XML signature, you will use the following classes from the flash.security package:
 
  • XMLSignatureValidator
  • IURIDereferencer
  • ReferencesValidationSetting
  • RevocationCheckSettings
  • SignatureStatus
As well as the following Flex framework utility classes:
 
  • mx.utils.Base64Decoder
  • mx.utils.Base64Encoder
  • mx.utils.SHA256
These utility classes are used to help extract the signing certificate from a signature and to compute the digest of external resources. In an application created with Flash CS, you can import these Flex classes using a SWC library. A SignatureUtils.swc file containing these classes is included in this example. You can also build your own library file using the Flex SDK. If you do not want to use Flex, then you will have to provide suitable SHA256 and Base 64 utilities.
 
To add the SWC to your project in Flash CS4, open the Advanced ActionScript 3.0 Settings dialog, and add the SWC under the Library path tab. If you are using Flash CS3, you can only load the library at runtime. To do this, open the SWC file with your favorite ZIP archive tool and extract the Library.swf file. This SWF file contains the utility classes. Load this SWF file into your application with the Loader class.
 
 
Preparing to validate
First, create an XMLSignatureValidator object and add event listeners for the complete and error events:
 
var verifier:XMLSignatureValidator = new XMLSignatureValidator(); verifier.addEventListener(Event.COMPLETE, signatureVerificationComplete); verifier.addEventListener(ErrorEvent.ERROR, signatureVerificationError);
Next, set the validation options:
 
var decoder:Base64Decoder = new Base64Decoder(); decoder.decode(xmlSig..signatureNS::X509Certificate); var certificate:ByteArray = decoder.toByteArray(); verifier.useSystemTrustStore = true; verifier.addCertificate(certificate, true); verifier.revocationCheckSetting = RevocationCheckSettings.BEST_EFFORT;
 
Establishing the parameters of trust
There are two ways to designate how you trust the signing certificate. You can add the certificate as a trust anchor with the addCertificate() method, or you can base trust on the system trust store. The system trust store is a repository of certificates that the user or the operating system publisher have designated as trusted. Many of these certificates are the root or intermediate certificates issued by certification authorities, which are used to sign the certificates issued by these authorities.
 
Normally, you should only add a known certificate using the addCertificate() method. For example, if you are using XML signatures to validate downloaded resources, you might only want to trust your own certificate. In this case, the certificate could be extracted from the application signature (after validating that it hadn't been tampered with), and then used to validate the resources. You could also include the certificate with your application separately for use with the XMLSignatureValidator (taking care that it cannot simply be replaced by another certificate).
 
In this XMLSignatureValidation example, we extract the certificate from the signature and use the addCertificate() method to explicitly trust it. Adding the certificate this way will always result in a valid status for the certificate identity (unless the signature document has been tampered with or the certificate is revoked or has expired), but that's rarely what you want when validating signatures. In this case, though, the goal is to display information about the signature, the certificate, and any signed files, so it is what we want. However, this is only safe because we aren't going to do anything with the validated files.
 
If you set the useSystemTrustStore property to true, then the the certificate will be trusted if it chains to a certificate in the system trust store. Usually this means the certificate has been issued by a well-known certification authority, but it is also possible that a user has imported the certificate into their trust store. Setting the useSystemTrustStore property to true is useful for some purposes, but also may not be what you want in all cases. While the identity of the entity owning a certificate issued by a certification authority has been verified by the authority, you still might not want to trust that entity.
 
Certificates can also be revoked if the certificate key is stolen or compromised. AIR gives you several options for checking revocation, ranging from always checking, to checking whenever possible, to never checking. This example uses the bestEffort setting for the revocationCheckSetting property so that revocation will be checked if the information is available and the user is online.
 
The XMLSignatureValidator dereferencer property must be set to an object that implements the IURIDereferencer interface. The IURIDereferencer object is responsible for getting the data addressed by a URI in the signature and returning it to the XMLSignatureValidator as a ByteArray object. How the dereferencer finds the data is entirely up to you. In this example, the signed document is passed to the implementation class' constructor. This dereferencer implementation can only be used to validate references contained within the signed document. (External resources in the signed manifest are checked in a separate step.)
 
The LimitedSignatureDereferencer class is the dereferencer used in this example. It is designed to dereferencer AIR-style package signatures, as well as enveloped signatures.
 
var dereferencer:IURIDereferencer = new LimitedSignatureDereferencer(xmlDoc); verifier.uriDereferencer = dereferencer;
 
Validating the signature
Verify the signature with the XMLSignatureValidator verify() method. Only the Signature element from the signed document is passed to the verification function. In some cases, the Signature element is the root of the signed document, but often you will need to extract the Signature element (and its children) from the parent document.
 
verifier.verify(xmlSig);
The XMLSignatureValidator object will dispatch a complete event when validation is finished (even if the signature is invalid). An error event is dispatched if the validator object cannot process the signature. Failures can be caused by XML syntax errors in the document or other problems.
 

 
Interpreting validation results

When complete, the validation results are reported by the validityStatus property. The validityStatus property is, in turn, determined by the individual digestStatus, identityStatus, and referencesStatus properties. If any of these three statuses are invalid, the validityStatus is also invalid. If any of these three statuses is unknown (and none are invalid), then the validityStatus is unknown. Only when all three individual statuses are valid, will the validityStatus be valid.
 
When a signature is verified, the digest of the signature is checked first. If the digest is invalid, then nothing else is checked because this means the signature document has been modified since it was signed. The identity status is checked next. The identity status is based on the signing certificate. The status is invalid if the certificate is expired, has been revoked, or is malformed. The status is unknown if the certificate is not trusted. Finally, the references status is checked. If the certificate status is invalid, the references are not checked. If the certificate status is unknown, the references are only checked if the referencesValidation setting is set to ReferencesValidationSetting.VALID_OR_UNKNOWN_IDENTITY. You can also specify that references not be checked.
 
You should be very cautious, when designing a signature verification system, that you only rely on data that is signed and validated. For example, in an AIR-type signature, only the Manifest element itself is signed by the signature. The files listed in the manifest are not signed and are not validated when the signature is verified. You could modify, replace, or delete any files and the signature would still be valid. The AIR application installer and this example application do verify the external files listed in the manifest, but this verification is performed as an extra step and is not performed by the XMLSignatureValidator itself.
 
The complete event handler displays the validation results and, if the referencesStatus is valid, calls verifyManifest() to validate the external resources.
 
private function signatureVerificationComplete(event:Event):void { var signature:XMLSignatureValidator = event.target as XMLSignatureValidator; //Report the verification results log("Signature status: " + signature.validityStatus + "\n"); log(" Digest status: " + signature.digestStatus + "\n"); log(" Identity status: " + signature.identityStatus + "\n"); log(" Reference status: " + signature.referencesStatus + "\n"); //Display certificate information if( signature.identityStatus == SignatureStatus.VALID ){ log("\nSigning certificate information:\n"); log(" Common name field: " + signature.signerCN + "\n"); log(" Distinguished name field: " + signature.signerDN + "\n"); log(" Extended key usage OIDs: " + formatArray(signature.signerExtendedKeyUsages) + "\n"); log(" Trust settings: " + formatArray(signature.signerTrustSettings) + "\n"); } else { log("\nSigning certificate information only available for valid certificates.\n"); } //Verify the referenced files, but only if signature is valid if( signature.referencesStatus == SignatureStatus.VALID ){ var manifest:XMLList = xmlSig.signatureNS::Object.signatureNS::Manifest; if( manifest.length() > 0 ){ verifyManifest( manifest ); } } else { log("\nManifest only validated when signature references are valid.\n"); } }

 
Validating external resources

After validating the signature, we've only established that the signed portions of the signature document itself are unaltered. To fully validate an AIR-type signature, the files listed in the manifest have to be validated as well. To do this, you can compare the validated digest of each file stored in the signed manifest in the XML signature to a digest computed from the current bytes of the file. If the two digests match, then the file hasn't been altered since it was signed.
 
The example creates a File object for each entry in the Manifest element, and loads the file data into a ByteArray object. The URI paths are resolved relative to a parent directory of the META-INF directory containing the signature file. This corresponds to the structure of an AIR package, but is certainly not a universal convention.
 
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 );
The SHA256 digest of the file is computed using the SHA256 class from the mx.utils package. The computeDigest() method returns a string of hexadecimal characters. However, what we really need is a byte array containing the hexadecimal numbers, so a conversion is performed:
 
var digestHexString:String = SHA256.computeDigest( fileData ); var digest:ByteArray = new ByteArray(); for( var c:int = 0; c < digestHexString.length; c += 2 ){ var byteChar:String = digestHexString.charAt(c) + digestHexString.charAt(c+1); digest.writeByte( parseInt( byteChar, 16 )); } digest.position = 0;
The digest in the signature is Base64 encoded, so the computed digest must also be encoded. The Base64Encoder class from the mx.utils package is used. If the two digests are identical, then the resource is not changed.
 
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"; }

 
Dereferencing references

References in an XML signature contain a URI attribute that addresses the referenced data. The URI value could be an ID of another XML element in the signature document. It could be a relative path to an external file. It could be a URL. Or, it could be something else entirely. The IURIDereferencer implementation you assign to the XMLSignatureValidator object must dereference the URIs in the same way as the code creating the signature. Otherwise, the digest computed for the reference will not match the digest stored in the signature and validation will fail. The W3C XML-Signature Syntax and Processing recommendation does not specify a definitive way to create or interpret URI values. Therefore, you must know the convention used by the signing application before you can validate a signature. XPATH syntax is a recommended and commonly used method for referencing XML elements. Typically, when the reference addresses an XML element by its id attribute, a pound sign (#) followed by the ID value is used. When the entire XML document is signed and encloses the signature, an empty string is used as the URI.
 
This example implements a IURIDereferencer class designed for the type of signatures used for AIR applications. In an AIR signature, the package file names and digest values are stored in a Manifest element. This Manifest element is signed and referenced in the signature by the ID, "PackageContents". When validating an AIR-type signature, the XMLSignatureValidator object passes the value #PackageContents to the dereferencer object. The dereferencer looks for a Manifest element with this ID. If found, the element and its children are copied into a ByteArray object and returned. The XMLSignatureValidator object then computes the digest of that ByteArray and compares the digest value to that stored in the Reference element within the signature.
 
The IURIDereferencer interface has a single method, dereference() that takes a URI as a string and returns an object, such as a ByteArray, that implements the IDataInput interface. The interface doesn't specify a way to tell your implementation how to find the data it is supposed to dereference.You can add properties or methods to your implementation class for this. The example shown here passes the XML signature document to the class constructor. The dereferencer object then looks for the URI in this document when the dereference() method is called. The example dereference() method performs some minimal validation of the passed URI and throws an error if the URI cannot be resolved.
 
package { import flash.security.IURIDereferencer; import flash.utils.ByteArray; import flash.utils.IDataInput; public class LimitedSignatureDereferencer implements IURIDereferencer { private const signatureNS:Namespace = new Namespace( "http://www.w3.org/2000/09/xmldsig#" ); private var signedDocument:XML; public function LimitedSignatureDereferencer( signedDocument:XML ) { this.signedDocument = signedDocument; } public function dereference( uri:String ):IDataInput { var data:ByteArray = null; try { data = new ByteArray(); if( uri.length == 0 ) { data.writeUTFBytes( signedDocument.toXMLString() ); data.position = 0; } else if( uri.match(/^#/) ) { var manifest:XMLList = signedDocument..signatureNS::Manifest.(@Id == uri.slice( 1, uri.length )); if (manifest.length() == 0){ //try lower case id attribute manifest = signedDocument..signatureNS::Manifest.(@id == uri.slice( 1, uri.length )); if( manifest.length() == 0 ){ //give up throw new Error("Manifest with matching id attribute not found."); } } data.writeUTFBytes( manifest.toXMLString() ); data.position = 0; } else { throw( new Error("Unsupported signature type.") ); } } catch (e:Error) { data = null; throw new Error("URI not resolvable: " + uri + ", " + e.message); } finally { return data; } } } }
For good measure, this dereferencer can also resolve the empty string used as a reference to the root of the signed document. Thus, it can be used to validate enveloped signatures as well. In an enveloped signature, the Signature element is inserted into the signed document as a child of an existing element. Enveloped signatures must specify the enveloped-signature transform algorithm in the Reference element. When this transform algorithm is specified, the XMLSignatureValidator automatically removes the Signature element from the document before computing the digest of the data returned by the dereference() method. Your dereference() method should not remove the Signature element itself. An example of an enveloped signature is included with the source files for this article.
 

 
Loading a signature document

When a signature file is loaded, the example does some simple validation to check that the document is an XML signature. It then extracts the Signature portion of the document. You must pass the Signature element (and its children) to the XMLSignatureValidator verify() method.
 
private function loadFile(sigFile:File):void{ clearLog(); var sigFileStream:FileStream = new FileStream(); sigFileStream.open(sigFile, FileMode.READ); var fileContents:String = sigFileStream.readUTFBytes(sigFileStream.bytesAvailable); xmlDoc = XML( fileContents ); var signatureList:XMLList = xmlDoc..signatureNS::Signature; if( signatureList.length() > 0 ){ xmlSig = XML( signatureList[signatureList.length()-1] ); showSignatureText( xmlDoc.toXMLString() ); } else { showSignatureText( sigFile.name + " does not contain a recognized XML signature." ); } }

 
Creating an XML signature

The easiest way to create an XML signature for a set of files is to use the AIR Developer Tool (ADT). This tool is used with the Flex SDK and, behind the scenes, in Flash CS to create AIR installation files. You can use ADT to sign an arbitrary set of files by creating a dummy application descriptor file. Because ADT validates that the initial content is either a SWF or an HTML file, and that it is included in the package, you will also need a dummy HTML file to use in the content element of the application descriptor.
 
For example, if you had a directory containing resources to sign, and a dummy application descriptor named ResourcePack-mod.xml (that specified a file such as, description.html, in the content element), then you could use the following ADT command to create the package (note the dot on the end that instructs ADT to include all files in the directory as part of the package):
 
adt -package -storetype pkcs12 -keystore c:/certStore/testCert.pfx ResourcePack.zip ResourcePack-mod.xml .
 
This command sets the .zip file extension for the package to distinguish it from an AIR application. ADT creates a UCF package, which is an archive format very similar to the ZIP format. (The UCF format requires that certain files appear uncompressed at the beginning of the archive.) You can unzip the package with standard ZIP archive tools and validate the signature. Remember that if you change the relative location of the signature file to the external resources, you will have to modify the URIDereferencer so that it can resolve the new relative path. It is also possible to use the package directly and unpackage it using ActionScript. This is a convenient way to download and install resources. You could, for example, download a resource archive and unpackage it into a folder in the application storage directory. You could then use the XMLSignatureValidator to verify the resources before loading them. An example ActionScript class, Unpackager, for unpackaging UCF archives is included in the source files for this article.
 
Note: AIR does not currently provide an API for creating XML signatures.
 
 
Creating an enveloped signature with Java
You can create XML signatures using the Java Digital Signature API. See Java XML Digital Signature API for more information. Because the XMLSignatureValidator class does not implement the full W3C XML-Signature Syntax and Processing recommendation, not all legal XML signatures created with Java can be validated with AIR.
 
The following Java utility shows how to create an enveloped XML signature that is compatible with the AIR XMLSignatureValidator. You can use a certificate generated by ADT or other tools to sign XML messages. Note that the XMLSignatureValidator class does not always canonicalize white space and namespaces properly. Avoid white space between XML elements and redefining the default namespace within the XML document to be signed.
 
import java.io.FileInputStream; import java.io.FileOutputStream; import java.security.Key; import java.security.KeyPair; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Collections; import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.crypto.dsig.DigestMethod; import javax.xml.crypto.dsig.Reference; import javax.xml.crypto.dsig.SignatureMethod; import javax.xml.crypto.dsig.SignedInfo; import javax.xml.crypto.dsig.Transform; import javax.xml.crypto.dsig.XMLSignature; import javax.xml.crypto.dsig.XMLSignatureFactory; import javax.xml.crypto.dsig.dom.DOMSignContext; import javax.xml.crypto.dsig.keyinfo.KeyInfo; import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; import javax.xml.crypto.dsig.keyinfo.X509Data; import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; import javax.xml.crypto.dsig.spec.TransformParameterSpec; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; public class EnvelopedSigner { private static String docToSign = "message.xml"; private static String outputFileName = "signedmessage.xml"; public static void main(String[] args) { try{ //load the XML doc to sign DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); Document xmlDoc = docBuilder.parse(new FileInputStream( docToSign )); //Load the keystore containing the keys KeyStore keystore = KeyStore.getInstance("PKCS12"); char[] password = "password".toCharArray(); keystore.load(new FileInputStream("pkcs12Cert.p12"), password); KeyPair key = getKeyPair(keystore, "1", password); //Create the signing context DOMSignContext dsc = new DOMSignContext (key.getPrivate(), xmlDoc.getDocumentElement()); //Manufacture a signer object XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); //Specify the Reference attributes Reference ref = fac.newReference ("", fac.newDigestMethod(DigestMethod.SHA256, null), Collections.singletonList (fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null)), null, null); //Specify the SignedInfo attributes SignedInfo si = fac.newSignedInfo (fac.newCanonicalizationMethod (CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null), fac.newSignatureMethod(SignatureMethod.RSA_SHA1, null), Collections.singletonList(ref)); //Setup the KeyInfo attributes KeyInfoFactory kif = fac.getKeyInfoFactory(); ArrayList<Certificate> certificateList = new ArrayList<Certificate>(); certificateList.add(keystore.getCertificate("1")); X509Data kv = kif.newX509Data(certificateList); KeyInfo ki = kif.newKeyInfo(Collections.singletonList(kv)); //Sign the document XMLSignature signature = fac.newXMLSignature(si, ki); signature.sign(dsc); //Save the new signed XML file TransformerFactory tf = TransformerFactory.newInstance(); Transformer trans = tf.newTransformer(); trans.transform(new DOMSource(xmlDoc), new StreamResult(new FileOutputStream(outputFileName))); } catch (Exception e){ System.err.println("Signing error: " + e.getMessage()); e.printStackTrace(); } } public static KeyPair getKeyPair(KeyStore keystore, String alias, char[] password) { try { // Get private key Key key = keystore.getKey(alias, password); if (key instanceof PrivateKey) { // Get certificate of public key Certificate cert = keystore.getCertificate(alias); // Get public key PublicKey publicKey = cert.getPublicKey(); // Return a key pair return new KeyPair(publicKey, (PrivateKey)key); } } catch (UnrecoverableKeyException e) { } catch (NoSuchAlgorithmException e) { } catch (KeyStoreException e) { System.err.println("key error: " + e.getMessage()); e.printStackTrace(); } return null; } }