tisura-light 0.0.1-91
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 +176 -0
- package/dist/certify/embedCertificateEml.d.ts +21 -0
- package/dist/certify/embedCertificateEml.js +106 -0
- package/dist/certify/embedCertificatePdf.d.ts +23 -0
- package/dist/certify/embedCertificatePdf.js +98 -0
- package/dist/certify/embedQrCode.d.ts +27 -0
- package/dist/certify/embedQrCode.js +78 -0
- package/dist/certify/index.d.ts +73 -0
- package/dist/certify/index.js +478 -0
- package/dist/certify/parseEml.d.ts +27 -0
- package/dist/certify/parseEml.js +37 -0
- package/dist/certify/pdfExtractor.d.ts +3 -0
- package/dist/certify/pdfExtractor.js +76 -0
- package/dist/certify/providers/bourso.d.ts +2 -0
- package/dist/certify/providers/bourso.js +83 -0
- package/dist/certify/providers/dgfip.d.ts +2 -0
- package/dist/certify/providers/dgfip.js +45 -0
- package/dist/certify/providers/free.d.ts +2 -0
- package/dist/certify/providers/free.js +44 -0
- package/dist/certify/providers/index.d.ts +11 -0
- package/dist/certify/providers/index.js +42 -0
- package/dist/certify/providers/orange.d.ts +2 -0
- package/dist/certify/providers/orange.js +34 -0
- package/dist/certify/providers/types.d.ts +12 -0
- package/dist/certify/providers/types.js +1 -0
- package/dist/certify/redactPdf.d.ts +12 -0
- package/dist/certify/redactPdf.js +47 -0
- package/dist/certify/utils.d.ts +5 -0
- package/dist/certify/utils.js +83 -0
- package/dist/certify/zip.d.ts +42 -0
- package/dist/certify/zip.js +217 -0
- package/dist/client-light.d.ts +76 -0
- package/dist/client-light.js +113 -0
- package/dist/client.d.ts +73 -0
- package/dist/client.js +122 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +39 -0
- package/dist/connect/connect.d.ts +55 -0
- package/dist/connect/connect.js +133 -0
- package/dist/connect/index.d.ts +67 -0
- package/dist/connect/index.js +221 -0
- package/dist/connect/internal/connectionContext.d.ts +48 -0
- package/dist/connect/internal/connectionContext.js +43 -0
- package/dist/connect/internal/fileTypes.d.ts +3 -0
- package/dist/connect/internal/fileTypes.js +28 -0
- package/dist/connect/internal/httpRequest.d.ts +57 -0
- package/dist/connect/internal/httpRequest.js +612 -0
- package/dist/connect/internal/httpStatus.d.ts +6 -0
- package/dist/connect/internal/httpStatus.js +29 -0
- package/dist/connect/internal/tlsSession.d.ts +6 -0
- package/dist/connect/internal/tlsSession.js +182 -0
- package/dist/connect/internal/tokenize.d.ts +6 -0
- package/dist/connect/internal/tokenize.js +31 -0
- package/dist/connect/internal/tunnel.d.ts +15 -0
- package/dist/connect/internal/tunnel.js +154 -0
- package/dist/connect/tls/index.d.ts +8 -0
- package/dist/connect/tls/index.js +14 -0
- package/dist/connect/tls/wasm/tls.d.ts +123 -0
- package/dist/connect/tls/wasm/tls.js +692 -0
- package/dist/connect/tls/wasm/tls_bg.wasm +0 -0
- package/dist/connect/tls/wasm/tls_bg.wasm.d.ts +29 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +14 -0
- package/dist/index.light.d.ts +9 -0
- package/dist/index.light.js +5 -0
- package/dist/inputs.d.ts +80 -0
- package/dist/inputs.js +234 -0
- package/dist/parse/fetcher.d.ts +7 -0
- package/dist/parse/fetcher.js +59 -0
- package/dist/parse/index.d.ts +12 -0
- package/dist/parse/index.js +27 -0
- package/dist/parse/parser.d.ts +14 -0
- package/dist/parse/parser.js +142 -0
- package/dist/parse/types.d.ts +31 -0
- package/dist/parse/types.js +1 -0
- package/dist/prove/circuits/airdrop/contributions/target/Contributions.json +1 -0
- package/dist/prove/circuits/airdrop/ownership/target/Ownership.json +1 -0
- package/dist/prove/circuits/providers/bin/target/providers.json +1 -0
- package/dist/prove/circuits/substring/bin/target/chacha20_decrypt.json +1 -0
- package/dist/prove/index.d.ts +20 -0
- package/dist/prove/index.js +179 -0
- package/dist/tls13.d.ts +32 -0
- package/dist/tls13.js +152 -0
- package/dist/tls_bg.wasm +0 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +147 -0
- package/dist/verify/index.d.ts +22 -0
- package/dist/verify/index.js +31 -0
- package/dist/verify/twoddoc/certificates.d.ts +13 -0
- package/dist/verify/twoddoc/certificates.js +21 -0
- package/dist/verify/twoddoc/pdfBarcode.d.ts +1 -0
- package/dist/verify/twoddoc/pdfBarcode.js +43 -0
- package/dist/verify/twoddoc/twoDDoc.d.ts +23 -0
- package/dist/verify/twoddoc/twoDDoc.js +142 -0
- package/dist/verify/twoddoc/twoDDocTags.d.ts +3 -0
- package/dist/verify/twoddoc/twoDDocTags.js +72 -0
- package/dist/verify/twoddoc/twoDDocVerify.d.ts +8 -0
- package/dist/verify/twoddoc/twoDDocVerify.js +95 -0
- package/package.json +99 -0
- package/public/.gitkeep +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Sentinel SDK
|
|
2
|
+
|
|
3
|
+
This project contains the code for [tisura](https://www.npmjs.com/package/tisura) npm package.
|
|
4
|
+
It provides modules for parsing network packets, proving statements, managing connections, and handling TLS operations.
|
|
5
|
+
|
|
6
|
+
## Build
|
|
7
|
+
|
|
8
|
+
### Build Wasm files
|
|
9
|
+
|
|
10
|
+
Make sure to use `rustc` version of `1.82.0`.
|
|
11
|
+
|
|
12
|
+
To install it, you can run:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# first install rustup
|
|
16
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
17
|
+
# then install rust
|
|
18
|
+
rustup install 1.82.0
|
|
19
|
+
rustup default 1.82.0
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Build Noir files
|
|
23
|
+
|
|
24
|
+
Make sure to install `noir`.
|
|
25
|
+
|
|
26
|
+
To install it, you can run:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# first install noirup
|
|
30
|
+
curl -L https://raw.githubusercontent.com/noir-lang/noirup/refs/heads/main/install | bash
|
|
31
|
+
# then install noir
|
|
32
|
+
noirup --version 1.0.0-beta.6
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Local SDK Development
|
|
36
|
+
|
|
37
|
+
The `build-and-install.sh` script builds the SDK as a `.tgz` (exactly as it would be published to npm) and installs it into a sibling consumer project.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
./build-and-install.sh <mode> <dest>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- **`<mode>`** (optional, default `full`): `full` builds with WASM + TS, `light` produces a slim ESM bundle (no TLS/circuit code, just stamping + provider requests).
|
|
44
|
+
- **`<dest>`** (optional, default `twallet`): one of `extension`, `platform`, `airdrop`, `twallet`, `explorer`. `platform` and `airdrop` are installed via `yarn`, the others via `npm`.
|
|
45
|
+
|
|
46
|
+
The script removes any existing `tisura*.tgz` and `node_modules/tisura` in the destination, clears the package manager's cache for tisura, and force-installs the new tarball — so re-running with the same version reliably picks up the new SDK code.
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
./build-and-install.sh # full build → twallet (npm)
|
|
52
|
+
./build-and-install.sh light extension # light build → sentinel_extension (npm)
|
|
53
|
+
./build-and-install.sh full platform # full build → sentinel/platform (yarn)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install tisura
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
### Basic Setup
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { Tisura } from "tisura";
|
|
68
|
+
|
|
69
|
+
const client = new Tisura({
|
|
70
|
+
apiKey: "your-api-key", // Format: sk_dev_xxx key, sk_prod_xxx key, or simply sk_local for local development
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Sending a request to the server
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const connectClient = client.connect;
|
|
78
|
+
|
|
79
|
+
const conn = await connectClient.sendRequest({
|
|
80
|
+
serverHost, // e.g. "api.github.com"
|
|
81
|
+
serverPort, // e.g. 443
|
|
82
|
+
endpoint, // e.g. "/users"
|
|
83
|
+
params, // e.g. [{ key: "since", value: "1" }]
|
|
84
|
+
method, // e.g. "POST"
|
|
85
|
+
body , // eg. [{ key: "name", value: "sherlock" }] - Optional
|
|
86
|
+
headers, // e.g. { key: "Authorization", value: `Bearer ${token}` } - Optional
|
|
87
|
+
}: SendRequestArgs);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Generating a proof and verifying it
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const proveClient = client.prove;
|
|
94
|
+
|
|
95
|
+
const proof = await proveClient.generateProof({
|
|
96
|
+
circuitName, // e.g. "ownership"
|
|
97
|
+
connectionId, // optional, defaults to the last connection
|
|
98
|
+
plaintext, // the data you want to prove in "substring" circuit - Optional
|
|
99
|
+
nodeId, // the github node ID to prove in "ownership" circuit - Optional
|
|
100
|
+
repoName, // the github repo name to prove in "contributions" circuit - Optional
|
|
101
|
+
repoOwner, // the github repo owner to prove in "contributions" circuit - Optional
|
|
102
|
+
count, // the number of contributions to prove in "contributions" circuit - Optional
|
|
103
|
+
}: GenerateProofArgs);
|
|
104
|
+
|
|
105
|
+
const isValid = await proveClient.verifyProof({
|
|
106
|
+
circuitName, // e.g. "ownership"
|
|
107
|
+
proof, // the proof above
|
|
108
|
+
}: VerifyProofArgs);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Certifying a statement
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const certifyClient = client.certify;
|
|
115
|
+
|
|
116
|
+
const cert = await certifyClient.certify({
|
|
117
|
+
inputBytes, // the data you want to certify (in bytes)
|
|
118
|
+
connectionId, // optional, defaults to the last connection
|
|
119
|
+
inputSessionKey, // optional, defaults to the last session key
|
|
120
|
+
}: CertifyArgs)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Decrypting an encrypted statement
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const certifyClient = client.certify;
|
|
127
|
+
const decrypted = await certifyClient.decrypt({
|
|
128
|
+
connectionId, // optional, defaults to the last connection
|
|
129
|
+
}: DecryptArgs);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Available Modules
|
|
133
|
+
|
|
134
|
+
The SDK consists of several modules:
|
|
135
|
+
|
|
136
|
+
- **Parse**: Network packet parsing and analysis
|
|
137
|
+
- **Prove**: Statement proving and verification
|
|
138
|
+
- **Certify**: Statement certifying from client side
|
|
139
|
+
- **Connect**: Connection management
|
|
140
|
+
|
|
141
|
+
### Module-specific Imports
|
|
142
|
+
|
|
143
|
+
You can import specific modules directly:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { ParseClient } from "tisura/parse";
|
|
147
|
+
import { ProveClient } from "tisura/prove";
|
|
148
|
+
import { CertifyClient } from "tisura/certify";
|
|
149
|
+
import { ConnectClient } from "tisura/connect";
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Configuration
|
|
153
|
+
|
|
154
|
+
The SDK requires the following configuration parameters:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
interface TisuraConfig {
|
|
158
|
+
proxyUrl: string;
|
|
159
|
+
storeUrl: string;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
### Install, build and run tests
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Install dependencies
|
|
169
|
+
npm install
|
|
170
|
+
|
|
171
|
+
# Build the package
|
|
172
|
+
npm run build
|
|
173
|
+
|
|
174
|
+
# Run tests
|
|
175
|
+
npm test
|
|
176
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SessionKey } from "../inputs.js";
|
|
2
|
+
/**
|
|
3
|
+
* Embed certificate.json as a folded `X-Tisura-Certificate` header (RFC 5322).
|
|
4
|
+
*
|
|
5
|
+
* The cert JSON is base64-encoded and inserted right before the header/body
|
|
6
|
+
* separator. Body bytes are preserved byte-identical, and the original `.eml`
|
|
7
|
+
* is recoverable by stripping the inserted header.
|
|
8
|
+
*/
|
|
9
|
+
export declare function embedCertificateInEml(originalEmlBytes: Uint8Array, certificate: SessionKey): {
|
|
10
|
+
stampedEmlBytes: Uint8Array;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Extract the embedded certificate from a stamped `.eml` and return the
|
|
14
|
+
* recovered original bytes (with the `X-Tisura-Certificate` header stripped).
|
|
15
|
+
* Returns null if no embedded certificate is found.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractCertificateFromEml(emlBytes: Uint8Array): {
|
|
18
|
+
certObj: unknown;
|
|
19
|
+
originalEmlBytes: Uint8Array;
|
|
20
|
+
} | null;
|
|
21
|
+
export declare function isEmlWithCertificate(emlBytes: Uint8Array): boolean;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const HEADER_NAME = "X-Tisura-Certificate";
|
|
2
|
+
const FOLD_CHUNK = 76; // RFC 5322 recommended max line length for header folding
|
|
3
|
+
/**
|
|
4
|
+
* Embed certificate.json as a folded `X-Tisura-Certificate` header (RFC 5322).
|
|
5
|
+
*
|
|
6
|
+
* The cert JSON is base64-encoded and inserted right before the header/body
|
|
7
|
+
* separator. Body bytes are preserved byte-identical, and the original `.eml`
|
|
8
|
+
* is recoverable by stripping the inserted header.
|
|
9
|
+
*/
|
|
10
|
+
export function embedCertificateInEml(originalEmlBytes, certificate) {
|
|
11
|
+
const { sepIdx, eol } = locateHeaderBodySeparator(originalEmlBytes);
|
|
12
|
+
const certBytes = new TextEncoder().encode(JSON.stringify(certificate));
|
|
13
|
+
let bin = "";
|
|
14
|
+
for (const b of certBytes)
|
|
15
|
+
bin += String.fromCharCode(b);
|
|
16
|
+
const certB64 = btoa(bin);
|
|
17
|
+
const chunks = [];
|
|
18
|
+
for (let i = 0; i < certB64.length; i += FOLD_CHUNK) {
|
|
19
|
+
chunks.push(certB64.slice(i, i + FOLD_CHUNK));
|
|
20
|
+
}
|
|
21
|
+
const headerLine = `${HEADER_NAME}: ${chunks[0]}` +
|
|
22
|
+
chunks
|
|
23
|
+
.slice(1)
|
|
24
|
+
.map((c) => `${eol} ${c}`)
|
|
25
|
+
.join("");
|
|
26
|
+
const insertion = new TextEncoder().encode(eol + headerLine);
|
|
27
|
+
const stampedEmlBytes = new Uint8Array(originalEmlBytes.length + insertion.length);
|
|
28
|
+
stampedEmlBytes.set(originalEmlBytes.subarray(0, sepIdx), 0);
|
|
29
|
+
stampedEmlBytes.set(insertion, sepIdx);
|
|
30
|
+
stampedEmlBytes.set(originalEmlBytes.subarray(sepIdx), sepIdx + insertion.length);
|
|
31
|
+
return { stampedEmlBytes };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Extract the embedded certificate from a stamped `.eml` and return the
|
|
35
|
+
* recovered original bytes (with the `X-Tisura-Certificate` header stripped).
|
|
36
|
+
* Returns null if no embedded certificate is found.
|
|
37
|
+
*/
|
|
38
|
+
export function extractCertificateFromEml(emlBytes) {
|
|
39
|
+
let sep;
|
|
40
|
+
try {
|
|
41
|
+
sep = locateHeaderBodySeparator(emlBytes);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const headerBytes = emlBytes.subarray(0, sep.sepIdx);
|
|
47
|
+
const headerText = new TextDecoder("utf-8", { fatal: false }).decode(headerBytes);
|
|
48
|
+
const lines = headerText.split(sep.eol);
|
|
49
|
+
let startIdx = -1;
|
|
50
|
+
let endIdx = -1;
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
if (lines[i].toLowerCase().startsWith(HEADER_NAME.toLowerCase() + ":")) {
|
|
53
|
+
startIdx = i;
|
|
54
|
+
endIdx = i;
|
|
55
|
+
while (endIdx + 1 < lines.length && /^[ \t]/.test(lines[endIdx + 1])) {
|
|
56
|
+
endIdx++;
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (startIdx === -1)
|
|
62
|
+
return null;
|
|
63
|
+
let value = lines[startIdx].slice(HEADER_NAME.length + 1).trim();
|
|
64
|
+
for (let i = startIdx + 1; i <= endIdx; i++)
|
|
65
|
+
value += lines[i].trim();
|
|
66
|
+
let certObj;
|
|
67
|
+
try {
|
|
68
|
+
certObj = JSON.parse(atob(value));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// Strip the header (and the EOL preceding it that we inserted) to recover
|
|
74
|
+
// original bytes. The inserted region is `eol + headerLine`, placed at the
|
|
75
|
+
// byte offset where the previous header line ended.
|
|
76
|
+
const headerLines = lines.slice(0, startIdx);
|
|
77
|
+
const trailingLines = lines.slice(endIdx + 1);
|
|
78
|
+
const recoveredHeader = [...headerLines, ...trailingLines].join(sep.eol);
|
|
79
|
+
const recoveredHeaderBytes = new TextEncoder().encode(recoveredHeader);
|
|
80
|
+
const originalEmlBytes = new Uint8Array(recoveredHeaderBytes.length + (emlBytes.length - sep.sepIdx));
|
|
81
|
+
originalEmlBytes.set(recoveredHeaderBytes, 0);
|
|
82
|
+
originalEmlBytes.set(emlBytes.subarray(sep.sepIdx), recoveredHeaderBytes.length);
|
|
83
|
+
return { certObj, originalEmlBytes };
|
|
84
|
+
}
|
|
85
|
+
export function isEmlWithCertificate(emlBytes) {
|
|
86
|
+
return extractCertificateFromEml(emlBytes) !== null;
|
|
87
|
+
}
|
|
88
|
+
function locateHeaderBodySeparator(emlBytes) {
|
|
89
|
+
const findSeq = (haystack, needle) => {
|
|
90
|
+
outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
|
|
91
|
+
for (let j = 0; j < needle.length; j++) {
|
|
92
|
+
if (haystack[i + j] !== needle[j])
|
|
93
|
+
continue outer;
|
|
94
|
+
}
|
|
95
|
+
return i;
|
|
96
|
+
}
|
|
97
|
+
return -1;
|
|
98
|
+
};
|
|
99
|
+
let sepIdx = findSeq(emlBytes, [13, 10, 13, 10]);
|
|
100
|
+
if (sepIdx !== -1)
|
|
101
|
+
return { sepIdx, eol: "\r\n" };
|
|
102
|
+
sepIdx = findSeq(emlBytes, [10, 10]);
|
|
103
|
+
if (sepIdx !== -1)
|
|
104
|
+
return { sepIdx, eol: "\n" };
|
|
105
|
+
throw new Error("EML header/body separator not found");
|
|
106
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SessionKey } from "../inputs.js";
|
|
2
|
+
/**
|
|
3
|
+
* Embed certificate.json as a PDF file attachment (ISO 32000-2:2020, 7.11.4).
|
|
4
|
+
*
|
|
5
|
+
* Uses incremental save to preserve original bytes as a prefix of the stamped
|
|
6
|
+
* PDF. The certificate includes `original_content_length` so the verifier can
|
|
7
|
+
* slice the stamped PDF to recover the exact original bytes and compare them
|
|
8
|
+
* against the TLS-decrypted body.
|
|
9
|
+
*/
|
|
10
|
+
export declare function embedCertificateInPdf(originalPdfBytes: Uint8Array, certificate: SessionKey): Promise<{
|
|
11
|
+
stampedPdfBytes: Uint8Array;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Extract certificate.json from a PDF's EmbeddedFiles name tree.
|
|
15
|
+
* Returns null if no embedded certificate is found.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractCertificateFromPdf(pdfBytes: Uint8Array): Promise<{
|
|
18
|
+
certObj: unknown;
|
|
19
|
+
} | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a PDF has an embedded certificate attachment.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isPdfWithCertificate(pdfBytes: Uint8Array): Promise<boolean>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { PDFDocument, PDFDict, PDFName, PDFArray, PDFRawStream, PDFRef, } from "@cantoo/pdf-lib";
|
|
2
|
+
import { unzlibSync } from "fflate";
|
|
3
|
+
/**
|
|
4
|
+
* Embed certificate.json as a PDF file attachment (ISO 32000-2:2020, 7.11.4).
|
|
5
|
+
*
|
|
6
|
+
* Uses incremental save to preserve original bytes as a prefix of the stamped
|
|
7
|
+
* PDF. The certificate includes `original_content_length` so the verifier can
|
|
8
|
+
* slice the stamped PDF to recover the exact original bytes and compare them
|
|
9
|
+
* against the TLS-decrypted body.
|
|
10
|
+
*/
|
|
11
|
+
export async function embedCertificateInPdf(originalPdfBytes, certificate) {
|
|
12
|
+
const pdfDoc = await PDFDocument.load(originalPdfBytes, {
|
|
13
|
+
forIncrementalUpdate: true,
|
|
14
|
+
});
|
|
15
|
+
const certWithLength = {
|
|
16
|
+
...certificate,
|
|
17
|
+
original_content_length: originalPdfBytes.length,
|
|
18
|
+
};
|
|
19
|
+
const certJson = JSON.stringify(certWithLength, null, 2);
|
|
20
|
+
await pdfDoc.attach(new TextEncoder().encode(certJson), "certificate.json", {
|
|
21
|
+
mimeType: "application/json",
|
|
22
|
+
description: "Tisura TLS verification certificate",
|
|
23
|
+
});
|
|
24
|
+
const stampedPdfBytes = await pdfDoc.save();
|
|
25
|
+
return { stampedPdfBytes };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extract certificate.json from a PDF's EmbeddedFiles name tree.
|
|
29
|
+
* Returns null if no embedded certificate is found.
|
|
30
|
+
*/
|
|
31
|
+
export async function extractCertificateFromPdf(pdfBytes) {
|
|
32
|
+
const pdfDoc = await PDFDocument.load(pdfBytes);
|
|
33
|
+
const namesArray = getEmbeddedFilesNamesArray(pdfDoc);
|
|
34
|
+
if (!namesArray)
|
|
35
|
+
return null;
|
|
36
|
+
// The only embedded file is certificate.json — find the first entry
|
|
37
|
+
const certBytes = readFirstEmbeddedFile(pdfDoc, namesArray);
|
|
38
|
+
if (!certBytes)
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
return { certObj: JSON.parse(new TextDecoder().decode(certBytes)) };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if a PDF has an embedded certificate attachment.
|
|
49
|
+
*/
|
|
50
|
+
export async function isPdfWithCertificate(pdfBytes) {
|
|
51
|
+
try {
|
|
52
|
+
const result = await extractCertificateFromPdf(pdfBytes);
|
|
53
|
+
return result !== null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// ── Internal helpers ──────────────────────────────────────────────────
|
|
60
|
+
function getEmbeddedFilesNamesArray(pdfDoc) {
|
|
61
|
+
const catalog = pdfDoc.catalog;
|
|
62
|
+
if (!catalog.has(PDFName.of("Names")))
|
|
63
|
+
return null;
|
|
64
|
+
const names = catalog.lookup(PDFName.of("Names"), PDFDict);
|
|
65
|
+
if (!names.has(PDFName.of("EmbeddedFiles")))
|
|
66
|
+
return null;
|
|
67
|
+
const embeddedFiles = names.lookup(PDFName.of("EmbeddedFiles"), PDFDict);
|
|
68
|
+
if (!embeddedFiles.has(PDFName.of("Names")))
|
|
69
|
+
return null;
|
|
70
|
+
return embeddedFiles.lookup(PDFName.of("Names"), PDFArray);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Read the first embedded file from the EmbeddedFiles Names array.
|
|
74
|
+
* Array structure: [name1, filespec1, name2, filespec2, ...].
|
|
75
|
+
*/
|
|
76
|
+
function readFirstEmbeddedFile(pdfDoc, namesArray) {
|
|
77
|
+
if (namesArray.size() < 2)
|
|
78
|
+
return null;
|
|
79
|
+
const fileSpecObj = namesArray.get(1);
|
|
80
|
+
const fileSpec = fileSpecObj instanceof PDFRef
|
|
81
|
+
? pdfDoc.context.lookup(fileSpecObj, PDFDict)
|
|
82
|
+
: fileSpecObj;
|
|
83
|
+
if (!fileSpec.has(PDFName.of("EF")))
|
|
84
|
+
return null;
|
|
85
|
+
const ef = fileSpec.lookup(PDFName.of("EF"), PDFDict);
|
|
86
|
+
const streamRef = ef.get(PDFName.of("F"));
|
|
87
|
+
if (!streamRef)
|
|
88
|
+
return null;
|
|
89
|
+
const stream = pdfDoc.context.lookup(streamRef);
|
|
90
|
+
if (stream instanceof PDFRawStream) {
|
|
91
|
+
const filter = stream.dict.get(PDFName.of("Filter"));
|
|
92
|
+
if (filter && filter === PDFName.of("FlateDecode")) {
|
|
93
|
+
return unzlibSync(stream.contents);
|
|
94
|
+
}
|
|
95
|
+
return stream.contents;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SessionKey } from "../inputs.js";
|
|
2
|
+
export type QrPlacement = {
|
|
3
|
+
pageIndex: number;
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
size?: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Embed a QR code on the PDF.
|
|
10
|
+
*
|
|
11
|
+
* Defaults to bottom-right of the last page. Pass `placement` to override —
|
|
12
|
+
* callers that detect whitespace can pick a corner that won't collide with
|
|
13
|
+
* existing content.
|
|
14
|
+
*
|
|
15
|
+
* The QR encodes a verification URL with certificate query params:
|
|
16
|
+
* <verifyBaseUrl>?cr=<client_random>&secret=<secret>&len=<original_content_length>
|
|
17
|
+
*
|
|
18
|
+
* Uses QRCode.toCanvas with OffscreenCanvas in service workers (no DOM needed),
|
|
19
|
+
* falls back to QRCode.toDataURL in Node.js.
|
|
20
|
+
*
|
|
21
|
+
* Uses incremental save to preserve all preceding bytes (including any
|
|
22
|
+
* certificate attachment from embedCertificateInPdf) as a prefix of the
|
|
23
|
+
* output, so `original_content_length` slice still works for verification.
|
|
24
|
+
*/
|
|
25
|
+
export declare function embedQrCodeInPdf(pdfBytes: Uint8Array, certificate: SessionKey & {
|
|
26
|
+
original_content_length: number;
|
|
27
|
+
}, verifyBaseUrl?: string, placement?: QrPlacement): Promise<Uint8Array>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
import { PDFDocument } from "@cantoo/pdf-lib";
|
|
3
|
+
import QRCode from "qrcode";
|
|
4
|
+
const DEFAULT_VERIFY_BASE_URL = "https://tisura.xyz/verifier";
|
|
5
|
+
const DEFAULT_QR_SIZE = 50;
|
|
6
|
+
const DEFAULT_QR_MARGIN = 20;
|
|
7
|
+
/**
|
|
8
|
+
* Embed a QR code on the PDF.
|
|
9
|
+
*
|
|
10
|
+
* Defaults to bottom-right of the last page. Pass `placement` to override —
|
|
11
|
+
* callers that detect whitespace can pick a corner that won't collide with
|
|
12
|
+
* existing content.
|
|
13
|
+
*
|
|
14
|
+
* The QR encodes a verification URL with certificate query params:
|
|
15
|
+
* <verifyBaseUrl>?cr=<client_random>&secret=<secret>&len=<original_content_length>
|
|
16
|
+
*
|
|
17
|
+
* Uses QRCode.toCanvas with OffscreenCanvas in service workers (no DOM needed),
|
|
18
|
+
* falls back to QRCode.toDataURL in Node.js.
|
|
19
|
+
*
|
|
20
|
+
* Uses incremental save to preserve all preceding bytes (including any
|
|
21
|
+
* certificate attachment from embedCertificateInPdf) as a prefix of the
|
|
22
|
+
* output, so `original_content_length` slice still works for verification.
|
|
23
|
+
*/
|
|
24
|
+
export async function embedQrCodeInPdf(pdfBytes, certificate, verifyBaseUrl = DEFAULT_VERIFY_BASE_URL, placement) {
|
|
25
|
+
const params = new URLSearchParams({
|
|
26
|
+
cr: certificate.client_random,
|
|
27
|
+
secret: certificate.secret,
|
|
28
|
+
len: String(certificate.original_content_length),
|
|
29
|
+
});
|
|
30
|
+
const verifyUrl = `${verifyBaseUrl}?${params}`;
|
|
31
|
+
const qrPng = await generateQrPng(verifyUrl);
|
|
32
|
+
const pdfDoc = await PDFDocument.load(pdfBytes, {
|
|
33
|
+
forIncrementalUpdate: true,
|
|
34
|
+
});
|
|
35
|
+
const qrImage = await pdfDoc.embedPng(qrPng);
|
|
36
|
+
const pages = pdfDoc.getPages();
|
|
37
|
+
const { page, x, y, size } = resolvePlacement(pages, placement);
|
|
38
|
+
page.drawImage(qrImage, { x, y, width: size, height: size });
|
|
39
|
+
return pdfDoc.save();
|
|
40
|
+
}
|
|
41
|
+
function resolvePlacement(pages, placement) {
|
|
42
|
+
if (placement) {
|
|
43
|
+
if (placement.pageIndex < 0 || placement.pageIndex >= pages.length) {
|
|
44
|
+
throw new Error(`embedQrCodeInPdf: pageIndex ${placement.pageIndex} out of range (0..${pages.length - 1})`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
page: pages[placement.pageIndex],
|
|
48
|
+
x: placement.x,
|
|
49
|
+
y: placement.y,
|
|
50
|
+
size: placement.size ?? DEFAULT_QR_SIZE,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const page = pages[pages.length - 1];
|
|
54
|
+
return {
|
|
55
|
+
page,
|
|
56
|
+
x: page.getWidth() - DEFAULT_QR_SIZE - DEFAULT_QR_MARGIN,
|
|
57
|
+
y: DEFAULT_QR_MARGIN,
|
|
58
|
+
size: DEFAULT_QR_SIZE,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function generateQrPng(content) {
|
|
62
|
+
const width = 200;
|
|
63
|
+
const opts = { width, margin: 1 };
|
|
64
|
+
// In service workers, use OffscreenCanvas + convertToBlob (no DOM needed).
|
|
65
|
+
// In Node.js, use toDataURL which works with node-canvas/pngjs.
|
|
66
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
67
|
+
const canvas = new OffscreenCanvas(width, width);
|
|
68
|
+
await QRCode.toCanvas(canvas, content, opts);
|
|
69
|
+
const blob = await canvas.convertToBlob({ type: "image/png" });
|
|
70
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
71
|
+
}
|
|
72
|
+
const dataUrl = await QRCode.toDataURL(content, {
|
|
73
|
+
...opts,
|
|
74
|
+
type: "image/png",
|
|
75
|
+
});
|
|
76
|
+
const base64 = dataUrl.split(",")[1];
|
|
77
|
+
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
|
|
78
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { TisuraConfig } from "../config.js";
|
|
2
|
+
import { CertifyInputs, SessionKey, ScreenshotCertificate, ExtractedAttributesCertificate, AttributeVerificationResult, BatchCertificationResult } from "../inputs.js";
|
|
3
|
+
import type { ExtractionZone } from "./providers/types.js";
|
|
4
|
+
export declare class CertifyClient {
|
|
5
|
+
private config;
|
|
6
|
+
private parseClient;
|
|
7
|
+
constructor(config: TisuraConfig);
|
|
8
|
+
certify({ inputBytes, sessionKey, }: {
|
|
9
|
+
inputBytes: Uint8Array;
|
|
10
|
+
sessionKey: SessionKey;
|
|
11
|
+
}): Promise<boolean>;
|
|
12
|
+
certifyScreenshot({ imageBytes, certificate, }: {
|
|
13
|
+
imageBytes: Uint8Array;
|
|
14
|
+
certificate: ScreenshotCertificate;
|
|
15
|
+
}): Promise<boolean>;
|
|
16
|
+
certifyZipFile({ zipFile }: {
|
|
17
|
+
zipFile: File;
|
|
18
|
+
}): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Certify a PDF that has certificate.json embedded as a file attachment
|
|
21
|
+
* (ISO 32000-2:2020, 7.11.4).
|
|
22
|
+
*
|
|
23
|
+
* Extracts the TLS session key from the embedded certificate, recovers the
|
|
24
|
+
* original PDF bytes using `original_content_length` (incremental save
|
|
25
|
+
* preserves them as a prefix), then verifies by comparing against the
|
|
26
|
+
* TLS-decrypted response body.
|
|
27
|
+
*/
|
|
28
|
+
certifyPdfFile({ pdfFile }: {
|
|
29
|
+
pdfFile: File;
|
|
30
|
+
}): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Certify an `.eml` that has its TLS session key embedded as a folded
|
|
33
|
+
* `X-Tisura-Certificate` header (RFC 5322).
|
|
34
|
+
*
|
|
35
|
+
* Extracts the cert + recovers original bytes by stripping the header,
|
|
36
|
+
* then verifies by comparing against the TLS-decrypted response body.
|
|
37
|
+
*/
|
|
38
|
+
certifyEmlFile({ emlFile }: {
|
|
39
|
+
emlFile: File;
|
|
40
|
+
}): Promise<boolean>;
|
|
41
|
+
certifyExtractedAttributes({ pdfBytes, certificate, extractedAttributes, }: {
|
|
42
|
+
pdfBytes: Uint8Array;
|
|
43
|
+
certificate: ExtractedAttributesCertificate;
|
|
44
|
+
extractedAttributes: Record<string, string>;
|
|
45
|
+
}): Promise<AttributeVerificationResult>;
|
|
46
|
+
getExtractionZones(domain: string): Record<string, ExtractionZone> | null;
|
|
47
|
+
extractAttributes(rawZoneText: Record<string, string>, domain: string): Record<string, string>;
|
|
48
|
+
/**
|
|
49
|
+
* Shared logic for certifying a payload with its certificate.
|
|
50
|
+
* Detects certificate type and calls the appropriate certify method.
|
|
51
|
+
*/
|
|
52
|
+
private certifyPayloadWithCert;
|
|
53
|
+
certifyBatchZipFile({ zipFile, }: {
|
|
54
|
+
zipFile: File;
|
|
55
|
+
}): Promise<BatchCertificationResult[]>;
|
|
56
|
+
private certifySingleItem;
|
|
57
|
+
decrypt(sessionKey: SessionKey): Promise<Uint8Array>;
|
|
58
|
+
decryptBodyAndHeaders(sessionKey: SessionKey): Promise<{
|
|
59
|
+
body: Uint8Array;
|
|
60
|
+
headers: Record<string, string>;
|
|
61
|
+
}>;
|
|
62
|
+
decryptToString(sessionKey: SessionKey): Promise<string>;
|
|
63
|
+
getInput(sessionKey: SessionKey): Promise<CertifyInputs[]>;
|
|
64
|
+
private verifyExtractionData;
|
|
65
|
+
private log;
|
|
66
|
+
private computeHash;
|
|
67
|
+
private verifySignature;
|
|
68
|
+
private importRSAPublicKey;
|
|
69
|
+
private hexToBytes;
|
|
70
|
+
private isScreenshotCertificate;
|
|
71
|
+
private isExtractedAttributesCertificate;
|
|
72
|
+
private isSessionKeyCertificate;
|
|
73
|
+
}
|