@1shotapi/ows-signer 0.3.3 → 0.4.0
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 +16 -0
- package/html/index.html +1 -99
- package/html/signer.css +190 -0
- package/package.json +2 -1
- package/src/handlers.js +27 -10
- package/src/webauthn.js +30 -0
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Ceremony UI params (optional on WebAuthn-triggering methods): `explanationHeader
|
|
|
58
58
|
- `rpName` — relying party display name in passkey UI (default `"OWS"`); does not affect `rpId`
|
|
59
59
|
- `userDisplayName` — friendly account label (defaults to `name`)
|
|
60
60
|
- `userId` — stable user handle bytes (base64); random if omitted
|
|
61
|
+
- `deferKeyDerivation` — when true, skip the PRF follow-up get; `CredentialCreated` includes `credentialId` + `cosePublicKey` only (no `KeyDerived` / no `secp256k1PublicKey`)
|
|
61
62
|
|
|
62
63
|
## Signing schemes
|
|
63
64
|
|
|
@@ -87,6 +88,7 @@ Ceremony UI params (optional on WebAuthn-triggering methods): `explanationHeader
|
|
|
87
88
|
|
|
88
89
|
```
|
|
89
90
|
html/index.html Entry document
|
|
91
|
+
html/signer.css Ceremony / reveal UI styles (linked; no inline CSS)
|
|
90
92
|
src/main.js Bootstrap + message listener
|
|
91
93
|
src/handlers.js RPC dispatch
|
|
92
94
|
src/webauthn.js Passkey ceremonies
|
|
@@ -95,6 +97,20 @@ src/crypto/vendor/ Vendored @noble/secp256k1 + ed25519 (MIT)
|
|
|
95
97
|
reference/ 1ShotPay reference (not published)
|
|
96
98
|
```
|
|
97
99
|
|
|
100
|
+
## Content Security Policy
|
|
101
|
+
|
|
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
|
+
|
|
104
|
+
Canonical header (single line in production):
|
|
105
|
+
|
|
106
|
+
```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
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- 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`).
|
|
112
|
+
- Keep iframe Permissions Policy `allow` for WebAuthn / clipboard (set before `src` in `createSignerIframe`) — that is not CSP.
|
|
113
|
+
|
|
98
114
|
## On-chain
|
|
99
115
|
|
|
100
116
|
See `@1shotapi/ows-onchain`.
|
package/html/index.html
CHANGED
|
@@ -4,105 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>OWS Custody Signer</title>
|
|
7
|
-
<
|
|
8
|
-
:root {
|
|
9
|
-
color-scheme: light dark;
|
|
10
|
-
font-family: system-ui, sans-serif;
|
|
11
|
-
}
|
|
12
|
-
html,
|
|
13
|
-
body {
|
|
14
|
-
height: 100%;
|
|
15
|
-
margin: 0;
|
|
16
|
-
overflow: hidden;
|
|
17
|
-
}
|
|
18
|
-
body {
|
|
19
|
-
box-sizing: border-box;
|
|
20
|
-
}
|
|
21
|
-
.ows-signer-ui {
|
|
22
|
-
display: flex;
|
|
23
|
-
flex-direction: column;
|
|
24
|
-
gap: 8px;
|
|
25
|
-
box-sizing: border-box;
|
|
26
|
-
width: 100%;
|
|
27
|
-
height: 100%;
|
|
28
|
-
max-width: 100%;
|
|
29
|
-
padding: 12px;
|
|
30
|
-
overflow: hidden;
|
|
31
|
-
}
|
|
32
|
-
.ows-signer-ui--ceremony {
|
|
33
|
-
gap: 0;
|
|
34
|
-
}
|
|
35
|
-
.ows-key {
|
|
36
|
-
margin: 0;
|
|
37
|
-
padding: 8px;
|
|
38
|
-
border: 1px solid #8884;
|
|
39
|
-
border-radius: 4px;
|
|
40
|
-
font-family: ui-monospace, monospace;
|
|
41
|
-
font-size: 12px;
|
|
42
|
-
word-break: break-all;
|
|
43
|
-
white-space: pre-wrap;
|
|
44
|
-
overflow: auto;
|
|
45
|
-
flex: 1 1 auto;
|
|
46
|
-
min-height: 0;
|
|
47
|
-
}
|
|
48
|
-
.ows-password {
|
|
49
|
-
padding: 8px;
|
|
50
|
-
border: 1px solid #8884;
|
|
51
|
-
border-radius: 4px;
|
|
52
|
-
font: inherit;
|
|
53
|
-
}
|
|
54
|
-
.ows-ceremony-header {
|
|
55
|
-
flex: 0 0 auto;
|
|
56
|
-
margin: 0 0 8px;
|
|
57
|
-
font-size: 1.1rem;
|
|
58
|
-
font-weight: 700;
|
|
59
|
-
line-height: 1.3;
|
|
60
|
-
}
|
|
61
|
-
.ows-ceremony-body {
|
|
62
|
-
flex: 1 1 auto;
|
|
63
|
-
min-height: 0;
|
|
64
|
-
overflow-x: hidden;
|
|
65
|
-
overflow-y: auto;
|
|
66
|
-
margin: 0 0 12px;
|
|
67
|
-
}
|
|
68
|
-
.ows-ceremony-text {
|
|
69
|
-
margin: 0;
|
|
70
|
-
font-size: 0.95rem;
|
|
71
|
-
line-height: 1.4;
|
|
72
|
-
opacity: 0.9;
|
|
73
|
-
}
|
|
74
|
-
.ows-ceremony-actions {
|
|
75
|
-
flex: 0 0 auto;
|
|
76
|
-
display: flex;
|
|
77
|
-
flex-wrap: wrap;
|
|
78
|
-
gap: 8px;
|
|
79
|
-
}
|
|
80
|
-
.ows-actions {
|
|
81
|
-
flex: 0 0 auto;
|
|
82
|
-
display: flex;
|
|
83
|
-
flex-wrap: wrap;
|
|
84
|
-
gap: 8px;
|
|
85
|
-
}
|
|
86
|
-
.ows-copy,
|
|
87
|
-
.ows-confirm,
|
|
88
|
-
.ows-deny {
|
|
89
|
-
padding: 8px 12px;
|
|
90
|
-
border: 1px solid #8884;
|
|
91
|
-
border-radius: 4px;
|
|
92
|
-
background: transparent;
|
|
93
|
-
font: inherit;
|
|
94
|
-
cursor: pointer;
|
|
95
|
-
align-self: flex-start;
|
|
96
|
-
}
|
|
97
|
-
.ows-confirm {
|
|
98
|
-
font-weight: 600;
|
|
99
|
-
}
|
|
100
|
-
.ows-confirm:disabled,
|
|
101
|
-
.ows-deny:disabled {
|
|
102
|
-
opacity: 0.45;
|
|
103
|
-
cursor: default;
|
|
104
|
-
}
|
|
105
|
-
</style>
|
|
7
|
+
<link rel="stylesheet" href="./signer.css" />
|
|
106
8
|
</head>
|
|
107
9
|
<body>
|
|
108
10
|
<div id="ows-signer-root"></div>
|
package/html/signer.css
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
--ows-bg: #f3f4f6;
|
|
4
|
+
--ows-surface: #ffffff;
|
|
5
|
+
--ows-border: #e5e7eb;
|
|
6
|
+
--ows-text: #111827;
|
|
7
|
+
--ows-text-muted: #6b7280;
|
|
8
|
+
--ows-primary: #2a9aa8;
|
|
9
|
+
--ows-primary-hover: #238a97;
|
|
10
|
+
--ows-radius-card: 20px;
|
|
11
|
+
--ows-radius-control: 10px;
|
|
12
|
+
font-family:
|
|
13
|
+
system-ui,
|
|
14
|
+
-apple-system,
|
|
15
|
+
"Segoe UI",
|
|
16
|
+
Roboto,
|
|
17
|
+
Helvetica,
|
|
18
|
+
Arial,
|
|
19
|
+
sans-serif;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
html,
|
|
23
|
+
body {
|
|
24
|
+
height: 100%;
|
|
25
|
+
margin: 0;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
background: var(--ows-bg);
|
|
35
|
+
color: var(--ows-text);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ows-signer-ui {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
gap: 12px;
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
align-items: stretch;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
margin: auto;
|
|
46
|
+
width: calc(100% - 24px);
|
|
47
|
+
max-width: 360px;
|
|
48
|
+
height: auto;
|
|
49
|
+
max-height: calc(100% - 24px);
|
|
50
|
+
padding: 28px 24px;
|
|
51
|
+
overflow: auto;
|
|
52
|
+
background: var(--ows-surface);
|
|
53
|
+
border: 1px solid var(--ows-border);
|
|
54
|
+
border-radius: var(--ows-radius-card);
|
|
55
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
|
|
56
|
+
text-align: center;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ows-signer-ui--ceremony {
|
|
60
|
+
gap: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ows-signer-ui:empty {
|
|
64
|
+
display: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ows-key {
|
|
68
|
+
margin: 0;
|
|
69
|
+
padding: 12px;
|
|
70
|
+
border: 1px solid var(--ows-border);
|
|
71
|
+
border-radius: var(--ows-radius-control);
|
|
72
|
+
background: var(--ows-surface);
|
|
73
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
74
|
+
font-size: 12px;
|
|
75
|
+
color: var(--ows-text);
|
|
76
|
+
word-break: break-all;
|
|
77
|
+
white-space: pre-wrap;
|
|
78
|
+
overflow: auto;
|
|
79
|
+
flex: 1 1 auto;
|
|
80
|
+
min-height: 0;
|
|
81
|
+
text-align: left;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.ows-password {
|
|
85
|
+
box-sizing: border-box;
|
|
86
|
+
width: 100%;
|
|
87
|
+
padding: 12px 14px;
|
|
88
|
+
border: 1px solid var(--ows-border);
|
|
89
|
+
border-radius: var(--ows-radius-control);
|
|
90
|
+
background: var(--ows-surface);
|
|
91
|
+
color: var(--ows-text);
|
|
92
|
+
font: inherit;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.ows-password:focus {
|
|
96
|
+
outline: 2px solid color-mix(in srgb, var(--ows-primary) 45%, transparent);
|
|
97
|
+
outline-offset: 1px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.ows-ceremony-header {
|
|
101
|
+
flex: 0 0 auto;
|
|
102
|
+
margin: 0 0 8px;
|
|
103
|
+
font-size: 1.25rem;
|
|
104
|
+
font-weight: 700;
|
|
105
|
+
line-height: 1.3;
|
|
106
|
+
color: var(--ows-text);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.ows-ceremony-body {
|
|
110
|
+
flex: 0 1 auto;
|
|
111
|
+
min-height: 0;
|
|
112
|
+
overflow-x: hidden;
|
|
113
|
+
overflow-y: auto;
|
|
114
|
+
margin: 0 0 24px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.ows-ceremony-text {
|
|
118
|
+
margin: 0;
|
|
119
|
+
font-size: 1rem;
|
|
120
|
+
font-weight: 400;
|
|
121
|
+
line-height: 1.45;
|
|
122
|
+
color: var(--ows-text-muted);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.ows-ceremony-actions,
|
|
126
|
+
.ows-actions {
|
|
127
|
+
flex: 0 0 auto;
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-direction: column;
|
|
130
|
+
flex-wrap: nowrap;
|
|
131
|
+
gap: 12px;
|
|
132
|
+
width: 100%;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.ows-copy,
|
|
136
|
+
.ows-confirm,
|
|
137
|
+
.ows-deny {
|
|
138
|
+
box-sizing: border-box;
|
|
139
|
+
width: 100%;
|
|
140
|
+
min-height: 44px;
|
|
141
|
+
padding: 12px 16px;
|
|
142
|
+
border-radius: var(--ows-radius-control);
|
|
143
|
+
font: inherit;
|
|
144
|
+
font-size: 1rem;
|
|
145
|
+
font-weight: 600;
|
|
146
|
+
line-height: 1.2;
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
align-self: stretch;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.ows-confirm {
|
|
152
|
+
border: none;
|
|
153
|
+
background: var(--ows-primary);
|
|
154
|
+
color: #ffffff;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.ows-confirm:hover:not(:disabled) {
|
|
158
|
+
background: var(--ows-primary-hover);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.ows-confirm:focus-visible {
|
|
162
|
+
outline: 2px solid var(--ows-primary);
|
|
163
|
+
outline-offset: 2px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.ows-deny,
|
|
167
|
+
.ows-copy {
|
|
168
|
+
border: 1px solid var(--ows-border);
|
|
169
|
+
background: var(--ows-surface);
|
|
170
|
+
color: #4b5563;
|
|
171
|
+
font-weight: 500;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.ows-deny:hover:not(:disabled),
|
|
175
|
+
.ows-copy:hover:not(:disabled) {
|
|
176
|
+
background: #f9fafb;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.ows-deny:focus-visible,
|
|
180
|
+
.ows-copy:focus-visible {
|
|
181
|
+
outline: 2px solid color-mix(in srgb, var(--ows-primary) 50%, transparent);
|
|
182
|
+
outline-offset: 2px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.ows-confirm:disabled,
|
|
186
|
+
.ows-deny:disabled,
|
|
187
|
+
.ows-copy:disabled {
|
|
188
|
+
opacity: 0.45;
|
|
189
|
+
cursor: default;
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1shotapi/ows-signer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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": [
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"exports": {
|
|
12
12
|
"./html": "./html/index.html",
|
|
13
|
+
"./html/signer.css": "./html/signer.css",
|
|
13
14
|
"./src/*": "./src/*"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
package/src/handlers.js
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from "./ui.js";
|
|
42
42
|
import {
|
|
43
43
|
createPasskeyCredential,
|
|
44
|
-
|
|
44
|
+
getAssertionFieldsBase64Url,
|
|
45
45
|
getCredentialId,
|
|
46
46
|
getPasskeyAssertion,
|
|
47
47
|
getCosePublicKeyBase64Url,
|
|
@@ -249,16 +249,33 @@ async function handleCreateCredential(params, correlationId, targetOrigin) {
|
|
|
249
249
|
}
|
|
250
250
|
const options =
|
|
251
251
|
params.options && typeof params.options === "object"
|
|
252
|
-
? /** @type {{ rpName?: string, userDisplayName?: string, userId?: string, explanationHeader?: string, explanationText?: string, confirmButtonText?: string, denyButtonText?: string }} */ (
|
|
252
|
+
? /** @type {{ rpName?: string, userDisplayName?: string, userId?: string, deferKeyDerivation?: boolean, explanationHeader?: string, explanationText?: string, confirmButtonText?: string, denyButtonText?: string }} */ (
|
|
253
253
|
params.options
|
|
254
254
|
)
|
|
255
255
|
: {};
|
|
256
|
+
const deferKeyDerivation = options.deferKeyDerivation === true;
|
|
256
257
|
|
|
257
258
|
await withCeremony(async () => {
|
|
258
|
-
// Create + optional PRF follow-up get stay under one Confirm panel so the
|
|
259
|
-
// explanation remains visible for every WebAuthn prompt in this RPC.
|
|
260
259
|
/** @type {PublicKeyCredential | undefined} */
|
|
261
260
|
let registrationCredential;
|
|
261
|
+
|
|
262
|
+
if (deferKeyDerivation) {
|
|
263
|
+
// Registration only — opener (or another signer session) will unlock later.
|
|
264
|
+
await promptCeremonyConfirm(options, async () => {
|
|
265
|
+
registrationCredential = await createPasskeyCredential(name, options);
|
|
266
|
+
});
|
|
267
|
+
if (!registrationCredential) {
|
|
268
|
+
throw new Error("createCredential: registration credential missing");
|
|
269
|
+
}
|
|
270
|
+
emitEvent(window.parent, targetOrigin, "CredentialCreated", correlationId, {
|
|
271
|
+
credentialId: getCredentialId(registrationCredential),
|
|
272
|
+
cosePublicKey: getCosePublicKeyBase64Url(registrationCredential),
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Create + optional PRF follow-up get stay under one Confirm panel so the
|
|
278
|
+
// explanation remains visible for every WebAuthn prompt in this RPC.
|
|
262
279
|
const prfCredential = await promptCeremonyConfirm(options, async () => {
|
|
263
280
|
registrationCredential = await createPasskeyCredential(name, options);
|
|
264
281
|
const credentialId = getCredentialId(registrationCredential);
|
|
@@ -868,8 +885,8 @@ async function handleGetPublicKey(params, correlationId, targetOrigin) {
|
|
|
868
885
|
});
|
|
869
886
|
|
|
870
887
|
if (challenge) {
|
|
871
|
-
const
|
|
872
|
-
if (
|
|
888
|
+
const assertion = getAssertionFieldsBase64Url(credential);
|
|
889
|
+
if (assertion) {
|
|
873
890
|
emitEvent(
|
|
874
891
|
window.parent,
|
|
875
892
|
targetOrigin,
|
|
@@ -877,7 +894,7 @@ async function handleGetPublicKey(params, correlationId, targetOrigin) {
|
|
|
877
894
|
correlationId,
|
|
878
895
|
{
|
|
879
896
|
challenge: to0xHex(challenge),
|
|
880
|
-
|
|
897
|
+
assertion,
|
|
881
898
|
},
|
|
882
899
|
);
|
|
883
900
|
}
|
|
@@ -997,9 +1014,9 @@ async function handleExecuteBatch(params, correlationId, targetOrigin) {
|
|
|
997
1014
|
};
|
|
998
1015
|
}
|
|
999
1016
|
if (challenge && credential) {
|
|
1000
|
-
const
|
|
1001
|
-
if (
|
|
1002
|
-
data.
|
|
1017
|
+
const assertion = getAssertionFieldsBase64Url(credential);
|
|
1018
|
+
if (assertion) {
|
|
1019
|
+
data.assertion = assertion;
|
|
1003
1020
|
}
|
|
1004
1021
|
}
|
|
1005
1022
|
|
package/src/webauthn.js
CHANGED
|
@@ -245,3 +245,33 @@ export function getAssertionSignatureBase64Url(credential) {
|
|
|
245
245
|
}
|
|
246
246
|
return null;
|
|
247
247
|
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Full WebAuthn assertion fields for host/relayer auth (base64url).
|
|
251
|
+
* @param {PublicKeyCredential} credential
|
|
252
|
+
* @returns {{
|
|
253
|
+
* authenticatorData: string,
|
|
254
|
+
* clientDataJSON: string,
|
|
255
|
+
* signature: string,
|
|
256
|
+
* credentialId: string,
|
|
257
|
+
* } | null}
|
|
258
|
+
*/
|
|
259
|
+
export function getAssertionFieldsBase64Url(credential) {
|
|
260
|
+
const response = credential.response;
|
|
261
|
+
if (
|
|
262
|
+
!("authenticatorData" in response) ||
|
|
263
|
+
!response.authenticatorData ||
|
|
264
|
+
!("clientDataJSON" in response) ||
|
|
265
|
+
!response.clientDataJSON ||
|
|
266
|
+
!("signature" in response) ||
|
|
267
|
+
!response.signature
|
|
268
|
+
) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
authenticatorData: bufferToBase64Url(response.authenticatorData),
|
|
273
|
+
clientDataJSON: bufferToBase64Url(response.clientDataJSON),
|
|
274
|
+
signature: bufferToBase64Url(response.signature),
|
|
275
|
+
credentialId: getCredentialId(credential),
|
|
276
|
+
};
|
|
277
|
+
}
|