├── .gitignore
├── .gitattributes
├── web
├── index.jsp
├── webpkiorg.png
└── style.css
├── jws-ct.properties
├── empty.lib
└── .gitignore
├── sample-data-to-hash.json
├── sample-data-to-sign.json
├── LICENSE
├── .classpath
├── .project
├── src
└── org
│ └── webpki
│ └── webapps
│ └── jws_ct
│ ├── NoWebCryptoServlet.java
│ ├── JavaScriptSignatureServlet.java
│ ├── HomeServlet.java
│ ├── DumpASN1Servlet.java
│ ├── KeyConvertServlet.java
│ ├── HashServlet.java
│ ├── HTML.java
│ ├── ValidateServlet.java
│ ├── JwsCtService.java
│ ├── WebCryptoServlet.java
│ └── CreateServlet.java
├── web.xml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /bin
2 | /dist
3 | .tmp
4 | .DS_Store
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Disable LF normalization for all files
2 | * -text
--------------------------------------------------------------------------------
/web/index.jsp:
--------------------------------------------------------------------------------
1 | <%@page session="false"%><%response.sendRedirect ("home");%>
2 |
--------------------------------------------------------------------------------
/jws-ct.properties:
--------------------------------------------------------------------------------
1 | # Lots of stuff is fetched from here
2 | openkeystore=../openkeystore
3 |
--------------------------------------------------------------------------------
/web/webpkiorg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyberphone/jws-ct/master/web/webpkiorg.png
--------------------------------------------------------------------------------
/empty.lib/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
--------------------------------------------------------------------------------
/sample-data-to-hash.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Sample JSON object with 'difficult' data",
3 | "string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
4 | "numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
5 | "literals": [null, true, false]
6 | }
7 |
--------------------------------------------------------------------------------
/sample-data-to-sign.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Signed JSON object using 'Detached' JWS and JCS Canonicalization",
3 | "string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
4 | "numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
5 | "literals": [null, true, false]}
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 WebPKI.org
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | jws-ct
4 |
5 |
6 |
7 |
8 |
9 | org.eclipse.jdt.core.javabuilder
10 |
11 |
12 |
13 |
14 |
15 | org.eclipse.jdt.core.javanature
16 |
17 |
18 |
19 | 1723925813692
20 |
21 | 30
22 |
23 | org.eclipse.core.resources.regexFilterMatcher
24 | node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/NoWebCryptoServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 |
21 | import javax.servlet.ServletException;
22 |
23 | import javax.servlet.http.HttpServlet;
24 | import javax.servlet.http.HttpServletRequest;
25 | import javax.servlet.http.HttpServletResponse;
26 |
27 | public class NoWebCryptoServlet extends HttpServlet {
28 | private static final long serialVersionUID = 1L;
29 |
30 | public void doGet(HttpServletRequest request, HttpServletResponse response)
31 | throws IOException, ServletException {
32 | HTML.noWebCryptoPage(response);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/web/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin:10pt;
3 | font-size:8pt;
4 | font-family:Verdana,'Bitstream Vera Sans','DejaVu Sans',Arial,'Liberation Sans';
5 | }
6 |
7 | a {
8 | color:#007fff;
9 | text-decoration:none;
10 | outline:none;
11 | }
12 |
13 | li {
14 | padding-top:5pt
15 | }
16 |
17 | .staticbox, .textbox {
18 | box-sizing:border-box;
19 | width:100%;word-break:break-all;
20 | border-width:1px;
21 | border-style:solid;
22 | border-color:grey;
23 | padding:10pt;
24 | }
25 |
26 | .staticbox {
27 | background:#f8f8f8;
28 | }
29 |
30 | .textbox {
31 | background:#ffffea;
32 | }
33 |
34 | .header {
35 | text-align:center;
36 | font-weight:bolder;
37 | font-size:12pt;
38 | font-family:arial,verdana;
39 | }
40 |
41 | .sitefooter {
42 | display:flex;
43 | align-items:center;
44 | border-width:1px 0 0 0;
45 | border-style:solid;
46 | position:absolute;
47 | z-index:5;
48 | left:0px;
49 | bottom:0px;
50 | right:0px;
51 | padding:0.5em 0.7em;
52 | }
53 |
54 | .stdbtn, .multibtn {
55 | cursor:pointer;
56 | background:linear-gradient(to bottom, #eaeaea 14%,#fcfcfc 52%,#e5e5e5 89%);
57 | border-width:1px;
58 | border-style:solid;
59 | border-color:#a9a9a9;
60 | border-radius:5pt;
61 | padding:3pt 10pt;
62 | }
63 |
64 | .sigparmbox {
65 | border-radius:5pt;
66 | padding:0 5pt 5pt 5pt;
67 | }
68 |
69 | .sigparmhead {
70 | border-radius:3pt;
71 | padding:3pt 5pt;
72 | z-index:5;
73 | position:relative;
74 | top:-10pt;
75 | margin-bottom:-3pt;
76 | }
77 |
78 | .defbtn {
79 | border-radius:2pt;
80 | display:inline-block;
81 | cursor:pointer;
82 | padding:1pt 3pt;
83 | background-color:#f0f5fd;
84 | border-color:#2a53ea;
85 | }
86 |
87 | .sigparmbox, .sigparmhead, .defbtn {
88 | border-width:1px;
89 | border-style:solid;
90 | }
91 |
92 | .sigparmhead, .sitefooter {
93 | border-color:#c85000;
94 | background-color:#fffcfc;
95 | }
96 |
97 | .sigparmbox {
98 | border-color:#008040;
99 | background-color:#fbfff7;
100 | }
101 |
102 | .stdbtn, .multibtn, .sigparmhead {
103 | font-family:Arial,'Liberation Sans',Verdana,'Bitstream Vera Sans','DejaVu Sans';
104 | font-size:10pt;
105 | }
106 |
107 | .stdbtn, .multibtn, .sigparmbox, .staticbox, .textbox {
108 | box-shadow:3pt 3pt 3pt #d0d0d0;
109 | }
110 |
111 | .stdbtn {
112 | display:inline-block;
113 | }
114 |
115 | .stdbtn {
116 | margin-top:15pt;
117 | }
118 |
119 | .multibtn {
120 | margin-top:12pt;
121 | text-align:center;
122 | }
123 |
124 | @media (max-width:768px) {
125 |
126 | .stdbtn, .multibtn, .sigparmbox, .staticbox, .textbox {
127 | box-shadow:2pt 2pt 2pt #d0d0d0;
128 | }
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/JavaScriptSignatureServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 |
21 | import java.util.logging.Logger;
22 |
23 | import javax.servlet.ServletException;
24 |
25 | import javax.servlet.http.HttpServlet;
26 | import javax.servlet.http.HttpServletRequest;
27 | import javax.servlet.http.HttpServletResponse;
28 |
29 | import org.webpki.json.JSONOutputFormats;
30 | import org.webpki.json.JSONParser;
31 |
32 | public class JavaScriptSignatureServlet extends HttpServlet {
33 |
34 | private static final long serialVersionUID = 1L;
35 |
36 | static Logger logger = Logger.getLogger(JavaScriptSignatureServlet.class.getName());
37 |
38 | public void doPost(HttpServletRequest request, HttpServletResponse response)
39 | throws IOException, ServletException {
40 | try {
41 | request.setCharacterEncoding("utf-8");
42 | if (!request.getContentType().startsWith("application/x-www-form-urlencoded")) {
43 | throw new IOException("Unexpected MIME type: " + request.getContentType());
44 | }
45 | String htmlSafe = HTML.encode(
46 | JSONParser.parse(CreateServlet.getParameter(request,
47 | ValidateServlet.JWS_OBJECT))
48 | .serializeToString(JSONOutputFormats.PRETTY_JS_NATIVE), true)
49 | .replace(" ", " ");
50 | HTML.standardPage(response,
51 | null,
52 | new StringBuilder(
53 | "
")
54 | .append(HTML.fancyBox("verify",
55 | htmlSafe,
56 | "JavaScript compatible object featuring an embedded JWS signature element"))
57 | .append(
58 | "Note that the signature above is not verified. " +
59 | "The only difference between " +
60 | "the JavaScript notation and "true" JSON is the removal of the " +
61 | "(usually redundant) quote characters " +
62 | "around property names. Names that interfere with JavaScript naming " +
63 | "conventions for variables like '5' or 'my.prop' will though be quoted.
" +
64 | "Since the JavaScript JSON.stringify() " +
65 | "method restores the \"true\" JSON format, the two notations are fully " +
66 | "interoperable.
"));
67 | } catch (IOException e) {
68 | HTML.errorPage(response, e);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/HomeServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 |
21 | import javax.servlet.ServletException;
22 |
23 | import javax.servlet.http.HttpServlet;
24 | import javax.servlet.http.HttpServletRequest;
25 | import javax.servlet.http.HttpServletResponse;
26 |
27 | public class HomeServlet extends HttpServlet {
28 |
29 | private static final long serialVersionUID = 1L;
30 |
31 | public void doGet(HttpServletRequest request, HttpServletResponse response)
32 | throws IOException, ServletException {
33 |
34 | HTML.standardPage(response, null, new StringBuilder(
35 | "" +
36 | "This site permits testing and debugging " +
37 | "a scheme for \"Clear Text\" JSON signatures tentatively targeted for " +
38 | "publication as an
IETF RFC . " +
41 | "For detailed technical information and " +
42 | "open source code, click on the JWS/CT logotype.
" +
43 | "" +
44 | "" +
47 | "Create JSON Signatures" +
48 | "
" +
49 | "" +
52 | "Validate JSON Signatures" +
53 | "
" +
54 | "" +
57 | ""Experimental" - WebCrypto" +
58 | "
" +
59 | "" +
62 | "Canonicalize and Hash JSON" +
63 | "
" +
64 | "" +
67 | "Convert JWK <-> PEM Keys" +
68 | "
" +
69 | "" +
72 | "Dump PEM as ASN.1" +
73 | "
" +
74 | "
" +
75 | ""));
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/web.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | Logging flag
9 | logging
10 | @logging@
11 |
12 |
13 |
14 | org.webpki.webapps.jws_ct.JwsCtService
15 |
16 |
17 |
18 | HomeServlet
19 | org.webpki.webapps.jws_ct.HomeServlet
20 |
21 |
22 |
23 | CreateServlet
24 | org.webpki.webapps.jws_ct.CreateServlet
25 |
26 |
27 |
28 | ValidateServlet
29 | org.webpki.webapps.jws_ct.ValidateServlet
30 |
31 |
32 |
33 | WebCryptoServlet
34 | org.webpki.webapps.jws_ct.WebCryptoServlet
35 |
36 |
37 |
38 | NoWebCryptoServlet
39 | org.webpki.webapps.jws_ct.NoWebCryptoServlet
40 |
41 |
42 |
43 | JavaScriptSignatureServlet
44 | org.webpki.webapps.jws_ct.JavaScriptSignatureServlet
45 |
46 |
47 |
48 | HashServlet
49 | org.webpki.webapps.jws_ct.HashServlet
50 |
51 |
52 |
53 | KeyConvertServlet
54 | org.webpki.webapps.jws_ct.KeyConvertServlet
55 |
56 |
57 |
58 | DumpASN1Servlet
59 | org.webpki.webapps.jws_ct.DumpASN1Servlet
60 |
61 |
62 |
63 | HomeServlet
64 | /home
65 |
66 |
67 |
68 | CreateServlet
69 | /create
70 |
71 |
72 |
73 | ValidateServlet
74 | /validate
75 |
76 |
77 |
78 | WebCryptoServlet
79 | /webcrypto
80 |
81 |
82 |
83 | NoWebCryptoServlet
84 | /nowebcrypto
85 |
86 |
87 |
88 | JavaScriptSignatureServlet
89 | /jssignature
90 |
91 |
92 |
93 | HashServlet
94 | /hash
95 |
96 |
97 |
98 | KeyConvertServlet
99 | /keyconv
100 |
101 |
102 |
103 | DumpASN1Servlet
104 | /dumpasn1
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ## JWS/CT - Combining "Detached" JWS with JCS (JSON Canonicalization Scheme)
4 | This repository contains a PoC showing how you can create "clear text" JSON signatures
5 | by combining detached JWS compact objects with a simple
6 | [canonicalization](https://github.com/cyberphone/json-canonicalization#json-canonicalization)
7 | scheme.
8 |
9 | ### Problem Statement
10 | Assume you have a JSON object like the following:
11 | ```json
12 | {
13 | "statement": "Hello signed world!",
14 | "otherProperties": [2e+3, true]
15 | }
16 | ```
17 | If you would like to sign this object using JWS compact mode you would end-up with something like this:
18 | ```code
19 | eyJhbGciOiJIUzI1NiIsImtpZCI6Im15a2V5In0.eyJvdGhlclByb3BlcnRpZXMiOlsyMDAwLHRydWVdLCJzdG
20 | F0ZW1lbnQiOiJIZWxsbyBzaWduZWQgd29ybGQhIn0.FcE8h0GXJaOZ4Th3fNDBgcBE5HfEplOnS8GGtoSLU1K
21 | ```
22 | That's not very cool since one of the major benefits of text based schemes (*human readability*), got lost in the process.
23 | In addition, *the whole JSON structure was transformed into something entirely different*.
24 | ### Clear Text Signatures
25 | By rather using JWS in "detached" mode you can reap the benefits of text based schemes while
26 | keeping existing security standards!
27 | ```json
28 | {
29 | "statement": "Hello signed world!",
30 | "otherProperties": [2e+3, true],
31 | "signature": "eyJhbGciOiJIUzI1NiIsImtpZCI6Im15a2V5In0..5HfEplOnS8GGtoSLU1KFcE8h0GXJaOZ4Th3fNDBgcBE"
32 | }
33 | ```
34 | You may wonder why this is not already described in the JWS standard, right? Since JSON doesn't require
35 | object properties to be in any specific order as well as having multiple ways of representing the same data,
36 | you must apply a *filter process* to the original object in order to create a *unique and platform
37 | independent representation* of the JWS "payload". Applied to the sample you would get:
38 | ```json
39 | {"otherProperties":[2000,true],"statement":"Hello signed world!"}
40 | ```
41 | Note that this method is *internal to the signatures process*. The "wire format" can be kept "as is" although the
42 | canonicalized version of the object would also be fully valid.
43 |
44 | The knowledgeable reader probably realizes that this is quite similar to using an HTTP header for holding a detached JWS object.
45 | The primary advantages of this scheme versus using HTTP headers include:
46 | - Due to *transport independence*, signed objects can for example be used in
47 | browsers expressed in JavaScript or be asynchronously exchanged over WebSockets
48 | - Signed objects can be *stored in databases* without losing the signature
49 | - Signed objects can be *embedded in other JSON objects* since they conform to JSON
50 |
51 | ### On Line Demo
52 | If you want to test the signature scheme without any installation or downloading, a
53 | demo is currently available at: https://test.webpki.org/jws-ct/home
54 |
55 | ### Detailed Signing Operation
56 | 1. Create or parse the JSON object to be signed
57 | 2. Use the result of the previous step as input to the canonicalizing
58 | process described in
59 | https://tools.ietf.org/html/rfc8785#section-3.2
60 | 3. Use the result of the previous step as "JWS Payload" to the JWS signature process described in
61 | https://tools.ietf.org/html/rfc7515#appendix-F using the *compact* serialization mode
62 | 4. Add the resulting JWS string to the original JSON
63 | object through a *designated signature property of your choice*
64 |
65 | ### Detailed Validation Operation
66 | 1. Parse the signed JSON object
67 | 2. Read and save the JWS string from the *designated signature property*
68 | 3. Remove the *designated signature property* from the parsed JSON object
69 | 4. Apply the canonicalizing process described in
70 | https://tools.ietf.org/html/rfc8785#section-3.2 on the remaining object
71 | 5. Use the result of the previous step as "JWS Payload" to the JWS validation process described in
72 | https://tools.ietf.org/html/rfc7515#appendix-F
73 |
74 | ### Available Canonicalization Software
75 | - https://www.npmjs.com/package/canonicalize
76 | - https://github.com/cyberphone/json-canonicalization/tree/master/dotnet#json-canonicalizer-for-net
77 | - https://github.com/cyberphone/json-canonicalization/tree/master/java/canonicalizer#json-canonicalizer-for-java
78 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/DumpASN1Servlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2006-2019 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 |
21 | import java.util.logging.Logger;
22 |
23 | import javax.servlet.ServletException;
24 |
25 | import javax.servlet.http.HttpServlet;
26 | import javax.servlet.http.HttpServletRequest;
27 | import javax.servlet.http.HttpServletResponse;
28 |
29 | import org.webpki.asn1.DerDecoder;
30 |
31 | import org.webpki.util.Base64;
32 |
33 | public class DumpASN1Servlet extends HttpServlet {
34 |
35 | private static final long serialVersionUID = 1L;
36 |
37 | static Logger logger = Logger.getLogger(DumpASN1Servlet.class.getName());
38 |
39 | // HTML form arguments
40 | static final String PEM_OBJECT = "pem";
41 |
42 | public void doPost(HttpServletRequest request, HttpServletResponse response)
43 | throws IOException, ServletException {
44 | try {
45 | request.setCharacterEncoding("utf-8");
46 | if (!request.getContentType().startsWith("application/x-www-form-urlencoded")) {
47 | throw new IOException("Unexpected MIME type:" + request.getContentType());
48 | }
49 |
50 | String pem = CreateServlet.getParameter(request, PEM_OBJECT);
51 | int i = pem.indexOf("-----BEGIN ");
52 | if (i != 0) {
53 | badPem();
54 | }
55 | i += 11;
56 | int j = pem.indexOf("-----", 8);
57 | if (j < 0) {
58 | badPem();
59 | }
60 | String objectType = pem.substring(i, j);
61 | int l = objectType.length();
62 | if (l > 20) {
63 | badPem();
64 | }
65 | i = pem.lastIndexOf("-----END " + objectType + "-----");
66 | if (i < 0) {
67 | badPem();
68 | }
69 | if (i != pem.length() - l - 14) {
70 | badPem();
71 | }
72 | byte[] asn1 = Base64.decode(pem.substring(l + 17, pem.length() - 14 - l));
73 | StringBuilder html = new StringBuilder(
74 | "")
75 | .append(HTML.fancyCode("pem",
76 | pem,
77 | "PEM object"))
78 | .append(HTML.fancyBox("asn.1",
79 | "" +
80 | HTML.encode(DerDecoder.decode(asn1).toString(true, true),
81 | true).replace(" ", " ") +
82 | "",
83 | "ASN.1 dump"));
84 | // Finally, print it out
85 | HTML.standardPage(response, null, html.append("
"));
86 | } catch (Exception e) {
87 | HTML.errorPage(response, e);
88 | }
89 | }
90 |
91 | private void badPem() throws IOException {
92 | throw new IOException("Unrecognized PEM");
93 | }
94 |
95 | public void doGet(HttpServletRequest request, HttpServletResponse response)
96 | throws IOException, ServletException {
97 |
98 | HTML.standardPage(response, null, new StringBuilder(
99 | "" +
113 | "
"));
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/KeyConvertServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2006-2019 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 |
21 | import java.security.KeyPair;
22 | import java.security.PublicKey;
23 |
24 | import java.util.logging.Logger;
25 |
26 | import javax.servlet.ServletException;
27 |
28 | import javax.servlet.http.HttpServlet;
29 | import javax.servlet.http.HttpServletRequest;
30 | import javax.servlet.http.HttpServletResponse;
31 |
32 | import org.webpki.crypto.AlgorithmPreferences;
33 |
34 | import org.webpki.jose.JOSEKeyWords;
35 |
36 | import org.webpki.json.JSONObjectReader;
37 | import org.webpki.json.JSONOutputFormats;
38 | import org.webpki.json.JSONParser;
39 |
40 | import org.webpki.tools.KeyStore2JWKConverter;
41 | import org.webpki.tools.KeyStore2PEMConverter;
42 |
43 | import org.webpki.util.PEMDecoder;
44 |
45 | public class KeyConvertServlet extends HttpServlet {
46 |
47 | private static final long serialVersionUID = 1L;
48 |
49 | static Logger logger = Logger.getLogger(KeyConvertServlet.class.getName());
50 |
51 | // HTML form arguments
52 | static final String KEY_DATA = "key";
53 |
54 | public void doPost(HttpServletRequest request, HttpServletResponse response)
55 | throws IOException, ServletException {
56 | try {
57 | request.setCharacterEncoding("utf-8");
58 | if (!request.getContentType().startsWith("application/x-www-form-urlencoded")) {
59 | throw new IOException("Unexpected MIME type:" + request.getContentType());
60 | }
61 |
62 | // Get the input key
63 | KeyPair keyPair = null;
64 | PublicKey publicKey = null;
65 | String keyData = CreateServlet.getParameter(request, KEY_DATA);
66 | boolean jwkFound = false;
67 | if (keyData.startsWith("{")) {
68 | jwkFound = true;
69 | JSONObjectReader parsedJson = JSONParser.parse(keyData);
70 | if (parsedJson.hasProperty(JOSEKeyWords.KID_JSON)) {
71 | parsedJson.removeProperty(JOSEKeyWords.KID_JSON);
72 | }
73 | try {
74 | keyPair = parsedJson.getKeyPair();
75 | } catch (Exception e) {
76 | publicKey = parsedJson.getCorePublicKey(AlgorithmPreferences.JOSE);
77 | }
78 | } else {
79 | byte[] keyDataBin = keyData.getBytes("utf-8");
80 | try {
81 | keyPair = PEMDecoder.getKeyPair(keyDataBin);
82 | } catch (Exception e) {
83 | if (keyData.contains("PRIVATE KEY")) {
84 | throw e;
85 | }
86 | publicKey = PEMDecoder.getPublicKey(keyDataBin);
87 | }
88 | }
89 | KeyStore2PEMConverter pemConverter = new KeyStore2PEMConverter();
90 | if (keyPair == null) {
91 | pemConverter.writePublicKey(publicKey);
92 | } else {
93 | pemConverter.writePrivateKey(keyPair.getPrivate(), keyPair.getPublic());
94 | }
95 | String pem = new String(pemConverter.getData(), "utf-8");
96 | KeyStore2JWKConverter jwkConverter = new KeyStore2JWKConverter();
97 | String jwk = keyPair == null ?
98 | jwkConverter.writePublicKey(publicKey)
99 | :
100 | jwkConverter.writePrivateKey(keyPair.getPrivate(), keyPair.getPublic());
101 | if (jwkFound) {
102 | jwk = keyData;
103 | } else {
104 | pem = keyData;
105 | }
106 | StringBuilder html = new StringBuilder(
107 | "")
108 | .append(HTML.fancyBox("jwk",
109 | JSONParser.parse(jwk).serializeToString(
110 | JSONOutputFormats.PRETTY_HTML),
111 | "\"Pretty-printed\" JWK"))
112 | .append(HTML.fancyCode("pem",
113 | pem,
114 | "Key in PEM format"));
115 |
116 | // Finally, print it out
117 | HTML.standardPage(response, null, html.append("
"));
118 | } catch (Exception e) {
119 | HTML.errorPage(response, e);
120 | }
121 | }
122 |
123 | public void doGet(HttpServletRequest request, HttpServletResponse response)
124 | throws IOException, ServletException {
125 |
126 | HTML.standardPage(response, null, new StringBuilder(
127 | "" +
149 | "
"));
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/HashServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2006-2019 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 |
21 | import java.util.logging.Logger;
22 |
23 | import javax.servlet.ServletException;
24 |
25 | import javax.servlet.http.HttpServlet;
26 | import javax.servlet.http.HttpServletRequest;
27 | import javax.servlet.http.HttpServletResponse;
28 |
29 | import org.webpki.crypto.AlgorithmPreferences;
30 | import org.webpki.crypto.HashAlgorithms;
31 |
32 | import org.webpki.json.JSONObjectReader;
33 | import org.webpki.json.JSONOutputFormats;
34 | import org.webpki.json.JSONParser;
35 |
36 | import org.webpki.util.ArrayUtil;
37 | import org.webpki.util.Base64URL;
38 |
39 | public class HashServlet extends HttpServlet {
40 |
41 | private static final long serialVersionUID = 1L;
42 |
43 | static Logger logger = Logger.getLogger(HashServlet.class.getName());
44 |
45 | // HTML form arguments
46 | static final String JSON_DATA = "json";
47 |
48 | static final String HASH_ALGORITHM = "alg";
49 |
50 | public void doPost(HttpServletRequest request, HttpServletResponse response)
51 | throws IOException, ServletException {
52 | try {
53 | request.setCharacterEncoding("utf-8");
54 | if (!request.getContentType().startsWith("application/x-www-form-urlencoded")) {
55 | throw new IOException("Unexpected MIME type:" + request.getContentType());
56 | }
57 |
58 | // Get the input data items
59 | JSONObjectReader parsedJson = JSONParser.parse(
60 | CreateServlet.getParameter(request, JSON_DATA));
61 | HashAlgorithms hashAlgorithm = HashAlgorithms.getAlgorithmFromId(
62 | CreateServlet.getParameter(request, HASH_ALGORITHM),
63 | AlgorithmPreferences.JOSE);
64 |
65 | // Create a pretty-printed JSON object without canonicalization
66 | String prettyJson = parsedJson.serializeToString(JSONOutputFormats.PRETTY_HTML);
67 |
68 | // Create a canonicalized (RFC 8785) version of the JSON data
69 | String canonicalJson = parsedJson.serializeToString(JSONOutputFormats.CANONICALIZED);
70 | byte[] canonicalJsonBinary = canonicalJson.getBytes("utf-8");
71 |
72 | // Hash the UTF-8
73 | byte[] hashedJson = hashAlgorithm.digest(canonicalJsonBinary);
74 |
75 | StringBuilder html = new StringBuilder(
76 | "")
77 | .append(HTML.fancyBox("pretty",
78 | prettyJson,
79 | "\"Pretty-printed\" JSON data"))
80 | .append(HTML.fancyCode("canonical",
81 | canonicalJson,
82 | "Canonical (RFC 8785) version of the JSON data"))
83 | .append(HTML.fancyBox("canonicalhex",
84 | ArrayUtil.toHexString(canonicalJsonBinary, 0, -1, false, ' '),
85 | "Canonical data in hexadecimal"))
86 | .append(HTML.fancyBox("algorithm",
87 | hashAlgorithm.getJoseAlgorithmId(),
88 | "Hash algorithm in JOSE-like notation"))
89 | .append(HTML.fancyBox("hex",
90 | ArrayUtil.toHexString(hashedJson, 0, -1, false, ' '),
91 | "Hash in hexadecimal"))
92 | .append(HTML.fancyBox("b64u",
93 | Base64URL.encode(hashedJson),
94 | "Hash in Base64Url"));
95 |
96 | // Finally, print it out
97 | HTML.standardPage(response, null, html.append("
"));
98 | } catch (Exception e) {
99 | HTML.errorPage(response, e);
100 | }
101 | }
102 |
103 | StringBuilder algorithmSelector() throws IOException {
104 | StringBuilder html = new StringBuilder(
105 | "" +
106 | "
Selected hash algoritm: " +
107 | "");
108 |
109 | for (HashAlgorithms algorithm : HashAlgorithms.values()) {
110 | if (algorithm == HashAlgorithms.SHA1) {
111 | continue; // Deprecated these days...
112 | }
113 | String algId = algorithm.getAlgorithmId(AlgorithmPreferences.JOSE);
114 | html.append("" : ">")
118 | .append(algId)
119 | .append(" - ")
120 | .append(algorithm.toString())
121 | .append(" ");
122 | }
123 | return html.append("
");
124 | }
125 |
126 | public void doGet(HttpServletRequest request, HttpServletResponse response)
127 | throws IOException, ServletException {
128 |
129 | HTML.standardPage(response, null, new StringBuilder(
130 | "" +
145 | "
"));
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/HTML.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 |
21 | import java.util.logging.Logger;
22 |
23 | import javax.servlet.ServletException;
24 |
25 | import javax.servlet.http.HttpServletRequest;
26 | import javax.servlet.http.HttpServletResponse;
27 |
28 | public class HTML {
29 |
30 | static Logger logger = Logger.getLogger(HTML.class.getName());
31 |
32 | static final String HTML_INIT = "" +
33 | " " +
34 | " " +
35 | "JWS/CT Signature Lab " +
36 | " ";
37 |
38 | static String encode(String val, boolean newLineExpansion) {
39 | if (val != null) {
40 | StringBuilder buf = new StringBuilder(val.length() + 8);
41 | char c;
42 |
43 | for (int i = 0; i < val.length(); i++) {
44 | c = val.charAt(i);
45 | switch (c) {
46 | case '\n':
47 | buf.append(newLineExpansion ? " " : "\n");
48 | break;
49 | case '<':
50 | buf.append("<");
51 | break;
52 | case '>':
53 | buf.append(">");
54 | break;
55 | case '&':
56 | buf.append("&");
57 | break;
58 | case '\"':
59 | buf.append(""");
60 | break;
61 | case '\'':
62 | buf.append("'");
63 | break;
64 | default:
65 | buf.append(c);
66 | break;
67 | }
68 | }
69 | return buf.toString();
70 | } else {
71 | return new String("");
72 | }
73 | }
74 |
75 | static String getHTML(String javascript, String box) {
76 | StringBuilder html = new StringBuilder(HTML_INIT);
77 | if (javascript != null) {
78 | html.append("");
80 | }
81 | html.append("" +
82 | "" +
83 | "
" +
86 | "
" +
87 | "
" +
90 | "
" +
91 | "
")
92 | .append(box).append("");
93 | return html.toString();
94 | }
95 |
96 | static void output(HttpServletResponse response, String html)
97 | throws IOException, ServletException {
98 | if (JwsCtService.logging) {
99 | logger.info(html);
100 | }
101 | response.setContentType("text/html; charset=utf-8");
102 | response.setHeader("Pragma", "No-Cache");
103 | response.setDateHeader("EXPIRES", 0);
104 | response.getOutputStream().write(html.getBytes("utf-8"));
105 | }
106 |
107 | static String getConditionalParameter(HttpServletRequest request,
108 | String name) {
109 | String value = request.getParameter(name);
110 | if (value == null) {
111 | return "";
112 | }
113 | return value;
114 | }
115 |
116 | public static String boxHeader(String id, String text, boolean visible) {
117 | return new StringBuilder("" +
122 | "
" + text + ":
").toString();
123 | }
124 |
125 | public static String fancyBox(String id, String content, String header) {
126 | return boxHeader(id, header, true) +
127 | "
" + content + "
";
128 | }
129 |
130 | public static String fancyCode(String id, String content, String header) {
131 | return boxHeader(id, header, true) +
132 | "" + encode(content, true) + "
";
133 | }
134 |
135 | public static String fancyText(boolean visible,
136 | String id,
137 | int rows,
138 | String content,
139 | String header) {
140 | return boxHeader(id, header, visible) +
141 | "";
146 | }
147 |
148 | static void standardPage(HttpServletResponse response,
149 | String javaScript,
150 | StringBuilder html) throws IOException, ServletException {
151 | HTML.output(response, HTML.getHTML(javaScript, html.toString()));
152 | }
153 |
154 | public static void noWebCryptoPage(HttpServletResponse response)
155 | throws IOException, ServletException {
156 | HTML.output(
157 | response,
158 | HTML.getHTML(
159 | null,
160 | "Your Browser Doesn't Support WebCrypto :-("));
161 | }
162 |
163 | static String javaScript(String string) {
164 | StringBuilder html = new StringBuilder();
165 | for (char c : string.toCharArray()) {
166 | if (c == '\n') {
167 | html.append("\\n");
168 | } else {
169 | html.append(c);
170 | }
171 | }
172 | return html.toString();
173 | }
174 |
175 | public static void errorPage(HttpServletResponse response, Exception e)
176 | throws IOException, ServletException {
177 | StringBuilder error = new StringBuilder("Stack trace:\n")
178 | .append(e.getClass().getName())
179 | .append(": ")
180 | .append(e.getMessage());
181 | StackTraceElement[] st = e.getStackTrace();
182 | int length = st.length;
183 | if (length > 20) {
184 | length = 20;
185 | }
186 | for (int i = 0; i < length; i++) {
187 | String entry = st[i].toString();
188 | if (entry.contains(".HttpServlet")) {
189 | break;
190 | }
191 | error.append("\n at " + entry);
192 | }
193 | standardPage(response,
194 | null,
195 | new StringBuilder(
196 | "" +
197 | "")
198 | .append(encode(error.toString(), false))
199 | .append(" "));
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/ValidateServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2006-2019 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 |
21 | import java.security.cert.X509Certificate;
22 |
23 | import java.util.logging.Logger;
24 |
25 | import javax.servlet.ServletException;
26 |
27 | import javax.servlet.http.HttpServlet;
28 | import javax.servlet.http.HttpServletRequest;
29 | import javax.servlet.http.HttpServletResponse;
30 |
31 | import org.webpki.crypto.AlgorithmPreferences;
32 | import org.webpki.crypto.CertificateInfo;
33 |
34 | import org.webpki.json.JSONObjectReader;
35 | import org.webpki.json.JSONOutputFormats;
36 | import org.webpki.json.JSONParser;
37 |
38 | import org.webpki.jose.jws.JWSHmacValidator;
39 | import org.webpki.jose.jws.JWSValidator;
40 | import org.webpki.jose.jws.JWSAsymSignatureValidator;
41 | import org.webpki.jose.jws.JWSDecoder;
42 |
43 | import org.webpki.util.Base64URL;
44 | import org.webpki.util.PEMDecoder;
45 |
46 | public class ValidateServlet extends HttpServlet {
47 |
48 | private static final long serialVersionUID = 1L;
49 |
50 | static Logger logger = Logger.getLogger(ValidateServlet.class.getName());
51 |
52 | // HTML form arguments
53 | static final String JWS_OBJECT = "jws";
54 |
55 | static final String JWS_VALIDATION_KEY = "vkey";
56 |
57 | static final String JWS_SIGN_LABL = "siglbl";
58 |
59 | public void doPost(HttpServletRequest request, HttpServletResponse response)
60 | throws IOException, ServletException {
61 | try {
62 | request.setCharacterEncoding("utf-8");
63 | if (!request.getContentType().startsWith("application/x-www-form-urlencoded")) {
64 | throw new IOException("Unexpected MIME type:" + request.getContentType());
65 | }
66 |
67 | // Get the three input data items
68 | JSONObjectReader parsedObject = JSONParser.parse(
69 | CreateServlet.getParameter(request, JWS_OBJECT));
70 | String validationKey = CreateServlet.getParameter(request, JWS_VALIDATION_KEY);
71 | String signatureLabel = CreateServlet.getParameter(request, JWS_SIGN_LABL);
72 |
73 | // Create a pretty-printed JSON object without canonicalization
74 | String prettySignature = parsedObject.serializeToString(JSONOutputFormats.PRETTY_HTML);
75 |
76 | // Now begin the real work...
77 |
78 | // Decode
79 | JWSDecoder JWSDecoder = new JWSDecoder(parsedObject, signatureLabel);
80 |
81 | // For demo purposes only
82 | String jwsString = parsedObject.getString(signatureLabel);
83 |
84 | X509Certificate[] certificatePath = JWSDecoder.getOptionalCertificatePath();
85 | StringBuilder certificateData = null;
86 | if (certificatePath != null) {
87 | for (X509Certificate certificate : certificatePath) {
88 | if (certificateData == null) {
89 | certificateData = new StringBuilder();
90 | } else {
91 | certificateData.append("\n\n");
92 | }
93 | certificateData.append(new CertificateInfo(certificate).toString()
94 | .replace(" ", ""));
95 | }
96 | }
97 |
98 | // Recreate the validation key and validate the signature
99 | JWSValidator JWSValidator;
100 | boolean jwkValidationKey = validationKey.startsWith("{");
101 | if (JWSDecoder.getSignatureAlgorithm().isSymmetric()) {
102 | JWSValidator = new JWSHmacValidator(CreateServlet.decodeSymmetricKey(validationKey));
103 | } else {
104 | JWSValidator = new JWSAsymSignatureValidator(jwkValidationKey ?
105 | JSONParser.parse(validationKey).getCorePublicKey(AlgorithmPreferences.JOSE)
106 | :
107 | PEMDecoder.getPublicKey(validationKey.getBytes("utf-8")));
108 | }
109 | JWSValidator.validate(JWSDecoder);
110 | StringBuilder html = new StringBuilder(
111 | "")
112 | .append(HTML.fancyBox("signed",
113 | prettySignature,
114 | "\"Pretty-printed\" JWS/CT object"))
115 | .append(HTML.fancyCode("header",
116 | JWSDecoder.getJWSHeaderAsString(),
117 | "Decoded JWS header"))
118 | .append(HTML.fancyCode("canonical",
119 | new String(JWSDecoder.getPayload(), "utf-8"),
120 | "Canonical (RFC 8785) version of the signed JSON data " +
121 | "(\"JWS Payload\")"))
122 | .append(HTML.fancyBox("vkey",
123 | jwkValidationKey ?
124 | JSONParser.parse(validationKey)
125 | .serializeToString(JSONOutputFormats.PRETTY_HTML)
126 | :
127 | HTML.encode(validationKey, true),
128 | "Signature validation " +
129 | (JWSDecoder.getSignatureAlgorithm().isSymmetric() ?
130 | "secret key " +
131 | (validationKey.startsWith("@") ? "string value" : "in hexadecimal")
132 | :
133 | "public key in " +
134 | (jwkValidationKey ? "JWK" : "PEM") +
135 | " format")));
136 | if (certificateData != null) {
137 | html.append(HTML.fancyCode("certpath",
138 | certificateData.toString(),
139 | "Core certificate data"));
140 | }
141 | html.append(HTML.fancyBox("original",
142 | new StringBuilder(jwsString)
143 | .insert(jwsString.indexOf('.') + 1,
144 | Base64URL.encode(JWSDecoder.getPayload())).toString(),
145 | "Finally (as a reference only...), the same object expressed as a standard JWS"));
146 |
147 | // Finally, print it out
148 | HTML.standardPage(response, null, html.append("
"));
149 | } catch (Exception e) {
150 | HTML.errorPage(response, e);
151 | }
152 | }
153 |
154 | public void doGet(HttpServletRequest request, HttpServletResponse response)
155 | throws IOException, ServletException {
156 |
157 | HTML.standardPage(response, null, new StringBuilder(
158 | "" +
183 | "
"));
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/JwsCtService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 | import java.io.InputStream;
21 |
22 | import java.security.PrivateKey;
23 |
24 | import java.util.logging.Level;
25 | import java.util.logging.Logger;
26 |
27 | import javax.servlet.ServletContextEvent;
28 | import javax.servlet.ServletContextListener;
29 |
30 | import org.webpki.crypto.AlgorithmPreferences;
31 | import org.webpki.crypto.AsymSignatureAlgorithms;
32 | import org.webpki.crypto.CustomCryptoProvider;
33 | import org.webpki.crypto.HmacAlgorithms;
34 | import org.webpki.crypto.SignatureAlgorithms;
35 |
36 | import org.webpki.jose.jws.JWSAsymKeySigner;
37 |
38 | import org.webpki.json.JSONObjectWriter;
39 | import org.webpki.json.JSONOutputFormats;
40 | import org.webpki.json.JSONParser;
41 |
42 | import org.webpki.util.IO;
43 | import org.webpki.util.PEMDecoder;
44 |
45 | import org.webpki.webutil.InitPropertyReader;
46 |
47 | public class JwsCtService extends InitPropertyReader implements ServletContextListener {
48 |
49 | static Logger logger = Logger.getLogger(JwsCtService.class.getName());
50 |
51 | static String sampleSignature;
52 |
53 | static String sampleJsonForHashing;
54 |
55 | static String samplePublicKey;
56 |
57 | static String sampleKeyConversionKey;
58 |
59 | static String keyDeclarations;
60 |
61 | static boolean logging;
62 |
63 | class KeyDeclaration {
64 |
65 | static final String PRIVATE_KEYS = "privateKeys";
66 | static final String SECRET_KEYS = "secretKeys";
67 | static final String CERTIFICATES = "certifictes";
68 |
69 | StringBuilder decl = new StringBuilder("var ");
70 | StringBuilder after = new StringBuilder();
71 | String name;
72 | String last;
73 | String base;
74 |
75 | KeyDeclaration(String name, String base) {
76 | this.name = name;
77 | this.base = base;
78 | decl.append(name)
79 | .append(" = {");
80 | }
81 |
82 | KeyDeclaration addKey(SignatureAlgorithms alg, String fileOrNull) throws IOException {
83 | String algId = alg.getAlgorithmId(AlgorithmPreferences.JOSE);
84 | if (fileOrNull == null) {
85 | after.append(name)
86 | .append('.')
87 | .append(algId)
88 | .append(" = ")
89 | .append(name)
90 | .append('.')
91 | .append(last)
92 | .append(";\n");
93 |
94 | } else {
95 | if (last != null) {
96 | decl.append(',');
97 | }
98 | decl.append("\n ")
99 | .append(algId)
100 | .append(": '")
101 | .append(HTML.javaScript(getEmbeddedResourceString(fileOrNull + base)))
102 | .append('\'');
103 | last = algId;
104 | }
105 | return this;
106 | }
107 |
108 | public String toString() {
109 | return decl.append("\n};\n").append(after).toString();
110 | }
111 | }
112 |
113 | byte[] getEmbeddedResource(String name) throws IOException {
114 | InputStream is = this.getClass().getResourceAsStream(name);
115 | if (is == null) {
116 | throw new IOException("Resource fail for: " + name);
117 | }
118 | return IO.getByteArrayFromInputStream(is);
119 | }
120 |
121 | String getEmbeddedResourceString(String name) throws IOException {
122 | return new String(getEmbeddedResource(name), "utf-8").trim();
123 | }
124 |
125 | @Override
126 | public void contextDestroyed(ServletContextEvent event) {
127 | }
128 |
129 | @Override
130 | public void contextInitialized(ServletContextEvent event) {
131 | initProperties(event);
132 | CustomCryptoProvider.forcedLoad(false);
133 | try {
134 | //=========================================================================================//
135 | // Keys
136 | //=========================================================================================//
137 | keyDeclarations =
138 | new KeyDeclaration(KeyDeclaration.PRIVATE_KEYS, "privatekey.pem")
139 | .addKey(AsymSignatureAlgorithms.ED25519, "ed25519")
140 | .addKey(AsymSignatureAlgorithms.ED448, "ed448")
141 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA256, "p256")
142 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA384, "p384")
143 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA512, "p521")
144 | .addKey(AsymSignatureAlgorithms.RSA_SHA256, "r2048")
145 | .addKey(AsymSignatureAlgorithms.RSA_SHA384, null)
146 | .addKey(AsymSignatureAlgorithms.RSA_SHA512, null)
147 | .addKey(AsymSignatureAlgorithms.RSAPSS_SHA256, null)
148 | .addKey(AsymSignatureAlgorithms.RSAPSS_SHA384, null)
149 | .addKey(AsymSignatureAlgorithms.RSAPSS_SHA512, null).toString() +
150 | new KeyDeclaration(KeyDeclaration.CERTIFICATES, "certpath.pem")
151 | .addKey(AsymSignatureAlgorithms.ED25519, "ed25519")
152 | .addKey(AsymSignatureAlgorithms.ED448, "ed448")
153 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA256, "p256")
154 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA384, "p384")
155 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA512, "p521")
156 | .addKey(AsymSignatureAlgorithms.RSA_SHA256, "r2048")
157 | .addKey(AsymSignatureAlgorithms.RSA_SHA384, null)
158 | .addKey(AsymSignatureAlgorithms.RSA_SHA512, null)
159 | .addKey(AsymSignatureAlgorithms.RSAPSS_SHA256, null)
160 | .addKey(AsymSignatureAlgorithms.RSAPSS_SHA384, null)
161 | .addKey(AsymSignatureAlgorithms.RSAPSS_SHA512, null).toString() +
162 | new KeyDeclaration(KeyDeclaration.SECRET_KEYS, "bitkey.hex")
163 | .addKey(HmacAlgorithms.HMAC_SHA256, "a256")
164 | .addKey(HmacAlgorithms.HMAC_SHA384, "a384")
165 | .addKey(HmacAlgorithms.HMAC_SHA512, "a512").toString();
166 |
167 | //=========================================================================================//
168 | // Sample data for hashing
169 | //=========================================================================================//
170 | sampleJsonForHashing = getEmbeddedResourceString("sample-data-to-hash.json");
171 |
172 | //=========================================================================================//
173 | // Sample key for converting
174 | //=========================================================================================//
175 | sampleKeyConversionKey = getEmbeddedResourceString("ed25519privatekey.pem");
176 |
177 | //=========================================================================================//
178 | // Sample signature for verification
179 | //=========================================================================================//
180 | String sampleDataToSign = getEmbeddedResourceString("sample-data-to-sign.json");
181 | PrivateKey samplePrivateKey =
182 | PEMDecoder.getPrivateKey(getEmbeddedResource("p256privatekey.pem"));
183 | String jwsString = new JWSAsymKeySigner(samplePrivateKey,
184 | AsymSignatureAlgorithms.ECDSA_SHA256)
185 | .sign(JSONParser.parse(sampleDataToSign)
186 | .serializeToBytes(JSONOutputFormats.CANONICALIZED),
187 | true);
188 | String signature =
189 | new JSONObjectWriter()
190 | .setString(CreateServlet.DEFAULT_SIG_LBL,
191 | jwsString).serializeToString(JSONOutputFormats.PRETTY_PRINT);
192 | sampleSignature = sampleDataToSign.substring(0, sampleDataToSign.lastIndexOf('}')) +
193 | "," +
194 | signature.substring(signature.indexOf("\n "));
195 | samplePublicKey = getEmbeddedResourceString("p256publickey.pem");
196 |
197 | //=========================================================================================//
198 | // Logging?
199 | //=========================================================================================//
200 | logging = getPropertyBoolean("logging");
201 |
202 | logger.info("JWS/CT Demo Successfully Initiated");
203 | } catch (Exception e) {
204 | logger.log(Level.SEVERE, "********\n" + e.getMessage() + "\n********", e);
205 | }
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/WebCryptoServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 |
21 | import javax.servlet.ServletException;
22 |
23 | import javax.servlet.http.HttpServlet;
24 | import javax.servlet.http.HttpServletRequest;
25 | import javax.servlet.http.HttpServletResponse;
26 |
27 | import org.webpki.crypto.AlgorithmPreferences;
28 | import org.webpki.crypto.AsymSignatureAlgorithms;
29 | import org.webpki.crypto.KeyTypes;
30 |
31 | import org.webpki.jose.JOSEKeyWords;
32 |
33 | import org.webpki.json.JSONCryptoHelper;
34 |
35 | public class WebCryptoServlet extends HttpServlet {
36 |
37 | private static final long serialVersionUID = 1L;
38 |
39 | public void doGet(HttpServletRequest request, HttpServletResponse response)
40 | throws IOException, ServletException {
41 |
42 | StringBuilder html = new StringBuilder(
43 | "" +
54 | "" +
55 | "" +
56 | "This demo only relies on ES6 and WebCrypto features and " +
57 | "does not refer to any external libraries either.
" +
58 | "" +
59 | "
" +
60 | "Create RSA Key Pair" +
61 | "
" +
62 | "
" +
63 | "
");
64 |
65 | StringBuilder js = new StringBuilder(
66 | "var pubKey;\n" +
67 | "var privKey;\n" +
68 | "var jsonObject;\n" +
69 | "var publicKeyInJWKFormat; // The bridge between JWS-JCS and WebCrypto\n\n" +
70 | "//////////////////////////////////////////////////////////////////////////\n" +
71 | "// Utility methods //\n" +
72 | "//////////////////////////////////////////////////////////////////////////\n" +
73 | "var BASE64URL_ENCODE = [" +
74 | "'A','B','C','D','E','F','G','H'," +
75 | "'I','J','K','L','M','N','O','P'," +
76 | "'Q','R','S','T','U','V','W','X'," +
77 | "'Y','Z','a','b','c','d','e','f'," +
78 | "'g','h','i','j','k','l','m','n'," +
79 | "'o','p','q','r','s','t','u','v'," +
80 | "'w','x','y','z','0','1','2','3'," +
81 | "'4','5','6','7','8','9','-','_'];\n" +
82 | "function convertToBase64URL(binarray) {\n" +
83 | " var encoded = new String ();\n" +
84 | " var i = 0;\n" +
85 | " var modulo3 = binarray.length % 3;\n" +
86 | " while (i < binarray.length - modulo3) {\n" +
87 | " encoded += BASE64URL_ENCODE[(binarray[i] >>> 2) & 0x3F];\n" +
88 | " encoded += BASE64URL_ENCODE[((binarray[i++] << 4) & 0x30) | ((binarray[i] >>> 4) & 0x0F)];\n" +
89 | " encoded += BASE64URL_ENCODE[((binarray[i++] << 2) & 0x3C) | ((binarray[i] >>> 6) & 0x03)];\n" +
90 | " encoded += BASE64URL_ENCODE[binarray[i++] & 0x3F];\n" +
91 | " }\n" +
92 | " if (modulo3 == 1) {\n" +
93 | " encoded += BASE64URL_ENCODE[(binarray[i] >>> 2) & 0x3F];\n" +
94 | " encoded += BASE64URL_ENCODE[(binarray[i] << 4) & 0x30];\n" +
95 | " }\n" +
96 | " else if (modulo3 == 2) {\n" +
97 | " encoded += BASE64URL_ENCODE[(binarray[i] >>> 2) & 0x3F];\n" +
98 | " encoded += BASE64URL_ENCODE[((binarray[i++] << 4) & 0x30) | ((binarray[i] >>> 4) & 0x0F)];\n" +
99 | " encoded += BASE64URL_ENCODE[(binarray[i] << 2) & 0x3C];\n" +
100 | " }\n" +
101 | " return encoded;\n" +
102 | "}\n\n" +
103 | "function convertToUTF8(string) {\n" +
104 | " var buffer = [];\n" +
105 | " for (var i = 0; i < string.length; i++) {\n" +
106 | " var c = string.charCodeAt(i);\n" +
107 | " if (c < 128) {\n" +
108 | " buffer.push(c);\n" +
109 | " } else if ((c > 127) && (c < 2048)) {\n" +
110 | " buffer.push((c >> 6) | 0xC0);\n" +
111 | " buffer.push((c & 0x3F) | 0x80);\n" +
112 | " } else {\n" +
113 | " buffer.push((c >> 12) | 0xE0);\n" +
114 | " buffer.push(((c >> 6) & 0x3F) | 0x80);\n" +
115 | " buffer.push((c & 0x3F) | 0x80);\n" +
116 | " }\n" +
117 | " }\n" +
118 | " return new Uint8Array(buffer);\n" +
119 | "}\n\n" +
120 | "//////////////////////////////////////////////////////////////////////////\n" +
121 | "// Nice-looking text-boxes //\n" +
122 | "//////////////////////////////////////////////////////////////////////////\n" +
123 | "function fancyJSONBox(header, json) {\n" +
124 | " return '' + header + ':
' + " +
125 | "JSON.stringify(json, null, ' ')" +
126 | ".replace(/&/g,'&')" +
127 | ".replace(//g,'>')" +
129 | ".replace(/\\n/g,' ')" +
130 | ".replace(/ /g,' ') + '
';\n" +
131 | "}\n\n" +
132 | "//////////////////////////////////////////////////////////////////////////\n" +
133 | "// Error message helper //\n" +
134 | "//////////////////////////////////////////////////////////////////////////\n" +
135 | "function bad(id, message) {\n" +
136 | " document.getElementById (id).innerHTML = '' + message + ' ';\n" +
137 | "}\n\n" +
138 | "//////////////////////////////////////////////////////////////////////////\n" +
139 | "// Create key event handler //\n" +
140 | "//////////////////////////////////////////////////////////////////////////\n" +
141 | "function createKey() {\n" +
142 | " if (window.crypto === undefined || window.crypto.subtle == undefined) {\n" +
143 | " document.location.href = 'nowebcrypto';\n" +
144 | " return;\n" +
145 | " }\n" +
146 | " console.log('Begin creating key...');\n" +
147 | " document.getElementById('pub.key').innerHTML = 'Working... ';\n" +
148 | " crypto.subtle.generateKey({name: 'RSASSA-PKCS1-v1_5', " +
149 | "hash: {name: 'SHA-256'}, modulusLength: 2048, " +
150 | "publicExponent: new Uint8Array([0x01, 0x00, 0x01])},\n" +
151 | " false, ['sign', 'verify']).then(function(key) {\n" +
152 | " pubKey = key.publicKey;\n" +
153 | " privKey = key.privateKey;\n\n" +
154 | " crypto.subtle.exportKey('jwk', pubKey).then(function(key) {\n" +
155 | " publicKeyInJWKFormat = key;\n" +
156 | " console.log('generateKey() RSASSA-PKCS1-v1_5: PASS');\n" +
157 | " document.getElementById('pub.key').innerHTML = " +
158 | "fancyJSONBox('Generated public key in JWK format', publicKeyInJWKFormat) + " +
159 | "'" +
160 | "Editable sample data in JSON Format:
" +
161 | "" +
167 | "" +
168 | "
" +
169 | "Sign Sample Data" +
170 | "
" +
171 | "
" +
172 | "';\n" +
173 | " });\n" +
174 | " }).then(undefined, function() {\n" +
175 | " bad('pub.key', 'WebCrypto failed for unknown reasons');\n" +
176 | " });" +
177 | "\n}\n\n" +
178 | "//////////////////////////////////////////////////////////////////////////\n" +
179 | "// Canonicalizer //\n" +
180 | "//////////////////////////////////////////////////////////////////////////\n" +
181 | "var canonicalize = function(object) {\n" +
182 | "\n" +
183 | " var buffer = '';\n" +
184 | " serialize(object);\n" +
185 | " return buffer;\n" +
186 | "\n" +
187 | " function serialize(object) {\n" +
188 | " if (object !== null && typeof object === 'object') {\n" +
189 | " if (Array.isArray(object)) {\n" +
190 | " buffer += '[';\n" +
191 | " let next = false;\n" +
192 | " // Array - Maintain element order\n" +
193 | " object.forEach((element) => {\n" +
194 | " if (next) {\n" +
195 | " buffer += ',';\n" +
196 | " }\n" +
197 | " next = true;\n" +
198 | " // Recursive call\n" +
199 | " serialize(element);\n" +
200 | " });\n" +
201 | " buffer += ']';\n" +
202 | " } else {\n" +
203 | " buffer += '{';\n" +
204 | " let next = false;\n" +
205 | " // Object - Sort properties before serializing\n" +
206 | " Object.keys(object).sort().forEach((property) => {\n" +
207 | " if (next) {\n" +
208 | " buffer += ',';\n" +
209 | " }\n" +
210 | " next = true;\n" +
211 | " // Properties are just strings - Use ES6\n" +
212 | " buffer += JSON.stringify(property);\n" +
213 | " buffer += ':';\n" +
214 | " // Recursive call\n" +
215 | " serialize(object[property]);\n" +
216 | " });\n" +
217 | " buffer += '}';\n" +
218 | " }\n" +
219 | " } else {\n" +
220 | " // Primitive data type - Use ES6\n" +
221 | " buffer += JSON.stringify(object);\n" +
222 | " }\n" +
223 | " }\n" +
224 | "};\n\n" +
225 | "//////////////////////////////////////////////////////////////////////////\n" +
226 | "// Sign event handler //\n" +
227 | "//////////////////////////////////////////////////////////////////////////\n" +
228 | "function signSampleData() {\n" +
229 | " try {\n" +
230 | " document.getElementById('sign.res').innerHTML = '';\n" +
231 | " jsonObject = JSON.parse(document.getElementById('json.text').value);\n" +
232 | " if (typeof jsonObject !== 'object' || Array.isArray(jsonObject)) {\n" +
233 | " bad('sign.res', 'Only JSON objects can be signed');\n" +
234 | " return;\n" +
235 | " }\n" +
236 | " if (jsonObject." +
237 | CreateServlet.DEFAULT_SIG_LBL +
238 | ") {\n" +
239 | " bad('sign.res', 'Object is already signed');\n" +
240 | " return;\n" +
241 | " }\n" +
242 | " var jwsHeader = {};\n" +
243 | " jwsHeader." +
244 | JOSEKeyWords.ALG_JSON +
245 | " = '" +
246 | AsymSignatureAlgorithms.RSA_SHA256.getAlgorithmId(AlgorithmPreferences.JOSE) +
247 | "';\n" +
248 | " var publicKeyObject = {};\n" +
249 | " publicKeyObject." +
250 | JSONCryptoHelper.KTY_JSON +
251 | " = '" +
252 | KeyTypes.RSA.getJoseKty() +
253 | "';\n" +
254 | " publicKeyObject." +
255 | JSONCryptoHelper.N_JSON +
256 | " = publicKeyInJWKFormat." +
257 | JSONCryptoHelper.N_JSON +
258 | ";\n" +
259 | " publicKeyObject." +
260 | JSONCryptoHelper.E_JSON +
261 | " = publicKeyInJWKFormat." +
262 | JSONCryptoHelper.E_JSON +
263 | ";\n" +
264 | " } catch (err) {\n" +
265 | " bad('sign.res', 'JSON error: ' + err.toString());\n" +
266 | " return;\n" +
267 | " }\n" +
268 | " var jwsHeaderB64 = convertToBase64URL(convertToUTF8(JSON.stringify(jwsHeader)));\n" +
269 | " var payloadB64 = convertToBase64URL(convertToUTF8(canonicalize(jsonObject)));\n" +
270 | " crypto.subtle.sign({name: 'RSASSA-PKCS1-v1_5'}, privKey,\n" +
271 | " convertToUTF8(jwsHeaderB64 + '.' + payloadB64" +
272 | ")).then(function(signature) {\n" +
273 | " console.log('Sign with RSASSA-PKCS1-v1_5 - SHA-256: PASS');\n" +
274 | " document.getElementById('" +
275 | ValidateServlet.JWS_VALIDATION_KEY +
276 | "').value = JSON.stringify(publicKeyObject);\n" +
277 | " jsonObject." +
278 | CreateServlet.DEFAULT_SIG_LBL +
279 | " = jwsHeaderB64 + '..' + convertToBase64URL(new Uint8Array(signature));\n" +
280 | " document.getElementById('" + ValidateServlet.JWS_OBJECT +
281 | "').value = JSON.stringify(jsonObject);\n" +
282 | " document.getElementById('sign.res').innerHTML = " +
283 | "fancyJSONBox('Signed data in JWS-JCS format', jsonObject) + '" +
284 | "
" +
285 | "
" +
286 | "Validate Signature (on the server)" +
287 | "
" +
288 | "
';\n" +
289 | " }).then(undefined, function() {\n" +
290 | " bad('sign.res', 'WebCrypto failed for unknown reasons');\n" +
291 | " });\n" +
292 | "}\n\n" +
293 | "//////////////////////////////////////////////////////////////////////////\n" +
294 | "// Optional validation is in this demo/test happening on the server //\n" +
295 | "//////////////////////////////////////////////////////////////////////////\n" +
296 | "function verifySignatureOnServer() {\n" +
297 | " document.forms.shoot.submit();\n" +
298 | "}\n");
299 |
300 | HTML.standardPage(response, js.toString(), html);
301 | }
302 | }
303 |
--------------------------------------------------------------------------------
/src/org/webpki/webapps/jws_ct/CreateServlet.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org).
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.webpki.webapps.jws_ct;
18 |
19 | import java.io.IOException;
20 |
21 | import java.net.URLEncoder;
22 |
23 | import java.security.KeyPair;
24 |
25 | import java.util.logging.Logger;
26 |
27 | import javax.servlet.ServletException;
28 |
29 | import javax.servlet.http.HttpServlet;
30 | import javax.servlet.http.HttpServletRequest;
31 | import javax.servlet.http.HttpServletResponse;
32 |
33 | import org.webpki.crypto.AlgorithmPreferences;
34 | import org.webpki.crypto.AsymSignatureAlgorithms;
35 | import org.webpki.crypto.HmacAlgorithms;
36 | import org.webpki.crypto.SignatureAlgorithms;
37 |
38 | import org.webpki.jose.jws.JWSAsymKeySigner;
39 | import org.webpki.jose.jws.JWSHmacSigner;
40 | import org.webpki.jose.jws.JWSSigner;
41 |
42 | import org.webpki.json.JSONObjectReader;
43 | import org.webpki.json.JSONObjectWriter;
44 | import org.webpki.json.JSONOutputFormats;
45 | import org.webpki.json.JSONParser;
46 |
47 | import org.webpki.util.Base64;
48 | import org.webpki.util.Base64URL;
49 | import org.webpki.util.HexaDecimal;
50 | import org.webpki.util.PEMDecoder;
51 |
52 | public class CreateServlet extends HttpServlet {
53 |
54 | static Logger logger = Logger.getLogger(CreateServlet.class.getName());
55 |
56 | private static final long serialVersionUID = 1L;
57 |
58 | // HTML form arguments
59 | static final String PRM_JSON_DATA = "json";
60 |
61 | static final String PRM_JWS_EXTRA = "xtra";
62 |
63 | static final String PRM_SECRET_KEY = "sec";
64 |
65 | static final String PRM_PRIVATE_KEY = "priv";
66 |
67 | static final String PRM_CERT_PATH = "cert";
68 |
69 | static final String PRM_ALGORITHM = "alg";
70 | static final String PRM_SIG_LABEL = "siglbl";
71 |
72 | static final String FLG_CERT_PATH = "cerflg";
73 | static final String FLG_JAVASCRIPT = "jsflg";
74 | static final String FLG_JWK_INLINE = "jwkflg";
75 |
76 | static final String DEFAULT_ALG = "ES256";
77 | static final String DEFAULT_SIG_LBL = "signature";
78 |
79 | class SelectAlg {
80 |
81 | String preSelected;
82 | StringBuilder html = new StringBuilder("
");
85 |
86 | SelectAlg(String preSelected) {
87 | this.preSelected = preSelected;
88 | }
89 |
90 | SelectAlg add(SignatureAlgorithms algorithm) throws IOException {
91 | String algId = algorithm.getAlgorithmId(AlgorithmPreferences.JOSE);
92 | html.append("" : ">")
96 | .append(algId)
97 | .append(" ");
98 | return this;
99 | }
100 |
101 | @Override
102 | public String toString() {
103 | return html.append(" ").toString();
104 | }
105 | }
106 |
107 | StringBuilder checkBox(String idName, String text, boolean checked, String onchange) {
108 | StringBuilder html = new StringBuilder(
109 | "
");
125 | return html;
126 | }
127 |
128 | public void doGet(HttpServletRequest request, HttpServletResponse response)
129 | throws IOException, ServletException {
130 | String selected = "ES256";
131 | StringBuilder js = new StringBuilder("'use strict';\n")
132 | .append(JwsCtService.keyDeclarations);
133 | StringBuilder html = new StringBuilder(
134 | "
" +
211 | "
");
212 | js.append(
213 | "function fill(id, alg, keyHolder, unconditionally) {\n" +
214 | " let element = document.getElementById(id).children[1];\n" +
215 | " if (unconditionally || element.value == '') element.value = keyHolder[alg];\n" +
216 | "}\n" +
217 | "function disableAndClearCheckBox(id) {\n" +
218 | " let checkBox = document.getElementById(id);\n" +
219 | " checkBox.checked = false;\n" +
220 | " checkBox.disabled = true;\n" +
221 | "}\n" +
222 | "function enableCheckBox(id) {\n" +
223 | " document.getElementById(id).disabled = false;\n" +
224 | "}\n" +
225 | "function setUserData(unconditionally) {\n" +
226 | " let element = document.getElementById('" + PRM_JSON_DATA + "').children[1];\n" +
227 | " if (unconditionally || element.value == '') element.value = '{\\n" +
228 | " \"statement\": \"Hello signed \\\\u0077orld!\",\\n" +
229 | " \"otherProperties\": [2e+3, true]\\n}';\n" +
230 | " element = document.getElementById('" + PRM_JWS_EXTRA + "').children[1];\n" +
231 | " if (unconditionally || element.value == '') element.value = '{\\n}';\n" +
232 | "}\n" +
233 | "function setParameters(alg, unconditionally) {\n" +
234 | " if (alg.startsWith('HS')) {\n" +
235 | " showCert(false);\n" +
236 | " showPriv(false);\n" +
237 | " disableAndClearCheckBox('" + FLG_CERT_PATH + "');\n" +
238 | " disableAndClearCheckBox('" + FLG_JWK_INLINE + "');\n" +
239 | " fill('" + PRM_SECRET_KEY + "', alg, " +
240 | JwsCtService.KeyDeclaration.SECRET_KEYS + ", unconditionally);\n" +
241 | " showSec(true)\n" +
242 | " } else {\n" +
243 | " showSec(false)\n" +
244 | " enableCheckBox('" + FLG_CERT_PATH + "');\n" +
245 | " enableCheckBox('" + FLG_JWK_INLINE + "');\n" +
246 | " fill('" + PRM_PRIVATE_KEY + "', alg, " +
247 | JwsCtService.KeyDeclaration.PRIVATE_KEYS + ", unconditionally);\n" +
248 | " showPriv(true);\n" +
249 | " fill('" + PRM_CERT_PATH + "', alg, " +
250 | JwsCtService.KeyDeclaration.CERTIFICATES + ", unconditionally);\n" +
251 | " showCert(document.getElementById('" + FLG_CERT_PATH + "').checked);\n" +
252 | " }\n" +
253 | "}\n" +
254 | "function jwkFlagChange(flag) {\n" +
255 | " if (flag) {\n" +
256 | " document.getElementById('" + FLG_CERT_PATH + "').checked = false;\n" +
257 | " showCert(false);\n" +
258 | " }\n" +
259 | "}\n" +
260 | "function certFlagChange(flag) {\n" +
261 | " showCert(flag);\n" +
262 | " if (flag) {\n" +
263 | " document.getElementById('" + FLG_JWK_INLINE + "').checked = false;\n" +
264 | " }\n" +
265 | "}\n" +
266 | "function restoreDefaults() {\n" +
267 | " let s = document.getElementById('" + PRM_ALGORITHM + "');\n" +
268 | " for (let i = 0; i < s.options.length; i++) {\n" +
269 | " if (s.options[i].text == '" + DEFAULT_ALG + "') {\n" +
270 | " s.options[i].selected = true;\n" +
271 | " break;\n" +
272 | " }\n" +
273 | " }\n" +
274 | " setParameters('" + DEFAULT_ALG + "', true);\n" +
275 | " document.getElementById('" + FLG_CERT_PATH + "').checked = false;\n" +
276 | " document.getElementById('" + FLG_JAVASCRIPT + "').checked = false;\n" +
277 | " document.getElementById('" + FLG_JWK_INLINE + "').checked = false;\n" +
278 | " document.getElementById('" + PRM_SIG_LABEL + "').value = '" + DEFAULT_SIG_LBL + "';\n" +
279 | " showCert(false);\n" +
280 | " setUserData(true);\n" +
281 | "}\n" +
282 | "function algChange(alg) {\n" +
283 | " setParameters(alg, true);\n" +
284 | "}\n" +
285 | "function showCert(show) {\n" +
286 | " document.getElementById('" + PRM_CERT_PATH + "').style.display= show ? 'block' : 'none';\n" +
287 | "}\n" +
288 | "function showPriv(show) {\n" +
289 | " document.getElementById('" + PRM_PRIVATE_KEY + "').style.display= show ? 'block' : 'none';\n" +
290 | "}\n" +
291 | "function showSec(show) {\n" +
292 | " document.getElementById('" + PRM_SECRET_KEY + "').style.display= show ? 'block' : 'none';\n" +
293 | "}\n" +
294 | "window.addEventListener('load', function(event) {\n" +
295 | " setParameters(document.getElementById('" + PRM_ALGORITHM + "').value, false);\n" +
296 | " setUserData(false);\n" +
297 | "});\n");
298 | HTML.standardPage(response,
299 | js.toString(),
300 | html);
301 | }
302 |
303 | static String getParameter(HttpServletRequest request, String parameter) throws IOException {
304 | String string = request.getParameter(parameter);
305 | if (string == null) {
306 | throw new IOException("Missing data for: "+ parameter);
307 | }
308 | return string.trim();
309 | }
310 |
311 | static byte[] getBinaryParameter(HttpServletRequest request, String parameter) throws IOException {
312 | return getParameter(request, parameter).getBytes("utf-8");
313 | }
314 |
315 | static String getTextArea(HttpServletRequest request, String name)
316 | throws IOException {
317 | String string = getParameter(request, name);
318 | StringBuilder s = new StringBuilder();
319 | for (char c : string.toCharArray()) {
320 | if (c != '\r') {
321 | s.append(c);
322 | }
323 | }
324 | return s.toString();
325 | }
326 |
327 | static byte[] decodeSymmetricKey(String keyString) throws IOException {
328 | return keyString.startsWith("@") ?
329 | keyString.substring(1).getBytes("utf-8")
330 | :
331 | HexaDecimal.decode(keyString);
332 | }
333 |
334 | public void doPost(HttpServletRequest request, HttpServletResponse response)
335 | throws IOException, ServletException {
336 | try {
337 | request.setCharacterEncoding("utf-8");
338 | String jsonData = getTextArea(request, PRM_JSON_DATA);
339 | String signatureLabel = getParameter(request, PRM_SIG_LABEL);
340 | JSONObjectReader reader = JSONParser.parse(jsonData);
341 | if (reader.getJSONArrayReader() != null) {
342 | throw new IOException("The demo does not support signed arrays");
343 | }
344 | JSONObjectReader additionalHeaderData =
345 | JSONParser.parse(getParameter(request, PRM_JWS_EXTRA));
346 | boolean jsFlag = request.getParameter(FLG_JAVASCRIPT) != null;
347 | boolean keyInlining = request.getParameter(FLG_JWK_INLINE) != null;
348 | boolean certOption = request.getParameter(FLG_CERT_PATH) != null;
349 |
350 | // Get wanted signature algorithm
351 | String algorithmParam = getParameter(request, PRM_ALGORITHM);
352 | SignatureAlgorithms signatureAlgorithm = algorithmParam.startsWith("HS") ?
353 | HmacAlgorithms.getAlgorithmFromId(algorithmParam,
354 | AlgorithmPreferences.JOSE)
355 | :
356 | AsymSignatureAlgorithms.getAlgorithmFromId(algorithmParam,
357 | AlgorithmPreferences.JOSE);
358 |
359 | // Get the signature key
360 | JWSSigner JWSSigner;
361 | String validationKey;
362 |
363 | // Symmetric or asymmetric?
364 | if (signatureAlgorithm.isSymmetric()) {
365 | validationKey = getParameter(request, PRM_SECRET_KEY);
366 | JWSSigner = new JWSHmacSigner(decodeSymmetricKey(validationKey),
367 | (HmacAlgorithms)signatureAlgorithm);
368 | } else {
369 | // To simplify UI we require PKCS #8 with the public key embedded
370 | // but we also support JWK which also has the public key
371 | byte[] privateKeyBlob = getBinaryParameter(request, PRM_PRIVATE_KEY);
372 | KeyPair keyPair;
373 | if (privateKeyBlob[0] == '{') {
374 | keyPair = JSONParser.parse(privateKeyBlob).getKeyPair();
375 | validationKey =
376 | JSONObjectWriter.createCorePublicKey(
377 | keyPair.getPublic(),
378 | AlgorithmPreferences.JOSE).toString();
379 | } else {
380 | keyPair = PEMDecoder.getKeyPair(privateKeyBlob);
381 | validationKey = "-----BEGIN PUBLIC KEY-----\n" +
382 | Base64.mimeEncode(keyPair.getPublic().getEncoded()) +
383 | "\n-----END PUBLIC KEY-----";
384 | }
385 | privateKeyBlob = null; // Nullify it after use
386 | JWSSigner = new JWSAsymKeySigner(keyPair.getPrivate(),
387 | (AsymSignatureAlgorithms)signatureAlgorithm);
388 |
389 | // Add other JWS header data that the demo program fixes
390 | if (certOption) {
391 | ((JWSAsymKeySigner)JWSSigner).setCertificatePath(
392 | PEMDecoder.getCertificatePath(getBinaryParameter(request,
393 | PRM_CERT_PATH)));
394 | } else if (keyInlining) {
395 | ((JWSAsymKeySigner)JWSSigner).setPublicKey(keyPair.getPublic());
396 | }
397 | }
398 |
399 | // Add any optional (by the user specified) arguments
400 | JWSSigner.addHeaderItems(additionalHeaderData);
401 |
402 | // Create the detached JWS data to be signed. Of course using RFC 8785 :)
403 | byte[] jwsPayload = reader.serializeToBytes(JSONOutputFormats.CANONICALIZED);
404 |
405 | // Sign it using the provided algorithm and key
406 |
407 | // Note: we didn't use the JWS/CT API method because it hides
408 | // the data needed for illustrating the function.
409 | String jwsString = JWSSigner.sign(jwsPayload, true);
410 |
411 | // Create the completed object
412 | String signedJsonObject = new JSONObjectWriter(reader)
413 | .setString(signatureLabel, jwsString)
414 | .serializeToString(JSONOutputFormats.NORMALIZED);
415 |
416 | // How things should appear in a "regular" JWS
417 | if (JwsCtService.logging) {
418 | logger.info(jwsString.substring(0, jwsString.lastIndexOf('.')) +
419 | Base64URL.encode(jwsPayload) +
420 | jwsString.substring(jwsString.lastIndexOf('.')));
421 | }
422 |
423 | // We terminate by validating the signature as well
424 | request.getRequestDispatcher((jsFlag ? "jssignature?" : "validate?") +
425 | ValidateServlet.JWS_OBJECT +
426 | "=" +
427 | URLEncoder.encode(signedJsonObject, "utf-8") +
428 | "&" +
429 | ValidateServlet.JWS_VALIDATION_KEY +
430 | "=" +
431 | URLEncoder.encode(validationKey, "utf-8") +
432 | "&" +
433 | ValidateServlet.JWS_SIGN_LABL +
434 | "=" +
435 | URLEncoder.encode(signatureLabel, "utf-8"))
436 | .forward(request, response);
437 | } catch (Exception e) {
438 | HTML.errorPage(response, e);
439 | }
440 | }
441 | }
442 |
--------------------------------------------------------------------------------