├── .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 |
8 |
--------------------------------------------------------------------------------
/web/images/wallet-internal.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
24 |
--------------------------------------------------------------------------------
/docgen/fwp-crypto.svg:
--------------------------------------------------------------------------------
1 |
2 |
37 |
--------------------------------------------------------------------------------
/web/images/issuer.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 | "
");
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 | "" +
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.
" +
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 | "" +
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.
");
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 | "" +
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.
");
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 | "" +
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 | ").
");
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 | "" +
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 | "
"));
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 | "" +
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.
clientDataJSONas well 👈