├── .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 | "
Signatures in JavaScript Notation
") 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 | "
JSON Clear Text Signature
" + 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 | "" + 49 | "" + 54 | "" + 59 | "" + 64 | "" + 69 | "" + 74 | "
" + 47 | "Create JSON Signatures" + 48 | "
" + 52 | "Validate JSON Signatures" + 53 | "
" + 57 | ""Experimental" - WebCrypto" + 58 | "
" + 62 | "Canonicalize and Hash JSON" + 63 | "
" + 67 | "Convert JWK <-> PEM Keys" + 68 | "
" + 72 | "Dump PEM as ASN.1" + 73 | "
" + 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 | ![JWS-CT](https://cyberphone.github.io/doc/security/jws-ct.svg) 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 | "
PEM Successfully Decoded
") 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 | "
" + 100 | "
Dump PEM as ASN.1
") 101 | .append(HTML.fancyText(true, 102 | PEM_OBJECT, 103 | 10, 104 | JwsCtService.sampleKeyConversionKey, 105 | "Paste a single PEM object or try with the default")) 106 | .append( 107 | "
" + 108 | "
" + 109 | "Dump PEM Object" + 110 | "
" + 111 | "
" + 112 | "
" + 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 | "
Key Successfully Converted
") 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 | "
" + 128 | "
Convert JWK <-> PEM
") 129 | .append(HTML.fancyText(true, 130 | KEY_DATA, 131 | 10, 132 | JwsCtService.sampleKeyConversionKey, 133 | "Paste public or private key in JWK or PEM format or try with the default")) 134 | .append( 135 | "
Limitations:" + 136 | "" + 142 | "
" + 143 | "
" + 144 | "
" + 145 | "Convert Key" + 146 | "
" + 147 | "
" + 148 | "
" + 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 | "
JSON Data Successfully Hashed
") 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 | "" + 107 | "
Selected hash algoritm:
"); 124 | } 125 | 126 | public void doGet(HttpServletRequest request, HttpServletResponse response) 127 | throws IOException, ServletException { 128 | 129 | HTML.standardPage(response, null, new StringBuilder( 130 | "
" + 131 | "
Canonicalize and Hash JSON Data
") 132 | .append(HTML.fancyText(true, 133 | JSON_DATA, 134 | 10, 135 | JwsCtService.sampleJsonForHashing, 136 | "Paste JSON data in the text box or try with the default")) 137 | .append(algorithmSelector()) 138 | .append( 139 | "
" + 140 | "
" + 141 | "Hash JSON Data" + 142 | "
" + 143 | "
" + 144 | "
" + 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 | "" + 144 | encode(content, false) + 145 | ""; 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 | "
Something went wrong...
" + 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 | "
Signature Successfully Validated
") 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 | "
" + 159 | "
Testing JSON Signatures
") 160 | .append(HTML.fancyText(true, 161 | JWS_OBJECT, 162 | 10, 163 | JwsCtService.sampleSignature, 164 | "Paste a signed JSON object in the text box or try with the default")) 165 | .append(HTML.fancyText(true, 166 | JWS_VALIDATION_KEY, 167 | 4, 168 | JwsCtService.samplePublicKey, 169 | "Validation key: secret key in hexadecimal or @string or public " + 170 | "key in PEM or "plain" JWK format")) 171 | .append(HTML.fancyText(true, 172 | JWS_SIGN_LABL, 173 | 1, 174 | CreateServlet.DEFAULT_SIG_LBL, 175 | "Anticipated signature label")) 176 | .append( 177 | "
" + 178 | "
" + 179 | "Validate JSON Signature" + 180 | "
" + 181 | "
" + 182 | "
" + 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 | "
" + 44 | "" + 47 | "" + 50 | "" + 53 | "
" + 54 | "
WebCrypto / JWS-JCS Demo
" + 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("").toString(); 104 | } 105 | } 106 | 107 | StringBuilder checkBox(String idName, String text, boolean checked, String onchange) { 108 | StringBuilder html = new StringBuilder( 109 | "
") 123 | .append(text) 124 | .append("
"); 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 | "
" + 135 | "
JSON Signature Creation
" + 136 | HTML.fancyText( 137 | true, 138 | PRM_JSON_DATA, 139 | 10, 140 | "", 141 | "Paste an unsigned JSON object in the text box or try with the default") + 142 | "
" + 143 | "
" + 144 | "
" + 145 | "
Signature Parameters
" + 146 | "
") 147 | .append(new SelectAlg(selected) 148 | .add(HmacAlgorithms.HMAC_SHA256) 149 | .add(HmacAlgorithms.HMAC_SHA384) 150 | .add(HmacAlgorithms.HMAC_SHA512) 151 | .add(AsymSignatureAlgorithms.ED25519) 152 | .add(AsymSignatureAlgorithms.ED448) 153 | .add(AsymSignatureAlgorithms.ECDSA_SHA256) 154 | .add(AsymSignatureAlgorithms.ECDSA_SHA384) 155 | .add(AsymSignatureAlgorithms.ECDSA_SHA512) 156 | .add(AsymSignatureAlgorithms.RSA_SHA256) 157 | .add(AsymSignatureAlgorithms.RSA_SHA384) 158 | .add(AsymSignatureAlgorithms.RSA_SHA512) 159 | .add(AsymSignatureAlgorithms.RSAPSS_SHA256) 160 | .add(AsymSignatureAlgorithms.RSAPSS_SHA384) 161 | .add(AsymSignatureAlgorithms.RSAPSS_SHA512) 162 | .toString()) 163 | .append( 164 | "
Algorithm
" + 165 | "
Restore defaults
") 166 | .append(checkBox(FLG_JWK_INLINE, "Automagically insert public key (JWK)", 167 | false, "jwkFlagChange(this.checked)")) 168 | .append(checkBox(FLG_CERT_PATH, "Include provided certificate path (X5C)", 169 | false, "certFlagChange(this.checked)")) 170 | .append(checkBox(FLG_JAVASCRIPT, "Serialize as JavaScript (but do not verify)", 171 | false, null)) 172 | .append( 173 | "
" + 174 | "" + 177 | "
 Signature label
" + 178 | "
" + 179 | "
" + 180 | "
" + 181 | "
" + 182 | "Create JSON Signature" + 183 | "
" + 184 | "
") 185 | .append( 186 | HTML.fancyText(true, 187 | PRM_JWS_EXTRA, 188 | 4, 189 | "", 190 | "Additional JWS header parameters (here expressed as properties of a JSON object)")) 191 | .append( 192 | HTML.fancyText(false, 193 | PRM_SECRET_KEY, 194 | 1, 195 | "", 196 | "Secret key in hexadecimal format or @string (where string=key)")) 197 | .append( 198 | HTML.fancyText(false, 199 | PRM_PRIVATE_KEY, 200 | 4, 201 | "", 202 | "Private key in PEM/PKCS #8 or "plain" JWK format")) 203 | .append( 204 | HTML.fancyText(false, 205 | PRM_CERT_PATH, 206 | 4, 207 | "", 208 | "Certificate path in PEM format")) 209 | .append( 210 | "
" + 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 | --------------------------------------------------------------------------------