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,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ DIST_DIR = File.expand_path("../dist", __dir__)
6
+ SRC_DIR = File.expand_path("../src", __dir__)
7
+ ROOT_DIR = File.expand_path("../../..", __dir__)
8
+
9
+ desc "Build native host for current platform"
10
+ task build: :clean do
11
+ mkdir_p DIST_DIR
12
+
13
+ # For development, create a wrapper script that uses Ruby
14
+ create_dev_wrapper
15
+ end
16
+
17
+ desc "Package native host as self-contained executable"
18
+ task package: :clean do
19
+ mkdir_p DIST_DIR
20
+
21
+ # Check for ruby-packer
22
+ unless system("which rubyc > /dev/null 2>&1")
23
+ puts "ruby-packer (rubyc) not found. Install with: gem install rubyc"
24
+ puts "Falling back to development wrapper..."
25
+ create_dev_wrapper
26
+ exit 0
27
+ end
28
+
29
+ # Package with ruby-packer
30
+ platform = case RUBY_PLATFORM
31
+ when /darwin/ then "macos"
32
+ when /linux/ then "linux"
33
+ when /mingw|mswin/ then "windows"
34
+ else "unknown"
35
+ end
36
+
37
+ output_name = platform == "windows" ? "easy_sign_host.exe" : "easy_sign_host_#{platform}"
38
+ output_path = File.join(DIST_DIR, output_name)
39
+
40
+ puts "Packaging native host for #{platform}..."
41
+
42
+ # Create a temporary entry point that requires all dependencies
43
+ entry_point = File.join(SRC_DIR, "easy_sign_host.rb")
44
+
45
+ cmd = [
46
+ "rubyc",
47
+ entry_point,
48
+ "-o", output_path,
49
+ "--add-dependency", "easy_code_sign"
50
+ ]
51
+
52
+ unless system(*cmd)
53
+ puts "Packaging failed. Check ruby-packer installation."
54
+ exit 1
55
+ end
56
+
57
+ puts "Created: #{output_path}"
58
+ end
59
+
60
+ desc "Clean build artifacts"
61
+ task :clean do
62
+ FileUtils.rm_rf(Dir.glob("#{DIST_DIR}/*"))
63
+ end
64
+
65
+ desc "Run native host tests"
66
+ task :test do
67
+ sh "ruby -I#{SRC_DIR} -I#{ROOT_DIR}/lib #{SRC_DIR}/../test/native_host_test.rb"
68
+ end
69
+
70
+ private
71
+
72
+ def create_dev_wrapper
73
+ # Create a shell wrapper for development that uses system Ruby
74
+ wrapper_path = File.join(DIST_DIR, "easy_sign_host")
75
+ host_script = File.join(SRC_DIR, "easy_sign_host.rb")
76
+
77
+ File.write(wrapper_path, <<~BASH)
78
+ #!/bin/bash
79
+ # Development wrapper for EasySign Native Host
80
+ # Uses system Ruby and requires easy_code_sign gem to be installed
81
+
82
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
83
+ HOST_SCRIPT="#{host_script}"
84
+
85
+ exec ruby "$HOST_SCRIPT" "$@"
86
+ BASH
87
+
88
+ FileUtils.chmod(0o755, wrapper_path)
89
+ puts "Created development wrapper: #{wrapper_path}"
90
+ end
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "com.easysign.host",
3
+ "description": "EasySign PDF Signing Native Host",
4
+ "path": "{{HOST_PATH}}",
5
+ "type": "stdio",
6
+ "allowed_origins": [
7
+ "chrome-extension://{{EXTENSION_ID}}/"
8
+ ]
9
+ }
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+ # Install EasySign Native Messaging Host for Chrome/Chromium
3
+
4
+ set -e
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ HOST_NAME="com.easysign.host"
8
+
9
+ # Detect host executable path
10
+ if [ -f "$SCRIPT_DIR/../dist/easy_sign_host" ]; then
11
+ # Use packaged binary
12
+ HOST_PATH="$SCRIPT_DIR/../dist/easy_sign_host"
13
+ elif [ -f "$SCRIPT_DIR/../src/easy_sign_host.rb" ]; then
14
+ # Use Ruby script directly (development mode)
15
+ HOST_PATH="$SCRIPT_DIR/../src/easy_sign_host.rb"
16
+ chmod +x "$HOST_PATH"
17
+ else
18
+ echo "Error: Native host executable not found"
19
+ exit 1
20
+ fi
21
+
22
+ HOST_PATH="$(cd "$(dirname "$HOST_PATH")" && pwd)/$(basename "$HOST_PATH")"
23
+
24
+ # Get extension ID (can be overridden)
25
+ EXTENSION_ID="${EASYSIGN_EXTENSION_ID:-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}"
26
+
27
+ # Determine manifest directory based on OS
28
+ case "$(uname -s)" in
29
+ Darwin)
30
+ # macOS
31
+ if [ "$USER" = "root" ]; then
32
+ MANIFEST_DIR="/Library/Google/Chrome/NativeMessagingHosts"
33
+ else
34
+ MANIFEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
35
+ fi
36
+ ;;
37
+ Linux)
38
+ # Linux
39
+ if [ "$USER" = "root" ]; then
40
+ MANIFEST_DIR="/etc/opt/chrome/native-messaging-hosts"
41
+ else
42
+ MANIFEST_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
43
+ fi
44
+ ;;
45
+ MINGW*|MSYS*|CYGWIN*)
46
+ # Windows (Git Bash, MSYS2, Cygwin)
47
+ MANIFEST_DIR="$LOCALAPPDATA/Google/Chrome/User Data/NativeMessagingHosts"
48
+ ;;
49
+ *)
50
+ echo "Error: Unsupported operating system"
51
+ exit 1
52
+ ;;
53
+ esac
54
+
55
+ # Create manifest directory if it doesn't exist
56
+ mkdir -p "$MANIFEST_DIR"
57
+
58
+ # Generate manifest
59
+ MANIFEST_PATH="$MANIFEST_DIR/$HOST_NAME.json"
60
+
61
+ cat > "$MANIFEST_PATH" << EOF
62
+ {
63
+ "name": "$HOST_NAME",
64
+ "description": "EasySign PDF Signing Native Host",
65
+ "path": "$HOST_PATH",
66
+ "type": "stdio",
67
+ "allowed_origins": [
68
+ "chrome-extension://$EXTENSION_ID/"
69
+ ]
70
+ }
71
+ EOF
72
+
73
+ echo "EasySign Native Messaging Host installed for Chrome"
74
+ echo " Manifest: $MANIFEST_PATH"
75
+ echo " Host: $HOST_PATH"
76
+ echo ""
77
+ echo "NOTE: Update the extension ID in the manifest if needed:"
78
+ echo " Current: $EXTENSION_ID"
79
+ echo ""
80
+ echo "To update, run:"
81
+ echo " EASYSIGN_EXTENSION_ID=your_extension_id $0"
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+ # Install EasySign Native Messaging Host for Firefox
3
+
4
+ set -e
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ HOST_NAME="com.easysign.host"
8
+
9
+ # Detect host executable path
10
+ if [ -f "$SCRIPT_DIR/../dist/easy_sign_host" ]; then
11
+ # Use packaged binary
12
+ HOST_PATH="$SCRIPT_DIR/../dist/easy_sign_host"
13
+ elif [ -f "$SCRIPT_DIR/../src/easy_sign_host.rb" ]; then
14
+ # Use Ruby script directly (development mode)
15
+ HOST_PATH="$SCRIPT_DIR/../src/easy_sign_host.rb"
16
+ chmod +x "$HOST_PATH"
17
+ else
18
+ echo "Error: Native host executable not found"
19
+ exit 1
20
+ fi
21
+
22
+ HOST_PATH="$(cd "$(dirname "$HOST_PATH")" && pwd)/$(basename "$HOST_PATH")"
23
+
24
+ # Get extension ID (Firefox uses email-style IDs)
25
+ EXTENSION_ID="${EASYSIGN_EXTENSION_ID:-easysign@example.com}"
26
+
27
+ # Determine manifest directory based on OS
28
+ case "$(uname -s)" in
29
+ Darwin)
30
+ # macOS
31
+ if [ "$USER" = "root" ]; then
32
+ MANIFEST_DIR="/Library/Application Support/Mozilla/NativeMessagingHosts"
33
+ else
34
+ MANIFEST_DIR="$HOME/Library/Application Support/Mozilla/NativeMessagingHosts"
35
+ fi
36
+ ;;
37
+ Linux)
38
+ # Linux
39
+ if [ "$USER" = "root" ]; then
40
+ MANIFEST_DIR="/usr/lib/mozilla/native-messaging-hosts"
41
+ else
42
+ MANIFEST_DIR="$HOME/.mozilla/native-messaging-hosts"
43
+ fi
44
+ ;;
45
+ MINGW*|MSYS*|CYGWIN*)
46
+ # Windows (Git Bash, MSYS2, Cygwin)
47
+ MANIFEST_DIR="$APPDATA/Mozilla/NativeMessagingHosts"
48
+ ;;
49
+ *)
50
+ echo "Error: Unsupported operating system"
51
+ exit 1
52
+ ;;
53
+ esac
54
+
55
+ # Create manifest directory if it doesn't exist
56
+ mkdir -p "$MANIFEST_DIR"
57
+
58
+ # Generate manifest (Firefox uses allowed_extensions instead of allowed_origins)
59
+ MANIFEST_PATH="$MANIFEST_DIR/$HOST_NAME.json"
60
+
61
+ cat > "$MANIFEST_PATH" << EOF
62
+ {
63
+ "name": "$HOST_NAME",
64
+ "description": "EasySign PDF Signing Native Host",
65
+ "path": "$HOST_PATH",
66
+ "type": "stdio",
67
+ "allowed_extensions": [
68
+ "$EXTENSION_ID"
69
+ ]
70
+ }
71
+ EOF
72
+
73
+ echo "EasySign Native Messaging Host installed for Firefox"
74
+ echo " Manifest: $MANIFEST_PATH"
75
+ echo " Host: $HOST_PATH"
76
+ echo ""
77
+ echo "NOTE: Update the extension ID in the manifest if needed:"
78
+ echo " Current: $EXTENSION_ID"
79
+ echo ""
80
+ echo "To update, run:"
81
+ echo " EASYSIGN_EXTENSION_ID=your_extension_id $0"
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # EasySign Native Messaging Host
5
+ # Communicates with browser extension via stdin/stdout using length-prefixed JSON
6
+
7
+ require "json"
8
+
9
+ # Add lib to load path for development
10
+ $LOAD_PATH.unshift(File.expand_path("../../../lib", __dir__))
11
+ $LOAD_PATH.unshift(__dir__)
12
+
13
+ require "protocol"
14
+ require "signing_service"
15
+
16
+ module EasySign
17
+ # Native Messaging Host for browser extension communication
18
+ class NativeHost
19
+ MAX_MESSAGE_SIZE = 1024 * 1024 # 1MB limit
20
+
21
+ def initialize
22
+ @signing_service = SigningService.new
23
+
24
+ # Ensure binary mode for stdin/stdout
25
+ $stdin.binmode
26
+ $stdout.binmode
27
+
28
+ # Disable stdout buffering
29
+ $stdout.sync = true
30
+ end
31
+
32
+ # Main loop - read and process messages
33
+ def run
34
+ loop do
35
+ message = read_message
36
+ break unless message
37
+
38
+ response = process_message(message)
39
+ write_message(response)
40
+ end
41
+ rescue Interrupt
42
+ # Clean exit on Ctrl+C (shouldn't happen in normal use)
43
+ exit 0
44
+ rescue StandardError => e
45
+ # Log error but don't crash - browser will handle disconnect
46
+ write_message(Protocol.error_response(nil, Protocol::ErrorCodes::INTERNAL_ERROR, e.message))
47
+ end
48
+
49
+ private
50
+
51
+ # Read a native messaging message from stdin
52
+ # Format: 4-byte length (little-endian uint32) + JSON data
53
+ def read_message
54
+ # Read 4-byte length prefix
55
+ length_bytes = $stdin.read(4)
56
+ return nil unless length_bytes && length_bytes.bytesize == 4
57
+
58
+ # Unpack as little-endian unsigned 32-bit integer
59
+ length = length_bytes.unpack1("V")
60
+
61
+ # Validate length
62
+ return nil if length.zero? || length > MAX_MESSAGE_SIZE
63
+
64
+ # Read JSON data
65
+ json_data = $stdin.read(length)
66
+ return nil unless json_data && json_data.bytesize == length
67
+
68
+ JSON.parse(json_data, symbolize_names: true)
69
+ rescue JSON::ParserError => e
70
+ # Return error for invalid JSON
71
+ { type: "invalid", error: e.message }
72
+ end
73
+
74
+ # Write a native messaging message to stdout
75
+ # Format: 4-byte length (little-endian uint32) + JSON data
76
+ def write_message(message)
77
+ json = message.to_json
78
+ length = [json.bytesize].pack("V") # Little-endian uint32
79
+
80
+ $stdout.write(length)
81
+ $stdout.write(json)
82
+ $stdout.flush
83
+ end
84
+
85
+ # Process an incoming message and return response
86
+ def process_message(message)
87
+ request_id = message[:requestId] || message[:request_id]
88
+ type = message[:type]
89
+
90
+ case type
91
+ when Protocol::Types::SIGN
92
+ process_sign(request_id, message[:payload])
93
+ when Protocol::Types::VERIFY
94
+ process_verify(request_id, message[:payload])
95
+ when Protocol::Types::CHECK_AVAILABILITY
96
+ process_check_availability(request_id)
97
+ when "invalid"
98
+ Protocol.error_response(request_id, Protocol::ErrorCodes::INVALID_MESSAGE, message[:error])
99
+ else
100
+ Protocol.error_response(request_id, Protocol::ErrorCodes::INVALID_MESSAGE, "Unknown message type: #{type}")
101
+ end
102
+ end
103
+
104
+ def process_sign(request_id, payload)
105
+ result = @signing_service.sign(
106
+ pdf_data: payload[:pdfData],
107
+ pin: payload[:pin],
108
+ options: payload[:options] || {}
109
+ )
110
+
111
+ Protocol.sign_response(request_id, result)
112
+ rescue EasyCodeSign::PinError => e
113
+ Protocol.error_response(
114
+ request_id,
115
+ Protocol::ErrorCodes::PIN_INCORRECT,
116
+ e.message,
117
+ { retriesRemaining: e.respond_to?(:retries_remaining) ? e.retries_remaining : nil }
118
+ )
119
+ rescue EasyCodeSign::TokenNotFoundError => e
120
+ Protocol.error_response(request_id, Protocol::ErrorCodes::TOKEN_NOT_FOUND, e.message)
121
+ rescue EasyCodeSign::TokenLockedError => e
122
+ Protocol.error_response(request_id, Protocol::ErrorCodes::TOKEN_LOCKED, e.message)
123
+ rescue EasyCodeSign::InvalidPdfError => e
124
+ Protocol.error_response(request_id, Protocol::ErrorCodes::INVALID_PDF, e.message)
125
+ rescue EasyCodeSign::Error => e
126
+ Protocol.error_response(request_id, Protocol::ErrorCodes::SIGNING_FAILED, e.message)
127
+ rescue StandardError => e
128
+ Protocol.error_response(request_id, Protocol::ErrorCodes::INTERNAL_ERROR, e.message)
129
+ end
130
+
131
+ def process_verify(request_id, payload)
132
+ result = @signing_service.verify(
133
+ pdf_data: payload[:pdfData],
134
+ check_timestamp: payload[:checkTimestamp] != false
135
+ )
136
+
137
+ Protocol.verify_response(request_id, result)
138
+ rescue EasyCodeSign::InvalidPdfError => e
139
+ Protocol.error_response(request_id, Protocol::ErrorCodes::INVALID_PDF, e.message)
140
+ rescue EasyCodeSign::Error => e
141
+ Protocol.error_response(request_id, Protocol::ErrorCodes::VERIFICATION_FAILED, e.message)
142
+ rescue StandardError => e
143
+ Protocol.error_response(request_id, Protocol::ErrorCodes::INTERNAL_ERROR, e.message)
144
+ end
145
+
146
+ def process_check_availability(request_id)
147
+ result = @signing_service.check_availability
148
+ Protocol.availability_response(request_id, result)
149
+ rescue StandardError => e
150
+ Protocol.error_response(request_id, Protocol::ErrorCodes::INTERNAL_ERROR, e.message)
151
+ end
152
+ end
153
+ end
154
+
155
+ # Entry point
156
+ if __FILE__ == $PROGRAM_NAME
157
+ EasySign::NativeHost.new.run
158
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Native Messaging Protocol definitions
4
+ module EasySign
5
+ module Protocol
6
+ # Message types
7
+ module Types
8
+ SIGN = "sign"
9
+ VERIFY = "verify"
10
+ CHECK_AVAILABILITY = "check_availability"
11
+
12
+ SIGN_RESPONSE = "sign_response"
13
+ VERIFY_RESPONSE = "verify_response"
14
+ AVAILABILITY_RESPONSE = "availability_response"
15
+ ERROR = "error"
16
+ end
17
+
18
+ # Error codes
19
+ module ErrorCodes
20
+ TOKEN_NOT_FOUND = "TOKEN_NOT_FOUND"
21
+ PIN_INCORRECT = "PIN_INCORRECT"
22
+ TOKEN_LOCKED = "TOKEN_LOCKED"
23
+ INVALID_PDF = "INVALID_PDF"
24
+ SIGNING_FAILED = "SIGNING_FAILED"
25
+ VERIFICATION_FAILED = "VERIFICATION_FAILED"
26
+ INVALID_MESSAGE = "INVALID_MESSAGE"
27
+ INTERNAL_ERROR = "INTERNAL_ERROR"
28
+ end
29
+
30
+ class << self
31
+ # Create a success response
32
+ def success_response(request_id, type, payload)
33
+ {
34
+ type: type,
35
+ requestId: request_id,
36
+ payload: payload,
37
+ error: nil
38
+ }
39
+ end
40
+
41
+ # Create an error response
42
+ def error_response(request_id, code, message, details = nil)
43
+ {
44
+ type: Types::ERROR,
45
+ requestId: request_id,
46
+ payload: nil,
47
+ error: {
48
+ code: code,
49
+ message: message,
50
+ details: details
51
+ }.compact
52
+ }
53
+ end
54
+
55
+ # Create sign response
56
+ def sign_response(request_id, result)
57
+ success_response(request_id, Types::SIGN_RESPONSE, {
58
+ signedPdfData: result[:signed_pdf_data],
59
+ signerName: result[:signer_name],
60
+ signedAt: result[:signed_at]&.iso8601,
61
+ timestamped: result[:timestamped] || false
62
+ })
63
+ end
64
+
65
+ # Create verify response
66
+ def verify_response(request_id, result)
67
+ success_response(request_id, Types::VERIFY_RESPONSE, {
68
+ valid: result[:valid],
69
+ signerName: result[:signer_name],
70
+ signerOrganization: result[:signer_organization],
71
+ signedAt: result[:signed_at]&.iso8601,
72
+ signatureValid: result[:signature_valid],
73
+ integrityValid: result[:integrity_valid],
74
+ certificateValid: result[:certificate_valid],
75
+ chainValid: result[:chain_valid],
76
+ trusted: result[:trusted],
77
+ timestamped: result[:timestamped],
78
+ timestampValid: result[:timestamp_valid],
79
+ errors: result[:errors] || [],
80
+ warnings: result[:warnings] || []
81
+ })
82
+ end
83
+
84
+ # Create availability response
85
+ def availability_response(request_id, result)
86
+ success_response(request_id, Types::AVAILABILITY_RESPONSE, {
87
+ available: result[:available],
88
+ tokenPresent: result[:token_present],
89
+ slots: result[:slots]&.map do |slot|
90
+ {
91
+ index: slot[:index],
92
+ tokenLabel: slot[:token_label],
93
+ manufacturer: slot[:manufacturer],
94
+ serial: slot[:serial]
95
+ }
96
+ end
97
+ })
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "tempfile"
5
+ require "easy_code_sign"
6
+
7
+ module EasySign
8
+ # Service that wraps EasyCodeSign gem for browser extension use
9
+ class SigningService
10
+ def initialize
11
+ configure_easy_code_sign
12
+ end
13
+
14
+ # Sign a PDF document
15
+ # @param pdf_data [String] Base64-encoded PDF content
16
+ # @param pin [String] Token PIN
17
+ # @param options [Hash] Signing options
18
+ # @return [Hash] Result with signed PDF data
19
+ def sign(pdf_data:, pin:, options: {})
20
+ # Decode PDF from Base64
21
+ pdf_bytes = Base64.strict_decode64(pdf_data)
22
+
23
+ # Write to temp file
24
+ input_file = Tempfile.new(["input", ".pdf"], binmode: true)
25
+ output_file = Tempfile.new(["signed", ".pdf"], binmode: true)
26
+
27
+ begin
28
+ input_file.write(pdf_bytes)
29
+ input_file.close
30
+
31
+ # Build signing options
32
+ sign_opts = build_sign_options(options).merge(
33
+ pin: pin,
34
+ output_path: output_file.path
35
+ )
36
+
37
+ # Sign using EasyCodeSign
38
+ result = EasyCodeSign.sign(input_file.path, **sign_opts)
39
+
40
+ # Read signed PDF and encode as Base64
41
+ signed_bytes = File.binread(output_file.path)
42
+ signed_base64 = Base64.strict_encode64(signed_bytes)
43
+
44
+ {
45
+ signed_pdf_data: signed_base64,
46
+ signer_name: result.signer_name,
47
+ signed_at: result.signed_at,
48
+ timestamped: result.timestamped?
49
+ }
50
+ ensure
51
+ input_file.unlink
52
+ output_file.close
53
+ output_file.unlink
54
+ end
55
+ end
56
+
57
+ # Verify a signed PDF document
58
+ # @param pdf_data [String] Base64-encoded PDF content
59
+ # @param check_timestamp [Boolean] Whether to verify timestamp
60
+ # @return [Hash] Verification result
61
+ def verify(pdf_data:, check_timestamp: true)
62
+ # Decode PDF from Base64
63
+ pdf_bytes = Base64.strict_decode64(pdf_data)
64
+
65
+ # Write to temp file
66
+ temp_file = Tempfile.new(["verify", ".pdf"], binmode: true)
67
+
68
+ begin
69
+ temp_file.write(pdf_bytes)
70
+ temp_file.close
71
+
72
+ # Verify using EasyCodeSign
73
+ result = EasyCodeSign.verify(temp_file.path, check_timestamp: check_timestamp)
74
+
75
+ {
76
+ valid: result.valid?,
77
+ signer_name: result.signer_name,
78
+ signer_organization: result.signer_organization,
79
+ signed_at: result.timestamp,
80
+ signature_valid: result.signature_valid?,
81
+ integrity_valid: result.integrity_valid?,
82
+ certificate_valid: result.certificate_valid?,
83
+ chain_valid: result.chain_valid?,
84
+ trusted: result.trusted?,
85
+ timestamped: result.timestamped?,
86
+ timestamp_valid: result.timestamp_valid?,
87
+ errors: result.errors,
88
+ warnings: result.warnings
89
+ }
90
+ ensure
91
+ temp_file.unlink
92
+ end
93
+ end
94
+
95
+ # Check if signing is available (token connected, etc.)
96
+ # @return [Hash] Availability status
97
+ def check_availability
98
+ begin
99
+ slots = EasyCodeSign.list_slots
100
+
101
+ # Check if any slot has a token present
102
+ token_present = slots.any? { |s| s[:token_present] }
103
+
104
+ {
105
+ available: true,
106
+ token_present: token_present,
107
+ slots: slots.map do |slot|
108
+ {
109
+ index: slot[:index],
110
+ token_label: slot[:token_label],
111
+ manufacturer: slot[:manufacturer],
112
+ serial: slot[:serial]
113
+ }
114
+ end
115
+ }
116
+ rescue EasyCodeSign::Pkcs11LibraryError, EasyCodeSign::TokenNotFoundError => e
117
+ {
118
+ available: false,
119
+ token_present: false,
120
+ error: e.message,
121
+ slots: []
122
+ }
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ def configure_easy_code_sign
129
+ # Use default configuration - can be customized via env vars or config file
130
+ EasyCodeSign.configure do |config|
131
+ # Provider can be set via env var
132
+ config.provider = ENV.fetch("EASYSIGN_PROVIDER", "safenet").to_sym
133
+
134
+ # PKCS#11 library path (auto-detected if not set)
135
+ config.pkcs11_library = ENV["EASYSIGN_PKCS11_LIBRARY"] if ENV["EASYSIGN_PKCS11_LIBRARY"]
136
+
137
+ # Timestamp authority
138
+ config.timestamp_authority = ENV.fetch("EASYSIGN_TSA_URL", "http://timestamp.digicert.com")
139
+
140
+ # Network timeout
141
+ config.network_timeout = ENV.fetch("EASYSIGN_TIMEOUT", 30).to_i
142
+ end
143
+ end
144
+
145
+ def build_sign_options(options)
146
+ opts = {}
147
+
148
+ # Timestamp
149
+ opts[:timestamp] = options["timestamp"] || options[:timestamp] || false
150
+ if options["timestampAuthority"] || options[:timestamp_authority]
151
+ EasyCodeSign.configuration.timestamp_authority =
152
+ options["timestampAuthority"] || options[:timestamp_authority]
153
+ end
154
+
155
+ # Visible signature
156
+ opts[:visible_signature] = options["visibleSignature"] || options[:visible_signature] || false
157
+ opts[:signature_page] = options["signaturePage"] || options[:signature_page] || 1
158
+ opts[:signature_position] = (options["signaturePosition"] || options[:signature_position] || "bottom_right").to_sym
159
+
160
+ # Signature metadata
161
+ opts[:signature_reason] = options["reason"] || options[:reason]
162
+ opts[:signature_location] = options["location"] || options[:location]
163
+
164
+ opts.compact
165
+ end
166
+ end
167
+ end