easy_code_sign 0.1.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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +95 -0
  3. data/LICENSE +21 -0
  4. data/README.md +331 -0
  5. data/Rakefile +16 -0
  6. data/exe/easysign +7 -0
  7. data/lib/easy_code_sign/cli.rb +428 -0
  8. data/lib/easy_code_sign/configuration.rb +102 -0
  9. data/lib/easy_code_sign/deferred_signing_request.rb +104 -0
  10. data/lib/easy_code_sign/errors.rb +113 -0
  11. data/lib/easy_code_sign/pdf/appearance_builder.rb +104 -0
  12. data/lib/easy_code_sign/pdf/timestamp_handler.rb +31 -0
  13. data/lib/easy_code_sign/providers/base.rb +126 -0
  14. data/lib/easy_code_sign/providers/pkcs11_base.rb +197 -0
  15. data/lib/easy_code_sign/providers/safenet.rb +109 -0
  16. data/lib/easy_code_sign/signable/base.rb +98 -0
  17. data/lib/easy_code_sign/signable/gem_file.rb +224 -0
  18. data/lib/easy_code_sign/signable/pdf_file.rb +486 -0
  19. data/lib/easy_code_sign/signable/zip_file.rb +226 -0
  20. data/lib/easy_code_sign/signer.rb +254 -0
  21. data/lib/easy_code_sign/timestamp/client.rb +184 -0
  22. data/lib/easy_code_sign/timestamp/request.rb +114 -0
  23. data/lib/easy_code_sign/timestamp/response.rb +246 -0
  24. data/lib/easy_code_sign/timestamp/verifier.rb +227 -0
  25. data/lib/easy_code_sign/verification/certificate_chain.rb +298 -0
  26. data/lib/easy_code_sign/verification/result.rb +222 -0
  27. data/lib/easy_code_sign/verification/signature_checker.rb +196 -0
  28. data/lib/easy_code_sign/verification/trust_store.rb +140 -0
  29. data/lib/easy_code_sign/verifier.rb +426 -0
  30. data/lib/easy_code_sign/version.rb +5 -0
  31. data/lib/easy_code_sign.rb +183 -0
  32. data/plugin/.gitignore +21 -0
  33. data/plugin/Gemfile +24 -0
  34. data/plugin/Gemfile.lock +134 -0
  35. data/plugin/README.md +248 -0
  36. data/plugin/Rakefile +121 -0
  37. data/plugin/docs/API_REFERENCE.md +366 -0
  38. data/plugin/docs/DEVELOPMENT.md +522 -0
  39. data/plugin/docs/INSTALLATION.md +204 -0
  40. data/plugin/native_host/build/Rakefile +90 -0
  41. data/plugin/native_host/install/com.easysign.host.json +9 -0
  42. data/plugin/native_host/install/install_chrome.sh +81 -0
  43. data/plugin/native_host/install/install_firefox.sh +81 -0
  44. data/plugin/native_host/src/easy_sign_host.rb +158 -0
  45. data/plugin/native_host/src/protocol.rb +101 -0
  46. data/plugin/native_host/src/signing_service.rb +167 -0
  47. data/plugin/native_host/test/native_host_test.rb +113 -0
  48. data/plugin/src/easy_sign/background.rb +323 -0
  49. data/plugin/src/easy_sign/content.rb +74 -0
  50. data/plugin/src/easy_sign/inject.rb +239 -0
  51. data/plugin/src/easy_sign/messaging.rb +109 -0
  52. data/plugin/src/easy_sign/popup.rb +200 -0
  53. data/plugin/templates/manifest.json +58 -0
  54. data/plugin/templates/popup.css +223 -0
  55. data/plugin/templates/popup.html +59 -0
  56. data/sig/easy_code_sign.rbs +4 -0
  57. data/test/easy_code_sign_test.rb +122 -0
  58. data/test/pdf_signable_test.rb +569 -0
  59. data/test/signable_test.rb +334 -0
  60. data/test/test_helper.rb +18 -0
  61. data/test/timestamp_test.rb +163 -0
  62. data/test/verification_test.rb +350 -0
  63. metadata +219 -0
