/
signature.xml
executable file
·538 lines (435 loc) · 20.9 KB
/
signature.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
<chapter id="signature">
<title>Doseta Digital Signature Framework</title>
<para>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.</para>
<para>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.</para>
<para>The email world has a nice protocol called <ulink
url="http://dkim.org">Domain Keys Identified Mail</ulink> (DKIM). Work is
also being done to apply this header to protocols other than email (i.e.
HTTP) through the <ulink
url="https://tools.ietf.org/html/draft-crocker-doseta-base-02">DOSETA
specifications</ulink>. 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.</para>
<para><programlisting>DKIM-Signature: v=1;
a=rsa-sha256;
d=example.com;
s=burke;
c=simple/simple;
h=Content-Type;
x=0023423111111;
bh=2342322111;
b=M232234=
</programlisting></para>
<para>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:</para>
<variablelist>
<varlistentry>
<term>v</term>
<listitem>
<para>Protocol version. Always 1.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>a</term>
<listitem>
<para>Algorithm used to hash and sign the message. RSA signing and
SHA256 hashing is the only supported algorithm at the moment by
RESTEasy.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>d</term>
<listitem>
<para>Domain of the signer. This is used to identify the signer as
well as discover the public key to use to verify the signature.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>s</term>
<listitem>
<para>Selector of the domain. Also used to identify the signer and
discover the public key.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>c</term>
<listitem>
<para>Canonical algorithm. Only simple/simple is supported at the
moment. Basically this allows you to transform the message body before
calculating the hash</para>
</listitem>
</varlistentry>
<varlistentry>
<term>h</term>
<listitem>
<para>Semi-colon delimited list of headers that are included in the
signature calculation.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>x</term>
<listitem>
<para>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</para>
</listitem>
</varlistentry>
<varlistentry>
<term>t</term>
<listitem>
<para>Timestamp of signature. Numeric long value of the time in
seconds since epoch. Allows the verifier to control when a signature
expires.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>bh</term>
<listitem>
<para>Base 64 encoded hash of the message body.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>b</term>
<listitem>
<para>Base 64 encoded signature.</para>
</listitem>
</varlistentry>
</variablelist>
<para>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)</para>
<para><selector>._domainKey.<domain></para>
<para>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.</para>
<para>If you're interested in learning the possible use cases for digital
signatures, here's a <ulink
url="http://bill.burkecentral.com/2011/02/21/multiple-uses-for-content-signature/">blog</ulink>
you might find interesting.</para>
<section>
<title>Maven settings</title>
<para>You must include the resteasy-crypto project to use the digital
signature framework.</para>
<para><programlisting> <dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-crypto</artifactId>
<version>3.13.2.Final</version>
</dependency>
</programlisting></para>
</section>
<section>
<title>Signing API</title>
<para>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:</para>
<para><programlisting>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();
</programlisting>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.</para>
<para>If you are including more than one signature, then just add
additional DKIMSignature instances to the headers of the request or
response.</para>
<section>
<title>@Signed annotation</title>
<para>Instead of using the API, RESTEasy also provides you an
annotation alternative to the manual way of signing using a
DKIMSignature instances is to use the
@org.jboss.resteasy.annotations.security.doseta.Signed annotation. It is
required that you configure a KeyRepository as described later in this
chapter. Here's an example:</para>
<para><programlisting> @GET
@Produces("text/plain")
@Path("signedresource")
@Signed(selector="burke", domain="sample.com", timestamped=true, expires=@After(hours=24))
public String getSigned()
{
return "hello world";
}
</programlisting>The above example using a bunch of the optional annotation
attributes of @Signed to create the following Content-Signature
header:</para>
<para><programlisting>DKIM-Signature: v=1;
a=rsa-sha256;
c=simple/simple;
domain=sample.com;
s=burke;
t=02342342341;
x=02342342322;
bh=m0234fsefasf==;
b=mababaddbb==
</programlisting><parameter>This annotation also works with the client proxy
framework.</parameter></para>
</section>
</section>
<section>
<title>Signature Verification API</title>
<para>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:</para>
<para><programlisting>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());
}
</programlisting>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.</para>
<para>The client side is a little bit different:</para>
<para><programlisting>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();
</programlisting><parameter>On the client side, you create a verifier and add
it as a property to the ClientResponse. This will trigger the verification
interceptors.</parameter></para>
<section>
<title>Annotation-based verification</title>
<para>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.doseta.Verify (or
@Verifications which is used to verify multiple signatures). Here's an
example:</para>
<para><programlisting> @POST
@Consumes("text/plain")
@Verify
public void post(String input)
{
}
</programlisting>In the above example, any DKIM-Signature headers attached to
the posted message body will be verified. The public key to verify is
discovered using the configured KeyRepository (discussed later in this
chapter). You can also specify which specific signatures you want to
verify as well as define multiple verifications you want to happen via
the @Verifications annotation. Here's a complex example:</para>
<para><programlisting>@POST
@Consumes("text/plain")
@Verifications(
@Verify(identifierName="d", identiferValue="inventory.com", stale=@After(days=2)),
@Verify(identifierName="d", identiferValue="bill.com")
}
public void post(String input) {...}
</programlisting>The above is expecting 2 different signature to be included
within the DKIM-Signature header.</para>
<para>Failed verifications will throw an
org.jboss.resteasy.security.doseta.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.</para>
</section>
</section>
<section>
<title>Managing Keys via a KeyRepository</title>
<para>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.</para>
<section>
<title>Create a KeyStore</title>
<para>Use the Java keytool to generate RSA key pairs. Key aliases MUST
HAVE the form of:</para>
<para><selector>._domainKey.<domain></para>
<para>For example:</para>
<para><programlisting>$ keytool -genkeypair -alias burke._domainKey.example.com -keyalg RSA -keysize 1024 -keystore my-apps.jks </programlisting>You
can always import your own official certificates too. See the JDK
documentation for more details.</para>
</section>
<section>
<title>Configure Restreasy to use the KeyRepository</title>
<para>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
<literal>resteasy.keystore.classpath</literal> or
<literal>resteasy.keystore.filename</literal> context parameters. You
must also specify the password (sorry its clear text) using the
<literal>resteasy.keystore.password</literal> context parameter. The
resteasy.context.objects is used to create the instance of the
repository. For example:</para>
<para><programlisting> <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>
</programlisting></para>
<para>You can also manually register your own instance of a
KeyRepository within an Application class. For example:</para>
<para><programlisting>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;
}
}
</programlisting></para>
<para>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:</para>
<para><programlisting>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);
</programlisting></para>
</section>
<section>
<title>Using DNS to Discover Public Keys</title>
<para>Public keys can also be discover by a DNS text record lookup. You
must configure web.xml to turn this feature:</para>
<para><programlisting> <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>
</programlisting>The resteasy.doseta.dns.uri context-param is optional and
allows you to point to a specific DNS server to locate text
records.</para>
<section>
<title>Configuring DNS TXT Records</title>
<para>DNS TXT Records are stored via a format described by the DOSETA
specification. The public key is defined via a base 64 encoding. You
can obtain this text encoding by exporting your public keys from your
keystore, then using a tool like openssl to get the text-based format.
For example:</para>
<para><programlisting>$ keytool -export -alias bill._domainKey.client.com -keystore client.jks -file bill.der
$ openssl x509 -noout -pubkey -in bill.der -inform der > bill.pem</programlisting>The
output will look something like: </para>
<programlisting>-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCKxct5GHz8dFw0mzAMfvNju2b3
oeAv/EOPfVb9mD73Wn+CJYXvnryhqo99Y/q47urWYWAF/bqH9AMyMfibPr6IlP8m
O9pNYf/Zsqup/7oJxrvzJU7T0IGdLN1hHcC+qRnwkKddNmD8UPEQ4BXiX4xFxbTj
NvKWLZVKGQMyy6EFVQIDAQAB
-----END PUBLIC KEY-----
</programlisting>
<para>The DNS text record entry would look like this:</para>
<programlisting>test2._domainKey IN TXT "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIKFLFWuQfDfBug688BJ0dazQ/x+GEnH443KpnBK8agpJXSgFAPhlRvf0yhqHeuI+J5onsSOo9Rn4fKaFQaQNBfCQpHSMnZpBC3X0G5Bc1HWq1AtBl6Z1rbyFen4CmGYOyRzDBUOIW6n8QK47bf3hvoSxqpY1pHdgYoVK0YdIP+wIDAQAB; t=s"
</programlisting>
<para>Notice that the newlines are take out. Also, notice that the
text record is a name value ';' delimited list of parameters. The p
field contains the public key.</para>
</section>
</section>
</section>
</chapter>