@visa/cli 2.6.1-rc.1 → 2.7.0-rc.3
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 +2 -2
- package/dist/cli.js +135 -134
- package/dist/mcp-server/index.js +22 -21
- package/native/build-win.bat +42 -0
- package/native/visa-keychain-win.cpp +481 -0
- package/package.json +4 -1
- package/server.json +2 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM build-win.bat — compile visa-keychain-win.exe
|
|
3
|
+
REM Requires Visual Studio Build Tools 2019+ (cl.exe in PATH)
|
|
4
|
+
REM Invoke from Developer Command Prompt or after running vcvarsall.bat
|
|
5
|
+
REM
|
|
6
|
+
REM Usage:
|
|
7
|
+
REM build-win.bat (builds to .\bin\win32-x64\)
|
|
8
|
+
REM build-win.bat --ci (also computes SHA-256 hash file)
|
|
9
|
+
|
|
10
|
+
setlocal enabledelayedexpansion
|
|
11
|
+
|
|
12
|
+
set "SRC=%~dp0visa-keychain-win.cpp"
|
|
13
|
+
set "OUT_DIR=%~dp0bin\win32-x64"
|
|
14
|
+
set "OUT=%OUT_DIR%\visa-keychain-win.exe"
|
|
15
|
+
|
|
16
|
+
if not exist "%OUT_DIR%" mkdir "%OUT_DIR%"
|
|
17
|
+
|
|
18
|
+
echo [visa-cli] Compiling visa-keychain-win.exe...
|
|
19
|
+
|
|
20
|
+
cl /nologo /EHsc /std:c++17 /O2 /DNDEBUG /W3 ^
|
|
21
|
+
"%SRC%" ^
|
|
22
|
+
/Fe:"%OUT%" ^
|
|
23
|
+
/link /SUBSYSTEM:CONSOLE bcrypt.lib ncrypt.lib ole32.lib
|
|
24
|
+
|
|
25
|
+
if %ERRORLEVEL% neq 0 (
|
|
26
|
+
echo ERROR: Compilation failed. Ensure Visual Studio Build Tools are installed.
|
|
27
|
+
echo Install from: https://visualstudio.microsoft.com/visual-cpp-build-tools/
|
|
28
|
+
exit /b 1
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
REM Clean up intermediate files
|
|
32
|
+
if exist "%~dp0visa-keychain-win.obj" del "%~dp0visa-keychain-win.obj"
|
|
33
|
+
|
|
34
|
+
echo [visa-cli] Built: %OUT%
|
|
35
|
+
|
|
36
|
+
REM Generate SHA-256 hash for integrity verification
|
|
37
|
+
if "%1"=="--ci" (
|
|
38
|
+
certutil -hashfile "%OUT%" SHA256 | findstr /v ":" > "%OUT%.sha256"
|
|
39
|
+
echo [visa-cli] Hash: %OUT%.sha256
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
echo [visa-cli] Done.
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* visa-keychain-win.cpp — Windows Hello attestation for Visa CLI.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the macOS visa-keychain.m CLI contract:
|
|
5
|
+
* generate-key → creates P-256 key in CNG store, returns SPKI DER public key
|
|
6
|
+
* public-key → returns SPKI DER public key for stored key
|
|
7
|
+
* sign <chal> → Windows Hello prompt, ECDSA-SHA256 signature
|
|
8
|
+
* delete-key → removes stored key
|
|
9
|
+
* check → verifies Windows Hello availability
|
|
10
|
+
*
|
|
11
|
+
* Build:
|
|
12
|
+
* cl /EHsc /std:c++17 /O2 visa-keychain-win.cpp
|
|
13
|
+
* /Fe:visa-keychain-win.exe /link bcrypt.lib ncrypt.lib ole32.lib
|
|
14
|
+
*
|
|
15
|
+
* Key storage: CNG persisted key (TPM-backed if available, software fallback).
|
|
16
|
+
* The private key never leaves the CNG store — signing happens in-place.
|
|
17
|
+
* Windows Hello provides user-presence verification (fingerprint/face/PIN).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
#ifndef UNICODE
|
|
21
|
+
#define UNICODE
|
|
22
|
+
#endif
|
|
23
|
+
#ifndef _UNICODE
|
|
24
|
+
#define _UNICODE
|
|
25
|
+
#endif
|
|
26
|
+
|
|
27
|
+
#include <windows.h>
|
|
28
|
+
#include <bcrypt.h>
|
|
29
|
+
#include <ncrypt.h>
|
|
30
|
+
#include <wincred.h>
|
|
31
|
+
#include <stdio.h>
|
|
32
|
+
#include <stdlib.h>
|
|
33
|
+
#include <string.h>
|
|
34
|
+
#include <string>
|
|
35
|
+
#include <vector>
|
|
36
|
+
|
|
37
|
+
#pragma comment(lib, "bcrypt.lib")
|
|
38
|
+
#pragma comment(lib, "ncrypt.lib")
|
|
39
|
+
#pragma comment(lib, "ole32.lib")
|
|
40
|
+
|
|
41
|
+
static const wchar_t* KEY_NAME = L"visa-cli-attestation";
|
|
42
|
+
|
|
43
|
+
// P-256 SPKI DER header (26 bytes) — wraps a 65-byte uncompressed EC point
|
|
44
|
+
static const unsigned char SPKI_HEADER[] = {
|
|
45
|
+
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86,
|
|
46
|
+
0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A,
|
|
47
|
+
0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03,
|
|
48
|
+
0x42, 0x00
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ── Base64 encode/decode ────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
static const char B64_TABLE[] =
|
|
54
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
55
|
+
|
|
56
|
+
std::string base64_encode(const unsigned char* data, size_t len) {
|
|
57
|
+
std::string out;
|
|
58
|
+
out.reserve(((len + 2) / 3) * 4);
|
|
59
|
+
for (size_t i = 0; i < len; i += 3) {
|
|
60
|
+
unsigned int n = ((unsigned int)data[i]) << 16;
|
|
61
|
+
if (i + 1 < len) n |= ((unsigned int)data[i + 1]) << 8;
|
|
62
|
+
if (i + 2 < len) n |= (unsigned int)data[i + 2];
|
|
63
|
+
out.push_back(B64_TABLE[(n >> 18) & 0x3F]);
|
|
64
|
+
out.push_back(B64_TABLE[(n >> 12) & 0x3F]);
|
|
65
|
+
out.push_back((i + 1 < len) ? B64_TABLE[(n >> 6) & 0x3F] : '=');
|
|
66
|
+
out.push_back((i + 2 < len) ? B64_TABLE[n & 0x3F] : '=');
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static int b64_decode_char(char c) {
|
|
72
|
+
if (c >= 'A' && c <= 'Z') return c - 'A';
|
|
73
|
+
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
|
74
|
+
if (c >= '0' && c <= '9') return c - '0' + 52;
|
|
75
|
+
if (c == '+') return 62;
|
|
76
|
+
if (c == '/') return 63;
|
|
77
|
+
return -1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
std::vector<unsigned char> base64_decode(const char* input) {
|
|
81
|
+
std::vector<unsigned char> out;
|
|
82
|
+
size_t len = strlen(input);
|
|
83
|
+
if (len == 0) return out;
|
|
84
|
+
out.reserve((len / 4) * 3);
|
|
85
|
+
for (size_t i = 0; i < len; i += 4) {
|
|
86
|
+
int a = b64_decode_char(input[i]);
|
|
87
|
+
int b = (i + 1 < len) ? b64_decode_char(input[i + 1]) : 0;
|
|
88
|
+
int c = (i + 2 < len) ? b64_decode_char(input[i + 2]) : 0;
|
|
89
|
+
int d = (i + 3 < len) ? b64_decode_char(input[i + 3]) : 0;
|
|
90
|
+
if (a < 0) a = 0;
|
|
91
|
+
if (b < 0) b = 0;
|
|
92
|
+
unsigned int triple = (a << 18) | (b << 12) | (c << 6) | d;
|
|
93
|
+
out.push_back((triple >> 16) & 0xFF);
|
|
94
|
+
if (i + 2 < len && input[i + 2] != '=')
|
|
95
|
+
out.push_back((triple >> 8) & 0xFF);
|
|
96
|
+
if (i + 3 < len && input[i + 3] != '=')
|
|
97
|
+
out.push_back(triple & 0xFF);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── CNG helpers ─────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Open the best available key storage provider.
|
|
106
|
+
* Tries TPM (MS_PLATFORM_CRYPTO_PROVIDER) first, falls back to software.
|
|
107
|
+
*/
|
|
108
|
+
static SECURITY_STATUS open_provider(NCRYPT_PROV_HANDLE* phProv) {
|
|
109
|
+
SECURITY_STATUS s = NCryptOpenStorageProvider(phProv,
|
|
110
|
+
MS_PLATFORM_CRYPTO_PROVIDER, 0);
|
|
111
|
+
if (s == ERROR_SUCCESS) return s;
|
|
112
|
+
return NCryptOpenStorageProvider(phProv, MS_KEY_STORAGE_PROVIDER, 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Export the public key portion of a CNG key handle as SPKI DER bytes.
|
|
117
|
+
* Returns empty vector on failure.
|
|
118
|
+
*/
|
|
119
|
+
static std::vector<unsigned char> export_spki(NCRYPT_KEY_HANDLE hKey) {
|
|
120
|
+
std::vector<unsigned char> result;
|
|
121
|
+
|
|
122
|
+
// Export as ECC public blob
|
|
123
|
+
DWORD cbBlob = 0;
|
|
124
|
+
SECURITY_STATUS s = NCryptExportKey(hKey, 0, BCRYPT_ECCPUBLIC_BLOB,
|
|
125
|
+
NULL, NULL, 0, &cbBlob, 0);
|
|
126
|
+
if (s != ERROR_SUCCESS || cbBlob == 0) return result;
|
|
127
|
+
|
|
128
|
+
std::vector<unsigned char> blob(cbBlob);
|
|
129
|
+
s = NCryptExportKey(hKey, 0, BCRYPT_ECCPUBLIC_BLOB,
|
|
130
|
+
NULL, blob.data(), cbBlob, &cbBlob, 0);
|
|
131
|
+
if (s != ERROR_SUCCESS) return result;
|
|
132
|
+
|
|
133
|
+
// BCRYPT_ECCPUBLIC_BLOB layout:
|
|
134
|
+
// BCRYPT_ECCKEY_BLOB header (8 bytes): magic(4) + cbKey(4)
|
|
135
|
+
// X coordinate (cbKey bytes)
|
|
136
|
+
// Y coordinate (cbKey bytes)
|
|
137
|
+
BCRYPT_ECCKEY_BLOB* pHeader = (BCRYPT_ECCKEY_BLOB*)blob.data();
|
|
138
|
+
DWORD cbKey = pHeader->cbKey; // 32 for P-256
|
|
139
|
+
|
|
140
|
+
if (cbKey != 32) return result; // Only P-256 supported
|
|
141
|
+
|
|
142
|
+
// Build uncompressed EC point: 0x04 || X || Y (65 bytes)
|
|
143
|
+
unsigned char* pX = blob.data() + sizeof(BCRYPT_ECCKEY_BLOB);
|
|
144
|
+
unsigned char* pY = pX + cbKey;
|
|
145
|
+
|
|
146
|
+
// SPKI DER = header (26 bytes) + uncompressed point (65 bytes) = 91 bytes
|
|
147
|
+
result.resize(sizeof(SPKI_HEADER) + 1 + 2 * cbKey);
|
|
148
|
+
memcpy(result.data(), SPKI_HEADER, sizeof(SPKI_HEADER));
|
|
149
|
+
result[sizeof(SPKI_HEADER)] = 0x04; // uncompressed point marker
|
|
150
|
+
memcpy(result.data() + sizeof(SPKI_HEADER) + 1, pX, cbKey);
|
|
151
|
+
memcpy(result.data() + sizeof(SPKI_HEADER) + 1 + cbKey, pY, cbKey);
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Convert raw r||s signature (64 bytes for P-256) to DER-encoded ECDSA-Sig-Value.
|
|
158
|
+
* Node.js crypto.createVerify expects DER format.
|
|
159
|
+
*/
|
|
160
|
+
static std::vector<unsigned char> raw_sig_to_der(const unsigned char* sig, DWORD sigLen) {
|
|
161
|
+
std::vector<unsigned char> der;
|
|
162
|
+
if (sigLen != 64) return der;
|
|
163
|
+
|
|
164
|
+
const unsigned char* r = sig;
|
|
165
|
+
const unsigned char* s = sig + 32;
|
|
166
|
+
|
|
167
|
+
// Each integer: skip leading zeros, add 0x00 padding if high bit set
|
|
168
|
+
auto encode_int = [](const unsigned char* val, int len) -> std::vector<unsigned char> {
|
|
169
|
+
std::vector<unsigned char> out;
|
|
170
|
+
int start = 0;
|
|
171
|
+
while (start < len - 1 && val[start] == 0) start++;
|
|
172
|
+
bool pad = (val[start] & 0x80) != 0;
|
|
173
|
+
out.push_back(0x02); // INTEGER tag
|
|
174
|
+
out.push_back((unsigned char)(len - start + (pad ? 1 : 0)));
|
|
175
|
+
if (pad) out.push_back(0x00);
|
|
176
|
+
for (int i = start; i < len; i++) out.push_back(val[i]);
|
|
177
|
+
return out;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
auto r_der = encode_int(r, 32);
|
|
181
|
+
auto s_der = encode_int(s, 32);
|
|
182
|
+
|
|
183
|
+
// SEQUENCE tag + length + r + s
|
|
184
|
+
der.push_back(0x30);
|
|
185
|
+
der.push_back((unsigned char)(r_der.size() + s_der.size()));
|
|
186
|
+
der.insert(der.end(), r_der.begin(), r_der.end());
|
|
187
|
+
der.insert(der.end(), s_der.begin(), s_der.end());
|
|
188
|
+
|
|
189
|
+
return der;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Commands ────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* check — verify Windows Hello / CNG availability.
|
|
196
|
+
* Outputs "OK" if key storage is available, "ERROR:<msg>" otherwise.
|
|
197
|
+
*/
|
|
198
|
+
static int cmd_check() {
|
|
199
|
+
NCRYPT_PROV_HANDLE hProv = 0;
|
|
200
|
+
SECURITY_STATUS s = open_provider(&hProv);
|
|
201
|
+
if (s != ERROR_SUCCESS) {
|
|
202
|
+
printf("ERROR:Key storage provider unavailable (0x%08lx)\n", s);
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
NCryptFreeObject(hProv);
|
|
206
|
+
printf("OK\n");
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* generate-key — create P-256 key in CNG store.
|
|
212
|
+
* Output: "OK:<public-spki-b64>"
|
|
213
|
+
* The key is persisted under KEY_NAME with UI protection (Windows Hello consent).
|
|
214
|
+
*/
|
|
215
|
+
static int cmd_generate_key() {
|
|
216
|
+
NCRYPT_PROV_HANDLE hProv = 0;
|
|
217
|
+
NCRYPT_KEY_HANDLE hKey = 0;
|
|
218
|
+
SECURITY_STATUS s;
|
|
219
|
+
|
|
220
|
+
s = open_provider(&hProv);
|
|
221
|
+
if (s != ERROR_SUCCESS) {
|
|
222
|
+
printf("ERROR:Cannot open key storage provider (0x%08lx)\n", s);
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Delete existing key if present (fresh start, like macOS delete-then-add)
|
|
227
|
+
NCRYPT_KEY_HANDLE hOld = 0;
|
|
228
|
+
if (NCryptOpenKey(hProv, &hOld, KEY_NAME, 0, 0) == ERROR_SUCCESS) {
|
|
229
|
+
NCryptDeleteKey(hOld, 0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Create P-256 key with user-consent requirement
|
|
233
|
+
s = NCryptCreatePersistedKey(hProv, &hKey,
|
|
234
|
+
BCRYPT_ECDSA_P256_ALGORITHM, KEY_NAME, 0,
|
|
235
|
+
NCRYPT_OVERWRITE_KEY_FLAG);
|
|
236
|
+
if (s != ERROR_SUCCESS) {
|
|
237
|
+
printf("ERROR:Key creation failed (0x%08lx)\n", s);
|
|
238
|
+
NCryptFreeObject(hProv);
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Require Windows Hello consent for sign operations
|
|
243
|
+
NCRYPT_UI_POLICY uiPolicy = {};
|
|
244
|
+
uiPolicy.dwVersion = 1;
|
|
245
|
+
uiPolicy.dwFlags = NCRYPT_UI_PROTECT_KEY_FLAG;
|
|
246
|
+
uiPolicy.pszCreationTitle = L"Visa CLI";
|
|
247
|
+
uiPolicy.pszDescription = L"Biometric key for payment attestation";
|
|
248
|
+
s = NCryptSetProperty(hKey, NCRYPT_UI_POLICY_PROPERTY,
|
|
249
|
+
(PBYTE)&uiPolicy, sizeof(uiPolicy), 0);
|
|
250
|
+
if (s != ERROR_SUCCESS) {
|
|
251
|
+
// Non-fatal: key works without UI protection (CI/headless scenarios)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Finalize (persist to store)
|
|
255
|
+
s = NCryptFinalizeKey(hKey, 0);
|
|
256
|
+
if (s != ERROR_SUCCESS) {
|
|
257
|
+
printf("ERROR:Key finalization failed (0x%08lx)\n", s);
|
|
258
|
+
NCryptFreeObject(hKey);
|
|
259
|
+
NCryptFreeObject(hProv);
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Export public key as SPKI DER
|
|
264
|
+
auto spki = export_spki(hKey);
|
|
265
|
+
if (spki.empty()) {
|
|
266
|
+
printf("ERROR:Failed to export public key\n");
|
|
267
|
+
NCryptFreeObject(hKey);
|
|
268
|
+
NCryptFreeObject(hProv);
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
std::string pub_b64 = base64_encode(spki.data(), spki.size());
|
|
273
|
+
printf("OK:%s\n", pub_b64.c_str());
|
|
274
|
+
|
|
275
|
+
NCryptFreeObject(hKey);
|
|
276
|
+
NCryptFreeObject(hProv);
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* public-key — export SPKI DER public key for the stored attestation key.
|
|
282
|
+
* Does NOT require Windows Hello consent (read-only metadata export).
|
|
283
|
+
* Output: "OK:<public-spki-b64>"
|
|
284
|
+
*/
|
|
285
|
+
static int cmd_public_key() {
|
|
286
|
+
NCRYPT_PROV_HANDLE hProv = 0;
|
|
287
|
+
NCRYPT_KEY_HANDLE hKey = 0;
|
|
288
|
+
SECURITY_STATUS s;
|
|
289
|
+
|
|
290
|
+
s = open_provider(&hProv);
|
|
291
|
+
if (s != ERROR_SUCCESS) {
|
|
292
|
+
printf("ERROR:Cannot open key storage provider (0x%08lx)\n", s);
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
s = NCryptOpenKey(hProv, &hKey, KEY_NAME, 0, 0);
|
|
297
|
+
if (s != ERROR_SUCCESS) {
|
|
298
|
+
printf("ERROR:Attestation key not found. Run setup to generate a new key.\n");
|
|
299
|
+
NCryptFreeObject(hProv);
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
auto spki = export_spki(hKey);
|
|
304
|
+
if (spki.empty()) {
|
|
305
|
+
printf("ERROR:Failed to export public key\n");
|
|
306
|
+
NCryptFreeObject(hKey);
|
|
307
|
+
NCryptFreeObject(hProv);
|
|
308
|
+
return 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
std::string pub_b64 = base64_encode(spki.data(), spki.size());
|
|
312
|
+
printf("OK:%s\n", pub_b64.c_str());
|
|
313
|
+
|
|
314
|
+
NCryptFreeObject(hKey);
|
|
315
|
+
NCryptFreeObject(hProv);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* sign <challenge> [reason] — hash + sign with Windows Hello consent.
|
|
321
|
+
* The challenge string is SHA-256 hashed, then signed with ECDSA P-256.
|
|
322
|
+
* Windows Hello UI blocks until user confirms (fingerprint/face/PIN).
|
|
323
|
+
* Output: "OK:<der-signature-b64>"
|
|
324
|
+
*/
|
|
325
|
+
static int cmd_sign(const char* challenge, const char* reason) {
|
|
326
|
+
NCRYPT_PROV_HANDLE hProv = 0;
|
|
327
|
+
NCRYPT_KEY_HANDLE hKey = 0;
|
|
328
|
+
SECURITY_STATUS s;
|
|
329
|
+
|
|
330
|
+
s = open_provider(&hProv);
|
|
331
|
+
if (s != ERROR_SUCCESS) {
|
|
332
|
+
printf("ERROR:Cannot open key storage provider (0x%08lx)\n", s);
|
|
333
|
+
return 1;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
s = NCryptOpenKey(hProv, &hKey, KEY_NAME, 0, 0);
|
|
337
|
+
if (s != ERROR_SUCCESS) {
|
|
338
|
+
printf("ERROR:Attestation key not found. Run setup to generate a new key.\n");
|
|
339
|
+
NCryptFreeObject(hProv);
|
|
340
|
+
return 1;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Hash the challenge with SHA-256
|
|
344
|
+
BCRYPT_ALG_HANDLE hAlg = 0;
|
|
345
|
+
NTSTATUS ns = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM,
|
|
346
|
+
NULL, 0);
|
|
347
|
+
if (ns != 0) {
|
|
348
|
+
printf("ERROR:Cannot open SHA-256 provider\n");
|
|
349
|
+
NCryptFreeObject(hKey);
|
|
350
|
+
NCryptFreeObject(hProv);
|
|
351
|
+
return 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
unsigned char hash[32];
|
|
355
|
+
ns = BCryptHash(hAlg, NULL, 0,
|
|
356
|
+
(PUCHAR)challenge, (ULONG)strlen(challenge),
|
|
357
|
+
hash, sizeof(hash));
|
|
358
|
+
BCryptCloseAlgorithmProvider(hAlg, 0);
|
|
359
|
+
|
|
360
|
+
if (ns != 0) {
|
|
361
|
+
printf("ERROR:SHA-256 hash failed\n");
|
|
362
|
+
NCryptFreeObject(hKey);
|
|
363
|
+
NCryptFreeObject(hProv);
|
|
364
|
+
return 1;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Sign the hash — this triggers the Windows Hello consent UI.
|
|
368
|
+
// NCryptSignHash with a UI-protected key will show the biometric prompt.
|
|
369
|
+
DWORD cbSig = 0;
|
|
370
|
+
s = NCryptSignHash(hKey, NULL, hash, sizeof(hash),
|
|
371
|
+
NULL, 0, &cbSig, 0);
|
|
372
|
+
if (s != ERROR_SUCCESS || cbSig == 0) {
|
|
373
|
+
printf("ERROR:Cannot determine signature size (0x%08lx)\n", s);
|
|
374
|
+
NCryptFreeObject(hKey);
|
|
375
|
+
NCryptFreeObject(hProv);
|
|
376
|
+
return 1;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
std::vector<unsigned char> sig(cbSig);
|
|
380
|
+
s = NCryptSignHash(hKey, NULL, hash, sizeof(hash),
|
|
381
|
+
sig.data(), cbSig, &cbSig, 0);
|
|
382
|
+
|
|
383
|
+
if (s == (SECURITY_STATUS)NTE_USER_CANCELLED) {
|
|
384
|
+
printf("ERROR:Authentication cancelled by user\n");
|
|
385
|
+
NCryptFreeObject(hKey);
|
|
386
|
+
NCryptFreeObject(hProv);
|
|
387
|
+
return 1;
|
|
388
|
+
}
|
|
389
|
+
if (s != ERROR_SUCCESS) {
|
|
390
|
+
printf("ERROR:Signing failed (0x%08lx)\n", s);
|
|
391
|
+
NCryptFreeObject(hKey);
|
|
392
|
+
NCryptFreeObject(hProv);
|
|
393
|
+
return 1;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// CNG outputs raw r||s (64 bytes for P-256). Convert to DER for Node.js.
|
|
397
|
+
auto der_sig = raw_sig_to_der(sig.data(), cbSig);
|
|
398
|
+
if (der_sig.empty()) {
|
|
399
|
+
printf("ERROR:Signature DER encoding failed\n");
|
|
400
|
+
NCryptFreeObject(hKey);
|
|
401
|
+
NCryptFreeObject(hProv);
|
|
402
|
+
return 1;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
std::string sig_b64 = base64_encode(der_sig.data(), der_sig.size());
|
|
406
|
+
printf("OK:%s\n", sig_b64.c_str());
|
|
407
|
+
|
|
408
|
+
NCryptFreeObject(hKey);
|
|
409
|
+
NCryptFreeObject(hProv);
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* delete-key — remove the stored attestation key from CNG.
|
|
415
|
+
* Output: "OK" on success, "ERROR:<msg>" on failure.
|
|
416
|
+
*/
|
|
417
|
+
static int cmd_delete_key() {
|
|
418
|
+
NCRYPT_PROV_HANDLE hProv = 0;
|
|
419
|
+
NCRYPT_KEY_HANDLE hKey = 0;
|
|
420
|
+
SECURITY_STATUS s;
|
|
421
|
+
|
|
422
|
+
s = open_provider(&hProv);
|
|
423
|
+
if (s != ERROR_SUCCESS) {
|
|
424
|
+
printf("OK\n"); // No provider = no key = success
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
s = NCryptOpenKey(hProv, &hKey, KEY_NAME, 0, 0);
|
|
429
|
+
if (s != ERROR_SUCCESS) {
|
|
430
|
+
NCryptFreeObject(hProv);
|
|
431
|
+
printf("OK\n"); // Key doesn't exist = success
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
s = NCryptDeleteKey(hKey, 0);
|
|
436
|
+
NCryptFreeObject(hProv);
|
|
437
|
+
|
|
438
|
+
if (s != ERROR_SUCCESS) {
|
|
439
|
+
printf("ERROR:Failed to delete key (0x%08lx)\n", s);
|
|
440
|
+
return 1;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
printf("OK\n");
|
|
444
|
+
return 0;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
int main(int argc, char* argv[]) {
|
|
450
|
+
if (argc < 2) {
|
|
451
|
+
fprintf(stderr, "Usage: visa-keychain-win <command> [args...]\n");
|
|
452
|
+
fprintf(stderr, "Commands: check, generate-key, public-key, sign, delete-key\n");
|
|
453
|
+
return 1;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const char* cmd = argv[1];
|
|
457
|
+
|
|
458
|
+
if (strcmp(cmd, "check") == 0) {
|
|
459
|
+
return cmd_check();
|
|
460
|
+
}
|
|
461
|
+
if (strcmp(cmd, "generate-key") == 0) {
|
|
462
|
+
return cmd_generate_key();
|
|
463
|
+
}
|
|
464
|
+
if (strcmp(cmd, "public-key") == 0) {
|
|
465
|
+
return cmd_public_key();
|
|
466
|
+
}
|
|
467
|
+
if (strcmp(cmd, "sign") == 0) {
|
|
468
|
+
if (argc < 3) {
|
|
469
|
+
printf("ERROR:Usage: sign <challenge> [reason]\n");
|
|
470
|
+
return 1;
|
|
471
|
+
}
|
|
472
|
+
const char* reason = (argc >= 4) ? argv[3] : NULL;
|
|
473
|
+
return cmd_sign(argv[2], reason);
|
|
474
|
+
}
|
|
475
|
+
if (strcmp(cmd, "delete-key") == 0) {
|
|
476
|
+
return cmd_delete_key();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
fprintf(stderr, "ERROR:Unknown command: %s\n", cmd);
|
|
480
|
+
return 1;
|
|
481
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visa/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0-rc.3",
|
|
4
4
|
"description": "AI-powered payments for Claude Code",
|
|
5
5
|
"bin": {
|
|
6
6
|
"visa-cli": "./bin/visa-cli.js"
|
|
@@ -75,6 +75,9 @@
|
|
|
75
75
|
"install.ps1",
|
|
76
76
|
"install.sh",
|
|
77
77
|
"native/visa-keychain.m",
|
|
78
|
+
"native/visa-keychain-win.cpp",
|
|
79
|
+
"native/build-win.bat",
|
|
80
|
+
"native/bin/win32-x64/visa-keychain-win.exe",
|
|
78
81
|
"server.json",
|
|
79
82
|
"README.md",
|
|
80
83
|
"LICENSE"
|
package/server.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
|
|
3
3
|
"name": "io.github.visa-crypto-labs/visa-cli",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.7.0-rc.3",
|
|
5
5
|
"title": "Visa CLI",
|
|
6
6
|
"description": "AI-powered payments and creative tools for coding agents. Generate images, music, video, query crypto prices, and make purchases — all from your AI coding assistant.",
|
|
7
7
|
"websiteUrl": "https://github.com/Visa-Crypto-Labs/Visa-mono/tree/main/packages/cli#readme",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"registryType": "npm",
|
|
11
11
|
"identifier": "@visa/cli",
|
|
12
|
-
"version": "2.
|
|
12
|
+
"version": "2.7.0-rc.3",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
},
|