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
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+ # backtick_javascript: true
3
+
4
+ require "native"
5
+ require "promise"
6
+ require "easy_sign/messaging"
7
+
8
+ # Injected page script that exposes window.EasySign API
9
+ # This runs in the page context, not the extension context
10
+ module EasySign
11
+ class API
12
+ DEFAULT_TIMEOUT = 120_000 # 2 minutes
13
+
14
+ def initialize
15
+ @pending_requests = {}
16
+ setup_message_listener
17
+ end
18
+
19
+ # Check if EasySign extension is available and token is connected
20
+ # @return [Promise] Resolves with availability info
21
+ def is_available
22
+ Promise.new do |resolve, reject|
23
+ request = create_request(Messaging::Types::AVAILABILITY_REQUEST, {})
24
+ send_request(request, resolve, reject, 10_000) # 10 second timeout for availability check
25
+ end
26
+ end
27
+
28
+ # Sign a PDF document
29
+ # @param pdf_blob [Blob] The PDF file as a Blob
30
+ # @param options [Hash] Signing options
31
+ # @option options [String] :reason Reason for signing
32
+ # @option options [String] :location Location of signing
33
+ # @option options [Boolean] :visible_signature Add visible signature annotation
34
+ # @option options [String] :signature_position Position (top_left, top_right, bottom_left, bottom_right)
35
+ # @option options [Integer] :signature_page Page number for signature
36
+ # @option options [Boolean] :timestamp Add RFC 3161 timestamp
37
+ # @return [Promise] Resolves with signed PDF Blob
38
+ def sign(pdf_blob, options = {})
39
+ Promise.new do |resolve, reject|
40
+ # Convert Blob to Base64
41
+ blob_to_base64(pdf_blob).then do |base64_data|
42
+ request = create_request(
43
+ Messaging::Types::SIGN_REQUEST,
44
+ { pdf_data: base64_data, options: normalize_options(options) }
45
+ )
46
+
47
+ # Wrap resolve to convert Base64 back to Blob
48
+ wrapped_resolve = ->(response) {
49
+ if response[:payload] && response[:payload][:signedPdfData]
50
+ base64_to_blob(response[:payload][:signedPdfData], "application/pdf").then do |blob|
51
+ resolve.call({
52
+ blob: blob,
53
+ signer_name: response[:payload][:signerName],
54
+ signed_at: response[:payload][:signedAt],
55
+ timestamped: response[:payload][:timestamped]
56
+ })
57
+ end
58
+ else
59
+ resolve.call(response)
60
+ end
61
+ }
62
+
63
+ send_request(request, wrapped_resolve, reject)
64
+ end.fail do |error|
65
+ reject.call(error)
66
+ end
67
+ end
68
+ end
69
+
70
+ # Verify a signed PDF document
71
+ # @param pdf_blob [Blob] The signed PDF file as a Blob
72
+ # @param options [Hash] Verification options
73
+ # @option options [Boolean] :check_timestamp Verify timestamp (default: true)
74
+ # @return [Promise] Resolves with verification result
75
+ def verify(pdf_blob, options = {})
76
+ Promise.new do |resolve, reject|
77
+ blob_to_base64(pdf_blob).then do |base64_data|
78
+ request = create_request(
79
+ Messaging::Types::VERIFY_REQUEST,
80
+ {
81
+ pdf_data: base64_data,
82
+ check_timestamp: options[:check_timestamp] != false
83
+ }
84
+ )
85
+
86
+ send_request(request, resolve, reject)
87
+ end.fail do |error|
88
+ reject.call(error)
89
+ end
90
+ end
91
+ end
92
+
93
+ # Cancel an ongoing signing operation
94
+ # @param request_id [String] The request ID to cancel
95
+ # @return [Promise]
96
+ def cancel(request_id)
97
+ Promise.new do |resolve, reject|
98
+ request = {
99
+ type: Messaging::Types::CANCEL_REQUEST,
100
+ request_id: request_id,
101
+ target: Messaging::Targets::EXTENSION
102
+ }
103
+
104
+ `window.postMessage(#{request.to_n}, '*')`
105
+ resolve.call({ cancelled: true })
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def setup_message_listener
112
+ `window.addEventListener('message', (event) => {
113
+ if (event.source !== window) return;
114
+ if (!event.data || event.data.target !== #{Messaging::Targets::PAGE.to_n}) return;
115
+
116
+ #{handle_response(`event.data`)}
117
+ })`
118
+ end
119
+
120
+ def handle_response(data)
121
+ request_id = data[:request_id] || `data.requestId` || `data.request_id`
122
+ callbacks = @pending_requests.delete(request_id)
123
+
124
+ return unless callbacks
125
+
126
+ resolve = callbacks[:resolve]
127
+ reject = callbacks[:reject]
128
+
129
+ # Clear timeout
130
+ `clearTimeout(#{callbacks[:timeout_id]})` if callbacks[:timeout_id]
131
+
132
+ error = data[:error] || `data.error`
133
+ if error
134
+ error_obj = `new Error(#{error[:message] || `error.message` || 'Unknown error'})`
135
+ `#{error_obj}.code = #{error[:code] || `error.code`}`
136
+ reject.call(error_obj)
137
+ else
138
+ resolve.call(data)
139
+ end
140
+ end
141
+
142
+ def create_request(type, payload)
143
+ {
144
+ type: type,
145
+ request_id: generate_request_id,
146
+ payload: payload,
147
+ target: Messaging::Targets::EXTENSION
148
+ }
149
+ end
150
+
151
+ def send_request(request, resolve, reject, timeout = DEFAULT_TIMEOUT)
152
+ request_id = request[:request_id]
153
+
154
+ # Set up timeout
155
+ timeout_id = `setTimeout(() => {
156
+ #{handle_timeout(request_id)}
157
+ }, #{timeout})`
158
+
159
+ @pending_requests[request_id] = {
160
+ resolve: resolve,
161
+ reject: reject,
162
+ timeout_id: timeout_id
163
+ }
164
+
165
+ `window.postMessage(#{request.to_n}, '*')`
166
+ end
167
+
168
+ def handle_timeout(request_id)
169
+ callbacks = @pending_requests.delete(request_id)
170
+ return unless callbacks
171
+
172
+ error = `new Error('Operation timed out')`
173
+ `#{error}.code = 'TIMEOUT'`
174
+ callbacks[:reject].call(error)
175
+ end
176
+
177
+ def generate_request_id
178
+ `crypto.randomUUID ? crypto.randomUUID() :
179
+ Date.now().toString(36) + Math.random().toString(36).substr(2)`
180
+ end
181
+
182
+ def normalize_options(options)
183
+ # Convert Ruby-style options to camelCase for native host
184
+ {
185
+ reason: options[:reason],
186
+ location: options[:location],
187
+ visibleSignature: options[:visible_signature],
188
+ signaturePosition: options[:signature_position],
189
+ signaturePage: options[:signature_page],
190
+ timestamp: options[:timestamp],
191
+ timestampAuthority: options[:timestamp_authority]
192
+ }.compact
193
+ end
194
+
195
+ def blob_to_base64(blob)
196
+ Promise.new do |resolve, reject|
197
+ `const reader = new FileReader();
198
+ reader.onload = function() {
199
+ // Remove data URL prefix to get just base64
200
+ const base64 = reader.result.split(',')[1];
201
+ #{resolve.call(`base64`)}
202
+ };
203
+ reader.onerror = function() {
204
+ #{reject.call(`reader.error`)}
205
+ };
206
+ reader.readAsDataURL(#{blob});`
207
+ end
208
+ end
209
+
210
+ def base64_to_blob(base64, mime_type)
211
+ Promise.new do |resolve, _reject|
212
+ `const byteCharacters = atob(#{base64});
213
+ const byteNumbers = new Array(byteCharacters.length);
214
+ for (let i = 0; i < byteCharacters.length; i++) {
215
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
216
+ }
217
+ const byteArray = new Uint8Array(byteNumbers);
218
+ const blob = new Blob([byteArray], { type: #{mime_type} });
219
+ #{resolve.call(`blob`)};`
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ # Expose to window object
226
+ `window.EasySign = #{EasySign::API.new.to_n}`
227
+
228
+ # Also expose individual methods directly for convenience
229
+ `window.EasySign.sign = function(blob, options) {
230
+ return #{EasySign::API.new}.sign(blob, options || {});
231
+ };
232
+ window.EasySign.verify = function(blob, options) {
233
+ return #{EasySign::API.new}.verify(blob, options || {});
234
+ };
235
+ window.EasySign.isAvailable = function() {
236
+ return #{EasySign::API.new}.is_available();
237
+ };`
238
+
239
+ puts "EasySign API injected into page"
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+ # backtick_javascript: true
3
+
4
+ # Message protocol constants and helpers for EasySign browser extension
5
+ module EasySign
6
+ module Messaging
7
+ # Message types
8
+ module Types
9
+ # Requests from page/content script to background
10
+ SIGN_REQUEST = "sign_request"
11
+ VERIFY_REQUEST = "verify_request"
12
+ AVAILABILITY_REQUEST = "availability_request"
13
+ CANCEL_REQUEST = "cancel_request"
14
+
15
+ # Responses from background to page/content script
16
+ SIGN_RESPONSE = "sign_response"
17
+ VERIFY_RESPONSE = "verify_response"
18
+ AVAILABILITY_RESPONSE = "availability_response"
19
+ ERROR_RESPONSE = "error"
20
+
21
+ # Internal extension messages
22
+ PIN_SUBMITTED = "pin_submitted"
23
+ PIN_CANCELLED = "pin_cancelled"
24
+ POPUP_READY = "popup_ready"
25
+ end
26
+
27
+ # Error codes matching native host protocol
28
+ module ErrorCodes
29
+ TOKEN_NOT_FOUND = "TOKEN_NOT_FOUND"
30
+ PIN_INCORRECT = "PIN_INCORRECT"
31
+ TOKEN_LOCKED = "TOKEN_LOCKED"
32
+ INVALID_PDF = "INVALID_PDF"
33
+ SIGNING_FAILED = "SIGNING_FAILED"
34
+ VERIFICATION_FAILED = "VERIFICATION_FAILED"
35
+ NATIVE_HOST_NOT_FOUND = "NATIVE_HOST_NOT_FOUND"
36
+ TIMEOUT = "TIMEOUT"
37
+ CANCELLED = "CANCELLED"
38
+ ORIGIN_NOT_ALLOWED = "ORIGIN_NOT_ALLOWED"
39
+ INTERNAL_ERROR = "INTERNAL_ERROR"
40
+ end
41
+
42
+ # Message targets for postMessage routing
43
+ module Targets
44
+ EXTENSION = "easy-sign-extension"
45
+ PAGE = "easy-sign-page"
46
+ end
47
+
48
+ # Default timeout for signing operations (ms)
49
+ DEFAULT_TIMEOUT = 120_000 # 2 minutes
50
+
51
+ # Generate unique request ID
52
+ def self.generate_request_id
53
+ # Use crypto.randomUUID if available, fallback to timestamp + random
54
+ `typeof crypto !== 'undefined' && crypto.randomUUID ?
55
+ crypto.randomUUID() :
56
+ Date.now().toString(36) + Math.random().toString(36).substr(2)`
57
+ end
58
+
59
+ # Create a request message
60
+ def self.create_request(type, payload = {})
61
+ {
62
+ type: type,
63
+ request_id: generate_request_id,
64
+ payload: payload,
65
+ timestamp: `Date.now()`
66
+ }
67
+ end
68
+
69
+ # Create a response message
70
+ def self.create_response(request_id, type, payload = nil, error = nil)
71
+ {
72
+ type: type,
73
+ request_id: request_id,
74
+ payload: payload,
75
+ error: error,
76
+ timestamp: `Date.now()`
77
+ }
78
+ end
79
+
80
+ # Create an error response
81
+ def self.create_error(request_id, code, message, details = nil)
82
+ create_response(
83
+ request_id,
84
+ Types::ERROR_RESPONSE,
85
+ nil,
86
+ { code: code, message: message, details: details }
87
+ )
88
+ end
89
+
90
+ # Human-readable error messages
91
+ ERROR_MESSAGES = {
92
+ ErrorCodes::TOKEN_NOT_FOUND => "Hardware token not found. Please connect your token.",
93
+ ErrorCodes::PIN_INCORRECT => "Incorrect PIN entered.",
94
+ ErrorCodes::TOKEN_LOCKED => "Token is locked. Please contact your administrator.",
95
+ ErrorCodes::INVALID_PDF => "The PDF file is invalid or corrupted.",
96
+ ErrorCodes::SIGNING_FAILED => "Failed to sign the document.",
97
+ ErrorCodes::VERIFICATION_FAILED => "Failed to verify the signature.",
98
+ ErrorCodes::NATIVE_HOST_NOT_FOUND => "EasySign native host not installed.",
99
+ ErrorCodes::TIMEOUT => "Operation timed out.",
100
+ ErrorCodes::CANCELLED => "Operation was cancelled.",
101
+ ErrorCodes::ORIGIN_NOT_ALLOWED => "This website is not allowed to use EasySign.",
102
+ ErrorCodes::INTERNAL_ERROR => "An internal error occurred."
103
+ }.freeze
104
+
105
+ def self.error_message(code)
106
+ ERROR_MESSAGES[code] || "Unknown error: #{code}"
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+ # backtick_javascript: true
3
+
4
+ require "native"
5
+ require "easy_sign/messaging"
6
+
7
+ # Popup controller for PIN entry
8
+ module EasySign
9
+ class Popup
10
+ def initialize
11
+ @port = nil
12
+ @signing_context = nil
13
+ setup_ui
14
+ connect_to_background
15
+ end
16
+
17
+ def setup_ui
18
+ # Wait for DOM to be ready
19
+ `document.addEventListener('DOMContentLoaded', () => {
20
+ #{init_ui}
21
+ })`
22
+ end
23
+
24
+ def init_ui
25
+ # Get UI elements
26
+ @pin_input = `document.getElementById('pin-input')`
27
+ @submit_btn = `document.getElementById('submit-btn')`
28
+ @cancel_btn = `document.getElementById('cancel-btn')`
29
+ @status_text = `document.getElementById('status-text')`
30
+ @origin_text = `document.getElementById('origin-text')`
31
+ @error_text = `document.getElementById('error-text')`
32
+
33
+ setup_event_listeners
34
+ focus_pin_input
35
+ end
36
+
37
+ def setup_event_listeners
38
+ # Submit button click
39
+ `#{@submit_btn}.addEventListener('click', () => {
40
+ #{submit_pin}
41
+ })`
42
+
43
+ # Cancel button click
44
+ `#{@cancel_btn}.addEventListener('click', () => {
45
+ #{cancel_signing}
46
+ })`
47
+
48
+ # Enter key in PIN input
49
+ `#{@pin_input}.addEventListener('keypress', (e) => {
50
+ if (e.key === 'Enter') {
51
+ #{submit_pin}
52
+ }
53
+ })`
54
+
55
+ # Clear error on input
56
+ `#{@pin_input}.addEventListener('input', () => {
57
+ #{clear_error}
58
+ })`
59
+ end
60
+
61
+ def connect_to_background
62
+ @port = `chrome.runtime.connect({ name: 'popup' })`
63
+
64
+ `#{@port}.onMessage.addListener((message) => {
65
+ #{handle_background_message(`message`)}
66
+ })`
67
+
68
+ `#{@port}.onDisconnect.addListener(() => {
69
+ #{handle_disconnect}
70
+ })`
71
+
72
+ # Notify background that popup is ready
73
+ send_to_background({ type: Messaging::Types::POPUP_READY })
74
+ end
75
+
76
+ def handle_background_message(message)
77
+ type = message[:type] || `message.type`
78
+
79
+ case type
80
+ when "signing_context"
81
+ handle_signing_context(message)
82
+ when "error"
83
+ show_error(message[:error] || `message.error`)
84
+ when "pin_incorrect"
85
+ show_error("Incorrect PIN. Please try again.")
86
+ clear_pin
87
+ focus_pin_input
88
+ end
89
+ end
90
+
91
+ def handle_signing_context(context)
92
+ @signing_context = context
93
+ origin = context[:origin] || `context.origin`
94
+ options = context[:options] || `context.options` || {}
95
+
96
+ # Update UI with signing context
97
+ if origin && @origin_text
98
+ `#{@origin_text}.textContent = #{origin}`
99
+ end
100
+
101
+ # Show what will be signed
102
+ if options[:reason] && @status_text
103
+ `#{@status_text}.textContent = 'Reason: ' + #{options[:reason]}`
104
+ end
105
+ end
106
+
107
+ def handle_disconnect
108
+ # Background disconnected, close popup
109
+ `window.close()`
110
+ end
111
+
112
+ def submit_pin
113
+ pin = `#{@pin_input}.value`
114
+
115
+ # Validate PIN
116
+ if pin.nil? || `#{pin}.length === 0`
117
+ show_error("Please enter your PIN")
118
+ return
119
+ end
120
+
121
+ if `#{pin}.length < 4`
122
+ show_error("PIN must be at least 4 characters")
123
+ return
124
+ end
125
+
126
+ # Disable UI while processing
127
+ disable_ui
128
+ show_status("Signing document...")
129
+
130
+ # Send PIN to background
131
+ send_to_background({
132
+ type: Messaging::Types::PIN_SUBMITTED,
133
+ pin: pin
134
+ })
135
+
136
+ # Clear PIN from memory
137
+ clear_pin
138
+ end
139
+
140
+ def cancel_signing
141
+ send_to_background({ type: Messaging::Types::PIN_CANCELLED })
142
+ `window.close()`
143
+ end
144
+
145
+ def send_to_background(message)
146
+ return unless @port
147
+
148
+ `#{@port}.postMessage(#{message.to_n})`
149
+ end
150
+
151
+ def show_error(message)
152
+ return unless @error_text
153
+
154
+ `#{@error_text}.textContent = #{message}`
155
+ `#{@error_text}.style.display = 'block'`
156
+ enable_ui
157
+ end
158
+
159
+ def clear_error
160
+ return unless @error_text
161
+
162
+ `#{@error_text}.textContent = ''`
163
+ `#{@error_text}.style.display = 'none'`
164
+ end
165
+
166
+ def show_status(message)
167
+ return unless @status_text
168
+
169
+ `#{@status_text}.textContent = #{message}`
170
+ end
171
+
172
+ def clear_pin
173
+ return unless @pin_input
174
+
175
+ `#{@pin_input}.value = ''`
176
+ end
177
+
178
+ def focus_pin_input
179
+ return unless @pin_input
180
+
181
+ `#{@pin_input}.focus()`
182
+ end
183
+
184
+ def disable_ui
185
+ `#{@pin_input}.disabled = true` if @pin_input
186
+ `#{@submit_btn}.disabled = true` if @submit_btn
187
+ `#{@cancel_btn}.disabled = true` if @cancel_btn
188
+ end
189
+
190
+ def enable_ui
191
+ `#{@pin_input}.disabled = false` if @pin_input
192
+ `#{@submit_btn}.disabled = false` if @submit_btn
193
+ `#{@cancel_btn}.disabled = false` if @cancel_btn
194
+ focus_pin_input
195
+ end
196
+ end
197
+ end
198
+
199
+ # Initialize popup
200
+ EasySign::Popup.new
@@ -0,0 +1,58 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "EasySign PDF Signer",
4
+ "version": "1.0.0",
5
+ "description": "Sign PDF documents using hardware security tokens (HSM/smart cards)",
6
+
7
+ "permissions": [
8
+ "nativeMessaging",
9
+ "storage"
10
+ ],
11
+
12
+ "host_permissions": [
13
+ "https://*/*",
14
+ "http://localhost/*"
15
+ ],
16
+
17
+ "background": {
18
+ "service_worker": "background.js",
19
+ "type": "module"
20
+ },
21
+
22
+ "content_scripts": [
23
+ {
24
+ "matches": ["https://*/*", "http://localhost/*"],
25
+ "js": ["content.js"],
26
+ "run_at": "document_start",
27
+ "all_frames": false
28
+ }
29
+ ],
30
+
31
+ "web_accessible_resources": [
32
+ {
33
+ "resources": ["inject.js"],
34
+ "matches": ["https://*/*", "http://localhost/*"]
35
+ }
36
+ ],
37
+
38
+ "action": {
39
+ "default_popup": "popup/popup.html",
40
+ "default_icon": {
41
+ "16": "icons/icon16.png",
42
+ "48": "icons/icon48.png",
43
+ "128": "icons/icon128.png"
44
+ },
45
+ "default_title": "EasySign PDF Signer"
46
+ },
47
+
48
+ "icons": {
49
+ "16": "icons/icon16.png",
50
+ "48": "icons/icon48.png",
51
+ "128": "icons/icon128.png"
52
+ },
53
+
54
+ "options_ui": {
55
+ "page": "popup/popup.html",
56
+ "open_in_tab": false
57
+ }
58
+ }