Signed and sealed objects deliver secure serialized content
Protect information with the SignedObject and SealedObject classes
A developer building an application will encounter many situations in which he or she will want to protect the integrity and/or contents of a serialized object as it is transferred over or stored on untrusted media. Consider a distributed application in which a set of loosely coupled servers must serve a legion of mobile clients — clients that can connect to and disconnect from any one of the servers at any time. When the servers’ state is not synchronized or cannot be kept synchronized, the clients must assist with the state maintenance. If the transactions the clients engage in span more than one connection with more than one server, the client must maintain a state that contains the information necessary to reestablish transaction context with each server. If the clients, their users, or the underlying transmission and storage media cannot preserve the stored state’s integrity, the servers must take specific steps to guarantee that the state information they store on the client is protected.
The example above is not unique. Client-side wallet applications, mobile agent applications, and many kinds of mobile computing applications face the same challenges. This month, I will show you how to protect the contents of serialized objects when they traverse or reside on untrusted media. This information will better prepare you for designing and building distributed applications, especially those that use Java serialization to store state information.
The two players
The Java security package java.security
and the Java Cryptography Extension (JCE) package javax.crypto
contain a pair of classes designed to address these challenges. These classes protect the integrity and/or content of serialized objects that spend time in an untrusted environment. Before delving into the details of usage and implementation, let’s take a look at the two classes and the differences between them.
The SignedObject class
Included in the java.security
package along with the classes it depends upon, the SignedObject
class makes up part of the Java 2 Platform, Standard Edition.
An instance of the SignedObject
class acts as a wrapper around an instance of another class. A SignedObject
instance contains the serialized representation of the wrapped object, along with the signature information necessary to validate the wrapped object’s authenticity and integrity.
Three conditions must be met to create a SignedObject
instance:
- The wrapped class must be serializable. The
SignedObject
instance operates by transforming an instance of a class into a serialized byte stream and then signing that byte stream. - The constructor requires the signer’s private key. The
Signature
class presumes the use of a public/private key pair, even in situations in which a shared secret key would suffice for the wrapped object’s authentication and verification. - The constructor requires a signature-generation engine, represented by an instance of the
Signature
class.
With these stipulations satisfied, the programmer creates an instance of the SignedObject
class as follows:
Serializable serializable = ...
PrivateKey privatekey = ...
Signature signature =...
SignedObject signedobject = new SignedObject(serializable, privatekey,
signature);
Once a SignedObject
instance has been created or reconstituted from a stream of bytes, the verify()
method checks the wrapped object’s integrity and the purported signer’s authenticity, as shown below:
boolean bl = signedobject.verify(publickey, signature);
The verify()
method requires a signature engine and the public key associated with the private key that signed the object.
The getObject()
method retrieves the wrapped object, as this code illustrates:
signedobject.getObject()
There is one important caveat. You can obtain the wrapped object without first verifying the sender’s authenticity or the message’s integrity. If you’re concerned about these issues, always remember to verify the wrapped object first.
The SealedObject class
Unlike the SignedObject
class, the SealedObject
class is not included in the Java 2 Platform. Instead, JCE, one of Sun’s standard extensions to the Java Platform, incorporates the class.
Like a SignedObject
instance, a SealedObject
instance acts as a wrapper around an instance of another class. It contains an encrypted version of the wrapped object’s serialized representation. This signifies the most important difference between the two classes. With the SignedObject
class, you can recover the contents of the wrapped object without the signer’s public key. With the SealedObject
class, even that much is impossible. Nothing can tamper with or examine the wrapped object, and you can verify the authenticity of the sender as well.
The following conditions are required to create a SealedObject
instance:
- First, as was the case with the
SignedObject
class, the class being wrapped must be serializable. - Second, the constructor requires a cryptographic cipher to seal the object. An instance of the
Cipher
object represents the cipher.
Create an instance of the SealedObject
class as follows:
Serializable serializable = ...
Cipher cipher = ...
SealedObject sealedobject = new SealedObject(serializable, cipher);
Once a SealedObject
instance has been created or reconstituted from a stream of bytes, the getObject()
method retrieves the wrapped object:
// Any one of the following will work. Note the additional
// Cipher or Key parameter.
sealedobject.getObject(cipher);
sealedobject.getObject(key);
sealedobject.getObject(key, provider);
To summarize, the SignedObject
class simply signs the contents of the wrapped object. The signature allows others to authenticate the purported sender’s identity and to verify the contents’ integrity. The SealedObject
class goes one step further and actually encrypts the sealed contents to protect its meaning from prying eyes. However, to use it, you must move beyond the borders of the J2SE and install (and require all users to install) the JCE.
The code
The following examples demonstrate how to use these two classes in their native environment.
The first example reads a file into a byte array and wraps that byte array in a SignedObject
instance.
// Read a file from disk.
File fileIn = new File(...);
FileInputStream fileinputstream = new FileInputStream(fileIn);
byte [] rgb = new byte [(int)fileIn.length()];
fileinputstream.read(rgb);
// Assume the private key comes from somewhere, is created, or is
// deserialized from the file on disk.
PrivateKey privatekey = ...
// For this example, we'll use the "DSA" algorithm, which is part
// of Sun's standard library.
Signature signature = Signature.getInstance("DSA");
SignedObject signedobject = new SignedObject(rgb, privatekey, signature);
The accompanying example shows how to verify the signed object and obtain the wrapped object.
// Read the serialized signed object from disk.
File fileIn = new File(...);
FileInputStream fileinputstream = new FileInputStream(fileIn);
ObjectInputStream objectinputstream = new
ObjectInputStream(fileinputstream);
SignedObject signedobject = (SignedObject)objectinputstream.readObject();
// Assume the public key comes from somewhere, is created, or is
// deserialized from the file on disk. It should correspond
// to the private key used to sign the object.
PublicKey publickey = ...
// For this example, we'll use the "DSA" algorithm, which is part
// of Sun's standard library.
Signature signature = Signature.getInstance("DSA");
signedobject.verify(publickey, signature);
byte [] rgb = (byte [])signedobject.getObject();
The next example illustrates how to read a file into a byte array and wrap that byte array in a SealedObject
instance. Note the similarities in usage.
// Read a file from disk.
File fileIn = new File(...);
FileInputStream fileinputstream = new FileInputStream(fileIn);
byte [] rgb = new byte [(int)fileIn.length()];
fileinputstream.read(rgb);
// Assume the key comes from somewhere, is created, or is
// deserialized from the file on disk.
Key key = ...
// For this example, we'll use the "DES" algorithm, which is part of
// Sun's standard library.
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
SealedObject sealedobject = new SealedObject(rgb, cipher);
The final example demonstrates how to obtain the wrapped object.
// Read the serialized sealed object from disk.
File fileIn = new File(...);
FileInputStream fileinputstream = new FileInputStream(fileIn);
ObjectInputStream objectinputstream = new
ObjectInputStream(fileinputstream);
SealedObject sealedobject = (SealedObject)objectinputstream.readObject();
// Assume the key comes from somewhere, is created, or is
// deserialized from the file on disk.
Key key = ...
// For this example, we'll use the "DES" algorithm, which is part of
// Sun's standard library.
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, key);
byte [] rgb = (byte [])sealedobject.getObject(cipher);
You can download the complete source code for these four examples in Resources.
Conclusion
Both the SignedObject
class and the SealedObject
class are useful additions to any developer’s toolbox. They will serve you well whenever you need to ensure that nothing tampers with serialized instances stored in untrusted media (or even examines them, in the case of the SealedObject
).
Next month, I will present two more extremely handy classes, the GuardedObject
class and the AccessControlContext
class, both of which are useful when making security decisions outside of the caller’s current context.