0.0
No release in over 3 years
A Ruby gem for code signing operations using hardware tokens (HSM/smart cards) via PKCS#11. Supports SafeNet eToken, RFC 3161 timestamping, PDF visible signatures, and signature verification.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 5.0
~> 13.0
~> 1.21

Runtime

~> 0.2
~> 0.3
~> 2.3
~> 1.3
 Project Readme

EasyCodeSign

A Ruby gem for signing and verifying Ruby gems, ZIP files, and PDF documents using hardware security tokens (HSM/smart cards). Currently supports SafeNet eToken with plans for additional providers.

Features

  • Sign Ruby gems (.gem) - Creates PKCS#7 signatures compatible with gem cert
  • Sign ZIP archives (.zip, .jar, .apk, .war, .ear) - JAR-style signing with META-INF manifest
  • Sign PDF documents (.pdf) - Digital signatures with optional visible annotations
  • Hardware token support - SafeNet eToken via PKCS#11 (extensible for other HSMs)
  • RFC 3161 timestamping - Proves signature existed at a specific time
  • Full verification - Signature, certificate chain, trust, and timestamp validation
  • Certificate revocation checking - OCSP and CRL support
  • Command-line interface - Easy-to-use CLI for signing and verification

Installation

Add to your Gemfile:

gem 'easy_code_sign'

Or install directly:

gem install easy_code_sign

Prerequisites

  • Ruby 3.2+
  • SafeNet eToken drivers and PKCS#11 library installed
  • Hardware token with code signing certificate

Command-Line Usage

Sign a file

# Basic signing (will prompt for PIN securely)
easysign sign my_gem-1.0.0.gem

# Sign with timestamp
easysign sign my_gem-1.0.0.gem --timestamp --tsa http://timestamp.digicert.com

# Sign with custom output path
easysign sign archive.zip --output signed_archive.zip

# Use specific PKCS#11 library
easysign sign my_gem.gem --library /path/to/libeToken.dylib

Sign a PDF

# Basic PDF signing (invisible signature)
easysign sign document.pdf

# PDF with visible signature annotation
easysign sign document.pdf --visible-signature --signature-position bottom-right

# PDF with timestamp and metadata
easysign sign document.pdf -t --visible-signature --signature-reason "Approved" --signature-location "New York"

# PDF signing on specific page
easysign sign document.pdf --visible-signature --signature-page 2

Security Note: The PIN is always entered interactively via a secure prompt. It is never passed as a command-line argument to prevent exposure in shell history, process listings, or log files.

Verify a signature

# Basic verification
easysign verify signed.gem

# Output as JSON
easysign verify signed.gem --json

# Use custom trust store
easysign verify signed.gem --trust-store /path/to/ca-certs/

List available tokens

easysign list-slots

Show signature information

easysign info signed.gem

Ruby API

Configuration

require 'easy_code_sign'

EasyCodeSign.configure do |config|
  # Token provider (:safenet is currently supported)
  config.provider = :safenet

  # Path to PKCS#11 library (auto-detected if not specified)
  config.pkcs11_library = '/usr/local/lib/libeToken.dylib'

  # Token slot index (default: 0)
  config.slot_index = 0

  # Timestamp authority URL (optional)
  config.timestamp_authority = 'http://timestamp.digicert.com'

  # Hash algorithm for timestamps (default: :sha256)
  config.timestamp_hash_algorithm = :sha256

  # Require timestamp for all signatures (default: false)
  config.require_timestamp = false

  # Check certificate revocation during verification (default: true)
  config.check_revocation = true

  # Network timeout in seconds (default: 30)
  config.network_timeout = 30

  # Custom trust store path for verification (optional)
  config.trust_store_path = '/path/to/custom/ca-certs'

  # PIN callback for interactive PIN entry
  config.pin_callback = ->(slot_info) {
    print "Enter PIN for #{slot_info[:slot_index]}: "
    $stdin.noecho(&:gets).chomp
  }
end

Signing

# Sign a gem
result = EasyCodeSign.sign('my_gem-1.0.0.gem', pin: '1234')
puts "Signed: #{result.file_path}"
puts "Signer: #{result.signer_name}"

# Sign with timestamp
result = EasyCodeSign.sign('my_gem-1.0.0.gem',
  pin: '1234',
  timestamp: true
)
puts "Timestamp: #{result.timestamp}"

# Sign with custom output path
result = EasyCodeSign.sign('archive.zip',
  pin: '1234',
  output_path: 'signed_archive.zip',
  algorithm: :sha256_rsa
)

# Batch signing (single token session)
signer = EasyCodeSign.signer
results = signer.sign_batch(
  ['gem1.gem', 'gem2.gem', 'archive.zip'],
  pin: '1234'
)

