JBoss.orgCommunity Documentation

第 45 章 Doseta Digital Signature Framework

45.1. Maven settings
45.2. Signing API
45.2.1. @Signed annotation
45.3. Signature Verification API
45.3.1. Annotation-based verification
45.4. Managing Keys via a KeyRepository
45.4.1. Create a KeyStore
45.4.2. Configure Restreasy to use the KeyRepository
45.4.3. Using DNS to Discover Public Keys

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.

While the mime type multiple/signed exists, it does have drawbacks. Most importantly it requires the receiver of the message body to understand how to unpack. A receiver may not understand this mime type. A better approach would be to put signatures in an HTTP header so that receivers that don't need to worry about the digital signature, don't have to.

The email world has a nice protocol called Domain Keys Identified Mail (DKIM). Work is also being done to apply this header to protocols other than email (i.e. HTTP) through the DOSETA specifications. It allows you to sign a message body and attach the signature via a DKIM-Signature header. Signatures are calculated by first hashing the message body then combining this hash with an arbitrary set of metadata included within the DKIM-Signature header. You can also add other request or response headers to the calculation of the signature. Adding metadata to the signature calculation gives you a lot of flexibility to piggyback various features like expiration and authorization. Here's what an example DKIM-Signature header might look like.

DKIM-Signature: v=1;
                a=rsa-sha256;
                d=example.com;
                s=burke;
                c=simple/simple;
                h=Content-Type;
                x=0023423111111;
                bh=2342322111;
                b=M232234=

As you can see it is a set of name value pairs delimited by a ';'. While its not THAT important to know the structure of the header, here's an explanation of each parameter:

v

Protocol version. Always 1.

a

Algorithm used to hash and sign the message. RSA signing and SHA256 hashing is the only supported algorithm at the moment by RESTEasy.

d

Domain of the signer. This is used to identify the signer as well as discover the public key to use to verify the signature.

s

Selector of the domain. Also used to identify the signer and discover the public key.

c

Canonical algorithm. Only simple/simple is supported at the moment. Basically this allows you to transform the message body before calculating the hash

h

Semi-colon delimited list of headers that are included in the signature calculation.

x

When the signature expires. This is a numeric long value of the time in seconds since epoch. Allows signer to control when a signed message's signature expires

t

Timestamp of signature. Numeric long value of the time in seconds since epoch. Allows the verifier to control when a signature expires.

bh

Base 64 encoded hash of the message body.

b

Base 64 encoded signature.

To verify a signature you need a public key. DKIM uses DNS text records to discover a public key. To find a public key, the verifier concatenates the Selector (s parameter) with the domain (d parameter)

<selector>._domainKey.<domain>

It then takes that string and does a DNS request to retrieve a TXT record under that entry. In our above example burke._domainKey.example.com would be used as a string. This is a every interesting way to publish public keys. For one, it becomes very easy for verifiers to find public keys. There's no real central store that is needed. DNS is a infrastructure IT knows how to deploy. Verifiers can choose which domains they allow requests from. RESTEasy supports discovering public keys via DNS. It also instead allows you to discover public keys within a local Java KeyStore if you do not want to use DNS. It also allows you to plug in your own mechanism to discover keys.

If you're interested in learning the possible use cases for digital signatures, here's a blog you might find interesting.

You must include the resteasy-crypto project to use the digital signature framework.

        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-crypto</artifactId>
            <version>4.7.0-SNAPSHOT</version>
        </dependency>

To sign a request or response using the RESTEasy client or server framework you need to create an instance of org.jboss.resteasy.security.doseta.DKIMSignature. This class represents the DKIM-Signature header. You instantiate the DKIMSignature object and then set the "DKIM-Signature" header of the request or response. Here's an example of using it on the server-side:

import org.jboss.resteasy.security.doseta.DKIMSignature;
import java.security.PrivateKey;


@Path("/signed")
public static class SignedResource
{
   @GET
   @Path("manual")
   @Produces("text/plain")
   public Response getManual()
   {
      PrivateKey privateKey = ....; // get the private key to sign message
      
      DKIMSignature signature = new DKIMSignature();
      signature.setSelector("test");
      signature.setDomain("samplezone.org");
      signature.setPrivateKey(privateKey);

      Response.ResponseBuilder builder = Response.ok("hello world");
      builder.header(DKIMSignature.DKIM_SIGNATURE, signature);
      return builder.build();
   }
}

