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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +95 -0
- data/LICENSE +21 -0
- data/README.md +331 -0
- data/Rakefile +16 -0
- data/exe/easysign +7 -0
- data/lib/easy_code_sign/cli.rb +428 -0
- data/lib/easy_code_sign/configuration.rb +102 -0
- data/lib/easy_code_sign/deferred_signing_request.rb +104 -0
- data/lib/easy_code_sign/errors.rb +113 -0
- data/lib/easy_code_sign/pdf/appearance_builder.rb +104 -0
- data/lib/easy_code_sign/pdf/timestamp_handler.rb +31 -0
- data/lib/easy_code_sign/providers/base.rb +126 -0
- data/lib/easy_code_sign/providers/pkcs11_base.rb +197 -0
- data/lib/easy_code_sign/providers/safenet.rb +109 -0
- data/lib/easy_code_sign/signable/base.rb +98 -0
- data/lib/easy_code_sign/signable/gem_file.rb +224 -0
- data/lib/easy_code_sign/signable/pdf_file.rb +486 -0
- data/lib/easy_code_sign/signable/zip_file.rb +226 -0
- data/lib/easy_code_sign/signer.rb +254 -0
- data/lib/easy_code_sign/timestamp/client.rb +184 -0
- data/lib/easy_code_sign/timestamp/request.rb +114 -0
- data/lib/easy_code_sign/timestamp/response.rb +246 -0
- data/lib/easy_code_sign/timestamp/verifier.rb +227 -0
- data/lib/easy_code_sign/verification/certificate_chain.rb +298 -0
- data/lib/easy_code_sign/verification/result.rb +222 -0
- data/lib/easy_code_sign/verification/signature_checker.rb +196 -0
- data/lib/easy_code_sign/verification/trust_store.rb +140 -0
- data/lib/easy_code_sign/verifier.rb +426 -0
- data/lib/easy_code_sign/version.rb +5 -0
- data/lib/easy_code_sign.rb +183 -0
- data/plugin/.gitignore +21 -0
- data/plugin/Gemfile +24 -0
- data/plugin/Gemfile.lock +134 -0
- data/plugin/README.md +248 -0
- data/plugin/Rakefile +121 -0
- data/plugin/docs/API_REFERENCE.md +366 -0
- data/plugin/docs/DEVELOPMENT.md +522 -0
- data/plugin/docs/INSTALLATION.md +204 -0
- data/plugin/native_host/build/Rakefile +90 -0
- data/plugin/native_host/install/com.easysign.host.json +9 -0
- data/plugin/native_host/install/install_chrome.sh +81 -0
- data/plugin/native_host/install/install_firefox.sh +81 -0
- data/plugin/native_host/src/easy_sign_host.rb +158 -0
- data/plugin/native_host/src/protocol.rb +101 -0
- data/plugin/native_host/src/signing_service.rb +167 -0
- data/plugin/native_host/test/native_host_test.rb +113 -0
- data/plugin/src/easy_sign/background.rb +323 -0
- data/plugin/src/easy_sign/content.rb +74 -0
- data/plugin/src/easy_sign/inject.rb +239 -0
- data/plugin/src/easy_sign/messaging.rb +109 -0
- data/plugin/src/easy_sign/popup.rb +200 -0
- data/plugin/templates/manifest.json +58 -0
- data/plugin/templates/popup.css +223 -0
- data/plugin/templates/popup.html +59 -0
- data/sig/easy_code_sign.rbs +4 -0
- data/test/easy_code_sign_test.rb +122 -0
- data/test/pdf_signable_test.rb +569 -0
- data/test/signable_test.rb +334 -0
- data/test/test_helper.rb +18 -0
- data/test/timestamp_test.rb +163 -0
- data/test/verification_test.rb +350 -0
- 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,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
|