Verification

# Verify a signed file
result = EasyCodeSign.verify('signed.gem')

if result.valid?
  puts "Signature is valid!"
  puts "Signed by: #{result.signer_name}"
  puts "Organization: #{result.signer_organization}"

  if result.timestamped?
    puts "Timestamp: #{result.timestamp}"
    puts "TSA: #{result.timestamp_authority}"
  end
else
  puts "Verification failed:"
  result.errors.each { |e| puts "  - #{e}" }
end

# Detailed verification status
puts "Signature valid: #{result.signature_valid?}"
puts "Integrity valid: #{result.integrity_valid?}"
puts "Certificate valid: #{result.certificate_valid?}"
puts "Chain valid: #{result.chain_valid?}"
puts "Trusted: #{result.trusted?}"

# Get full result as hash
puts result.to_h

# Use custom trust store
trust_store = EasyCodeSign::Verification::TrustStore.new
trust_store.add_file('/path/to/custom_ca.pem')
result = EasyCodeSign.verify('signed.gem', trust_store: trust_store)

# Batch verification
verifier = EasyCodeSign.verifier
results = verifier.verify_batch(['file1.gem', 'file2.zip'])
results.each do |path, result|
  puts "#{path}: #{result.valid? ? 'VALID' : 'INVALID'}"
end

Working with Tokens

# List available token slots
slots = EasyCodeSign.list_slots
slots.each do |slot|
  puts "Slot #{slot[:index]}: #{slot[:token_label]}"
  puts "  Serial: #{slot[:serial]}"
end

# Direct provider access
provider = EasyCodeSign.provider
provider.with_session(pin: '1234') do |session|
  cert = session.certificate
  puts "Certificate: #{cert.subject}"
  puts "Expires: #{cert.not_after}"

  chain = session.certificate_chain
  puts "Chain length: #{chain.length}"
end

Supported Timestamp Authorities

Common free TSA endpoints:

Provider URL
DigiCert http://timestamp.digicert.com
GlobalSign http://timestamp.globalsign.com/tsa/r6advanced1
Sectigo http://timestamp.sectigo.com
SSL.com http://ts.ssl.com

Error Handling

begin
  EasyCodeSign.sign('file.gem', pin: '1234')
rescue EasyCodeSign::TokenNotFoundError
  puts "Hardware token not connected"
rescue EasyCodeSign::PinError => e
  puts "PIN error: #{e.message}"
  puts "Retries remaining: #{e.retries_remaining}" if e.retries_remaining
rescue EasyCodeSign::TokenLockedError
  puts "Token is locked - contact your administrator"
rescue EasyCodeSign::TimestampAuthorityError => e
  puts "Timestamp failed: #{e.message}"
  puts "HTTP status: #{e.http_status}" if e.http_status
rescue EasyCodeSign::InvalidFileError => e
  puts "Invalid file: #{e.message}"
rescue EasyCodeSign::Error => e
  puts "Signing error: #{e.message}"
end

Architecture

EasyCodeSign
├── Providers           # Hardware token abstraction
│   ├── Base            # Abstract provider interface
│   ├── Pkcs11Base      # Shared PKCS#11 functionality
│   └── Safenet         # SafeNet eToken implementation
├── Signable            # File type handlers
│   ├── Base            # Abstract signable interface
│   ├── GemFile         # Ruby gem signing
│   └── ZipFile         # JAR-style ZIP signing
├── Timestamp           # RFC 3161 timestamping
│   ├── Client          # TSA HTTP client
│   ├── Request         # TimeStampReq builder
│   ├── Response        # TimeStampResp parser
│   └── Verifier        # Timestamp verification
├── Verification        # Signature verification
│   ├── Result          # Verification result
│   ├── TrustStore      # CA certificate management
│   ├── CertificateChain# Chain validation
│   └── SignatureChecker# Cryptographic verification
├── Signer              # Signing orchestrator
├── Verifier            # Verification orchestrator
└── CLI                 # Command-line interface

Security Considerations

  • PINs are never passed as CLI arguments - Always entered via secure interactive prompt
  • PINs are never logged or stored - Use pin_callback for programmatic secure entry
  • Hardware tokens protect private keys - Keys never leave the HSM
  • Timestamps provide non-repudiation - Signatures remain valid after certificate expiry
  • Certificate revocation is checked - OCSP (real-time) with CRL fallback
  • System CA store is used by default - Custom trust stores supported

Development

# Install dependencies
bin/setup

# Run tests
bundle exec rake test

# Run linter
bundle exec rubocop

# Interactive console
bin/console

# Install locally
bundle exec rake install

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/mpantel/easy_code_sign.

License

The gem is available as open source under the terms of the MIT License.