Saturday, October 31, 2009

Item 64: Use SignedObject to provide integrity of Serialized objects











 < Day Day Up > 





Item 64: Use SignedObject to provide integrity of Serialized objects



Frequently, serializable data travels over media we don't trust inherently (which, by the way, should be all of them�see Item 60). For example, authentication or authorization information that can be spoofed by an attacker could be a powerful way to gain entrance into a system without having to provide the correct credentials�in any situation where authentication or authorization objects are being passed around or stored onto disk, we should make sure the object came from the appropriate server issuing it. In essence, we need to ensure that this token cannot be forged�any modification, benevolent or otherwise, must be immediately detected so that the token can be discarded.



In other words, ask your objects to prove their authenticity to you.



Your first reaction, particularly if you've already read Item 6 and started looking through the Java Object Serialization Specification for ways to use the hook points defined by this specification (see Item 71) might be to provide custom serialization behaviors via readObject/writeObject implementations, encrypt the information using the issuer's PrivateKey (from the javax.security APIs) when the object is first serialized, and deserialize using the issuer's PublicKey that travels with the Serialized object. You'd be on the right track, but two things stand in your way: (1) doing all this is tedious and hard, and (2) there's a really good chance that, unless you're a security expert, you'll accidentally leave a hole in the process.



Fortunately, Java already has this covered, via a little-known class in the java.security package called SignedObject. This class wraps a Signature and a Serialized object (actually a copy of the source Serializable object, so future modifications to the Serialized object source won't be reflected in the SignedObject copy), and provides easy methods for verifying the object's authenticity given a PublicKey.



Putting that into code, imagine for a moment that a procedural-first persistence engine (see Item 42) is passing Serialized RowSet objects back and forth between client and server over an ordinary HTTP link. The data isn't confidential, but we need to verify its authenticity as coming from the server�otherwise we could conceivably be receiving replies from some man-in-the-middle process trying to masquerade as the server and influence the results.



In order to circumvent this, create a PrivateKey instance embedded in the server, and serialize the RowSet into a SignedObject by signing it with the PrivateKey:










PrivateKey privateKey = getPrivateKey();



RowSet data = ...;



Signature signingEngine =

Signature.getInstance("SHA1withDSA");



SignedObject signedObject =

new SignedObject(data, privateKey, signingEngine);



// Serialize the signedObject and ship it

// over the wire




The PrivateKey will usually be embedded, probably as a bytestream, somewhere inside the server software resources (usually disguised in some way to avoid being easily discerned as the private key). When serialized, the signedObject instance will contain the digital signature of the private key, and the authenticity of the signedObject can be verified by calling the verify method with a Signature instance that matches the one used to sign the data in the first place and the corresponding public key, again usually embedded as part of the client software footprint:










PublicKey publicKey = getPublicKey();



Signature verificationEngine =

Signature.getInstance("SHA1withDSA");



SignedObject signedObject = ...;

// Deserialize from server socket



if (signedObject.verify(publicKey, verificationEngine))

{

RowSet data = (RowSet)signedObject.getObject();

// We know data was issued under the private key, and

// that it wasn't tampered with

}




As an experiment, use a java.util.HashMap instead of the RowSet, serialize the SignedObject to disk, then modify the contents of the SignedObject by using a hex editor. (SignedObject provides only integrity, not confidentiality; confidentiality is the province of the SealedObject�see Item 65.) When verifying the modified data, the signature won't match, proving that the data was tampered with after the signature was applied. More importantly, unless the attacker has the ability to replace the public key in the client software with a public key of his or her own choosing, we can always ensure that data received from the server is in fact from that server.



Working directly with SignedObject can sometimes be onerous to programmers who don't understand its purpose or the need to call verify before working with the object itself�in order to hide those details from clients, extend SignedObject directly. So, for example, to build a signed authorization context that can be passed around the network, create a SignedObject subclass that contains a JAAS Subject as its Serializable content, as in the following code:










public class AuthorizationToken extends SignedObject

{

private static Signature engine =

Signature.getInstance("SHA1withDSA");



public AuthorizationToken(PrivateKey privKey,

Subject subject)

{

super(subject, privKey, engine);

}



public Subject getSubject(PublicKey pubKey)

{

if (verify(pubKey, engine))

return (Subject)getObject();

else

throw new

AuthenticationFailedException("Spoofed!");

}

}




Alternatively, instead of extending SignedObject you could simply compose it (hold it as a field and delegate calls to it directly); either approach works relatively well. Many developers favor the inheritance-based approach because it saves typing, but in this particular case it comes out about the same either way.



By the way, generating a PublicKey/PrivateKey pair is as simple as exercising the KeyPairGenerator class that also comes from the java.security package; a utility that writes out the pair to .publickey and .privatekey files looks like this:










import java.io.*;

import java.io.*;

import java.security.*;



public class genkeypair

{

public static void main(String[] args)

throws Exception

{

KeyPairGenerator kpg =

KeyPairGenerator.getInstance("DSA");

KeyPair kp = kpg.generateKeyPair();

PrivateKey privKey = kp.getPrivate();

PublicKey pubKey = kp.getPublic();



try

{

ObjectOutputStream oos =

new ObjectOutputStream(

new FileOutputStream(".privatekey"));

oos.writeObject(privKey);

oos.close();



oos = new ObjectOutputStream(

new FileOutputStream(".publickey"));

oos.writeObject(pubKey);

oos.close();

}

catch (Exception ex)

{

ex.printStackTrace();

}

}

}




Note that you should never ship software with these keys distributed in this file-based form; it becomes too easy for even basic attackers to replace the files with their own versions on either client or server. The principal concern here is, of course, the keys themselves. An attacker who gets hold of the server's private key could masquerade as your server to any clients. If the attacker can replace the public key value in client systems with a public key of his or her own choosing, the attacker could intercept communications between client and server, resigning data from the server using his or her own private key, which then would verify as authentic to the hacked clients. (This is why digital certificates usually accompany any public key�the certificate acts as a way to verify the authenticity of the public key, by trusting the issuer of the certificate.)



Note, too, that SignedObject instances can nest inside of one another (using a SignedObject as the data for another SignedObject) if multiple signatures are required; the only tricky part of verifying the object is to make sure that the signatures are verified in the reverse order of their signing. (It's reported that a future version of SignedObject will take multiple signatures, but for now stacking them isn't too awkward to work with since the order of signatures could be conveyed as part of the data itself, if desired.)



Again, remember that the purpose of SignedObject is merely to verify the integrity of the data being sent/stored/serialized, to ensure that the data hasn't been tampered with or sent from somebody other than whom we want it from; it provides no confidentiality whatsoever to the data itself. That becomes the responsibility of SealedObject, as described in Item 65.













     < Day Day Up > 



    No comments: