├── .gitignore ├── .gitattributes ├── testdata ├── challenge.txt ├── ad.cbor ├── esad.cbor ├── sad.cbor ├── hashed-AD.bin ├── clientDataJSON.json ├── encryption.jwk ├── signature.jwk ├── AD.txt ├── FWP-assertion.json ├── SAD.txt ├── PSP-request.json ├── ESAD.txt ├── ISSUER-request.json └── vectors.txt ├── web ├── index.jsp ├── images │ ├── waiting.gif │ ├── webpkiorg.png │ ├── wallet-ui.svg │ ├── wallet-internal.svg │ ├── fwp.svg │ ├── psp.svg │ ├── issuer.svg │ ├── fwpminiplus-pay.svg │ ├── fwp-pay.svg │ ├── paypal-pay.svg │ ├── visamc-pay.svg │ └── legacy-visamc-pay.svg └── style.css ├── empty.lib └── .gitignore ├── fwp.properties ├── README.md ├── context.xml ├── .classpath ├── .settings └── org.eclipse.jdt.core.prefs ├── .project ├── lib └── org │ └── webpki │ └── fwp │ ├── FWPException.java │ ├── FWPElements.java │ ├── FWPJsonAssertion.java │ ├── FWPPaymentRequest.java │ ├── FWPAssertionBuilder.java │ └── FWPAssertionDecoder.java ├── test └── org │ └── webpki │ └── fwp │ ├── Ctap2Test.java │ ├── IssuerRequest.java │ ├── PSPRequest.java │ └── CryptoImages.java ├── docgen ├── fwp-crypto.svg └── cbor-crypto.svg ├── src └── org │ └── webpki │ └── webapps │ └── fwp │ ├── Actors.java │ ├── SystemDetection.java │ ├── FIDOPayServlet.java │ ├── ReplayCache.java │ ├── CardServlet.java │ ├── HomeServlet.java │ ├── admin │ └── RegistrationListServlet.java │ ├── SADServlet.java │ ├── ApplicationService.java │ ├── WalletAdminServlet.java │ ├── PSPServlet.java │ ├── ESADServlet.java │ ├── MerchantServlet.java │ ├── FinalizeAssertionServlet.java │ ├── PaymentRequestServlet.java │ ├── HTML.java │ ├── FIDOLoginServlet.java │ ├── FIDOEnrollServlet.java │ ├── ADServlet.java │ └── LoginServlet.java └── web.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /dist 3 | .tmp 4 | .DS_store 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Disable LF normalization for all files 2 | * -text -------------------------------------------------------------------------------- /testdata/challenge.txt: -------------------------------------------------------------------------------- 1 | 0fbrom0qcwjuzc0qIVRg1axQo5XecsovXENDYi6KzyM -------------------------------------------------------------------------------- /web/index.jsp: -------------------------------------------------------------------------------- 1 | <%@page session="false"%><%response.sendRedirect ("home");%> 2 | -------------------------------------------------------------------------------- /testdata/ad.cbor: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberphone/fwp/main/testdata/ad.cbor -------------------------------------------------------------------------------- /testdata/esad.cbor: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberphone/fwp/main/testdata/esad.cbor -------------------------------------------------------------------------------- /testdata/sad.cbor: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberphone/fwp/main/testdata/sad.cbor -------------------------------------------------------------------------------- /testdata/hashed-AD.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberphone/fwp/main/testdata/hashed-AD.bin -------------------------------------------------------------------------------- /web/images/waiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberphone/fwp/main/web/images/waiting.gif -------------------------------------------------------------------------------- /web/images/webpkiorg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberphone/fwp/main/web/images/webpkiorg.png -------------------------------------------------------------------------------- /empty.lib/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /fwp.properties: -------------------------------------------------------------------------------- 1 | # Lots of stuff is fetched from here 2 | openkeystore=../openkeystore 3 | fido-web-pay=../fido-web-pay 4 | -------------------------------------------------------------------------------- /testdata/clientDataJSON.json: -------------------------------------------------------------------------------- 1 | {"type":"webauthn.get","origin":"https://mybank.fr","challenge":"sE1wcX3d4X_IuPl2ISUHzMx4PdV60s6KLKvi-Jg34ck"} -------------------------------------------------------------------------------- /testdata/encryption.jwk: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "crv": "X25519", 4 | "x": "6ZoM7yBYlJYNmxwFl4UT3MtCoTv7ztUjpRuKEXrV8Aw", 5 | "d": "cxfl86EVmcqrR07mWENCf1F_5Ni5mt1ViGyERB6Q1vA" 6 | } 7 | -------------------------------------------------------------------------------- /testdata/signature.jwk: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "crv": "P-256", 4 | "x": "6BKxpty8cI-exDzCkh-goU6dXq3MbcY0cd1LaAxiNrU", 5 | "y": "mCbcvUzm44j3Lt2b5BPyQloQ91tf2D2V-gzeUxWaUdg", 6 | "d": "6XxMFXhcYT5QN9w5TIg2aSKsbcj-pj4BnZkK7ZOt4B8" 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## FIDO Web Pay (FWP) 2 | 3 | This repository holds a Web-based FWP emulator using the WebAuthn API. 4 | It should be possible to test by anybody using a FIDO-compliant client platform: https://test.webpki.org/fwp. 5 | 6 | It is intended as a proof-of-concept for the FWP specification: https://fido-web-pay.github.io. 7 | -------------------------------------------------------------------------------- /web/images/wallet-ui.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wallet 5 | UI 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/images/wallet-internal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wallet 5 | Internal 6 | 7 | 8 | -------------------------------------------------------------------------------- /context.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /testdata/AD.txt: -------------------------------------------------------------------------------- 1 | { 2 | 1: { 3 | 1: "Space Shop", 4 | 2: "7040566321", 5 | 3: "435.00", 6 | 4: "EUR" 7 | }, 8 | 2: "spaceshop.com", 9 | 3: "FR7630002111110020050014382", 10 | 4: "https://banknet2.org", 11 | 5: "0057162932", 12 | 6: "additional stuff...", 13 | 7: { 14 | 1: { 15 | 3: "Android", 16 | 4: "12.0" 17 | }, 18 | 2: { 19 | 3: "Chrome", 20 | 4: "108" 21 | } 22 | }, 23 | 8: [40.74844, -73.984559], 24 | 9: "2023-02-16T10:14:07+01:00", 25 | -1: { 26 | 1: -7, 27 | 2: { 28 | 1: 2, 29 | -1: 1, 30 | -2: h'e812b1a6dcbc708f9ec43cc2921fa0a14e9d5eadcc6dc63471dd4b680c6236b5', 31 | -3: h'9826dcbd4ce6e388f72edd9be413f2425a10f75b5fd83d95fa0cde53159a51d8' 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=15 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 5 | org.eclipse.jdt.core.compiler.compliance=15 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 10 | org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled 11 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 12 | org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning 13 | org.eclipse.jdt.core.compiler.release=enabled 14 | org.eclipse.jdt.core.compiler.source=15 15 | -------------------------------------------------------------------------------- /testdata/FWP-assertion.json: -------------------------------------------------------------------------------- 1 | { 2 | "paymentNetworkId": "https://banknet2.org", 3 | "issuerId": "https://mybank.fr/payment", 4 | "userAuthorization": "2QPygngkaHR0cHM6Ly9maWRvLXdlYi1wYXkuZ2l0aHViLmlvL25zL3AxpQEDAqQBOB4DbXgyNTUxOToyMDIyOjEHowEBIAQhWCADTpJz2dVcPfD7Nm_DNCVkjYFQ3lBMGzSZ4KfayRosFwpYKC_WImgpm14v5Xuv1XYqjv86i5mR-svsLTYJPNrLI-1d_1dQyjvV1_wIUMIKsWFF8eU0nB2F-rTK8KMJTFfnNBs7E3nYdlrmEwpZAZ8gTl9bStY9ATrIddFg_8T3YrdRU_uLMKnZ7O-vI6MImM1orBBO39-FTgYNkG8SKfc5s35S3_7YdKB98_1mHAYdbXtNVhr-n8MfFP-7FaXWLevh9ctUqFH9xLVKg9b45kpaWwxEWWCZKvlkEmwXqlWR10e550pAxeotbCpfOHQBxjaFuxzCp6MxubRFBWIuJ6fCkxTe2qzI0_QltIAQ2XEV92ctwa2JprAbPW8EJzM9Gr8GZ_61TEI4POtKiIOiS5O0t5IWSdBUNfti1NSq_LTOkyONNTj8iCG_anG8kGFzFS-TOzWcz5pUathAUQuuvey27hX93ENIuO-NgMs2-EEKlHhOIlQiCL-_bKsZifI9NL51zMOKKVAr8OlSF0q4I99nKMOTFcKs8751-4oHKgSMCOHv7DXuUVjPgo8ri4qeMEgk_13HyROa82ZxZevF3KDPwguqni5F-mWq1UrgJue0Y-yPl02-N-kCF_ar4iPFmMM06aqphkfuSF62Xycac4bbccE4Q7dXDuIRpbBV7oPpq5BoU2rgtpiCG60aed41" 5 | } 6 | -------------------------------------------------------------------------------- /testdata/SAD.txt: -------------------------------------------------------------------------------- 1 | { 2 | 1: { 3 | 1: "Space Shop", 4 | 2: "7040566321", 5 | 3: "435.00", 6 | 4: "EUR" 7 | }, 8 | 2: "spaceshop.com", 9 | 3: "FR7630002111110020050014382", 10 | 4: "https://banknet2.org", 11 | 5: "0057162932", 12 | 6: "additional stuff...", 13 | 7: { 14 | 1: { 15 | 3: "Android", 16 | 4: "12.0" 17 | }, 18 | 2: { 19 | 3: "Chrome", 20 | 4: "108" 21 | } 22 | }, 23 | 8: [40.74844, -73.984559], 24 | 9: "2023-02-16T10:14:07+01:00", 25 | -1: { 26 | 1: -7, 27 | 2: { 28 | 1: 2, 29 | -1: 1, 30 | -2: h'e812b1a6dcbc708f9ec43cc2921fa0a14e9d5eadcc6dc63471dd4b680c6236b5', 31 | -3: h'9826dcbd4ce6e388f72edd9be413f2425a10f75b5fd83d95fa0cde53159a51d8' 32 | }, 33 | 3: h'412e175a0f0bdc06dabf0b1db79b97541c08dbacee7e31c97a553588ee922ea70500000017', 34 | 4: h'304402204fbd186e8eac7d7dbb915a7a443b0939af77de5e35cf87831663ae3a8bfc1d940220201d0c51ff9b683648a626cbe0bbb69fed29ce854aea65763e0e33edf2af9e09' 35 | } 36 | } -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | fwp 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | org.eclipse.jdt.ls.unmanagedFolderNature 17 | 18 | 19 | 20 | 1714018386907 21 | 22 | 30 23 | 24 | org.eclipse.core.resources.regexFilterMatcher 25 | node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /testdata/PSP-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "paymentRequest": { 3 | "payeeName": "Space Shop", 4 | "requestId": "7040566321", 5 | "amount": "435.00", 6 | "currency": "EUR" 7 | }, 8 | "fwpAssertion": { 9 | "paymentNetworkId": "https://banknet2.org", 10 | "issuerId": "https://mybank.fr/payment", 11 | "userAuthorization": "2QPygngkaHR0cHM6Ly9maWRvLXdlYi1wYXkuZ2l0aHViLmlvL25zL3AxpQEDAqQBOB4DbXgyNTUxOToyMDIyOjEHowEBIAQhWCADTpJz2dVcPfD7Nm_DNCVkjYFQ3lBMGzSZ4KfayRosFwpYKC_WImgpm14v5Xuv1XYqjv86i5mR-svsLTYJPNrLI-1d_1dQyjvV1_wIUMIKsWFF8eU0nB2F-rTK8KMJTFfnNBs7E3nYdlrmEwpZAZ8gTl9bStY9ATrIddFg_8T3YrdRU_uLMKnZ7O-vI6MImM1orBBO39-FTgYNkG8SKfc5s35S3_7YdKB98_1mHAYdbXtNVhr-n8MfFP-7FaXWLevh9ctUqFH9xLVKg9b45kpaWwxEWWCZKvlkEmwXqlWR10e550pAxeotbCpfOHQBxjaFuxzCp6MxubRFBWIuJ6fCkxTe2qzI0_QltIAQ2XEV92ctwa2JprAbPW8EJzM9Gr8GZ_61TEI4POtKiIOiS5O0t5IWSdBUNfti1NSq_LTOkyONNTj8iCG_anG8kGFzFS-TOzWcz5pUathAUQuuvey27hX93ENIuO-NgMs2-EEKlHhOIlQiCL-_bKsZifI9NL51zMOKKVAr8OlSF0q4I99nKMOTFcKs8751-4oHKgSMCOHv7DXuUVjPgo8ri4qeMEgk_13HyROa82ZxZevF3KDPwguqni5F-mWq1UrgJue0Y-yPl02-N-kCF_ar4iPFmMM06aqphkfuSF62Xycac4bbccE4Q7dXDuIRpbBV7oPpq5BoU2rgtpiCG60aed41" 12 | }, 13 | "receiveAccount": "DE89370400440532013000", 14 | "clientIpAddress": "220.13.198.144", 15 | "timeStamp": "2023-02-16T09:14:22Z" 16 | } 17 | -------------------------------------------------------------------------------- /testdata/ESAD.txt: -------------------------------------------------------------------------------- 1 | 1010(["https://fido-web-pay.github.io/ns/p1", { 2 | 1: 3, 3 | 2: { 4 | 1: -31, 5 | 3: "x25519:2022:1", 6 | 7: { 7 | 1: 1, 8 | -1: 4, 9 | -2: h'034e9273d9d55c3df0fb366fc33425648d8150de504c1b3499e0a7dac91a2c17' 10 | }, 11 | 10: h'2fd62268299b5e2fe57bafd5762a8eff3a8b9991facbec2d36093cdacb23ed5dff5750ca3bd5d7fc' 12 | }, 13 | 8: h'c20ab16145f1e5349c1d85fab4caf0a3', 14 | 9: h'57e7341b3b1379d8765ae613', 15 | 10: h'204e5f5b4ad63d013ac875d160ffc4f762b75153fb8b30a9d9ecefaf23a30898cd68ac104edfdf854e060d906f1229f739b37e52dffed874a07df3fd661c061d6d7b4d561afe9fc31f14ffbb15a5d62debe1f5cb54a851fdc4b54a83d6f8e64a5a5b0c445960992af964126c17aa5591d747b9e74a40c5ea2d6c2a5f387401c63685bb1cc2a7a331b9b44505622e27a7c29314dedaacc8d3f425b48010d97115f7672dc1ad89a6b01b3d6f0427333d1abf0667feb54c42383ceb4a8883a24b93b4b7921649d05435fb62d4d4aafcb4ce93238d3538fc8821bf6a71bc906173152f933b359ccf9a546ad840510baebdecb6ee15fddc4348b8ef8d80cb36f8410a94784e22542208bfbf6cab1989f23d34be75ccc38a29502bf0e952174ab823df6728c39315c2acf3be75fb8a072a048c08e1efec35ee5158cf828f2b8b8a9e304824ff5dc7c9139af3667165ebc5dca0cfc20baa9e2e45fa65aad54ae026e7b463ec8f974dbe37e90217f6abe223c598c334e9aaa98647ee485eb65f271a7386db71c13843b7570ee211a5b055ee83e9ab9068536ae0b698821bad1a79de35' 16 | }]) -------------------------------------------------------------------------------- /testdata/ISSUER-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "pspRequest": { 3 | "paymentRequest": { 4 | "payeeName": "Space Shop", 5 | "requestId": "7040566321", 6 | "amount": "435.00", 7 | "currency": "EUR" 8 | }, 9 | "fwpAssertion": { 10 | "paymentNetworkId": "https://banknet2.org", 11 | "issuerId": "https://mybank.fr/payment", 12 | "userAuthorization": "2QPygngkaHR0cHM6Ly9maWRvLXdlYi1wYXkuZ2l0aHViLmlvL25zL3AxpQEDAqQBOB4DbXgyNTUxOToyMDIyOjEHowEBIAQhWCADTpJz2dVcPfD7Nm_DNCVkjYFQ3lBMGzSZ4KfayRosFwpYKC_WImgpm14v5Xuv1XYqjv86i5mR-svsLTYJPNrLI-1d_1dQyjvV1_wIUMIKsWFF8eU0nB2F-rTK8KMJTFfnNBs7E3nYdlrmEwpZAZ8gTl9bStY9ATrIddFg_8T3YrdRU_uLMKnZ7O-vI6MImM1orBBO39-FTgYNkG8SKfc5s35S3_7YdKB98_1mHAYdbXtNVhr-n8MfFP-7FaXWLevh9ctUqFH9xLVKg9b45kpaWwxEWWCZKvlkEmwXqlWR10e550pAxeotbCpfOHQBxjaFuxzCp6MxubRFBWIuJ6fCkxTe2qzI0_QltIAQ2XEV92ctwa2JprAbPW8EJzM9Gr8GZ_61TEI4POtKiIOiS5O0t5IWSdBUNfti1NSq_LTOkyONNTj8iCG_anG8kGFzFS-TOzWcz5pUathAUQuuvey27hX93ENIuO-NgMs2-EEKlHhOIlQiCL-_bKsZifI9NL51zMOKKVAr8OlSF0q4I99nKMOTFcKs8751-4oHKgSMCOHv7DXuUVjPgo8ri4qeMEgk_13HyROa82ZxZevF3KDPwguqni5F-mWq1UrgJue0Y-yPl02-N-kCF_ar4iPFmMM06aqphkfuSF62Xycac4bbccE4Q7dXDuIRpbBV7oPpq5BoU2rgtpiCG60aed41" 13 | }, 14 | "receiveAccount": "DE89370400440532013000", 15 | "clientIpAddress": "220.13.198.144", 16 | "timeStamp": "2023-02-16T09:14:23Z" 17 | }, 18 | "payeeHost": "spaceshop.com", 19 | "timeStamp": "2023-02-16T09:14:23Z" 20 | } 21 | -------------------------------------------------------------------------------- /lib/org/webpki/fwp/FWPException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2006-2021 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.fwp; 18 | 19 | /** 20 | * Wrapper for making the FWP library only throw unchecked exceptions. 21 | */ 22 | public class FWPException extends RuntimeException { 23 | 24 | private static final long serialVersionUID = 1L; 25 | 26 | /** 27 | * Constructor for rethrowing checked exceptions. 28 | * 29 | * @param sourceException 30 | */ 31 | public FWPException(Exception sourceException) { 32 | super(sourceException); 33 | } 34 | 35 | /** 36 | * Constructor for original exceptions. 37 | * 38 | * @param message 39 | */ 40 | public FWPException(String message) { 41 | super(message); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/images/fwp.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | FIDO Web Pay Logotype 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/org/webpki/fwp/Ctap2Test.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2006-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.util.Base64; 22 | 23 | import org.webpki.cbor.CBORDecoder; 24 | 25 | 26 | /** 27 | * Test externally generated Ctap2 data. 28 | */ 29 | public class Ctap2Test { 30 | 31 | static byte[] base64UrlDecode(String b64u) { 32 | return Base64.getUrlDecoder().decode(b64u); 33 | } 34 | 35 | public static void main(String[] args) { 36 | try { 37 | if (args.length != 3) { 38 | throw new IOException("Wrong number of parameters"); 39 | } 40 | byte[] sadObject = FWPCrypto.addSignature(base64UrlDecode(args[0]), 41 | null, // CTAP2 mode 42 | base64UrlDecode(args[2]), 43 | base64UrlDecode(args[1])); 44 | new FWPAssertionDecoder(sadObject); 45 | System.out.println(CBORDecoder.decode(sadObject).toString()); 46 | } catch (Exception e) { 47 | e.printStackTrace(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/org/webpki/fwp/FWPElements.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import org.webpki.cbor.CBORInt; 20 | 21 | /** 22 | * Core elements of an FWP assertion. 23 | * 24 | */ 25 | public enum FWPElements { 26 | 27 | PAYMENT_REQUEST (1), 28 | PAYEE_HOST (2), 29 | ACCOUNT_ID (3), 30 | PAYMENT_NETWORK_ID (4), 31 | SERIAL_NUMBER (5), 32 | NETWORK_OPTIONS (6), // Optional (Merchant) 33 | PLATFORM_DATA (7), 34 | LOCATION (8), // Optional (Client) 35 | TIME_STAMP (9), 36 | AUTHORIZATION (-1); 37 | 38 | 39 | CBORInt cborLabel; 40 | 41 | FWPElements(int cborLabel) { 42 | this.cborLabel = new CBORInt(cborLabel); 43 | } 44 | 45 | // Platform Data 46 | public static final CBORInt CBOR_PD_OPERATING_SYSTEM = new CBORInt(1); 47 | public static final CBORInt CBOR_PD_USER_AGENT = new CBORInt(2); 48 | 49 | // Platform Data sub elements 50 | public static final CBORInt CBOR_PDSUB_NAME = new CBORInt(3); 51 | public static final CBORInt CBOR_PDSUB_VERSION = new CBORInt(4); 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /web/images/psp.svg: -------------------------------------------------------------------------------- 1 | 2 | PSP Symbol 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docgen/fwp-crypto.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | FWP Encryption Layout 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Main Map 19 | (Content Encryption) 20 | Sub Map 21 | (Key Encryption) 22 | 23 | 24 | algorithm (1) 25 | keyEncryption (2) 26 | tag (8) 27 | iv (9) 28 | cipherText (10) 29 | algorithm (1) 30 | keyId (3) 31 | publicKey (4) 32 | ephemeralKey (7) 33 | cipherText (10) 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /web/images/issuer.svg: -------------------------------------------------------------------------------- 1 | 2 | Issuer Symbol 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | B  A  N  K 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/Actors.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | public enum Actors { 20 | 21 | SITE ( 22 | "
" + 23 | ""), 26 | FWP ( 27 | "
" + 28 | ""), 31 | WALLET ( 32 | "
" + 33 | ""), 36 | MERCHANT ( 37 | "
" + 38 | ""), 41 | PSP ( 42 | "
" + 43 | ""), 46 | ISSUER ( 47 | "
" + 48 | ""), 51 | ADMIN ( 52 | "
" + 53 | ""); 56 | 57 | String html; 58 | 59 | Actors(String html) { 60 | this.html = html; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /web/images/fwpminiplus-pay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | FIDO Web Pay 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | SEPA, CB, AMEX, 41 | VISA, MasterCard 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /web/images/fwp-pay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | FIDO Web Pay 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | FIDO Web Pay Logotype 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docgen/cbor-crypto.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | CBOR Encryption Format 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Main Map 22 | (Content Encryption) 23 | Optional Sub Map 24 | (Key Encryption) 25 | 26 | 27 | customData (0) 28 | algorithm (1) 29 | keyEncryption (2) 30 | keyId (3) 31 | tag (8) 32 | iv (9) 33 | cipherText (10) 34 | algorithm (1) 35 | keyId (3) 36 | publicKey (4) 37 | certificatePath (5) 38 | ephemeralKey (7) 39 | cipherText (10) 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /lib/org/webpki/fwp/FWPJsonAssertion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import org.webpki.json.JSONObjectReader; 22 | import org.webpki.json.JSONObjectWriter; 23 | import org.webpki.json.JSONOutputFormats; 24 | 25 | /** 26 | * The FWP Assertion as provided by the browser. 27 | */ 28 | public class FWPJsonAssertion { 29 | 30 | public static final String PAYMENT_NETWORK_ID = "paymentNetworkId"; 31 | public static final String ISSUER_ID = "issuerId"; 32 | public static final String USER_AUTHORIZATION = "userAuthorization"; 33 | 34 | String paymentNetworkId; 35 | public String getPaymentNetwordId() { 36 | return paymentNetworkId; 37 | } 38 | 39 | String issuerId; 40 | public String getIssuerId() { 41 | return issuerId; 42 | } 43 | 44 | byte[] userAuthorization; 45 | public byte[] getUserAuthorization() { 46 | return userAuthorization; 47 | } 48 | 49 | public FWPJsonAssertion(JSONObjectReader reader) throws IOException { 50 | paymentNetworkId = reader.getString(PAYMENT_NETWORK_ID); 51 | issuerId = reader.getString(ISSUER_ID); 52 | userAuthorization = reader.getBinary(USER_AUTHORIZATION); 53 | } 54 | 55 | public FWPJsonAssertion(String paymentNetworkId, 56 | String issuerId, 57 | byte[] userAuthorization) { 58 | this.paymentNetworkId = paymentNetworkId; 59 | this.issuerId = issuerId; 60 | this.userAuthorization = userAuthorization; 61 | } 62 | 63 | public String serialize() throws IOException { 64 | return getWriter().serializeToString(JSONOutputFormats.NORMALIZED); 65 | } 66 | 67 | public JSONObjectWriter getWriter() throws IOException { 68 | return new JSONObjectWriter() 69 | .setString(PAYMENT_NETWORK_ID, paymentNetworkId) 70 | .setString(ISSUER_ID, issuerId) 71 | .setBinary(USER_AUTHORIZATION, userAuthorization); 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | try { 77 | return getWriter().toString(); 78 | } catch (IOException e) { 79 | throw new RuntimeException(e); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/org/webpki/fwp/IssuerRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.util.GregorianCalendar; 22 | 23 | import org.webpki.json.JSONObjectReader; 24 | import org.webpki.json.JSONObjectWriter; 25 | import org.webpki.json.JSONOutputFormats; 26 | 27 | import org.webpki.util.ISODateTime; 28 | 29 | /** 30 | * Sample Issuer request matching the FWP documentation. 31 | */ 32 | public class IssuerRequest { 33 | 34 | public static final String PSP_REQUEST = "pspRequest"; 35 | public static final String PAYEE_HOST = "payeeHost"; 36 | public static final String TIME_STAMP = "timeStamp"; 37 | 38 | PSPRequest pspRequest; 39 | public PSPRequest getPspRequest() { 40 | return pspRequest; 41 | } 42 | 43 | String payeeHost; 44 | public String getPayeeHost() { 45 | return payeeHost; 46 | } 47 | 48 | GregorianCalendar timeStamp; 49 | public GregorianCalendar getTimeStamp() { 50 | return timeStamp; 51 | } 52 | 53 | public IssuerRequest(JSONObjectReader reader) throws IOException { 54 | pspRequest = new PSPRequest(reader.getObject(PSP_REQUEST)); 55 | payeeHost = reader.getString(PAYEE_HOST); 56 | timeStamp = reader.getDateTime(TIME_STAMP, ISODateTime.COMPLETE); 57 | } 58 | 59 | public IssuerRequest(PSPRequest pspRequest, 60 | String payeeHost, 61 | GregorianCalendar timeStamp) { 62 | this.pspRequest = pspRequest; 63 | this.payeeHost = payeeHost; 64 | this.timeStamp = timeStamp; 65 | } 66 | 67 | public String serialize() throws IOException { 68 | return getWriter().serializeToString(JSONOutputFormats.NORMALIZED); 69 | } 70 | 71 | public JSONObjectWriter getWriter() throws IOException { 72 | return new JSONObjectWriter() 73 | .setObject(PSP_REQUEST, pspRequest.getWriter()) 74 | .setString(PAYEE_HOST, payeeHost) 75 | .setDateTime(TIME_STAMP, timeStamp, ISODateTime.UTC_NO_SUBSECONDS); 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | try { 81 | return getWriter().toString(); 82 | } catch (IOException e) { 83 | throw new RuntimeException(e); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/SystemDetection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | /** 20 | * This is not a part of a serious solution... 21 | * 22 | */ 23 | public class SystemDetection { 24 | 25 | String operatingSystemName = "Unknown"; 26 | String operatingSystemVersion = "N/A"; 27 | String browserName = "Unknown"; 28 | String browserVersion = "N/A"; 29 | 30 | SystemDetection(String userAgent) { 31 | if (userAgent == null) { 32 | return; 33 | } 34 | if (userAgent.contains("Android")) { 35 | operatingSystemName = "Android"; 36 | } else if (userAgent.contains("Win")) { 37 | operatingSystemName = "Windows"; 38 | } else if (userAgent.contains("Linux")) { 39 | operatingSystemName = "Linux"; 40 | } else if (userAgent.contains("iPhone")) { 41 | operatingSystemName = "iOS"; 42 | } else if (userAgent.contains("iPad")) { 43 | operatingSystemName = "iOS"; 44 | } else if (userAgent.contains("Mac OS")) { 45 | operatingSystemName = "Mac OS"; 46 | } 47 | String versionFix = null; 48 | if (userAgent.contains("Edg/")) { 49 | browserName = "Edge"; 50 | versionFix = " Edg"; 51 | } else if (userAgent.contains("EdgA/")) { 52 | browserName = "Edge"; 53 | versionFix = " EdgA"; 54 | } else if (userAgent.contains("Chrome")) { 55 | browserName = "Chrome"; 56 | } else if (userAgent.contains("Safari")) { 57 | browserName = "Safari"; 58 | versionFix = " Version"; 59 | } else if (userAgent.contains("Firefox")) { 60 | browserName = "Firefox"; 61 | } else { 62 | return; 63 | } 64 | String target = versionFix == null ? browserName : versionFix; 65 | int i = userAgent.indexOf(target + "/"); 66 | if (i <= 0) { 67 | return; 68 | } 69 | i += target.length(); 70 | browserVersion = ""; 71 | while (++i < userAgent.length()) { 72 | char c = userAgent.charAt(i); 73 | if (c < '0' || c > '9') { 74 | if (c == '.' && browserName.equals("Safari") && versionFix != null) { 75 | versionFix = null; 76 | } else { 77 | break; 78 | } 79 | } 80 | browserVersion += c; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/FIDOPayServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | import java.io.PrintWriter; 21 | 22 | import java.util.logging.Logger; 23 | import java.util.logging.Level; 24 | 25 | import javax.servlet.ServletException; 26 | 27 | import javax.servlet.http.HttpServlet; 28 | import javax.servlet.http.HttpServletRequest; 29 | import javax.servlet.http.HttpServletResponse; 30 | 31 | import org.webpki.fwp.FWPCrypto; 32 | 33 | import org.webpki.json.JSONObjectReader; 34 | import org.webpki.json.JSONObjectWriter; 35 | 36 | /** 37 | * This Servlet creates Signed Authorization Data (SAD). 38 | * 39 | */ 40 | public class FIDOPayServlet extends HttpServlet { 41 | 42 | private static final long serialVersionUID = 1L; 43 | 44 | static Logger logger = Logger.getLogger(FIDOPayServlet.class.getName()); 45 | 46 | public void doPost(HttpServletRequest request, HttpServletResponse response) 47 | throws IOException, ServletException { 48 | try { 49 | // Get the input (request) data. 50 | JSONObjectReader requestJson = WalletCore.getJSON(request); 51 | 52 | // Get the Authorization Data (AD). 53 | byte[]unsignedAssertion = requestJson.getBinary(WalletCore.FWP_AD); 54 | 55 | // Get the associated FIDO/WebAuthn assertion elements. 56 | byte[] clientDataJSON = requestJson.getBinary(FWPCrypto.CLIENT_DATA_JSON); 57 | byte[] authenticatorData = requestJson.getBinary(FWPCrypto.AUTHENTICATOR_DATA); 58 | byte[] signature = requestJson.getBinary(FWPCrypto.SIGNATURE); 59 | 60 | // Add the assertion elements creating a complete SAD object and return it. 61 | WalletCore.returnJSON(response, new JSONObjectWriter() 62 | .setBinary(WalletCore.FWP_SAD, 63 | FWPCrypto.addSignature(unsignedAssertion, 64 | clientDataJSON, 65 | authenticatorData, 66 | signature))); 67 | 68 | } catch (Exception e) { 69 | String message = e.getMessage(); 70 | logger.log(Level.SEVERE, WalletCore.getStackTrace(e, message)); 71 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 72 | PrintWriter writer = response.getWriter(); 73 | writer.print(message); 74 | writer.flush(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/ReplayCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.nio.ByteBuffer; 20 | 21 | import java.util.concurrent.ConcurrentHashMap; 22 | 23 | import java.util.logging.Logger; 24 | 25 | /** 26 | * Reply cache support. 27 | * 28 | * Replays are only checked within the time limits for authorizations, because 29 | * if a received authorization has already expired, it should be rejected, 30 | * rather than being cached. 31 | * 32 | */ 33 | public enum ReplayCache { 34 | 35 | // According to multiple Java information resources, the "enum" type represents 36 | // a viable option for creating singletons in multi-threaded applications. 37 | INSTANCE; 38 | 39 | private Logger logger = Logger.getLogger(ReplayCache.class.getName()); 40 | 41 | private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); 42 | 43 | private ReplayCache() { 44 | new Thread(new Runnable() { 45 | 46 | @Override 47 | public void run() { 48 | while (true) { 49 | try { 50 | Thread.sleep(IssuerServlet.AUTHORIZATION_MAX_AGE / 5); 51 | long now = System.currentTimeMillis(); 52 | cache.forEach((hashableSadObject, expirationTime) -> { 53 | if (expirationTime < now) { 54 | // The authorization has apparently expired so we can safely 55 | // remove it from the replay cache in order to keep the cache 56 | // as small and up-to-date as possible. 57 | cache.remove(hashableSadObject); 58 | logger.info("Removed authorization token: " + 59 | hashableSadObject.hashCode()); 60 | } 61 | }); 62 | } catch (InterruptedException e) { 63 | new RuntimeException("Unexpected interrupt", e); 64 | } 65 | } 66 | } 67 | 68 | }).start(); 69 | } 70 | 71 | /** 72 | * Add validated SAD object to the replay cache. 73 | * 74 | * Note: the expirationTime stays the same for replayed SAD objects, 75 | * making rewrites benign. 76 | * 77 | * @param hashableSadObject The SAD object packaged to suit HashMap 78 | * @param expirationTime For the SAD object 79 | * @return true if replay, else false 80 | */ 81 | public boolean add(ByteBuffer hashableSadObject, long expirationTime) { 82 | return cache.put(hashableSadObject, expirationTime) != null; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /web/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin:10pt; 3 | font-size:10pt; 4 | font-family:Roboto,sans-serif,"Segoe UI"; 5 | } 6 | 7 | a { 8 | color:#007fff; 9 | text-decoration:none; 10 | outline:none; 11 | font-weight:bold; 12 | } 13 | 14 | .actor { 15 | color:darkgreen; 16 | font-weight:500; 17 | } 18 | 19 | .ctbl { 20 | padding-bottom:1em; 21 | word-break:break-all; 22 | font-family:"Noto Mono",monospace; 23 | } 24 | 25 | .ctblh { 26 | padding-bottom:0.2em; 27 | font-weight:bolder; 28 | } 29 | 30 | li { 31 | padding-top:5pt 32 | } 33 | 34 | .staticbox, .textbox { 35 | font-family:"Noto Mono",monospace; 36 | margin-top: 1.5em; 37 | box-sizing:border-box; 38 | width:100%;word-break:break-all; 39 | border-width:1px; 40 | border-style:solid; 41 | border-color:grey; 42 | padding:10pt; 43 | } 44 | 45 | .staticbox { 46 | background:#f8f8f8; 47 | } 48 | 49 | .textbox { 50 | background:#ffffea; 51 | } 52 | 53 | .header { 54 | text-align:center; 55 | font-weight:bolder; 56 | font-size:12pt; 57 | } 58 | 59 | .toasting { 60 | border-color:#c85000; 61 | border-style:solid; 62 | border-width:1pt; 63 | text-align:left; 64 | border-radius:7px; 65 | z-index:8; 66 | background-color:#fffdf2; 67 | position:absolute; 68 | visibility:hidden; 69 | padding:5pt 10pt; 70 | } 71 | 72 | .sitefooter { 73 | display:flex; 74 | align-items:center; 75 | border-width:1px 0 0 0; 76 | border-style:solid; 77 | position:absolute; 78 | z-index:-5; 79 | left:0px; 80 | bottom:0px; 81 | right:0px; 82 | font-size: 8pt; 83 | padding:0.5em 0.7em; 84 | border-color:#c85000; 85 | background-color:#fffdf2; 86 | } 87 | 88 | .payimage { 89 | cursor:pointer; 90 | height:3em; 91 | } 92 | 93 | .stdbtn, .multibtn { 94 | cursor:pointer; 95 | background:linear-gradient(to bottom, #eaeaea 14%,#fcfcfc 52%,#e5e5e5 89%); 96 | border-width:1px; 97 | border-style:solid; 98 | border-color:#a9a9a9; 99 | border-radius:5pt; 100 | padding:3pt 10pt; 101 | } 102 | 103 | .important { 104 | display:flex; 105 | justify-content:center; 106 | margin-top:15pt; 107 | color:#4366bf; 108 | font-weight:bold; 109 | } 110 | 111 | .comment { 112 | max-width: 40em; 113 | padding:0.5em 1em; 114 | background-color:#fffdf2; 115 | box-shadow: 0.2em 0.2em 0.2em #d0d0d0; 116 | border-width:1px; 117 | border-style:solid; 118 | border-color:black; 119 | } 120 | 121 | .card { 122 | width:20em; 123 | max-width:80%; 124 | margin-top:1.5em; 125 | } 126 | 127 | .errorText { 128 | color:red; 129 | font-weight:bold; 130 | text-align:center; 131 | padding-top:3em; 132 | display:none; 133 | } 134 | 135 | .stdbtn, .multibtn, .staticbox, .textbox { 136 | box-shadow:3pt 3pt 3pt #d0d0d0; 137 | } 138 | 139 | .stdbtn { 140 | display:inline-block; 141 | margin-top:15pt; 142 | } 143 | 144 | .multibtn { 145 | margin-top:12pt; 146 | text-align:center; 147 | } 148 | 149 | .tftable { 150 | border-collapse:collapse; 151 | margin-left:auto; 152 | margin-right:auto; 153 | } 154 | 155 | .tftable td { 156 | background-color: #fffdf2; 157 | padding: 0.4em 0.5em; 158 | border-width: 1px; 159 | border-style: solid; 160 | border-color: black; 161 | word-break:break-all; 162 | } 163 | 164 | .tftable th { 165 | font-weight: normal; 166 | padding: 0.4em 0.5em; 167 | text-align: right; 168 | background-color: #f8f8f8; 169 | border-width: 1px; 170 | border-style: solid; 171 | border-color: black; 172 | } 173 | 174 | @media (max-width:768px) { 175 | 176 | .stdbtn, .multibtn, .staticbox, .textbox { 177 | box-shadow:2pt 2pt 2pt #d0d0d0; 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/CardServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 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 | /** 28 | * 29 | * Provides filled-in card images to the WalletUIServlet 30 | * 31 | */ 32 | public class CardServlet extends HttpServlet { 33 | 34 | private static final long serialVersionUID = 1L; 35 | 36 | static final String ACCOUNT = "p1"; 37 | static final String USER = "p2"; 38 | 39 | String getParameter(String name, HttpServletRequest request) { 40 | String value = request.getParameter(name); 41 | if (value == null) { 42 | return "Undefined"; 43 | } 44 | return value; 45 | } 46 | 47 | public void doGet(HttpServletRequest request, HttpServletResponse response) 48 | throws IOException, ServletException { 49 | 50 | String user = getParameter(USER, request); 51 | String account = getParameter(ACCOUNT, request); 52 | boolean iban = account.startsWith("FR"); 53 | String svg = new StringBuilder( 54 | "" + 55 | "" + 56 | "" + 57 | "" + 58 | "" + 59 | "" + 60 | "") 61 | .append( 62 | iban ? 63 | "" + 64 | "" + 65 | "" 66 | : 67 | "" + 68 | "" + 69 | "" 70 | ) 71 | .append( 72 | "" + 73 | "" + 74 | "" + 75 | "" + 76 | "" + 77 | USER + 78 | "" + 79 | "" + 80 | ACCOUNT + 81 | "" + 82 | "") 83 | .append(iban ? "BankNet2" : "Supercard") 84 | .append( 85 | "" + 86 | "").toString().replace(USER, user).replace(ACCOUNT, account); 87 | 88 | WalletCore.returnSVG(response, svg); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/HomeServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 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 | private static final String BUTTONS_ID = "buttons"; 32 | private static final String FAILED_ID = "failed"; 33 | 34 | public void doGet(HttpServletRequest request, HttpServletResponse response) 35 | throws IOException, ServletException { 36 | 37 | HTML.standardPage(response, 38 | Actors.SITE, 39 | "window.addEventListener('load', function(event) {\n" + 40 | " if (!window.PublicKeyCredential) {\n" + 41 | " document.getElementById('" + BUTTONS_ID + 42 | "').style.display = 'none';\n" + 43 | " document.getElementById('" + FAILED_ID + 44 | "').style.display = 'block';\n" + 45 | " }\n" + 46 | "});\n", 47 | new StringBuilder( 48 | "
FIDO® Web Pay (FWP) Demo
" + 49 | 50 | "
This site permits testing and debugging " + 51 | "a scheme for a universal payment authorization system based on FIDO2. " + 52 | "Due to the lack of built-in browser support, the "Wallet" UI is " + 53 | "currently implemented as a Web emulator." + 54 | "

Note that you can always return to the main menu by clicking " + 55 | "" + 56 | "the lab

" + 57 | "
" + 58 | 59 | "
" + 60 | "" + 61 | 62 | "" + 65 | 66 | "" + 69 | 70 | WalletAdminServlet.WALLET_ADMIN_BUTTON + 71 | 72 | "" + 75 | 76 | "
" + 63 | "Buy Something!" + 64 | "
" + 67 | "Enroll Payment Cards..." + 68 | "
" + 73 | ""WebAuthn" Login..." + 74 | "
" + 77 | "
" + 78 | 79 | "
" + 80 | "Your browser does not support FIDO2/WebAuthn" + 81 | "
" + 82 | 83 | "")); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/org/webpki/fwp/PSPRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.util.GregorianCalendar; 22 | 23 | import org.webpki.json.JSONObjectReader; 24 | import org.webpki.json.JSONObjectWriter; 25 | import org.webpki.json.JSONOutputFormats; 26 | 27 | import org.webpki.util.ISODateTime; 28 | 29 | /** 30 | * Sample PSP request matching the FWP documentation. 31 | */ 32 | public class PSPRequest { 33 | 34 | public static final String PAYMENT_REQUEST = "paymentRequest"; 35 | public static final String FWP_ASSERTION = "fwpAssertion"; 36 | public static final String RECEIVE_ACCOUNT = "receiveAccount"; 37 | public static final String CLIENT_IP_ADDRESS = "clientIpAddress"; 38 | public static final String TIME_STAMP = "timeStamp"; 39 | 40 | 41 | FWPPaymentRequest paymentRequest; 42 | public FWPPaymentRequest getPaymentRequest() { 43 | return paymentRequest; 44 | } 45 | 46 | FWPJsonAssertion fwpAssertion; 47 | public FWPJsonAssertion getFwpAssertion() { 48 | return fwpAssertion; 49 | } 50 | 51 | String receiveAccount; 52 | public String getReceiveAccount() { 53 | return receiveAccount; 54 | } 55 | 56 | String clientIpAddress; 57 | public String getClientIpAddress() { 58 | return clientIpAddress; 59 | } 60 | 61 | GregorianCalendar timeStamp; 62 | public GregorianCalendar getTimeStamp() { 63 | return timeStamp; 64 | } 65 | 66 | JSONObjectReader reader; 67 | 68 | public PSPRequest(JSONObjectReader reader) throws IOException { 69 | this.reader = reader; 70 | paymentRequest = new FWPPaymentRequest(reader.getObject(PAYMENT_REQUEST)); 71 | fwpAssertion = new FWPJsonAssertion(reader.getObject(FWP_ASSERTION)); 72 | receiveAccount = reader.getString(RECEIVE_ACCOUNT); 73 | clientIpAddress = reader.getString(CLIENT_IP_ADDRESS); 74 | timeStamp = reader.getDateTime(TIME_STAMP, ISODateTime.COMPLETE); 75 | } 76 | 77 | public PSPRequest(FWPPaymentRequest paymentRequest, 78 | FWPJsonAssertion fwpAssertion, 79 | String receiveAccount, 80 | String clientIpAddress, 81 | GregorianCalendar timeStamp) { 82 | this.paymentRequest = paymentRequest; 83 | this.fwpAssertion = fwpAssertion; 84 | this.receiveAccount = receiveAccount; 85 | this.clientIpAddress = clientIpAddress; 86 | this.timeStamp = timeStamp; 87 | } 88 | 89 | public String serialize() throws IOException { 90 | return getWriter().serializeToString(JSONOutputFormats.NORMALIZED); 91 | } 92 | 93 | public JSONObjectWriter getWriter() throws IOException { 94 | return new JSONObjectWriter() 95 | .setObject(PAYMENT_REQUEST, paymentRequest.getWriter()) 96 | .setObject(FWP_ASSERTION, fwpAssertion.getWriter()) 97 | .setString(RECEIVE_ACCOUNT, receiveAccount) 98 | .setString(CLIENT_IP_ADDRESS, clientIpAddress) 99 | .setDateTime(TIME_STAMP, timeStamp, ISODateTime.UTC_NO_SUBSECONDS); 100 | } 101 | 102 | @Override 103 | public String toString() { 104 | try { 105 | return getWriter().toString(); 106 | } catch (IOException e) { 107 | throw new RuntimeException(e); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/admin/RegistrationListServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp.admin; 18 | 19 | import java.io.IOException; 20 | 21 | import java.sql.Connection; 22 | import java.sql.PreparedStatement; 23 | import java.sql.ResultSet; 24 | 25 | import java.util.logging.Logger; 26 | 27 | import javax.servlet.ServletException; 28 | 29 | import javax.servlet.http.HttpServlet; 30 | import javax.servlet.http.HttpServletRequest; 31 | import javax.servlet.http.HttpServletResponse; 32 | 33 | import org.webpki.webapps.fwp.Actors; 34 | import org.webpki.webapps.fwp.HTML; 35 | import org.webpki.webapps.fwp.ApplicationService; 36 | /** 37 | * For listing site activity... 38 | * 39 | */ 40 | public class RegistrationListServlet extends HttpServlet { 41 | 42 | static Logger logger = Logger.getLogger(RegistrationListServlet.class.getName()); 43 | 44 | private static final long serialVersionUID = 1L; 45 | 46 | String convertToOptionalString(int value) { 47 | return "" + (value == 0 ? "-" : String.valueOf(value)); 48 | } 49 | 50 | public void doGet(HttpServletRequest request, HttpServletResponse response) 51 | throws IOException, ServletException { 52 | request.setCharacterEncoding("utf-8"); 53 | try { 54 | StringBuilder html = new StringBuilder( 55 | "
Successful Registrations
" + 56 | 57 | "
" + 58 | "" + 59 | "" + 60 | "" + 61 | "" + 62 | "" + 63 | "" + 64 | "" + 65 | ""); 66 | 67 | try (Connection connection = ApplicationService.jdbcDataSource.getConnection();) { 68 | try (PreparedStatement stmt = connection.prepareStatement( 69 | "SELECT UserId, Created, ClientIpAddress, " + 70 | "ClientHost, WebAuthn, BasicBuy, FWPSteps from USERS " + 71 | "WHERE PublicKey IS NOT NULL " + 72 | "ORDER BY Created DESC LIMIT 100;");) { 73 | try (ResultSet rs = stmt.executeQuery();) { 74 | while (rs.next()) { 75 | String host = rs.getString(4); 76 | html.append(""); 88 | } 89 | } 90 | } 91 | } 92 | 93 | html.append("
User IDCreatedIP AddressHost NameWebAuthnBasic BuyFWP Steps
") 77 | .append(rs.getString(1)) 78 | .append("") 79 | .append(rs.getString(2)) 80 | .append("") 81 | .append(rs.getString(3)) 82 | .append("") 83 | .append(host == null ? "
-
" : host) 84 | .append(convertToOptionalString(rs.getInt(5))) 85 | .append(convertToOptionalString(rs.getInt(6))) 86 | .append(convertToOptionalString(rs.getInt(7))) 87 | .append("
" + 94 | "
"); 95 | 96 | HTML.standardPage(response, Actors.ADMIN, null, html); 97 | } catch (Exception e) { 98 | HTML.errorPage(response, e); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/org/webpki/fwp/FWPPaymentRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import org.webpki.cbor.CBORInt; 20 | import org.webpki.cbor.CBORMap; 21 | import org.webpki.cbor.CBORObject; 22 | import org.webpki.cbor.CBORString; 23 | 24 | import org.webpki.json.JSONObjectReader; 25 | import org.webpki.json.JSONObjectWriter; 26 | import org.webpki.json.JSONOutputFormats; 27 | 28 | 29 | /** 30 | * The FWP PaymentRequest in JSON and CBOR format. 31 | */ 32 | public class FWPPaymentRequest { 33 | 34 | // Payment Request constants in CBOR 35 | public static final CBORInt CBOR_PR_PAYEE_NAME = new CBORInt(1); 36 | public static final CBORInt CBOR_PR_REQUEST_ID = new CBORInt(2); 37 | public static final CBORInt CBOR_PR_AMOUNT = new CBORInt(3); 38 | public static final CBORInt CBOR_PR_CURRENCY = new CBORInt(4); 39 | 40 | // Payment Request constants in JSON 41 | public static final String JSON_PR_PAYEE_NAME = "payeeName"; 42 | public static final String JSON_PR_REQUEST_ID = "requestId"; 43 | public static final String JSON_PR_AMOUNT = "amount"; 44 | public static final String JSON_PR_CURRENCY = "currency"; 45 | 46 | String payeeName; 47 | public String getPayeeName() { 48 | return payeeName; 49 | } 50 | 51 | String requestId; 52 | public String getRequestId() { 53 | return requestId; 54 | } 55 | 56 | String currency; 57 | public String getCurrency() { 58 | return currency; 59 | } 60 | 61 | String amount; 62 | public String getAmount() { 63 | return amount; 64 | } 65 | 66 | public FWPPaymentRequest(JSONObjectReader reader) { 67 | payeeName = reader.getString(JSON_PR_PAYEE_NAME); 68 | requestId = reader.getString(JSON_PR_REQUEST_ID); 69 | amount = reader.getString(JSON_PR_AMOUNT); 70 | currency = reader.getString(JSON_PR_CURRENCY); 71 | reader.checkForUnread(); 72 | } 73 | 74 | public FWPPaymentRequest(CBORObject cborObject) { 75 | CBORMap cborPaymentRequest = cborObject.getMap(); 76 | payeeName = cborPaymentRequest.get(CBOR_PR_PAYEE_NAME).getString(); 77 | requestId = cborPaymentRequest.get(CBOR_PR_REQUEST_ID).getString(); 78 | amount = cborPaymentRequest.get(CBOR_PR_AMOUNT).getString(); 79 | currency = cborPaymentRequest.get(CBOR_PR_CURRENCY).getString(); 80 | cborObject.checkForUnread(); 81 | } 82 | 83 | public FWPPaymentRequest(String payeeName, 84 | String requestId, 85 | String amount, 86 | String currency) { 87 | this.payeeName = payeeName; 88 | this.requestId = requestId; 89 | this.amount = amount; 90 | this.currency = currency; 91 | } 92 | 93 | public String serializeAsJSON() { 94 | return serializeAsJSON(JSONOutputFormats.NORMALIZED); 95 | } 96 | 97 | public String serializeAsJSON(JSONOutputFormats format) { 98 | return getWriter().serializeToString(format); 99 | } 100 | 101 | public CBORMap serializeAsCBOR() { 102 | return new CBORMap() 103 | .set(CBOR_PR_PAYEE_NAME, new CBORString(payeeName)) 104 | .set(CBOR_PR_REQUEST_ID, new CBORString(requestId)) 105 | .set(CBOR_PR_AMOUNT, new CBORString(amount)) 106 | .set(CBOR_PR_CURRENCY, new CBORString(currency)); 107 | } 108 | 109 | public JSONObjectWriter getWriter() { 110 | return new JSONObjectWriter() 111 | .setString(JSON_PR_PAYEE_NAME, payeeName) 112 | .setString(JSON_PR_REQUEST_ID, requestId) 113 | .setString(JSON_PR_AMOUNT, amount) 114 | .setString(JSON_PR_CURRENCY, currency); 115 | } 116 | 117 | @Override 118 | public String toString() { 119 | return getWriter().toString(); 120 | } 121 | 122 | @Override 123 | public boolean equals(Object o) { 124 | return serializeAsCBOR().equals(((FWPPaymentRequest)o).serializeAsCBOR()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/SADServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.util.logging.Logger; 22 | 23 | import javax.servlet.ServletException; 24 | 25 | import javax.servlet.http.HttpServlet; 26 | import javax.servlet.http.HttpServletRequest; 27 | import javax.servlet.http.HttpServletResponse; 28 | 29 | import org.webpki.cbor.CBORDecoder; 30 | 31 | /** 32 | * Receives and shows the Signed Authorization Data (SAD). 33 | * 34 | */ 35 | public class SADServlet extends HttpServlet { 36 | 37 | static Logger logger = Logger.getLogger(SADServlet.class.getName()); 38 | 39 | private static final long serialVersionUID = 1L; 40 | 41 | // DIV elements to turn on and turn off. 42 | private static final String WAITING_ID = "wait"; 43 | private static final String ACTIVATE_ID = "activate"; 44 | 45 | public void doPost(HttpServletRequest request, HttpServletResponse response) 46 | throws IOException, ServletException { 47 | request.setCharacterEncoding("utf-8"); 48 | String signedAuthorizationB64U = request.getParameter(WalletCore.FWP_SAD); 49 | if (signedAuthorizationB64U == null) { 50 | WalletCore.failed("Missing signed authorization data"); 51 | return; 52 | } 53 | String walletInternal = request.getParameter(WalletCore.WALLET_INTERNAL); 54 | if (walletInternal == null) { 55 | WalletCore.failed("Missing wallet data"); 56 | return; 57 | } 58 | logger.info("Successful authorization by: " + WalletCore.getWalletCookie(request)); 59 | StringBuilder html = new StringBuilder( 60 | "
" + 61 | "" + 65 | "" + 69 | "
" + 70 | 71 | "
Signed Authorization Data (SAD)
" + 72 | 73 | "
" + 74 | "
") 75 | .append(ADServlet.sectionReference("seq-4.3")) 76 | .append( 77 | ": The FIDO signature has now been added. " + 78 | "
👉 Note that the Web emulator " + 79 | "for compatibility with browsers uses a slightly different signature " + 80 | "scheme than the specification, " + 81 | "requiring clientDataJSON as well 👈
" + 82 | "
Since FWP is a privacy-centric scheme, " + 83 | "the authorization data is not yet ready for release.
" + 84 | "
" + 85 | "
" + 86 | 87 | "
" + 88 | "waiting" + 90 | "
" + 91 | 92 | "
" + 93 | "
" + 94 | "Encrypt Authorization" + 95 | "
" + 96 | "
" + 97 | 98 | "
") 99 | .append(HTML.encode(CBORDecoder.decode( 100 | ApplicationService.base64UrlDecode(signedAuthorizationB64U)).toString(), true)) 101 | .append( 102 | "
"); 103 | 104 | String js = new StringBuilder( 105 | 106 | WalletCore.GO_HOME_JAVASCRIPT + 107 | 108 | "function doEncrypt() {\n" + 109 | " document.getElementById('" + ACTIVATE_ID + "').style.display = 'none';\n" + 110 | " document.getElementById('" + WAITING_ID + "').style.display = 'block';\n" + 111 | " setTimeout(function() {\n" + 112 | " document.forms.shoot.submit();\n" + 113 | " }, 500);\n" + 114 | "}\n").toString(); 115 | 116 | HTML.standardPage(response, Actors.FWP, js, html); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/ApplicationService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | 22 | import java.security.KeyPair; 23 | 24 | import java.util.Base64; 25 | 26 | import java.util.logging.Level; 27 | import java.util.logging.Logger; 28 | 29 | import javax.naming.Context; 30 | import javax.naming.InitialContext; 31 | 32 | import javax.sql.DataSource; 33 | 34 | import javax.servlet.ServletContextEvent; 35 | import javax.servlet.ServletContextListener; 36 | 37 | import org.webpki.cbor.CBORString; 38 | 39 | import org.webpki.crypto.CustomCryptoProvider; 40 | import org.webpki.crypto.ContentEncryptionAlgorithms; 41 | import org.webpki.crypto.KeyEncryptionAlgorithms; 42 | 43 | import org.webpki.jose.JOSEKeyWords; 44 | 45 | import org.webpki.json.JSONParser; 46 | 47 | import org.webpki.util.IO; 48 | import org.webpki.util.UTF8; 49 | import org.webpki.webutil.InitPropertyReader; 50 | 51 | /** 52 | * A single service for the whole application?! Yes, this is an emulator, not a product :) 53 | */ 54 | public class ApplicationService extends InitPropertyReader implements ServletContextListener { 55 | 56 | static Logger logger = Logger.getLogger(ApplicationService.class.getName()); 57 | 58 | public static DataSource jdbcDataSource; 59 | 60 | static KeyPair issuerEncryptionKey; 61 | 62 | static CBORString issuerEncryptionKeyId = new CBORString("x25519:2021:01"); 63 | 64 | static String issuerId = "https://mybank.fr/payment"; 65 | 66 | static KeyEncryptionAlgorithms issuerKeyEncryptionAlgorithm = 67 | KeyEncryptionAlgorithms.ECDH_ES_A256KW; 68 | 69 | static ContentEncryptionAlgorithms issuerContentEncryptionAlgorithm = 70 | ContentEncryptionAlgorithms.A256GCM; 71 | 72 | static String samplePayeeHostname = "spaceshop.com"; 73 | 74 | static boolean logging; 75 | 76 | 77 | static String base64UrlEncode(byte[] bytes) { 78 | return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); 79 | } 80 | 81 | static byte[] base64UrlDecode(String b64u) { 82 | return Base64.getUrlDecoder().decode(b64u); 83 | } 84 | 85 | byte[] getEmbeddedResource(String name) throws IOException { 86 | InputStream is = this.getClass().getResourceAsStream(name); 87 | if (is == null) { 88 | throw new IOException("Resource fail for: " + name); 89 | } 90 | return IO.getByteArrayFromInputStream(is); 91 | } 92 | 93 | String getEmbeddedResourceString(String name) throws IOException { 94 | return UTF8.decode(getEmbeddedResource(name)).trim(); 95 | } 96 | 97 | @Override 98 | public void contextDestroyed(ServletContextEvent event) { 99 | } 100 | 101 | @Override 102 | public void contextInitialized(ServletContextEvent event) { 103 | initProperties(event); 104 | CustomCryptoProvider.forcedLoad(false); 105 | try { 106 | 107 | //=========================================================================================// 108 | // Logging? 109 | //=========================================================================================// 110 | logging = getPropertyBoolean("logging"); 111 | 112 | //=========================================================================================// 113 | // Hard coded issuer data 114 | //=========================================================================================// 115 | issuerEncryptionKey = JSONParser.parse(getEmbeddedResource("x25519privatekey.jwk")) 116 | .removeProperty(JOSEKeyWords.KID_JSON).getKeyPair(); 117 | 118 | //=========================================================================================// 119 | // Database 120 | //=========================================================================================// 121 | Context initContext = new InitialContext(); 122 | Context envContext = (Context)initContext.lookup("java:/comp/env"); 123 | jdbcDataSource = (DataSource)envContext.lookup("jdbc/FWP"); 124 | DataBaseOperations.testConnection(); 125 | 126 | logger.info("FWP Demo Successfully Initiated"); 127 | } catch (Exception e) { 128 | logger.log(Level.SEVERE, "********\n" + e.getMessage() + "\n********", e); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/WalletAdminServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | //import java.sql.Connection; 22 | 23 | import java.util.logging.Logger; 24 | 25 | import javax.servlet.ServletException; 26 | 27 | import javax.servlet.http.Cookie; 28 | import javax.servlet.http.HttpServlet; 29 | import javax.servlet.http.HttpServletRequest; 30 | import javax.servlet.http.HttpServletResponse; 31 | 32 | /** 33 | * 34 | * Horribly primitive wallet administration UI. 35 | * 36 | */ 37 | 38 | public class WalletAdminServlet extends HttpServlet { 39 | 40 | static Logger logger = Logger.getLogger(WalletAdminServlet.class.getName()); 41 | 42 | private static final long serialVersionUID = 1L; 43 | 44 | static final String WALLET_ADMIN_BUTTON = 45 | "
" + 46 | "Wallet Administration..." + 47 | "
"; 48 | 49 | 50 | public void doGet(HttpServletRequest request, HttpServletResponse response) 51 | throws IOException, ServletException { 52 | try { 53 | StringBuilder html = new StringBuilder( 54 | "
Wallet Administration
" + 55 | 56 | "
" + 57 | "
" + 58 | "The "Wallet" is a central part of FWP since it holds all " + 59 | "related payment cards" + 60 | "
" + 61 | "
"); 62 | 63 | html.append(WalletCore.hasPaymentCards(request) ? 64 | 65 | "
" + 66 | "
" + 67 | "
" + 68 | "Delete Cards!" + 69 | "
" + 70 | "
" 71 | : 72 | "
" + 73 | "You have not yet enrolled the FIDO wallet." + 74 | "
" + 75 | 76 | "
" + 77 | "
" + 78 | "Go to Enrollment!" + 79 | "
" + 80 | "
"); 81 | 82 | HTML.standardPage(response, Actors.WALLET, null, html); 83 | } catch (Exception e) { 84 | HTML.errorPage(response, e); 85 | } 86 | } 87 | 88 | public void doPost(HttpServletRequest request, HttpServletResponse response) 89 | throws IOException, ServletException { 90 | try { 91 | // The user ID is stored in a persistent cookie. 92 | String userId = WalletCore.getWalletCookie(request); 93 | 94 | // This is the only database call needed for deleting payment cards (all of them...). 95 | 96 | /* To not destroy statistics we removed this step and only clear the wallet cookie 97 | try (Connection connection = ApplicationService.jdbcDataSource.getConnection();) { 98 | DataBaseOperations.deletePaymentCards(userId, connection); 99 | } 100 | */ 101 | 102 | // Tell the user that it worked... 103 | StringBuilder html = new StringBuilder( 104 | "
Payment Cards Deleted
" + 105 | "
" + 106 | 107 | "
" + 108 | "You have now disenrolled the Wallet. " + 109 | "Thank you for testing, we hope you liked it 😁" + 110 | "
" + 111 | "
"); 112 | 113 | // We remove the cookie as well. 114 | Cookie walletCookie = new Cookie(WalletCore.WALLET_COOKIE, ""); 115 | walletCookie.setMaxAge(0); // 100 days. 116 | walletCookie.setSecure(true); 117 | response.addCookie(walletCookie); 118 | 119 | HTML.standardPage(response, Actors.WALLET, null, html); 120 | 121 | logger.info("Deleted payment cards for user: " + userId); 122 | } catch (Exception e) { 123 | HTML.errorPage(response, e); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/org/webpki/fwp/FWPAssertionBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.util.GregorianCalendar; 20 | import java.util.HashSet; 21 | 22 | import org.webpki.cbor.CBORMap; 23 | import org.webpki.cbor.CBORObject; 24 | import org.webpki.cbor.CBORString; 25 | import org.webpki.cbor.CBORArray; 26 | import org.webpki.cbor.CBORFloat; 27 | import org.webpki.cbor.CBORDiagnosticNotation; 28 | 29 | import org.webpki.fwp.FWPCrypto.FWPPreSigner; 30 | 31 | import org.webpki.util.ISODateTime; 32 | 33 | /** 34 | * FWP client side assertion (AD) support. 35 | */ 36 | public class FWPAssertionBuilder { 37 | 38 | CBORMap fwpAssertion = new CBORMap(); 39 | 40 | HashSet elementList = new HashSet<>(); 41 | 42 | private FWPAssertionBuilder setElement(FWPElements name, CBORObject value) { 43 | if (!elementList.add(name)) { 44 | throw new FWPException("Duplicate: " + name.toString()); 45 | } 46 | fwpAssertion.set(name.cborLabel, value); 47 | return this; 48 | } 49 | 50 | private FWPAssertionBuilder setStringElement(FWPElements element, String string) { 51 | return setElement(element, new CBORString(string)); 52 | } 53 | 54 | private CBORMap nameVersion(String name, String version) { 55 | return new CBORMap().set(FWPElements.CBOR_PDSUB_NAME, new CBORString(name)) 56 | .set(FWPElements.CBOR_PDSUB_VERSION, new CBORString(version)); 57 | } 58 | 59 | public FWPAssertionBuilder setPaymentRequest(FWPPaymentRequest jsonPaymentRequest) { 60 | return setElement(FWPElements.PAYMENT_REQUEST, jsonPaymentRequest.serializeAsCBOR()); 61 | } 62 | 63 | public FWPAssertionBuilder setPayeeHost(String payeeHost) { 64 | return setStringElement(FWPElements.PAYEE_HOST, payeeHost); 65 | } 66 | 67 | public FWPAssertionBuilder setPlatformData(String osName, 68 | String osVersion, 69 | String browserName, 70 | String browserVersion) { 71 | return setElement(FWPElements.PLATFORM_DATA, 72 | new CBORMap().set(FWPElements.CBOR_PD_OPERATING_SYSTEM, 73 | nameVersion(osName, osVersion)) 74 | .set(FWPElements.CBOR_PD_USER_AGENT, 75 | nameVersion(browserName, browserVersion))); 76 | } 77 | 78 | public FWPAssertionBuilder setPaymentInstrumentData(String accountId, 79 | String serialNumber, 80 | String paymentNetworkId) { 81 | setStringElement(FWPElements.ACCOUNT_ID, accountId); 82 | setStringElement(FWPElements.SERIAL_NUMBER, serialNumber); 83 | setStringElement(FWPElements.PAYMENT_NETWORK_ID, paymentNetworkId); 84 | return this; 85 | } 86 | 87 | public FWPAssertionBuilder setLocation(double latitude, double longitude) { 88 | setElement(FWPElements.LOCATION, 89 | new CBORArray() 90 | .add(new CBORFloat(latitude)) 91 | .add(new CBORFloat(longitude))); 92 | return this; 93 | } 94 | 95 | public FWPAssertionBuilder setOptionalTimeStamp(GregorianCalendar timeStamp) { 96 | return setElement(FWPElements.TIME_STAMP, 97 | new CBORString(ISODateTime.encode(timeStamp, 98 | ISODateTime.LOCAL_NO_SUBSECONDS))); 99 | } 100 | 101 | public FWPAssertionBuilder setNetworkOptions(String jsonStringOrNull) { 102 | return jsonStringOrNull == null ? this : 103 | setElement(FWPElements.NETWORK_OPTIONS, 104 | CBORDiagnosticNotation.convert(jsonStringOrNull)); 105 | } 106 | 107 | public byte[] create(FWPPreSigner fwpPreSigner) { 108 | // Default time is now. 109 | if (!elementList.contains(FWPElements.TIME_STAMP)) { 110 | setOptionalTimeStamp(new GregorianCalendar()); 111 | } 112 | setElement(FWPElements.AUTHORIZATION, fwpPreSigner.appendSignatureObject()); 113 | for (FWPElements name : FWPElements.values()) { 114 | // NETWORK_DATA and LOCATION are optional. 115 | if (!elementList.contains(name) && 116 | name != FWPElements.NETWORK_OPTIONS && 117 | name != FWPElements.LOCATION) { 118 | throw new FWPException("Missing element: " + name.toString()); 119 | } 120 | } 121 | // Attempts rebuilding will return NPE. 122 | elementList = null; 123 | return fwpAssertion.encode(); 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/PSPServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.util.GregorianCalendar; 22 | 23 | import java.util.logging.Logger; 24 | 25 | import javax.servlet.ServletException; 26 | 27 | import javax.servlet.http.HttpServlet; 28 | import javax.servlet.http.HttpServletRequest; 29 | import javax.servlet.http.HttpServletResponse; 30 | 31 | import org.webpki.fwp.IssuerRequest; 32 | import org.webpki.fwp.PSPRequest; 33 | 34 | import org.webpki.json.JSONParser; 35 | 36 | /** 37 | * PSP step. 38 | * 39 | */ 40 | public class PSPServlet extends HttpServlet { 41 | 42 | static Logger logger = Logger.getLogger(PSPServlet.class.getName()); 43 | 44 | private static final long serialVersionUID = 1L; 45 | 46 | public static final String PSP_REQUEST = "pspRequest"; 47 | 48 | // DIV elements to turn on and turn off. 49 | private static final String WAITING_ID = "wait"; 50 | private static final String ACTIVATE_ID = "activate"; 51 | 52 | public void doPost(HttpServletRequest request, HttpServletResponse response) 53 | throws IOException, ServletException { 54 | request.setCharacterEncoding("utf-8"); 55 | String pspRequest = request.getParameter(PSP_REQUEST); 56 | if (pspRequest == null) { 57 | WalletCore.failed("Missing PSP request"); 58 | return; 59 | } 60 | PSPRequest decodedPspRequest = new PSPRequest(JSONParser.parse(pspRequest)); 61 | 62 | // This is wrong, PSPs have databases with merchant data. 63 | String payeeName = decodedPspRequest.getPaymentRequest().getPayeeName(); 64 | if (!payeeName.equals("Space Shop")) { 65 | throw new IOException("Unexpected merchant name: " + payeeName); 66 | } 67 | 68 | // Russian doll messaging is cool. 69 | IssuerRequest issuerRequest = 70 | new IssuerRequest(decodedPspRequest, 71 | // This is wrong, PSPs have databases with merchant data. 72 | request.getServerName(), 73 | new GregorianCalendar()); 74 | StringBuilder html = new StringBuilder( 75 | "
" + 76 | "" + 81 | "
" + 82 | 83 | "
PSP Process
" + 84 | 85 | "
" + 86 | "
") 87 | .append(ADServlet.sectionReference("seq-8")) 88 | .append( 89 | ": The PSP has received a payment request message " + 90 | "from the Merchant, " + 91 | "and now needs to route the request to the proper Issuer. " + 92 | "Although not shown here, " + 93 | "the PSP also authenticates the " + 94 | "Merchant." + 95 | "
Below is a non-normative " + 96 | "sample Issuer message.
" + 97 | "
" + 98 | "
" + 99 | 100 | "
" + 101 | "waiting" + 103 | "
" + 104 | 105 | "
" + 106 | "
" + 107 | "Send Request to Issuer" + 108 | "
" + 109 | "
" + 110 | 111 | "
") 112 | .append(HTML.encode(issuerRequest.toString(), true)) 113 | .append( 114 | "
"); 115 | String js = new StringBuilder( 116 | 117 | WalletCore.GO_HOME_JAVASCRIPT + 118 | 119 | "function doReturn() {\n" + 120 | " document.getElementById('" + ACTIVATE_ID + "').style.display = 'none';\n" + 121 | " document.getElementById('" + WAITING_ID + "').style.display = 'block';\n" + 122 | " setTimeout(function() {\n" + 123 | " document.forms.shoot.submit();\n" + 124 | " }, 500);\n" + 125 | "}\n").toString(); 126 | 127 | HTML.standardPage(response, Actors.PSP, js, html); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /web/images/paypal-pay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | PayPal payment symbol 4 | 5 | 6 | 7 | 9 | 14 | 20 | 24 | 29 | 35 | 37 | 44 | 49 | 53 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /web/images/visamc-pay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | VISA/MasterCard Payment Symbol 4 | 5 | 6 | 7 | 14 | 32 | 33 | 40 | 43 | 45 | 47 | 49 | 50 | 53 | 59 | 62 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/ESADServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.util.logging.Logger; 22 | 23 | import javax.servlet.ServletException; 24 | 25 | import javax.servlet.http.HttpServlet; 26 | import javax.servlet.http.HttpServletRequest; 27 | import javax.servlet.http.HttpServletResponse; 28 | 29 | import org.webpki.cbor.CBORAsymKeyEncrypter; 30 | import org.webpki.cbor.CBOREncrypter; 31 | import org.webpki.cbor.CBORMap; 32 | import org.webpki.cbor.CBORObject; 33 | import org.webpki.cbor.CBORTag; 34 | 35 | import org.webpki.fwp.FWPCrypto; 36 | 37 | /** 38 | * Creates and shows the encrypted SAD object (ESAD). 39 | * 40 | */ 41 | public class ESADServlet extends HttpServlet { 42 | 43 | static Logger logger = Logger.getLogger(ESADServlet.class.getName()); 44 | 45 | private static final long serialVersionUID = 1L; 46 | 47 | // DIV elements to turn on and turn off. 48 | private static final String WAITING_ID = "wait"; 49 | private static final String ACTIVATE_ID = "activate"; 50 | 51 | private static CBOREncrypter encrypter = new CBORAsymKeyEncrypter( 52 | ApplicationService.issuerEncryptionKey.getPublic(), 53 | ApplicationService.issuerKeyEncryptionAlgorithm, 54 | ApplicationService.issuerContentEncryptionAlgorithm) 55 | .setKeyId(ApplicationService.issuerEncryptionKeyId); 56 | 57 | public void doPost(HttpServletRequest request, HttpServletResponse response) 58 | throws IOException, ServletException { 59 | request.setCharacterEncoding("utf-8"); 60 | try { 61 | String signedAuthorizationDataB64U = request.getParameter(WalletCore.FWP_SAD); 62 | if (signedAuthorizationDataB64U == null) { 63 | WalletCore.failed("FWP assertion missing"); 64 | } 65 | String walletInternal = request.getParameter(WalletCore.WALLET_INTERNAL); 66 | if (walletInternal == null) { 67 | WalletCore.failed("Missing wallet data"); 68 | return; 69 | } 70 | CBORObject encryptedAssertion = encrypter.encrypt( 71 | ApplicationService.base64UrlDecode(signedAuthorizationDataB64U), 72 | new CBORTag(FWPCrypto.FWP_ESAD_OBJECT_ID, new CBORMap())); 73 | 74 | StringBuilder html = new StringBuilder( 75 | "
" + 76 | "" + 81 | "" + 85 | "
" + 86 | 87 | "
Encrypted SAD => ESAD
" + 88 | 89 | "
" + 90 | "
") 91 | .append(ADServlet.sectionReference("seq-4.4")) 92 | .append( 93 | ": The authorization data has now been signed and encrypted, " + 94 | "the latter using an issuer-specific key." + 95 | "
However, payment backend processing needs some " + 96 | "additional data (in clear) in order to perform its work.
" + 97 | "
" + 98 | "
" + 99 | 100 | "
" + 101 | "waiting" + 103 | "
" + 104 | 105 | "
" + 106 | "
" + 107 | "Finalize FWP Assertion" + 108 | "
" + 109 | "
" + 110 | 111 | "
") 112 | .append(HTML.encode(encryptedAssertion.toString(), true)) 113 | .append( 114 | "
"); 115 | 116 | String js = new StringBuilder( 117 | 118 | WalletCore.GO_HOME_JAVASCRIPT + 119 | 120 | "function doFinalize() {\n" + 121 | " document.getElementById('" + ACTIVATE_ID + "').style.display = 'none';\n" + 122 | " document.getElementById('" + WAITING_ID + "').style.display = 'block';\n" + 123 | " setTimeout(function() {\n" + 124 | " document.forms.shoot.submit();\n" + 125 | " }, 500);\n" + 126 | "}\n").toString(); 127 | 128 | HTML.standardPage(response, Actors.FWP, js, html); 129 | } catch (Exception e) { 130 | HTML.errorPage(response, e); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/MerchantServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.util.GregorianCalendar; 22 | 23 | import java.util.logging.Logger; 24 | 25 | import javax.servlet.ServletException; 26 | 27 | import javax.servlet.http.HttpServlet; 28 | import javax.servlet.http.HttpServletRequest; 29 | import javax.servlet.http.HttpServletResponse; 30 | 31 | import org.webpki.fwp.FWPJsonAssertion; 32 | import org.webpki.fwp.FWPPaymentRequest; 33 | import org.webpki.fwp.PSPRequest; 34 | 35 | import org.webpki.json.JSONParser; 36 | 37 | /** 38 | * Return to merchant after successful FWP invocation. 39 | * 40 | */ 41 | public class MerchantServlet extends HttpServlet { 42 | 43 | static Logger logger = Logger.getLogger(MerchantServlet.class.getName()); 44 | 45 | private static final long serialVersionUID = 1L; 46 | 47 | // DIV elements to turn on and turn off. 48 | private static final String WAITING_ID = "wait"; 49 | private static final String ACTIVATE_ID = "activate"; 50 | 51 | public void doPost(HttpServletRequest request, HttpServletResponse response) 52 | throws IOException, ServletException { 53 | request.setCharacterEncoding("utf-8"); 54 | String fwpAssertion = request.getParameter(WalletCore.FWP_ASSERTION); 55 | if (fwpAssertion == null) { 56 | WalletCore.failed("Missing FWP assertion"); 57 | return; 58 | } 59 | String paymentRequest = request.getParameter(WalletCore.PAYMENT_REQUEST); 60 | if (paymentRequest == null) { 61 | WalletCore.failed("Missing payment request"); 62 | return; 63 | } 64 | FWPJsonAssertion fwpJsonAssertion = 65 | new FWPJsonAssertion(JSONParser.parse(fwpAssertion)); 66 | FWPPaymentRequest fwpPaymentRequest = 67 | new FWPPaymentRequest(JSONParser.parse(paymentRequest)); 68 | PSPRequest pspRequest = new PSPRequest(fwpPaymentRequest, 69 | fwpJsonAssertion, 70 | "DE89370400440532013000", 71 | request.getRemoteAddr(), 72 | new GregorianCalendar()); 73 | StringBuilder html = new StringBuilder( 74 | "
" + 75 | "" + 80 | "
" + 81 | 82 | "
Return to Merchant
" + 83 | 84 | "
" + 85 | "
") 86 | .append(ADServlet.sectionReference("seq-6")) 87 | .append( 88 | ": The Merchant checkout software has now " + 89 | "returned the authorization response " + 90 | "from the Wallet. The next step is taking " + 91 | "the authorization (together with other data), " + 92 | "to a suitable Payment System Provider " + 93 | "(PSP) " + 94 | "for fulfillment." + 95 | "
Below is a non-normative " + 96 | "sample PSP message.
" + 97 | "
Note: "backend" processing " + 98 | "is invisible for the User; " + 99 | "he/she stays in the Merchant context " + 100 | "throughout the payment journey!
" + 101 | "
" + 102 | "
" + 103 | 104 | "
" + 105 | "waiting" + 107 | "
" + 108 | 109 | "
" + 110 | "
" + 111 | "Send Request to PSP" + 112 | "
" + 113 | "
" + 114 | 115 | "
") 116 | .append(HTML.encode(pspRequest.toString(), true)) 117 | .append( 118 | "
"); 119 | String js = new StringBuilder( 120 | 121 | WalletCore.GO_HOME_JAVASCRIPT + 122 | 123 | "function doReturn() {\n" + 124 | " document.getElementById('" + ACTIVATE_ID + "').style.display = 'none';\n" + 125 | " document.getElementById('" + WAITING_ID + "').style.display = 'block';\n" + 126 | " setTimeout(function() {\n" + 127 | " document.forms.shoot.submit();\n" + 128 | " }, 500);\n" + 129 | "}\n").toString(); 130 | 131 | HTML.standardPage(response, Actors.MERCHANT, js, html); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /web/images/legacy-visamc-pay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | VISA/MC Legacy Payment 4 | 5 | 6 | 7 | 14 | 32 | 33 | 39 | height="40" 40 | 43 | 45 | 47 | 49 | 50 | 53 | 59 | 62 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Payment 72 | Card 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/FinalizeAssertionServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.util.logging.Logger; 22 | 23 | import javax.servlet.ServletException; 24 | 25 | import javax.servlet.http.HttpServlet; 26 | import javax.servlet.http.HttpServletRequest; 27 | import javax.servlet.http.HttpServletResponse; 28 | 29 | import org.webpki.fwp.FWPJsonAssertion; 30 | 31 | import org.webpki.json.JSONObjectReader; 32 | import org.webpki.json.JSONOutputFormats; 33 | import org.webpki.json.JSONParser; 34 | 35 | /** 36 | * Creates and shows the finalized FWP assertion. 37 | * 38 | */ 39 | public class FinalizeAssertionServlet extends HttpServlet { 40 | 41 | static Logger logger = Logger.getLogger(FinalizeAssertionServlet.class.getName()); 42 | 43 | private static final long serialVersionUID = 1L; 44 | 45 | // DIV elements to turn on and turn off. 46 | private static final String WAITING_ID = "wait"; 47 | private static final String ACTIVATE_ID = "activate"; 48 | 49 | public void doPost(HttpServletRequest request, HttpServletResponse response) 50 | throws IOException, ServletException { 51 | request.setCharacterEncoding("utf-8"); 52 | String encryptedSignedAuthorizationB64U = request.getParameter(WalletCore.FWP_ESAD); 53 | if (encryptedSignedAuthorizationB64U == null) { 54 | WalletCore.failed("Missing encrypted signed authorization data"); 55 | return; 56 | } 57 | String walletInternal = request.getParameter(WalletCore.WALLET_INTERNAL); 58 | if (walletInternal == null) { 59 | WalletCore.failed("Missing wallet data"); 60 | return; 61 | } 62 | JSONObjectReader selectedCard = 63 | JSONParser.parse(walletInternal).getObject(WalletCore.SELECTED_CARD); 64 | FWPJsonAssertion fwpAssertion = 65 | new FWPJsonAssertion(selectedCard.getString(WalletCore.PAYMENT_NETWORK_ID), 66 | selectedCard.getString(WalletCore.ISSUER_ID), 67 | ApplicationService.base64UrlDecode(encryptedSignedAuthorizationB64U)); 68 | StringBuilder html = new StringBuilder( 69 | "
" + 70 | "" + 75 | "" + 81 | "
" + 82 | 83 | "
Completed FWP Assertion
" + 84 | 85 | "
" + 86 | "
") 87 | .append(ADServlet.sectionReference("seq-4.5")) 88 | .append( 89 | ": The following data represents the completed FWP assertion." + 90 | "
To simplify usage in browsers and " + 91 | "payment processors, FWP assertions are provided as JSON objects. "+ 92 | "Only verifiers need to deal with low-level CBOR processing.
" + 93 | "
Note that due to the end-to-end " + 94 | "security model, a verifier must either be the " + 95 | "Issuer (which typically is a bank), or a " + 96 | "party acting on behalf of the Issuer (") 97 | .append(ADServlet.sectionReference("delegatedauthorization")) 98 | .append( 99 | ").
" + 100 | "
" + 101 | "
" + 102 | 103 | "
" + 104 | "waiting" + 106 | "
" + 107 | 108 | "
" + 109 | "
" + 110 | "Return FWP Assertion to Merchant" + 111 | "
" + 112 | "
" + 113 | 114 | "
") 115 | .append(HTML.encode(fwpAssertion.toString(), true)) 116 | .append( 117 | "
"); 118 | String js = new StringBuilder( 119 | 120 | WalletCore.GO_HOME_JAVASCRIPT + 121 | 122 | "function doReturn() {\n" + 123 | " document.getElementById('" + ACTIVATE_ID + "').style.display = 'none';\n" + 124 | " document.getElementById('" + WAITING_ID + "').style.display = 'block';\n" + 125 | " setTimeout(function() {\n" + 126 | " document.forms.shoot.submit();\n" + 127 | " }, 500);\n" + 128 | "}\n").toString(); 129 | 130 | HTML.standardPage(response, Actors.FWP, js, html); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/org/webpki/fwp/FWPAssertionDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | 20 | import java.util.GregorianCalendar; 21 | import java.util.HashSet; 22 | 23 | import org.webpki.cbor.CBORArray; 24 | import org.webpki.cbor.CBORDecoder; 25 | import org.webpki.cbor.CBORMap; 26 | import org.webpki.cbor.CBORObject; 27 | 28 | import org.webpki.util.ISODateTime; 29 | 30 | /** 31 | * FWP relying party side assertion (SAD) support. 32 | */ 33 | public class FWPAssertionDecoder { 34 | 35 | private CBORMap fwpAssertion; 36 | 37 | public class PlatformNameVersion { 38 | String name; 39 | String version; 40 | 41 | public String getName() { 42 | return name; 43 | } 44 | 45 | public String getVersion() { 46 | return version; 47 | } 48 | 49 | PlatformNameVersion(CBORObject nameVersion) { 50 | this.name = nameVersion.getMap().get(FWPElements.CBOR_PDSUB_NAME).getString(); 51 | this.version = nameVersion.getMap().get(FWPElements.CBOR_PDSUB_VERSION).getString(); 52 | } 53 | } 54 | 55 | private PlatformNameVersion operatingSystem; 56 | public PlatformNameVersion getOperatingSystem() { 57 | return operatingSystem; 58 | } 59 | 60 | private PlatformNameVersion userAgent; 61 | public PlatformNameVersion getUserAgent() { 62 | return userAgent; 63 | } 64 | 65 | private GregorianCalendar timeStamp; 66 | public GregorianCalendar getTimeStamp() { 67 | return timeStamp; 68 | } 69 | 70 | private CBORObject networkOptions; 71 | public CBORObject getnetworkOptions() { 72 | return networkOptions; 73 | } 74 | 75 | private FWPPaymentRequest paymentRequest; 76 | public FWPPaymentRequest getPaymentRequest() { 77 | return paymentRequest; 78 | } 79 | 80 | private String payeeHost; 81 | public String getPayeeHost() { 82 | return payeeHost; 83 | } 84 | 85 | private String accountId; 86 | public String getAccountId() { 87 | return accountId; 88 | } 89 | 90 | private String serialNumber; 91 | public String getSerialNumber() { 92 | return serialNumber; 93 | } 94 | 95 | private String paymentNetwork; 96 | public String getPaymentNetwork() { 97 | return paymentNetwork; 98 | } 99 | 100 | private double[] location; 101 | public double[] getLocation() { 102 | return location; 103 | } 104 | 105 | public void verifyClaimedPaymentRequest(FWPPaymentRequest claimedPaymentRequest) { 106 | if (!paymentRequest.equals(claimedPaymentRequest)) { 107 | throw new FWPException("Claimed:\n" + claimedPaymentRequest.toString() + 108 | "Actual:\n" + paymentRequest.toString()); 109 | } 110 | } 111 | 112 | private String getString(FWPElements name) { 113 | return fwpAssertion.get(name.cborLabel).getString(); 114 | } 115 | 116 | private byte[] publicKey; 117 | public byte[] getPublicKey() { 118 | return publicKey; 119 | } 120 | 121 | private HashSet userValidation = new HashSet<>(); 122 | public HashSet getUserValidation() { 123 | return userValidation; 124 | } 125 | 126 | public FWPAssertionDecoder(byte[] signedFwpAssertion) { 127 | // Convert SAD binary into CBOR objects. 128 | this(CBORDecoder.decode(signedFwpAssertion).getMap()); 129 | } 130 | 131 | public FWPAssertionDecoder(CBORMap signedFwpAssertion) { 132 | fwpAssertion = signedFwpAssertion; 133 | 134 | // Payment Request (PRCD) 135 | paymentRequest = new FWPPaymentRequest( 136 | fwpAssertion.get(FWPElements.PAYMENT_REQUEST.cborLabel)); 137 | 138 | // Account. 139 | accountId = getString(FWPElements.ACCOUNT_ID); 140 | 141 | // For usage with the following payment network. 142 | paymentNetwork = getString(FWPElements.PAYMENT_NETWORK_ID); 143 | 144 | // Serial number of payment credential. Note: this is unrelated to the 145 | // FIDO "credentialId" (which only used locally by the wallet). 146 | serialNumber = getString(FWPElements.SERIAL_NUMBER); 147 | 148 | // Platform Data 149 | CBORMap platformData = fwpAssertion.get( 150 | FWPElements.PLATFORM_DATA.cborLabel).getMap(); 151 | operatingSystem = new PlatformNameVersion( 152 | platformData.get(FWPElements.CBOR_PD_OPERATING_SYSTEM)); 153 | userAgent = new PlatformNameVersion( 154 | platformData.get(FWPElements.CBOR_PD_USER_AGENT)); 155 | 156 | // Time Stamp 157 | timeStamp = ISODateTime.decode(getString(FWPElements.TIME_STAMP), 158 | ISODateTime.COMPLETE); 159 | 160 | // Payee Host information from the browser 161 | payeeHost = getString(FWPElements.PAYEE_HOST); 162 | 163 | // Optional Network Data. 164 | if (fwpAssertion.containsKey(FWPElements.NETWORK_OPTIONS.cborLabel)) { 165 | // There is such data, get it! It can be any CBOR data 166 | // that has a 1-2-1 translation to JSON. 167 | networkOptions = fwpAssertion.get(FWPElements.NETWORK_OPTIONS.cborLabel); 168 | // We mark it as "read" to not get a problem with checkForUnread(). 169 | networkOptions.scan(); 170 | } 171 | 172 | // Optional location. 173 | if (fwpAssertion.containsKey(FWPElements.LOCATION.cborLabel)) { 174 | // There is a location, get it! 175 | CBORArray cborLocation = 176 | fwpAssertion.get(FWPElements.LOCATION.cborLabel).getArray(); 177 | location = new double[2]; 178 | for (int i = 0; i < 2; i++) { 179 | location[i] = cborLocation.get(i).getFloat64(); 180 | } 181 | } 182 | 183 | // Finally, the authorization signature. 184 | // Note: this must be the last step since it modifies the fwpAssertion. 185 | publicKey = FWPCrypto.validateFwpSignature(fwpAssertion, userValidation); 186 | 187 | // Check that we didn't forgot anything or that there is "other" data. 188 | fwpAssertion.checkForUnread(); 189 | } 190 | } 191 | 192 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/PaymentRequestServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.sql.Connection; 22 | 23 | import java.util.ArrayList; 24 | 25 | import java.util.logging.Logger; 26 | 27 | import javax.servlet.ServletException; 28 | 29 | import javax.servlet.http.HttpServlet; 30 | import javax.servlet.http.HttpServletRequest; 31 | import javax.servlet.http.HttpServletResponse; 32 | 33 | import org.webpki.json.JSONArrayReader; 34 | import org.webpki.json.JSONArrayWriter; 35 | import org.webpki.json.JSONObjectReader; 36 | import org.webpki.json.JSONObjectWriter; 37 | import org.webpki.json.JSONOutputFormats; 38 | import org.webpki.json.JSONParser; 39 | 40 | /** 41 | * This is the request by the Merchant. 42 | * 43 | */ 44 | public class PaymentRequestServlet extends HttpServlet { 45 | 46 | static Logger logger = Logger.getLogger(PaymentRequestServlet.class.getName()); 47 | 48 | private static final long serialVersionUID = 1L; 49 | 50 | // DIV elements to turn on and turn off. 51 | private static final String WAITING_ID = "wait"; 52 | private static final String ACTIVATE_ID = "activate"; 53 | 54 | public void doPost(HttpServletRequest request, HttpServletResponse response) 55 | throws IOException, ServletException { 56 | request.setCharacterEncoding("utf-8"); 57 | String walletRequest = request.getParameter(WalletCore.WALLET_REQUEST); 58 | if (walletRequest == null) { 59 | WalletCore.failed("Missing wallet request"); 60 | } 61 | JSONObjectReader walletRequestJson = JSONParser.parse(walletRequest); 62 | try { 63 | // Get the enrolled user. 64 | String userId = WalletCore.getWalletCookie(request); 65 | if (userId == null) { 66 | response.sendRedirect("walletadmin"); 67 | return; 68 | } 69 | // What the Merchant wants... 70 | JSONObjectReader paymentRequest = 71 | walletRequestJson.getObject(WalletCore.PAYMENT_REQUEST); 72 | 73 | // Lookup virtual cards in the wallet database 74 | ArrayList virtualCards; 75 | try (Connection connection = ApplicationService.jdbcDataSource.getConnection();) { 76 | virtualCards = DataBaseOperations.getVirtualCards(userId, connection); 77 | } 78 | if (virtualCards.isEmpty()) { 79 | response.sendRedirect("walletadmin"); 80 | return; 81 | } 82 | 83 | // Match against Merchant list 84 | JSONArrayWriter matching = null; 85 | JSONArrayReader networks = walletRequestJson.getArray(WalletCore.NETWORKS); 86 | while (networks.hasMore()) { 87 | JSONObjectReader network = networks.getObject(); 88 | String paymentNetwordId = network.getString("id"); 89 | for (DataBaseOperations.VirtualCard virtualCard : virtualCards) { 90 | if (paymentNetwordId.equals(virtualCard.paymentNetworkId)) { 91 | if (matching == null) { 92 | matching = new JSONArrayWriter(); 93 | } 94 | matching.setObject(new JSONObjectWriter() 95 | .setBinary(WalletCore.CREDENTIAL_ID, virtualCard.credentialId) 96 | .setBinary(WalletCore.PUBLIC_KEY, virtualCard.publicKey) 97 | .setString(WalletCore.ACCOUNT_ID, virtualCard.accountId) 98 | .setString(WalletCore.CARD_HOLDER, virtualCard.cardHolder) 99 | .setString(WalletCore.PAYMENT_NETWORK_ID, paymentNetwordId) 100 | .setString(WalletCore.SERIAL_NUMBER, virtualCard.serialNumber) 101 | // Hard-coded at the moment 102 | .setString(WalletCore.ISSUER_ID, ApplicationService.issuerId)); 103 | } 104 | } 105 | } 106 | if (matching == null) { 107 | throw new IOException("No matching card"); 108 | } 109 | 110 | JSONObjectWriter walletInternal = new JSONObjectWriter() 111 | .setObject(WalletCore.PAYMENT_REQUEST, paymentRequest) 112 | .setArray(WalletCore.MATCHING_CARDS, matching); 113 | 114 | StringBuilder html = new StringBuilder( 115 | "
" + 116 | "
" + 121 | "
Payment Request
" + 122 | 123 | "
" + 124 | "
") 125 | .append(ADServlet.sectionReference("seq-1")) 126 | .append( 127 | ": This is what the Merchant's " + 128 | "call to the W3C PaymentRequest API " + 129 | "boils down to, here expressed as JSON. The "" + WalletCore.NETWORKS + 130 | "" array holds a list of FWP compatible payment networks that the " + 131 | "Merchant accepts." + 132 | "
" + 133 | "
" + 134 | 135 | "
" + 136 | "waiting" + 138 | "
" + 139 | 140 | "
" + 141 | "
" + 142 | "Continue..." + 143 | "
" + 144 | "
" + 145 | 146 | "
") 147 | .append(HTML.encode(walletRequestJson.toString(), true)) 148 | .append( 149 | "
"); 150 | 151 | String js = new StringBuilder( 152 | 153 | WalletCore.GO_HOME_JAVASCRIPT + 154 | 155 | "function doContinue() {\n" + 156 | " document.getElementById('" + ACTIVATE_ID + "').style.display = 'none';\n" + 157 | " document.getElementById('" + WAITING_ID + "').style.display = 'block';\n" + 158 | " setTimeout(function() {\n" + 159 | " document.forms.shoot.submit();\n" + 160 | " }, 500);\n" + 161 | "}\n").toString(); 162 | 163 | HTML.standardPage(response, Actors.FWP, js.toString(), html); 164 | } catch (Exception e) { 165 | HTML.errorPage(response, e); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Logging flag 9 | logging 10 | @logging@ 11 | 12 | 13 | 14 | org.webpki.webapps.fwp.ApplicationService 15 | 16 | 17 | 18 | HomeServlet 19 | org.webpki.webapps.fwp.HomeServlet 20 | 21 | 22 | 23 | EnrollServlet 24 | org.webpki.webapps.fwp.EnrollServlet 25 | 26 | 27 | 28 | WalletAdminServlet 29 | org.webpki.webapps.fwp.WalletAdminServlet 30 | 31 | 32 | 33 | CardServlet 34 | org.webpki.webapps.fwp.CardServlet 35 | 36 | 37 | 38 | FIDOEnrollServlet 39 | org.webpki.webapps.fwp.FIDOEnrollServlet 40 | 41 | 42 | 43 | BuyServlet 44 | org.webpki.webapps.fwp.BuyServlet 45 | 46 | 47 | 48 | PaymentRequestServlet 49 | org.webpki.webapps.fwp.PaymentRequestServlet 50 | 51 | 52 | 53 | WalletUIServlet 54 | org.webpki.webapps.fwp.WalletUIServlet 55 | 56 | 57 | 58 | ADServlet 59 | org.webpki.webapps.fwp.ADServlet 60 | 61 | 62 | 63 | SADServlet 64 | org.webpki.webapps.fwp.SADServlet 65 | 66 | 67 | 68 | ESADServlet 69 | org.webpki.webapps.fwp.ESADServlet 70 | 71 | 72 | 73 | FinalizeAssertionServlet 74 | org.webpki.webapps.fwp.FinalizeAssertionServlet 75 | 76 | 77 | 78 | MerchantServlet 79 | org.webpki.webapps.fwp.MerchantServlet 80 | 81 | 82 | 83 | PSPServlet 84 | org.webpki.webapps.fwp.PSPServlet 85 | 86 | 87 | 88 | IssuerServlet 89 | org.webpki.webapps.fwp.IssuerServlet 90 | 91 | 92 | 93 | FIDOPayServlet 94 | org.webpki.webapps.fwp.FIDOPayServlet 95 | 96 | 97 | 98 | LoginServlet 99 | org.webpki.webapps.fwp.LoginServlet 100 | 101 | 102 | 103 | FIDOLoginServlet 104 | org.webpki.webapps.fwp.FIDOLoginServlet 105 | 106 | 107 | 108 | RegistrationListServlet 109 | org.webpki.webapps.fwp.admin.RegistrationListServlet 110 | 111 | 112 | 113 | HomeServlet 114 | /home 115 | 116 | 117 | 118 | EnrollServlet 119 | /enroll 120 | 121 | 122 | 123 | WalletAdminServlet 124 | /walletadmin 125 | 126 | 127 | 128 | CardServlet 129 | /card 130 | 131 | 132 | 133 | FIDOEnrollServlet 134 | /fidoenroll 135 | 136 | 137 | 138 | BuyServlet 139 | /buy 140 | 141 | 142 | 143 | PaymentRequestServlet 144 | /pr 145 | 146 | 147 | 148 | WalletUIServlet 149 | /ui 150 | 151 | 152 | 153 | ADServlet 154 | /ad 155 | 156 | 157 | 158 | SADServlet 159 | /sad 160 | 161 | 162 | 163 | ESADServlet 164 | /esad 165 | 166 | 167 | 168 | FinalizeAssertionServlet 169 | /finalizeassertion 170 | 171 | 172 | 173 | MerchantServlet 174 | /merchant 175 | 176 | 177 | 178 | PSPServlet 179 | /pspreq 180 | 181 | 182 | 183 | IssuerServlet 184 | /issuerreq 185 | 186 | 187 | 188 | FIDOPayServlet 189 | /fidopay 190 | 191 | 192 | 193 | LoginServlet 194 | /login 195 | 196 | 197 | 198 | FIDOLoginServlet 199 | /fidologin 200 | 201 | 202 | 203 | RegistrationListServlet 204 | /admin/reglist 205 | 206 | 207 | 208 | 209 | Bank admin manager 210 | /admin/* 211 | 212 | 213 | manager 214 | 215 | 216 | 217 | 218 | BASIC 219 | Bank admin 220 | 221 | 222 | 223 | The role that is required to log in as admin 224 | manager 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /test/org/webpki/fwp/CryptoImages.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2006-2021 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.fwp; 18 | 19 | import java.io.File; 20 | 21 | import org.webpki.cbor.CBORObject; 22 | 23 | import static org.webpki.cbor.CBORCryptoConstants.*; 24 | 25 | import org.webpki.util.IO; 26 | 27 | /** 28 | * Make SVG images for CBOR encryption and the FWP subset 29 | */ 30 | public class CryptoImages { 31 | 32 | static int HEADER_HEIGHT = 150; 33 | 34 | static int MARGIN = 20; 35 | 36 | static int LABEL_GUTTER = 20; 37 | static int LABEL_HEIGHT = 80; 38 | 39 | static int IMAGE_WIDTH = 83; 40 | 41 | static final int TEXT_LEFT_MARGIN = 24; 42 | static final int TEXT_FONT_SIZE = 40; 43 | static final int TEXT_Y_OFFSET = 54; 44 | static final int HEADER_FONT_SIZE = 50; 45 | static final int HEADER_Y_OFFSET = 60; 46 | static final int SUB_HEADER_Y_OFFSET = 110; 47 | 48 | boolean cborFull; 49 | boolean initialLabel; 50 | StringBuilder svg; 51 | StringBuilder textLabels; 52 | StringBuilder headerTexts; 53 | int left; 54 | int top; 55 | int width; 56 | 57 | int mainMapWidth() { 58 | return 460; 59 | } 60 | 61 | int subMapWidth() { 62 | return cborFull ? 510 : 440; 63 | } 64 | 65 | int totalHeight() { 66 | return (cborFull ? 7 * LABEL_HEIGHT + 6 * LABEL_GUTTER 67 | : 68 | 5 * LABEL_HEIGHT + 4 * LABEL_GUTTER) + HEADER_HEIGHT + MARGIN; 69 | } 70 | 71 | void label(String labelText, CBORObject cborLabel, boolean mandatory) throws Exception { 72 | if (!initialLabel) { 73 | top += LABEL_GUTTER; 74 | } 75 | initialLabel = false; 76 | svg.append("\n"); 85 | 86 | textLabels.append("") 91 | .append(labelText) 92 | .append(" (") 93 | .append(cborLabel.getInt32()) 94 | .append(")\n"); 95 | 96 | top += LABEL_HEIGHT; 97 | } 98 | 99 | void headers(String subHeaderText, String mainHeaderText) { 100 | initialLabel = true; 101 | headerTexts.append("") 106 | .append(mainHeaderText) 107 | .append("\n") 112 | .append(subHeaderText) 113 | .append("\n"); 114 | 115 | } 116 | 117 | void execute(String fileName, boolean cborFull) throws Exception { 118 | this.cborFull = cborFull; 119 | textLabels = new StringBuilder(); 120 | headerTexts = new StringBuilder(); 121 | svg = new StringBuilder( 122 | "\n" + 123 | "\n" + 128 | "") 129 | .append(cborFull ? "CBOR Encryption Format" : "FWP Encryption Layout") 130 | .append( 131 | "\n\n" + 132 | "\n"); 133 | top = HEADER_HEIGHT; 134 | left = MARGIN; 135 | width = mainMapWidth(); 136 | 137 | headers("(Content Encryption)", "Main Map"); 138 | 139 | if (cborFull) { 140 | label("customData", CXF_CUSTOM_DATA_LBL, false); 141 | } 142 | label("algorithm", CXF_ALGORITHM_LBL, true); 143 | label("keyEncryption", CEF_KEY_ENCRYPTION_LBL, !cborFull); 144 | if (cborFull) { 145 | label("keyId", CXF_KEY_ID_LBL, false); 146 | } 147 | label("tag", CEF_TAG_LBL, true); 148 | label("iv", CEF_IV_LBL, true); 149 | label("cipherText", CEF_CIPHER_TEXT_LBL, true); 150 | 151 | top = HEADER_HEIGHT; 152 | left += width + IMAGE_WIDTH + 2 * MARGIN; 153 | width = subMapWidth(); 154 | 155 | headers("(Key Encryption)", cborFull ? "Optional Sub Map" : "Sub Map"); 156 | 157 | label("algorithm", CXF_ALGORITHM_LBL, true); 158 | label("keyId", CXF_KEY_ID_LBL, false); 159 | label("publicKey", CXF_PUBLIC_KEY_LBL, false); 160 | if (cborFull) { 161 | label("certificatePath", CXF_CERT_PATH_LBL, false); 162 | } 163 | label("ephemeralKey", CEF_EPHEMERAL_KEY_LBL, !cborFull); 164 | label("cipherText", CEF_CIPHER_TEXT_LBL, false); 165 | 166 | int lowerPath = (LABEL_HEIGHT + LABEL_GUTTER) * 2; 167 | int upperPath = cborFull ? (LABEL_GUTTER + LABEL_HEIGHT) : 0; 168 | int turnParam = (LABEL_GUTTER + LABEL_GUTTER + LABEL_HEIGHT) / 2; 169 | 170 | svg.append("\n\n") 172 | .append(headerTexts) 173 | .append("\n" + 174 | "\n") 176 | .append(textLabels) 177 | .append("\n" + 178 | "\n"); 186 | 187 | IO.writeFile(fileName, svg.append("\n").toString()); 188 | } 189 | 190 | public CryptoImages(String buildDirectory) throws Exception { 191 | String directory = buildDirectory + File.separatorChar + 192 | CryptoDocument.DOC_GEN_DIRECTORY + File.separatorChar; 193 | execute(directory + "fwp-crypto.svg", false); 194 | execute(directory + "cbor-crypto.svg", true); 195 | } 196 | 197 | public static void main(String[] args) { 198 | try { 199 | new CryptoImages(args[0]); 200 | } catch (Exception e) { 201 | e.printStackTrace(); 202 | } 203 | 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/HTML.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 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 String encode(String val, boolean codeListings) { 33 | if (val != null) { 34 | StringBuilder buf = new StringBuilder(val.length() + 8); 35 | char c; 36 | 37 | for (int i = 0; i < val.length(); i++) { 38 | c = val.charAt(i); 39 | switch (c) { 40 | case '\n': 41 | buf.append(codeListings ? "
" : "\n"); 42 | break; 43 | case '<': 44 | buf.append("<"); 45 | break; 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(codeListings ? " " : " "); 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(Actors actor, String javascript, String box) { 73 | String admin = actor == Actors.ADMIN ? "../" : ""; 74 | StringBuilder html = new StringBuilder( 75 | "" + 76 | "" + 79 | "" + 80 | "FWP Lab" + 81 | ""); 84 | 85 | if (javascript != null) { 86 | html.append(""); 89 | } 90 | 91 | html.append( 92 | "" + 93 | "
" + 94 | "
") 103 | .append(actor.html) 104 | .append( 105 | "
" + 106 | "
") 107 | .append(box).append(""); 108 | return html.toString(); 109 | } 110 | 111 | static void output(HttpServletResponse response, String html) 112 | throws IOException, ServletException { 113 | if (ApplicationService.logging) { 114 | logger.info(html); 115 | } 116 | response.setContentType("text/html; charset=utf-8"); 117 | response.setHeader("Pragma", "No-Cache"); 118 | response.setDateHeader("EXPIRES", 0); 119 | response.getOutputStream().write(html.getBytes("utf-8")); 120 | } 121 | 122 | static String getConditionalParameter(HttpServletRequest request, 123 | String name) { 124 | String value = request.getParameter(name); 125 | if (value == null) { 126 | return ""; 127 | } 128 | return value; 129 | } 130 | 131 | public static String boxHeader(String id, String text, boolean visible) { 132 | return new StringBuilder("
" + 137 | "
" + text + ":
").toString(); 138 | } 139 | 140 | public static String fancyBox(String id, String content, String header) { 141 | return boxHeader(id, header, true) + 142 | "
" + content + "
"; 143 | } 144 | 145 | public static String fancyCode(String id, String content, String header) { 146 | return boxHeader(id, header, true) + 147 | "
" + encode(content, true) + "
"; 148 | } 149 | 150 | public static String fancyText(boolean visible, 151 | String id, 152 | int rows, 153 | String content, 154 | String header) { 155 | return boxHeader(id, header, visible) + 156 | "" + 159 | encode(content, false) + 160 | "
"; 161 | } 162 | 163 | public static void standardPage(HttpServletResponse response, 164 | Actors actor, 165 | String javaScript, 166 | StringBuilder html) throws IOException, ServletException { 167 | HTML.output(response, HTML.getHTML(actor, javaScript, html.toString())); 168 | } 169 | 170 | static String javaScript(String string) { 171 | StringBuilder html = new StringBuilder(); 172 | for (char c : string.toCharArray()) { 173 | if (c == '\n') { 174 | html.append("\\n"); 175 | } else { 176 | html.append(c); 177 | } 178 | } 179 | return html.toString(); 180 | } 181 | 182 | public static void errorPage(HttpServletResponse response, Exception e) 183 | throws IOException, ServletException { 184 | StringBuilder error = new StringBuilder("Stack trace:\n") 185 | .append(e.getClass().getName()) 186 | .append(": ") 187 | .append(e.getMessage()); 188 | StackTraceElement[] st = e.getStackTrace(); 189 | int length = st.length; 190 | if (length > 20) { 191 | length = 20; 192 | } 193 | for (int i = 0; i < length; i++) { 194 | String entry = st[i].toString(); 195 | if (entry.contains(".HttpServlet")) { 196 | break; 197 | } 198 | error.append("\n at " + entry); 199 | } 200 | standardPage(response, 201 | Actors.SITE, 202 | null, 203 | new StringBuilder( 204 | "
Something went wrong...
" + 205 | "
")
206 |         .append(encode(error.toString(), false))
207 |         .append("
")); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /testdata/vectors.txt: -------------------------------------------------------------------------------- 1 | |===================================| 2 | | FIDO Web Pay (FWP) - Test Vectors | 3 | |===================================| 4 | 5 | 6 | 7 | User FIDO key in JWK format: 8 | { 9 | "kty": "EC", 10 | "crv": "P-256", 11 | "x": "6BKxpty8cI-exDzCkh-goU6dXq3MbcY0cd1LaAxiNrU", 12 | "y": "mCbcvUzm44j3Lt2b5BPyQloQ91tf2D2V-gzeUxWaUdg", 13 | "d": "6XxMFXhcYT5QN9w5TIg2aSKsbcj-pj4BnZkK7ZOt4B8" 14 | } 15 | 16 | 17 | Merchant 'W3C PaymentRequest' (PRCD) data in pretty-printed JSON notation: 18 | { 19 | "payeeName": "Space Shop", 20 | "requestId": "7040566321", 21 | "amount": "435.00", 22 | "currency": "EUR" 23 | } 24 | 25 | Merchant 'hostname' according to the browser: spaceshop.com 26 | 27 | 28 | Unsigned FWP assertion, here in CBOR 'diagnostic notation': 29 | { 30 | 1: { 31 | 1: "Space Shop", 32 | 2: "7040566321", 33 | 3: "435.00", 34 | 4: "EUR" 35 | }, 36 | 2: "spaceshop.com", 37 | 3: "FR7630002111110020050014382", 38 | 4: "https://banknet2.org", 39 | 5: "0057162932", 40 | 6: "additional stuff...", 41 | 7: { 42 | 1: { 43 | 3: "Android", 44 | 4: "12.0" 45 | }, 46 | 2: { 47 | 3: "Chrome", 48 | 4: "108" 49 | } 50 | }, 51 | 8: [40.74844, -73.984559], 52 | 9: "2023-02-16T10:14:07+01:00", 53 | -1: { 54 | 1: -7, 55 | 2: { 56 | 1: 2, 57 | -1: 1, 58 | -2: h'e812b1a6dcbc708f9ec43cc2921fa0a14e9d5eadcc6dc63471dd4b680c6236b5', 59 | -3: h'9826dcbd4ce6e388f72edd9be413f2425a10f75b5fd83d95fa0cde53159a51d8' 60 | } 61 | } 62 | } 63 | 64 | Note that the last element (-1) contains the COSE signature algorithm (ES256) and the FIDO public key (EC/P256) which is also is part of the data to be signed. 65 | 66 | 67 | The unsigned FWP assertion (binary) converted into a SHA256 hash, here in Base64Url notation: 68 | 0fbrom0qcwjuzc0qIVRg1axQo5XecsovXENDYi6KzyM 69 | This is subsequently used as FIDO 'challenge'. 70 | 71 | 72 | **************************************** 73 | * FIDO/WebAuthn assertion happens here * 74 | **************************************** 75 | Relying party URL: https://mybank.fr 76 | 77 | Returned FIDO 'authenticatorData' in hexadecimal notation: 78 | 412e175a0f0bdc06dabf0b1db79b97541c08dbacee7e31c97a553588ee922ea70500000017 79 | (here using the UP+UV flags and a zero counter value) 80 | 81 | Returned FIDO 'signature' in hexadecimal notation: 82 | 304402204fbd186e8eac7d7dbb915a7a443b0939af77de5e35cf87831663ae3a8bfc1d940220201d0c51ff9b683648a626cbe0bbb69fed29ce854aea65763e0e33edf2af9e09 83 | 84 | Signed FWP assertion (SAD), here in CBOR 'diagnostic notation': 85 | { 86 | 1: { 87 | 1: "Space Shop", 88 | 2: "7040566321", 89 | 3: "435.00", 90 | 4: "EUR" 91 | }, 92 | 2: "spaceshop.com", 93 | 3: "FR7630002111110020050014382", 94 | 4: "https://banknet2.org", 95 | 5: "0057162932", 96 | 6: "additional stuff...", 97 | 7: { 98 | 1: { 99 | 3: "Android", 100 | 4: "12.0" 101 | }, 102 | 2: { 103 | 3: "Chrome", 104 | 4: "108" 105 | } 106 | }, 107 | 8: [40.74844, -73.984559], 108 | 9: "2023-02-16T10:14:07+01:00", 109 | -1: { 110 | 1: -7, 111 | 2: { 112 | 1: 2, 113 | -1: 1, 114 | -2: h'e812b1a6dcbc708f9ec43cc2921fa0a14e9d5eadcc6dc63471dd4b680c6236b5', 115 | -3: h'9826dcbd4ce6e388f72edd9be413f2425a10f75b5fd83d95fa0cde53159a51d8' 116 | }, 117 | 3: h'412e175a0f0bdc06dabf0b1db79b97541c08dbacee7e31c97a553588ee922ea70500000017', 118 | 4: h'304402204fbd186e8eac7d7dbb915a7a443b0939af77de5e35cf87831663ae3a8bfc1d940220201d0c51ff9b683648a626cbe0bbb69fed29ce854aea65763e0e33edf2af9e09' 119 | } 120 | } 121 | 122 | The added elements 3,5,4','clientDataJSON' and 'signature' respectively. 123 | 124 | 125 | The signed FWP assertion as a hex-encoded binary: aa01a4016a53706163652053686f70026a3730343035363633323103663433352e30300463455552026d737061636573686f702e636f6d03781b465237363330303032313131313130303230303530303134333832047468747470733a2f2f62616e6b6e6574322e6f7267056a3030353731363239333206736164646974696f6e616c2073747566662e2e2e07a201a20367416e64726f6964046431322e3002a203664368726f6d6504633130380882fb40445fcce1c58256fbc0527f0303c07ee1097819323032332d30322d31365431303a31343a30372b30313a303020a4012602a401022001215820e812b1a6dcbc708f9ec43cc2921fa0a14e9d5eadcc6dc63471dd4b680c6236b52258209826dcbd4ce6e388f72edd9be413f2425a10f75b5fd83d95fa0cde53159a51d8035825412e175a0f0bdc06dabf0b1db79b97541c08dbacee7e31c97a553588ee922ea70500000017045846304402204fbd186e8eac7d7dbb915a7a443b0939af77de5e35cf87831663ae3a8bfc1d940220201d0c51ff9b683648a626cbe0bbb69fed29ce854aea65763e0e33edf2af9e09 126 | 127 | 128 | ******************************* 129 | * FWP encryption happens here * 130 | ******************************* 131 | 132 | Issuer encryption key in JWK format: 133 | { 134 | "kty": "OKP", 135 | "crv": "X25519", 136 | "x": "6ZoM7yBYlJYNmxwFl4UT3MtCoTv7ztUjpRuKEXrV8Aw", 137 | "d": "cxfl86EVmcqrR07mWENCf1F_5Ni5mt1ViGyERB6Q1vA" 138 | } 139 | 140 | 141 | Encrypted FWP assertion (ESAD), here in CBOR 'diagnostic notation: 142 | 1010(["https://fido-web-pay.github.io/ns/p1", { 143 | 1: 3, 144 | 2: { 145 | 1: -31, 146 | 3: "x25519:2022:1", 147 | 7: { 148 | 1: 1, 149 | -1: 4, 150 | -2: h'034e9273d9d55c3df0fb366fc33425648d8150de504c1b3499e0a7dac91a2c17' 151 | }, 152 | 10: h'2fd62268299b5e2fe57bafd5762a8eff3a8b9991facbec2d36093cdacb23ed5dff5750ca3bd5d7fc' 153 | }, 154 | 8: h'c20ab16145f1e5349c1d85fab4caf0a3', 155 | 9: h'57e7341b3b1379d8765ae613', 156 | 10: h'204e5f5b4ad63d013ac875d160ffc4f762b75153fb8b30a9d9ecefaf23a30898cd68ac104edfdf854e060d906f1229f739b37e52dffed874a07df3fd661c061d6d7b4d561afe9fc31f14ffbb15a5d62debe1f5cb54a851fdc4b54a83d6f8e64a5a5b0c445960992af964126c17aa5591d747b9e74a40c5ea2d6c2a5f387401c63685bb1cc2a7a331b9b44505622e27a7c29314dedaacc8d3f425b48010d97115f7672dc1ad89a6b01b3d6f0427333d1abf0667feb54c42383ceb4a8883a24b93b4b7921649d05435fb62d4d4aafcb4ce93238d3538fc8821bf6a71bc906173152f933b359ccf9a546ad840510baebdecb6ee15fddc4348b8ef8d80cb36f8410a94784e22542208bfbf6cab1989f23d34be75ccc38a29502bf0e952174ab823df6728c39315c2acf3be75fb8a072a048c08e1efec35ee5158cf828f2b8b8a9e304824ff5dc7c9139af3667165ebc5dca0cfc20baa9e2e45fa65aad54ae026e7b463ec8f974dbe37e90217f6abe223c598c334e9aaa98647ee485eb65f271a7386db71c13843b7570ee211a5b055ee83e9ab9068536ae0b698821bad1a79de35' 157 | }]) 158 | 159 | And as a hex-encoded binary: d903f282782468747470733a2f2f6669646f2d7765622d7061792e6769746875622e696f2f6e732f7031a5010302a401381e036d7832353531393a323032323a3107a301012004215820034e9273d9d55c3df0fb366fc33425648d8150de504c1b3499e0a7dac91a2c170a58282fd62268299b5e2fe57bafd5762a8eff3a8b9991facbec2d36093cdacb23ed5dff5750ca3bd5d7fc0850c20ab16145f1e5349c1d85fab4caf0a3094c57e7341b3b1379d8765ae6130a59019f204e5f5b4ad63d013ac875d160ffc4f762b75153fb8b30a9d9ecefaf23a30898cd68ac104edfdf854e060d906f1229f739b37e52dffed874a07df3fd661c061d6d7b4d561afe9fc31f14ffbb15a5d62debe1f5cb54a851fdc4b54a83d6f8e64a5a5b0c445960992af964126c17aa5591d747b9e74a40c5ea2d6c2a5f387401c63685bb1cc2a7a331b9b44505622e27a7c29314dedaacc8d3f425b48010d97115f7672dc1ad89a6b01b3d6f0427333d1abf0667feb54c42383ceb4a8883a24b93b4b7921649d05435fb62d4d4aafcb4ce93238d3538fc8821bf6a71bc906173152f933b359ccf9a546ad840510baebdecb6ee15fddc4348b8ef8d80cb36f8410a94784e22542208bfbf6cab1989f23d34be75ccc38a29502bf0e952174ab823df6728c39315c2acf3be75fb8a072a048c08e1efec35ee5158cf828f2b8b8a9e304824ff5dc7c9139af3667165ebc5dca0cfc20baa9e2e45fa65aad54ae026e7b463ec8f974dbe37e90217f6abe223c598c334e9aaa98647ee485eb65f271a7386db71c13843b7570ee211a5b055ee83e9ab9068536ae0b698821bad1a79de35 160 | 161 | 162 | FWP assertion delivered by the browser: 163 | { 164 | "paymentNetworkId": "https://banknet2.org", 165 | "issuerId": "https://mybank.fr/payment", 166 | "userAuthorization": "2QPygngkaHR0cHM6Ly9maWRvLXdlYi1wYXkuZ2l0aHViLmlvL25zL3AxpQEDAqQBOB4DbXgyNTUxOToyMDIyOjEHowEBIAQhWCADTpJz2dVcPfD7Nm_DNCVkjYFQ3lBMGzSZ4KfayRosFwpYKC_WImgpm14v5Xuv1XYqjv86i5mR-svsLTYJPNrLI-1d_1dQyjvV1_wIUMIKsWFF8eU0nB2F-rTK8KMJTFfnNBs7E3nYdlrmEwpZAZ8gTl9bStY9ATrIddFg_8T3YrdRU_uLMKnZ7O-vI6MImM1orBBO39-FTgYNkG8SKfc5s35S3_7YdKB98_1mHAYdbXtNVhr-n8MfFP-7FaXWLevh9ctUqFH9xLVKg9b45kpaWwxEWWCZKvlkEmwXqlWR10e550pAxeotbCpfOHQBxjaFuxzCp6MxubRFBWIuJ6fCkxTe2qzI0_QltIAQ2XEV92ctwa2JprAbPW8EJzM9Gr8GZ_61TEI4POtKiIOiS5O0t5IWSdBUNfti1NSq_LTOkyONNTj8iCG_anG8kGFzFS-TOzWcz5pUathAUQuuvey27hX93ENIuO-NgMs2-EEKlHhOIlQiCL-_bKsZifI9NL51zMOKKVAr8OlSF0q4I99nKMOTFcKs8751-4oHKgSMCOHv7DXuUVjPgo8ri4qeMEgk_13HyROa82ZxZevF3KDPwguqni5F-mWq1UrgJue0Y-yPl02-N-kCF_ar4iPFmMM06aqphkfuSF62Xycac4bbccE4Q7dXDuIRpbBV7oPpq5BoU2rgtpiCG60aed41" 167 | } 168 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/FIDOLoginServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | import java.io.PrintWriter; 21 | 22 | import java.security.PublicKey; 23 | 24 | import java.sql.Connection; 25 | 26 | import java.util.Arrays; 27 | 28 | import java.util.logging.Level; 29 | import java.util.logging.Logger; 30 | 31 | import javax.servlet.ServletException; 32 | 33 | import javax.servlet.http.HttpServlet; 34 | import javax.servlet.http.HttpServletRequest; 35 | import javax.servlet.http.HttpServletResponse; 36 | import javax.servlet.http.HttpSession; 37 | 38 | import org.webpki.cbor.CBORDecoder; 39 | import org.webpki.cbor.CBORPublicKey; 40 | 41 | import org.webpki.crypto.CryptoRandom; 42 | 43 | import org.webpki.fwp.FWPCrypto; 44 | 45 | import org.webpki.json.JSONObjectReader; 46 | import org.webpki.json.JSONObjectWriter; 47 | import org.webpki.json.JSONParser; 48 | 49 | /** 50 | * This Servlet is called by the LoginServlet SPA 51 | * 52 | */ 53 | public class FIDOLoginServlet extends HttpServlet { 54 | 55 | private static final long serialVersionUID = 1L; 56 | 57 | static Logger logger = Logger.getLogger(FIDOLoginServlet.class.getName()); 58 | 59 | static final String MISSING_ENROLL = "User ID missing, have you enrolled?"; 60 | 61 | public void doPost(HttpServletRequest request, HttpServletResponse response) 62 | throws IOException, ServletException { 63 | try { 64 | // Get the input (request) data. 65 | JSONObjectReader requestJson = WalletCore.getJSON(request); 66 | 67 | // Prepare for writing a response. 68 | JSONObjectWriter resultJson = new JSONObjectWriter(); 69 | 70 | // The FIDO server is stateful and its state MUST be checked 71 | // with that of the client. 72 | String phase = requestJson.getString(WalletCore.PHASE_JSON); 73 | 74 | // Tentative: return the same phase info as in the request. 75 | resultJson.setString(WalletCore.PHASE_JSON, phase); 76 | 77 | // Get the enrolled user. 78 | String userId = WalletCore.getWalletCookie(request); 79 | if (userId == null) { 80 | WalletCore.softError(response, resultJson, MISSING_ENROLL); 81 | return; 82 | } 83 | 84 | // Determine where are in the process. 85 | if (phase.equals(WalletCore.INIT_PHASE)) { 86 | 87 | // Firing up! We may have an old session but we don't really care. 88 | HttpSession session = request.getSession(true); 89 | 90 | // Clear existing login if any. 91 | session.removeAttribute(WalletCore.ATTR_LOGGED_IN_USER); 92 | 93 | // We need to specify which FIDO key to use. 94 | try (Connection connection = ApplicationService.jdbcDataSource.getConnection();) { 95 | // Get FIDO credentialId. 96 | DataBaseOperations.CoreClientData coreClientData = 97 | DataBaseOperations.getCoreClientData(userId, connection); 98 | if (coreClientData == null) { 99 | WalletCore.softError(response, resultJson, "User is missing, you need to reenroll"); 100 | return; 101 | } 102 | resultJson.setBinary(FWPCrypto.CREDENTIAL_ID, coreClientData.credentialId); 103 | } 104 | 105 | // - Provide FIDO challenge data 106 | byte[] challenge = CryptoRandom.generateRandom(32); 107 | resultJson.setBinary(FWPCrypto.CHALLENGE, challenge); 108 | 109 | // This what we send but we must also 110 | session.setAttribute(WalletCore.ATTR_LOGIN_DATA, new JSONObjectReader(resultJson)); 111 | 112 | } else if (phase.equals(WalletCore.FINALIZE_PHASE)) { 113 | 114 | // Login response! Now we must have an HTTP session. 115 | HttpSession session = request.getSession(false); 116 | if (session == null) { 117 | WalletCore.failed("Missing finalize session"); 118 | } 119 | 120 | // Get the object holding the login session in progress. 121 | JSONObjectReader loginData = 122 | (JSONObjectReader) session.getAttribute(WalletCore.ATTR_LOGIN_DATA); 123 | if (loginData == null) { 124 | WalletCore.failed("Login data missing"); 125 | } 126 | 127 | // Check that we are in "sync". 128 | byte[] clientDataJSON = requestJson.getBinary(FWPCrypto.CLIENT_DATA_JSON); 129 | if (!Arrays.equals( 130 | JSONParser.parse(clientDataJSON).getBinary(FWPCrypto.CHALLENGE), 131 | loginData.getBinary(FWPCrypto.CHALLENGE))) { 132 | WalletCore.failed("Challenge mismatch"); 133 | } 134 | 135 | // Here we are supposed to the check the signature.... 136 | byte[] authenticatorData = requestJson.getBinary(FWPCrypto.AUTHENTICATOR_DATA); 137 | session.setAttribute(WalletCore.ATTR_LOGIN_DATA, authenticatorData); 138 | byte[] signature = requestJson.getBinary(FWPCrypto.SIGNATURE); 139 | 140 | // Now, we have all client data needed to verify the signature. 141 | try (Connection connection = ApplicationService.jdbcDataSource.getConnection();) { 142 | // Get the anticipated public key 143 | DataBaseOperations.CoreClientData coreClientData = 144 | DataBaseOperations.getCoreClientData(userId, connection); 145 | PublicKey publicKey = 146 | CBORPublicKey.convert(CBORDecoder.decode(coreClientData.cosePublicKey)); 147 | FWPCrypto.validateFidoSignature( 148 | FWPCrypto.getWebPkiAlgorithm( 149 | FWPCrypto.publicKey2CoseSignatureAlgorithm(publicKey)), 150 | publicKey, 151 | authenticatorData, 152 | clientDataJSON, 153 | signature); 154 | } 155 | 156 | // User statistics... 157 | try (Connection connection = ApplicationService.jdbcDataSource.getConnection();) { 158 | DataBaseOperations.updateUserStatistics(userId, true, connection); 159 | } 160 | // We did it, set logged-in attribute. 161 | // Note that the session cookie is returned and set via the fetch() operation. 162 | session.setAttribute(WalletCore.ATTR_LOGGED_IN_USER, userId); 163 | 164 | // Refresh the persistent cookie. 165 | FIDOEnrollServlet.setWalletCookie(response, userId); 166 | 167 | logger.info("Logged-in user: " + userId); 168 | } else { 169 | WalletCore.failed("Unknown phase: " + phase); 170 | } 171 | WalletCore.returnJSON(response, resultJson); 172 | 173 | } catch (Exception e) { 174 | String message = e.getMessage(); 175 | logger.log(Level.SEVERE, WalletCore.getStackTrace(e, message)); 176 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 177 | PrintWriter writer = response.getWriter(); 178 | writer.print(message); 179 | writer.flush(); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/FIDOEnrollServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | import java.io.PrintWriter; 21 | 22 | import java.sql.Connection; 23 | 24 | import java.util.Arrays; 25 | import java.util.UUID; 26 | 27 | import java.util.logging.Logger; 28 | import java.util.logging.Level; 29 | 30 | import javax.servlet.ServletException; 31 | 32 | import javax.servlet.http.Cookie; 33 | import javax.servlet.http.HttpServlet; 34 | import javax.servlet.http.HttpServletRequest; 35 | import javax.servlet.http.HttpServletResponse; 36 | import javax.servlet.http.HttpSession; 37 | 38 | import org.webpki.crypto.CryptoRandom; 39 | 40 | import org.webpki.fwp.FWPCrypto; 41 | 42 | import org.webpki.json.JSONObjectReader; 43 | import org.webpki.json.JSONObjectWriter; 44 | import org.webpki.json.JSONParser; 45 | 46 | /** 47 | * This Servlet is called by the EnrollServlet SPA 48 | * 49 | */ 50 | public class FIDOEnrollServlet extends HttpServlet { 51 | 52 | private static final long serialVersionUID = 1L; 53 | 54 | static void setWalletCookie(HttpServletResponse response, String userId) { 55 | Cookie walletCookie = new Cookie(WalletCore.WALLET_COOKIE, userId); 56 | walletCookie.setMaxAge(8640000); // 100 days. 57 | walletCookie.setSecure(true); 58 | response.addCookie(walletCookie); 59 | } 60 | 61 | static Logger logger = Logger.getLogger(FIDOEnrollServlet.class.getName()); 62 | 63 | public void doPost(HttpServletRequest request, HttpServletResponse response) 64 | throws IOException, ServletException { 65 | try { 66 | // Get the input (request) data. 67 | JSONObjectReader requestJson = WalletCore.getJSON(request); 68 | 69 | // Prepare for writing a response. 70 | JSONObjectWriter resultJson = new JSONObjectWriter(); 71 | 72 | // The FIDO server is stateful and its state MUST be checked 73 | // with that of the client. 74 | String phase = requestJson.getString(WalletCore.PHASE_JSON); 75 | 76 | // Tentative: return the same phase info as in the request. 77 | resultJson.setString(WalletCore.PHASE_JSON, phase); 78 | 79 | // Determine where are in the process. 80 | if (phase.equals(WalletCore.INIT_PHASE)) { 81 | 82 | // Firing up! We may have an old session but we don't really care. 83 | HttpSession session = request.getSession(true); 84 | 85 | // Get the card holder 86 | String cardHolder = requestJson.getString(WalletCore.CARD_HOLDER); 87 | 88 | // Due to limitations in FIDO credential management we 89 | // reuse an existing user ID if there is one. 90 | String userId = WalletCore.getWalletCookie(request); 91 | if (userId == null) { 92 | userId = UUID.randomUUID().toString(); 93 | } 94 | 95 | // - Provide FIDO register challenge data 96 | byte[] challenge = CryptoRandom.generateRandom(32); 97 | resultJson.setBinary(FWPCrypto.CHALLENGE, challenge); 98 | 99 | // We use a UUID as the sole entry in the database and tie payment 100 | // credentials and (a single) FIDO authenticator to that. 101 | resultJson.setString(FWPCrypto.USER_ID, userId); 102 | 103 | // And the card holder. Also displayed by WebAuthn 104 | resultJson.setString(WalletCore.CARD_HOLDER, cardHolder); 105 | 106 | // We must also keep a copy of emitted data in a server session. 107 | // The client can only partially be trusted! 108 | session.setAttribute(WalletCore.ATTR_REGISTER_DATA, 109 | new JSONObjectReader(resultJson)); 110 | 111 | } else if (phase.equals(WalletCore.FINALIZE_PHASE)) { 112 | 113 | // Finalizing! Now we must have an HTTP session. 114 | HttpSession session = request.getSession(false); 115 | if (session == null) { 116 | WalletCore.failed("Missing finalize session"); 117 | } 118 | JSONObjectReader registerData = 119 | (JSONObjectReader) session.getAttribute(WalletCore.ATTR_REGISTER_DATA); 120 | if (registerData == null) { 121 | WalletCore.failed("Enrollment register data missing"); 122 | } 123 | 124 | // Check that we are in "sync". 125 | byte[] clientDataJSON = requestJson.getBinary(FWPCrypto.CLIENT_DATA_JSON); 126 | if (!Arrays.equals( 127 | JSONParser.parse(clientDataJSON).getBinary(FWPCrypto.CHALLENGE), 128 | registerData.getBinary(FWPCrypto.CHALLENGE))) { 129 | WalletCore.failed("Challenge mismatch"); 130 | } 131 | 132 | // User ID is central. 133 | String userId = registerData.getString(FWPCrypto.USER_ID); 134 | 135 | // Get card holder name. 136 | String cardHolder = registerData.getString(WalletCore.CARD_HOLDER); 137 | 138 | // The object that holds it all but we don't care about attestations yet... 139 | byte[] attestationObject = requestJson.getBinary(FWPCrypto.ATTESTATION_OBJECT); 140 | 141 | // But we do extract the core data... 142 | FWPCrypto.UserCredential userCredential = 143 | FWPCrypto.extractUserCredential(attestationObject); 144 | 145 | // Test only 146 | if (cardHolder.equals("-1")) WalletCore.failed(cardHolder); // Hard server error 147 | if (cardHolder.equals("-2")) { // Soft server error 148 | WalletCore.softError(response, resultJson, "Sorry, something isn't as it should"); 149 | return; 150 | } 151 | 152 | // Assuming that everything has been verified we are finally ready 153 | // issuing the requested payment credentials. 154 | 155 | // A single call will do the trick. 156 | try (Connection connection = ApplicationService.jdbcDataSource.getConnection();) { 157 | // Store basic data. 158 | DataBaseOperations.initiateUserAccount(userId, 159 | cardHolder, 160 | userCredential.credentialId, 161 | request.getServerName(), 162 | userCredential.rawCosePublicKey, 163 | request.getRemoteAddr(), 164 | connection); 165 | } 166 | 167 | // To enable the Web emulator, put the UUID in a persistent cookie. 168 | setWalletCookie(response, userId); 169 | 170 | logger.info("Successfully enrolled user: " + userId); 171 | } else { 172 | WalletCore.failed("Unknown phase: " + phase); 173 | } 174 | WalletCore.returnJSON(response, resultJson); 175 | 176 | } catch (Exception e) { 177 | String message = e.getMessage(); 178 | logger.log(Level.SEVERE, WalletCore.getStackTrace(e, message)); 179 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 180 | PrintWriter writer = response.getWriter(); 181 | writer.print(message); 182 | writer.flush(); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/ADServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.util.logging.Logger; 22 | 23 | import javax.servlet.ServletException; 24 | 25 | import javax.servlet.http.HttpServlet; 26 | import javax.servlet.http.HttpServletRequest; 27 | import javax.servlet.http.HttpServletResponse; 28 | 29 | import org.webpki.cbor.CBORDecoder; 30 | 31 | import org.webpki.crypto.HashAlgorithms; 32 | 33 | import org.webpki.fwp.FWPAssertionBuilder; 34 | import org.webpki.fwp.FWPCrypto; 35 | import org.webpki.fwp.FWPPaymentRequest; 36 | 37 | import org.webpki.json.JSONObjectReader; 38 | import org.webpki.json.JSONParser; 39 | 40 | /** 41 | * This FWP step creates Authorization Data (AD). 42 | * 43 | */ 44 | public class ADServlet extends HttpServlet { 45 | 46 | static Logger logger = Logger.getLogger(ADServlet.class.getName()); 47 | 48 | private static final long serialVersionUID = 1L; 49 | 50 | // DIV elements to turn on and turn off. 51 | private static final String WAITING_ID = "wait"; 52 | private static final String FAILED_ID = "fail"; 53 | private static final String ACTIVATE_ID = "activate"; 54 | 55 | 56 | static String sectionReference(String section) { 57 | return "" + section + ""; 59 | } 60 | 61 | public void doPost(HttpServletRequest request, HttpServletResponse response) 62 | throws IOException, ServletException { 63 | request.setCharacterEncoding("utf-8"); 64 | String walletInternal = request.getParameter(WalletCore.WALLET_INTERNAL); 65 | if (walletInternal == null) { 66 | WalletCore.failed("Missing wallet data"); 67 | } 68 | JSONObjectReader walletInternalJson = JSONParser.parse(walletInternal); 69 | try { 70 | // Get the enrolled user. 71 | String userId = WalletCore.getWalletCookie(request); 72 | if (userId == null) { 73 | response.sendRedirect("walletadmin"); 74 | return; 75 | } 76 | 77 | SystemDetection system = new SystemDetection(request.getHeader("user-agent")); 78 | 79 | // Build Authorization Data (AD) 80 | JSONObjectReader selectedCard = 81 | walletInternalJson.getObject(WalletCore.SELECTED_CARD); 82 | JSONObjectReader paymentRequest = 83 | walletInternalJson.getObject(WalletCore.PAYMENT_REQUEST); 84 | byte[] unsignedAssertion = new FWPAssertionBuilder() 85 | .setPaymentRequest(new FWPPaymentRequest(paymentRequest)) 86 | .setPaymentInstrumentData(selectedCard.getString(WalletCore.ACCOUNT_ID), 87 | selectedCard.getString(WalletCore.SERIAL_NUMBER), 88 | selectedCard.getString(WalletCore.PAYMENT_NETWORK_ID)) 89 | .setPayeeHost(request.getServerName()) 90 | .setPlatformData(system.operatingSystemName, 91 | system.operatingSystemVersion, 92 | system.browserName, 93 | system.browserVersion) 94 | .create(new FWPCrypto.FWPPreSigner(selectedCard.getBinary(WalletCore.PUBLIC_KEY))); 95 | 96 | StringBuilder html = new StringBuilder( 97 | "
" + 98 | "" + 100 | "" + 104 | "
" + 105 | 106 | "
Authorization Data (AD)
" + 107 | 108 | "
" + 109 | "
") 110 | .append(sectionReference("seq-4.2")) 111 | .append( 112 | ": The payment data to authorize. " + 113 | "AD represents the sole data input to the FIDO signature process." + 114 | "
That is, there is no FIDO authentication " + 115 | "server involved since FWP builds on the same " + 116 | ""Card Present" authorization concept as " + 117 | "EMV® and " + 119 | "Apple Pay®.
" + 120 | "
The data is shown in " + 121 | "CBOR diagnostic notation.
" + 123 | "
" + 124 | "
" + 125 | 126 | "
" + 127 | "waiting" + 129 | "
" + 130 | 131 | "
" + 132 | 133 | "
" + 134 | "
" + 135 | "Authorize (Sign) using FIDO..." + 136 | "
" + 137 | "
" + 138 | 139 | "
") 140 | .append(HTML.encode(CBORDecoder.decode(unsignedAssertion).toString(), true)) 141 | .append( 142 | "
"); 143 | 144 | String js = new StringBuilder( 145 | 146 | WalletCore.GO_HOME_JAVASCRIPT + 147 | 148 | "const serviceUrl = 'fidopay';\n" + 149 | 150 | WalletCore.FWP_JAVASCRIPT + 151 | 152 | "async function doPay() {\n" + 153 | " try {\n" + 154 | " document.getElementById('" + ACTIVATE_ID + "').style.display = 'none';\n" + 155 | " document.getElementById('" + WAITING_ID + "').style.display = 'block';\n" + 156 | 157 | " const options = {\n" + 158 | " challenge: b64urlToU8arr('" + 159 | ApplicationService.base64UrlEncode( 160 | HashAlgorithms.SHA256.digest(unsignedAssertion)) + 161 | "'),\n" + 162 | 163 | " allowCredentials: [{type: 'public-key', " + 164 | "id: b64urlToU8arr('" + 165 | selectedCard.getString(WalletCore.CREDENTIAL_ID) + 166 | "')}],\n" + 167 | 168 | " userVerification: '" + WalletCore.USER_VERIFICATION + "',\n" + 169 | 170 | " timeout: 120000\n" + 171 | " };\n" + 172 | 173 | // " console.log(options);\n" + 174 | " const result = await navigator.credentials.get({ publicKey: options });\n" + 175 | // " console.log(result);\n" + 176 | " const returnJson = await exchangeJSON({" + 177 | 178 | FWPCrypto.AUTHENTICATOR_DATA + 179 | ":arrBufToB64url(result.response.authenticatorData)," + 180 | 181 | FWPCrypto.SIGNATURE + 182 | ":arrBufToB64url(result.response.signature)," + 183 | 184 | FWPCrypto.CLIENT_DATA_JSON + 185 | ":arrBufToB64url(result.response.clientDataJSON)," + 186 | 187 | WalletCore.FWP_AD + 188 | ": '" + 189 | ApplicationService.base64UrlEncode(unsignedAssertion) + 190 | "'}, null);\n" + 191 | 192 | " document.getElementById('" + WalletCore.FWP_SAD + 193 | "').value = returnJson." + WalletCore.FWP_SAD + ";\n" + 194 | " document.forms.shoot.submit();\n" + 195 | 196 | // Errors are effectively aborting so a single try-catch does the trick. 197 | " } catch (error) {\n" + 198 | " let message = 'Fail: ' + error;\n" + 199 | " console.log(message);\n" + 200 | " document.getElementById('" + WAITING_ID + "').style.display = 'none';\n" + 201 | " let e = document.getElementById('" + FAILED_ID + "');\n" + 202 | " e.textContent = message;\n" + 203 | " e.style.display = 'block';\n" + 204 | " }\n" + 205 | 206 | "}\n").toString(); 207 | HTML.standardPage(response, Actors.FWP, js, html); 208 | } catch (Exception e) { 209 | HTML.errorPage(response, e); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/org/webpki/webapps/fwp/LoginServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 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.fwp; 18 | 19 | import java.io.IOException; 20 | 21 | import java.sql.Connection; 22 | 23 | import java.util.logging.Logger; 24 | 25 | import javax.servlet.ServletException; 26 | 27 | import javax.servlet.http.HttpServlet; 28 | import javax.servlet.http.HttpServletRequest; 29 | import javax.servlet.http.HttpServletResponse; 30 | import javax.servlet.http.HttpSession; 31 | 32 | import org.webpki.cbor.CBORDecoder; 33 | import org.webpki.cbor.CBORObject; 34 | import org.webpki.cbor.CBORPublicKey; 35 | 36 | import org.webpki.util.HexaDecimal; 37 | 38 | import org.webpki.crypto.KeyAlgorithms; 39 | 40 | import org.webpki.fwp.FWPCrypto; 41 | 42 | /** 43 | * Enrolled credentials can also be used for FIDO/WebAuthn. 44 | * 45 | */ 46 | public class LoginServlet extends HttpServlet { 47 | 48 | static Logger logger = Logger.getLogger(LoginServlet.class.getName()); 49 | 50 | private static final long serialVersionUID = 1L; 51 | 52 | // DIV elements to turn on and turn off. 53 | private static final String WAITING_ID = "wait"; 54 | private static final String FAILED_ID = "fail"; 55 | private static final String ACTIVATE_ID = "activate"; 56 | 57 | 58 | public void doGet(HttpServletRequest request, HttpServletResponse response) 59 | throws IOException, ServletException { 60 | StringBuilder html = new StringBuilder( 61 | "
" + 62 | 63 | "
Login Test
" + 64 | 65 | "
" + 66 | "
" + 67 | "Although login is not a part of FIDO Web Pay, associated FIDO authenticators " + 68 | "can optionally be used for authentication as well." + 69 | "
Note that for authentication (unlike " + 70 | "for payments), normal FIDO domain constraints apply. That is, " + 71 | "logins are restricted to the actual Issuer.
" + 72 | "
" + 73 | "
" + 74 | 75 | "
" + 76 | "waiting" + 78 | "
" + 79 | 80 | "
" + 81 | 82 | "
" + 83 | "
" + 84 | "Login..." + 85 | "
" + 86 | "
" + 87 | "
"); 88 | 89 | String js = new StringBuilder( 90 | "const serviceUrl = 'fidologin';\n" + 91 | 92 | WalletCore.FWP_JAVASCRIPT + 93 | 94 | "async function doLogin() {\n" + 95 | " try {\n" + 96 | " document.getElementById('" + ACTIVATE_ID + "').style.display = 'none';\n" + 97 | " document.getElementById('" + WAITING_ID + "').style.display = 'block';\n" + 98 | " const initPhase = await exchangeJSON({},'" + WalletCore.INIT_PHASE + "');\n" + 99 | 100 | " const options = {\n" + 101 | " challenge: b64urlToU8arr(initPhase." + FWPCrypto.CHALLENGE + "),\n" + 102 | 103 | " allowCredentials: [{type: 'public-key', " + 104 | "id: b64urlToU8arr(initPhase." + FWPCrypto.CREDENTIAL_ID + ")}],\n" + 105 | 106 | " userVerification: '" + WalletCore.USER_VERIFICATION + "',\n" + 107 | 108 | " timeout: 120000\n" + 109 | " };\n" + 110 | 111 | // " console.log(options);\n" + 112 | " const result = await navigator.credentials.get({ publicKey: options });\n" + 113 | // " console.log(result);\n" + 114 | " const finalizePhase = await exchangeJSON({" + 115 | 116 | FWPCrypto.AUTHENTICATOR_DATA + 117 | ":arrBufToB64url(result.response.authenticatorData)," + 118 | 119 | FWPCrypto.SIGNATURE + 120 | ":arrBufToB64url(result.response.signature)," + 121 | 122 | FWPCrypto.CLIENT_DATA_JSON + 123 | ":arrBufToB64url(result.response.clientDataJSON)},'" + 124 | 125 | WalletCore.FINALIZE_PHASE + "');\n" + 126 | 127 | " document.forms.shoot.submit();\n" + 128 | 129 | // Errors are effectively aborting so a single try-catch does the trick. 130 | " } catch (error) {\n" + 131 | " if (error == '" + FIDOLoginServlet.MISSING_ENROLL + "') {\n" + 132 | " document.location.href = 'walletadmin';\n" + 133 | " return;\n" + 134 | " }\n" + 135 | " let message = 'Fail: ' + error;\n" + 136 | " console.log(message);\n" + 137 | " document.getElementById('" + WAITING_ID + "').style.display = 'none';\n" + 138 | " let e = document.getElementById('" + FAILED_ID + "');\n" + 139 | " e.textContent = message;\n" + 140 | " e.style.display = 'block';\n" + 141 | " }\n" + 142 | 143 | "}\n").toString(); 144 | HTML.standardPage(response, Actors.ISSUER, js, html); 145 | } 146 | 147 | public void doPost(HttpServletRequest request, HttpServletResponse response) 148 | throws IOException, ServletException { 149 | try { 150 | // Get the enrolled user. 151 | String userId = WalletCore.getWalletCookie(request); 152 | if (userId == null) { 153 | WalletCore.failed("User ID missing, have you enrolled?"); 154 | } 155 | 156 | // Lookup in database 157 | DataBaseOperations.CoreClientData coreClientData; 158 | try (Connection connection = ApplicationService.jdbcDataSource.getConnection();) { 159 | // Get the anticipated public key 160 | coreClientData = DataBaseOperations.getCoreClientData(userId, connection); 161 | } 162 | 163 | HttpSession session = request.getSession(false); 164 | 165 | if (session == null) { 166 | WalletCore.failed("No session!"); 167 | } 168 | 169 | CBORObject publicKey = CBORDecoder.decode(coreClientData.cosePublicKey); 170 | 171 | StringBuilder html = new StringBuilder( 172 | "
Login Succeeded!
" + 173 | 174 | "
" + 175 | "
" + 176 | "This is what the login returned from the " + 177 | "Issuer database " + 178 | "and from the assertion." + 179 | "
" + 180 | "
" + 181 | 182 | "
" + 183 | "
Card Holder
" + 184 | "
") 185 | .append(HTML.encode(coreClientData.cardHolder, false)) 186 | .append("
" + 187 | 188 | "
User ID
" + 189 | "
") 190 | .append(userId) 191 | .append("
" + 192 | 193 | "
Web Session ID
" + 194 | "
") 195 | .append(session.getId()) 196 | .append("
" + 197 | 198 | "
FIDO Credential ID (B64U)
" + 199 | "
") 200 | .append(ApplicationService.base64UrlEncode(coreClientData.credentialId)) 201 | .append("
" + 202 | 203 | "
RP ID
" + 204 | "
") 205 | .append(coreClientData.rpId) 206 | .append("
" + 207 | 208 | "
FIDO Authenticator Data (HEX)
" + 209 | "
") 210 | .append(HexaDecimal.encode((byte[])session.getAttribute(WalletCore.ATTR_LOGIN_DATA))) 211 | .append("
" + 212 | 213 | "
FIDO ") 214 | .append(KeyAlgorithms.getKeyAlgorithm(CBORPublicKey.convert(publicKey)).getKeyType()) 215 | .append( 216 | " Public Key (COSE)
" + 217 | "
") 218 | .append(HTML.encode(publicKey.toString(), true)) 219 | .append("
" + 220 | 221 | "
"); 222 | 223 | // In our case we have no application using the authentication... 224 | session.invalidate(); 225 | 226 | HTML.standardPage(response, Actors.ISSUER, WalletCore.GO_HOME_JAVASCRIPT, html); 227 | } catch (Exception e) { 228 | HTML.errorPage(response, e); 229 | } 230 | } 231 | } 232 | --------------------------------------------------------------------------------