@1shotapi/ows-signer 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/html/signer.css +8 -0
- package/package.json +1 -1
- package/src/main.js +13 -1
- package/src/rpc.js +4 -0
- package/src/ui.js +29 -0
package/README.md
CHANGED
|
@@ -101,16 +101,23 @@ reference/ 1ShotPay reference (not published)
|
|
|
101
101
|
|
|
102
102
|
Serve the Signing Layer document (and its `/src/*` modules) with a **fail-closed** CSP. The signer has no legitimate network I/O — only same-origin scripts/styles and `postMessage` / WebAuthn / WebCrypto.
|
|
103
103
|
|
|
104
|
+
**Branding and Signing must share the same origin** (e.g. `https://wallet.example/` + `/signer/`). Cross-origin embeds of a shared CDN/IPFS signer are not supported: a hostile Branding could otherwise drive passkey-gated signing with attacker-written Confirm copy. The Signing Layer rejects parent `postMessage` when `event.origin !== location.origin`, and hosts should set `frame-ancestors 'self'` (optionally `X-Frame-Options: SAMEORIGIN`) so foreign pages cannot frame `/signer/` at all.
|
|
105
|
+
|
|
104
106
|
Canonical header (single line in production):
|
|
105
107
|
|
|
106
108
|
```http
|
|
107
|
-
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'none'; img-src 'none'; font-src 'none'; media-src 'none'; object-src 'none'; child-src 'none'; frame-src 'none'; worker-src 'none'; manifest-src 'none'; base-uri 'none'; form-action 'none'; upgrade-insecure-requests
|
|
109
|
+
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'none'; img-src 'none'; font-src 'none'; media-src 'none'; object-src 'none'; child-src 'none'; frame-src 'none'; worker-src 'none'; manifest-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'; upgrade-insecure-requests
|
|
108
110
|
```
|
|
109
111
|
|
|
110
112
|
- Apply on the **Signing Layer origin path** (e.g. nginx `location /signer/`), not on the Branding Layer SPA.
|
|
111
|
-
-
|
|
113
|
+
- Nesting safety also remains in JS (`event.source === window.parent`, `window.parent !== window.top`, same-origin parent).
|
|
114
|
+
- Reveal / export UI requires the signer document to be **visible and focused** before showing the key or writing the clipboard (defense against obscured-frame clickjacking).
|
|
112
115
|
- Keep iframe Permissions Policy `allow` for WebAuthn / clipboard (set before `src` in `createSignerIframe`) — that is not CSP.
|
|
113
116
|
|
|
117
|
+
## Audits
|
|
118
|
+
|
|
119
|
+
See [audits/](audits/) for scoped security review notes (AI-assisted).
|
|
120
|
+
|
|
114
121
|
## On-chain
|
|
115
122
|
|
|
116
123
|
See `@1shotapi/ows-onchain`.
|
package/html/signer.css
CHANGED
|
@@ -169,6 +169,14 @@ body.ows-ceremony-open {
|
|
|
169
169
|
color: var(--ows-text);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
.ows-ceremony-origin {
|
|
173
|
+
margin: 8px 0 0;
|
|
174
|
+
font-size: 0.75rem;
|
|
175
|
+
line-height: 1.4;
|
|
176
|
+
color: var(--ows-text-muted);
|
|
177
|
+
word-break: break-all;
|
|
178
|
+
}
|
|
179
|
+
|
|
172
180
|
.ows-ceremony-body {
|
|
173
181
|
flex: 0 1 auto;
|
|
174
182
|
min-height: 0;
|
package/package.json
CHANGED
package/src/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { handleRequest } from "./handlers.js";
|
|
2
2
|
import {
|
|
3
3
|
abandonCeremony,
|
|
4
|
+
getTrustedParentOrigin,
|
|
4
5
|
isValidNesting,
|
|
5
6
|
setCancelConfirmHook,
|
|
6
7
|
setTrustedParentOrigin,
|
|
@@ -8,6 +9,17 @@ import {
|
|
|
8
9
|
import { cancelPendingCeremonyConfirm, initUi } from "./ui.js";
|
|
9
10
|
import { isValidParentMessage, parseRequest } from "./rpc.js";
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @param {MessageEvent} event
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
function acceptParentMessage(event) {
|
|
17
|
+
if (!isValidParentMessage(event)) return false;
|
|
18
|
+
const trusted = getTrustedParentOrigin();
|
|
19
|
+
if (trusted !== null && event.origin !== trusted) return false;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
function bootstrap() {
|
|
12
24
|
const root = document.getElementById("ows-signer-root");
|
|
13
25
|
if (!root) {
|
|
@@ -22,7 +34,7 @@ function bootstrap() {
|
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
window.addEventListener("message", (event) => {
|
|
25
|
-
if (!
|
|
37
|
+
if (!acceptParentMessage(event)) return;
|
|
26
38
|
|
|
27
39
|
const data = event.data;
|
|
28
40
|
if (
|
package/src/rpc.js
CHANGED
|
@@ -23,6 +23,9 @@ export function emitEvent(target, targetOrigin, event, correlationId, data) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
+
* Branding↔Signing must be same origin. Reject cross-origin parents
|
|
27
|
+
* (e.g. sketchy.ru embedding wallet.1shotapi.com/signer/).
|
|
28
|
+
*
|
|
26
29
|
* @param {MessageEvent} event
|
|
27
30
|
* @returns {boolean}
|
|
28
31
|
*/
|
|
@@ -30,6 +33,7 @@ export function isValidParentMessage(event) {
|
|
|
30
33
|
if (event.source !== window.parent) return false;
|
|
31
34
|
if (window.parent === window.top) return false;
|
|
32
35
|
if (event.source === window) return false;
|
|
36
|
+
if (event.origin !== window.location.origin) return false;
|
|
33
37
|
return true;
|
|
34
38
|
}
|
|
35
39
|
|
package/src/ui.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { to0xHex } from "./hex.js";
|
|
2
2
|
import { debugLog, describeCeremonyEnvironment } from "./debug.js";
|
|
3
|
+
import { getTrustedParentOrigin } from "./state.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reveal / clipboard must not run when the signer document is hidden or
|
|
7
|
+
* unfocused (clickjack / obscured-frame mitigation).
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isSignerDocumentInteractive() {
|
|
11
|
+
if (typeof document === "undefined") return false;
|
|
12
|
+
if (document.visibilityState !== "visible") return false;
|
|
13
|
+
if (typeof document.hasFocus === "function" && !document.hasFocus()) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
3
18
|
|
|
4
19
|
/** @type {HTMLElement | null} */
|
|
5
20
|
let root = null;
|
|
@@ -198,6 +213,13 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
198
213
|
text.textContent = ui.explanation;
|
|
199
214
|
body.append(text);
|
|
200
215
|
|
|
216
|
+
const requesterOrigin =
|
|
217
|
+
getTrustedParentOrigin() ?? window.location.origin;
|
|
218
|
+
const originLine = document.createElement("p");
|
|
219
|
+
originLine.className = "ows-ceremony-origin";
|
|
220
|
+
originLine.textContent = `Requester: ${requesterOrigin}`;
|
|
221
|
+
body.append(originLine);
|
|
222
|
+
|
|
201
223
|
const actions = document.createElement("div");
|
|
202
224
|
actions.className = "ows-ceremony-actions";
|
|
203
225
|
|
|
@@ -258,6 +280,9 @@ export function promptCeremonyConfirm(fields, runOnConfirm) {
|
|
|
258
280
|
* Show the private key hex with Copy + Done. Resolves when the user clicks Done
|
|
259
281
|
* (or the UI is cleared), so Branding can keep the ceremony panel open.
|
|
260
282
|
*
|
|
283
|
+
* Requires the signer document to be visible and focused before showing the key
|
|
284
|
+
* or writing the clipboard.
|
|
285
|
+
*
|
|
261
286
|
* @param {Uint8Array} privateKey
|
|
262
287
|
* @returns {Promise<void>}
|
|
263
288
|
*/
|
|
@@ -265,6 +290,9 @@ export function showPrivateKey(privateKey) {
|
|
|
265
290
|
if (!root) {
|
|
266
291
|
return Promise.reject(new Error("uiNotInitialized"));
|
|
267
292
|
}
|
|
293
|
+
if (!isSignerDocumentInteractive()) {
|
|
294
|
+
return Promise.reject(new Error("signerNotInteractive"));
|
|
295
|
+
}
|
|
268
296
|
cancelPendingCeremonyConfirm();
|
|
269
297
|
clearUi();
|
|
270
298
|
const hex = to0xHex(privateKey);
|
|
@@ -277,6 +305,7 @@ export function showPrivateKey(privateKey) {
|
|
|
277
305
|
copyBtn.className = "ows-copy";
|
|
278
306
|
copyBtn.textContent = "Copy";
|
|
279
307
|
copyBtn.addEventListener("click", async () => {
|
|
308
|
+
if (!isSignerDocumentInteractive()) return;
|
|
280
309
|
await navigator.clipboard.writeText(pre.textContent ?? "");
|
|
281
310
|
});
|
|
282
311
|
const doneBtn = document.createElement("button");
|