├── .gitignore ├── .gitattributes ├── web ├── index.jsp ├── webpkiorg.png ├── style.css └── images │ ├── shreq.svg │ └── thelab.svg ├── shreq.properties ├── .settings ├── org.eclipse.core.runtime.prefs └── org.eclipse.core.resources.prefs ├── empty.lib └── .gitignore ├── .classpath ├── LICENSE ├── README.md ├── .project ├── src └── org │ └── webpki │ └── webapps │ └── shreq │ ├── 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 │ └── shreq │ ├── ValidationKeyService.java │ ├── URIRequestValidation.java │ ├── JSONRequestValidation.java │ ├── SHREQSupport.java │ └── ValidationCore.java ├── web.xml └── test └── org └── webpki └── shreq └── 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 | -------------------------------------------------------------------------------- /web/webpkiorg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberphone/shreq/master/web/webpkiorg.png -------------------------------------------------------------------------------- /shreq.properties: -------------------------------------------------------------------------------- 1 | # Lots of stuff is fetched from here 2 | openkeystore=../openkeystore 3 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.runtime.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | line.separator=\n 3 | -------------------------------------------------------------------------------- /empty.lib/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding/=UTF-8 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | shreq 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | 19 | 1723925813693 20 | 21 | 30 22 | 23 | org.eclipse.core.resources.regexFilterMatcher 24 | node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreq/ExtConfReq2Servlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 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/shreq/ExtConfReqServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 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/shreq/PreConfReq2Servlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 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/shreq/PreConfReqServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 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/shreq/ValidationKeyService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.shreq; 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.jws.JWSValidator; 27 | 28 | public interface ValidationKeyService { 29 | 30 | public JWSValidator getSignatureValidator(ValidationCore valiationCode, 31 | SignatureAlgorithms signatureAlgorithm, 32 | PublicKey publicKey, // Also filled for X5C 33 | String keyId) throws IOException, 34 | GeneralSecurityException; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /web/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin:10pt; 3 | font-size:8pt; 4 | font-family:Verdana,'Bitstream Vera Sans','DejaVu Sans',Arial,'Liberation Sans'; 5 | } 6 | 7 | a { 8 | color:#007fff; 9 | text-decoration:none; 10 | outline:none; 11 | } 12 | 13 | li { 14 | padding-top:5pt 15 | } 16 | 17 | .staticbox, .textbox { 18 | box-sizing:border-box; 19 | width:100%;word-break:break-all; 20 | border-width:1px; 21 | border-style:solid; 22 | border-color:grey; 23 | padding:10pt; 24 | } 25 | 26 | .staticbox { 27 | background:#f8f8f8; 28 | } 29 | 30 | .textbox { 31 | background:#ffffea; 32 | } 33 | 34 | .header { 35 | text-align:center; 36 | font-weight:bolder; 37 | font-size:12pt; 38 | font-family:arial,verdana; 39 | } 40 | 41 | .sitefooter { 42 | display:flex; 43 | align-items:center; 44 | border-width:1px 0 0 0; 45 | border-style:solid; 46 | position:absolute; 47 | z-index:5; 48 | left:0px; 49 | bottom:0px; 50 | right:0px; 51 | padding:0.5em 0.7em; 52 | } 53 | 54 | .stdbtn, .multibtn { 55 | cursor:pointer; 56 | background:linear-gradient(to bottom, #eaeaea 14%,#fcfcfc 52%,#e5e5e5 89%); 57 | border-width:1px; 58 | border-style:solid; 59 | border-color:#a9a9a9; 60 | border-radius:5pt; 61 | padding:3pt 10pt; 62 | } 63 | 64 | .sigparmbox { 65 | border-radius:5pt; 66 | padding:0 5pt 5pt 5pt; 67 | } 68 | 69 | .sigparmhead { 70 | border-radius:3pt; 71 | padding:3pt 5pt; 72 | z-index:5; 73 | position:relative; 74 | top:-10pt; 75 | margin-bottom:-3pt; 76 | } 77 | 78 | .defbtn { 79 | border-radius:2pt; 80 | display:inline-block; 81 | cursor:pointer; 82 | padding:1pt 3pt; 83 | background-color:#f0f5fd; 84 | border-color:#2a53ea; 85 | } 86 | 87 | .sigparmbox, .sigparmhead, .defbtn { 88 | border-width:1px; 89 | border-style:solid; 90 | } 91 | 92 | .sigparmhead, .sitefooter { 93 | border-color:#c85000; 94 | background-color:#fffcfc; 95 | } 96 | 97 | .sigparmbox { 98 | border-color:#008040; 99 | background-color:#fbfff7; 100 | } 101 | 102 | .stdbtn, .multibtn, .sigparmhead { 103 | font-family:Arial,'Liberation Sans',Verdana,'Bitstream Vera Sans','DejaVu Sans'; 104 | font-size:10pt; 105 | } 106 | 107 | .stdbtn, .multibtn, .sigparmbox, .staticbox, .textbox { 108 | box-shadow:3pt 3pt 3pt #d0d0d0; 109 | } 110 | 111 | .stdbtn { 112 | display:inline-block; 113 | } 114 | 115 | .stdbtn { 116 | margin-top:15pt; 117 | } 118 | 119 | .multibtn { 120 | margin-top:12pt; 121 | text-align:center; 122 | } 123 | 124 | @media (max-width:768px) { 125 | 126 | .stdbtn, .multibtn, .sigparmbox, .staticbox, .textbox { 127 | box-shadow:2pt 2pt 2pt #d0d0d0; 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreq/HomeServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 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 - 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. 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 | -------------------------------------------------------------------------------- /shreq/org/webpki/shreq/URIRequestValidation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.shreq; 18 | 19 | import java.io.IOException; 20 | 21 | import java.security.GeneralSecurityException; 22 | 23 | import java.util.Arrays; 24 | import java.util.LinkedHashMap; 25 | 26 | import org.webpki.json.JSONParser; 27 | 28 | public class URIRequestValidation extends ValidationCore { 29 | 30 | static final String QUERY_STRING = SHREQSupport.SHREQ_JWS_QUERY_LABEL + "="; 31 | static final int QUERY_LENGTH = QUERY_STRING.length(); 32 | 33 | public URIRequestValidation(String targetUri, 34 | String targetMethod, 35 | LinkedHashMap headerMap) throws IOException { 36 | super(targetUri, targetMethod, headerMap); 37 | } 38 | 39 | @Override 40 | protected void validateImplementation() throws IOException, 41 | GeneralSecurityException { 42 | int i = normalizedTargetUri.indexOf(QUERY_STRING); 43 | if (i < 10) { 44 | error("URI lacks a signature ( " + QUERY_STRING + " ) element"); 45 | } 46 | int next = normalizedTargetUri.indexOf('&', i); 47 | String jwsString; 48 | if (next < 0) { 49 | jwsString = normalizedTargetUri.substring(i + QUERY_LENGTH); 50 | normalizedTargetUri = normalizedTargetUri.substring(0, i - 1); 51 | } else { 52 | jwsString = normalizedTargetUri.substring(i + QUERY_LENGTH, next); 53 | normalizedTargetUri = 54 | normalizedTargetUri.substring(0, i) + normalizedTargetUri.substring(next + 1); 55 | } 56 | 57 | decodeJwsString(jwsString, false); 58 | secinf = commonDataFilter(JSONParser.parse(jwsPayload), true); 59 | 60 | if (!Arrays.equals(secinf.getBinary(SHREQSupport.SHREQ_HASHED_TARGET_URI), 61 | getDigest(normalizedTargetUri))) { 62 | error("URI mismatch. Normalized URI: " + normalizedTargetUri); 63 | } 64 | } 65 | 66 | @Override 67 | protected String defaultMethod() { 68 | return SHREQSupport.SHREQ_DEFAULT_URI_METHOD; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /shreq/org/webpki/shreq/JSONRequestValidation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.shreq; 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.JSONObjectWriter; 27 | import org.webpki.json.JSONOutputFormats; 28 | 29 | public class JSONRequestValidation extends ValidationCore { 30 | 31 | JSONObjectReader message; // "message" in the specification 32 | 33 | public JSONRequestValidation(String targetUri, 34 | String targetMethod, 35 | LinkedHashMap headerMap, 36 | JSONObjectReader message) throws IOException { 37 | super(targetUri, targetMethod, headerMap); 38 | this.message = message; 39 | } 40 | 41 | @Override 42 | protected void validateImplementation() throws IOException, 43 | GeneralSecurityException { 44 | JSONObjectReader temp = message.getObject(SHREQSupport.SHREQ_SECINF_LABEL); 45 | String jwsString = temp.getString(SHREQSupport.SHREQ_JWS_STRING); 46 | decodeJwsString(jwsString, true); 47 | 48 | secinf = commonDataFilter(temp, false); 49 | 50 | String normalizedURI = secinf.getString(SHREQSupport.SHREQ_TARGET_URI); 51 | if (!normalizedURI.equals(normalizedTargetUri)) { 52 | error("Declared URI=" + normalizedURI + " Actual URI=" + normalizedTargetUri); 53 | } 54 | 55 | // All but the signature element is signed 56 | secinf.removeProperty(SHREQSupport.SHREQ_JWS_STRING); 57 | 58 | jwsPayload = message.serializeToBytes(JSONOutputFormats.CANONICALIZED); 59 | 60 | // However, be nice and restore the signature element after canonicalization 61 | JSONObjectWriter msg = new JSONObjectWriter(secinf); 62 | msg.setupForRewrite(SHREQSupport.SHREQ_JWS_STRING); 63 | msg.setString(SHREQSupport.SHREQ_JWS_STRING, jwsString); 64 | secinf.scanAway(SHREQSupport.SHREQ_JWS_STRING); 65 | } 66 | 67 | @Override 68 | protected String defaultMethod() { 69 | return SHREQSupport.SHREQ_DEFAULT_JSON_METHOD; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreq/CurlServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 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/json" + 47 | " -d "" + sampleJsonRequest_CURL + "" " + 48 | sampleJsonRequestUri, 49 | "JSON based POST request accessed through CURL")) 50 | 51 | .append( 52 | HTML.fancyBox("jsonrequest2", "curl" + 53 | " -X PUT" + 54 | " -H x-debug:full" + 55 | " -H content-type:application/json" + 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 | Hash algorithm override 9 | hash_algorithm 10 | @hash-algorithm@ 11 | 12 | 13 | 14 | Logging flag 15 | logging 16 | @logging@ 17 | 18 | 19 | 20 | org.webpki.webapps.shreq.SHREQService 21 | 22 | 23 | 24 | HomeServlet 25 | org.webpki.webapps.shreq.HomeServlet 26 | 27 | 28 | 29 | CreateServlet 30 | org.webpki.webapps.shreq.CreateServlet 31 | 32 | 33 | 34 | ValidateServlet 35 | org.webpki.webapps.shreq.ValidateServlet 36 | 37 | 38 | 39 | CurlServlet 40 | org.webpki.webapps.shreq.CurlServlet 41 | 42 | 43 | 44 | ExtConfReqServlet 45 | org.webpki.webapps.shreq.ExtConfReqServlet 46 | 47 | 48 | 49 | ExtConfReq2Servlet 50 | org.webpki.webapps.shreq.ExtConfReq2Servlet 51 | 52 | 53 | 54 | PreConfReqServlet 55 | org.webpki.webapps.shreq.PreConfReqServlet 56 | 57 | 58 | 59 | PreConfReq2Servlet 60 | org.webpki.webapps.shreq.PreConfReq2Servlet 61 | 62 | 63 | 64 | HomeServlet 65 | /home 66 | 67 | 68 | 69 | CreateServlet 70 | /create 71 | 72 | 73 | 74 | ValidateServlet 75 | /validate 76 | 77 | 78 | 79 | CurlServlet 80 | /curl 81 | 82 | 83 | 84 | ExtConfReqServlet 85 | /extconfreq 86 | 87 | 88 | 89 | ExtConfReqServlet 90 | /extconfreq/* 91 | 92 | 93 | 94 | ExtConfReq2Servlet 95 | /extconfreq2 96 | 97 | 98 | 99 | ExtConfReq2Servlet 100 | /extconfreq2/* 101 | 102 | 103 | 104 | PreConfReqServlet 105 | /preconfreq 106 | 107 | 108 | 109 | PreConfReqServlet 110 | /preconfreq/* 111 | 112 | 113 | 114 | PreConfReq2Servlet 115 | /preconfreq2 116 | 117 | 118 | 119 | PreConfReq2Servlet 120 | /preconfreq2/* 121 | 122 | 123 | 124 | 125 | The app 126 | /* 127 | 128 | 129 | CONFIDENTIAL 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreq/JSONTokenExtractor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 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/shreq/HTML.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 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/shreq/SHREQService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 18 | 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | 22 | import java.security.GeneralSecurityException; 23 | import java.security.KeyPair; 24 | import java.security.KeyStore; 25 | import java.security.cert.X509Certificate; 26 | import java.util.LinkedHashMap; 27 | 28 | import java.util.logging.Level; 29 | import java.util.logging.Logger; 30 | 31 | import javax.servlet.ServletContextEvent; 32 | import javax.servlet.ServletContextListener; 33 | 34 | import org.webpki.crypto.AlgorithmPreferences; 35 | import org.webpki.crypto.AsymSignatureAlgorithms; 36 | import org.webpki.crypto.CustomCryptoProvider; 37 | import org.webpki.crypto.HmacAlgorithms; 38 | import org.webpki.crypto.SignatureAlgorithms; 39 | import org.webpki.crypto.KeyStoreVerifier; 40 | 41 | import org.webpki.shreq.SHREQSupport; 42 | 43 | import org.webpki.util.HexaDecimal; 44 | import org.webpki.util.IO; 45 | import org.webpki.util.PEMDecoder; 46 | 47 | import org.webpki.webutil.InitPropertyReader; 48 | 49 | public class SHREQService extends InitPropertyReader implements ServletContextListener { 50 | 51 | static Logger logger = Logger.getLogger(SHREQService.class.getName()); 52 | 53 | static String sampleKey; 54 | 55 | static String keyDeclarations; 56 | 57 | static KeyStoreVerifier certificateVerifier; 58 | 59 | static boolean logging; 60 | 61 | static LinkedHashMap predefinedSecretKeys = new LinkedHashMap(); 62 | 63 | static LinkedHashMap predefinedKeyPairs = new LinkedHashMap(); 64 | 65 | static final String BOUNCYCASTLE = "bouncycastle_first"; 66 | 67 | class KeyDeclaration { 68 | 69 | static final String PRIVATE_KEYS = "privateKeys"; 70 | static final String SECRET_KEYS = "secretKeys"; 71 | static final String CERTIFICATES = "certificates"; 72 | 73 | StringBuilder decl = new StringBuilder("var "); 74 | StringBuilder after = new StringBuilder(); 75 | String name; 76 | String last; 77 | String base; 78 | 79 | KeyDeclaration(String name, String base) { 80 | this.name = name; 81 | this.base = base; 82 | decl.append(name) 83 | .append(" = {"); 84 | } 85 | 86 | KeyDeclaration addKey(SignatureAlgorithms alg, String fileOrNull) throws IOException, 87 | GeneralSecurityException { 88 | String algId = alg.getAlgorithmId(AlgorithmPreferences.JOSE); 89 | if (name.equals(PRIVATE_KEYS)) { 90 | if (fileOrNull == null) { 91 | predefinedKeyPairs.put(algId, predefinedKeyPairs.get(last)); 92 | } else { 93 | predefinedKeyPairs.put(algId, 94 | PEMDecoder.getKeyPair(getEmbeddedResourceBinary(fileOrNull + base))); 95 | } 96 | } else if (name.equals(SECRET_KEYS)) { 97 | predefinedSecretKeys.put(algId, 98 | HexaDecimal.decode(getEmbeddedResourceString(fileOrNull + base).trim())); 99 | } 100 | if (fileOrNull == null) { 101 | after.append(name) 102 | .append('.') 103 | .append(algId) 104 | .append(" = ") 105 | .append(name) 106 | .append('.') 107 | .append(last) 108 | .append(";\n"); 109 | 110 | } else { 111 | if (last != null) { 112 | decl.append(','); 113 | } 114 | decl.append("\n ") 115 | .append(algId) 116 | .append(": '") 117 | .append(HTML.javaScript(getEmbeddedResourceString(fileOrNull + base).trim())) 118 | .append('\''); 119 | last = algId; 120 | } 121 | return this; 122 | } 123 | 124 | public String toString() { 125 | return decl.append("\n};\n").append(after).toString(); 126 | } 127 | } 128 | 129 | InputStream getResource(String name) throws IOException { 130 | InputStream is = this.getClass().getResourceAsStream(name); 131 | if (is == null) { 132 | throw new IOException("Resource fail for: " + name); 133 | } 134 | return is; 135 | } 136 | 137 | byte[] getEmbeddedResourceBinary(String name) throws IOException { 138 | return IO.getByteArrayFromInputStream(getResource(name)); 139 | } 140 | 141 | String getEmbeddedResourceString(String name) throws IOException { 142 | return new String(getEmbeddedResourceBinary(name), "utf-8"); 143 | } 144 | 145 | @Override 146 | public void contextDestroyed(ServletContextEvent event) { 147 | } 148 | 149 | @Override 150 | public void contextInitialized(ServletContextEvent event) { 151 | initProperties(event); 152 | CustomCryptoProvider.forcedLoad(false); 153 | try { 154 | //=========================================================================================// 155 | // Sample key for verification 156 | //=========================================================================================// 157 | sampleKey = getEmbeddedResourceString("p256publickey.pem").trim(); 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(HmacAlgorithms.HMAC_SHA256, "a256") 179 | .addKey(HmacAlgorithms.HMAC_SHA384, "a384") 180 | .addKey(HmacAlgorithms.HMAC_SHA512, "a512").toString(); 181 | 182 | KeyStore keyStore = KeyStore.getInstance("PKCS12"); 183 | keyStore.load(null, null); 184 | X509Certificate[] path = PEMDecoder.getCertificatePath(getEmbeddedResourceBinary("rootca.pem")); 185 | keyStore.setCertificateEntry("mykey", path[path.length - 1]); 186 | certificateVerifier = new KeyStoreVerifier(keyStore); 187 | 188 | //=========================================================================================// 189 | // Hash algorithm override? 190 | //=========================================================================================// 191 | String algorithmId = getPropertyString("hash_algorithm"); 192 | if (algorithmId.length() > 0) { 193 | SHREQSupport.getHashAlgorithm(algorithmId); 194 | SHREQSupport.overridedHashAlgorithm = algorithmId; 195 | logger.info("Hash OVERRIDE MODE"); 196 | } 197 | 198 | //=========================================================================================// 199 | // Logging? 200 | //=========================================================================================// 201 | logging = getPropertyBoolean("logging"); 202 | 203 | logger.info("SHREQ Demo Successfully Initiated"); 204 | } catch (Exception e) { 205 | logger.log(Level.SEVERE, "********\n" + e.getMessage() + "\n********", e); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreq/BaseRequestServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 18 | 19 | import java.io.IOException; 20 | 21 | import java.security.GeneralSecurityException; 22 | import java.security.PublicKey; 23 | 24 | import java.text.SimpleDateFormat; 25 | 26 | import java.util.Enumeration; 27 | import java.util.GregorianCalendar; 28 | import java.util.LinkedHashMap; 29 | import java.util.TimeZone; 30 | 31 | import java.util.logging.Logger; 32 | 33 | import javax.servlet.ServletException; 34 | 35 | import javax.servlet.http.HttpServlet; 36 | import javax.servlet.http.HttpServletRequest; 37 | import javax.servlet.http.HttpServletResponse; 38 | 39 | import org.webpki.crypto.AlgorithmPreferences; 40 | import org.webpki.crypto.SignatureAlgorithms; 41 | 42 | import org.webpki.jose.jws.JWSAsymSignatureValidator; 43 | import org.webpki.jose.jws.JWSHmacValidator; 44 | import org.webpki.jose.jws.JWSValidator; 45 | 46 | import org.webpki.json.JSONParser; 47 | 48 | import org.webpki.shreq.JSONRequestValidation; 49 | import org.webpki.shreq.URIRequestValidation; 50 | import org.webpki.shreq.ValidationCore; 51 | import org.webpki.shreq.ValidationKeyService; 52 | 53 | import org.webpki.util.HexaDecimal; 54 | 55 | import org.webpki.webutil.ServletUtil; 56 | 57 | public abstract class BaseRequestServlet extends HttpServlet implements ValidationKeyService { 58 | 59 | private static final long serialVersionUID = 1L; 60 | 61 | private static final String CONTENT_TYPE = "Content-Type"; 62 | private static final String CONTENT_LENGTH = "Content-Length"; 63 | 64 | private static final String JSON_CONTENT = "application/json"; 65 | 66 | static final String EXTCONFREQ = "/extconfreq"; 67 | static final String PRECONFREQ = "/preconfreq"; 68 | static final String EXTCONFREQ2 = "/extconfreq2"; 69 | static final String PRECONFREQ2 = "/preconfreq2"; 70 | 71 | static final int TIME_STAMP_TOLERANCE = 300000; // Milliseconds 72 | 73 | static Logger logger = Logger.getLogger(BaseRequestServlet.class.getName()); 74 | 75 | protected abstract boolean externallyConfigured(); 76 | 77 | static String getFormattedUTCTime(GregorianCalendar dateTime) { 78 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss 'UTC'"); 79 | sdf.setTimeZone(TimeZone.getTimeZone("UTC")); 80 | return sdf.format(dateTime.getTime()); 81 | } 82 | 83 | static String getStackTrace(Exception e) { 84 | StringBuilder error = new StringBuilder("Stack trace:\n") 85 | .append(e.getClass().getName()) 86 | .append(": ") 87 | .append(e.getMessage()); 88 | StackTraceElement[] st = e.getStackTrace(); 89 | int length = st.length; 90 | if (length > 20) { 91 | length = 20; 92 | } 93 | for (int i = 0; i < length; i++) { 94 | String entry = st[i].toString(); 95 | error.append("\n at " + entry); 96 | if (entry.contains("HttpServlet")) { 97 | break; 98 | } 99 | } 100 | return error.toString(); 101 | } 102 | 103 | static String getUrlFromRequest(HttpServletRequest request) { 104 | return request.getScheme() + 105 | "://" + 106 | request.getServerName() + 107 | ":" + 108 | request.getServerPort() + 109 | request.getRequestURI() + 110 | (request.getQueryString() == null ? 111 | "" : "?" + request.getQueryString()); 112 | } 113 | 114 | protected boolean enforceTimeStamp() { 115 | return false; 116 | } 117 | 118 | private void returnResponse(HttpServletResponse response, int status, String text) throws IOException { 119 | response.resetBuffer(); 120 | response.setStatus(status); 121 | response.getOutputStream().write(text.getBytes("utf-8"));; 122 | response.setHeader(CONTENT_TYPE, "text/plain;utf-8"); 123 | response.flushBuffer(); 124 | } 125 | 126 | @Override 127 | public void service(HttpServletRequest request, HttpServletResponse response) 128 | throws IOException, ServletException { 129 | ValidationCore validationCore = null; 130 | 131 | // Get the Target Method (4.2:1 , 5.2:1) 132 | String targetMethod = request.getMethod(); 133 | 134 | // Recreate the Target URI (4.2:2 , 5.2:2) 135 | String targetUri = getUrlFromRequest(request); 136 | 137 | // Collect HTTP Headers 138 | Enumeration headerNames = request.getHeaderNames(); 139 | 140 | try { 141 | LinkedHashMap headerMap = new LinkedHashMap(); 142 | while (headerNames.hasMoreElements()) { 143 | String headerName = headerNames.nextElement().toLowerCase(); 144 | Enumeration headerValues = request.getHeaders(headerName); 145 | boolean next = false; 146 | do { 147 | String headerValue = headerValues.nextElement().trim(); 148 | if (next) { 149 | headerMap.put(headerName, headerMap.get(headerName) + ", " + headerValue); 150 | } else { 151 | headerMap.put(headerName, headerValue); 152 | next = true; 153 | } 154 | } while (headerValues.hasMoreElements()); 155 | } 156 | 157 | // 3. Determining Request Type 158 | if (request.getHeader(CONTENT_LENGTH) == null) { 159 | 160 | // 5.2 URI Request 161 | if (request.getHeader(CONTENT_TYPE) != null) { 162 | throw new IOException("Unexpected: " + CONTENT_TYPE); 163 | } 164 | validationCore = new URIRequestValidation(targetUri, 165 | targetMethod, 166 | headerMap); 167 | } else { 168 | 169 | // 4.2 JSON Request 170 | if (!JSON_CONTENT.equals(request.getHeader(CONTENT_TYPE))) { 171 | throw new IOException(CONTENT_TYPE + 172 | "=" + 173 | request.getHeader(CONTENT_TYPE) + 174 | " must be=" + 175 | JSON_CONTENT); 176 | } 177 | validationCore = new JSONRequestValidation(targetUri, 178 | targetMethod, 179 | headerMap, 180 | JSONParser.parse(ServletUtil.getData(request))); 181 | } 182 | 183 | // Core Request Data Successfully Collected - Validate! 184 | validationCore.validate(this); 185 | 186 | // In *this* service we don't accept any unread/unused JWS header variables 187 | validationCore.getJwsProtectedHeader().checkForUnread(); 188 | 189 | // Optional test 190 | if (enforceTimeStamp()) { 191 | validationCore.enforceTimeStamp(TIME_STAMP_TOLERANCE, TIME_STAMP_TOLERANCE); 192 | } 193 | 194 | // No exceptions => We did it! 195 | returnResponse(response, HttpServletResponse.SC_OK, 196 | "\n" + 197 | " |============================================|\n" + 198 | " | SUCCESSFUL REQUEST " + 199 | getFormattedUTCTime(new GregorianCalendar()) + " |\n" + 200 | " |============================================|\n" + 201 | validationCore.printCoreData()); 202 | 203 | 204 | } catch (Exception e) { 205 | 206 | // Houston, we got a problem... 207 | returnResponse(response, HttpServletResponse.SC_BAD_REQUEST, 208 | "\n" + 209 | " *************\n" + 210 | " * E R R O R *\n" + 211 | " *************\n" + 212 | getStackTrace(e) + (validationCore == null ? 213 | "\nValidation context not available\n" : validationCore.printCoreData())); 214 | } 215 | } 216 | 217 | void extConfError() throws IOException { 218 | throw new IOException("'" + 219 | EXTCONFREQ + 220 | "' only supports requests with in-lined asymmetric JWKs and X5Cs"); 221 | } 222 | 223 | @Override 224 | public JWSValidator getSignatureValidator(ValidationCore validationCore, 225 | SignatureAlgorithms signatureAlgorithm, 226 | PublicKey publicKey, 227 | String keyId) 228 | throws IOException, GeneralSecurityException { 229 | if (signatureAlgorithm.isSymmetric()) { 230 | if (externallyConfigured()) { 231 | extConfError(); 232 | } 233 | byte[] secretKey = SHREQService.predefinedSecretKeys 234 | .get(signatureAlgorithm.getAlgorithmId(AlgorithmPreferences.JOSE)); 235 | validationCore.setCookie(HexaDecimal.encode(secretKey)); 236 | return new JWSHmacValidator(secretKey); 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 JWSAsymSignatureValidator(validationKey); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /shreq/org/webpki/shreq/SHREQSupport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.shreq; 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 | -------------------------------------------------------------------------------- /test/org/webpki/shreq/TestVectors.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.shreq; 18 | 19 | import java.io.File; 20 | import java.io.IOException; 21 | 22 | import java.security.KeyPair; 23 | 24 | import java.util.GregorianCalendar; 25 | import java.util.LinkedHashMap; 26 | 27 | import org.webpki.crypto.AlgorithmPreferences; 28 | import org.webpki.crypto.AsymSignatureAlgorithms; 29 | import org.webpki.crypto.HmacAlgorithms; 30 | import org.webpki.crypto.SignatureAlgorithms; 31 | 32 | import org.webpki.json.JSONObjectReader; 33 | import org.webpki.json.JSONObjectWriter; 34 | import org.webpki.json.JSONOutputFormats; 35 | import org.webpki.json.JSONParser; 36 | 37 | import org.webpki.jose.jws.JWSAsymKeySigner; 38 | import org.webpki.jose.jws.JWSHmacSigner; 39 | import org.webpki.jose.jws.JWSSigner; 40 | 41 | import org.webpki.util.Base64; 42 | import org.webpki.util.HexaDecimal; 43 | import org.webpki.util.IO; 44 | import org.webpki.util.ISODateTime; 45 | import org.webpki.util.PEMDecoder; 46 | import org.webpki.util.ArrayUtil; 47 | 48 | public class TestVectors { 49 | 50 | static String keyDirectory; 51 | 52 | static int testVectorNumber; 53 | 54 | static StringBuilder rfcText = new StringBuilder(); 55 | 56 | static final int RFC_ARTWORK_LINE_MAX = 64; 57 | 58 | static class Test { 59 | String uri; 60 | String method; 61 | String optionalJSONBody; 62 | SignatureAlgorithms signatureAlgorithm; 63 | String optionalOverrideHashAlgorithm; 64 | String keyAlgName; 65 | GregorianCalendar optionalTimeStamp; 66 | LinkedHashMap optionalHeaders; 67 | 68 | JWSSigner jwsSigner; 69 | String keyInRFCText; 70 | String keyRFCDescription; 71 | String signatureAlgorithmId; 72 | JSONObjectWriter secinf; 73 | byte[] JWS_Payload; 74 | String signedUri; 75 | 76 | Test(String uri, 77 | String method, 78 | String optionalJSONBody, 79 | SignatureAlgorithms signatureAlgorithm, 80 | String optionalOverrideHashAlgorithm, 81 | String keyAlgName, 82 | GregorianCalendar optionalTimeStamp, 83 | LinkedHashMap optionalHeaders) throws Exception { 84 | this.uri = uri; 85 | this.method = method; 86 | this.optionalJSONBody = optionalJSONBody; 87 | this.signatureAlgorithm = signatureAlgorithm; 88 | this.optionalOverrideHashAlgorithm = optionalOverrideHashAlgorithm; 89 | this.keyAlgName = keyAlgName; 90 | this.optionalTimeStamp = optionalTimeStamp; 91 | this.optionalHeaders = optionalHeaders == null ? 92 | new LinkedHashMap() : optionalHeaders; 93 | createVector(); 94 | } 95 | 96 | String utf8(byte[] data) throws IOException { 97 | return new String(data, "utf-8"); 98 | } 99 | 100 | void createVector() throws Exception { 101 | signatureAlgorithmId = signatureAlgorithm.getAlgorithmId(AlgorithmPreferences.JOSE); 102 | SHREQSupport.overridedHashAlgorithm = optionalOverrideHashAlgorithm; 103 | if (signatureAlgorithm.isSymmetric()) { 104 | String keyInHex = utf8(readKey(keyAlgName + "bitkey.hex")); 105 | keyInRFCText = keyInHex; 106 | keyRFCDescription = "Symmetric signature validation key, here in hexadecimal notation:"; 107 | jwsSigner = new JWSHmacSigner(HexaDecimal.decode(keyInHex), 108 | (HmacAlgorithms) signatureAlgorithm); 109 | } else { 110 | KeyPair keyPair = PEMDecoder.getKeyPair(readKey(keyAlgName + "privatekey.pem")); 111 | keyRFCDescription = "Public signature validation key, here in PEM format:"; 112 | keyInRFCText = 113 | "-----BEGIN PUBLIC KEY-----\n" + 114 | Base64.mimeEncode(keyPair.getPublic().getEncoded()) + 115 | "\n-----END PUBLIC KEY-----"; 116 | jwsSigner = new JWSAsymKeySigner(keyPair.getPrivate(), 117 | (AsymSignatureAlgorithms) signatureAlgorithm); 118 | } 119 | 120 | secinf = new JSONObjectWriter(); 121 | if (optionalJSONBody == null) { 122 | uriRequest(); 123 | } else { 124 | jsonRequest(JSONParser.parse(optionalJSONBody)); 125 | } 126 | rfcText.append("
\n\nTarget URI:\n\n") 135 | .append(artWork(lineCutter(uri))); 136 | 137 | if (optionalJSONBody == null) { 138 | rfcText.append("\nSigned URI:\n\n") 139 | .append(artWork(lineCutter(signedUri))) 140 | .append("\nDecoded JWS Payload:\n\n") 141 | .append(artWork(lineCutter(secinf.serializeToString(JSONOutputFormats.PRETTY_PRINT)))); 142 | } else { 143 | rfcText.append("\nJSON Body:\n\n") 144 | .append(artWork(lineCutter(optionalJSONBody))); 145 | } 146 | if (optionalOverrideHashAlgorithm != null) { 147 | rfcText.append("\nNote the overridden hash algorithm.\n\n"); 148 | } 149 | if (!optionalHeaders.isEmpty()) { 150 | StringBuilder headers = new StringBuilder(); 151 | for (String header : optionalHeaders.keySet()) { 152 | headers.append(header) 153 | .append(": ") 154 | .append(optionalHeaders.get(header)) 155 | .append('\n'); 156 | } 157 | rfcText.append("\nRequired HTTP Headers:\n\n") 158 | .append(artWork(headers.toString().trim())); 159 | } 160 | rfcText.append("\n") 161 | .append(keyRFCDescription) 162 | .append("\n\n") 163 | .append(artWork(keyInRFCText)) 164 | .append("
\n"); 165 | } 166 | 167 | String lineCutter(String string) { 168 | int position = 0; 169 | StringBuilder cutted = new StringBuilder(); 170 | for (char c : string.toCharArray()) { 171 | if (c == '\n') { 172 | position = 0; 173 | } else if (position++ == RFC_ARTWORK_LINE_MAX) { 174 | cutted.append('\n'); 175 | position = 1; 176 | } 177 | cutted.append(c); 178 | } 179 | return cutted.toString().trim(); 180 | } 181 | 182 | StringBuilder artWork(String string) { 183 | StringBuilder total = new StringBuilder("
\n"); 191 | } 192 | 193 | void jsonRequest(JSONObjectReader message) throws Exception { 194 | secinf = SHREQSupport.createJSONRequestSecInf(uri, 195 | method, 196 | optionalTimeStamp, 197 | optionalHeaders, 198 | signatureAlgorithm); 199 | JSONObjectWriter writer = new JSONObjectWriter(message); 200 | writer.setObject(SHREQSupport.SHREQ_SECINF_LABEL, secinf); 201 | JWS_Payload = writer.serializeToBytes(JSONOutputFormats.CANONICALIZED); 202 | String jwsString = jwsSigner.sign(JWS_Payload, true); 203 | // Create the completed object which now is in "writer" 204 | secinf.setString(SHREQSupport.SHREQ_JWS_STRING, jwsString); 205 | optionalJSONBody = writer.serializeToString(JSONOutputFormats.PRETTY_PRINT); 206 | } 207 | 208 | void uriRequest() throws Exception { 209 | secinf = SHREQSupport.createURIRequestSecInf(uri, 210 | method, 211 | optionalTimeStamp, 212 | optionalHeaders, 213 | signatureAlgorithm); 214 | String jwsString = jwsSigner.sign( 215 | secinf.serializeToBytes(JSONOutputFormats.NORMALIZED), false); 216 | signedUri = SHREQSupport.addJwsToTargetUri(uri, jwsString); 217 | } 218 | } 219 | 220 | public static void main(String[] argv) { 221 | try { 222 | SHREQSupport.useDefaultForMethod = true; 223 | keyDirectory = argv[0]; 224 | GregorianCalendar frozen = 225 | ISODateTime.parseDateTime("2019-03-07T09:45:00Z", 226 | ISODateTime.UTC_NO_SUBSECONDS); 227 | String john = "{\"name\":\"John Doe\", \"profession\":\"Unknown\"}"; 228 | String jane = "{\"name\":\"Jane Smith\", \"profession\":\"Hacker\"}"; 229 | LinkedHashMap header = new LinkedHashMap(); 230 | header.put("x-debug", "full"); 231 | 232 | /* 233 | String uri 234 | String method 235 | String optionalJSONBody 236 | SignatureAlgorithms signatureAlgorithm 237 | String optionalOverrideHashAlgorithm 238 | String keyAlgName 239 | GregorianCalendar optionalTimeStamp 240 | LinkedHashMap optionalHeaders 241 | */ 242 | new Test( 243 | "https://example.com/users/456", 244 | "GET", 245 | null, 246 | HmacAlgorithms.HMAC_SHA256, 247 | null, 248 | "a256", 249 | frozen, 250 | null); 251 | 252 | new Test( 253 | "https://example.com/users", 254 | "POST", 255 | john, 256 | AsymSignatureAlgorithms.ECDSA_SHA256, 257 | null, 258 | "P256", 259 | frozen, 260 | null); 261 | 262 | new Test( 263 | "https://example.com/users/456", 264 | "PUT", 265 | jane, 266 | AsymSignatureAlgorithms.ECDSA_SHA256, 267 | null, 268 | "P256", 269 | frozen, 270 | null); 271 | 272 | new Test( 273 | "https://example.com/users/456", 274 | "DELETE", 275 | null, 276 | AsymSignatureAlgorithms.RSA_SHA256, 277 | "S512", 278 | "R2048", 279 | frozen, 280 | header); 281 | 282 | IO.writeFile(argv[1], rfcText.toString()); 283 | } catch (Exception e) { 284 | e.printStackTrace(); 285 | } 286 | } 287 | 288 | static byte[] readKey(String filename) throws IOException { 289 | return IO.readFile(keyDirectory + File.separator + filename); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /shreq/org/webpki/shreq/ValidationCore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.shreq; 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.Arrays; 27 | import java.util.GregorianCalendar; 28 | import java.util.HashSet; 29 | import java.util.LinkedHashMap; 30 | 31 | import java.util.logging.Logger; 32 | 33 | import org.webpki.crypto.AlgorithmPreferences; 34 | import org.webpki.crypto.AsymSignatureAlgorithms; 35 | import org.webpki.crypto.HashAlgorithms; 36 | import org.webpki.crypto.HmacAlgorithms; 37 | import org.webpki.crypto.SignatureAlgorithms; 38 | 39 | import org.webpki.jose.JOSEKeyWords; 40 | 41 | import org.webpki.jose.jws.JWSDecoder; 42 | 43 | import org.webpki.json.JSONArrayReader; 44 | import org.webpki.json.JSONObjectReader; 45 | import org.webpki.json.JSONParser; 46 | 47 | import org.webpki.util.Base64URL; 48 | 49 | public abstract class ValidationCore { 50 | 51 | protected LinkedHashMap headerMap; 52 | 53 | protected String jwsProtectedHeaderB64U; 54 | 55 | protected JSONObjectReader jwsProtectedHeader; 56 | 57 | protected byte[] jwsPayload; 58 | 59 | protected String jwsSignatureB64U; 60 | 61 | private boolean validationMode; 62 | 63 | private ValidationKeyService validationKeyService; 64 | 65 | protected String normalizedTargetUri; 66 | 67 | protected String targetMethod; 68 | 69 | protected GregorianCalendar issuedAt; 70 | 71 | protected SignatureAlgorithms signatureAlgorithm; 72 | 73 | protected HashAlgorithms hashAlgorithm; // For digests 74 | 75 | protected String keyId; 76 | 77 | protected PublicKey publicKey; 78 | 79 | protected X509Certificate[] certificatePath; 80 | 81 | JSONObjectReader secinf; 82 | 83 | JWSDecoder jwsDecoder; 84 | 85 | private boolean detached; 86 | 87 | private Object cookie; 88 | 89 | protected ValidationCore(String targetUri, 90 | String targetMethod, 91 | LinkedHashMap headerMap) throws IOException { 92 | this.normalizedTargetUri = SHREQSupport.normalizeTargetURI(targetUri); 93 | this.headerMap = headerMap; 94 | for (String method : SHREQSupport.HTTP_METHODS) { 95 | if (method.equals(targetMethod)) { 96 | this.targetMethod = targetMethod; 97 | return; 98 | } 99 | } 100 | error("Unsupported method: " + targetMethod); 101 | } 102 | 103 | protected static Logger logger = Logger.getLogger(ValidationCore.class.getName()); 104 | 105 | protected abstract String defaultMethod(); 106 | 107 | protected abstract void validateImplementation() throws IOException, GeneralSecurityException; 108 | 109 | public void setCookie(Object cookie) { 110 | this.cookie = cookie; 111 | } 112 | 113 | public Object getCookie() { 114 | return cookie; 115 | } 116 | 117 | public byte[] getJwsPayload() { 118 | return jwsPayload; 119 | } 120 | 121 | public JSONObjectReader getJwsProtectedHeader() { 122 | return jwsProtectedHeader; 123 | } 124 | 125 | public JSONObjectReader getSHREQRecord() { 126 | return secinf; 127 | } 128 | 129 | public X509Certificate[] getCertificatePath() { 130 | return certificatePath; 131 | } 132 | 133 | public PublicKey getPublicKey() { 134 | return publicKey; 135 | } 136 | 137 | public String getKeyId() { 138 | return keyId; 139 | } 140 | 141 | public SignatureAlgorithms getSignatureAlgorithm() { 142 | return signatureAlgorithm; 143 | } 144 | 145 | public GregorianCalendar getIssuedAt() { 146 | return issuedAt; 147 | } 148 | 149 | public void validate(ValidationKeyService validationKeyService) throws IOException, 150 | GeneralSecurityException { 151 | this.validationKeyService = validationKeyService; 152 | validateImplementation(); 153 | secinf.checkForUnread(); 154 | validateSignature(); 155 | } 156 | 157 | public boolean isValidating() { 158 | return validationMode; 159 | } 160 | 161 | public void enforceTimeStamp(int after, int before) throws IOException { 162 | if (issuedAt == null) { 163 | error("Missing time stamp"); 164 | } 165 | long diff = new GregorianCalendar().getTimeInMillis() - issuedAt.getTimeInMillis(); 166 | if (diff > after || -diff > before) { 167 | error("Time stamp diff (" + diff/1000.0 + " seconds) is outside of limits"); 168 | } 169 | } 170 | 171 | public String printCoreData() throws IOException { 172 | StringBuilder coreData = new StringBuilder("\nReceived Headers:\n") 173 | .append(targetMethod) 174 | .append(' ') 175 | .append(normalizedTargetUri) 176 | .append('\n'); 177 | for (String header : headerMap.keySet()) { 178 | coreData.append(header) 179 | .append(':') 180 | .append(headerMap.get(header)) 181 | .append('\n'); 182 | } 183 | coreData.append("\nJWS Protected Header:\n") 184 | .append(jwsProtectedHeader == null ? 185 | "NOT AVAILABLE\n" : jwsProtectedHeader.toString()) 186 | .append("\n\"" + SHREQSupport.SHREQ_SECINF_LABEL + "\" Data:\n") 187 | .append(secinf == null ? "NOT AVAILABLE\n" : secinf.toString()); 188 | coreData.append("\nValidation Key:\n") 189 | .append(cookie == null ? "NOT AVAILABLE\n" : (String) cookie); 190 | return coreData.append('\n').toString(); 191 | } 192 | 193 | protected void error(String what) throws IOException { 194 | throw new IOException(what); 195 | } 196 | 197 | protected JSONObjectReader commonDataFilter(JSONObjectReader tempSecinf, boolean iatRequired) 198 | throws IOException, GeneralSecurityException { 199 | if (tempSecinf.hasProperty(SHREQSupport.SHREQ_HASH_ALG_OVERRIDE)) { 200 | hashAlgorithm = SHREQSupport.getHashAlgorithm( 201 | tempSecinf.getString(SHREQSupport.SHREQ_HASH_ALG_OVERRIDE)); 202 | } else { 203 | hashAlgorithm = signatureAlgorithm.getDigestAlgorithm(); 204 | } 205 | String method = 206 | tempSecinf.getStringConditional(SHREQSupport.SHREQ_HTTP_METHOD, defaultMethod()); 207 | if (!targetMethod.equals(method)){ 208 | error("Declared Method=" + method + " Actual Method=" + targetMethod); 209 | } 210 | if (iatRequired || tempSecinf.hasProperty(SHREQSupport.SHREQ_ISSUED_AT_TIME)) { 211 | issuedAt = new GregorianCalendar(); 212 | issuedAt.setTimeInMillis( 213 | // Nobody [as far as I can tell] use fractions but JWT say you can... 214 | tempSecinf.getInt53(SHREQSupport.SHREQ_ISSUED_AT_TIME) * 1000); 215 | } 216 | if (tempSecinf.hasProperty(SHREQSupport.SHREQ_HEADER_RECORD)) { 217 | JSONArrayReader array = tempSecinf.getArray(SHREQSupport.SHREQ_HEADER_RECORD); 218 | byte[] headerDigest = array.getBinary(); 219 | String headerList = array.getString(); 220 | if (array.hasMore()) { 221 | error("Excess elements in \"" + SHREQSupport.SHREQ_HEADER_RECORD + "\""); 222 | } 223 | if (!SHREQSupport.HEADER_STRING_ARRAY_SYNTAX.matcher(headerList).matches()) { 224 | error("Syntax error in \"" + SHREQSupport.SHREQ_HEADER_RECORD + "\""); 225 | } 226 | StringBuilder headerBlob = new StringBuilder(); 227 | HashSet checker = new HashSet(); 228 | boolean next = false; 229 | for (String header : headerList.split(",")) { 230 | String argument = headerMap.get(header); 231 | if (argument == null) { 232 | error("Missing header in request: " + header); 233 | } 234 | if (next) { 235 | headerBlob.append('\n'); 236 | } 237 | next = true; 238 | headerBlob.append(header) 239 | .append(':') 240 | .append(SHREQSupport.normalizeHeaderArgument(argument)); 241 | if (!checker.add(header)) { 242 | error("Duplicate header in \"" + SHREQSupport.SHREQ_HEADER_RECORD + "\""); 243 | } 244 | } 245 | if (!Arrays.equals(headerDigest, getDigest(headerBlob.toString()))) { 246 | error("\"" + SHREQSupport.SHREQ_HEADER_RECORD + "\" digest error"); 247 | } 248 | } 249 | return tempSecinf; 250 | } 251 | 252 | protected byte[] getDigest(String data) throws IOException, GeneralSecurityException { 253 | return hashAlgorithm.digest(data.getBytes("utf-8")); 254 | } 255 | 256 | // 6.6 257 | protected void decodeJwsString(String jwsString, boolean detached) throws IOException, 258 | GeneralSecurityException { 259 | this.detached = detached; 260 | jwsDecoder = new JWSDecoder(jwsString); 261 | // :1 262 | int endOfHeader = jwsString.indexOf('.'); 263 | int lastDot = jwsString.lastIndexOf('.'); 264 | if (endOfHeader < 5 || lastDot > jwsString.length() - 5) { 265 | error("JWS syntax, must be Header.[Payload].Signature"); 266 | } 267 | if (detached) { 268 | if (endOfHeader != lastDot - 1) { 269 | error("JWS syntax, must be Header..Signature"); 270 | } 271 | } else { 272 | jwsPayload = Base64URL.decode(jwsString.substring(endOfHeader + 1, lastDot)); 273 | } 274 | // :2 275 | jwsProtectedHeaderB64U = jwsString.substring(0, endOfHeader); 276 | 277 | // :3-4 278 | jwsProtectedHeader = JSONParser.parse(Base64URL.decode(jwsProtectedHeaderB64U)); 279 | 280 | // :5-6 281 | jwsSignatureB64U = jwsString.substring(lastDot + 1); 282 | 283 | // Start decoding the JWS header. Algorithm is the minimum 284 | String algorithmParam = jwsProtectedHeader.getString(JOSEKeyWords.ALG_JSON); 285 | if (algorithmParam.equals(JOSEKeyWords.EdDSA)) { 286 | signatureAlgorithm = jwsSignatureB64U.length() < 100 ? 287 | AsymSignatureAlgorithms.ED25519 : AsymSignatureAlgorithms.ED448; 288 | } else if (algorithmParam.startsWith("HS")) { 289 | signatureAlgorithm = 290 | HmacAlgorithms.getAlgorithmFromId(algorithmParam, 291 | AlgorithmPreferences.JOSE); 292 | } else { 293 | signatureAlgorithm = 294 | AsymSignatureAlgorithms.getAlgorithmFromId(algorithmParam, 295 | AlgorithmPreferences.JOSE); 296 | } 297 | 298 | keyId = jwsDecoder.getOptionalKeyId(); 299 | publicKey = jwsDecoder.getOptionalPublicKey(); 300 | certificatePath = jwsDecoder.getOptionalCertificatePath(); 301 | } 302 | 303 | 304 | // 6.8 305 | protected void validateHeaderDigest(JSONObjectReader headerObject) throws IOException { 306 | error("Not implemented"); 307 | } 308 | 309 | // 6.9 310 | private void validateSignature() throws IOException, GeneralSecurityException { 311 | // 4.2:10 or 5.2:5 312 | 313 | validationMode = true; 314 | 315 | // 6.9:1 316 | // Unused JWS header elements indicate problems... 317 | // Disabled, this is a demo :) 318 | // JWS_Protected_Header.checkForUnread(); 319 | 320 | // 6.9:2-4 321 | if (detached) { 322 | validationKeyService.getSignatureValidator( 323 | this, 324 | signatureAlgorithm, 325 | certificatePath == null ? 326 | publicKey : 327 | certificatePath[0].getPublicKey(), 328 | keyId).validate(jwsDecoder, jwsPayload); 329 | } else { 330 | validationKeyService.getSignatureValidator( 331 | this, 332 | signatureAlgorithm, 333 | certificatePath == null ? 334 | publicKey : 335 | certificatePath[0].getPublicKey(), 336 | keyId).validate(jwsDecoder); 337 | } 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreq/ValidateServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 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 | import java.util.Vector; 28 | 29 | import javax.servlet.ServletException; 30 | 31 | import javax.servlet.http.HttpServletRequest; 32 | import javax.servlet.http.HttpServletResponse; 33 | 34 | import org.webpki.crypto.AlgorithmPreferences; 35 | import org.webpki.crypto.CertificateInfo; 36 | import org.webpki.crypto.SignatureAlgorithms; 37 | 38 | import org.webpki.json.JSONObjectReader; 39 | import org.webpki.json.JSONOutputFormats; 40 | import org.webpki.json.JSONParser; 41 | 42 | import org.webpki.jose.jws.JWSAsymSignatureValidator; 43 | import org.webpki.jose.jws.JWSHmacValidator; 44 | import org.webpki.jose.jws.JWSValidator; 45 | 46 | import org.webpki.shreq.JSONRequestValidation; 47 | import org.webpki.shreq.URIRequestValidation; 48 | import org.webpki.shreq.ValidationCore; 49 | import org.webpki.shreq.ValidationKeyService; 50 | 51 | import org.webpki.util.HexaDecimal; 52 | import org.webpki.util.PEMDecoder; 53 | 54 | public class ValidateServlet extends BaseGuiServlet implements ValidationKeyService { 55 | 56 | private static final long serialVersionUID = 1L; 57 | 58 | public void doPost(HttpServletRequest request, HttpServletResponse response) 59 | throws IOException, ServletException { 60 | try { 61 | request.setCharacterEncoding("utf-8"); 62 | if (!request.getContentType().startsWith("application/x-www-form-urlencoded")) { 63 | throw new IOException("Unexpected MIME type:" + request.getContentType()); 64 | } 65 | // Get the two input data items 66 | String targetUri = getParameter(request, TARGET_URI); 67 | String signedJsonObject = getParameter(request, JSON_PAYLOAD); 68 | boolean jsonRequest = Boolean.valueOf(getParameter(request, REQUEST_TYPE)); 69 | LinkedHashMap httpHeaderData = createHeaderData(getParameter(request, TXT_OPT_HEADERS)); 70 | String validationKey = getParameter(request, JWS_VALIDATION_KEY); 71 | String targetMethod = getParameter(request, PRM_HTTP_METHOD); 72 | ValidationCore validationCore = null; 73 | 74 | // Determining Request Type 75 | if (jsonRequest) { 76 | JSONObjectReader parsedObject = JSONParser.parse(signedJsonObject); 77 | // Create a pretty-printed JSON object without canonicalization 78 | String prettySignature = 79 | parsedObject.serializeToString(JSONOutputFormats.PRETTY_HTML); 80 | Vector tokens = 81 | new JSONTokenExtractor().getTokens(signedJsonObject); 82 | int fromIndex = 0; 83 | for (String token : tokens) { 84 | int start = prettySignature.indexOf("", start); 86 | // 87 | prettySignature = 88 | prettySignature.substring(0, 89 | start + 28) + 90 | token + 91 | prettySignature.substring(stop); 92 | fromIndex = start + 1; 93 | } 94 | signedJsonObject = prettySignature; 95 | validationCore = new JSONRequestValidation(targetUri, 96 | targetMethod, 97 | httpHeaderData, 98 | parsedObject); 99 | } else { 100 | validationCore = new URIRequestValidation(targetUri, 101 | targetMethod, 102 | httpHeaderData); 103 | } 104 | 105 | // Now assign the key 106 | boolean jwkValidationKey = validationKey.startsWith("{"); 107 | validationCore.setCookie(jwkValidationKey ? 108 | JSONParser.parse(validationKey).getCorePublicKey(AlgorithmPreferences.JOSE) 109 | : 110 | validationKey.contains("-----") ? 111 | PEMDecoder.getPublicKey(validationKey.getBytes("utf-8")) : 112 | HexaDecimal.decode(validationKey)); 113 | 114 | 115 | // Core Request Data Successfully Collected - Validate! 116 | validationCore.validate(this); 117 | 118 | // Parse the JSON data 119 | 120 | StringBuilder html = new StringBuilder( 121 | "
Request Successfully Validated
") 122 | .append(HTML.fancyBox("targeturi", targetUri, 123 | "Target URI to be accessed by an HTTP " + targetMethod + " request")); 124 | if (jsonRequest) { 125 | html.append(HTML.fancyBox("httpjsonbody", signedJsonObject, 126 | "HTTP Body - JSON object signed by an embedded JWS element")); 127 | } 128 | html.append(HTML.fancyBox("jwsheader", 129 | validationCore.getJwsProtectedHeader() 130 | .serializeToString(JSONOutputFormats.PRETTY_HTML), 131 | "Decoded JWS header")) 132 | .append(HTML.fancyBox("vkey", 133 | jwkValidationKey ? 134 | JSONParser.parse(validationKey) 135 | .serializeToString(JSONOutputFormats.PRETTY_HTML) 136 | : 137 | HTML.encode(validationKey).replace("\n", "
"), 138 | "Signature validation " + 139 | (validationCore.getSignatureAlgorithm().isSymmetric() ? 140 | "secret key in hexadecimal" : 141 | "public key in " + 142 | (jwkValidationKey ? "JWK" : "PEM") + 143 | " format"))); 144 | if (jsonRequest) { 145 | html.append(HTML.fancyBox( 146 | "canonical", 147 | HTML.encode(new String(validationCore.getJwsPayload(), "utf-8")), 148 | "Canonical version of the JSON data (what is actually signed) with possible line breaks " + 149 | "for display purposes only")); 150 | } else { 151 | html.append(HTML.fancyBox( 152 | "jwspayload", 153 | JSONParser.parse(validationCore.getJwsPayload()).serializeToString(JSONOutputFormats.PRETTY_HTML), 154 | "Decoded JWS Payload")); 155 | } 156 | if (validationCore.getCertificatePath() != null) { 157 | StringBuilder certificateData = null; 158 | for (X509Certificate certificate : validationCore.getCertificatePath()) { 159 | if (certificateData == null) { 160 | certificateData = new StringBuilder(); 161 | } else { 162 | certificateData.append("
 
"); 163 | } 164 | certificateData.append( 165 | HTML.encode(new CertificateInfo(certificate).toString()) 166 | .replace("\n", "
").replace(" ", "")); 167 | } 168 | html.append(HTML.fancyBox("certpath", 169 | certificateData.toString(), 170 | "Core certificate data")); 171 | } 172 | String time; 173 | if (validationCore.getIssuedAt() == null) { 174 | time = "Request does not contain a time stamp"; 175 | } else { 176 | time = BaseRequestServlet.getFormattedUTCTime(validationCore.getIssuedAt()); 177 | } 178 | html.append(HTML.fancyBox( 179 | "timestamp", 180 | time, 181 | "Time stamp")); 182 | HTML.standardPage(response, null, html.append("
")); 183 | } catch (Exception e) { 184 | HTML.errorPage(response, e); 185 | } 186 | } 187 | 188 | @Override 189 | public JWSValidator getSignatureValidator(ValidationCore validationCore, 190 | SignatureAlgorithms signatureAlgorithm, 191 | PublicKey publicKey, 192 | String keyId) 193 | throws IOException, GeneralSecurityException { 194 | if (signatureAlgorithm.isSymmetric()) { 195 | return new JWSHmacValidator((byte[])validationCore.getCookie()); 196 | } 197 | PublicKey validationKey = (PublicKey)validationCore.getCookie(); 198 | if (publicKey != null && !publicKey.equals(validationKey)) { 199 | throw new GeneralSecurityException("In-lined public key differs from predefined public key"); 200 | } 201 | return new JWSAsymSignatureValidator(validationKey); 202 | } 203 | 204 | 205 | public void doGet(HttpServletRequest request, HttpServletResponse response) 206 | throws IOException, ServletException { 207 | getSampleData(request); 208 | StringBuilder html = new StringBuilder( 209 | "
" + 210 | "
SHREQ Message Validation
") 211 | 212 | .append( 213 | HTML.fancyText( 214 | true, 215 | TARGET_URI, 216 | 1, 217 | HTML.encode(sampleJsonRequestUri), 218 | "Target URI")) 219 | 220 | .append( 221 | HTML.fancyText( 222 | true, 223 | JSON_PAYLOAD, 224 | 10, 225 | "", 226 | "Paste a signed JSON request in the text box or try with the default")) 227 | 228 | .append( 229 | HTML.fancyText( 230 | false, 231 | TXT_OPT_HEADERS, 232 | 4, 233 | "", 234 | "Optional HTTP headers, each on a separate line")) 235 | 236 | .append(getRequestParameters()) 237 | 238 | .append( 239 | HTML.fancyText( 240 | true, 241 | JWS_VALIDATION_KEY, 242 | 4, 243 | HTML.encode(SHREQService.sampleKey), 244 | "Validation key (secret key in hexadecimal or public key in PEM or "plain" JWK format)")) 245 | 246 | .append( 247 | "
" + 248 | "
" + 249 | "Validate Signed Request" + 250 | "
" + 251 | "
" + 252 | "
" + 253 | "
 
"); 254 | 255 | StringBuilder js = new StringBuilder("\"use strict\";\n") 256 | .append( 257 | 258 | "function setUserData(unconditionally) {\n" + 259 | " let element = document.getElementById('" + JSON_PAYLOAD + "').children[1];\n" + 260 | " if (unconditionally || element.value == '') element.value = '") 261 | .append(sampleJsonRequest_JS) 262 | .append("';\n" + 263 | " element = document.getElementById('" + TARGET_URI + "').children[1];\n" + 264 | " if (unconditionally || element.value == '') element.value = '") 265 | .append(sampleJsonRequestUri) 266 | .append("';\n" + 267 | "}\n" + 268 | "function showJson(show) {\n" + 269 | " document.getElementById('" + JSON_PAYLOAD + "').style.display= show ? 'block' : 'none';\n" + 270 | "}\n" + 271 | "function setMethod(method) {\n" + 272 | " let s = document.getElementById('" + PRM_HTTP_METHOD + "');\n" + 273 | " for (let i = 0; i < s.options.length; i++) {\n" + 274 | " if (s.options[i].text == method) {\n" + 275 | " s.options[i].selected = true;\n" + 276 | " break;\n" + 277 | " }\n" + 278 | " }\n" + 279 | "}\n" + 280 | "function showHeaders(show) {\n" + 281 | " document.getElementById('" + TXT_OPT_HEADERS + "').style.display= show ? 'block' : 'none';\n" + 282 | "}\n" + 283 | "function restoreRequestDefaults() {\n" + 284 | " let radioButtons = document.getElementsByName('" + REQUEST_TYPE + "');\n" + 285 | " radioButtons[0].checked = true;\n" + 286 | " requestChange(true);\n" + 287 | " document.getElementById('" + FLG_HEADERS + "').checked = false;\n" + 288 | " showHeaders(false);\n" + 289 | " setUserData(true);\n" + 290 | "}\n" + 291 | "function requestChange(jsonRequest) {\n" + 292 | " document.getElementById('" + JSON_PAYLOAD + "').style.display= jsonRequest ? 'block' : 'none';\n" + 293 | " setMethod(jsonRequest ? '" + DEFAULT_JSON_METHOD + "' : '" + DEFAULT_URI_METHOD + "');\n" + 294 | " let element = document.getElementById('" + TARGET_URI + "').children[1];\n" + 295 | " if (jsonRequest) {\n" + 296 | " if (element.value == '" + sampleUriRequestUri + "') {\n" + 297 | " element.value = '" + sampleJsonRequestUri + "';\n" + 298 | " }\n" + 299 | " } else {\n" + 300 | " if (element.value == '" + sampleJsonRequestUri + "') {\n" + 301 | " element.value = '" + sampleUriRequestUri + "';\n" + 302 | " }\n" + 303 | " }\n" + 304 | "}\n" + 305 | "function headerFlagChange(flag) {\n" + 306 | " showHeaders(flag);\n" + 307 | "}\n" + 308 | "window.addEventListener('load', function(event) {\n" + 309 | " let radioButtons = document.getElementsByName('" + REQUEST_TYPE + "');\n" + 310 | " showJson(radioButtons[0].checked);\n" + 311 | " showHeaders(document.getElementById('" + FLG_HEADERS + "').checked);\n" + 312 | " setUserData(false);\n" + 313 | "});\n"); 314 | HTML.standardPage(response, js.toString(), html); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreq/BaseGuiServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 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.jws.JWSAsymKeySigner; 40 | 41 | import org.webpki.json.JSONObjectWriter; 42 | import org.webpki.json.JSONOutputFormats; 43 | import org.webpki.json.JSONParser; 44 | 45 | import org.webpki.shreq.SHREQSupport; 46 | 47 | import org.webpki.util.Base64; 48 | 49 | public class BaseGuiServlet extends HttpServlet { 50 | 51 | static Logger logger = Logger.getLogger(BaseGuiServlet.class.getName()); 52 | 53 | private static final long serialVersionUID = 1L; 54 | 55 | // HTML form arguments 56 | static final String TARGET_URI = "uri"; 57 | 58 | static final String REQUEST_TYPE = "reqtyp"; // True = JSON else URI 59 | 60 | static final String JSON_PAYLOAD = "json"; 61 | 62 | static final String JWS_VALIDATION_KEY = "vkey"; 63 | 64 | static final String PRM_HTTP_METHOD = "mtd"; 65 | 66 | static final String TXT_OPT_HEADERS = "hdrs"; 67 | 68 | static final String TXT_JWS_EXTRA = "xtra"; 69 | 70 | static final String TXT_SECRET_KEY = "sec"; 71 | 72 | static final String TXT_PRIVATE_KEY = "priv"; 73 | 74 | static final String TXT_CERT_PATH = "cert"; 75 | 76 | static final String PRM_JWS_ALGORITHM = "alg"; 77 | 78 | static final String FLG_CERT_PATH = "cerflg"; 79 | static final String FLG_JWK_INLINE = "jwkflg"; 80 | static final String FLG_DEF_METHOD = "defmtd"; 81 | static final String FLG_IAT_PRESENT = "iatflg"; 82 | static final String FLG_HEADERS = "hdrflg"; 83 | 84 | static final String DEFAULT_ALGORITHM = "ES256"; 85 | static final String DEFAULT_JSON_METHOD = "POST"; 86 | static final String DEFAULT_URI_METHOD = "GET"; 87 | 88 | static class SelectMethod { 89 | 90 | StringBuilder html = new StringBuilder("").toString(); 107 | } 108 | } 109 | 110 | class SelectAlg { 111 | 112 | String preSelected; 113 | StringBuilder html = new StringBuilder("").toString(); 135 | } 136 | } 137 | 138 | StringBuilder checkBox(String idName, String text, boolean checked, String onchange) { 139 | StringBuilder html = new StringBuilder("
") 153 | .append(text) 154 | .append("
"); 155 | return html; 156 | } 157 | 158 | StringBuilder radioButton(String name, String text, String value, boolean checked, String onchange) { 159 | StringBuilder html = new StringBuilder("
") 173 | .append(text) 174 | .append("
"); 175 | return html; 176 | } 177 | 178 | StringBuilder parameterBox(String header, StringBuilder body) { 179 | return new StringBuilder( 180 | "
" + 181 | "
" + 182 | "
" + 183 | "
") 184 | .append(header) 185 | .append( 186 | "
" + 187 | "
") 188 | .append(body) 189 | .append( 190 | "
" + 191 | "
"); 192 | } 193 | 194 | StringBuilder getRequestParameters() { 195 | return parameterBox("Request Parameters", 196 | new StringBuilder() 197 | .append( 198 | "
") 199 | .append(new SelectMethod().toString()) 200 | .append( 201 | "
HTTP Method
" + 202 | "
Restore defaults
") 203 | .append(radioButton(REQUEST_TYPE, "JSON based request", "true", true, "requestChange(true)")) 204 | .append(radioButton(REQUEST_TYPE, "URI based request", "false", false, "requestChange(false)")) 205 | .append(checkBox(FLG_HEADERS, "Include HTTP headers", 206 | false, "headerFlagChange(this.checked)"))); 207 | } 208 | 209 | String getParameter(HttpServletRequest request, String parameter) throws IOException { 210 | String string = request.getParameter(parameter); 211 | if (string == null) { 212 | throw new IOException("Missing data for: "+ parameter); 213 | } 214 | return string.trim(); 215 | } 216 | 217 | byte[] getBinaryParameter(HttpServletRequest request, String parameter) throws IOException { 218 | return getParameter(request, parameter).getBytes("utf-8"); 219 | } 220 | 221 | String getTextArea(HttpServletRequest request, String name) throws IOException { 222 | String string = getParameter(request, name); 223 | StringBuilder s = new StringBuilder(); 224 | for (char c : string.toCharArray()) { 225 | if (c != '\r') { 226 | s.append(c); 227 | } 228 | } 229 | return s.toString(); 230 | } 231 | private static final String HEADER_SYNTAX = "[ \t]*[a-z0-9A-Z\\$\\._\\-]+[ \t]*:.*"; 232 | 233 | static final Pattern HEADER_STRING_ARRAY_SYNTAX = 234 | Pattern.compile(HEADER_SYNTAX + "+(\n" + HEADER_SYNTAX + "+)*"); 235 | 236 | LinkedHashMap createHeaderData(String rawText) throws IOException { 237 | LinkedHashMap headerData = new LinkedHashMap(); 238 | if (!rawText.isEmpty()) { 239 | rawText = rawText.trim().replace("\r", ""); 240 | if (!HEADER_STRING_ARRAY_SYNTAX.matcher(rawText).matches()) { 241 | throw new IOException("HTTP Header syntax"); 242 | } 243 | for (String headerLine : rawText.split("\n")) { 244 | int colon = headerLine.indexOf(':'); 245 | String headerName = headerLine.substring(0, colon).trim().toLowerCase(); 246 | String headerValue = headerLine.substring(colon + 1).trim(); 247 | if (headerData.containsKey(headerName)) { 248 | headerData.put(headerName, headerData.get(headerName) + ", " + headerValue); 249 | } else { 250 | headerData.put(headerName, headerValue); 251 | } 252 | } 253 | } 254 | return headerData; 255 | } 256 | 257 | static String getPEMFromPublicKey(PublicKey publicKey) { 258 | return "-----BEGIN PUBLIC KEY-----\n" + 259 | Base64.encode(publicKey.getEncoded()) + 260 | "\n-----END PUBLIC KEY-----"; 261 | } 262 | 263 | private static final String TEST_MESSAGE = 264 | "{\n" + 265 | " \"statement\": \"Hello signed world!\",\n" + 266 | " \"otherProperties\": [2e+3, true]\n" + 267 | "}"; 268 | 269 | // The ! doesn't work great in bash... 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("/shreq/") + 6) + 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 message = 318 | new JSONObjectWriter(JSONParser.parse(TEST_MESSAGE)); 319 | 320 | JSONObjectWriter secinf = 321 | SHREQSupport.createJSONRequestSecInf(sampleJsonRequestUri, 322 | null, 323 | new GregorianCalendar(), 324 | noHeaders, 325 | signatureAlgorithm); 326 | message.setObject(SHREQSupport.SHREQ_SECINF_LABEL, secinf); 327 | byte[] JWS_Payload = message.serializeToBytes(JSONOutputFormats.CANONICALIZED); 328 | 329 | String jwsString = new JWSAsymKeySigner(privateKey, signatureAlgorithm) 330 | .sign(JWS_Payload, true); 331 | // Create the completed object which now is in "writer" 332 | secinf.setString(SHREQSupport.SHREQ_JWS_STRING, jwsString); 333 | 334 | sampleJsonRequest_JS = 335 | HTML.javaScript(message.serializeToString(JSONOutputFormats.PRETTY_PRINT)); 336 | 337 | message = new JSONObjectWriter(JSONParser.parse(CURL_TEST_MESSAGE)); 338 | 339 | secinf = SHREQSupport.createJSONRequestSecInf(sampleJsonRequestUri, 340 | null, 341 | new GregorianCalendar(), 342 | noHeaders, 343 | signatureAlgorithm); 344 | message.setObject(SHREQSupport.SHREQ_SECINF_LABEL, secinf); 345 | JWS_Payload = message.serializeToBytes(JSONOutputFormats.CANONICALIZED); 346 | 347 | jwsString = new JWSAsymKeySigner(privateKey, signatureAlgorithm) 348 | .sign(JWS_Payload, true); 349 | // Create the completed object which now is in "writer" 350 | secinf.setString(SHREQSupport.SHREQ_JWS_STRING, jwsString); 351 | 352 | sampleJsonRequest_CURL = 353 | message.serializeToString(JSONOutputFormats.NORMALIZED).replace("\"", "\\\""); 354 | 355 | message = new JSONObjectWriter(JSONParser.parse(CURL_PUT_TEST_MESSAGE)); 356 | 357 | LinkedHashMap oneHeader = new LinkedHashMap(); 358 | oneHeader.put("x-debug", "full"); 359 | 360 | secinf = SHREQSupport.createJSONRequestSecInf(sampleUriRequestUri2BeSigned, 361 | "PUT", 362 | new GregorianCalendar(), 363 | oneHeader, 364 | signatureAlgorithm); 365 | message.setObject(SHREQSupport.SHREQ_SECINF_LABEL, secinf); 366 | JWS_Payload = message.serializeToBytes(JSONOutputFormats.CANONICALIZED); 367 | 368 | jwsString = new JWSAsymKeySigner(privateKey, signatureAlgorithm) 369 | .sign(JWS_Payload, true); 370 | // Create the completed object which now is in "writer" 371 | secinf.setString(SHREQSupport.SHREQ_JWS_STRING, jwsString); 372 | 373 | sampleJsonRequest_CURL_Header_PUT = 374 | message.serializeToString(JSONOutputFormats.NORMALIZED).replace("\"", "\\\""); 375 | 376 | secinf = SHREQSupport.createURIRequestSecInf(sampleUriRequestUri2BeSigned, 377 | null, 378 | new GregorianCalendar(), 379 | noHeaders, 380 | signatureAlgorithm); 381 | sampleUriRequestUri = SHREQSupport.addJwsToTargetUri( 382 | sampleUriRequestUri2BeSigned, 383 | new JWSAsymKeySigner(privateKey, signatureAlgorithm) 384 | .sign(secinf.serializeToBytes(JSONOutputFormats.NORMALIZED), 385 | false)); 386 | 387 | sampleJson_JS = HTML.javaScript(TEST_MESSAGE); 388 | 389 | } catch (GeneralSecurityException e) { 390 | sampleJsonRequest_JS = "Internal error - Call admin"; 391 | } 392 | } 393 | } 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/shreq/CreateServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 WebPKI.org (http://webpki.org). 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package org.webpki.webapps.shreq; 18 | 19 | import java.io.IOException; 20 | 21 | import java.net.URLEncoder; 22 | 23 | import java.security.KeyPair; 24 | 25 | import java.util.GregorianCalendar; 26 | import java.util.LinkedHashMap; 27 | 28 | import javax.servlet.ServletException; 29 | 30 | import javax.servlet.http.HttpServletRequest; 31 | import javax.servlet.http.HttpServletResponse; 32 | 33 | import org.webpki.crypto.AlgorithmPreferences; 34 | import org.webpki.crypto.AsymSignatureAlgorithms; 35 | import org.webpki.crypto.HmacAlgorithms; 36 | import org.webpki.crypto.SignatureAlgorithms; 37 | 38 | import org.webpki.jose.jws.JWSAsymKeySigner; 39 | import org.webpki.jose.jws.JWSHmacSigner; 40 | import org.webpki.jose.jws.JWSSigner; 41 | 42 | import org.webpki.json.JSONObjectReader; 43 | import org.webpki.json.JSONObjectWriter; 44 | import org.webpki.json.JSONOutputFormats; 45 | import org.webpki.json.JSONParser; 46 | 47 | import org.webpki.shreq.SHREQSupport; 48 | 49 | import org.webpki.util.HexaDecimal; 50 | import org.webpki.util.PEMDecoder; 51 | 52 | public class CreateServlet extends BaseGuiServlet { 53 | 54 | private static final long serialVersionUID = 1L; 55 | 56 | public void doGet(HttpServletRequest request, HttpServletResponse response) 57 | throws IOException, ServletException { 58 | String defaultAlgorithm = "ES256"; 59 | getSampleData(request); 60 | StringBuilder html = new StringBuilder( 61 | "
" + 62 | "
SHREQ Message Creation
") 63 | 64 | .append( 65 | HTML.fancyText( 66 | true, 67 | TARGET_URI, 68 | 1, 69 | HTML.encode(sampleJsonRequestUri), 70 | "Target URI")) 71 | 72 | .append( 73 | HTML.fancyText( 74 | true, 75 | JSON_PAYLOAD, 76 | 10, 77 | "", 78 | "Paste an unsigned JSON object in the text box or try with the default")) 79 | 80 | .append( 81 | HTML.fancyText( 82 | false, 83 | TXT_OPT_HEADERS, 84 | 4, 85 | "", 86 | "Optional HTTP headers, each on a separate line")) 87 | 88 | .append(getRequestParameters()) 89 | 90 | .append(parameterBox("Security Parameters", 91 | new StringBuilder() 92 | .append( 93 | "
") 94 | .append(new SelectAlg(defaultAlgorithm) 95 | .add(HmacAlgorithms.HMAC_SHA256) 96 | .add(HmacAlgorithms.HMAC_SHA384) 97 | .add(HmacAlgorithms.HMAC_SHA512) 98 | .add(AsymSignatureAlgorithms.ECDSA_SHA256) 99 | .add(AsymSignatureAlgorithms.ECDSA_SHA384) 100 | .add(AsymSignatureAlgorithms.ECDSA_SHA512) 101 | .add(AsymSignatureAlgorithms.RSA_SHA256) 102 | .add(AsymSignatureAlgorithms.RSA_SHA384) 103 | .add(AsymSignatureAlgorithms.RSA_SHA512) 104 | .toString()) 105 | .append( 106 | "
Algorithm
" + 107 | "
Restore defaults
") 108 | .append(checkBox(FLG_JWK_INLINE, "Automagically insert public key (JWK)", 109 | false, "jwkFlagChange(this.checked)")) 110 | .append(checkBox(FLG_CERT_PATH, "Include provided certificate path (X5C)", 111 | false, "certFlagChange(this.checked)")) 112 | .append(checkBox(FLG_DEF_METHOD, "Include method even when default", 113 | false, null)) 114 | .append(checkBox(FLG_IAT_PRESENT, "Include time stamp (IAT)", 115 | true, "iatFlagChange()")))) 116 | .append( 117 | "
" + 118 | "
" + 119 | "Create Signed Request" + 120 | "
" + 121 | "
") 122 | 123 | .append( 124 | HTML.fancyText( 125 | true, 126 | TXT_JWS_EXTRA, 127 | 4, 128 | "", 129 | "Additional JWS header parameters (here expressed as properties of a JSON object)")) 130 | 131 | .append( 132 | HTML.fancyText( 133 | false, 134 | TXT_SECRET_KEY, 135 | 1, 136 | "", 137 | "Secret key in hexadecimal format")) 138 | 139 | .append( 140 | HTML.fancyText( 141 | false, 142 | TXT_PRIVATE_KEY, 143 | 4, 144 | "", 145 | "Private key in PEM/PKCS #8 or "plain" JWK format")) 146 | 147 | .append( 148 | HTML.fancyText( 149 | false, 150 | TXT_CERT_PATH, 151 | 4, 152 | "", 153 | "Certificate path in PEM format")) 154 | 155 | .append( 156 | "
" + 157 | "
 
"); 158 | 159 | StringBuilder js = new StringBuilder("\"use strict\";\n") 160 | .append(SHREQService.keyDeclarations) 161 | .append( 162 | "function fill(id, alg, keyHolder, unconditionally) {\n" + 163 | " let element = document.getElementById(id).children[1];\n" + 164 | " if (unconditionally || element.value == '') element.value = keyHolder[alg];\n" + 165 | "}\n" + 166 | "function disableAndClearCheckBox(id) {\n" + 167 | " let checkBox = document.getElementById(id);\n" + 168 | " checkBox.checked = false;\n" + 169 | " checkBox.disabled = true;\n" + 170 | "}\n" + 171 | "function enableCheckBox(id) {\n" + 172 | " document.getElementById(id).disabled = false;\n" + 173 | "}\n" + 174 | "function setUserData(unconditionally) {\n" + 175 | " let element = document.getElementById('" + JSON_PAYLOAD + "').children[1];\n" + 176 | " if (unconditionally || element.value == '') element.value = '") 177 | .append(sampleJson_JS) 178 | .append("';\n" + 179 | " element = document.getElementById('" + TXT_JWS_EXTRA + "').children[1];\n" + 180 | " if (unconditionally || element.value == '') element.value = '{\\n}';\n" + 181 | " element = document.getElementById('" + TARGET_URI + "').children[1];\n" + 182 | " if (unconditionally || element.value == '') element.value = '") 183 | .append(sampleJsonRequestUri) 184 | .append("';\n" + 185 | "}\n" + 186 | "function setParameters(alg, unconditionally) {\n" + 187 | " if (alg.startsWith('HS')) {\n" + 188 | " showCert(false);\n" + 189 | " showPriv(false);\n" + 190 | " disableAndClearCheckBox('" + FLG_CERT_PATH + "');\n" + 191 | " disableAndClearCheckBox('" + FLG_JWK_INLINE + "');\n" + 192 | " fill('" + TXT_SECRET_KEY + "', alg, " + 193 | SHREQService.KeyDeclaration.SECRET_KEYS + ", unconditionally);\n" + 194 | " showSec(true)\n" + 195 | " } else {\n" + 196 | " showSec(false)\n" + 197 | " enableCheckBox('" + FLG_CERT_PATH + "');\n" + 198 | " enableCheckBox('" + FLG_JWK_INLINE + "');\n" + 199 | " fill('" + TXT_PRIVATE_KEY + "', alg, " + 200 | SHREQService.KeyDeclaration.PRIVATE_KEYS + ", unconditionally);\n" + 201 | " showPriv(true);\n" + 202 | " fill('" + TXT_CERT_PATH + "', alg, " + 203 | SHREQService.KeyDeclaration.CERTIFICATES + ", unconditionally);\n" + 204 | " showCert(document.getElementById('" + FLG_CERT_PATH + "').checked);\n" + 205 | " }\n" + 206 | "}\n" + 207 | "function jwkFlagChange(flag) {\n" + 208 | " if (flag) {\n" + 209 | " document.getElementById('" + FLG_CERT_PATH + "').checked = false;\n" + 210 | " showCert(false);\n" + 211 | " }\n" + 212 | "}\n" + 213 | "function certFlagChange(flag) {\n" + 214 | " showCert(flag);\n" + 215 | " if (flag) {\n" + 216 | " document.getElementById('" + FLG_JWK_INLINE + "').checked = false;\n" + 217 | " }\n" + 218 | "}\n" + 219 | "function iatFlagChange() {\n" + 220 | " if (document.getElementsByName('" + REQUEST_TYPE + "')[1].checked) {\n" + 221 | " document.getElementById('" + FLG_IAT_PRESENT + "').checked = true;\n" + 222 | " }\n" + 223 | "}\n" + 224 | "function restoreSecurityDefaults() {\n" + 225 | " let s = document.getElementById('" + PRM_JWS_ALGORITHM + "');\n" + 226 | " for (let i = 0; i < s.options.length; i++) {\n" + 227 | " if (s.options[i].text == '" + DEFAULT_ALGORITHM + "') {\n" + 228 | " s.options[i].selected = true;\n" + 229 | " break;\n" + 230 | " }\n" + 231 | " }\n" + 232 | " setParameters('" + DEFAULT_ALGORITHM + "', true);\n" + 233 | " document.getElementById('" + FLG_CERT_PATH + "').checked = false;\n" + 234 | " document.getElementById('" + FLG_JWK_INLINE + "').checked = false;\n" + 235 | " document.getElementById('" + FLG_IAT_PRESENT + "').checked = true;\n" + 236 | " setUserData(true);\n" + 237 | "}\n" + 238 | "function algChange(alg) {\n" + 239 | " setParameters(alg, true);\n" + 240 | "}\n" + 241 | "function showJson(show) {\n" + 242 | " document.getElementById('" + JSON_PAYLOAD + "').style.display= show ? 'block' : 'none';\n" + 243 | "}\n" + 244 | "function showCert(show) {\n" + 245 | " document.getElementById('" + TXT_CERT_PATH + "').style.display= show ? 'block' : 'none';\n" + 246 | "}\n" + 247 | "function showPriv(show) {\n" + 248 | " document.getElementById('" + TXT_PRIVATE_KEY + "').style.display= show ? 'block' : 'none';\n" + 249 | "}\n" + 250 | "function showSec(show) {\n" + 251 | " document.getElementById('" + TXT_SECRET_KEY + "').style.display= show ? 'block' : 'none';\n" + 252 | "}\n" + 253 | "function setMethod(method) {\n" + 254 | " let s = document.getElementById('" + PRM_HTTP_METHOD + "');\n" + 255 | " for (let i = 0; i < s.options.length; i++) {\n" + 256 | " if (s.options[i].text == method) {\n" + 257 | " s.options[i].selected = true;\n" + 258 | " break;\n" + 259 | " }\n" + 260 | " }\n" + 261 | "}\n" + 262 | "function showHeaders(show) {\n" + 263 | " document.getElementById('" + TXT_OPT_HEADERS + "').style.display= show ? 'block' : 'none';\n" + 264 | "}\n" + 265 | "function restoreRequestDefaults() {\n" + 266 | " let radioButtons = document.getElementsByName('" + REQUEST_TYPE + "');\n" + 267 | " radioButtons[0].checked = true;\n" + 268 | " requestChange(true);\n" + 269 | " document.getElementById('" + FLG_HEADERS + "').checked = false;\n" + 270 | " showHeaders(false);\n" + 271 | "}\n" + 272 | "function requestChange(jsonRequest) {\n" + 273 | " document.getElementById('" + JSON_PAYLOAD + "').style.display= jsonRequest ? 'block' : 'none';\n" + 274 | " setMethod(jsonRequest ? '" + DEFAULT_JSON_METHOD + "' : '" + DEFAULT_URI_METHOD + "');\n" + 275 | " if (!jsonRequest) {\n" + 276 | " document.getElementById('" + FLG_IAT_PRESENT + "').checked = true;\n" + 277 | " }\n" + 278 | " let element = document.getElementById('" + TARGET_URI + "').children[1];\n" + 279 | " if (jsonRequest) {\n" + 280 | " if (element.value == '" + sampleUriRequestUri2BeSigned + "') {\n" + 281 | " element.value = '" + sampleJsonRequestUri + "';\n" + 282 | " }\n" + 283 | " } else {\n" + 284 | " if (element.value == '" + sampleJsonRequestUri + "') {\n" + 285 | " element.value = '" + sampleUriRequestUri2BeSigned + "';\n" + 286 | " }\n" + 287 | " }\n" + 288 | "}\n" + 289 | "function headerFlagChange(flag) {\n" + 290 | " showHeaders(flag);\n" + 291 | "}\n" + 292 | "window.addEventListener('load', function(event) {\n" + 293 | " setParameters(document.getElementById('" + PRM_JWS_ALGORITHM + "').value, false);\n" + 294 | " let radioButtons = document.getElementsByName('" + REQUEST_TYPE + "');\n" + 295 | " showJson(radioButtons[0].checked);\n" + 296 | " showHeaders(document.getElementById('" + FLG_HEADERS + "').checked);\n" + 297 | " setUserData(false);\n" + 298 | "});\n"); 299 | HTML.standardPage(response, js.toString(), html); 300 | } 301 | 302 | public void doPost(HttpServletRequest request, HttpServletResponse response) 303 | throws IOException, ServletException { 304 | try { 305 | request.setCharacterEncoding("utf-8"); 306 | String targetUri = SHREQSupport.normalizeTargetURI(getTextArea(request, TARGET_URI)); 307 | String jsonData = getTextArea(request, JSON_PAYLOAD); 308 | String rawHttpHeaderData = getTextArea(request, TXT_OPT_HEADERS); 309 | String method = getParameter(request, PRM_HTTP_METHOD); 310 | boolean jsonRequest = Boolean.valueOf(getParameter(request, REQUEST_TYPE)); 311 | JSONObjectReader additionalHeaderData = JSONParser.parse(getParameter(request, TXT_JWS_EXTRA)); 312 | boolean keyInlining = request.getParameter(FLG_JWK_INLINE) != null; 313 | boolean certOption = request.getParameter(FLG_CERT_PATH) != null; 314 | boolean iatOption = request.getParameter(FLG_IAT_PRESENT) != null; 315 | boolean forceMethod = request.getParameter(FLG_DEF_METHOD) != null; 316 | boolean httpHeaders = request.getParameter(FLG_HEADERS) != null; 317 | LinkedHashMap httpHeaderData = 318 | createHeaderData(httpHeaders ? rawHttpHeaderData : ""); 319 | 320 | // Get wanted signature algorithm 321 | String algorithmParam = getParameter(request, PRM_JWS_ALGORITHM); 322 | SignatureAlgorithms signatureAlgorithm = algorithmParam.startsWith("HS") ? 323 | HmacAlgorithms.getAlgorithmFromId(algorithmParam, 324 | AlgorithmPreferences.JOSE) 325 | : 326 | AsymSignatureAlgorithms.getAlgorithmFromId(algorithmParam, 327 | AlgorithmPreferences.JOSE); 328 | 329 | // Get the signature key 330 | JWSSigner jwsSigner; 331 | String validationKey; 332 | 333 | // Symmetric or asymmetric? 334 | if (signatureAlgorithm.isSymmetric()) { 335 | validationKey = getParameter(request, TXT_SECRET_KEY); 336 | jwsSigner = new JWSHmacSigner(HexaDecimal.decode(validationKey), 337 | (HmacAlgorithms) signatureAlgorithm); 338 | } else { 339 | // To simplify UI we require PKCS #8 with the public key embedded 340 | // but we also support JWK which also has the public key 341 | byte[] privateKeyBlob = getBinaryParameter(request, TXT_PRIVATE_KEY); 342 | KeyPair keyPair; 343 | if (privateKeyBlob[0] == '{') { 344 | keyPair = JSONParser.parse(privateKeyBlob).getKeyPair(); 345 | } else { 346 | keyPair = PEMDecoder.getKeyPair(privateKeyBlob); 347 | } 348 | privateKeyBlob = null; // Nullify it after use 349 | validationKey = getPEMFromPublicKey(keyPair.getPublic()); 350 | jwsSigner = new JWSAsymKeySigner(keyPair.getPrivate(), 351 | (AsymSignatureAlgorithms) signatureAlgorithm); 352 | 353 | // Add other JWS header data that the demo program fixes 354 | if (certOption) { 355 | ((JWSAsymKeySigner)jwsSigner).setCertificatePath( 356 | PEMDecoder.getCertificatePath(getBinaryParameter(request, 357 | TXT_CERT_PATH))); 358 | } else if (keyInlining) { 359 | ((JWSAsymKeySigner)jwsSigner).setPublicKey(keyPair.getPublic()); 360 | } 361 | } 362 | 363 | // Add any optional (by the user specified) arguments 364 | jwsSigner.addHeaderItems(additionalHeaderData); 365 | 366 | String signedJSONRequest; 367 | if (jsonRequest) { 368 | // Creating JSON data to be signed 369 | JSONObjectReader reader = JSONParser.parse(jsonData); 370 | if (reader.getJSONArrayReader() != null) { 371 | throw new IOException("The demo does not support signed arrays"); 372 | } 373 | JSONObjectWriter message = new JSONObjectWriter(reader); 374 | JSONObjectWriter secinf = 375 | SHREQSupport.createJSONRequestSecInf( 376 | targetUri, 377 | forceMethod || 378 | !method.equals(SHREQSupport.SHREQ_DEFAULT_JSON_METHOD) ? 379 | method : null, 380 | iatOption ? new GregorianCalendar() : null, 381 | httpHeaderData, 382 | signatureAlgorithm); 383 | message.setObject(SHREQSupport.SHREQ_SECINF_LABEL, secinf); 384 | byte[] JWS_Payload = message.serializeToBytes(JSONOutputFormats.CANONICALIZED); 385 | 386 | // Sign it using the provided algorithm and key 387 | String jwsString = jwsSigner.sign(JWS_Payload, true); 388 | jwsSigner = null; // Nullify it after use 389 | 390 | // Create the completed object which now is in "writer" 391 | secinf.setString(SHREQSupport.SHREQ_JWS_STRING, jwsString); 392 | 393 | signedJSONRequest = message.serializeToString(JSONOutputFormats.NORMALIZED); 394 | 395 | // The following is just for the demo. That is, we want to preserve 396 | // the original ("untouched") JSON data for educational purposes. 397 | int i = signedJSONRequest.lastIndexOf("\"" + SHREQSupport.SHREQ_SECINF_LABEL); 398 | if (signedJSONRequest.charAt(i - 1) == ',') { 399 | i--; 400 | } 401 | int j = jsonData.lastIndexOf("}"); 402 | signedJSONRequest = jsonData.substring(0, j) + 403 | signedJSONRequest.substring(i, signedJSONRequest.length() - 1) + 404 | jsonData.substring(j); 405 | } else { 406 | signedJSONRequest=""; 407 | JSONObjectWriter writer = 408 | SHREQSupport.createURIRequestSecInf( 409 | targetUri, 410 | forceMethod || 411 | !method.equals(SHREQSupport.SHREQ_DEFAULT_URI_METHOD) ? 412 | method : null, 413 | iatOption ? new GregorianCalendar() : null, 414 | httpHeaderData, 415 | signatureAlgorithm); 416 | targetUri = SHREQSupport.addJwsToTargetUri( 417 | targetUri, 418 | jwsSigner.sign(writer.serializeToBytes(JSONOutputFormats.NORMALIZED), 419 | false)); 420 | } 421 | 422 | // We terminate by validating the signature as well 423 | request.getRequestDispatcher("validate?" + 424 | TARGET_URI + 425 | "=" + 426 | URLEncoder.encode(targetUri, "utf-8") + 427 | "&" + 428 | REQUEST_TYPE + 429 | "=" + 430 | jsonRequest + 431 | "&" + 432 | JSON_PAYLOAD + 433 | "=" + 434 | URLEncoder.encode(signedJSONRequest, "utf-8") + 435 | "&" + 436 | TXT_OPT_HEADERS + 437 | "=" + 438 | URLEncoder.encode(rawHttpHeaderData, "utf-8") + 439 | "&" + 440 | JWS_VALIDATION_KEY + 441 | "=" + 442 | URLEncoder.encode(validationKey, "utf-8") + 443 | "&" + 444 | PRM_HTTP_METHOD + 445 | "=" + 446 | URLEncoder.encode(method, "utf-8")) 447 | .forward(request, response); 448 | } catch (Exception e) { 449 | HTML.errorPage(response, e); 450 | } 451 | } 452 | } 453 | --------------------------------------------------------------------------------