├── .gitignore ├── .gitattributes ├── web ├── index.jsp ├── webpkiorg.png ├── style.css └── images │ ├── shreq.svg │ └── thelab.svg ├── shreq.properties ├── .project ├── .classpath ├── LICENSE ├── README.md ├── src └── org │ └── webpki │ └── webapps │ └── shreqb64 │ ├── ExtConfReq2Servlet.java │ ├── ExtConfReqServlet.java │ ├── PreConfReq2Servlet.java │ ├── PreConfReqServlet.java │ ├── HomeServlet.java │ ├── CurlServlet.java │ ├── JSONTokenExtractor.java │ ├── HTML.java │ ├── SHREQService.java │ ├── BaseRequestServlet.java │ ├── ValidateServlet.java │ ├── BaseGuiServlet.java │ └── CreateServlet.java ├── shreq └── org │ └── webpki │ └── shreqb64 │ ├── ValidationKeyService.java │ ├── JSONRequestValidation.java │ ├── URIRequestValidation.java │ ├── SHREQSupport.java │ └── ValidationCore.java ├── web.xml └── test └── org └── webpki └── shreqb64 └── TestVectors.java /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /dist 3 | .tmp 4 | 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Disable LF normalization for all files 2 | * -text -------------------------------------------------------------------------------- /web/index.jsp: -------------------------------------------------------------------------------- 1 | <%@page session="false"%><%response.sendRedirect ("home");%> 2 | -------------------------------------------------------------------------------- /shreq.properties: -------------------------------------------------------------------------------- 1 | # Lots of stuff is fetched from here 2 | openkeystore=../openkeystore 3 | -------------------------------------------------------------------------------- /web/webpkiorg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberphone/shreqb64/master/web/webpkiorg.png -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | shreqb64 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 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![SHREQ](https://cyberphone.github.io/doc/security/shreq.svg) 2 | 3 | # Signed HTTP Requests 4 | 5 | [SHREQ documentation](https://github.com/cyberphone/ietf-signed-http-requests) 6 | 7 | This repository contains Java code for SHREQ demo and validation. 8 | 9 | ### Online Testing 10 | There is currently a hosted version of this code at https://mobilepki.org/shreq. 11 | 12 | ### Testing with "Curl" 13 | This line POSTs a signed JSON request: 14 | ```code 15 | $ curl -k --data-binary @myrequest.json -i -H content-type:application/json https://localhost:8442/shreq/preconfreq?something=7 16 | ``` 17 | Note: the -k option is *only for testing* using self-certified servers! 18 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/ExtConfReq2Servlet.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.shreqb64; 18 | 19 | public class ExtConfReq2Servlet extends ExtConfReqServlet { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | @Override 24 | protected boolean enforceTimeStamp() { 25 | return true; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/ExtConfReqServlet.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.shreqb64; 18 | 19 | public class ExtConfReqServlet extends BaseRequestServlet { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | @Override 24 | protected boolean externallyConfigured() { 25 | return true; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/PreConfReq2Servlet.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.shreqb64; 18 | 19 | public class PreConfReq2Servlet extends PreConfReqServlet { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | @Override 24 | protected boolean enforceTimeStamp() { 25 | return true; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/PreConfReqServlet.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.shreqb64; 18 | 19 | public class PreConfReqServlet extends BaseRequestServlet { 20 | 21 | private static final long serialVersionUID = 1L; 22 | 23 | @Override 24 | protected boolean externallyConfigured() { 25 | return false; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /shreq/org/webpki/shreqb64/ValidationKeyService.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.shreqb64; 18 | 19 | import java.io.IOException; 20 | 21 | import java.security.GeneralSecurityException; 22 | import java.security.PublicKey; 23 | 24 | import org.webpki.crypto.SignatureAlgorithms; 25 | 26 | import org.webpki.jose.JOSESupport; 27 | 28 | public interface ValidationKeyService { 29 | 30 | public JOSESupport.CoreSignatureValidator 31 | getSignatureValidator(ValidationCore valiationCode, 32 | SignatureAlgorithms signatureAlgorithm, 33 | PublicKey publicKey, // Also filled for X5C 34 | String keyId) throws IOException, GeneralSecurityException; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /shreq/org/webpki/shreqb64/JSONRequestValidation.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.shreqb64; 18 | 19 | import java.io.IOException; 20 | 21 | import java.security.GeneralSecurityException; 22 | 23 | import java.util.LinkedHashMap; 24 | 25 | import org.webpki.json.JSONObjectReader; 26 | import org.webpki.json.JSONParser; 27 | 28 | public class JSONRequestValidation extends ValidationCore { 29 | 30 | JSONObjectReader message; // "message" in the specification 31 | 32 | public JSONRequestValidation(String targetUri, 33 | String targetMethod, 34 | LinkedHashMap headerMap, 35 | String jwsString) throws IOException, GeneralSecurityException { 36 | super(targetUri, targetMethod, headerMap); 37 | decodeJwsString(jwsString, false); 38 | this.message = JSONParser.parse(JWS_Payload); 39 | } 40 | 41 | @Override 42 | protected void validateImplementation() throws IOException, 43 | GeneralSecurityException { 44 | JSONObjectReader temp = message.getObject(SHREQSupport.SHREQ_SECINF_LABEL); 45 | secinf = commonDataFilter(temp, false); 46 | 47 | String normalizedURI = secinf.getString(SHREQSupport.SHREQ_TARGET_URI); 48 | if (!normalizedURI.equals(normalizedTargetUri)) { 49 | error("Declared URI=" + normalizedURI + " Actual URI=" + normalizedTargetUri); 50 | } 51 | } 52 | 53 | @Override 54 | protected String defaultMethod() { 55 | return SHREQSupport.SHREQ_DEFAULT_JSON_METHOD; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /shreq/org/webpki/shreqb64/URIRequestValidation.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.shreqb64; 18 | 19 | import java.io.IOException; 20 | 21 | import java.security.GeneralSecurityException; 22 | 23 | import java.util.LinkedHashMap; 24 | 25 | import org.webpki.json.JSONParser; 26 | 27 | import org.webpki.util.ArrayUtil; 28 | 29 | public class URIRequestValidation extends ValidationCore { 30 | 31 | static final String QUERY_STRING = SHREQSupport.SHREQ_JWS_QUERY_LABEL + "="; 32 | static final int QUERY_LENGTH = QUERY_STRING.length(); 33 | 34 | public URIRequestValidation(String targetUri, 35 | String targetMethod, 36 | LinkedHashMap headerMap) throws IOException { 37 | super(targetUri, targetMethod, headerMap); 38 | } 39 | 40 | @Override 41 | protected void validateImplementation() throws IOException, 42 | GeneralSecurityException { 43 | int i = normalizedTargetUri.indexOf(QUERY_STRING); 44 | if (i < 10) { 45 | error("URI lacks a signature ( " + QUERY_STRING + " ) element"); 46 | } 47 | int next = normalizedTargetUri.indexOf('&', i); 48 | String jwsString; 49 | if (next < 0) { 50 | jwsString = normalizedTargetUri.substring(i + QUERY_LENGTH); 51 | normalizedTargetUri = normalizedTargetUri.substring(0, i - 1); 52 | } else { 53 | jwsString = normalizedTargetUri.substring(i + QUERY_LENGTH, next); 54 | normalizedTargetUri = 55 | normalizedTargetUri.substring(0, i) + normalizedTargetUri.substring(next + 1); 56 | } 57 | 58 | decodeJwsString(jwsString, false); 59 | secinf = commonDataFilter(JSONParser.parse(JWS_Payload), true); 60 | 61 | if (!ArrayUtil.compare(secinf.getBinary(SHREQSupport.SHREQ_HASHED_TARGET_URI), 62 | getDigest(normalizedTargetUri))) { 63 | error("URI mismatch. Normalized URI: " + normalizedTargetUri); 64 | } 65 | } 66 | 67 | @Override 68 | protected String defaultMethod() { 69 | return SHREQSupport.SHREQ_DEFAULT_URI_METHOD; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/HomeServlet.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.shreqb64; 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 | "
SHREQ/B64 - Signed HTTP Requests
" + 36 | "
This site permits testing and debugging systems utilizing a " + 37 | "scheme for signing HTTP requests tentatively targeted for " + 38 | "IETF standardization (here in a "tweaked" version using JWS+B64). For detailed technical information and " + 39 | "open source code, click on the SHREQ logotype.
" + 40 | "
" + 41 | "" + 46 | "" + 51 | "" + 56 | "
" + 44 | "Create Signed Request" + 45 | "
" + 49 | "Validate Signed Request" + 50 | "
" + 54 | "Online Testing with CURL/Browser" + 55 | "
" + 57 | "
Privacy/security notice: No user provided data is " + 58 | "ever stored or logged on the server; it only processes the data and returns the " + 59 | "result.
")); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/CurlServlet.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.shreqb64; 18 | 19 | import java.io.IOException; 20 | 21 | import javax.servlet.ServletException; 22 | 23 | import javax.servlet.http.HttpServletRequest; 24 | import javax.servlet.http.HttpServletResponse; 25 | 26 | public class CurlServlet extends BaseGuiServlet { 27 | 28 | private static final long serialVersionUID = 1L; 29 | 30 | public void doGet(HttpServletRequest request, HttpServletResponse response) 31 | throws IOException, ServletException { 32 | getSampleData(request); 33 | StringBuilder html = new StringBuilder( 34 | "
CURL/Browser Online Testing
") 35 | 36 | .append( 37 | HTML.fancyBox("urirequestbrowser", sampleUriRequestUri, 38 | "URI based GET request which can be directly accessed by a Browser")) 39 | 40 | .append( 41 | HTML.fancyBox("urirequest", "curl " + sampleUriRequestUri, 42 | "URI based GET request accessed through CURL")) 43 | 44 | .append( 45 | HTML.fancyBox("jsonrequest", "curl" + 46 | " -H content-type:application/jws" + 47 | " -d " + sampleJsonRequest_CURL + " " + 48 | sampleJsonRequestUri, 49 | "JSON based POST request accessed through CURL")) 50 | 51 | .append( 52 | HTML.fancyBox("jsonrequest", "curl" + 53 | " -X PUT" + 54 | " -H x-debug:full" + 55 | " -H content-type:application/jws" + 56 | " -d " + sampleJsonRequest_CURL_Header_PUT + " " + 57 | sampleUriRequestUri2BeSigned, 58 | "JSON based PUT request plus HTTP header variable accessed through CURL")) 59 | 60 | .append( 61 | "
CURL: " + 62 | "https://curl.haxx.se/
" + 63 | "
Note that these tests depend on the preconfigured keys " + 64 | "used by this Web application (one specific key for each signature algorithm). " + 65 | "You can create compatible requests using the create application.
"); 66 | 67 | HTML.standardPage(response, null, html); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /web/images/shreq.svg: -------------------------------------------------------------------------------- 1 | 2 | SHREQ Logotype 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /web/images/thelab.svg: -------------------------------------------------------------------------------- 1 | 2 | Lab Icon 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Bouncycastle to be loaded before other providers 9 | bouncycastle_first 10 | @bouncycastle-first@ 11 | 12 | 13 | 14 | Hash algorithm override 15 | hash_algorithm 16 | @hash-algorithm@ 17 | 18 | 19 | 20 | Logging flag 21 | logging 22 | @logging@ 23 | 24 | 25 | 26 | org.webpki.webapps.shreqb64.SHREQService 27 | 28 | 29 | 30 | HomeServlet 31 | org.webpki.webapps.shreqb64.HomeServlet 32 | 33 | 34 | 35 | CreateServlet 36 | org.webpki.webapps.shreqb64.CreateServlet 37 | 38 | 39 | 40 | ValidateServlet 41 | org.webpki.webapps.shreqb64.ValidateServlet 42 | 43 | 44 | 45 | CurlServlet 46 | org.webpki.webapps.shreqb64.CurlServlet 47 | 48 | 49 | 50 | ExtConfReqServlet 51 | org.webpki.webapps.shreqb64.ExtConfReqServlet 52 | 53 | 54 | 55 | ExtConfReq2Servlet 56 | org.webpki.webapps.shreqb64.ExtConfReq2Servlet 57 | 58 | 59 | 60 | PreConfReqServlet 61 | org.webpki.webapps.shreqb64.PreConfReqServlet 62 | 63 | 64 | 65 | PreConfReq2Servlet 66 | org.webpki.webapps.shreqb64.PreConfReq2Servlet 67 | 68 | 69 | 70 | HomeServlet 71 | /home 72 | 73 | 74 | 75 | CreateServlet 76 | /create 77 | 78 | 79 | 80 | ValidateServlet 81 | /validate 82 | 83 | 84 | 85 | CurlServlet 86 | /curl 87 | 88 | 89 | 90 | ExtConfReqServlet 91 | /extconfreq 92 | 93 | 94 | 95 | ExtConfReqServlet 96 | /extconfreq/* 97 | 98 | 99 | 100 | ExtConfReq2Servlet 101 | /extconfreq2 102 | 103 | 104 | 105 | ExtConfReq2Servlet 106 | /extconfreq2/* 107 | 108 | 109 | 110 | PreConfReqServlet 111 | /preconfreq 112 | 113 | 114 | 115 | PreConfReqServlet 116 | /preconfreq/* 117 | 118 | 119 | 120 | PreConfReq2Servlet 121 | /preconfreq2 122 | 123 | 124 | 125 | PreConfReq2Servlet 126 | /preconfreq2/* 127 | 128 | 129 | 130 | 131 | The app 132 | /* 133 | 134 | 135 | CONFIDENTIAL 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/JSONTokenExtractor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2006-2018 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.shreqb64; 18 | 19 | import java.io.IOException; 20 | 21 | import java.util.Vector; 22 | 23 | /** 24 | * Parses JSON string/byte array data. 25 | */ 26 | public class JSONTokenExtractor { 27 | 28 | static final char LEFT_CURLY_BRACKET = '{'; 29 | static final char RIGHT_CURLY_BRACKET = '}'; 30 | static final char DOUBLE_QUOTE = '"'; 31 | static final char COLON_CHARACTER = ':'; 32 | static final char LEFT_BRACKET = '['; 33 | static final char RIGHT_BRACKET = ']'; 34 | static final char COMMA_CHARACTER = ','; 35 | static final char BACK_SLASH = '\\'; 36 | 37 | int index; 38 | 39 | int maxLength; 40 | 41 | String jsonData; 42 | 43 | Vector tokens; 44 | 45 | JSONTokenExtractor() { 46 | tokens = new Vector(); 47 | } 48 | 49 | Vector getTokens(String jsonString) throws IOException { 50 | jsonData = jsonString; 51 | maxLength = jsonData.length(); 52 | scanFor(LEFT_CURLY_BRACKET); 53 | scanObject(); 54 | return tokens; 55 | } 56 | 57 | 58 | void scanElement() throws IOException { 59 | switch (scan()) { 60 | case LEFT_CURLY_BRACKET: 61 | scanObject(); 62 | break; 63 | 64 | case DOUBLE_QUOTE: 65 | scanQuotedString(); 66 | break; 67 | 68 | case LEFT_BRACKET: 69 | scanArray(); 70 | break; 71 | 72 | default: 73 | scanSimpleType(); 74 | } 75 | } 76 | 77 | void scanObject() throws IOException { 78 | boolean next = false; 79 | while (testNextNonWhiteSpaceChar() != RIGHT_CURLY_BRACKET) { 80 | if (next) { 81 | scanFor(COMMA_CHARACTER); 82 | } 83 | next = true; 84 | scanFor(DOUBLE_QUOTE); 85 | scanQuotedString(); 86 | scanFor(COLON_CHARACTER); 87 | scanElement(); 88 | } 89 | scan(); 90 | } 91 | 92 | void scanArray() throws IOException { 93 | boolean next = false; 94 | while (testNextNonWhiteSpaceChar() != RIGHT_BRACKET) { 95 | if (next) { 96 | scanFor(COMMA_CHARACTER); 97 | } else { 98 | next = true; 99 | } 100 | scanElement(); 101 | } 102 | scan(); 103 | } 104 | 105 | void scanSimpleType() throws IOException { 106 | index--; 107 | StringBuilder tempBuffer = new StringBuilder(); 108 | char c; 109 | while ((c = testNextNonWhiteSpaceChar()) != COMMA_CHARACTER && c != RIGHT_BRACKET && c != RIGHT_CURLY_BRACKET) { 110 | if (isWhiteSpace(c = nextChar())) { 111 | break; 112 | } 113 | tempBuffer.append(c); 114 | } 115 | tokens.add(tempBuffer.toString()); 116 | } 117 | 118 | void scanQuotedString() throws IOException { 119 | StringBuilder result = new StringBuilder(); 120 | while (true) { 121 | char c = nextChar(); 122 | if (c == DOUBLE_QUOTE) { 123 | break; 124 | } 125 | if (c == BACK_SLASH) { 126 | result.append(BACK_SLASH); 127 | result.append(nextChar()); 128 | } else { 129 | switch (c) { 130 | case '&': 131 | result.append("&"); 132 | break; 133 | case '>': 134 | result.append(">"); 135 | break; 136 | case '<': 137 | result.append("<"); 138 | break; 139 | default: 140 | result.append(c); 141 | } 142 | } 143 | } 144 | tokens.add(result.toString()); 145 | } 146 | 147 | char testNextNonWhiteSpaceChar() throws IOException { 148 | int save = index; 149 | char c = scan(); 150 | index = save; 151 | return c; 152 | } 153 | 154 | void scanFor(char expected) throws IOException { 155 | char c = scan(); 156 | if (c != expected) { 157 | throw new IOException("Expected '" + expected + "' but got '" + c + "'"); 158 | } 159 | } 160 | 161 | char nextChar() throws IOException { 162 | if (index < maxLength) { 163 | return jsonData.charAt(index++); 164 | } 165 | throw new IOException("Unexpected EOF reached"); 166 | } 167 | 168 | boolean isWhiteSpace(char c) { 169 | return c == 0x20 || c == 0x0A || c == 0x0D || c == 0x09; 170 | } 171 | 172 | char scan() throws IOException { 173 | while (true) { 174 | char c = nextChar(); 175 | if (isWhiteSpace(c)) { 176 | continue; 177 | } 178 | return c; 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/HTML.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.shreqb64; 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 | "SHREQ Lab" + 36 | ""; 37 | 38 | static String encode(String val) { 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 '<': 47 | buf.append("<"); 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 | default: 62 | buf.append(c); 63 | break; 64 | } 65 | } 66 | return buf.toString(); 67 | } else { 68 | return new String(""); 69 | } 70 | } 71 | 72 | static String getHTML(String javascript, String box) { 73 | StringBuilder html = new StringBuilder(HTML_INIT); 74 | if (javascript != null) { 75 | html.append(""); 77 | } 78 | html.append("" + 79 | "
" + 80 | "" + 83 | "
" + 84 | "" + 87 | "
" + 88 | "
") 89 | .append(box).append(""); 90 | return html.toString(); 91 | } 92 | 93 | static void output(HttpServletResponse response, String html) 94 | throws IOException, ServletException { 95 | if (SHREQService.logging) { 96 | logger.info(html); 97 | } 98 | response.setContentType("text/html; charset=utf-8"); 99 | response.setHeader("Pragma", "No-Cache"); 100 | response.setDateHeader("EXPIRES", 0); 101 | response.getOutputStream().write(html.getBytes("utf-8")); 102 | } 103 | 104 | static String getConditionalParameter(HttpServletRequest request, 105 | String name) { 106 | String value = request.getParameter(name); 107 | if (value == null) { 108 | return ""; 109 | } 110 | return value; 111 | } 112 | 113 | public static String boxHeader(String id, String text, boolean visible) { 114 | return new StringBuilder("
" + 119 | "
" + text + ":
").toString(); 120 | } 121 | 122 | public static String fancyBox(String id, String content, String header) { 123 | return boxHeader(id, header, true) + 124 | "
" + content + "
"; 125 | } 126 | 127 | public static String fancyText(boolean visible, 128 | String id, 129 | int rows, 130 | String content, 131 | String header) { 132 | return boxHeader(id, header, visible) + 133 | "" + 136 | content + 137 | ""; 138 | } 139 | 140 | static void standardPage(HttpServletResponse response, 141 | String javaScript, 142 | StringBuilder html) throws IOException, ServletException { 143 | HTML.output(response, HTML.getHTML(javaScript, html.toString())); 144 | } 145 | 146 | static String javaScript(String string) { 147 | StringBuilder html = new StringBuilder(); 148 | for (char c : string.toCharArray()) { 149 | if (c == '\n') { 150 | html.append("\\n"); 151 | } else { 152 | html.append(c); 153 | } 154 | } 155 | return html.toString(); 156 | } 157 | 158 | public static void errorPage(HttpServletResponse response, Exception e) 159 | throws IOException, ServletException { 160 | standardPage(response, 161 | null, 162 | new StringBuilder( 163 | "
Something went wrong...
" + 164 | "
")
165 |         .append(encode(BaseRequestServlet.getStackTrace(e)))
166 |         .append("
")); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/SHREQService.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.shreqb64; 18 | 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.security.GeneralSecurityException; 22 | import java.security.KeyPair; 23 | import java.security.KeyStore; 24 | import java.util.LinkedHashMap; 25 | import java.util.logging.Level; 26 | import java.util.logging.Logger; 27 | 28 | import javax.servlet.ServletContextEvent; 29 | import javax.servlet.ServletContextListener; 30 | 31 | import org.webpki.crypto.AlgorithmPreferences; 32 | import org.webpki.crypto.AsymSignatureAlgorithms; 33 | import org.webpki.crypto.CustomCryptoProvider; 34 | import org.webpki.crypto.KeyStoreVerifier; 35 | import org.webpki.crypto.MACAlgorithms; 36 | import org.webpki.crypto.SignatureAlgorithms; 37 | import org.webpki.shreqb64.SHREQSupport; 38 | import org.webpki.util.ArrayUtil; 39 | import org.webpki.util.HexaDecimal; 40 | import org.webpki.util.PEMDecoder; 41 | import org.webpki.webutil.InitPropertyReader; 42 | 43 | public class SHREQService extends InitPropertyReader implements ServletContextListener { 44 | 45 | static Logger logger = Logger.getLogger(SHREQService.class.getName()); 46 | 47 | static String sampleKey; 48 | 49 | static String keyDeclarations; 50 | 51 | static KeyStoreVerifier certificateVerifier; 52 | 53 | static boolean logging; 54 | 55 | static LinkedHashMap predefinedSecretKeys = new LinkedHashMap(); 56 | 57 | static LinkedHashMap predefinedKeyPairs = new LinkedHashMap(); 58 | 59 | static final String BOUNCYCASTLE = "bouncycastle_first"; 60 | 61 | class KeyDeclaration { 62 | 63 | static final String PRIVATE_KEYS = "privateKeys"; 64 | static final String SECRET_KEYS = "secretKeys"; 65 | static final String CERTIFICATES = "certificates"; 66 | 67 | StringBuilder decl = new StringBuilder("var "); 68 | StringBuilder after = new StringBuilder(); 69 | String name; 70 | String last; 71 | String base; 72 | 73 | KeyDeclaration(String name, String base) { 74 | this.name = name; 75 | this.base = base; 76 | decl.append(name) 77 | .append(" = {"); 78 | } 79 | 80 | KeyDeclaration addKey(SignatureAlgorithms alg, String fileOrNull) throws IOException, 81 | GeneralSecurityException { 82 | String algId = alg.getAlgorithmId(AlgorithmPreferences.JOSE); 83 | if (name.equals(PRIVATE_KEYS)) { 84 | if (fileOrNull == null) { 85 | predefinedKeyPairs.put(algId, predefinedKeyPairs.get(last)); 86 | } else { 87 | predefinedKeyPairs.put(algId, 88 | PEMDecoder.getKeyPair(getEmbeddedResourceBinary(fileOrNull + base))); 89 | } 90 | } else if (name.equals(SECRET_KEYS)) { 91 | predefinedSecretKeys.put(algId, 92 | HexaDecimal.decode(getEmbeddedResourceString(fileOrNull + base).trim())); 93 | } 94 | if (fileOrNull == null) { 95 | after.append(name) 96 | .append('.') 97 | .append(algId) 98 | .append(" = ") 99 | .append(name) 100 | .append('.') 101 | .append(last) 102 | .append(";\n"); 103 | 104 | } else { 105 | if (last != null) { 106 | decl.append(','); 107 | } 108 | decl.append("\n ") 109 | .append(algId) 110 | .append(": '") 111 | .append(HTML.javaScript(getEmbeddedResourceString(fileOrNull + base).trim())) 112 | .append('\''); 113 | last = algId; 114 | } 115 | return this; 116 | } 117 | 118 | public String toString() { 119 | return decl.append("\n};\n").append(after).toString(); 120 | } 121 | } 122 | 123 | InputStream getResource(String name) throws IOException { 124 | InputStream is = this.getClass().getResourceAsStream(name); 125 | if (is == null) { 126 | throw new IOException("Resource fail for: " + name); 127 | } 128 | return is; 129 | } 130 | 131 | byte[] getEmbeddedResourceBinary(String name) throws IOException { 132 | return ArrayUtil.getByteArrayFromInputStream(getResource(name)); 133 | } 134 | 135 | String getEmbeddedResourceString(String name) throws IOException { 136 | return new String(getEmbeddedResourceBinary(name), "utf-8"); 137 | } 138 | 139 | @Override 140 | public void contextDestroyed(ServletContextEvent event) { 141 | } 142 | 143 | @Override 144 | public void contextInitialized(ServletContextEvent event) { 145 | initProperties(event); 146 | try { 147 | ///////////////////////////////////////////////////////////////////////////////////////////// 148 | // Sample key for verification 149 | ///////////////////////////////////////////////////////////////////////////////////////////// 150 | sampleKey = getEmbeddedResourceString("p256publickey.pem").trim(); 151 | 152 | ///////////////////////////////////////////////////////////////////////////////////////////// 153 | // Optionally load Bouncycastle 154 | ///////////////////////////////////////////////////////////////////////////////////////////// 155 | if (!getPropertyString(BOUNCYCASTLE).isEmpty()) { 156 | CustomCryptoProvider.forcedLoad(true); 157 | } 158 | 159 | ///////////////////////////////////////////////////////////////////////////////////////////// 160 | // Keys 161 | ///////////////////////////////////////////////////////////////////////////////////////////// 162 | keyDeclarations = 163 | new KeyDeclaration(KeyDeclaration.PRIVATE_KEYS, "privatekey.pem") 164 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA256, "p256") 165 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA384, "p384") 166 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA512, "p521") 167 | .addKey(AsymSignatureAlgorithms.RSA_SHA256, "r2048") 168 | .addKey(AsymSignatureAlgorithms.RSA_SHA384, null) 169 | .addKey(AsymSignatureAlgorithms.RSA_SHA512, null).toString() + 170 | new KeyDeclaration(KeyDeclaration.CERTIFICATES, "certpath.pem") 171 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA256, "p256") 172 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA384, "p384") 173 | .addKey(AsymSignatureAlgorithms.ECDSA_SHA512, "p521") 174 | .addKey(AsymSignatureAlgorithms.RSA_SHA256, "r2048") 175 | .addKey(AsymSignatureAlgorithms.RSA_SHA384, null) 176 | .addKey(AsymSignatureAlgorithms.RSA_SHA512, null).toString() + 177 | new KeyDeclaration(KeyDeclaration.SECRET_KEYS, "bitkey.hex") 178 | .addKey(MACAlgorithms.HMAC_SHA256, "a256") 179 | .addKey(MACAlgorithms.HMAC_SHA384, "a384") 180 | .addKey(MACAlgorithms.HMAC_SHA512, "a512").toString(); 181 | 182 | KeyStore keyStore = KeyStore.getInstance("PKCS12"); 183 | keyStore.load(null, null); 184 | keyStore.setCertificateEntry( 185 | "mykey", 186 | PEMDecoder.getRootCertificate(getEmbeddedResourceBinary("rootca.pem"))); 187 | certificateVerifier = new KeyStoreVerifier(keyStore); 188 | 189 | ///////////////////////////////////////////////////////////////////////////////////////////// 190 | // Hash algorithm override? 191 | ///////////////////////////////////////////////////////////////////////////////////////////// 192 | String algorithmId = getPropertyString("hash_algorithm"); 193 | if (algorithmId.length() > 0) { 194 | SHREQSupport.getHashAlgorithm(algorithmId); 195 | SHREQSupport.overridedHashAlgorithm = algorithmId; 196 | logger.info("Hash OVERRIDE MODE"); 197 | } 198 | 199 | ///////////////////////////////////////////////////////////////////////////////////////////// 200 | // Logging? 201 | ///////////////////////////////////////////////////////////////////////////////////////////// 202 | logging = getPropertyBoolean("logging"); 203 | 204 | logger.info("SHREQ/B64 Demo Successfully Initiated"); 205 | } catch (Exception e) { 206 | logger.log(Level.SEVERE, "********\n" + e.getMessage() + "\n********", e); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/BaseRequestServlet.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.shreqb64; 18 | 19 | import java.io.IOException; 20 | 21 | import java.security.GeneralSecurityException; 22 | import java.security.PublicKey; 23 | 24 | import java.text.SimpleDateFormat; 25 | import java.util.Enumeration; 26 | import java.util.GregorianCalendar; 27 | import java.util.LinkedHashMap; 28 | import java.util.TimeZone; 29 | 30 | import java.util.logging.Logger; 31 | 32 | import javax.servlet.ServletException; 33 | import javax.servlet.http.HttpServlet; 34 | import javax.servlet.http.HttpServletRequest; 35 | import javax.servlet.http.HttpServletResponse; 36 | 37 | import org.webpki.crypto.AlgorithmPreferences; 38 | import org.webpki.crypto.AsymSignatureAlgorithms; 39 | import org.webpki.crypto.MACAlgorithms; 40 | import org.webpki.crypto.SignatureAlgorithms; 41 | 42 | import org.webpki.jose.JOSEAsymSignatureValidator; 43 | import org.webpki.jose.JOSEHmacValidator; 44 | import org.webpki.jose.JOSESupport; 45 | 46 | import org.webpki.shreqb64.JSONRequestValidation; 47 | import org.webpki.shreqb64.URIRequestValidation; 48 | import org.webpki.shreqb64.ValidationCore; 49 | import org.webpki.shreqb64.ValidationKeyService; 50 | 51 | import org.webpki.util.HexaDecimal; 52 | 53 | import org.webpki.webutil.ServletUtil; 54 | 55 | public abstract class BaseRequestServlet extends HttpServlet implements ValidationKeyService { 56 | 57 | private static final long serialVersionUID = 1L; 58 | 59 | private static final String CONTENT_TYPE = "Content-Type"; 60 | private static final String CONTENT_LENGTH = "Content-Length"; 61 | 62 | private static final String JWS_CONTENT = "application/jws"; 63 | 64 | static final String EXTCONFREQ = "/extconfreq"; 65 | static final String PRECONFREQ = "/preconfreq"; 66 | static final String EXTCONFREQ2 = "/extconfreq2"; 67 | static final String PRECONFREQ2 = "/preconfreq2"; 68 | 69 | static final int TIME_STAMP_TOLERANCE = 300000; // Milliseconds 70 | 71 | static Logger logger = Logger.getLogger(BaseRequestServlet.class.getName()); 72 | 73 | protected abstract boolean externallyConfigured(); 74 | 75 | static String getFormattedUTCTime(GregorianCalendar dateTime) { 76 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss 'UTC'"); 77 | sdf.setTimeZone(TimeZone.getTimeZone("UTC")); 78 | return sdf.format(dateTime.getTime()); 79 | } 80 | static String getStackTrace(Exception e) { 81 | StringBuffer error = new StringBuffer("Stack trace:\n") 82 | .append(e.getClass().getName()) 83 | .append(": ") 84 | .append(e.getMessage()); 85 | StackTraceElement[] st = e.getStackTrace(); 86 | int length = st.length; 87 | if (length > 20) { 88 | length = 20; 89 | } 90 | for (int i = 0; i < length; i++) { 91 | String entry = st[i].toString(); 92 | error.append("\n at " + entry); 93 | if (entry.contains("HttpServlet")) { 94 | break; 95 | } 96 | } 97 | return error.toString(); 98 | } 99 | 100 | static String getUrlFromRequest(HttpServletRequest request) { 101 | return request.getScheme() + 102 | "://" + 103 | request.getServerName() + 104 | ":" + 105 | request.getServerPort() + 106 | request.getRequestURI() + 107 | (request.getQueryString() == null ? 108 | "" : "?" + request.getQueryString()); 109 | } 110 | 111 | protected boolean enforceTimeStamp() { 112 | return false; 113 | } 114 | 115 | private void returnResponse(HttpServletResponse response, int status, String text) throws IOException { 116 | response.resetBuffer(); 117 | response.setStatus(status); 118 | response.getOutputStream().write(text.getBytes("utf-8"));; 119 | response.setHeader(CONTENT_TYPE, "text/plain;utf-8"); 120 | response.flushBuffer(); 121 | } 122 | 123 | @Override 124 | public void service(HttpServletRequest request, HttpServletResponse response) 125 | throws IOException, ServletException { 126 | ValidationCore validationCore = null; 127 | 128 | // Get the Target Method (4.2:1 , 5.2:1) 129 | String targetMethod = request.getMethod(); 130 | 131 | // Recreate the Target URI (4.2:2 , 5.2:2) 132 | String targetUri = getUrlFromRequest(request); 133 | 134 | // Collect HTTP Headers 135 | @SuppressWarnings("unchecked") 136 | Enumeration headerNames = request.getHeaderNames(); 137 | 138 | try { 139 | LinkedHashMap headerMap = new LinkedHashMap(); 140 | while (headerNames.hasMoreElements()) { 141 | String headerName = headerNames.nextElement().toLowerCase(); 142 | @SuppressWarnings("unchecked") 143 | Enumeration headerValues = request.getHeaders(headerName); 144 | boolean next = false; 145 | do { 146 | String headerValue = headerValues.nextElement().trim(); 147 | if (next) { 148 | headerMap.put(headerName, headerMap.get(headerName) + ", " + headerValue); 149 | } else { 150 | headerMap.put(headerName, headerValue); 151 | next = true; 152 | } 153 | } while (headerValues.hasMoreElements()); 154 | } 155 | 156 | // 3. Determining Request Type 157 | if (request.getHeader(CONTENT_LENGTH) == null) { 158 | 159 | // 5.2 URI Request 160 | if (request.getHeader(CONTENT_TYPE) != null) { 161 | throw new IOException("Unexpected: " + CONTENT_TYPE); 162 | } 163 | validationCore = new URIRequestValidation(targetUri, 164 | targetMethod, 165 | headerMap); 166 | } else { 167 | 168 | // 4.2 JSON Request 169 | if (!JWS_CONTENT.equals(request.getHeader(CONTENT_TYPE))) { 170 | throw new IOException(CONTENT_TYPE + 171 | "=" + 172 | request.getHeader(CONTENT_TYPE) + 173 | " must be=" + 174 | JWS_CONTENT); 175 | } 176 | validationCore = new JSONRequestValidation(targetUri, 177 | targetMethod, 178 | headerMap, 179 | new String(ServletUtil.getData(request), "utf-8")); 180 | } 181 | 182 | // Core Request Data Successfully Collected - Validate! 183 | validationCore.validate(this); 184 | 185 | // In *this* service we don't accept any unread/unused JWS header variables 186 | validationCore.getJwsProtectedHeader().checkForUnread(); 187 | 188 | // Optional test 189 | if (enforceTimeStamp()) { 190 | validationCore.enforceTimeStamp(TIME_STAMP_TOLERANCE, TIME_STAMP_TOLERANCE); 191 | } 192 | 193 | // No exceptions => We did it! 194 | returnResponse(response, HttpServletResponse.SC_OK, 195 | "\n" + 196 | " |============================================|\n" + 197 | " | SUCCESSFUL REQUEST " + 198 | getFormattedUTCTime(new GregorianCalendar()) + " |\n" + 199 | " |============================================|\n" + 200 | validationCore.printCoreData()); 201 | 202 | 203 | } catch (Exception e) { 204 | 205 | // Houston, we got a problem... 206 | returnResponse(response, HttpServletResponse.SC_BAD_REQUEST, 207 | "\n" + 208 | " *************\n" + 209 | " * E R R O R *\n" + 210 | " *************\n" + 211 | getStackTrace(e) + (validationCore == null ? 212 | "\nValidation context not available\n" : validationCore.printCoreData())); 213 | } 214 | } 215 | 216 | void extConfError() throws IOException { 217 | throw new IOException("'" + 218 | EXTCONFREQ + 219 | "' only supports requests with in-lined asymmetric JWKs and X5Cs"); 220 | } 221 | 222 | @Override 223 | public JOSESupport.CoreSignatureValidator getSignatureValidator(ValidationCore validationCore, 224 | SignatureAlgorithms signatureAlgorithm, 225 | PublicKey publicKey, 226 | String keyId) 227 | throws IOException, GeneralSecurityException { 228 | if (signatureAlgorithm.isSymmetric()) { 229 | if (externallyConfigured()) { 230 | extConfError(); 231 | } 232 | byte[] secretKey = SHREQService.predefinedSecretKeys 233 | .get(signatureAlgorithm.getAlgorithmId(AlgorithmPreferences.JOSE)); 234 | validationCore.setCookie(HexaDecimal.encode(secretKey)); 235 | return new JOSEHmacValidator(secretKey, 236 | (MACAlgorithms) signatureAlgorithm); 237 | } 238 | PublicKey validationKey; 239 | if (externallyConfigured()) { 240 | if (publicKey == null) { 241 | extConfError(); 242 | } 243 | validationKey = publicKey; 244 | } else { 245 | // Lookup predefined validation key 246 | validationKey = SHREQService.predefinedKeyPairs 247 | .get(signatureAlgorithm.getAlgorithmId(AlgorithmPreferences.JOSE)).getPublic(); 248 | if (publicKey != null && !publicKey.equals(validationKey)) { 249 | throw new GeneralSecurityException("In-lined public key differs from predefined public key"); 250 | } 251 | if (validationCore.getCertificatePath() != null) { 252 | SHREQService.certificateVerifier 253 | .verifyCertificatePath(validationCore.getCertificatePath()); 254 | } 255 | } 256 | validationCore.setCookie(BaseGuiServlet.getPEMFromPublicKey(validationKey)); 257 | return new JOSEAsymSignatureValidator(validationKey, 258 | (AsymSignatureAlgorithms)signatureAlgorithm); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /shreq/org/webpki/shreqb64/SHREQSupport.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.shreqb64; 18 | 19 | import java.io.IOException; 20 | 21 | import java.security.GeneralSecurityException; 22 | 23 | import java.util.GregorianCalendar; 24 | import java.util.LinkedHashMap; 25 | 26 | import java.util.regex.Pattern; 27 | 28 | import org.webpki.crypto.HashAlgorithms; 29 | import org.webpki.crypto.SignatureAlgorithms; 30 | 31 | import org.webpki.json.JSONObjectWriter; 32 | 33 | public class SHREQSupport { 34 | 35 | private SHREQSupport() {} 36 | 37 | public static final String SHREQ_SECINF_LABEL = ".secinf"; // For JSON based requests only 38 | public static final String SHREQ_JWS_QUERY_LABEL = ".jws"; // For URI based requests only 39 | 40 | public static final String SHREQ_TARGET_URI = "uri"; // For JSON based requests only 41 | public static final String SHREQ_HASHED_TARGET_URI = "htu"; // For URI based requests only 42 | public static final String SHREQ_HTTP_METHOD = "mtd"; 43 | public static final String SHREQ_ISSUED_AT_TIME = "iat"; 44 | public static final String SHREQ_HEADER_RECORD = "hdr"; 45 | public static final String SHREQ_HASH_ALG_OVERRIDE = "hao"; 46 | public static final String SHREQ_JWS_STRING = "jws"; // For JSON based requests only 47 | 48 | public static final String SHREQ_DEFAULT_JSON_METHOD = "POST"; 49 | public static final String SHREQ_DEFAULT_URI_METHOD = "GET"; 50 | 51 | public static final String[] HTTP_METHODS = {"GET", 52 | "POST", 53 | "PUT", 54 | "DELETE", 55 | "PATCH", 56 | "HEAD", 57 | "CONNECT"}; 58 | 59 | static final boolean[] RESERVED = new boolean[128]; 60 | 61 | static { 62 | for (int q = 0; q < 128; q++) { 63 | RESERVED[q] = (q < '0' || q > '9') && 64 | (q < 'A' || q > 'Z') && 65 | (q < 'a' || q > 'z') && 66 | q != '-' && 67 | q != '.' && 68 | q != '~' && 69 | q != '_'; 70 | } 71 | } 72 | 73 | static final LinkedHashMap hashAlgorithms = 74 | new LinkedHashMap(); 75 | static { 76 | hashAlgorithms.put("S256", HashAlgorithms.SHA256); 77 | hashAlgorithms.put("S384", HashAlgorithms.SHA384); 78 | hashAlgorithms.put("S512", HashAlgorithms.SHA512); 79 | } 80 | 81 | private static final String HEADER_SYNTAX = "[a-z0-9\\-\\$_\\.]"; 82 | 83 | static final Pattern HEADER_STRING_ARRAY_SYNTAX = 84 | Pattern.compile(HEADER_SYNTAX + "+(," + HEADER_SYNTAX + "+)*"); 85 | 86 | public static HashAlgorithms getHashAlgorithm(String algorithmId) throws GeneralSecurityException { 87 | HashAlgorithms algorithm = hashAlgorithms.get(algorithmId); 88 | if (algorithm == null) { 89 | throw new GeneralSecurityException("Unknown hash algorithm: " + algorithmId); 90 | } 91 | return algorithm; 92 | } 93 | 94 | public static String overridedHashAlgorithm; // Ugly system wide setting 95 | 96 | public static boolean useDefaultForMethod; // Ugly system wide setting 97 | 98 | private static byte[] digest(SignatureAlgorithms defaultAlgorithmSource, String data) 99 | throws IOException, GeneralSecurityException { 100 | return (overridedHashAlgorithm == null ? 101 | defaultAlgorithmSource.getDigestAlgorithm() 102 | : 103 | getHashAlgorithm(overridedHashAlgorithm)) 104 | .digest(data.getBytes("utf-8")); 105 | } 106 | 107 | private static JSONObjectWriter setHeader(JSONObjectWriter wr, 108 | LinkedHashMap httpHeaderData, 109 | SignatureAlgorithms signatureAlgorithm, 110 | boolean required) 111 | throws IOException, GeneralSecurityException { 112 | boolean headerFlag = httpHeaderData != null && !httpHeaderData.isEmpty(); 113 | if ((headerFlag || required) && overridedHashAlgorithm != null) { 114 | wr.setString(SHREQ_HASH_ALG_OVERRIDE, overridedHashAlgorithm); 115 | } 116 | if (headerFlag) { 117 | StringBuilder headerBlob = new StringBuilder(); 118 | StringBuilder headerList = new StringBuilder(); 119 | boolean next = false; 120 | for (String header : httpHeaderData.keySet()) { 121 | if (next) { 122 | headerBlob.append('\n'); 123 | headerList.append(','); 124 | } 125 | next = true; 126 | headerList.append(header); 127 | headerBlob.append(header) 128 | .append(':') 129 | .append(normalizeHeaderArgument(httpHeaderData.get(header))); 130 | } 131 | wr.setArray(SHREQ_HEADER_RECORD) 132 | .setBinary(digest(signatureAlgorithm, headerBlob.toString())) 133 | .setString(headerList.toString()); 134 | } 135 | return wr; 136 | } 137 | 138 | public static JSONObjectWriter createJSONRequestSecInf(String targetUri, 139 | String targetMethod, 140 | GregorianCalendar issuetAt, 141 | LinkedHashMap httpHeaderData, 142 | SignatureAlgorithms signatureAlgorithm) 143 | throws IOException, GeneralSecurityException { 144 | JSONObjectWriter secinf = new JSONObjectWriter() 145 | .setString(SHREQ_TARGET_URI, normalizeTargetURI(targetUri)) 146 | 147 | // If the method is "POST" this element MAY be skipped 148 | .setDynamic((wr) -> targetMethod == null || 149 | (useDefaultForMethod && targetMethod.equals(SHREQ_DEFAULT_JSON_METHOD)) ? 150 | wr : wr.setString(SHREQ_HTTP_METHOD, targetMethod)) 151 | 152 | // If "message" already has a "DateTime" object this element MAY be skipped 153 | .setDynamic((wr) -> issuetAt == null ? 154 | wr : wr.setInt53(SHREQ_ISSUED_AT_TIME, issuetAt.getTimeInMillis() / 1000)); 155 | 156 | // Optional HTTP headers 157 | return setHeader(secinf, httpHeaderData, signatureAlgorithm, false); 158 | } 159 | 160 | public static JSONObjectWriter createURIRequestSecInf(String targetUri, 161 | String targetMethod, 162 | GregorianCalendar issuetAt, 163 | LinkedHashMap httpHeaderData, 164 | SignatureAlgorithms signatureAlgorithm) 165 | throws IOException, GeneralSecurityException { 166 | JSONObjectWriter secinf = new JSONObjectWriter() 167 | .setBinary(SHREQ_HASHED_TARGET_URI, 168 | getDigestedURI(normalizeTargetURI(targetUri), signatureAlgorithm)) 169 | 170 | // If the method is "GET" this element MAY be skipped 171 | .setDynamic((wr) -> targetMethod == null || 172 | (useDefaultForMethod && targetMethod.equals(SHREQ_DEFAULT_URI_METHOD)) ? 173 | wr : wr.setString(SHREQ_HTTP_METHOD, targetMethod)) 174 | 175 | // This element MAY be skipped 176 | .setDynamic((wr) -> issuetAt == null ? 177 | wr : wr.setInt53(SHREQ_ISSUED_AT_TIME, issuetAt.getTimeInMillis() / 1000)); 178 | 179 | // Optional headers 180 | return setHeader(secinf, httpHeaderData, signatureAlgorithm, true); 181 | } 182 | 183 | static final char[] BIG_HEX = {'0', '1', '2', '3', '4', '5', '6', '7', 184 | '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; 185 | 186 | static void addEscape(StringBuilder escaped, byte b) { 187 | escaped.append('%') 188 | .append(BIG_HEX[(b & 0xf0) >> 4]) 189 | .append(BIG_HEX[b & 0xf]); 190 | } 191 | 192 | static int getEscape(byte[] utf8, int index) throws IOException { 193 | if (index >= utf8.length) { 194 | throw new IOException("Malformed URI escape"); 195 | } 196 | byte b = utf8[index]; 197 | if (b >= 'a' && b <= 'f') { 198 | return b - ('a' - 10); 199 | } 200 | if (b >= '0' && b <= '9') { 201 | return b - '0'; 202 | } 203 | if (b >= 'A' && b <= 'F') { 204 | return b - ('A' - 10); 205 | } 206 | throw new IOException("Malformed URI escape"); 207 | } 208 | 209 | public static String utf8EscapeUri(String uri) throws IOException { 210 | StringBuilder escaped = new StringBuilder(); 211 | byte[] utf8 = uri.getBytes("utf-8"); 212 | int q = 0; 213 | while (q < utf8.length) { 214 | byte b = utf8[q++]; 215 | if (b == '%') { 216 | b = (byte)((getEscape(utf8, q++) << 4) + getEscape(utf8, q++)); 217 | if (b > 0 && RESERVED[b]) { 218 | addEscape(escaped, b); 219 | continue; 220 | } 221 | } 222 | if (b < 0) { 223 | addEscape(escaped, b); 224 | } else { 225 | escaped.append((char)b); 226 | } 227 | } 228 | return escaped.toString(); 229 | } 230 | 231 | public static String normalizeTargetURI(String uri) throws IOException { 232 | // Incomplete...but still useful in most cases 233 | if (uri.startsWith("https:")) { 234 | uri = uri.replace(":443/", "/"); 235 | } else { 236 | uri = uri.replace(":80/", "/"); 237 | } 238 | return utf8EscapeUri(uri); 239 | } 240 | 241 | public static String addJwsToTargetUri(String targetUri, String jwsString) { 242 | return targetUri + (targetUri.contains("?") ? 243 | '&' : '?') + SHREQSupport.SHREQ_JWS_QUERY_LABEL + "=" + jwsString; 244 | } 245 | 246 | static byte[] getDigestedURI(String alreadyNormalizedUri, 247 | SignatureAlgorithms signatureAlgorithm) 248 | throws IOException, GeneralSecurityException { 249 | return digest(signatureAlgorithm, alreadyNormalizedUri); 250 | } 251 | 252 | static String normalizeHeaderArgument(String argument) { 253 | return argument.trim(); 254 | } 255 | 256 | } 257 | -------------------------------------------------------------------------------- /shreq/org/webpki/shreqb64/ValidationCore.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.shreqb64; 18 | 19 | import java.io.IOException; 20 | 21 | import java.security.GeneralSecurityException; 22 | import java.security.PublicKey; 23 | 24 | import java.security.cert.X509Certificate; 25 | 26 | import java.util.GregorianCalendar; 27 | import java.util.HashSet; 28 | import java.util.LinkedHashMap; 29 | 30 | import java.util.logging.Logger; 31 | 32 | import org.webpki.crypto.HashAlgorithms; 33 | import org.webpki.crypto.SignatureAlgorithms; 34 | 35 | import org.webpki.jose.JOSESupport; 36 | 37 | import org.webpki.json.JSONArrayReader; 38 | import org.webpki.json.JSONObjectReader; 39 | import org.webpki.json.JSONParser; 40 | 41 | import org.webpki.util.ArrayUtil; 42 | import org.webpki.util.Base64URL; 43 | 44 | public abstract class ValidationCore { 45 | 46 | protected LinkedHashMap headerMap; 47 | 48 | protected String jwsProtectedHeaderB64U; 49 | 50 | protected JSONObjectReader JWS_Protected_Header; 51 | 52 | protected byte[] JWS_Payload; 53 | 54 | protected byte[] JWS_Signature; 55 | 56 | private boolean validationMode; 57 | 58 | private ValidationKeyService validationKeyService; 59 | 60 | protected String normalizedTargetUri; 61 | 62 | protected String targetMethod; 63 | 64 | protected GregorianCalendar issuedAt; 65 | 66 | protected SignatureAlgorithms signatureAlgorithm; 67 | 68 | protected HashAlgorithms hashAlgorithm; // For digests 69 | 70 | protected String keyId; 71 | 72 | protected PublicKey publicKey; 73 | 74 | protected X509Certificate[] certificatePath; 75 | 76 | JSONObjectReader secinf; 77 | 78 | private Object cookie; 79 | 80 | protected ValidationCore(String targetUri, 81 | String targetMethod, 82 | LinkedHashMap headerMap) throws IOException { 83 | this.normalizedTargetUri = SHREQSupport.normalizeTargetURI(targetUri); 84 | this.headerMap = headerMap; 85 | for (String method : SHREQSupport.HTTP_METHODS) { 86 | if (method.equals(targetMethod)) { 87 | this.targetMethod = targetMethod; 88 | return; 89 | } 90 | } 91 | error("Unsupported method: " + targetMethod); 92 | } 93 | 94 | protected static Logger logger = Logger.getLogger(ValidationCore.class.getName()); 95 | 96 | protected abstract String defaultMethod(); 97 | 98 | protected abstract void validateImplementation() throws IOException, GeneralSecurityException; 99 | 100 | public void setCookie(Object cookie) { 101 | this.cookie = cookie; 102 | } 103 | 104 | public Object getCookie() { 105 | return cookie; 106 | } 107 | 108 | public byte[] getJwsPayload() { 109 | return JWS_Payload; 110 | } 111 | 112 | public JSONObjectReader getJwsProtectedHeader() { 113 | return JWS_Protected_Header; 114 | } 115 | 116 | public JSONObjectReader getSHREQRecord() { 117 | return secinf; 118 | } 119 | 120 | public X509Certificate[] getCertificatePath() { 121 | return certificatePath; 122 | } 123 | 124 | public PublicKey getPublicKey() { 125 | return publicKey; 126 | } 127 | 128 | public String getKeyId() { 129 | return keyId; 130 | } 131 | 132 | public SignatureAlgorithms getSignatureAlgorithm() { 133 | return signatureAlgorithm; 134 | } 135 | 136 | public GregorianCalendar getIssuedAt() { 137 | return issuedAt; 138 | } 139 | 140 | public void validate(ValidationKeyService validationKeyService) throws IOException, 141 | GeneralSecurityException { 142 | this.validationKeyService = validationKeyService; 143 | validateImplementation(); 144 | secinf.checkForUnread(); 145 | validateSignature(); 146 | } 147 | 148 | public boolean isValidating() { 149 | return validationMode; 150 | } 151 | 152 | public void enforceTimeStamp(int after, int before) throws IOException { 153 | if (issuedAt == null) { 154 | error("Missing time stamp"); 155 | } 156 | long diff = new GregorianCalendar().getTimeInMillis() - issuedAt.getTimeInMillis(); 157 | if (diff > after || -diff > before) { 158 | error("Time stamp diff (" + diff/1000.0 + " seconds) is outside of limits"); 159 | } 160 | } 161 | 162 | public String printCoreData() throws IOException { 163 | StringBuilder coreData = new StringBuilder("\nReceived Headers:\n") 164 | .append(targetMethod) 165 | .append(' ') 166 | .append(normalizedTargetUri) 167 | .append('\n'); 168 | for (String header : headerMap.keySet()) { 169 | coreData.append(header) 170 | .append(':') 171 | .append(headerMap.get(header)) 172 | .append('\n'); 173 | } 174 | coreData.append("\nJWS Protected Header:\n") 175 | .append(JWS_Protected_Header == null ? 176 | "NOT AVAILABLE\n" : JWS_Protected_Header.toString()) 177 | .append("\nJWS Payload:\n") 178 | .append(JWS_Payload == null ? "NOT AVAILABLE\n" : JSONParser.parse(JWS_Payload).toString()); 179 | coreData.append("\nValidation Key:\n") 180 | .append(cookie == null ? "NOT AVAILABLE\n" : (String) cookie); 181 | return coreData.append('\n').toString(); 182 | } 183 | 184 | protected void error(String what) throws IOException { 185 | throw new IOException(what); 186 | } 187 | 188 | protected JSONObjectReader commonDataFilter(JSONObjectReader tempSecinf, boolean iatRequired) 189 | throws IOException, GeneralSecurityException { 190 | if (tempSecinf.hasProperty(SHREQSupport.SHREQ_HASH_ALG_OVERRIDE)) { 191 | hashAlgorithm = SHREQSupport.getHashAlgorithm( 192 | tempSecinf.getString(SHREQSupport.SHREQ_HASH_ALG_OVERRIDE)); 193 | } else { 194 | hashAlgorithm = signatureAlgorithm.getDigestAlgorithm(); 195 | } 196 | String method = 197 | tempSecinf.getStringConditional(SHREQSupport.SHREQ_HTTP_METHOD, defaultMethod()); 198 | if (!targetMethod.equals(method)){ 199 | error("Declared Method=" + method + " Actual Method=" + targetMethod); 200 | } 201 | if (iatRequired || tempSecinf.hasProperty(SHREQSupport.SHREQ_ISSUED_AT_TIME)) { 202 | issuedAt = new GregorianCalendar(); 203 | issuedAt.setTimeInMillis( 204 | // Nobody [as far as I can tell] use fractions but JWT say you can... 205 | tempSecinf.getInt53(SHREQSupport.SHREQ_ISSUED_AT_TIME) * 1000); 206 | } 207 | if (tempSecinf.hasProperty(SHREQSupport.SHREQ_HEADER_RECORD)) { 208 | JSONArrayReader array = tempSecinf.getArray(SHREQSupport.SHREQ_HEADER_RECORD); 209 | byte[] headerDigest = array.getBinary(); 210 | String headerList = array.getString(); 211 | if (array.hasMore()) { 212 | error("Excess elements in \"" + SHREQSupport.SHREQ_HEADER_RECORD + "\""); 213 | } 214 | if (!SHREQSupport.HEADER_STRING_ARRAY_SYNTAX.matcher(headerList).matches()) { 215 | error("Syntax error in \"" + SHREQSupport.SHREQ_HEADER_RECORD + "\""); 216 | } 217 | StringBuilder headerBlob = new StringBuilder(); 218 | HashSet checker = new HashSet(); 219 | boolean next = false; 220 | for (String header : headerList.split(",")) { 221 | String argument = headerMap.get(header); 222 | if (argument == null) { 223 | error("Missing header in request: " + header); 224 | } 225 | if (next) { 226 | headerBlob.append('\n'); 227 | } 228 | next = true; 229 | headerBlob.append(header) 230 | .append(':') 231 | .append(SHREQSupport.normalizeHeaderArgument(argument)); 232 | if (!checker.add(header)) { 233 | error("Duplicate header in \"" + SHREQSupport.SHREQ_HEADER_RECORD + "\""); 234 | } 235 | } 236 | if (!ArrayUtil.compare(headerDigest, getDigest(headerBlob.toString()))) { 237 | error("\"" + SHREQSupport.SHREQ_HEADER_RECORD + "\" digest error"); 238 | } 239 | } 240 | return tempSecinf; 241 | } 242 | 243 | protected byte[] getDigest(String data) throws IOException { 244 | return hashAlgorithm.digest(data.getBytes("utf-8")); 245 | } 246 | 247 | // 6.6 248 | protected void decodeJwsString(String jwsString, boolean detached) throws IOException, 249 | GeneralSecurityException { 250 | // :1 251 | int endOfHeader = jwsString.indexOf('.'); 252 | int lastDot = jwsString.lastIndexOf('.'); 253 | if (endOfHeader < 5 || lastDot > jwsString.length() - 5) { 254 | error("JWS syntax, must be Header.[Payload].Signature"); 255 | } 256 | if (detached) { 257 | if (endOfHeader != lastDot - 1) { 258 | error("JWS syntax, must be Header..Signature"); 259 | } 260 | } else { 261 | JWS_Payload = Base64URL.decode(jwsString.substring(endOfHeader + 1, lastDot)); 262 | } 263 | // :2 264 | jwsProtectedHeaderB64U = jwsString.substring(0, endOfHeader); 265 | 266 | // :3-4 267 | JWS_Protected_Header = JSONParser.parse(Base64URL.decode(jwsProtectedHeaderB64U)); 268 | signatureAlgorithm = JOSESupport.getSignatureAlgorithm(JWS_Protected_Header); 269 | keyId = JWS_Protected_Header.hasProperty(JOSESupport.KID_JSON) ? 270 | JOSESupport.getKeyId(JWS_Protected_Header) : null; 271 | publicKey = JWS_Protected_Header.hasProperty(JOSESupport.JWK_JSON) ? 272 | JOSESupport.getPublicKey(JWS_Protected_Header) : null; 273 | certificatePath = JWS_Protected_Header.hasProperty(JOSESupport.X5C_JSON) ? 274 | JOSESupport.getCertificatePath(JWS_Protected_Header) : null; 275 | if (publicKey != null) { 276 | if (signatureAlgorithm.isSymmetric()) { 277 | throw new GeneralSecurityException("Public key and HMAC algorithm"); 278 | } 279 | if (certificatePath != null) { 280 | throw new GeneralSecurityException("Mixing \"" + 281 | JOSESupport.JWK_JSON + 282 | "\" and \"" + 283 | JOSESupport.X5C_JSON + 284 | "\""); 285 | } 286 | } 287 | 288 | 289 | // :5-6 290 | JWS_Signature = Base64URL.decode(jwsString.substring(lastDot + 1)); 291 | } 292 | 293 | 294 | // 6.8 295 | protected void validateHeaderDigest(JSONObjectReader headerObject) throws IOException { 296 | error("Not implemented"); 297 | } 298 | 299 | // 6.9 300 | private void validateSignature() throws IOException, GeneralSecurityException { 301 | // 4.2:10 or 5.2:5 302 | 303 | validationMode = true; 304 | 305 | // 6.9:1 306 | // Unused JWS header elements indicate problems... 307 | // Disabled, this is a demo :) 308 | // JWS_Protected_Header.checkForUnread(); 309 | 310 | // 6.9:2-4 311 | JOSESupport.validateJwsSignature( 312 | jwsProtectedHeaderB64U, 313 | JWS_Payload, 314 | JWS_Signature, 315 | validationKeyService.getSignatureValidator( 316 | this, 317 | signatureAlgorithm, 318 | certificatePath == null ? publicKey : certificatePath[0].getPublicKey(), 319 | keyId)); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /test/org/webpki/shreqb64/TestVectors.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.shreqb64; 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | import java.security.KeyPair; 22 | import java.util.GregorianCalendar; 23 | import java.util.LinkedHashMap; 24 | 25 | import org.webpki.crypto.AlgorithmPreferences; 26 | import org.webpki.crypto.AsymSignatureAlgorithms; 27 | import org.webpki.crypto.MACAlgorithms; 28 | import org.webpki.crypto.SignatureAlgorithms; 29 | import org.webpki.json.JSONObjectReader; 30 | import org.webpki.json.JSONObjectWriter; 31 | import org.webpki.json.JSONOutputFormats; 32 | import org.webpki.json.JSONParser; 33 | import org.webpki.jose.JOSEAsymKeyHolder; 34 | import org.webpki.jose.JOSESupport; 35 | import org.webpki.jose.JOSESymKeyHolder; 36 | import org.webpki.shreqb64.SHREQSupport; 37 | import org.webpki.util.Base64; 38 | import org.webpki.util.HexaDecimal; 39 | import org.webpki.util.ISODateTime; 40 | import org.webpki.util.PEMDecoder; 41 | import org.webpki.util.ArrayUtil; 42 | 43 | public class TestVectors { 44 | 45 | static String keyDirectory; 46 | 47 | static int testVectorNumber; 48 | 49 | static StringBuilder rfcText = new StringBuilder(); 50 | 51 | static final int RFC_ARTWORK_LINE_MAX = 64; 52 | 53 | static class Test { 54 | String uri; 55 | String method; 56 | String optionalJSONBody; 57 | SignatureAlgorithms signatureAlgorithm; 58 | String optionalOverrideHashAlgorithm; 59 | String keyAlgName; 60 | GregorianCalendar optionalTimeStamp; 61 | LinkedHashMap optionalHeaders; 62 | 63 | JOSESupport.CoreKeyHolder keyHolder; 64 | String keyInRFCText; 65 | String keyRFCDescription; 66 | String signatureAlgorithmId; 67 | JSONObjectWriter secinf; 68 | byte[] JWS_Payload; 69 | JSONObjectWriter JWS_Protected_Header; 70 | String signedUri; 71 | 72 | Test(String uri, 73 | String method, 74 | String optionalJSONBody, 75 | SignatureAlgorithms signatureAlgorithm, 76 | String optionalOverrideHashAlgorithm, 77 | String keyAlgName, 78 | GregorianCalendar optionalTimeStamp, 79 | LinkedHashMap optionalHeaders) throws Exception { 80 | this.uri = uri; 81 | this.method = method; 82 | this.optionalJSONBody = optionalJSONBody; 83 | this.signatureAlgorithm = signatureAlgorithm; 84 | this.optionalOverrideHashAlgorithm = optionalOverrideHashAlgorithm; 85 | this.keyAlgName = keyAlgName; 86 | this.optionalTimeStamp = optionalTimeStamp; 87 | this.optionalHeaders = optionalHeaders == null ? 88 | new LinkedHashMap() : optionalHeaders; 89 | createVector(); 90 | } 91 | 92 | String utf8(byte[] data) throws IOException { 93 | return new String(data, "utf-8"); 94 | } 95 | 96 | void createVector() throws Exception { 97 | signatureAlgorithmId = signatureAlgorithm.getAlgorithmId(AlgorithmPreferences.JOSE); 98 | SHREQSupport.overridedHashAlgorithm = optionalOverrideHashAlgorithm; 99 | if (signatureAlgorithm.isSymmetric()) { 100 | String keyInHex = utf8(readKey(keyAlgName + "bitkey.hex")); 101 | keyInRFCText = keyInHex; 102 | keyRFCDescription = "Symmetric signature validation key, here in hexadecimal notation:"; 103 | keyHolder = new JOSESymKeyHolder(HexaDecimal.decode(keyInHex)); 104 | } else { 105 | KeyPair keyPair = PEMDecoder.getKeyPair(readKey(keyAlgName + "privatekey.pem")); 106 | keyRFCDescription = "Public signature validation key, here in PEM format:"; 107 | keyInRFCText = 108 | "-----BEGIN PUBLIC KEY-----\n" + 109 | new Base64(RFC_ARTWORK_LINE_MAX).getBase64StringFromBinary(keyPair.getPublic().getEncoded()) + 110 | "\n-----END PUBLIC KEY-----"; 111 | keyHolder = new JOSEAsymKeyHolder(keyPair.getPrivate()); 112 | } 113 | 114 | JWS_Protected_Header = JOSESupport.setSignatureAlgorithm(new JSONObjectWriter(), 115 | signatureAlgorithm); 116 | secinf = new JSONObjectWriter(); 117 | if (optionalJSONBody == null) { 118 | uriRequest(); 119 | } else { 120 | jsonRequest(JSONParser.parse(optionalJSONBody)); 121 | } 122 | rfcText.append("
\n\nTarget URI:\n\n") 131 | .append(artWork(lineCutter(uri))); 132 | 133 | if (optionalJSONBody == null) { 134 | rfcText.append("\nSigned URI:\n\n") 135 | .append(artWork(lineCutter(signedUri))) 136 | .append("\nDecoded JWS Payload:\n\n") 137 | .append(artWork(lineCutter(secinf.serializeToString(JSONOutputFormats.PRETTY_PRINT)))); 138 | } else { 139 | rfcText.append("\nJSON Body:\n\n") 140 | .append(artWork(lineCutter(optionalJSONBody))); 141 | } 142 | if (optionalOverrideHashAlgorithm != null) { 143 | rfcText.append("\nNote the overridden hash algorithm.\n\n"); 144 | } 145 | if (!optionalHeaders.isEmpty()) { 146 | StringBuilder headers = new StringBuilder(); 147 | for (String header : optionalHeaders.keySet()) { 148 | headers.append(header) 149 | .append(": ") 150 | .append(optionalHeaders.get(header)) 151 | .append('\n'); 152 | } 153 | rfcText.append("\nRequired HTTP Headers:\n\n") 154 | .append(artWork(headers.toString().trim())); 155 | } 156 | rfcText.append("\n") 157 | .append(keyRFCDescription) 158 | .append("\n\n") 159 | .append(artWork(keyInRFCText)) 160 | .append("
\n"); 161 | } 162 | 163 | String lineCutter(String string) { 164 | int position = 0; 165 | StringBuilder cutted = new StringBuilder(); 166 | for (char c : string.toCharArray()) { 167 | if (c == '\n') { 168 | position = 0; 169 | } else if (position++ == RFC_ARTWORK_LINE_MAX) { 170 | cutted.append('\n'); 171 | position = 1; 172 | } 173 | cutted.append(c); 174 | } 175 | return cutted.toString().trim(); 176 | } 177 | 178 | StringBuilder artWork(String string) { 179 | StringBuilder total = new StringBuilder("
\n"); 187 | } 188 | 189 | void jsonRequest(JSONObjectReader message) throws Exception { 190 | secinf = SHREQSupport.createJSONRequestSecInf(uri, 191 | method, 192 | optionalTimeStamp, 193 | optionalHeaders, 194 | signatureAlgorithm); 195 | JSONObjectWriter writer = new JSONObjectWriter(message); 196 | writer.setObject(SHREQSupport.SHREQ_SECINF_LABEL, secinf); 197 | JWS_Payload = writer.serializeToBytes(JSONOutputFormats.CANONICALIZED); 198 | String jwsString = JOSESupport.createJwsSignature(JWS_Protected_Header, 199 | JWS_Payload, 200 | keyHolder, 201 | true); 202 | // Create the completed object which now is in "writer" 203 | secinf.setString(SHREQSupport.SHREQ_JWS_STRING, jwsString); 204 | optionalJSONBody = writer.serializeToString(JSONOutputFormats.PRETTY_PRINT); 205 | } 206 | 207 | void uriRequest() throws Exception { 208 | secinf = SHREQSupport.createURIRequestSecInf(uri, 209 | method, 210 | optionalTimeStamp, 211 | optionalHeaders, 212 | signatureAlgorithm); 213 | String jwsString = JOSESupport.createJwsSignature(JWS_Protected_Header, 214 | secinf.serializeToBytes(JSONOutputFormats.NORMALIZED), 215 | keyHolder, 216 | false); 217 | signedUri = SHREQSupport.addJwsToTargetUri(uri, jwsString); 218 | } 219 | } 220 | 221 | public static void main(String[] argv) { 222 | try { 223 | SHREQSupport.useDefaultForMethod = true; 224 | keyDirectory = argv[0]; 225 | GregorianCalendar frozen = 226 | ISODateTime.parseDateTime("2019-03-07T09:45:00Z", 227 | ISODateTime.UTC_NO_SUBSECONDS); 228 | String john = "{\"name\":\"John Doe\", \"profession\":\"Unknown\"}"; 229 | String jane = "{\"name\":\"Jane Smith\", \"profession\":\"Hacker\"}"; 230 | LinkedHashMap header = new LinkedHashMap(); 231 | header.put("x-debug", "full"); 232 | 233 | /* 234 | String uri 235 | String method 236 | String optionalJSONBody 237 | SignatureAlgorithms signatureAlgorithm 238 | String optionalOverrideHashAlgorithm 239 | String keyAlgName 240 | GregorianCalendar optionalTimeStamp 241 | LinkedHashMap optionalHeaders 242 | */ 243 | new Test( 244 | "https://example.com/users/456", 245 | "GET", 246 | null, 247 | MACAlgorithms.HMAC_SHA256, 248 | null, 249 | "a256", 250 | frozen, 251 | null); 252 | 253 | new Test( 254 | "https://example.com/users", 255 | "POST", 256 | john, 257 | AsymSignatureAlgorithms.ECDSA_SHA256, 258 | null, 259 | "P256", 260 | frozen, 261 | null); 262 | 263 | new Test( 264 | "https://example.com/users/456", 265 | "PUT", 266 | jane, 267 | AsymSignatureAlgorithms.ECDSA_SHA256, 268 | null, 269 | "P256", 270 | frozen, 271 | null); 272 | 273 | new Test( 274 | "https://example.com/users/456", 275 | "DELETE", 276 | null, 277 | AsymSignatureAlgorithms.RSA_SHA256, 278 | "S512", 279 | "R2048", 280 | frozen, 281 | header); 282 | 283 | ArrayUtil.writeFile(argv[1], rfcText.toString().getBytes("utf-8")); 284 | } catch (Exception e) { 285 | e.printStackTrace(); 286 | } 287 | } 288 | 289 | static byte[] readKey(String filename) throws IOException { 290 | return ArrayUtil.readFile(keyDirectory + File.separator + filename); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/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.shreqb64; 18 | 19 | import java.io.IOException; 20 | 21 | import java.security.GeneralSecurityException; 22 | import java.security.PublicKey; 23 | 24 | import java.security.cert.X509Certificate; 25 | 26 | import java.util.LinkedHashMap; 27 | 28 | import javax.servlet.ServletException; 29 | import javax.servlet.http.HttpServletRequest; 30 | import javax.servlet.http.HttpServletResponse; 31 | 32 | import org.webpki.crypto.AlgorithmPreferences; 33 | import org.webpki.crypto.AsymSignatureAlgorithms; 34 | import org.webpki.crypto.CertificateInfo; 35 | import org.webpki.crypto.MACAlgorithms; 36 | import org.webpki.crypto.SignatureAlgorithms; 37 | 38 | import org.webpki.json.JSONOutputFormats; 39 | import org.webpki.json.JSONParser; 40 | 41 | import org.webpki.jose.JOSEAsymSignatureValidator; 42 | import org.webpki.jose.JOSEHmacValidator; 43 | import org.webpki.jose.JOSESupport; 44 | 45 | import org.webpki.shreqb64.JSONRequestValidation; 46 | import org.webpki.shreqb64.URIRequestValidation; 47 | import org.webpki.shreqb64.ValidationCore; 48 | import org.webpki.shreqb64.ValidationKeyService; 49 | 50 | import org.webpki.util.HexaDecimal; 51 | import org.webpki.util.PEMDecoder; 52 | 53 | public class ValidateServlet extends BaseGuiServlet implements ValidationKeyService { 54 | 55 | private static final long serialVersionUID = 1L; 56 | 57 | public void doPost(HttpServletRequest request, HttpServletResponse response) 58 | throws IOException, ServletException { 59 | try { 60 | request.setCharacterEncoding("utf-8"); 61 | if (!request.getContentType().startsWith("application/x-www-form-urlencoded")) { 62 | throw new IOException("Unexpected MIME type:" + request.getContentType()); 63 | } 64 | // Get the two input data items 65 | String targetUri = getParameter(request, TARGET_URI); 66 | String signedJsonObject = getParameter(request, JSON_PAYLOAD); 67 | boolean jsonRequest = new Boolean(getParameter(request, REQUEST_TYPE)); 68 | LinkedHashMap httpHeaderData = createHeaderData(getParameter(request, TXT_OPT_HEADERS)); 69 | String validationKey = getParameter(request, JWS_VALIDATION_KEY); 70 | String targetMethod = getParameter(request, PRM_HTTP_METHOD); 71 | ValidationCore validationCore = null; 72 | 73 | // Determining Request Type 74 | if (jsonRequest) { 75 | validationCore = new JSONRequestValidation(targetUri, 76 | targetMethod, 77 | httpHeaderData, 78 | signedJsonObject); 79 | } else { 80 | validationCore = new URIRequestValidation(targetUri, 81 | targetMethod, 82 | httpHeaderData); 83 | } 84 | 85 | // Now assign the key 86 | boolean jwkValidationKey = validationKey.startsWith("{"); 87 | validationCore.setCookie(jwkValidationKey ? 88 | JSONParser.parse(validationKey).getCorePublicKey(AlgorithmPreferences.JOSE) 89 | : 90 | validationKey.contains("-----") ? 91 | PEMDecoder.getPublicKey(validationKey.getBytes("utf-8")) : 92 | HexaDecimal.decode(validationKey)); 93 | 94 | 95 | // Core Request Data Successfully Collected - Validate! 96 | validationCore.validate(this); 97 | 98 | // Parse the JSON data 99 | 100 | StringBuilder html = new StringBuilder( 101 | "
Request Successfully Validated
") 102 | .append(HTML.fancyBox("targeturi", targetUri, 103 | "Target URI to be accessed by an HTTP " + targetMethod + " request")); 104 | if (jsonRequest) { 105 | html.append(HTML.fancyBox("httpjsonbody", signedJsonObject, 106 | "HTTP Body - Base64Url encoded JSON object signed by a JWS")); 107 | } 108 | html.append(HTML.fancyBox("jwsheader", 109 | validationCore.getJwsProtectedHeader() 110 | .serializeToString(JSONOutputFormats.PRETTY_HTML), 111 | "Decoded JWS header")) 112 | .append(HTML.fancyBox("vkey", 113 | jwkValidationKey ? 114 | JSONParser.parse(validationKey) 115 | .serializeToString(JSONOutputFormats.PRETTY_HTML) 116 | : 117 | HTML.encode(validationKey).replace("\n", "
"), 118 | "Signature validation " + 119 | (validationCore.getSignatureAlgorithm().isSymmetric() ? 120 | "secret key in hexadecimal" : 121 | "public key in " + 122 | (jwkValidationKey ? "JWK" : "PEM") + 123 | " format"))); 124 | html.append(HTML.fancyBox( 125 | "jwspayload", 126 | JSONParser.parse(validationCore.getJwsPayload()).serializeToString(JSONOutputFormats.PRETTY_HTML), 127 | "Decoded JWS Payload")); 128 | if (validationCore.getCertificatePath() != null) { 129 | StringBuilder certificateData = null; 130 | for (X509Certificate certificate : validationCore.getCertificatePath()) { 131 | if (certificateData == null) { 132 | certificateData = new StringBuilder(); 133 | } else { 134 | certificateData.append("
 
"); 135 | } 136 | certificateData.append( 137 | HTML.encode(new CertificateInfo(certificate).toString()) 138 | .replace("\n", "
").replace(" ", "")); 139 | } 140 | html.append(HTML.fancyBox("certpath", 141 | certificateData.toString(), 142 | "Core certificate data")); 143 | } 144 | String time; 145 | if (validationCore.getIssuedAt() == null) { 146 | time = "Request does not contain a time stamp"; 147 | } else { 148 | time = BaseRequestServlet.getFormattedUTCTime(validationCore.getIssuedAt()); 149 | } 150 | html.append(HTML.fancyBox( 151 | "timestamp", 152 | time, 153 | "Time stamp")); 154 | HTML.standardPage(response, null, html.append("
")); 155 | } catch (Exception e) { 156 | HTML.errorPage(response, e); 157 | } 158 | } 159 | 160 | @Override 161 | public JOSESupport.CoreSignatureValidator getSignatureValidator(ValidationCore validationCore, 162 | SignatureAlgorithms signatureAlgorithm, 163 | PublicKey publicKey, 164 | String keyId) 165 | throws IOException, GeneralSecurityException { 166 | if (signatureAlgorithm.isSymmetric()) { 167 | return new JOSEHmacValidator((byte[])validationCore.getCookie(), 168 | (MACAlgorithms) signatureAlgorithm); 169 | } 170 | PublicKey validationKey = (PublicKey)validationCore.getCookie(); 171 | if (publicKey != null && !publicKey.equals(validationKey)) { 172 | throw new GeneralSecurityException("In-lined public key differs from predefined public key"); 173 | } 174 | return new JOSEAsymSignatureValidator(validationKey, 175 | (AsymSignatureAlgorithms)signatureAlgorithm); 176 | } 177 | 178 | 179 | public void doGet(HttpServletRequest request, HttpServletResponse response) 180 | throws IOException, ServletException { 181 | getSampleData(request); 182 | StringBuilder html = new StringBuilder( 183 | "
" + 184 | "
SHREQ Message Validation
") 185 | 186 | .append( 187 | HTML.fancyText( 188 | true, 189 | TARGET_URI, 190 | 1, 191 | HTML.encode(sampleJsonRequestUri), 192 | "Target URI")) 193 | 194 | .append( 195 | HTML.fancyText( 196 | true, 197 | JSON_PAYLOAD, 198 | 10, 199 | "", 200 | "Paste a signed JSON request in the text box or try with the default")) 201 | 202 | .append( 203 | HTML.fancyText( 204 | false, 205 | TXT_OPT_HEADERS, 206 | 4, 207 | "", 208 | "Optional HTTP headers, each on a separate line")) 209 | 210 | .append(getRequestParameters()) 211 | 212 | .append( 213 | HTML.fancyText( 214 | true, 215 | JWS_VALIDATION_KEY, 216 | 4, 217 | HTML.encode(SHREQService.sampleKey), 218 | "Validation key (secret key in hexadecimal or public key in PEM or "plain" JWK format)")) 219 | 220 | .append( 221 | "
" + 222 | "
" + 223 | "Validate Signed Request" + 224 | "
" + 225 | "
" + 226 | "
" + 227 | "
 
"); 228 | 229 | StringBuilder js = new StringBuilder("\"use strict\";\n") 230 | .append( 231 | 232 | "function setUserData(unconditionally) {\n" + 233 | " let element = document.getElementById('" + JSON_PAYLOAD + "').children[1];\n" + 234 | " if (unconditionally || element.value == '') element.value = '") 235 | .append(sampleJsonRequest_JS) 236 | .append("';\n" + 237 | " element = document.getElementById('" + TARGET_URI + "').children[1];\n" + 238 | " if (unconditionally || element.value == '') element.value = '") 239 | .append(sampleJsonRequestUri) 240 | .append("';\n" + 241 | "}\n" + 242 | "function showJson(show) {\n" + 243 | " document.getElementById('" + JSON_PAYLOAD + "').style.display= show ? 'block' : 'none';\n" + 244 | "}\n" + 245 | "function setMethod(method) {\n" + 246 | " let s = document.getElementById('" + PRM_HTTP_METHOD + "');\n" + 247 | " for (let i = 0; i < s.options.length; i++) {\n" + 248 | " if (s.options[i].text == method) {\n" + 249 | " s.options[i].selected = true;\n" + 250 | " break;\n" + 251 | " }\n" + 252 | " }\n" + 253 | "}\n" + 254 | "function showHeaders(show) {\n" + 255 | " document.getElementById('" + TXT_OPT_HEADERS + "').style.display= show ? 'block' : 'none';\n" + 256 | "}\n" + 257 | "function restoreRequestDefaults() {\n" + 258 | " let radioButtons = document.getElementsByName('" + REQUEST_TYPE + "');\n" + 259 | " radioButtons[0].checked = true;\n" + 260 | " requestChange(true);\n" + 261 | " document.getElementById('" + FLG_HEADERS + "').checked = false;\n" + 262 | " showHeaders(false);\n" + 263 | " setUserData(true);\n" + 264 | "}\n" + 265 | "function requestChange(jsonRequest) {\n" + 266 | " document.getElementById('" + JSON_PAYLOAD + "').style.display= jsonRequest ? 'block' : 'none';\n" + 267 | " setMethod(jsonRequest ? '" + DEFAULT_JSON_METHOD + "' : '" + DEFAULT_URI_METHOD + "');\n" + 268 | " let element = document.getElementById('" + TARGET_URI + "').children[1];\n" + 269 | " if (jsonRequest) {\n" + 270 | " if (element.value == '" + sampleUriRequestUri + "') {\n" + 271 | " element.value = '" + sampleJsonRequestUri + "';\n" + 272 | " }\n" + 273 | " } else {\n" + 274 | " if (element.value == '" + sampleJsonRequestUri + "') {\n" + 275 | " element.value = '" + sampleUriRequestUri + "';\n" + 276 | " }\n" + 277 | " }\n" + 278 | "}\n" + 279 | "function headerFlagChange(flag) {\n" + 280 | " showHeaders(flag);\n" + 281 | "}\n" + 282 | "window.addEventListener('load', function(event) {\n" + 283 | " let radioButtons = document.getElementsByName('" + REQUEST_TYPE + "');\n" + 284 | " showJson(radioButtons[0].checked);\n" + 285 | " showHeaders(document.getElementById('" + FLG_HEADERS + "').checked);\n" + 286 | " setUserData(false);\n" + 287 | "});\n"); 288 | HTML.standardPage(response, js.toString(), html); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/BaseGuiServlet.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.shreqb64; 18 | 19 | import java.io.IOException; 20 | 21 | import java.security.GeneralSecurityException; 22 | import java.security.PrivateKey; 23 | import java.security.PublicKey; 24 | 25 | import java.util.GregorianCalendar; 26 | import java.util.LinkedHashMap; 27 | 28 | import java.util.logging.Logger; 29 | 30 | import java.util.regex.Pattern; 31 | 32 | import javax.servlet.http.HttpServlet; 33 | import javax.servlet.http.HttpServletRequest; 34 | 35 | import org.webpki.crypto.AlgorithmPreferences; 36 | import org.webpki.crypto.AsymSignatureAlgorithms; 37 | import org.webpki.crypto.SignatureAlgorithms; 38 | 39 | import org.webpki.jose.JOSEAsymKeyHolder; 40 | import org.webpki.jose.JOSESupport; 41 | 42 | import org.webpki.json.JSONObjectWriter; 43 | import org.webpki.json.JSONOutputFormats; 44 | import org.webpki.json.JSONParser; 45 | 46 | import org.webpki.shreqb64.SHREQSupport; 47 | 48 | import org.webpki.util.Base64; 49 | 50 | public class BaseGuiServlet extends HttpServlet { 51 | 52 | static Logger logger = Logger.getLogger(BaseGuiServlet.class.getName()); 53 | 54 | private static final long serialVersionUID = 1L; 55 | 56 | // HTML form arguments 57 | static final String TARGET_URI = "uri"; 58 | 59 | static final String REQUEST_TYPE = "reqtyp"; // True = JSON else URI 60 | 61 | static final String JSON_PAYLOAD = "json"; 62 | 63 | static final String JWS_VALIDATION_KEY = "vkey"; 64 | 65 | static final String PRM_HTTP_METHOD = "mtd"; 66 | 67 | static final String TXT_OPT_HEADERS = "hdrs"; 68 | 69 | static final String TXT_JWS_EXTRA = "xtra"; 70 | 71 | static final String TXT_SECRET_KEY = "sec"; 72 | 73 | static final String TXT_PRIVATE_KEY = "priv"; 74 | 75 | static final String TXT_CERT_PATH = "cert"; 76 | 77 | static final String PRM_JWS_ALGORITHM = "alg"; 78 | 79 | static final String FLG_CERT_PATH = "cerflg"; 80 | static final String FLG_JWK_INLINE = "jwkflg"; 81 | static final String FLG_DEF_METHOD = "defmtd"; 82 | static final String FLG_IAT_PRESENT = "iatflg"; 83 | static final String FLG_HEADERS = "hdrflg"; 84 | 85 | static final String DEFAULT_ALGORITHM = "ES256"; 86 | static final String DEFAULT_JSON_METHOD = "POST"; 87 | static final String DEFAULT_URI_METHOD = "GET"; 88 | 89 | static class SelectMethod { 90 | 91 | StringBuilder html = new StringBuilder("").toString(); 108 | } 109 | } 110 | 111 | class SelectAlg { 112 | 113 | String preSelected; 114 | StringBuilder html = new StringBuilder("").toString(); 136 | } 137 | } 138 | 139 | StringBuilder checkBox(String idName, String text, boolean checked, String onchange) { 140 | StringBuilder html = new StringBuilder("
") 154 | .append(text) 155 | .append("
"); 156 | return html; 157 | } 158 | 159 | StringBuilder radioButton(String name, String text, String value, boolean checked, String onchange) { 160 | StringBuilder html = new StringBuilder("
") 174 | .append(text) 175 | .append("
"); 176 | return html; 177 | } 178 | 179 | StringBuilder parameterBox(String header, StringBuilder body) { 180 | return new StringBuilder( 181 | "
" + 182 | "
" + 183 | "
" + 184 | "
") 185 | .append(header) 186 | .append( 187 | "
" + 188 | "
") 189 | .append(body) 190 | .append( 191 | "
" + 192 | "
"); 193 | } 194 | 195 | StringBuilder getRequestParameters() { 196 | return parameterBox("Request Parameters", 197 | new StringBuilder() 198 | .append( 199 | "
") 200 | .append(new SelectMethod().toString()) 201 | .append( 202 | "
HTTP Method
" + 203 | "
Restore defaults
") 204 | .append(radioButton(REQUEST_TYPE, "JSON based request", "true", true, "requestChange(true)")) 205 | .append(radioButton(REQUEST_TYPE, "URI based request", "false", false, "requestChange(false)")) 206 | .append(checkBox(FLG_HEADERS, "Include HTTP headers", 207 | false, "headerFlagChange(this.checked)"))); 208 | } 209 | 210 | String getParameter(HttpServletRequest request, String parameter) throws IOException { 211 | String string = request.getParameter(parameter); 212 | if (string == null) { 213 | throw new IOException("Missing data for: "+ parameter); 214 | } 215 | return string.trim(); 216 | } 217 | 218 | byte[] getBinaryParameter(HttpServletRequest request, String parameter) throws IOException { 219 | return getParameter(request, parameter).getBytes("utf-8"); 220 | } 221 | 222 | String getTextArea(HttpServletRequest request, String name) throws IOException { 223 | String string = getParameter(request, name); 224 | StringBuilder s = new StringBuilder(); 225 | for (char c : string.toCharArray()) { 226 | if (c != '\r') { 227 | s.append(c); 228 | } 229 | } 230 | return s.toString(); 231 | } 232 | private static final String HEADER_SYNTAX = "[ \t]*[a-z0-9A-Z\\$\\._\\-]+[ \t]*:.*"; 233 | 234 | static final Pattern HEADER_STRING_ARRAY_SYNTAX = 235 | Pattern.compile(HEADER_SYNTAX + "+(\n" + HEADER_SYNTAX + "+)*"); 236 | 237 | LinkedHashMap createHeaderData(String rawText) throws IOException { 238 | LinkedHashMap headerData = new LinkedHashMap(); 239 | if (!rawText.isEmpty()) { 240 | rawText = rawText.trim().replace("\r", ""); 241 | if (!HEADER_STRING_ARRAY_SYNTAX.matcher(rawText).matches()) { 242 | throw new IOException("HTTP Header syntax"); 243 | } 244 | for (String headerLine : rawText.split("\n")) { 245 | int colon = headerLine.indexOf(':'); 246 | String headerName = headerLine.substring(0, colon).trim().toLowerCase(); 247 | String headerValue = headerLine.substring(colon + 1).trim(); 248 | if (headerData.containsKey(headerName)) { 249 | headerData.put(headerName, headerData.get(headerName) + ", " + headerValue); 250 | } else { 251 | headerData.put(headerName, headerValue); 252 | } 253 | } 254 | } 255 | return headerData; 256 | } 257 | 258 | static String getPEMFromPublicKey(PublicKey publicKey) { 259 | return "-----BEGIN PUBLIC KEY-----\n" + 260 | new Base64().getBase64StringFromBinary(publicKey.getEncoded()) + 261 | "\n-----END PUBLIC KEY-----"; 262 | } 263 | 264 | private static final String TEST_MESSAGE = 265 | "{\n" + 266 | " \"statement\": \"Hello signed world!\",\n" + 267 | " \"otherProperties\": [2e+3, true]\n" + 268 | "}"; 269 | 270 | private static final String CURL_TEST_MESSAGE = 271 | "{\n" + 272 | " \"name\": \"Jane Smith\",\n" + 273 | " \"profession\": \"Hacker\"\n" + 274 | "}"; 275 | 276 | private static final String CURL_PUT_TEST_MESSAGE = 277 | "{\n" + 278 | " \"name\": \"Jane Smith\",\n" + 279 | " \"profession\": \"Software Engineer\"\n" + 280 | "}"; 281 | 282 | static String sampleJson_JS; 283 | 284 | static String sampleJsonRequest_JS; 285 | 286 | static String sampleJsonRequest_CURL; 287 | 288 | static String sampleJsonRequest_CURL_Header_PUT; 289 | 290 | static String sampleJsonRequestUri; 291 | 292 | static String sampleUriRequestUri; 293 | 294 | static String sampleUriRequestUri2BeSigned; 295 | 296 | protected void getSampleData(HttpServletRequest request) throws IOException { 297 | if (sampleJsonRequest_JS == null) { 298 | synchronized(this) { 299 | try { 300 | String baseUri = 301 | SHREQSupport.normalizeTargetURI(BaseRequestServlet.getUrlFromRequest(request)); 302 | sampleJsonRequestUri = 303 | baseUri.substring(0, baseUri.indexOf("/shreqb64/") + 9) + 304 | BaseRequestServlet.PRECONFREQ; 305 | sampleUriRequestUri2BeSigned = sampleJsonRequestUri + "/456"; 306 | 307 | LinkedHashMap noHeaders = new LinkedHashMap(); 308 | 309 | AsymSignatureAlgorithms signatureAlgorithm = AsymSignatureAlgorithms.ECDSA_SHA256; 310 | 311 | // Sign it using the provided algorithm and key 312 | PrivateKey privateKey = 313 | SHREQService.predefinedKeyPairs 314 | .get(signatureAlgorithm 315 | .getAlgorithmId(AlgorithmPreferences.JOSE)).getPrivate(); 316 | 317 | JSONObjectWriter JWS_Protected_Header = 318 | JOSESupport.setSignatureAlgorithm(new JSONObjectWriter(), 319 | signatureAlgorithm); 320 | JSONObjectWriter message = 321 | new JSONObjectWriter(JSONParser.parse(TEST_MESSAGE)); 322 | 323 | JSONObjectWriter secinf = 324 | SHREQSupport.createJSONRequestSecInf(sampleJsonRequestUri, 325 | null, 326 | new GregorianCalendar(), 327 | noHeaders, 328 | signatureAlgorithm); 329 | message.setObject(SHREQSupport.SHREQ_SECINF_LABEL, secinf); 330 | byte[] JWS_Payload = message.serializeToBytes(JSONOutputFormats.NORMALIZED); 331 | 332 | sampleJsonRequest_JS = 333 | JOSESupport.createJwsSignature(JWS_Protected_Header, 334 | JWS_Payload, 335 | new JOSEAsymKeyHolder(privateKey), 336 | false); 337 | 338 | message = new JSONObjectWriter(JSONParser.parse(CURL_TEST_MESSAGE)); 339 | 340 | secinf = SHREQSupport.createJSONRequestSecInf(sampleJsonRequestUri, 341 | null, 342 | new GregorianCalendar(), 343 | noHeaders, 344 | signatureAlgorithm); 345 | message.setObject(SHREQSupport.SHREQ_SECINF_LABEL, secinf); 346 | JWS_Payload = message.serializeToBytes(JSONOutputFormats.NORMALIZED); 347 | 348 | sampleJsonRequest_CURL = 349 | JOSESupport.createJwsSignature(JWS_Protected_Header, 350 | JWS_Payload, 351 | new JOSEAsymKeyHolder(privateKey), 352 | false); 353 | 354 | message = new JSONObjectWriter(JSONParser.parse(CURL_PUT_TEST_MESSAGE)); 355 | 356 | LinkedHashMap oneHeader = new LinkedHashMap(); 357 | oneHeader.put("x-debug", "full"); 358 | 359 | secinf = SHREQSupport.createJSONRequestSecInf(sampleUriRequestUri2BeSigned, 360 | "PUT", 361 | new GregorianCalendar(), 362 | oneHeader, 363 | signatureAlgorithm); 364 | message.setObject(SHREQSupport.SHREQ_SECINF_LABEL, secinf); 365 | JWS_Payload = message.serializeToBytes(JSONOutputFormats.NORMALIZED); 366 | 367 | sampleJsonRequest_CURL_Header_PUT = 368 | JOSESupport.createJwsSignature(JWS_Protected_Header, 369 | JWS_Payload, 370 | new JOSEAsymKeyHolder(privateKey), 371 | false); 372 | 373 | secinf = SHREQSupport.createURIRequestSecInf(sampleUriRequestUri2BeSigned, 374 | null, 375 | new GregorianCalendar(), 376 | noHeaders, 377 | signatureAlgorithm); 378 | sampleUriRequestUri = SHREQSupport.addJwsToTargetUri( 379 | sampleUriRequestUri2BeSigned, 380 | JOSESupport.createJwsSignature( 381 | JWS_Protected_Header, 382 | secinf.serializeToBytes(JSONOutputFormats.NORMALIZED), 383 | new JOSEAsymKeyHolder(privateKey), 384 | false)); 385 | 386 | sampleJson_JS = HTML.javaScript(TEST_MESSAGE); 387 | 388 | } catch (GeneralSecurityException e) { 389 | sampleJsonRequest_JS = "Internal error - Call admin"; 390 | } 391 | } 392 | } 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreqb64/CreateServlet.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.shreqb64; 18 | 19 | import java.io.IOException; 20 | import java.net.URLEncoder; 21 | import java.security.KeyPair; 22 | import java.util.GregorianCalendar; 23 | import java.util.LinkedHashMap; 24 | 25 | import javax.servlet.ServletException; 26 | import javax.servlet.http.HttpServletRequest; 27 | import javax.servlet.http.HttpServletResponse; 28 | 29 | import org.webpki.crypto.AsymSignatureAlgorithms; 30 | import org.webpki.crypto.MACAlgorithms; 31 | import org.webpki.crypto.SignatureAlgorithms; 32 | import org.webpki.jose.JOSEAsymKeyHolder; 33 | import org.webpki.jose.JOSESupport; 34 | import org.webpki.jose.JOSESymKeyHolder; 35 | import org.webpki.json.JSONObjectReader; 36 | import org.webpki.json.JSONObjectWriter; 37 | import org.webpki.json.JSONOutputFormats; 38 | import org.webpki.json.JSONParser; 39 | import org.webpki.shreqb64.SHREQSupport; 40 | import org.webpki.util.HexaDecimal; 41 | import org.webpki.util.PEMDecoder; 42 | 43 | public class CreateServlet extends BaseGuiServlet { 44 | 45 | private static final long serialVersionUID = 1L; 46 | 47 | public void doGet(HttpServletRequest request, HttpServletResponse response) 48 | throws IOException, ServletException { 49 | String defaultAlgorithm = "ES256"; 50 | getSampleData(request); 51 | StringBuilder html = new StringBuilder( 52 | "
" + 53 | "
SHREQ Message Creation
") 54 | 55 | .append( 56 | HTML.fancyText( 57 | true, 58 | TARGET_URI, 59 | 1, 60 | HTML.encode(sampleJsonRequestUri), 61 | "Target URI")) 62 | 63 | .append( 64 | HTML.fancyText( 65 | true, 66 | JSON_PAYLOAD, 67 | 10, 68 | "", 69 | "Paste an unsigned JSON object in the text box or try with the default")) 70 | 71 | .append( 72 | HTML.fancyText( 73 | false, 74 | TXT_OPT_HEADERS, 75 | 4, 76 | "", 77 | "Optional HTTP headers, each on a separate line")) 78 | 79 | .append(getRequestParameters()) 80 | 81 | .append(parameterBox("Security Parameters", 82 | new StringBuilder() 83 | .append( 84 | "
") 85 | .append(new SelectAlg(defaultAlgorithm) 86 | .add(MACAlgorithms.HMAC_SHA256) 87 | .add(MACAlgorithms.HMAC_SHA384) 88 | .add(MACAlgorithms.HMAC_SHA512) 89 | .add(AsymSignatureAlgorithms.ECDSA_SHA256) 90 | .add(AsymSignatureAlgorithms.ECDSA_SHA384) 91 | .add(AsymSignatureAlgorithms.ECDSA_SHA512) 92 | .add(AsymSignatureAlgorithms.RSA_SHA256) 93 | .add(AsymSignatureAlgorithms.RSA_SHA384) 94 | .add(AsymSignatureAlgorithms.RSA_SHA512) 95 | .toString()) 96 | .append( 97 | "
Algorithm
" + 98 | "
Restore defaults
") 99 | .append(checkBox(FLG_JWK_INLINE, "Automagically insert public key (JWK)", 100 | false, "jwkFlagChange(this.checked)")) 101 | .append(checkBox(FLG_CERT_PATH, "Include provided certificate path (X5C)", 102 | false, "certFlagChange(this.checked)")) 103 | .append(checkBox(FLG_DEF_METHOD, "Include method even when default", 104 | false, null)) 105 | .append(checkBox(FLG_IAT_PRESENT, "Include time stamp (IAT)", 106 | true, "iatFlagChange()")))) 107 | .append( 108 | "
" + 109 | "
" + 110 | "Create Signed Request" + 111 | "
" + 112 | "
") 113 | 114 | .append( 115 | HTML.fancyText( 116 | true, 117 | TXT_JWS_EXTRA, 118 | 4, 119 | "", 120 | "Additional JWS header parameters (here expressed as properties of a JSON object)")) 121 | 122 | .append( 123 | HTML.fancyText( 124 | false, 125 | TXT_SECRET_KEY, 126 | 1, 127 | "", 128 | "Secret key in hexadecimal format")) 129 | 130 | .append( 131 | HTML.fancyText( 132 | false, 133 | TXT_PRIVATE_KEY, 134 | 4, 135 | "", 136 | "Private key in PEM/PKCS #8 or "plain" JWK format")) 137 | 138 | .append( 139 | HTML.fancyText( 140 | false, 141 | TXT_CERT_PATH, 142 | 4, 143 | "", 144 | "Certificate path in PEM format")) 145 | 146 | .append( 147 | "
" + 148 | "
 
"); 149 | 150 | StringBuilder js = new StringBuilder("\"use strict\";\n") 151 | .append(SHREQService.keyDeclarations) 152 | .append( 153 | "function fill(id, alg, keyHolder, unconditionally) {\n" + 154 | " let element = document.getElementById(id).children[1];\n" + 155 | " if (unconditionally || element.value == '') element.value = keyHolder[alg];\n" + 156 | "}\n" + 157 | "function disableAndClearCheckBox(id) {\n" + 158 | " let checkBox = document.getElementById(id);\n" + 159 | " checkBox.checked = false;\n" + 160 | " checkBox.disabled = true;\n" + 161 | "}\n" + 162 | "function enableCheckBox(id) {\n" + 163 | " document.getElementById(id).disabled = false;\n" + 164 | "}\n" + 165 | "function setUserData(unconditionally) {\n" + 166 | " let element = document.getElementById('" + JSON_PAYLOAD + "').children[1];\n" + 167 | " if (unconditionally || element.value == '') element.value = '") 168 | .append(sampleJson_JS) 169 | .append("';\n" + 170 | " element = document.getElementById('" + TXT_JWS_EXTRA + "').children[1];\n" + 171 | " if (unconditionally || element.value == '') element.value = '{\\n}';\n" + 172 | " element = document.getElementById('" + TARGET_URI + "').children[1];\n" + 173 | " if (unconditionally || element.value == '') element.value = '") 174 | .append(sampleJsonRequestUri) 175 | .append("';\n" + 176 | "}\n" + 177 | "function setParameters(alg, unconditionally) {\n" + 178 | " if (alg.startsWith('HS')) {\n" + 179 | " showCert(false);\n" + 180 | " showPriv(false);\n" + 181 | " disableAndClearCheckBox('" + FLG_CERT_PATH + "');\n" + 182 | " disableAndClearCheckBox('" + FLG_JWK_INLINE + "');\n" + 183 | " fill('" + TXT_SECRET_KEY + "', alg, " + 184 | SHREQService.KeyDeclaration.SECRET_KEYS + ", unconditionally);\n" + 185 | " showSec(true)\n" + 186 | " } else {\n" + 187 | " showSec(false)\n" + 188 | " enableCheckBox('" + FLG_CERT_PATH + "');\n" + 189 | " enableCheckBox('" + FLG_JWK_INLINE + "');\n" + 190 | " fill('" + TXT_PRIVATE_KEY + "', alg, " + 191 | SHREQService.KeyDeclaration.PRIVATE_KEYS + ", unconditionally);\n" + 192 | " showPriv(true);\n" + 193 | " fill('" + TXT_CERT_PATH + "', alg, " + 194 | SHREQService.KeyDeclaration.CERTIFICATES + ", unconditionally);\n" + 195 | " showCert(document.getElementById('" + FLG_CERT_PATH + "').checked);\n" + 196 | " }\n" + 197 | "}\n" + 198 | "function jwkFlagChange(flag) {\n" + 199 | " if (flag) {\n" + 200 | " document.getElementById('" + FLG_CERT_PATH + "').checked = false;\n" + 201 | " showCert(false);\n" + 202 | " }\n" + 203 | "}\n" + 204 | "function certFlagChange(flag) {\n" + 205 | " showCert(flag);\n" + 206 | " if (flag) {\n" + 207 | " document.getElementById('" + FLG_JWK_INLINE + "').checked = false;\n" + 208 | " }\n" + 209 | "}\n" + 210 | "function iatFlagChange() {\n" + 211 | " if (document.getElementsByName('" + REQUEST_TYPE + "')[1].checked) {\n" + 212 | " document.getElementById('" + FLG_IAT_PRESENT + "').checked = true;\n" + 213 | " }\n" + 214 | "}\n" + 215 | "function restoreSecurityDefaults() {\n" + 216 | " let s = document.getElementById('" + PRM_JWS_ALGORITHM + "');\n" + 217 | " for (let i = 0; i < s.options.length; i++) {\n" + 218 | " if (s.options[i].text == '" + DEFAULT_ALGORITHM + "') {\n" + 219 | " s.options[i].selected = true;\n" + 220 | " break;\n" + 221 | " }\n" + 222 | " }\n" + 223 | " setParameters('" + DEFAULT_ALGORITHM + "', true);\n" + 224 | " document.getElementById('" + FLG_CERT_PATH + "').checked = false;\n" + 225 | " document.getElementById('" + FLG_JWK_INLINE + "').checked = false;\n" + 226 | " document.getElementById('" + FLG_IAT_PRESENT + "').checked = true;\n" + 227 | " setUserData(true);\n" + 228 | "}\n" + 229 | "function algChange(alg) {\n" + 230 | " setParameters(alg, true);\n" + 231 | "}\n" + 232 | "function showJson(show) {\n" + 233 | " document.getElementById('" + JSON_PAYLOAD + "').style.display= show ? 'block' : 'none';\n" + 234 | "}\n" + 235 | "function showCert(show) {\n" + 236 | " document.getElementById('" + TXT_CERT_PATH + "').style.display= show ? 'block' : 'none';\n" + 237 | "}\n" + 238 | "function showPriv(show) {\n" + 239 | " document.getElementById('" + TXT_PRIVATE_KEY + "').style.display= show ? 'block' : 'none';\n" + 240 | "}\n" + 241 | "function showSec(show) {\n" + 242 | " document.getElementById('" + TXT_SECRET_KEY + "').style.display= show ? 'block' : 'none';\n" + 243 | "}\n" + 244 | "function setMethod(method) {\n" + 245 | " let s = document.getElementById('" + PRM_HTTP_METHOD + "');\n" + 246 | " for (let i = 0; i < s.options.length; i++) {\n" + 247 | " if (s.options[i].text == method) {\n" + 248 | " s.options[i].selected = true;\n" + 249 | " break;\n" + 250 | " }\n" + 251 | " }\n" + 252 | "}\n" + 253 | "function showHeaders(show) {\n" + 254 | " document.getElementById('" + TXT_OPT_HEADERS + "').style.display= show ? 'block' : 'none';\n" + 255 | "}\n" + 256 | "function restoreRequestDefaults() {\n" + 257 | " let radioButtons = document.getElementsByName('" + REQUEST_TYPE + "');\n" + 258 | " radioButtons[0].checked = true;\n" + 259 | " requestChange(true);\n" + 260 | " document.getElementById('" + FLG_HEADERS + "').checked = false;\n" + 261 | " showHeaders(false);\n" + 262 | "}\n" + 263 | "function requestChange(jsonRequest) {\n" + 264 | " document.getElementById('" + JSON_PAYLOAD + "').style.display= jsonRequest ? 'block' : 'none';\n" + 265 | " setMethod(jsonRequest ? '" + DEFAULT_JSON_METHOD + "' : '" + DEFAULT_URI_METHOD + "');\n" + 266 | " if (!jsonRequest) {\n" + 267 | " document.getElementById('" + FLG_IAT_PRESENT + "').checked = true;\n" + 268 | " }\n" + 269 | " let element = document.getElementById('" + TARGET_URI + "').children[1];\n" + 270 | " if (jsonRequest) {\n" + 271 | " if (element.value == '" + sampleUriRequestUri2BeSigned + "') {\n" + 272 | " element.value = '" + sampleJsonRequestUri + "';\n" + 273 | " }\n" + 274 | " } else {\n" + 275 | " if (element.value == '" + sampleJsonRequestUri + "') {\n" + 276 | " element.value = '" + sampleUriRequestUri2BeSigned + "';\n" + 277 | " }\n" + 278 | " }\n" + 279 | "}\n" + 280 | "function headerFlagChange(flag) {\n" + 281 | " showHeaders(flag);\n" + 282 | "}\n" + 283 | "window.addEventListener('load', function(event) {\n" + 284 | " setParameters(document.getElementById('" + PRM_JWS_ALGORITHM + "').value, false);\n" + 285 | " let radioButtons = document.getElementsByName('" + REQUEST_TYPE + "');\n" + 286 | " showJson(radioButtons[0].checked);\n" + 287 | " showHeaders(document.getElementById('" + FLG_HEADERS + "').checked);\n" + 288 | " setUserData(false);\n" + 289 | "});\n"); 290 | HTML.standardPage(response, js.toString(), html); 291 | } 292 | 293 | public void doPost(HttpServletRequest request, HttpServletResponse response) 294 | throws IOException, ServletException { 295 | try { 296 | request.setCharacterEncoding("utf-8"); 297 | String targetUri = SHREQSupport.normalizeTargetURI(getTextArea(request, TARGET_URI)); 298 | String jsonData = getTextArea(request, JSON_PAYLOAD); 299 | String rawHttpHeaderData = getTextArea(request, TXT_OPT_HEADERS); 300 | String method = getParameter(request, PRM_HTTP_METHOD); 301 | boolean jsonRequest = new Boolean(getParameter(request, REQUEST_TYPE)); 302 | JSONObjectReader additionalHeaderData = JSONParser.parse(getParameter(request, TXT_JWS_EXTRA)); 303 | boolean keyInlining = request.getParameter(FLG_JWK_INLINE) != null; 304 | boolean certOption = request.getParameter(FLG_CERT_PATH) != null; 305 | boolean iatOption = request.getParameter(FLG_IAT_PRESENT) != null; 306 | boolean forceMethod = request.getParameter(FLG_DEF_METHOD) != null; 307 | boolean httpHeaders = request.getParameter(FLG_HEADERS) != null; 308 | LinkedHashMap httpHeaderData = 309 | createHeaderData(httpHeaders ? rawHttpHeaderData : ""); 310 | SignatureAlgorithms algorithm = 311 | JOSESupport.getSignatureAlgorithm(getParameter(request, PRM_JWS_ALGORITHM)); 312 | 313 | // Create the minimal JWS header 314 | JSONObjectWriter JWS_Protected_Header = 315 | JOSESupport.setSignatureAlgorithm(new JSONObjectWriter(), algorithm); 316 | 317 | // Add any optional (by the user specified) arguments 318 | for (String key : additionalHeaderData.getProperties()) { 319 | JWS_Protected_Header.copyElement(key, key, additionalHeaderData); 320 | } 321 | 322 | // Get the signature key 323 | JOSESupport.CoreKeyHolder keyHolder; 324 | String validationKey; 325 | 326 | // Symmetric or asymmetric? 327 | if (algorithm.isSymmetric()) { 328 | validationKey = getParameter(request, TXT_SECRET_KEY); 329 | keyHolder = new JOSESymKeyHolder(HexaDecimal.decode(validationKey)); 330 | } else { 331 | // To simplify UI we require PKCS #8 with the public key embedded 332 | // but we also support JWK which also has the public key 333 | byte[] privateKeyBlob = getBinaryParameter(request, TXT_PRIVATE_KEY); 334 | KeyPair keyPair; 335 | if (privateKeyBlob[0] == '{') { 336 | keyPair = JSONParser.parse(privateKeyBlob).getKeyPair(); 337 | } else { 338 | keyPair = PEMDecoder.getKeyPair(privateKeyBlob); 339 | } 340 | privateKeyBlob = null; // Nullify it after use 341 | validationKey = getPEMFromPublicKey(keyPair.getPublic()); 342 | 343 | // Add other JWS header data that the demo program fixes 344 | if (certOption) { 345 | JOSESupport.setCertificatePath(JWS_Protected_Header, 346 | PEMDecoder.getCertificatePath(getBinaryParameter(request, 347 | TXT_CERT_PATH))); 348 | } else if (keyInlining) { 349 | JOSESupport.setPublicKey(JWS_Protected_Header, keyPair.getPublic()); 350 | } 351 | keyHolder = new JOSEAsymKeyHolder(keyPair.getPrivate()); 352 | } 353 | String signedJSONRequest; 354 | if (jsonRequest) { 355 | // Creating JSON data to be signed 356 | JSONObjectReader reader = JSONParser.parse(jsonData); 357 | if (reader.getJSONArrayReader() != null) { 358 | throw new IOException("The demo does not support signed arrays"); 359 | } 360 | JSONObjectWriter message = new JSONObjectWriter(reader); 361 | JSONObjectWriter secinf = 362 | SHREQSupport.createJSONRequestSecInf( 363 | targetUri, 364 | forceMethod || 365 | !method.equals(SHREQSupport.SHREQ_DEFAULT_JSON_METHOD) ? 366 | method : null, 367 | iatOption ? new GregorianCalendar() : null, 368 | httpHeaderData, 369 | algorithm); 370 | message.setObject(SHREQSupport.SHREQ_SECINF_LABEL, secinf); 371 | byte[] JWS_Payload = message.serializeToBytes(JSONOutputFormats.NORMALIZED); 372 | 373 | // Sign it using the provided algorithm and key 374 | signedJSONRequest = JOSESupport.createJwsSignature(JWS_Protected_Header, 375 | JWS_Payload, 376 | keyHolder, 377 | false); 378 | keyHolder = null; // Nullify it after use 379 | } else { 380 | signedJSONRequest=""; 381 | JSONObjectWriter writer = 382 | SHREQSupport.createURIRequestSecInf( 383 | targetUri, 384 | forceMethod || 385 | !method.equals(SHREQSupport.SHREQ_DEFAULT_URI_METHOD) ? 386 | method : null, 387 | iatOption ? new GregorianCalendar() : null, 388 | httpHeaderData, 389 | algorithm); 390 | targetUri = SHREQSupport.addJwsToTargetUri( 391 | targetUri, 392 | JOSESupport.createJwsSignature( 393 | JWS_Protected_Header, 394 | writer.serializeToBytes(JSONOutputFormats.NORMALIZED), 395 | keyHolder, 396 | false)); 397 | } 398 | 399 | // We terminate by validating the signature as well 400 | request.getRequestDispatcher("validate?" + 401 | TARGET_URI + 402 | "=" + 403 | URLEncoder.encode(targetUri, "utf-8") + 404 | "&" + 405 | REQUEST_TYPE + 406 | "=" + 407 | jsonRequest + 408 | "&" + 409 | JSON_PAYLOAD + 410 | "=" + 411 | URLEncoder.encode(signedJSONRequest, "utf-8") + 412 | "&" + 413 | TXT_OPT_HEADERS + 414 | "=" + 415 | URLEncoder.encode(rawHttpHeaderData, "utf-8") + 416 | "&" + 417 | JWS_VALIDATION_KEY + 418 | "=" + 419 | URLEncoder.encode(validationKey, "utf-8") + 420 | "&" + 421 | PRM_HTTP_METHOD + 422 | "=" + 423 | URLEncoder.encode(method, "utf-8")) 424 | .forward(request, response); 425 | } catch (Exception e) { 426 | HTML.errorPage(response, e); 427 | } 428 | } 429 | } 430 | --------------------------------------------------------------------------------