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.
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.
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 = .vaultpasshandtokening.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: reloadedhandtokening-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.ymlIn 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.
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
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.
Determines the log level of the root logger. Set to WARNING by default.
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.
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.
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.
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.
Defaults to osslsigncode which means it will look up the application on the $PATH list.
Defaults to /usr/bin/clamdscan. You could change this to /usr/bin/true to skip ClamAV scans.
Normally set by systemd to /var/lib/handtokening.
Normally set by systemd to /etc/handtokening.
Normally set by systemd to /run/handtokening.
It's normally set by systemd to /home/handtokening.
Used to set the STATIC_ROOT variable unless it's set directly.
Standard Django variable: #STATIC_ROOT.
Used as the destination directory when running django-admin collectstatic.
Standard Django variable: #STATIC_URL
Set to static/ by default.
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.
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.
Standard Django variable: #USE_X_FORWARDED_HOST
Set to False by default.
Standard Django variable: #USE_X_FORWARDED_PORT
Set to False by default.
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.
The SameSite value to add to cookies.
Defaults to Lax.
Standard Django variable: #CSRF_COOKIE_AGE
Expiration time of the CSRF cookie. Defaults to 31449600 (1 year in seconds).
Standard Django variable: #SESSION_COOKIE_AGE
Expiration time of the session cookie. Defaults to 31449600 (1 year in seconds).
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.
Standard Django variable: #LANGUAGE_COOKIE_NAME
Set to django_language by default.
Standard Django variable: #CSRF_COOKIE_NAME
Set to csrftoken by default.
Standard Django variable: #SESSION_COOKIE_NAME
Set to sessionid by default.
Standard Django variable: #CSRF_HEADER_NAME
Set to HTTP_X_CSRFTOKEN by default.
Standard Django variable: #CSRF_TRUSTED_ORIGINS
Standard Django variable: #SESSION_EXPIRE_AT_BROWSER_CLOSE
Standard Django variable: #CSRF_USE_SESSIONS
Standard Django variable: #SECURE_HSTS_INCLUDE_SUBDOMAINS
Standard Django variable: #SECURE_HSTS_PRELOAD
Standard Django variable: #SECURE_HSTS_SECONDS
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.
Standard Django variable: #SECURE_SSL_HOST
Standard Django variable: #SECURE_SSL_REDIRECT
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.
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 migrateTo create an admin user, run the following command and follow the interactive steps:
sudo ~handtokening/run-ht --pty --collect django-admin createsuperuserSee 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-serversTimestamp servers must be added to a signing profile before they're used.
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/.