data/plugin/Rakefile ADDED
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "opal"
5
+ require "fileutils"
6
+
7
+ DIST_DIR = File.expand_path("dist", __dir__)
8
+ SRC_DIR = File.expand_path("src", __dir__)
9
+
10
+ desc "Build all extension components"
11
+ task build: ["build:background", "build:content", "build:inject", "build:popup", "build:manifest", "build:assets"]
12
+
13
+ namespace :build do
14
+ desc "Build background service worker"
15
+ task :background do
16
+ puts "Building background.js..."
17
+ compile_opal("easy_sign/background", "background.js")
18
+ end
19
+
20
+ desc "Build content script"
21
+ task :content do
22
+ puts "Building content.js..."
23
+ compile_opal("easy_sign/content", "content.js")
24
+ end
25
+
26
+ desc "Build injected page script"
27
+ task :inject do
28
+ puts "Building inject.js..."
29
+ compile_opal("easy_sign/inject", "inject.js")
30
+ end
31
+
32
+ desc "Build popup script"
33
+ task :popup do
34
+ puts "Building popup.js..."
35
+ compile_opal("easy_sign/popup", "popup/popup.js")
36
+
37
+ # Copy popup HTML and CSS
38
+ FileUtils.mkdir_p("#{DIST_DIR}/popup")
39
+ FileUtils.cp("templates/popup.html", "#{DIST_DIR}/popup/popup.html")
40
+ FileUtils.cp("templates/popup.css", "#{DIST_DIR}/popup/popup.css")
41
+ end
42
+
43
+ desc "Copy manifest.json"
44
+ task :manifest do
45
+ puts "Copying manifest.json..."
46
+ FileUtils.mkdir_p(DIST_DIR)
47
+ FileUtils.cp("templates/manifest.json", "#{DIST_DIR}/manifest.json")
48
+ end
49
+
50
+ desc "Copy static assets (icons, etc)"
51
+ task :assets do
52
+ puts "Copying assets..."
53
+ FileUtils.mkdir_p("#{DIST_DIR}/icons")
54
+ if Dir.exist?("assets/icons")
55
+ FileUtils.cp_r("assets/icons/.", "#{DIST_DIR}/icons/")
56
+ end
57
+ end
58
+ end
59
+
60
+ desc "Watch for changes and rebuild"
61
+ task :watch do
62
+ require "listen"
63
+
64
+ puts "Watching for changes in src/ and templates/..."
65
+
66
+ listener = Listen.to(SRC_DIR, "templates") do |modified, added, removed|
67
+ puts "\nChanges detected: #{(modified + added + removed).join(', ')}"
68
+ Rake::Task["build"].reenable
69
+ Rake::Task["build"].invoke
70
+ puts "Rebuild complete."
71
+ end
72
+
73
+ listener.start
74
+ puts "Press Ctrl+C to stop."
75
+ sleep
76
+ end
77
+
78
+ desc "Clean dist directory"
79
+ task :clean do
80
+ puts "Cleaning dist/..."
81
+ FileUtils.rm_rf(Dir.glob("#{DIST_DIR}/*"))
82
+ puts "Clean complete."
83
+ end
84
+
85
+ desc "Run tests"
86
+ task :test do
87
+ require "minitest/autorun"
88
+ Dir.glob("test/**/*_test.rb").each { |f| require_relative f }
89
+ end
90
+
91
+ # Development server for testing
92
+ desc "Start a simple HTTP server for testing"
93
+ task :server do
94
+ require "webrick"
95
+ server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: DIST_DIR)
96
+ trap("INT") { server.shutdown }
97
+ puts "Serving dist/ at http://localhost:8080"
98
+ server.start
99
+ end
100
+
101
+ def compile_opal(entry_point, output_file)
102
+ # Create a fresh builder with custom paths
103
+ builder = Opal::Builder.new(
104
+ stubs: [],
105
+ compiler_options: {
106
+ dynamic_require_severity: :ignore
107
+ }
108
+ )
109
+
110
+ # Add source directory to builder's path
111
+ builder.append_paths(SRC_DIR)
112
+
113
+ # Build the entry point
114
+ builder.build(entry_point)
115
+
116
+ output_path = File.join(DIST_DIR, output_file)
117
+ FileUtils.mkdir_p(File.dirname(output_path))
118
+
119
+ File.write(output_path, builder.to_s)
120
+ puts " -> #{output_path} (#{File.size(output_path)} bytes)"
121
+ end
@@ -0,0 +1,366 @@
1
+ # EasySign API Reference
2
+
3
+ The EasySign browser extension exposes a `window.EasySign` object that web applications can use to sign and verify PDF documents using hardware security tokens.
4
+
5
+ ## Overview
6
+
7
+ ```javascript
8
+ // Check if EasySign is available
9
+ const status = await window.EasySign.isAvailable();
10
+
11
+ // Sign a PDF
12
+ const result = await window.EasySign.sign(pdfBlob, options);
13
+
14
+ // Verify a signed PDF
15
+ const verification = await window.EasySign.verify(pdfBlob);
16
+ ```
17
+
18
+ ## Methods
19
+
20
+ ### `window.EasySign.isAvailable()`
21
+
22
+ Check if the EasySign extension is installed and the hardware token is connected.
23
+
24
+ **Parameters:** None
25
+
26
+ **Returns:** `Promise<AvailabilityResult>`
27
+
28
+ ```typescript
29
+ interface AvailabilityResult {
30
+ available: boolean; // Extension and native host are working
31
+ tokenPresent: boolean; // Hardware token is connected
32
+ slots: TokenSlot[]; // List of available token slots
33
+ }
34
+
35
+ interface TokenSlot {
36
+ index: number; // Slot index
37
+ tokenLabel: string; // Token display name
38
+ manufacturer: string; // Token manufacturer
39
+ serial: string; // Token serial number
40
+ }
41
+ ```
42
+
43
+ **Example:**
44
+ ```javascript
45
+ window.EasySign.isAvailable()
46
+ .then(result => {
47
+ if (!result.available) {
48
+ alert('Please install the EasySign extension');
49
+ return;
50
+ }
51
+ if (!result.tokenPresent) {
52
+ alert('Please connect your hardware token');
53
+ return;
54
+ }
55
+ console.log('Ready to sign!', result.slots);
56
+ })
57
+ .catch(err => {
58
+ console.error('EasySign error:', err);
59
+ });
60
+ ```
61
+
62
+ ---
63
+
64
+ ### `window.EasySign.sign(pdfBlob, options)`
65
+
66
+ Sign a PDF document. Opens a popup for secure PIN entry.
67
+
68
+ **Parameters:**
69
+
70
+ | Parameter | Type | Required | Description |
71
+ |-----------|------|----------|-------------|
72
+ | `pdfBlob` | `Blob` | Yes | The PDF file to sign |
73
+ | `options` | `SignOptions` | No | Signing options |
74
+
75
+ ```typescript
76
+ interface SignOptions {
77
+ // Signature metadata
78
+ reason?: string; // Reason for signing (e.g., "Approved")
79
+ location?: string; // Location of signing (e.g., "New York")
80
+
81
+ // Visible signature
82
+ visibleSignature?: boolean; // Add visible signature annotation (default: false)
83
+ signaturePosition?: string; // Position: "top_left", "top_right", "bottom_left", "bottom_right"
84
+ signaturePage?: number; // Page number for signature (default: 1)
85
+
86
+ // Timestamp
87
+ timestamp?: boolean; // Add RFC 3161 timestamp (default: false)
88
+ timestampAuthority?: string; // TSA URL (default: http://timestamp.digicert.com)
89
+ }
90
+ ```
91
+
92
+ **Returns:** `Promise<SignResult>`
93
+
94
+ ```typescript
95
+ interface SignResult {
96
+ blob: Blob; // The signed PDF file
97
+ signer_name: string; // Certificate common name
98
+ signed_at: string; // ISO 8601 timestamp
99
+ timestamped: boolean; // Whether timestamp was added
100
+ }
101
+ ```
102
+
103
+ **Example:**
104
+ ```javascript
105
+ // Get PDF from file input
106
+ const fileInput = document.getElementById('pdf-file');
107
+ const pdfBlob = fileInput.files[0];
108
+
109
+ // Sign with options
110
+ window.EasySign.sign(pdfBlob, {
111
+ reason: 'Document approved',
112
+ location: 'New York, NY',
113
+ visibleSignature: true,
114
+ signaturePosition: 'bottom_right',
115
+ timestamp: true
116
+ })
117
+ .then(result => {
118
+ console.log('Signed by:', result.signer_name);
119
+ console.log('Signed at:', result.signed_at);
120
+
121
+ // Download the signed PDF
122
+ const url = URL.createObjectURL(result.blob);
123
+ const link = document.createElement('a');
124
+ link.href = url;
125
+ link.download = 'signed_document.pdf';
126
+ link.click();
127
+ URL.revokeObjectURL(url);
128
+ })
129
+ .catch(err => {
130
+ if (err.code === 'CANCELLED') {
131
+ console.log('User cancelled signing');
132
+ } else {
133
+ console.error('Signing failed:', err.message);
134
+ }
135
+ });
136
+ ```
137
+
138
+ ---
139
+
140
+ ### `window.EasySign.verify(pdfBlob, options)`
141
+
142
+ Verify a signed PDF document.
143
+
144
+ **Parameters:**
145
+
146
+ | Parameter | Type | Required | Description |
147
+ |-----------|------|----------|-------------|
148
+ | `pdfBlob` | `Blob` | Yes | The signed PDF file to verify |
149
+ | `options` | `VerifyOptions` | No | Verification options |
150
+
151
+ ```typescript
152
+ interface VerifyOptions {
153
+ checkTimestamp?: boolean; // Verify timestamp (default: true)
154
+ }
155
+ ```
156
+
157
+ **Returns:** `Promise<VerifyResult>`
158
+
159
+ ```typescript
160
+ interface VerifyResult {
161
+ payload: {
162
+ valid: boolean; // Overall signature validity
163
+ signerName: string; // Certificate common name
164
+ signerOrganization: string; // Certificate organization
165
+ signedAt: string; // Signing timestamp (ISO 8601)
166
+
167
+ // Detailed checks
168
+ signatureValid: boolean; // Cryptographic signature OK
169
+ integrityValid: boolean; // Document not tampered
170
+ certificateValid: boolean; // Certificate not expired
171
+ chainValid: boolean; // Certificate chain OK
172
+ trusted: boolean; // Root CA is trusted
173
+
174
+ // Timestamp
175
+ timestamped: boolean; // Has timestamp
176
+ timestampValid: boolean; // Timestamp is valid
177
+
178
+ // Issues
179
+ errors: string[]; // List of errors
180
+ warnings: string[]; // List of warnings
181
+ }
182
+ }
183
+ ```
184
+
185
+ **Example:**
186
+ ```javascript
187
+ window.EasySign.verify(signedPdfBlob)
188
+ .then(result => {
189
+ const v = result.payload;
190
+
191
+ if (v.valid) {
192
+ console.log('✓ Signature is valid');
193
+ console.log(' Signed by:', v.signerName);
194
+ console.log(' Organization:', v.signerOrganization);
195
+ console.log(' Date:', new Date(v.signedAt).toLocaleString());
196
+
197
+ if (v.timestamped) {
198
+ console.log(' Timestamped: Yes');
199
+ }
200
+ } else {
201
+ console.log('✗ Signature is invalid');
202
+ v.errors.forEach(err => console.log(' Error:', err));
203
+ }
204
+
205
+ if (v.warnings.length > 0) {
206
+ console.log('Warnings:');
207
+ v.warnings.forEach(w => console.log(' -', w));
208
+ }
209
+ });
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Error Handling
215
+
216
+ All methods return Promises that reject with an `Error` object containing a `code` property.
217
+
218
+ ```typescript
219
+ interface EasySignError extends Error {
220
+ code: string; // Error code for programmatic handling
221
+ }
222
+ ```
223
+
224
+ ### Error Codes
225
+
226
+ | Code | Description | User Action |
227
+ |------|-------------|-------------|
228
+ | `TOKEN_NOT_FOUND` | Hardware token not connected | Connect token and retry |
229
+ | `PIN_INCORRECT` | Wrong PIN entered | Re-enter correct PIN |
230
+ | `TOKEN_LOCKED` | Token locked (too many wrong PINs) | Contact administrator |
231
+ | `INVALID_PDF` | PDF file is corrupted or invalid | Use a valid PDF file |
232
+ | `SIGNING_FAILED` | Signing operation failed | Check token and retry |
233
+ | `VERIFICATION_FAILED` | Verification operation failed | Check PDF file |
234
+ | `NATIVE_HOST_NOT_FOUND` | Native host not installed | Install native host |
235
+ | `TIMEOUT` | Operation timed out | Retry the operation |
236
+ | `CANCELLED` | User cancelled the operation | No action needed |
237
+ | `ORIGIN_NOT_ALLOWED` | Website not allowed to use EasySign | N/A |
238
+ | `INTERNAL_ERROR` | Unexpected error | Report bug |
239
+
240
+ ### Error Handling Example
241
+
242
+ ```javascript
243
+ window.EasySign.sign(pdfBlob, options)
244
+ .then(result => {
245
+ // Success
246
+ })
247
+ .catch(err => {
248
+ switch (err.code) {
249
+ case 'TOKEN_NOT_FOUND':
250
+ showMessage('Please connect your hardware token');
251
+ break;
252
+
253
+ case 'PIN_INCORRECT':
254
+ showMessage('Incorrect PIN. Please try again.');
255
+ break;
256
+
257
+ case 'TOKEN_LOCKED':
258
+ showMessage('Your token is locked. Contact your IT administrator.');
259
+ break;
260
+
261
+ case 'CANCELLED':
262
+ // User cancelled - no message needed
263
+ break;
264
+
265
+ case 'TIMEOUT':
266
+ showMessage('Operation timed out. Please try again.');
267
+ break;
268
+
269
+ default:
270
+ showMessage(`Signing failed: ${err.message}`);
271
+ console.error('EasySign error:', err);
272
+ }
273
+ });
274
+ ```
275
+
276
+ ---
277
+
278
+ ## TypeScript Definitions
279
+
280
+ For TypeScript projects, you can use these type definitions:
281
+
282
+ ```typescript
283
+ declare global {
284
+ interface Window {
285
+ EasySign: {
286
+ isAvailable(): Promise<{
287
+ available: boolean;
288
+ tokenPresent: boolean;
289
+ slots: Array<{
290
+ index: number;
291
+ tokenLabel: string;
292
+ manufacturer: string;
293
+ serial: string;
294
+ }>;
295
+ }>;
296
+
297
+ sign(pdfBlob: Blob, options?: {
298
+ reason?: string;
299
+ location?: string;
300
+ visibleSignature?: boolean;
301
+ signaturePosition?: 'top_left' | 'top_right' | 'bottom_left' | 'bottom_right';
302
+ signaturePage?: number;
303
+ timestamp?: boolean;
304
+ timestampAuthority?: string;
305
+ }): Promise<{
306
+ blob: Blob;
307
+ signer_name: string;
308
+ signed_at: string;
309
+ timestamped: boolean;
310
+ }>;
311
+
312
+ verify(pdfBlob: Blob, options?: {
313
+ checkTimestamp?: boolean;
314
+ }): Promise<{
315
+ payload: {
316
+ valid: boolean;
317
+ signerName: string;
318
+ signerOrganization: string;
319
+ signedAt: string;
320
+ signatureValid: boolean;
321
+ integrityValid: boolean;
322
+ certificateValid: boolean;
323
+ chainValid: boolean;
324
+ trusted: boolean;
325
+ timestamped: boolean;
326
+ timestampValid: boolean;
327
+ errors: string[];
328
+ warnings: string[];
329
+ };
330
+ }>;
331
+ };
332
+ }
333
+ }
334
+ ```
335
+
336
+ Save this as `easysign.d.ts` in your project.
337
+
338
+ ---
339
+
340
+ ## Browser Compatibility
341
+
342
+ | Browser | Minimum Version | Notes |
343
+ |---------|-----------------|-------|
344
+ | Chrome | 88+ | Full support |
345
+ | Edge | 88+ | Chromium-based, full support |
346
+ | Firefox | 78+ | Full support |
347
+ | Safari | Not supported | No native messaging API |
348
+ | Opera | 74+ | Chromium-based, should work |
349
+
350
+ ---
351
+
352
+ ## Security Considerations
353
+
354
+ 1. **HTTPS Only**: The extension only works on HTTPS sites (except localhost)
355
+ 2. **PIN Security**: PINs are never stored, only passed directly to the native host
356
+ 3. **Origin Validation**: Only whitelisted origins can use the API
357
+ 4. **No Key Export**: Private keys never leave the hardware token
358
+
359
+ ---
360
+
361
+ ## Rate Limiting
362
+
363
+ There are no rate limits on the API, but:
364
+ - Each `sign()` call requires user interaction (PIN entry)
365
+ - Native host operations are sequential (one at a time)
366
+ - Signing large PDFs may take several seconds