JBoss.orgCommunity Documentation
Digital signatures allow you to protect the integrity of a message. They are used to verify that a message sent was sent by the actual user that sent the message and was modified in transit. Most web apps handle message integrity by using TLS, like HTTPS, to secure the connection between the client and server. Sometimes though, we have representations that are going to be forwarded to more than one recipient. Some representations may hop around from server to server. In this case, TLS is not enough. There needs to be a mechanism to verify who sent the original representation and that they actually sent that message. This is where digital signatures come in.
Resteasy has developed its own protocol for digital signatures. It allows you to attach one or more digital signatures to a request or response via a Content-Signature header. Signatures are comprised of the message body and an arbitrary set of metadata. Adding metadata to the signature calculation gives you a lot of flexiblity to piggyback various features like expiration and authorization. Here's what an example Content-Signature header might look like.
Content-Signature: values=signer:expiration; headers=Content-Type:Date; signer=bill; expiration="Sunday, 06-Nov-11 08:49:37 GMT"; signature=0f341265ffa32211333f6ab2d1
A complete description of the protocol is available on Bill Burke's Blog. We are also submitting this protocol as an Internet-Draft at IETF to hopefully get it standardized in some way or form. There's also another blog about various uses you might find for the protocol. The resteasy distribution also has an example you can check out.
To sign a request or response using the Resteasy client or server framework you need to create an instance of org.jboss.resteasy.security.signing.ContentSignatures. ContentSignatures hold one or more signatures you want to attach to the request or response and models the Content-Signature header. You instantiate the ContentSignatures object and then set the "Content-Signature" header of the request or response. Here's an example of using it on the server-side:
import org.jboss.resteasy.security.signing.ContentSignature; import org.jboss.resteasy.security.signing.ContentSignatures; import java.security.PrivateKey; @Path("/signed") public static class SignedResource { @GET @Produces("text/plain") @Path("manual") public Response getManual() { ContentSignature signature = new ContentSignature(); PrivateKey privateKey = ...; // go find it signature.setPrivateKey(privateKey); Response.ResponseBuilder builder = Response.ok("hello"); builder.header("Content-Signature", signature); return builder.build(); } } // client example ContentSignature signature = new ContentSignature(); PrivateKey privateKey = ...; // go find it signature.setPrivateKey(privateKey); ClientRequest request = new ClientRequest("http://..."); request.header("Content-Signature", signature); request.body("text/plain", "some body to sign"); ClientResponse response = request.put();
To sign a message you need a PrivateKey. This can be generated by KeyTool or manually using regular, standard JDK Signature APIs. The default algorithm used is an SHA1 256 hash with RSA, "SHA256withRSA". Resteasy supports any algorithm that uses private/public key pairs and available through the java.security.Signature class. The ContentSignature class also allows you to add and control how various pieces of metadata are added to the Content-Signature header and the signature calculation. See the javadoc for more details.
If you are including more than one signature, then use the ContentSignatures class to hold multiple signatures.
In the above example, you are managing your own keys. Resteasy can manage keys for you through a org.jboss.resteasy.security.KeyRepository. Currently, only Java KeyStores are supported, but you can implement and plug in your own implementation of this interface if you need a different mechanism to manage keys.
If you're going to use the built in Java KeyStore support, use Java keytool to generate a private and public key, for example:
$ keytool -genkeypair -alias myKey -keyalg RSA -keysize 1024 -keystore my-apps.jks
You can always import your own official certificates too. See the JDK documentation for more details.
You can reference a Java key store you want the Resteasy signature
framework to use within web.xml using either
resteasy.keystore.classpath
or
resteasy.keystore.filename
context parameters. You
must also specify the password (sorry its clear text) using the
resteasy.keystore.password
context parameter. For
example:
<context-param> <param-name>resteasy.keystore.classpath</param-name> <param-value>test.jks</param-value> </context-param> <context-param> <param-name>resteasy.keystore.password</param-name> <param-value>geheim</param-value> </context-param>
If you invoke the ContentSignature.setKeyAlias() method, resteasy will use a configured KeyRepository to lookup that key alias within the repository. Alternatively, if you do not set the key alias via setKeyAlias(), Resteasy will use the signer attribute of the ContentSignature which is set via the ContentSignature.setSigner() method. For example:
@GET @Produces("text/plain") @Path("manual") public Response getManual() { ContentSignature signature = new ContentSignature(); signature.setKeyAlias("myKey); Response.ResponseBuilder builder = Response.ok("hello"); builder.header("Content-Signature", signature); return builder.build(); }
You can also manually register your own instance of a KeyRepository within an Application class. For example:
import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.security.keys.KeyRepository; import org.jboss.resteasy.security.keys.KeyStoreKeyRepository; import javax.ws.rs.core.Application; import javax.ws.rs.core.Context; public class SignatureApplication extends Application { private HashSet<Class<?>> classes = new HashSet<Class<?>>(); private KeyRepository repository; public SignatureApplication(@Context Dispatcher dispatcher) { classes.add(SignedResource.class); InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("test.jks"); repository = new KeyStoreKeyRepository(is, "password"); dispatcher.getDefaultContextObjects().put(KeyRepository.class, repository); } @Override public Set<Class<?>> getClasses() { return classes; } }
On the client side, you can load a KeyStore manually, by instantiating an instance of org.jboss.resteasy.security.keys.KeyStoreKeyRepository. You then set a request attribute, "org.jboss.resteasy.security.keys.KeyRepository", with the value of the created instance. Use the ClientRequest.getAttributes() method to do this. For example:
KeyStoreKeyRepository keyRepository = new KeyStoreKeyRepository("/usr/local/home/bill/my-apps.jks", "geheim"); ContentSignature signature = new ContentSignature(); signature.setSigner("myKey", true, true); ClientRequest request = new ClientRequest("http://..."); request.getAttributes().put(KeyRepository.class.getName(), repository); request.header("Content-Signature", signatures);
An alternative to the manual way of signing using a ContentSignatures instance is to use the @org.jboss.resteasy.annotations.security.signature.Signed annotation. It is required that you configure a KeyRepository as described earlier. Here's an example:
@GET @Produces("text/plain") @Path("signedresource") @Signed(id="employee", signer="bill", timestamped=true, expires=@After(hours=24)) public String getSigned() { return "hello world"; }
The above example using a bunch of the optional annotation attributes of @Signed to create the following Content-Signature header:
Content-Signature: id=employee; signer=bill; timestamp="Wed, 09 Mar 2011 23:04:25 GMT"; expiration="Thur, 10 Mar 2011 23:04:25 GMT"; values=id:signer:timestamp:expiration; signature=0FAB0CEF0050
This annotation also works with the client proxy
framework.
The easiest way to verify a signature sent in a HTTP request on the server side is to use the @@org.jboss.resteasy.annotations.security.signature.Verify (or @Verifications which is used to verify multilpe signatures). Here's an example:
@POST @Consumes("text/plain") @Verify public void post(String input) { }
By default, Resteasy will look for a signer attribute in every contained signature to determine which keyAlias to use to obtain a public key and verify the signatures. You can also specify which specific signatures you want to verify as well as define multiple verifications you want to happen via the @Verifications annoation. Here's a complex example:
@POST @Consumes("text/plain") @Verifications( @Verify(id="inventory-signature", stale=@After(days=2)), @Verify(signer="bill"), @Verify(id="provider", attributeAlias="id) } public void post(String input) {...}
The above is expecting 3 different signature to be included within the Content-Signature header. The attributeAlias tells resteasy which attribute within a signature contains a keyAlias to use to lookup a public key within a KeyRepository.
Failed verifications will throw an org.jboss.resteasy.security.signing.UnauthorizedSignatureException. This causes a 401 error code to be sent back to the client. If you catch this exception using an ExceptionHandler you can browse the failure results.
If you want fine grain control over verification, this is an API to verify signatures manually. Its a little tricky because you'll need the raw bytes of the HTTP message body in order to verify the signature. You can get at an unmarshalled message body as well as the underlying raw bytes by using a org.jboss.resteasy.spi.MarshalledEntity injection. Here's an example of doing this on the server side:
@POST @Consumes("text/plain") @Path("verify-manual") public void verifyManual(@HeaderParam("Content-Signature") ContentSignature signature, @Context KeyRepository repository, @Context HttpHeaders headers, MarshalledEntity<String> input) throws Exception { PublicKey publicKey = repository.getPublicKey("someAlias"); boolean verified = signature.verify(headers.getRequestHeaders(), input.getMarshalledBytes(), publicKey); System.out.println("The text message posted is: " + input.getEntity()); }
MarshalledEntity is a generic interface. The template parameter should be the Java type you want the message body to be converted into. Use a ContentSignatures injection if you are expecting more than one signature within the Content-Signature header. Here's an example of doing this on the client side:
ClientRequest request = new ClientRequest(TestPortProvider.generateURL("/signed")); ClientResponse<MarshalledEntity<String>> response = request.get(new GenericType<MarshalledEntity<String>>() {}); MarshalledEntity<String> marshalledEntity = response.getEntity(); String signatureHeader = response.getHeaders().getFirst(ContentSignature.CONTENT_SIGNATURE); ContentSignature contentSignature = new ContentSignature(signatureHeader); boolean verified = contentSignature.verify(response.getHeaders(), marshalledEntity.getMarshalledBytes(), publicKey);