// client example

DKIMSignature signature = new DKIMSignature();
PrivateKey privateKey = ...; // go find it
signature.setSelector("test");
signature.setDomain("samplezone.org");
signature.setPrivateKey(privateKey);

ClientRequest request = new ClientRequest("http://...");
request.header("DKIM-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. RESTEasy currently only supports RSA key pairs. The DKIMSignature class also allows you to add and control how various pieces of metadata are added to the DKIM-Signature header and the signature calculation. See the javadoc for more details.

If you are including more than one signature, then just add additional DKIMSignature instances to the headers of the request or response.

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:

import org.jboss.resteasy.spi.MarshalledEntity;


@POST
@Consumes("text/plain")
@Path("verify-manual")
public void verifyManual(@HeaderParam("Content-Signature") DKIMSignature signature,
                         @Context KeyRepository repository, 
                         @Context HttpHeaders headers, 
                         MarshalledEntity<String> input) throws Exception
{
      Verifier verifier = new Verifier();
      Verification verification = verifier.addNew();
      verification.setRepository(repository);
      verification.setStaleCheck(true);
      verification.setStaleSeconds(100);
      try {
          verifier.verifySignature(headers.getRequestHeaders(), input.getMarshalledBytes, signature);
      } catch (SignatureException ex) {
      }
      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. You will also have to configure a KeyRepository. This is describe later in this chapter.

The client side is a little bit different:

ClientRequest request = new ClientRequest("http://localhost:9095/signed"));


ClientResponse<String> response = request.get(String.class);
Verifier verifier = new Verifier();
Verification verification = verifier.addNew();
verification.setRepository(repository);
response.getProperties().put(Verifier.class.getName(), verifier);

// signature verification happens when you get the entity
String entity = response.getEntity();

On the client side, you create a verifier and add it as a property to the ClientResponse. This will trigger the verification interceptors.

RESTEasy manages keys for you through a org.jboss.resteasy.security.doseta.KeyRepository. By default, the KeyRepository is backed by a Java KeyStore. Private keys are always discovered by looking into this KeyStore. Public keys may also be discovered via a DNS text (TXT) record lookup if configured to do so. You can also implement and plug in your own implementation of KeyRepository.

Next you need to configure the KeyRepository in your web.xml file so that it is created and made available to RESTEasy to discover private and public keys.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. The resteasy.context.objects is used to create the instance of the repository. For example:

    <context-param>
        <param-name>resteasy.doseta.keystore.classpath</param-name>
        <param-value>test.jks</param-value>
    </context-param>
    <context-param>
        <param-name>resteasy.doseta.keystore.password</param-name>
        <param-value>geheim</param-value>
    </context-param>
    <context-param>
        <param-name>resteasy.context.objects</param-name>
        <param-value>org.jboss.resteasy.security.doseta.KeyRepository : org.jboss.resteasy.security.doseta.ConfiguredDosetaKeyRepository</param-value>
    </context-param>

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.doseta.KeyRepository;
import org.jboss.resteasy.security.doseta.DosetaKeyRepository;

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);

      repository = new DosetaKeyRepository();
      repository.setKeyStorePath("test.jks");
      repository.setKeyStorePassword("password");
      repository.setUseDns(false);
      repository.start();

      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.doseta.DosetaKeyRepository. You then set a request attribute, "org.jboss.resteasy.security.doseta.KeyRepository", with the value of the created instance. Use the ClientRequest.getAttributes() method to do this. For example:

DosetaKeyRepository keyRepository = new DoestaKeyRepository();
repository.setKeyStorePath("test.jks");
repository.setKeyStorePassword("password");
repository.setUseDns(false);
repository.start();

DKIMSignature signature = new DKIMSignature();
signature.setDomain("example.com");

ClientRequest request = new ClientRequest("http://...");
request.getAttributes().put(KeyRepository.class.getName(), repository);
request.header("DKIM-Signature", signatures);

Public keys can also be discover by a DNS text record lookup. You must configure web.xml to turn this feature:

    <context-param>
        <param-name>resteasy.doseta.use.dns</param-name>
        <param-value>true</param-value>
    </context-param>
    <context-param>
        <param-name>resteasy.doseta.dns.uri</param-name>
        <param-value>dns://localhost:9095</param-value>
    </context-param>

The resteasy.doseta.dns.uri context-param is optional and allows you to point to a specific DNS server to locate text records.