Code-signing server built in Python/Django
Project description
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.
Defaults to handtokening.settings.local.
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/.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file handtokening-1.0.2.tar.gz.
File metadata
- Download URL: handtokening-1.0.2.tar.gz
- Upload date:
- Size: 29.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7690c6074bab323454e593819dc4debb87741636298865c9e6baafeb50479fe5
|
|
| MD5 |
03fe5262a52727da8b7639686fecf470
|
|
| BLAKE2b-256 |
df81aba28e6c1568ae8a52df463da7008eb9388b7dcb43a59189bf2122058fb3
|
Provenance
The following attestation bundles were made for handtokening-1.0.2.tar.gz:
Publisher:
python-publish.yml on dextercd/Handtokening
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
handtokening-1.0.2.tar.gz -
Subject digest:
7690c6074bab323454e593819dc4debb87741636298865c9e6baafeb50479fe5 - Sigstore transparency entry: 563677861
- Sigstore integration time:
-
Permalink:
dextercd/Handtokening@c3391f32f5163d2431ad94793c30b3e43f869f75 -
Branch / Tag:
refs/tags/release-1.0.2 - Owner: https://github.com/dextercd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@c3391f32f5163d2431ad94793c30b3e43f869f75 -
Trigger Event:
release
-
Statement type:
File details
Details for the file handtokening-1.0.2-py3-none-any.whl.
File metadata
- Download URL: handtokening-1.0.2-py3-none-any.whl
- Upload date:
- Size: 47.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a05806e63a86b1292a5834c07a65664f529336178fb6a14f93aeb6c5ee9d5dfa
|
|
| MD5 |
9e0a123f294c94c6403fc0fece0d8007
|
|
| BLAKE2b-256 |
ef974dfd2db6a3da090ad550ad25a7d3404020c29db77f94da6d549dbe67ec2f
|
Provenance
The following attestation bundles were made for handtokening-1.0.2-py3-none-any.whl:
Publisher:
python-publish.yml on dextercd/Handtokening
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
handtokening-1.0.2-py3-none-any.whl -
Subject digest:
a05806e63a86b1292a5834c07a65664f529336178fb6a14f93aeb6c5ee9d5dfa - Sigstore transparency entry: 563677862
- Sigstore integration time:
-
Permalink:
dextercd/Handtokening@c3391f32f5163d2431ad94793c30b3e43f869f75 -
Branch / Tag:
refs/tags/release-1.0.2 - Owner: https://github.com/dextercd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@c3391f32f5163d2431ad94793c30b3e43f869f75 -
Trigger Event:
release
-
Statement type: