Skip to content

dextercd/Handtokening

Repository files navigation

Handtokening

Code Signing server written in Django/Python for remotely signing Windows programs with osslsigncode.

It has a simple HTTP API to submit files for signing.

It automatically creates test signing resources, supports AV scans (ClamAV and VirusTotal), and keeps a log of every signing operation.

Most of the signing configuration is performed in the Django admin interface. This is also where the signing logs can be viewed.

Tested on Debian 13 and Arch Linux.

Deployment

It's not particularly hard to set up, but there are quite a lot of steps. In summary:

  • Create a system user under which the web service will run.
  • Install osslsigncode and pcscd/CCID/OpenSC/libp11 for signing code with a hardware token.
  • Set up ClamAV daemon for scanning incoming files.
  • Create a Python virtualenv and install the application and dependencies in there.
  • Configure polkit so the service user can communicate with pcscd.
  • Generate a django-secret file with proper permissions set.
  • Create an empty file as the sqlite3 database with proper permissions set.
  • Deploy the systemd .service/.socket files.
  • Write variables to the config file that are appropriate for your setup.
  • Expose the service via a reverse proxy; sitting in front of the gunicorn socket.

There's an Ansible role to perform all these steps. You can use this to automatically deploy the application or as a reference and perform the steps manually.

Ansible sample config

Here's how I've deployed multiple test servers using the Ansible role:

