|
1
|
# FWD web certificate does not cover the hosts browsers connect to (remote broker clients, signature simulator)
|
|
2
|
|
|
3
|
Spun off from task **11409a** (SIGNATURE widget / SigWeb simulator). Task: **11987**.
|
|
4
|
Background notes: `11409a/.tmp/11409a-simulator-cert-san.md`.
|
|
5
|
|
|
6
|
## Problem
|
|
7
|
|
|
8
|
An FWD server has **one web certificate**, and its SAN holds **one DNS name**. Every embedded HTTPS server
|
|
9
|
that FWD starts presents that same certificate, but browsers reach those servers under **different host
|
|
10
|
names**. When the name in the URL is not in the certificate's SAN, the browser rejects the connection.
|
|
11
|
|
|
12
|
Two known cases:
|
|
13
|
|
|
14
|
| Case | URL the browser opens | Name the certificate must cover |
|
|
15
|
|---|---|---|
|
|
16
|
| **Web client spawned by a remote broker** | `https://<broker remote:agent:host>:<client port>/index.html?<token>` | each broker host's `remote:agent:host` (host name **or IP address**) |
|
|
17
|
| **Signature pad simulator** (11409a, `webClient/signaturePadInSimMode`) | `https://tablet.sigwebtablet.com:47290/SigWeb/...` | `tablet.sigwebtablet.com` |
|
|
18
|
|
|
19
|
With the default FWD setup the certificate is also issued by a private project CA (e.g. `hotel-root`,
|
|
20
|
valid until **2027-06-03**), so the browser first fails with `ERR_CERT_AUTHORITY_INVALID`. Once the CA is
|
|
21
|
trusted, it fails with `ERR_CERT_COMMON_NAME_INVALID`. Users can click past the warning for a top-level
|
|
22
|
page (remote broker client), but not for XHR calls (simulator, `SigWebTablet.js`). Even where clicking past
|
|
23
|
works, Chrome stores the exception per host, so it has to be repeated for every broker host.
|
|
24
|
|
|
25
|
## Root cause
|
|
26
|
|
|
27
|
### One certificate, shipped to every client
|
|
28
|
|
|
29
|
- The web certificate is stored per server in the directory under
|
|
30
|
`/security/certificates/webCertificates/<serverId>/{alias,certificate,key/...}`
|
|
31
|
(`security/WebCertificates.java:85`, `getWebCertificates` around :187). `SecurityCache.java:636-662` loads
|
|
32
|
it as the single `webStore`, under `webServerAlias`.
|
|
33
|
- `ServerKeyStore.getStore()` (`main/ServerKeyStore.java:226-288`) makes a copy for each spawn that contains
|
|
34
|
the web key store, protected with fresh passwords. `ServerKeyStore` is `Externalizable`, so the web
|
|
35
|
private key and certificate are serialized to the spawned client JVM, **including clients on remote broker
|
|
36
|
hosts**.
|
|
37
|
- `WebClientSpawner.java:203` → `TemporaryClientTask` → `EmbeddedWebServer.startupServer(keyStore, ...)`
|
|
38
|
(:528). `GenericWebServer.getSslContextFactory()` (`web/GenericWebServer.java:983-990`) uses
|
|
39
|
`getWebKeyStore()` when it is valid.
|
|
40
|
- `SignatureSimulator.start()` passes `ServerKeyStore.getStore()` to its own `GenericWebServer`, so the
|
|
41
|
simulator presents the same web certificate.
|
|
42
|
- Nothing is keyed by broker host: the broker, spawner and key store code has no per-host alias, key store or
|
|
43
|
directory node.
|
|
44
|
|
|
45
|
### Remote clients are reached under the broker's name
|
|
46
|
|
|
47
|
- `BrokerCore.initialize()` (`main/BrokerCore.java:456-476`) sets `dedicatedHost` from `remote:agent:host`.
|
|
48
|
The default is `InetAddress.getLocalHost().getHostName()` of the broker machine. The value may be a host name
|
|
49
|
**or an IP address** (validated by `Utils.isValidHostNameOrIpAddress`).
|
|
50
|
- For a remote spawn, `WebClientBuilder.java:173-177` passes `client:web:host=${remote.agent.host}`. Also,
|
|
51
|
`WebClientSpawner.java:747-756` deliberately does **not** forward the configured `webClient/host`.
|
|
52
|
- `EmbeddedWebServerImpl.getServerURI()` (`ui/client/driver/web/EmbeddedWebServerImpl.java:327-336`)
|
|
53
|
builds `https://<host>:<port>/` from that value, and the browser is redirected there.
|
|
54
|
- Exceptions:
|
|
55
|
- Dedicated mode forces the host to `localhost` (`WebClientSpawner.java:513-520`).
|
|
56
|
- With `webClient/portsRange/forwardedHost` set, the browser sees only the reverse proxy name
|
|
57
|
(`WebClientSpawner.java:531-540`).
|
|
58
|
|
|
59
|
### The tooling can only write one name
|
|
60
|
|
|
61
|
- `SSLCertGenUtil.generateCertificate(alias)` (`security/SSLCertGenUtil.java:1344-1365`) takes the CN from
|
|
62
|
the single string at `/security/certificates/company/hostnames/<alias>` (default `"<alias> server|account"`).
|
|
63
|
- `BCCertFactory.java:876` copies that CN into **one** `dNSName` SAN entry:
|
|
64
|
```java
|
|
65
|
GeneralNames ext4 = new GeneralNames(new GeneralName(GeneralName.dNSName, cn));
|
|
66
|
```
|
|
67
|
It writes no further DNS names, no IP-address SANs and no wildcards.
|
|
68
|
- The only existing way around this is the admin REST upload (`admin/server/CertRestHandler.java:189-224`).
|
|
69
|
It stores an externally issued PEM certificate/key/chain as the server's web certificate and hot-reloads
|
|
70
|
it. That allows a cert with several SANs or a wildcard, but it is still one cert per server, made outside
|
|
71
|
FWD.
|
|
72
|
|
|
73
|
## Already done in 11409a (not part of this task)
|
|
74
|
|
|
75
|
- `SignatureSimulator.warnIfHostNotCovered()` logs a startup WARNING when no key entry in the simulator's key
|
|
76
|
store lists `tablet.sigwebtablet.com` in its SAN. The check is `SignatureSimulator.sanCoversHost()` (DNS
|
|
77
|
entries only, case-insensitive, one-label wildcard), covered by unit test `SignatureSimulatorSanTest`.
|
|
78
|
|
|
79
|
## Scope
|
|
80
|
|
|
81
|
### In scope
|
|
82
|
|
|
83
|
1. **Multi-name certificates from FWD tooling.** `gencert.sh` / `SSLCertGenUtil` can produce a certificate
|
|
84
|
whose SAN lists several DNS names, IP addresses and `*.` wildcards.
|
|
85
|
2. **Remote broker clients.** A remote-broker web client presents a certificate that covers the host name the
|
|
86
|
browser is redirected to, whether `remote:agent:host` is a host name or an IP address.
|
|
87
|
3. **Signature simulator.** The simulator presents a certificate that covers `tablet.sigwebtablet.com`.
|
|
88
|
4. **Diagnostics.** When an FWD embedded web server starts on a host name its certificate does not cover, a
|
|
89
|
WARNING is logged (remote broker client start-up, alongside the existing simulator warning). The SAN
|
|
90
|
matcher is shared by both, not duplicated.
|
|
91
|
5. **Documentation.** How to configure the names, regenerate the certificate, distribute it, and trust the
|
|
92
|
project root CA (Chrome/NSS, Firefox, Windows). How to choose between multi-name certs, wildcards and a
|
|
93
|
reverse proxy for remote brokers.
|
|
94
|
|
|
95
|
### Out of scope
|
|
96
|
|
|
97
|
- Any change to `SigWebTablet.js` or `p2j.signature.html`. These client resources are frozen; the vendor SDK
|
|
98
|
must stay pristine.
|
|
99
|
- Replacing the per-project private CA, or using a publicly trusted certificate (still possible through the
|
|
100
|
admin upload).
|
|
101
|
- TLS between FWD client and server on the FWD protocol. Its SAN/CN check (`SecurityManager.java:1258-1280`,
|
|
102
|
`fetchAltNames`) is not the browser-facing certificate.
|
|
103
|
- Renewing `hotel-root` before 2027-06-03. Track that separately; it is only noted here.
|
|
104
|
|
|
105
|
## Options
|
|
106
|
|
|
107
|
| Option | Change | Pros | Cons |
|
|
108
|
|---|---|---|---|
|
|
109
|
| **A. Multi-name SAN in the generator** (chosen) | `/security/certificates/company/hostnames/<alias>` takes a comma-separated list. The first entry becomes the CN, and every entry becomes a SAN entry: `dNSName` for host names and wildcards, `iPAddress` for IPv4/IPv6 literals. Regenerate with `./gencert.sh <alias>`. | Small, generic change in one place. One cert covers the server, every broker host and `tablet.sigwebtablet.com`. Works with the existing "one web cert per server" distribution. No runtime change. | A new broker host means the certificate must be regenerated and the CA-signed cert redeployed. The list grows with the broker fleet (a wildcard keeps it short). |
|
|
110
|
| B. Per-broker-host certificate (possible follow-up) | Key a web certificate by `remote:agent:host`. When the spawn is remote, `ServerKeyStore.getStore()` / `WebClientSpawner` picks the broker's cert, falling back to the server web cert. | Each host gets a cert that covers exactly its own name. A broker's private key is not shared with other hosts. | New directory layout, plus admin UI/REST and generation support. More code in the security-sensitive path. Doesn't help the simulator by itself. |
|
|
111
|
| C. Wildcard only | `*.example.com` in the SAN (needs A's wildcard support, or an admin upload) | One short entry covers every broker in the domain | Doesn't cover IP addresses, `localhost`, or `tablet.sigwebtablet.com`, so in practice it is used together with A. |
|
|
112
|
| D. Reverse proxy (existing) | Set `webClient/portsRange/forwardedHost`, and terminate TLS on the proxy | No FWD change. The browser sees only one name. | Infrastructure to operate. Doesn't cover the simulator, which the browser reaches directly. |
|
|
113
|
|
|
114
|
**Decision (2026-10-13): option A**, with the shared diagnostic (scope item 4). It removes the root
|
|
115
|
limitation where it lives and covers both known cases with the existing distribution model. Options B–D are
|
|
116
|
not part of this task. C (wildcards) and D (reverse proxy) remain available to deployments: C is supported by
|
|
117
|
A's syntax, and D needs no FWD change. B is a separate follow-up, to be done only if a deployment requires
|
|
118
|
per-host keys.
|
|
119
|
|
|
120
|
## Requirements
|
|
121
|
|
|
122
|
- **R1.** The host name value accepts a list separated by commas. Whitespace around items and empty items are
|
|
123
|
ignored.
|
|
124
|
- **R2.** The first entry is the subject CN. **All** entries become SAN entries, in configured order with
|
|
125
|
duplicates removed: `iPAddress` for IPv4/IPv6 literals, and `dNSName` for everything else (including `*.`
|
|
126
|
wildcards).
|
|
127
|
- **R3.** A single-name value produces exactly the same certificate as today (CN = SAN = that name).
|
|
128
|
- **R4.** Invalid input is rejected with a clear error: an empty list, an entry that is not a valid DNS
|
|
129
|
name/wildcard/IP literal, or a wildcard that is not just the leftmost label. The tool must not produce a
|
|
130
|
certificate without a SAN.
|
|
131
|
- **R5.** Move the SAN matcher (`SignatureSimulator.sanCoversHost`) into a shared security/web utility.
|
|
132
|
Extend it to `iPAddress` entries, which must be compared as addresses, not as strings.
|
|
133
|
- **R6.** Remote-broker web client: after `startupServer`, if the URI host (not a `forwardedHost` URI, and not
|
|
134
|
dedicated-mode `localhost`) is not covered by the certificate's SAN, log one WARNING. Name the host and the
|
|
135
|
SAN entries, and point to the configuration that fixes it.
|
|
136
|
- **R7.** Signature simulator: keep the existing warning, now based on the shared matcher. No other simulator
|
|
137
|
change is needed.
|
|
138
|
- **R8.** Docs (wiki / `gencert.sh` notes): the new syntax and examples, e.g.
|
|
139
|
`fwdsrv.example.com,broker1.example.com,10.0.0.15,tablet.sigwebtablet.com`, with a wildcard variant.
|
|
140
|
Explain how to trust the root CA, and when to use a proxy instead.
|
|
141
|
|
|
142
|
## Acceptance criteria
|
|
143
|
|
|
144
|
1. **Generation.** Set the web alias host name list to
|
|
145
|
`localhost,broker1.test,10.0.0.15,*.brokers.test,tablet.sigwebtablet.com` and run `./gencert.sh <alias>`.
|
|
146
|
Then `openssl x509 -noout -ext subjectAltName` shows exactly those entries, with the IP as `IP Address:`.
|
|
147
|
2. **Single name, no regression.** Regenerating an alias that has one host name gives the same CN/SAN as
|
|
148
|
before.
|
|
149
|
3. **Remote broker.**
|
|
150
|
- Configure a broker whose `remote:agent:host` is in the list and the root CA is trusted. The browser opens
|
|
151
|
the remote web client with no certificate warning, and no WARNING is logged.
|
|
152
|
- Configure a broker whose host is **not** in the list. The client log shows the R6 WARNING.
|
|
153
|
- Repeat both runs with `remote:agent:host` set to an IP address.
|
|
154
|
4. **Simulator.** With `tablet.sigwebtablet.com` in the list, `?simMode` loads with no certificate error and
|
|
155
|
the "does not cover" warning is not logged. `IsSigWebInstalled()` succeeds and a signature is captured end
|
|
156
|
to end. `openssl s_client -connect 127.0.0.1:47290 -servername tablet.sigwebtablet.com` shows the new cert.
|
|
157
|
5. **Unit tests** for R1–R5: single name, several names, whitespace, duplicates, wildcard, IPv4, IPv6, invalid
|
|
158
|
input, and IP matching.
|
|
159
|
|
|
160
|
## Open questions
|
|
161
|
|
|
162
|
- Could a directory `cfgOverrides` / `webOverrides` entry (appended later in `WebClientSpawner.java:776-784`)
|
|
163
|
replace the broker-supplied `client:web:host` on the command line? If so, R6 must check the effective host.
|
|
164
|
Override precedence in `BootstrapConfig.processOverrides` has not been traced.
|
|
165
|
- Does the server's main web server (`WebServer`, including `hotReloadSslCertificate`) use the same
|
|
166
|
`webServerAlias` cert? This is assumed but not verified. It affects whether the server's own host name must
|
|
167
|
stay in the list (it should anyway).
|
|
168
|
|
|
169
|
## Risks / notes
|
|
170
|
|
|
171
|
- Certificate tooling is security-sensitive, so review it carefully. The CA key handling must not change.
|
|
172
|
- The web private key is already shared with every remote broker host (serialized in `ServerKeyStore`).
|
|
173
|
Option A doesn't change that; option B would.
|
|
174
|
- Existing deployments keep working until they regenerate certificates; nothing changes automatically.
|
|
175
|
- `hotel-root` expires on 2027-06-03, after which every certificate it issued stops being valid.
|
|
176
|
- Real Topaz SigWeb takes the same approach (local HTTPS service, a cert for `tablet.sigwebtablet.com`, and
|
|
177
|
the issuer installed into the OS trust store), so the CA trust step is expected for the simulator.
|