diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 952f8fd..0000000 --- a/.drone.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -kind: pipeline -type: docker -name: build-images - -image_pull_secrets: - - dockerconfig - -trigger: - event: - - cron - - push - cron: - - build-image - -steps: - - name: build-base-image - image: plugins/docker - settings: - username: - from_secret: docker_username - password: - from_secret: docker_password - registry: h3m6q87t.gra7.container-registry.ovh.net - repo: h3m6q87t.gra7.container-registry.ovh.net/sign-pdf-worker/worker - tag: - - latest - pull_image: true - context: ./pythonProject - dockerfile: ./pythonProject/Dockerfile ---- -kind: signature -hmac: 538ac6bab02bd9fcc1d0124c64135c87c850e07391a5271e7e7864913f3cad61 - -... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..104922e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +local.env \ No newline at end of file diff --git a/local.env.dist b/local.env.dist new file mode 100644 index 0000000..ce1c609 --- /dev/null +++ b/local.env.dist @@ -0,0 +1,7 @@ +AMQP_URL=amqp://guest:guest@localhost:32772/%2f/to_python_sign +LOG_LEVEL=DEBUG +PKCS12_PATH=./assets/dummy.p12 +TIMESTAMP_URL=http://freetsa.org/tsr +QUEUE_IN=to_python_sign +EXCHANGE_OUT=signed_docs +OUT_ROUTING_KEY=signed_doc diff --git a/pythonProject/Dockerfile b/pythonProject/Dockerfile index 472965a..c708cee 100644 --- a/pythonProject/Dockerfile +++ b/pythonProject/Dockerfile @@ -4,6 +4,9 @@ FROM python:3.10-alpine # Set working directory WORKDIR /app +# add required clis +RUN apk add --no-cache openssl + # Copy requirements.txt to the Docker container COPY requirements.txt . diff --git a/pythonProject/sign.py b/pythonProject/sign.py index 4c1d293..079116f 100644 --- a/pythonProject/sign.py +++ b/pythonProject/sign.py @@ -7,11 +7,15 @@ from pyhanko.sign import signers, timestamps, fields from pyhanko_certvalidator import ValidationContext from typing_extensions import Buffer +from pythonProject.timestamp import LocalOpensslTimestamp + class SignOrchestrator: """Orchestrate the signature on document""" - def __init__(self, pkcs12_path: str, timestamp_url: str, pkcs12_password: Optional[bytes] = None): + def __init__(self, pkcs12_path: str, + tsa_config_path: str, tsa_password: str, tsa_cert_chain: str, + pkcs12_password: Optional[bytes] = None): # Load signer key material from PKCS#12 file # This assumes that any relevant intermediate certs are also included # in the PKCS#12 file. @@ -20,9 +24,7 @@ class SignOrchestrator: ) # Set up a timestamping client to fetch timestamps tokens - self.timestamper = timestamps.HTTPTimeStamper( - url=timestamp_url, - ) + self.timestamper = LocalOpensslTimestamp(tsa_config_path, tsa_password, tsa_cert_chain) self.stamp_style = stamp.TextStampStyle( stamp_text="Signé par:\n%(signer_text)s\nLe %(ts)s", diff --git a/pythonProject/sign_individual.py b/pythonProject/sign_individual.py index 9e3c79c..bb9e404 100644 --- a/pythonProject/sign_individual.py +++ b/pythonProject/sign_individual.py @@ -1,6 +1,16 @@ from sign import SignOrchestrator -orchestrator = SignOrchestrator('./assets/dummy.p12','http://freetsa.org/tsr', pkcs12_password=None) +""" +This is a script to sign a file with the dummy assets + +It is created mainly for testing purpose +""" + +orchestrator = SignOrchestrator('./assets/dummy.p12', + '/home/julien/dev/chill/sign-pdf-worker/ts-authority/rootca.conf', + '5678', + '/home/julien/dev/chill/sign-pdf-worker/ts-authority/ca/tsa-chain.pem', + pkcs12_password=None) with open('./assets/test.pdf', 'rb') as input: signed_content = orchestrator.sign(reason="first signer", signature_index=0, @@ -11,9 +21,9 @@ with open('./assets/test.pdf', 'rb') as input: output.write(signed_content.read()) with open('./assets/test_signed_0.pdf', 'rb') as input: - signed_content = orchestrator.sign(reason="second signer", signature_index=1, - input_content=input.read(), box_place=(100, 600, 300, 660), on_page=0, - signer_text="M. Bah Mamadou") + signed_content = orchestrator.sign(reason="second signer", signature_index=1, + input_content=input.read(), box_place=(100, 600, 300, 660), on_page=0, + signer_text="M. Bah Mamadou") - with open('./assets/test_signed_1.pdf', 'wb') as output: - output.write(signed_content.read()) + with open('./assets/test_signed_1.pdf', 'wb') as output: + output.write(signed_content.read()) diff --git a/pythonProject/sign_individual_protected_CA.py b/pythonProject/sign_individual_protected_CA.py new file mode 100644 index 0000000..019d349 --- /dev/null +++ b/pythonProject/sign_individual_protected_CA.py @@ -0,0 +1,29 @@ +from sign import SignOrchestrator + +""" +This is a script to sign a file with the dummy assets + +It is created mainly for testing purpose +""" + +orchestrator = SignOrchestrator('/run/user/1000/ca/cachet.p12', + '/home/julien/dev/chill/sign-pdf-worker/ts-authority/vendee-tsa.conf', + 'xxxxxxxxxxxxxxxxxxx', + '/run/user/1000/ca/tsa-chain.pem', + pkcs12_password=b"xxxxxxxxxxxxxxxx") + +with open('./assets/test.pdf', 'rb') as input: + signed_content = orchestrator.sign(reason="first signer", signature_index=0, + input_content=input.read(), box_place=(300, 600, 500, 660), on_page=0, + signer_text="Mme Caroline Diallo") + + with open('./assets/test_signed_0.pdf', 'wb') as output: + output.write(signed_content.read()) + +with open('./assets/test_signed_0.pdf', 'rb') as input: + signed_content = orchestrator.sign(reason="second signer", signature_index=1, + input_content=input.read(), box_place=(100, 600, 300, 660), on_page=0, + signer_text="M. Bah Mamadou") + + with open('./assets/test_signed_1.pdf', 'wb') as output: + output.write(signed_content.read()) diff --git a/pythonProject/timestamp.py b/pythonProject/timestamp.py new file mode 100644 index 0000000..5916c01 --- /dev/null +++ b/pythonProject/timestamp.py @@ -0,0 +1,64 @@ +import logging +import os + +from asn1crypto import tsp +from asn1crypto.tsp import TimeStampResp +from pyhanko.sign.timestamps import TimeStamper +import subprocess + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(os.environ.get('LOG_LEVEL', logging.DEBUG)) + + +class LocalOpensslTimestamp(TimeStamper): + """ + Apply a timestamp using a local openssl cli. + + The class provides methods to request a timestamp response from a local OpenSSL TSA cli. + """ + def __init__(self, path_config: str, password: str, path_chain: str): + super().__init__() + self.path_conf = path_config + self.password = password + self.path_chain = path_chain + + def request_tsa_response(self, req: tsp.TimeStampReq) -> tsp.TimeStampResp: + with open('/tmp/request.tsq', 'wb') as request: + request.write(req.dump()) + + cmd = [ + 'openssl', 'ts', '-reply', + '-config', self.path_conf, + '-queryfile', '/tmp/request.tsq', + '-chain', self.path_chain, + '-out', '/tmp/response.tsr', + '-passin', f'pass:{self.password}' + ] + + try: + subprocess.run(cmd, capture_output=True, check=True, timeout=10) + except subprocess.CalledProcessError as e: + LOGGER.error("Could not generate a timestamp") + LOGGER.error(f"response code: {e.returncode}") + LOGGER.error(f"stderr: {e.stderr}") + LOGGER.error(f"stdout: {e.stdout}") + LOGGER.error(f"error: {e.output}") + + raise Exception("could not generate a timestamp") + except subprocess.TimeoutExpired as e: + LOGGER.error("timeout expired") + LOGGER.error(f"stderr: {e.stderr}") + LOGGER.error(f"stdout: {e.stdout}") + LOGGER.error(f"error: {e.output}") + + raise e + + with open('/tmp/response.tsr', mode='rb') as f: + tsp = TimeStampResp.load(f.read()) + + os.unlink('/tmp/response.tsr') + + return tsp + + async def async_request_tsa_response(self, req: tsp.TimeStampReq) -> tsp.TimeStampResp: + return self.request_tsa_response(req) diff --git a/pythonProject/worker.py b/pythonProject/worker.py index eb65f46..8197a66 100644 --- a/pythonProject/worker.py +++ b/pythonProject/worker.py @@ -19,13 +19,14 @@ for v in ['AMQP_URL', 'PKCS12_PATH', 'TIMESTAMP_URL', 'QUEUE_IN', 'EXCHANGE_OUT' DSN = os.environ.get('AMQP_URL') PKCS12_PATH = os.environ.get('PKCS12_PATH') -TIMESTAMP_URL = os.environ.get('TIMESTAMP_URL') QUEUE_IN = os.environ.get('QUEUE_IN') EXCHANGE_OUT = os.environ.get('EXCHANGE_OUT') OUT_ROUTING_KEY = os.environ.get('OUT_ROUTING_KEY') +TSA_CONFIG_PATH = os.environ.get('TSA_CONFIG_PATH') +TSA_CERT_CHAIN = os.environ.get('TSA_CERT_CHAIN') +TSA_KEY_PASSWORD = os.environ.get('TSA_KEY_PASSWORD') - -orchestrator = sign.SignOrchestrator(PKCS12_PATH, TIMESTAMP_URL, pkcs12_password=os.environ.get('PKCS12_PASSWORD', None)) +orchestrator = sign.SignOrchestrator(PKCS12_PATH, TSA_CONFIG_PATH, TSA_KEY_PASSWORD, TSA_CERT_CHAIN, pkcs12_password=os.environ.get('PKCS12_PASSWORD', None)) parameters = pika.URLParameters(DSN) connection = pika.BlockingConnection(parameters) diff --git a/ts-authority/.gitignore b/ts-authority/.gitignore new file mode 100644 index 0000000..6b87dd0 --- /dev/null +++ b/ts-authority/.gitignore @@ -0,0 +1,4 @@ +ca/* +!ca/certs/.gitkeep +!ca/db/.gitkeep +!ca/private/.gitkeep \ No newline at end of file diff --git a/ts-authority/README.md b/ts-authority/README.md new file mode 100644 index 0000000..7b8bdd0 --- /dev/null +++ b/ts-authority/README.md @@ -0,0 +1,167 @@ +# Set up CA + +## Sources: + +- https://www.jimby.name/techbits/recent/openssl_tsa/ + +## Create directory + +```bash +# mkdir certs db private ## should already be created +# chmod 700 private ## should already be set +touch db/index +openssl rand -hex 16 > db/serial +echo ‘1001’ > db/crlnumber +echo 01 > tsa_serial +``` + +## Create files for certificate authority + +Create a new private key and root CA certificate request in one step: + +```bash +openssl req -new -newkey rsa:2048 -config ./rootca.conf -out ca/root-ca.csr -keyout ca/private/root-ca.key +``` + +Don’t forget the password – you’ll need it again and again below. + +Now self-sign the certificate request. + +```bash +openssl ca -selfsign -config ./rootca.conf -in ca/root-ca.csr -out ca/root-ca.crt -extensions ca_ext +``` + +Check the certificate: + +```bash +openssl x509 -text -in ca/root-ca.crt -noout +``` + +## Create certificate for timestamp + +To proceed, we first make a key and a certificate request for a non-CA certificate. We use the -subj option so we don’t +have to use a configuration file for this step. The Country (C=US) and Organization (O=Example Inc.) elements must +match the root certificate. + +```bash +openssl req -new \ + -newkey rsa:2048 \ + -subj "/C=US/O=Example Inc./OU=Engineering/CN=Example Inc. TSA Responder" \ + -keyout ca/private/tsa.key \ + -out ca/tsa.csr +``` + + +You should use a different password for the tsa.key private key. + +Then we generate a non-CA certificate using the -extension tsa_ext command line option which points to the required +extendedKeyUsage in the configuration file. + +```bash +openssl ca -config ./rootca.conf -in ca/tsa.csr -out ca/tsa.crt -extensions tsa_ext -days 365 +``` + +Sign with the root-ca.key private key password, and commit to the database. + +Examine the new TSA certificate as follows: + +```bash +openssl x509 -in ca/tsa.crt -text -noout +``` + +Ensure that it has CA: false , keyUsage nonRepudiation, and extendedKeyUsage timeStamping. + +Requests to the time stamp service usually require that the reply include the certificate chain of the service. We now create the certificate chain as follows: + +First, extract just the PEM form of the x509 certificates for root-ca.crt and tsa.crt : + +```bash +openssl x509 -in ca/root-ca.crt -outform PEM -out ca/root-ca.pem +openssl x509 -in ca/tsa.crt -outform PEM -out ca/tsa.pem +``` + +Next, concatenate the two bare certificates ensuring that the root certificate is last in the file: + +```bash +cat ca/tsa.pem ca/root-ca.pem > ca/tsa-chain.pem +``` + +You can verify this chain by just viewing the file: + +```bash +cat ca/tsa-chain.pem +``` + +## Generate a timestamp request + +Ok! We are now ready to create a time stamp request. First, we prepare a query: + +```bash +openssl ts -query -config ./rootca.conf -cert -data /etc/hosts -out /tmp/request.tsq +``` + +View the request with + +```bash +openssl ts -query -in /tmp/request.tsq -text +``` + +Note that since we did not request certificate checking (using the -cert option in the request command above), the text +output of this command shows “Certificate required: no”. Also, note that we did not specify our own configuration +file in the above example. + +If you want to use a stronger digest algorithm, specify it on the command line (sha512 requested here): + +```bash +openssl ts -query -config ./rootca.conf -data /etc/hosts -out /tmp/request.tsr -sha512 +``` + +## Generating a reply + +We can now process a reply to the the request. Note that the openssl ts -reply sub-command does require a configuration +file, including the all the tsa sections. In particular, it uses the tsa_policy1(2,3) options we added at the top of the file. + +Here (and everywhere you utilize the services of the tsa.crt certificate), you must enter the password for the tsa +certificate private key. + +```bash +openssl ts -reply -config ./rootca.conf -queryfile /tmp/request.tsq -chain ca/tsa-chain.pem -out /tmp/response.tsr +``` + +```bash +openssl ts -reply -config ./rootca.conf -in /tmp/response.tsr -text +``` + + +## Verification + + + +Openssl can also verify the received timestamp ensuring that the data file or data digest the query was based on still +applies to the current version of the file. + +```bash +openssl ts -verify -queryfile /tmp/request.tsq -in /tmp/response.tsr -CAfile ca/root-ca.pem -untrusted ca/tsa.pem +``` + +The OK response ensures that the original signed timestamp is correctly authorized by the root and tsa certificates +(in PEM format). + +```bash +openssl ts -verify -data /etc/hosts -in /tmp/response.tsr -CAfile ca/root-ca.pem -untrusted ca/tsa.pem +``` + + +# Préparation pour Vendée + +## Extraire les infos + +```bash +openssl pkcs12 -info -in horodatage.p12 -legacy +``` + +Ca demandera un mot de passe pour déchiffrer, et un autre mot de passe pour chiffrer la clé qui apparaitra. + +- on recopie la clé et on fait un copier-coller dans /run/user/1000/ca/private/tsa.key +- on recopie tous les certificats, on supprime les interligne, et on colle ça dans /run/user/1000/ca/tsa-chain.pem +- on recopie le premier certificat, pour céer /run/user/1000/ca/tsa.crt diff --git a/ts-authority/rootca.conf b/ts-authority/rootca.conf new file mode 100644 index 0000000..2228570 --- /dev/null +++ b/ts-authority/rootca.conf @@ -0,0 +1,168 @@ +# +# rootca.conf +# +# See Ristic OpenSSL Cookbook URL above. + +oid_section = new_oids + +[ new_oids ] +tsa_policy1 = 1.2.3.4.1 +tsa_policy2 = 1.2.3.4.5.6 +tsa_policy3 = 1.2.3.4.5.7 + +###### First Part ######## + +[default] +name = root-ca +domain_suffix = example.com +aia_url = http://$name.$domain_suffix/$name.crt +crl_url = http://$name.$domain_suffix/$name.crl +ocsp_url = http://ocsp.$name.$domain_suffix:9080 +default_ca = ca_default +name_opt = utf8,esc_ctrl,multiline,lname,align + +[ca_dn] +countryName = "US" +organizationName = "Example Inc." +commonName = "Root CA" + +###### Second Part ####### + +[ca_default] +home = . +database = $home/ca/db/index +serial = $home/ca/db/serial +crlnumber = $home/ca/db/crlnumber +certificate = $home/ca/$name.crt +private_key = $home/ca/private/$name.key +RANDFILE = $home/ca/private/random +new_certs_dir = $home/ca/certs +unique_subject = no +copy_extensions = none +default_days = 3650 +default_crl_days = 30 +default_md = sha256 +policy = policy_c_o_match +name = foo@example.com + + +[policy_c_o_match] +countryName = match +stateOrProvinceName = optional +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + + +##### Third Part ####### + +[req] +default_bits = 4096 +encrypt_key = yes +default_md = sha256 +utf8 = yes +string_mask = utf8only +prompt = no +distinguished_name = ca_dn +req_extensions = ca_ext + + +[ca_ext] +basicConstraints = critical,CA:true +keyUsage = critical,keyCertSign,cRLSign +subjectKeyIdentifier = hash + + +####### Fourth Part - Extensions ######## +# +# Value Meaning - see x509v3.cnf(5) +# -------- ------------------------------ +# serverAuth SSL/TLS web server authentication +# clientAuth SSL/TLS web client authentication +# codeSigning code signing +# emailProtection email protection (S/MIME) +# timeStamping trusted doc hash timestamping +# OCSPSigning OCSP Signing +# ipsecIKE IPsec internet key exchange +# msCodeInd Microsoft individual code signing (authenticode) +# msCodeCom Microsoft commercial code signing (authenticode) +# msCTLSign Microsoft trust list signing +# msEFS Microsoft encrypted file system (EFS) + + +[sub_ca_ext] +authorityInfoAccess = @issuer_info +authorityKeyIdentifier = keyid:always +basicConstraints = critical,CA:true,pathlen:0 +crlDistributionPoints = @crl_info +keyUsage = critical,keyCertSign,cRLSign +extendedKeyUsage = clientAuth,serverAuth +nameConstraints = @name_constraints +subjectKeyIdentifier = hash + + +[crl_info] +URI.0 = $crl_url + +[issuer_info] +caIssuers;URI.0 = $aia_url +OCSP;URI.0 = $ocsp_url + +[name_constraints] +permitted;DNS.0=example.com +permitted;DNS.1=example.org +excluded;IP.0=0.0.0.0/0.0.0.0 +excluded;IP.1=0:0:0:0:0:0:0:0/0:0:0:0:0:0:0:0 + + +####### Fifth Part ========== + + +[ocsp_ext] +authorityKeyIdentifier = keyid:always +basicConstraints = critical,CA:false +extendedKeyUsage = OCSPSigning +keyUsage = critical,digitalSignature +subjectKeyIdentifier = hash + + +########### TSA extension ############## +# +# Copied from the OpenSSL CAtsa.cnf test configuration and modified for use as a TSA extension. +# +# + +[ tsa ] + +default_tsa = tsa_config1 + +[ tsa_config1 ] +dir = /home/julien/dev/chill/sign-pdf-worker/ts-authority # TSA root directory, same as root-ca +serial = $dir/ca/tsa_serial # current serial number (mandatory) +signer_cert = $dir/ca/tsa.crt # signing certificate (optional) +certs = $dir/ca/tsa-chain.pem # certification chain (optional) +signer_key = $dir/ca/private/tsa.key # tsa private key (optional) +default_policy = tsa_policy1 +signer_digest = sha256 # digest to use for signing (optional) +other_policies = tsa_policy2,tsa_policy3 # other policies (optional) +digests = sha256,sha384,sha512 # acceptable digests (mandatory) +accuracy = secs:1,millisecs:500,microsecs:100 # accuracy optional +ordering = yes # is ordering defined? (optional, default: no) +tsa_name = yes # must tsa name be included in reply? (opt., default: no) +ess_cert_id_chain = yes # must ess cert id change be incl? (opt., default: no) +ess_cert_id_alg = sha256 # alg to compute cert. id (optional, default: sha1) + +# added, was missing in the blog post +crypto_device = builtin + +# The tsa_ext extension is +# used to create the tsa cert tsa.crt + +[ tsa_ext ] + +authorityKeyIdentifier = keyid:always +basicConstraints = critical,CA:false +extendedKeyUsage = critical,timeStamping +keyUsage = critical,nonRepudiation +subjectKeyIdentifier = hash diff --git a/ts-authority/vendee-tsa.conf b/ts-authority/vendee-tsa.conf new file mode 100644 index 0000000..ed803ce --- /dev/null +++ b/ts-authority/vendee-tsa.conf @@ -0,0 +1,46 @@ +# +# rootca.conf +# +# See Ristic OpenSSL Cookbook URL above. + +oid_section = new_oids + +[ new_oids ] +tsa_policy1 = 1.2.3.4.1 +tsa_policy2 = 1.2.3.4.5.6 +tsa_policy3 = 1.2.3.4.5.7 + + +[ tsa ] + +default_tsa = tsa_config1 + +[ tsa_config1 ] +dir = /run/user/1000/ca # TSA root directory, same as root-ca +serial = $dir/tsa_serial # current serial number (mandatory) +signer_cert = $dir/tsa.crt # signing certificate (optional) +certs = $dir/tsa-chain.pem # certification chain (optional) +signer_key = $dir/private/tsa.key # tsa private key (optional) +default_policy = tsa_policy1 +signer_digest = sha256 # digest to use for signing (optional) +other_policies = tsa_policy2,tsa_policy3 # other policies (optional) +digests = sha256,sha384,sha512 # acceptable digests (mandatory) +accuracy = secs:1,millisecs:500,microsecs:100 # accuracy optional +ordering = yes # is ordering defined? (optional, default: no) +tsa_name = yes # must tsa name be included in reply? (opt., default: no) +ess_cert_id_chain = yes # must ess cert id change be incl? (opt., default: no) +ess_cert_id_alg = sha256 # alg to compute cert. id (optional, default: sha1) + +# added, was missing in the blog post +crypto_device = builtin + +# The tsa_ext extension is +# used to create the tsa cert tsa.crt + +[ tsa_ext ] + +authorityKeyIdentifier = keyid:always +basicConstraints = critical,CA:false +extendedKeyUsage = critical,timeStamping +keyUsage = critical,nonRepudiation +subjectKeyIdentifier = hash