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,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "easy_code_sign/version"
4
+ require_relative "easy_code_sign/errors"
5
+ require_relative "easy_code_sign/configuration"
6
+ require_relative "easy_code_sign/providers/base"
7
+ require_relative "easy_code_sign/providers/pkcs11_base"
8
+ require_relative "easy_code_sign/providers/safenet"
9
+ require_relative "easy_code_sign/signable/base"
10
+ require_relative "easy_code_sign/signable/gem_file"
11
+ require_relative "easy_code_sign/signable/zip_file"
12
+ require_relative "easy_code_sign/signable/pdf_file"
13
+ require_relative "easy_code_sign/pdf/timestamp_handler"
14
+ require_relative "easy_code_sign/pdf/appearance_builder"
15
+ require_relative "easy_code_sign/timestamp/request"
16
+ require_relative "easy_code_sign/timestamp/response"
17
+ require_relative "easy_code_sign/timestamp/client"
18
+ require_relative "easy_code_sign/timestamp/verifier"
19
+ require_relative "easy_code_sign/verification/result"
20
+ require_relative "easy_code_sign/verification/trust_store"
21
+ require_relative "easy_code_sign/verification/certificate_chain"
22
+ require_relative "easy_code_sign/verification/signature_checker"
23
+ require_relative "easy_code_sign/deferred_signing_request"
24
+ require_relative "easy_code_sign/signer"
25
+ require_relative "easy_code_sign/verifier"
26
+
27
+ module EasyCodeSign
28
+ class << self
29
+ # Get the current configuration
30
+ # @return [Configuration]
31
+ def configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ # Configure EasyCodeSign
36
+ # @yield [Configuration] the configuration object
37
+ # @return [Configuration]
38
+ def configure
39
+ yield(configuration)
40
+ configuration
41
+ end
42
+
43
+ # Reset configuration to defaults
44
+ # @return [Configuration]
45
+ def reset_configuration!
46
+ @configuration = Configuration.new
47
+ end
48
+
49
+ # Get a provider instance based on current configuration
50
+ # @return [Providers::Base] a provider instance
51
+ def provider
52
+ @provider ||= build_provider
53
+ end
54
+
55
+ # Reset the cached provider instance
56
+ # @return [void]
57
+ def reset_provider!
58
+ @provider = nil
59
+ end
60
+
61
+ # Sign a file using the configured provider
62
+ #
63
+ # @param file_path [String] path to the file to sign
64
+ # @param pin [String, nil] PIN for the token (uses callback if nil)
65
+ # @param output_path [String, nil] output path for signed file
66
+ # @param timestamp [Boolean] whether to add timestamp (default: from config)
67
+ # @param algorithm [Symbol] signature algorithm (default: :sha256_rsa)
68
+ # @return [SigningResult] signing result with file path and metadata
69
+ #
70
+ # @example Sign a gem
71
+ # EasyCodeSign.sign("my_gem-1.0.0.gem", pin: "1234")
72
+ #
73
+ # @example Sign with timestamp
74
+ # EasyCodeSign.sign("archive.zip", pin: "1234", timestamp: true)
75
+ #
76
+ def sign(file_path, pin: nil, output_path: nil, timestamp: nil, algorithm: :sha256_rsa)
77
+ signer = Signer.new
78
+ signer.sign(file_path, pin: pin, output_path: output_path, timestamp: timestamp, algorithm: algorithm)
79
+ end
80
+
81
+ # Verify a signed file
82
+ #
83
+ # @param file_path [String] path to the signed file
84
+ # @param check_timestamp [Boolean] whether to verify timestamp (default: true)
85
+ # @param trust_store [Verification::TrustStore, nil] custom trust store
86
+ # @return [Verification::Result] verification result
87
+ #
88
+ # @example Verify a signed gem
89
+ # result = EasyCodeSign.verify("signed.gem")
90
+ # puts result.valid? ? "Valid!" : result.errors.join(", ")
91
+ #
92
+ def verify(file_path, check_timestamp: true, trust_store: nil)
93
+ verifier = Verifier.new(trust_store: trust_store)
94
+ verifier.verify(file_path, check_timestamp: check_timestamp)
95
+ end
96
+
97
+ # Phase 1 of deferred PDF signing.
98
+ # Prepares a PDF with placeholder signature and returns a DeferredSigningRequest
99
+ # containing the digest to be signed by an external signer (Fortify, WebCrypto, etc.).
100
+ #
101
+ # @param file_path [String] path to the PDF
102
+ # @param pin [String, nil] PIN for hardware token (needed for certificate retrieval)
103
+ # @param digest_algorithm [String] "sha256", "sha384", or "sha512"
104
+ # @param timestamp [Boolean] whether to reserve timestamp space
105
+ # @return [DeferredSigningRequest]
106
+ #
107
+ # @example
108
+ # request = EasyCodeSign.prepare_pdf("document.pdf", pin: "1234")
109
+ # request.digest_base64 #=> "abc123..." (send to external signer)
110
+ #
111
+ def prepare_pdf(file_path, pin: nil, digest_algorithm: "sha256", timestamp: false, **extra_options)
112
+ signer = Signer.new
113
+ signer.prepare_pdf(file_path, pin: pin, digest_algorithm: digest_algorithm,
114
+ timestamp: timestamp, **extra_options)
115
+ end
116
+
117
+ # Phase 2 of deferred PDF signing.
118
+ # Embeds an externally-produced raw signature into the prepared PDF.
119
+ #
120
+ # @param deferred_request [DeferredSigningRequest] from prepare_pdf
121
+ # @param raw_signature [String] raw signature bytes from external signer
122
+ # @return [SigningResult]
123
+ #
124
+ # @example
125
+ # result = EasyCodeSign.finalize_pdf(request, raw_signature)
126
+ # result.file_path #=> "document_prepared.pdf"
127
+ #
128
+ def finalize_pdf(deferred_request, raw_signature, **options)
129
+ signer = Signer.new
130
+ signer.finalize_pdf(deferred_request, raw_signature, **options)
131
+ end
132
+
133
+ # Create a verifier instance for batch operations
134
+ # @param trust_store [Verification::TrustStore, nil]
135
+ # @return [Verifier]
136
+ def verifier(trust_store: nil)
137
+ Verifier.new(trust_store: trust_store)
138
+ end
139
+
140
+ # List available token slots
141
+ # @return [Array<Hash>] array of slot information
142
+ def list_slots
143
+ provider.list_slots
144
+ end
145
+
146
+ # Create a signer instance for batch operations
147
+ # @return [Signer]
148
+ def signer
149
+ Signer.new
150
+ end
151
+
152
+ # Detect file type and return appropriate signable handler
153
+ # @param file_path [String] path to file
154
+ # @return [Signable::Base] signable handler
155
+ def signable_for(file_path, **options)
156
+ extension = File.extname(file_path).downcase
157
+
158
+ case extension
159
+ when ".gem"
160
+ Signable::GemFile.new(file_path, **options)
161
+ when ".zip", ".jar", ".apk", ".war", ".ear"
162
+ Signable::ZipFile.new(file_path, **options)
163
+ when ".pdf"
164
+ Signable::PdfFile.new(file_path, **options)
165
+ else
166
+ raise InvalidFileError, "Unsupported file type: #{extension}"
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ def build_provider
173
+ configuration.validate!
174
+
175
+ case configuration.provider
176
+ when :safenet
177
+ Providers::Safenet.new(configuration)
178
+ else
179
+ raise ConfigurationError, "Unknown provider: #{configuration.provider}"
180
+ end
181
+ end
182
+ end
183
+ end
data/plugin/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ # Built extension output
2
+ dist/
3
+
4
+ # Ruby dependencies
5
+ .bundle/
6
+ vendor/bundle/
7
+
8
+ # Logs
9
+ *.log
10
+
11
+ # OS files
12
+ .DS_Store
13
+ Thumbs.db
14
+
15
+ # Editor files
16
+ *.swp
17
+ *.swo
18
+ *~
19
+
20
+ # Native host packaged binaries
21
+ native_host/dist/
data/plugin/Gemfile ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Opal - Ruby to JavaScript compiler
6
+ gem "opal", "~> 1.8"
7
+ gem "opal-sprockets", "~> 1.0"
8
+
9
+ # Required for Ruby 3.4+
10
+ gem "base64"
11
+
12
+ # Build tooling
13
+ gem "rake", "~> 13.0"
14
+
15
+ # For watching file changes during development
16
+ gem "listen", "~> 3.8"
17
+
18
+ # Testing - use minitest (native host tests are plain Ruby)
19
+ gem "minitest", "~> 5.0"
20
+
21
+ group :development do
22
+ # Code quality
23
+ gem "rubocop", require: false
24
+ end
@@ -0,0 +1,134 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ ast (2.4.3)
5
+ base64 (0.3.0)
6
+ concurrent-ruby (1.3.6)
7
+ ffi (1.17.3)
8
+ ffi (1.17.3-aarch64-linux-gnu)
9
+ ffi (1.17.3-aarch64-linux-musl)
10
+ ffi (1.17.3-arm-linux-gnu)
11
+ ffi (1.17.3-arm-linux-musl)
12
+ ffi (1.17.3-arm64-darwin)
13
+ ffi (1.17.3-x86-linux-gnu)
14
+ ffi (1.17.3-x86-linux-musl)
15
+ ffi (1.17.3-x86_64-darwin)
16
+ ffi (1.17.3-x86_64-linux-gnu)
17
+ ffi (1.17.3-x86_64-linux-musl)
18
+ json (2.18.0)
19
+ language_server-protocol (3.17.0.5)
20
+ lint_roller (1.1.0)
21
+ listen (3.9.0)
22
+ rb-fsevent (~> 0.10, >= 0.10.3)
23
+ rb-inotify (~> 0.9, >= 0.9.10)
24
+ logger (1.7.0)
25
+ minitest (5.27.0)
26
+ opal (1.8.2)
27
+ ast (>= 2.3.0)
28
+ parser (~> 3.0, >= 3.0.3.2)
29
+ opal-sprockets (1.0.4)
30
+ opal (>= 1.0, < 2.0)
31
+ sprockets (~> 4.0)
32
+ tilt (>= 1.4)
33
+ parallel (1.27.0)
34
+ parser (3.3.10.0)
35
+ ast (~> 2.4.1)
36
+ racc
37
+ prism (1.7.0)
38
+ racc (1.8.1)
39
+ rack (3.2.4)
40
+ rainbow (3.1.1)
41
+ rake (13.3.1)
42
+ rb-fsevent (0.11.2)
43
+ rb-inotify (0.11.1)
44
+ ffi (~> 1.0)
45
+ regexp_parser (2.11.3)
46
+ rubocop (1.82.1)
47
+ json (~> 2.3)
48
+ language_server-protocol (~> 3.17.0.2)
49
+ lint_roller (~> 1.1.0)
50
+ parallel (~> 1.10)
51
+ parser (>= 3.3.0.2)
52
+ rainbow (>= 2.2.2, < 4.0)
53
+ regexp_parser (>= 2.9.3, < 3.0)
54
+ rubocop-ast (>= 1.48.0, < 2.0)
55
+ ruby-progressbar (~> 1.7)
56
+ unicode-display_width (>= 2.4.0, < 4.0)
57
+ rubocop-ast (1.49.0)
58
+ parser (>= 3.3.7.2)
59
+ prism (~> 1.7)
60
+ ruby-progressbar (1.13.0)
61
+ sprockets (4.2.2)
62
+ concurrent-ruby (~> 1.0)
63
+ logger
64
+ rack (>= 2.2.4, < 4)
65
+ tilt (2.6.1)
66
+ unicode-display_width (3.2.0)
67
+ unicode-emoji (~> 4.1)
68
+ unicode-emoji (4.2.0)
69
+
70
+ PLATFORMS
71
+ aarch64-linux-gnu
72
+ aarch64-linux-musl
73
+ arm-linux-gnu
74
+ arm-linux-musl
75
+ arm64-darwin
76
+ ruby
77
+ x86-linux-gnu
78
+ x86-linux-musl
79
+ x86_64-darwin
80
+ x86_64-linux-gnu
81
+ x86_64-linux-musl
82
+
83
+ DEPENDENCIES
84
+ base64
85
+ listen (~> 3.8)
86
+ minitest (~> 5.0)
87
+ opal (~> 1.8)
88
+ opal-sprockets (~> 1.0)
89
+ rake (~> 13.0)
90
+ rubocop
91
+
92
+ CHECKSUMS
93
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
94
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
95
+ concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
96
+ ffi (1.17.3) sha256=0e9f39f7bb3934f77ad6feab49662be77e87eedcdeb2a3f5c0234c2938563d4c
97
+ ffi (1.17.3-aarch64-linux-gnu) sha256=28ad573df26560f0aedd8a90c3371279a0b2bd0b4e834b16a2baa10bd7a97068
98
+ ffi (1.17.3-aarch64-linux-musl) sha256=020b33b76775b1abacc3b7d86b287cef3251f66d747092deec592c7f5df764b2
99
+ ffi (1.17.3-arm-linux-gnu) sha256=5bd4cea83b68b5ec0037f99c57d5ce2dd5aa438f35decc5ef68a7d085c785668
100
+ ffi (1.17.3-arm-linux-musl) sha256=0d7626bb96265f9af78afa33e267d71cfef9d9a8eb8f5525344f8da6c7d76053
101
+ ffi (1.17.3-arm64-darwin) sha256=0c690555d4cee17a7f07c04d59df39b2fba74ec440b19da1f685c6579bb0717f
102
+ ffi (1.17.3-x86-linux-gnu) sha256=868a88fcaf5186c3a46b7c7c2b2c34550e1e61a405670ab23f5b6c9971529089
103
+ ffi (1.17.3-x86-linux-musl) sha256=f0286aa6ef40605cf586e61406c446de34397b85dbb08cc99fdaddaef8343945
104
+ ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5
105
+ ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
106
+ ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
107
+ json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
108
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
109
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
110
+ listen (3.9.0) sha256=db9e4424e0e5834480385197c139cb6b0ae0ef28cc13310cfd1ca78377d59c67
111
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
112
+ minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
113
+ opal (1.8.2) sha256=cedde4e1691d7cdaaf0aadac99fbf1fdb5bb90fe2dcc60e968f984601bd5d4e3
114
+ opal-sprockets (1.0.4) sha256=ba247b54c1cea23a4858f70a8851322c170436ed904b2c9c9d76d0b07e25ccfc
115
+ parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
116
+ parser (3.3.10.0) sha256=ce3587fa5cc55a88c4ba5b2b37621b3329aadf5728f9eafa36bbd121462aabd6
117
+ prism (1.7.0) sha256=10062f734bf7985c8424c44fac382ac04a58124ea3d220ec3ba9fe4f2da65103
118
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
119
+ rack (3.2.4) sha256=5d74b6f75082a643f43c1e76b419c40f0e5527fcfee1e669ac1e6b73c0ccb6f6
120
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
121
+ rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
122
+ rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
123
+ rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
124
+ regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
125
+ rubocop (1.82.1) sha256=09f1a6a654a960eda767aebea33e47603080f8e9c9a3f019bf9b94c9cab5e273
126
+ rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
127
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
128
+ sprockets (4.2.2) sha256=761e5a49f1c288704763f73139763564c845a8f856d52fba013458f8af1b59b1
129
+ tilt (2.6.1) sha256=35a99bba2adf7c1e362f5b48f9b581cce4edfba98117e34696dde6d308d84770
130
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
131
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
132
+
133
+ BUNDLED WITH
134
+ 4.0.3
data/plugin/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # EasySign Browser Plugin
2
+
3
+ Browser extension for signing PDF documents using hardware security tokens (HSM/smart cards).
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────┐
9
+ │ Web App (Rails, etc.) │
10
+ │ window.EasySign.sign(pdfBlob, options) │
11
+ └─────────────────┬───────────────────────────────────┘
12
+ │ postMessage
13
+ ┌─────────────────▼───────────────────────────────────┐
14
+ │ Content Script (Opal.rb → JS) │
15
+ │ Bridges page ↔ extension, validates origins │
16
+ └─────────────────┬───────────────────────────────────┘
17
+ │ chrome.runtime.sendMessage
18
+ ┌─────────────────▼───────────────────────────────────┐
19
+ │ Background Service Worker (Opal.rb → JS) │
20
+ │ Opens PIN popup, manages native connection │
21
+ └─────────────────┬───────────────────────────────────┘
22
+ │ Native Messaging (JSON over stdio)
23
+ ┌─────────────────▼───────────────────────────────────┐
24
+ │ Native Host (Ruby → Packaged Binary) │
25
+ │ Uses EasyCodeSign gem for actual signing │
26
+ │ Accesses PKCS#11 hardware token │
27
+ └─────────────────────────────────────────────────────┘
28
+ ```
29
+
30
+ ## Development Setup
31
+
32
+ ### Prerequisites
33
+
34
+ - Ruby 3.2+
35
+ - Node.js (optional, for alternative build tools)
36
+ - Chrome or Firefox browser
37
+
38
+ ### Install Dependencies
39
+
40
+ ```bash
41
+ cd plugin
42
+ bundle install
43
+ ```
44
+
45
+ ### Build Extension
46
+
47
+ ```bash
48
+ # Build all components
49
+ bundle exec rake build
50
+
51
+ # Watch for changes during development
52
+ bundle exec rake watch
53
+
54
+ # Clean build artifacts
55
+ bundle exec rake clean
56
+ ```
57
+
58
+ ### Load Extension in Browser
59
+
60
+ **Chrome:**
61
+ 1. Open `chrome://extensions`
62
+ 2. Enable "Developer mode"
63
+ 3. Click "Load unpacked"
64
+ 4. Select the `plugin/dist` directory
65
+
66
+ **Firefox:**
67
+ 1. Open `about:debugging`
68
+ 2. Click "This Firefox"
69
+ 3. Click "Load Temporary Add-on"
70
+ 4. Select `plugin/dist/manifest.json`
71
+
72
+ ### Install Native Host (Development)
73
+
74
+ ```bash
75
+ # For Chrome
76
+ ./native_host/install/install_chrome.sh
77
+
78
+ # For Firefox
79
+ ./native_host/install/install_firefox.sh
80
+ ```
81
+
82
+ ## Web App Integration
83
+
84
+ ### Check Availability
85
+
86
+ ```javascript
87
+ window.EasySign.isAvailable()
88
+ .then(result => {
89
+ console.log('Extension installed:', result.available);
90
+ console.log('Token connected:', result.tokenPresent);
91
+ console.log('Available slots:', result.slots);
92
+ })
93
+ .catch(err => {
94
+ console.error('EasySign not available:', err);
95
+ });
96
+ ```
97
+
98
+ ### Sign a PDF
99
+
100
+ ```javascript
101
+ // Get PDF as Blob (from file input, fetch, etc.)
102
+ const pdfBlob = await fetch('/document.pdf').then(r => r.blob());
103
+
104
+ // Sign the PDF
105
+ window.EasySign.sign(pdfBlob, {
106
+ reason: 'Approved',
107
+ location: 'New York',
108
+ visibleSignature: true,
109
+ signaturePosition: 'bottom_right',
110
+ timestamp: true
111
+ })
112
+ .then(result => {
113
+ console.log('Signed by:', result.signer_name);
114
+ console.log('Signed at:', result.signed_at);
115
+
116
+ // Download signed PDF
117
+ const url = URL.createObjectURL(result.blob);
118
+ const a = document.createElement('a');
119
+ a.href = url;
120
+ a.download = 'signed_document.pdf';
121
+ a.click();
122
+
123
+ // Or upload to server
124
+ const formData = new FormData();
125
+ formData.append('signed_pdf', result.blob, 'signed.pdf');
126
+ fetch('/upload', { method: 'POST', body: formData });
127
+ })
128
+ .catch(err => {
129
+ console.error('Signing failed:', err.message, err.code);
130
+ });
131
+ ```
132
+
133
+ ### Verify a Signed PDF
134
+
135
+ ```javascript
136
+ window.EasySign.verify(signedPdfBlob)
137
+ .then(result => {
138
+ if (result.payload.valid) {
139
+ console.log('Signature is valid!');
140
+ console.log('Signer:', result.payload.signerName);
141
+ } else {
142
+ console.log('Signature invalid:', result.payload.errors);
143
+ }
144
+ });
145
+ ```
146
+
147
+ ## API Reference
148
+
149
+ ### `window.EasySign.isAvailable()`
150
+
151
+ Check if extension is installed and token is connected.
152
+
153
+ **Returns:** `Promise<Object>`
154
+ - `available` (boolean): Extension is installed and native host is connected
155
+ - `tokenPresent` (boolean): Hardware token is connected
156
+ - `slots` (Array): List of available token slots
157
+
158
+ ### `window.EasySign.sign(pdfBlob, options)`
159
+
160
+ Sign a PDF document. Opens PIN entry popup.
161
+
162
+ **Parameters:**
163
+ - `pdfBlob` (Blob): The PDF file to sign
164
+ - `options` (Object):
165
+ - `reason` (string): Reason for signing
166
+ - `location` (string): Location of signing
167
+ - `visibleSignature` (boolean): Add visible signature annotation
168
+ - `signaturePosition` (string): Position (`top_left`, `top_right`, `bottom_left`, `bottom_right`)
169
+ - `signaturePage` (number): Page number for signature (default: 1)
170
+ - `timestamp` (boolean): Add RFC 3161 timestamp
171
+
172
+ **Returns:** `Promise<Object>`
173
+ - `blob` (Blob): Signed PDF file
174
+ - `signer_name` (string): Name from signing certificate
175
+ - `signed_at` (string): ISO 8601 timestamp
176
+ - `timestamped` (boolean): Whether timestamp was added
177
+
178
+ ### `window.EasySign.verify(pdfBlob, options)`
179
+
180
+ Verify a signed PDF document.
181
+
182
+ **Parameters:**
183
+ - `pdfBlob` (Blob): The signed PDF file
184
+ - `options` (Object):
185
+ - `checkTimestamp` (boolean): Verify timestamp (default: true)
186
+
187
+ **Returns:** `Promise<Object>` with verification details
188
+
189
+ ## Error Handling
190
+
191
+ ```javascript
192
+ window.EasySign.sign(pdfBlob, options)
193
+ .catch(err => {
194
+ switch (err.code) {
195
+ case 'TOKEN_NOT_FOUND':
196
+ alert('Please connect your hardware token');
197
+ break;
198
+ case 'PIN_INCORRECT':
199
+ alert('Incorrect PIN');
200
+ break;
201
+ case 'TOKEN_LOCKED':
202
+ alert('Token is locked. Contact administrator.');
203
+ break;
204
+ case 'CANCELLED':
205
+ // User cancelled - no action needed
206
+ break;
207
+ default:
208
+ alert('Signing failed: ' + err.message);
209
+ }
210
+ });
211
+ ```
212
+
213
+ ## Building for Production
214
+
215
+ ### Package Native Host
216
+
217
+ The native host can be packaged as a self-contained executable:
218
+
219
+ ```bash
220
+ cd native_host
221
+ bundle exec rake build:package
222
+ ```
223
+
224
+ This creates platform-specific binaries in `native_host/dist/`.
225
+
226
+ ### Create Installer
227
+
228
+ ```bash
229
+ # macOS
230
+ ./create_installer_macos.sh
231
+
232
+ # Windows
233
+ ./create_installer_windows.bat
234
+
235
+ # Linux
236
+ ./create_installer_linux.sh
237
+ ```
238
+
239
+ ## Security
240
+
241
+ - PIN is entered only in browser popup, never on web pages
242
+ - PIN is passed directly to native host, never stored
243
+ - Origin validation restricts which websites can use the API
244
+ - All signing happens in the native host, private keys never leave the token
245
+
246
+ ## License
247
+
248
+ MIT License - See LICENSE file