ansible.cfg
[defaults]
inventory = hosts.ini
roles_path = /roles:/usr/share/ansible/roles:/etc/ansible/roles:[PATH TO]/handtokening/ansible_roles
vault_password_file = .vaultpass
handtokening.yml
- hosts: all
  vars_files:
    - handtokening-vars.yml
  tasks:
    - name: Install NGINX
      tags: nginx
      become: true
      ansible.builtin.package:
        name:
          - nginx

    - name: Enable NGINX
      tags: nginx
      become: true
      ansible.builtin.systemd:
        name: nginx.service
        state: started
        enabled: true

    - name: Make NGINX drop-in directory
      tags: nginx
      become: true
      ansible.builtin.file:
        path: /etc/nginx/conf.d
        state: directory
        owner: root
        group: root
        mode: '0755'

    - name: NGINX config
      tags: nginx
      become: true
      when: ansible_os_family == 'Archlinux'
      ansible.builtin.copy:
        content: |
          user http;
          worker_processes auto;
          worker_cpu_affinity auto;
          events {
              worker_connections 1024;
          }
          http {
              charset utf-8;
              sendfile on;
              tcp_nopush on;
              tcp_nodelay on;
              server_tokens off;
              log_not_found off;
              types_hash_max_size 4096;
              client_max_body_size 16M;
              # MIME
              include mime.types;
              default_type application/octet-stream;
              # logging
              access_log /var/log/nginx/access.log;
              error_log /var/log/nginx/error.log warn;
              # load configs
              include /etc/nginx/conf.d/*.conf;
              include /etc/nginx/sites-enabled/*;
          }
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: Reload NGINX

    - name: Run Handtokening role
      ansible.builtin.include_role:
        name: handtokening
      tags: always

  handlers:
    - name: Reload NGINX
      become: true
      ansible.builtin.service:
        name: nginx
        state: reloaded
handtokening-vars.yml
ht_nginx: true
ht_nginx_reload_handler: "Reload NGINX"

ht_host_names:
  - localhost
  - ::1
  - 127.0.0.1
  - "{{ ansible_all_ipv4_addresses[0] }}"

Once this is set up, you can run the following command to deploy the application to all hosts listed in hosts.ini:

ansible-playbook handtokening.yml

In a production deployment, you should add/change the following options inside handtokening-vars.yml:

ht_secure: true
ht_host_names:
  - ht.example.com

ht_nginx: true

ht_nginx_server_listen: |
  listen 443 ssl;
  listen [::]:443 ssl;
  ssl_certificate     /etc/nginx/certs/handtokening.fullchain;
  ssl_certificate_key /etc/nginx/certs/handtokening.key;

ht_nginx_location_extra: |
  allow 127.0.0.1;
  allow ::1;
  deny all;

ht_nginx_location_sign_api_extra: 'allow all;'

This sets up TLS handling in NGINX and tells Handtokening that it's behind HTTPS.

The *_extra config variables are used to enable localhost only access to all Handtokening routes (e.g., the Django admin interface). The signing API has allow all; so that it can be invoked from GitHub Actions, for example.

Of course, the above is just one way of doing things. You're free to change any of the details or take a completely different approach. See defaults/main.yml for more information on the available role configuration variables.

Some useful environment variables aren't set by the Ansible role. You can write to /etc/handtokening/env.extra to set or override Handtokening environment variables.

Environment variables

With the standard Ansible role configuration, systemd will launch the service with environment variables from:

  • /etc/handtokening/env
  • /etc/handtokening/env.extra

The contents of the env file is determined by the Ansible role, and env.extra is an optional environment variables file where you can write your own configuration.

Detailed list of available environment variables

DJANGO_SETTINGS_MODULE

Standard Django variable: #DJANGO_SETTINGS_MODULE.

Has no default. If you try to run the program without setting this, it'll exit with an error.

Set to handtokening.settings.prod by the Ansible role.

This should be set to handtokening.settings.prod normally. This module is responsible for loading settings from environment variables. It also sets many of the default values listed below.

DJANGO_LOG_LEVEL

Determines the log level of the root logger. Set to WARNING by default.

UNSAFE_DEBUG

Used to set the standard Django variable: #DEBUG.

This should not be set to true on a production deployment, as it makes the application return internal details when error occur.

OSSL_PROVIDER_PATH

Path to a OpenSSL provider module that allows OpenSSL use PKCS #11 modules. This is passed to osslsigncode using the -provider option.

This is set to a operating system specific default or is not set if it couldn't be found.

OSSL_ENGINE_PATH

Path to a OpenSSL engine module that allows OpenSSL use PKCS #11 modules. This is passed to osslsigncode using the -pkcs11engine option.

This is an older OpenSSL extension mechanism and is only used if OSSL_PROVIDER_PATH is not set.

This is set to a operating system specific default or is not set if it couldn't be found.

PKCS11_MODULE_PATH

The PKCS #11 module to use for pkcs11-enabled certificates if no certificate-specific module is configured.

Defaults to OpenSC's PKCS #11 module on Arch and Debian.

Set to the OpenSC PKCS #11 module by the Ansible role.

OSSLSIGNCODE_PATH

Defaults to osslsigncode which means it will look up the application on the $PATH list.

CLAMSCAN_PATH

Defaults to /usr/bin/clamdscan. You could change this to /usr/bin/true to skip ClamAV scans.

STATE_DIRECTORY

Normally set by systemd to /var/lib/handtokening.

CONFIGURATION_DIRECTORY

Normally set by systemd to /etc/handtokening.

RUNTIME_DIRECTORY

Normally set by systemd to /run/handtokening.

HOME

It's normally set by systemd to /home/handtokening.

Used to set the STATIC_ROOT variable unless it's set directly.

STATIC_ROOT

Standard Django variable: #STATIC_ROOT.

Used as the destination directory when running django-admin collectstatic.

STATIC_URL

Standard Django variable: #STATIC_URL

Set to static/ by default.

IPWARE_META_PRECEDENCE_ORDER

Comma separated list of sources from which the original requester IP address can be retrieved. This should be properly configured so that the IP address in the logs is accurate and also can't be spoofed maliciously.

Automatically configured by Ansible to HTTP_X_REAL_IP if the ht_nginx role variable is set to true.

Set to REMOTE_ADDR if not configured.

ALLOWED_HOSTS

Standard Django variable: #ALLOWED_HOSTS

Comma separated list of host names that the service should respond to. Any request for a host name that's not on this list will be rejected.

Automatically configured by the Ansible role to the ht_host_names list.

USE_X_FORWARDED_HOST

Standard Django variable: #USE_X_FORWARDED_HOST

Set to False by default.

USE_X_FORWARDED_PORT

Standard Django variable: #USE_X_FORWARDED_PORT

Set to False by default.

SCRIPT_NAME

Subdirectory that the application is accessible under. Must match with the reverse proxy configuration.

It should start but NOT end with a trailing slash.

Automatically configured by the Ansible role using the ht_path variable.

SAMESITE

The SameSite value to add to cookies. Defaults to Lax.

CSRF_COOKIE_AGE

Standard Django variable: #CSRF_COOKIE_AGE

Expiration time of the CSRF cookie. Defaults to 31449600 (1 year in seconds).

SESSION_COOKIE_AGE

Standard Django variable: #SESSION_COOKIE_AGE

Expiration time of the session cookie. Defaults to 31449600 (1 year in seconds).

COOKIE_SECURE

Whether to set the Secure flag on the cookies. This means the cookies are only transferred by the browser over https.

Set by the Ansible role to true if ht_secure is set to true.

Defaults to False.

LANGUAGE_COOKIE_NAME

Standard Django variable: #LANGUAGE_COOKIE_NAME

Set to django_language by default.

CSRF_COOKIE_NAME

Standard Django variable: #CSRF_COOKIE_NAME

Set to csrftoken by default.

SESSION_COOKIE_NAME

Standard Django variable: #SESSION_COOKIE_NAME

Set to sessionid by default.

CSRF_HEADER_NAME

Standard Django variable: #CSRF_HEADER_NAME

Set to HTTP_X_CSRFTOKEN by default.

CSRF_TRUSTED_ORIGINS

Standard Django variable: #CSRF_TRUSTED_ORIGINS

SESSION_EXPIRE_AT_BROWSER_CLOSE

Standard Django variable: #SESSION_EXPIRE_AT_BROWSER_CLOSE

CSRF_USE_SESSIONS

Standard Django variable: #CSRF_USE_SESSIONS

SECURE_HSTS_INCLUDE_SUBDOMAINS

Standard Django variable: #SECURE_HSTS_INCLUDE_SUBDOMAINS

SECURE_HSTS_PRELOAD

Standard Django variable: #SECURE_HSTS_PRELOAD

SECURE_HSTS_SECONDS

Standard Django variable: #SECURE_HSTS_SECONDS

SECURE_PROXY_SSL_HEADER

Standard Django variable: #SECURE_PROXY_SSL_HEADER

Header name followed by value that specifies that the request started out as HTTPS.

Configured to HTTP_X_FORWARDED_PROTO,https by the Ansible role if ht_nginx is set to true.

Unset by default.

SECURE_SSL_HOST

Standard Django variable: #SECURE_SSL_HOST

SECURE_SSL_REDIRECT

Standard Django variable: #SECURE_SSL_REDIRECT

WEB_CONCURRENCY

Standard Gunicorn option: #workers

Amount of worker processes to spawn for handling incoming requests.

Set by the Ansible role to ht_workers (defaults to 4).

This is one of many environment variables read by Gunicorn. Read its documentation to see what other options are available.

Admin interface

The Ansible role deploys a run-ht script in the handtokening user's home directory for running administration commands with the right environment variables set. All arguments provided to the script are passed to systemd-run.

The database migrations are automatically run when the service starts, but you may want to run them manually the first time:

sudo ~handtokening/run-ht --pty --collect django-admin migrate

To create an admin user, run the following command and follow the interactive steps:

sudo ~handtokening/run-ht --pty --collect django-admin createsuperuser

See the Django admin documenation or run django-admin help [<command>] for more details.

Once an admin user is created, you can open the /admin/login/ and sign in. From here, you can create certificates, signing profiles, users/clients, and review signing logs.

You can add timestamping servers via the admin interface or run the following command to import a standard list of servers:

sudo ~handtokening/run-ht --pty --collect django-admin add_timestamp_server --add-standard-servers

Timestamp servers must be added to a signing profile before they're used.

License

Handtokening code signing server. Copyright (C) 2025 Dexter Castor Döpping

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License version 3 as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.

About

Authenticode Code Signing Server that runs on Linux

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages