@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 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
- - **Omit `frame-ancestors`** so arbitrary Branding Layers may embed the signer. Nesting safety remains in JS (`event.source === window.parent`, `window.parent !== window.top`).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1shotapi/ows-signer",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "OWS Signing Layer — plain JavaScript WebAuthn PRF key custody with curve-based signing (on-chain via EIP-8244)",
5
5
  "type": "module",
6
6
  "files": [
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 (!isValidParentMessage(event)) return;
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");