Related
i need to create a CSR with specific configuration using python
this is my configuration :
oid_section = OIDs
[ OIDs ]
certificateTemplateName= 1.3.6.1.4.1.311.20.2
[ req ]
default_bits = 2048
emailAddress = test#gmail.com
req_extensions = v3_req
x509_extensions = v3_ca
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[ dn ]
C=SA
OU=3111111117
O=shesh
CN = tat-1
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment
[req_ext]
certificateTemplateName = ASN1:PRINTABLESTRING:PREZATCA-Code-Signing
subjectAltName = dirName:alt_names
[alt_names]
SN=1-Device|2-234|3-mohamm
UID=30000000000000003
title=1000
registeredAddress=Zatca 12
businessCategory=Technology
i can create a CSR with this configuration using OpenSSL
but i need to Create a CSR with this configuration using Python.
i tried to do it using this code:
from OpenSSL.SSL import FILETYPE_PEM
from OpenSSL.crypto import dump_certificate_request, dump_privatekey,dump_publickey, PKey, TYPE_DSA, X509Req
# create public/private key
key = PKey()
key.generate_key(TYPE_DSA,1028)
print(key.to_cryptography_key())
# Generate CSR
req = X509Req()
req.get_subject().CN = 'localhost'
req.get_subject().O = 'XYZ Widgets Inc'
req.get_subject().OU = 'IT Department'
req.get_subject().L = 'Seattle'
req.get_subject().ST = 'Washington'
req.get_subject().C = 'US'
req.get_subject().emailAddress = 'e#example.com'
req.set_pubkey(key)
req.sign(key, 'sha256')
with open("csr_testo.pem", 'wb+') as f:
f.write(dump_certificate_request(FILETYPE_PEM, req))
with open("Private_key_testo.pem", 'wb+') as f:
f.write(dump_privatekey(FILETYPE_PEM, key))
with open("public_key_testo.pem", 'wb+') as f:
f.write(dump_publickey(FILETYPE_PEM, key))
but it does not take all of my configuration.
[alt_names]
SN=1-Device|2-234|3-mohamm
UID=30000000000000003
title=1000
registeredAddress=Zatca 12
businessCategory=Technology
these configurations are very important to include them in the CSR
Please try this code
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import dsa
from cryptography.x509.oid import NameOID, ExtensionOID
# Generate a private key
private_key = dsa.generate_private_key(key_size=2048)
# Set the subject name and add extensions
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "SA"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "shesh"),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "3111111117"),
x509.NameAttribute(NameOID.COMMON_NAME, "tat-1"),
])
extensions = [
x509.BasicConstraints(ca=False, path_length=None),
x509.KeyUsage(digital_signature=True, key_encipherment=True, content_commitment=False, data_encipherment=False, key_agreement=False, encipher_only=False, decipher_only=False),
x509.SubjectAlternativeName([
x509.DirectoryName(x509.Name([
x509.NameAttribute(NameOID.SERIAL_NUMBER, "1-Device|2-234|3-mohamm"),
x509.NameAttribute(NameOID.USER_ID, "30000000000000003"),
x509.NameAttribute(NameOID.TITLE, "1000"),
x509.NameAttribute(NameOID.REGISTERED_ADDRESS, "Zatca 12"),
x509.NameAttribute(NameOID.BUSINESS_CATEGORY, "Technology"),
]))
]),
x509.CertificatePolicies([
x509.PolicyInformation(x509.ObjectIdentifier("1.3.6.1.4.1.311.20.2"), [])
])
]
# Generate the CSR
csr = x509.CertificateSigningRequestBuilder().subject_name(subject).add_extensions(extensions).sign(private_key, hashes.SHA256())
# Save the CSR and private key to files
with open("csr_testo.pem", "wb") as f:
f.write(csr.public_bytes(encoding=x509.Encoding.PEM))
with open("private_key_testo.pem", "wb") as f:
f.write(private_key.private_bytes(encoding=x509.Encoding.PEM, format=x509.PrivateFormat.PKCS8, encryption_algorithm=x509.NoEncryption()))
I build my own certificates chain and try to verify it.
When I have only 2 certificates (the root CA and the to-be-verified): it works fine.
When I have 3 (the root CA, an intermediate, and the to-be-verified): I get the message "invalid CA certificate".
Here is a minimalist code :
import OpenSSL
def create_cert_chain ( nb ):
chain = []
issuer_key = None
issuer_name = None
is_root = True
for i in range( nb ):
my_name = b'certificate #%d'%i # certificate #0 is the root CA
cert = OpenSSL.crypto.X509()
my_key = OpenSSL.crypto.PKey()
my_key . generate_key( OpenSSL.crypto.TYPE_RSA,2048 )
if is_root :
cert.add_extensions( [OpenSSL.crypto.X509Extension(b"subjectKeyIdentifier", False, b"hash",subject=cert)])
cert.add_extensions( [OpenSSL.crypto.X509Extension(b"authorityKeyIdentifier", False, b"keyid:always",issuer=cert)])
cert.add_extensions( [OpenSSL.crypto.X509Extension(b"basicConstraints", False, b"CA:TRUE,pathlen:5")])
cert.add_extensions( [OpenSSL.crypto.X509Extension(b"keyUsage", False, b"keyCertSign, cRLSign")])
cert.get_subject().O = my_name
cert.get_issuer().O = my_name if is_root else issuer_name
cert.gmtime_adj_notBefore( 0 )
cert.gmtime_adj_notAfter( 365*24*60*60 )
cert.set_pubkey( my_key )
cert.sign( my_key if is_root else issuer_key,'sha256' )
chain += [cert]
issuer_key = my_key
issuer_name = my_name
is_root = False
return chain
def check_chain ( nb ):
chain = create_cert_chain( nb )
store = OpenSSL.crypto.X509Store()
for cert in chain[:-1] :
print( 'store certificate%s'%cert.get_subject().O )
store.add_cert( cert )
print( 'check certificate%s'%chain[-1].get_subject().O )
ctx = OpenSSL.crypto.X509StoreContext( store,chain[-1] )
ctx.verify_certificate()
print('ok')
check_chain( 2 ) # works fine
check_chain( 3 ) # fails with "invalid CA certificate"
What do I wrong ?
I suspect a mistake about the extensions but I can't figure out what it is...
And by the way, why does it works with only 2 certificates ?
I found out my mistake : the basic contraint CA:TRUE is not only to be applied to the root certificate, but to all the intermediate ones.
So, the correct test is if i < nb-1 : (instead of if is_root :).
Add if I want to be as must restrictive as possible about the path length, I have to set it to nb-i-2.
And by the way, for my need, all other extensions are optional.
I am using the "cryptography" module in Python to create self-signed certificates for testing.
I followed the examples here "https://cryptography.io/en/latest/x509/tutorial.html" and "https://gist.github.com/bloodearnest/9017111a313777b9cce5", and have the following code so far --
def generate_selfsigned_cert(hostname, san_list=None):
"""Generates self signed certificate for a hostname, and optional IP addresses."""
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from datetime import datetime
from datetime import timedelta
import ipaddress
# Generate pvt key
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
# Write key to file
pvt_key = self.log_directory + '/server.key.pem'
with open(pvt_key, 'wb') as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
# Create cert
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
x509.NameAttribute(NameOID.COUNTRY_NAME, 'X'),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, 'X'),
x509.NameAttribute(NameOID.LOCALITY_NAME, 'X'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'X'),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, 'X'),
x509.NameAttribute(NameOID.EMAIL_ADDRESS, 'X')
])
# best practice seem to be to include the hostname in the SAN, which *SHOULD* mean COMMON_NAME is ignored.
alt_names = [x509.DNSName(hostname)]
# allow addressing by IP, for when you don't have real DNS (common in most testing scenarios)
if san_list:
for addr in san_list:
# openssl wants DNSnames for ips...
alt_names.append(x509.DNSName(addr))
# ... whereas golang's crypto/tls is stricter, and needs IPAddresses
# note: older versions of cryptography do not understand ip_address objects
alt_names.append(x509.IPAddress(ipaddress.ip_address(addr)))
san = x509.SubjectAlternativeName(alt_names)
# path_len=0 means this cert can only sign itself, not other certs.
basic_contraints = x509.BasicConstraints(ca=True, path_length=0)
key_usage = x509.KeyUsage(digital_signature=True, key_encipherment=True, key_cert_sign=True,
key_agreement=False, content_commitment=False, data_encipherment=False,
crl_sign=False, encipher_only=False, decipher_only=False)
extended_key_usage = x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH])
subject_key = x509.SubjectKeyIdentifier(digest=key.public_key())
authority_key = x509.AuthorityKeyIdentifier(key_identifier=key.public_key(), authority_cert_issuer=None, authority_cert_serial_number=None)**
cert = x509.CertificateBuilder()\
.subject_name(subject)\
.issuer_name(issuer)\
.public_key(key.public_key())\
.serial_number(x509.random_serial_number())\
.not_valid_before(datetime.utcnow())\
.not_valid_after(datetime.utcnow() + timedelta(days=10 * 365))\
.add_extension(basic_contraints, False)\
.add_extension(san, False)\
.add_extension(key_usage, True)\
.add_extension(extended_key_usage, False)\
.sign(key, hashes.SHA256())
** .add_extension(subject_key, False)
.add_extension(authority_key, False)**
# Write cert to file
server_cert = self.log_directory + '/server.cert.pem'
with open(server_cert, 'wb') as f:
f.write(cert.public_bytes(encoding=serialization.Encoding.PEM))
When I run this, I get an error --
> File
> "/teams/subhish/pyuniti/projects/sqa/scripts/BSL/TC_31v03_03_01_02_01_ASCG_configuration_and_bring_up.py",
> line 219, in generate_selfsigned_cert
> x509.CertificateBuilder() File "/venvs/subhish/lib/python3.8/site-packages/cryptography/x509/base.py",
> line 723, in sign
> return backend.create_x509_certificate(self, private_key, algorithm) File
> "/venvs/subhish/lib/python3.8/site-packages/cryptography/hazmat/backends/openssl/backend.py",
> line 1035, in create_x509_certificate
> self._create_x509_extensions( File "/venvs/subhish/lib/python3.8/site-packages/cryptography/hazmat/backends/openssl/backend.py",
> line 1141, in _create_x509_extensions
> x509_extension = self._create_x509_extension(handlers, extension) File
> "/venvs/subhish/lib/python3.8/site-packages/cryptography/hazmat/backends/openssl/backend.py",
> line 1182, in _create_x509_extension
> ext_struct = encode(self, extension.value) File "/venvs/subhish/lib/python3.8/site-packages/cryptography/hazmat/backends/openssl/encode_asn1.py",
> line 393, in _encode_subject_key_identifier
> return _encode_asn1_str_gc(backend, ski.digest) File "/venvs/subhish/lib/python3.8/site-packages/cryptography/hazmat/backends/openssl/encode_asn1.py",
> line 74, in _encode_asn1_str_gc
> s = _encode_asn1_str(backend, data) File "/venvs/subhish/lib/python3.8/site-packages/cryptography/hazmat/backends/openssl/encode_asn1.py",
> line 54, in _encode_asn1_str
> res = backend._lib.ASN1_OCTET_STRING_set(s, data, len(data)) TypeError: object of type '_RSAPublicKey' has no len()
The documentation says this --
digest
Type: bytes
The binary value of the identifier. An alias of key_identifier.
But I can't figure out how to generate the key_identifier with python/cryptography. It is my understanding this value needs to be generated from the public_key as per RFC5280 (4.2.1.2, 4.2.1.1) from the private_key.
In the end I want to generate a certificate with the following X509 extensions --
X509v3 extensions:
X509v3 Subject Key Identifier:
CA:38:62:01:AA:0D:AA:EC:18:55:E4:A6:93:36:32:F5:97:F2:5F:88
X509v3 Authority Key Identifier:
keyid:CA:38:62:01:AA:0D:AA:EC:18:55:E4:A6:93:36:32:F5:97:F2:5F:88
X509v3 Basic Constraints:
CA:TRUE, pathlen:0
X509v3 Key Usage: critical
Digital Signature, Key Encipherment, Certificate Sign
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Subject Alternative Name:
DNS:abcd.xyz.net
Any help would be greatly appreciated. Thanks - Subhish
SubjectKeyIdentifier and AuthorityKeyIdentifier do not take the public key in their constructors. If you want to construct a typical identifier via the public key then you should use AuthorityKeyIdentifier.from_issuer_public_key and SubjectKeyIdentifier.from_public_key.
Yep, that worked.
Modified the lines like this --
subject_key = x509.SubjectKeyIdentifier.from_public_key(key.public_key())
authority_key = x509.AuthorityKeyIdentifier.from_issuer_public_key(key.public_key())
Thanks Paul!
I found Xades Signature for Python GitHub. My plan is to apply Xades-EPES signature to XML files. According to the work from GitHub, it is capable to perform this process, but I could not make this run.
test_xades.py has two methods. I receive a error message when I try to run it. Well, the issue is that I am not sure about if the lib can sign Xades-EPES or how to achieve it.
Thank you in advance
CODE:
import unittest
from datetime import datetime
from os import path
from OpenSSL import crypto
import xmlsig
from xades import XAdESContext, template, utils, ObjectIdentifier
from xades.policy import GenericPolicyId, ImpliedPolicy
from basex import parse_xml, BASE_DIR
class TestXadesSignature(unittest.TestCase):
def test_verify(self):
root = parse_xml('data/sample.xml')
sign = root.xpath(
'//ds:Signature', namespaces={'ds': xmlsig.constants.DSigNs}
)[0]
ctx = XAdESContext()
ctx.verify(sign)
def test_sign(self):
root = parse_xml('data/unsigned-sample.xml')
sign = root.xpath(
'//ds:Signature', namespaces={'ds': xmlsig.constants.DSigNs}
)[0]
policy = GenericPolicyId(
'http://www.facturae.es/politica_de_firma_formato_facturae/'
'politica_de_firma_formato_facturae_v3_1.pdf',
u"Politica de Firma FacturaE v3.1",
xmlsig.constants.TransformSha1
)
ctx = XAdESContext(policy)
with open(path.join(BASE_DIR, "data/keyStore.p12"), "rb") as key_file:
ctx.load_pkcs12(crypto.load_pkcs12(key_file.read()))
ctx.sign(sign)
ctx.verify(sign)
def test_create(self):
root = parse_xml('data/free-sample.xml')
signature = xmlsig.template.create(
xmlsig.constants.TransformInclC14N,
xmlsig.constants.TransformRsaSha1,
"Signature"
)
signature_id = utils.get_unique_id()
ref = xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha1, uri="", name="REF"
)
xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped)
xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha1, uri="#KI"
)
xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha1, uri="#" + signature_id
)
ki = xmlsig.template.ensure_key_info(signature, name='KI')
data = xmlsig.template.add_x509_data(ki)
xmlsig.template.x509_data_add_certificate(data)
serial = xmlsig.template.x509_data_add_issuer_serial(data)
xmlsig.template.x509_issuer_serial_add_issuer_name(serial)
xmlsig.template.x509_issuer_serial_add_serial_number(serial)
xmlsig.template.add_key_value(ki)
qualifying = template.create_qualifying_properties(
signature, name=utils.get_unique_id()
)
props = template.create_signed_properties(
qualifying, name=signature_id
)
template.add_claimed_role(props, "Supp2")
template.add_production_place(props, city='Madrid')
template.add_production_place(
props, state='BCN', postal_code='08000', country='ES')
template.add_claimed_role(props, "Supp")
policy = GenericPolicyId(
'http://www.facturae.es/politica_de_firma_formato_facturae/'
'politica_de_firma_formato_facturae_v3_1.pdf',
u"Politica de Firma FacturaE v3.1",
xmlsig.constants.TransformSha1
)
root.append(signature)
ctx = XAdESContext(policy)
with open(path.join(BASE_DIR, "data/keyStore.p12"), "rb") as key_file:
ctx.load_pkcs12(crypto.load_pkcs12(key_file.read()))
ctx.sign(signature)
ctx.verify(signature)
def test_create_2(self):
root = parse_xml('data/free-sample.xml')
signature = xmlsig.template.create(
xmlsig.constants.TransformInclC14N,
xmlsig.constants.TransformRsaSha1,
"Signature"
)
ref = xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha1, uri="", name="R1"
)
xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped)
xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha1, uri="#KI", name="RKI"
)
ki = xmlsig.template.ensure_key_info(signature, name='KI')
data = xmlsig.template.add_x509_data(ki)
xmlsig.template.x509_data_add_certificate(data)
serial = xmlsig.template.x509_data_add_issuer_serial(data)
xmlsig.template.x509_issuer_serial_add_issuer_name(serial)
xmlsig.template.x509_issuer_serial_add_serial_number(serial)
xmlsig.template.add_key_value(ki)
qualifying = template.create_qualifying_properties(signature)
utils.ensure_id(qualifying)
utils.ensure_id(qualifying)
props = template.create_signed_properties(
qualifying, datetime=datetime.now()
)
template.add_claimed_role(props, "Supp")
signed_do = template.ensure_signed_data_object_properties(props)
template.add_data_object_format(
signed_do,
"#R1",
identifier=ObjectIdentifier("Idenfitier0", "Description")
)
template.add_commitment_type_indication(
signed_do,
ObjectIdentifier("Idenfitier0", "Description"),
qualifiers_type=["Tipo"]
)
template.add_commitment_type_indication(
signed_do,
ObjectIdentifier("Idenfitier1", references=["#R1"]),
references=["#R1"]
)
template.add_data_object_format(
signed_do,
"#RKI",
description="Desc",
mime_type="application/xml",
encoding='UTF-8'
)
root.append(signature)
ctx = XAdESContext(ImpliedPolicy(xmlsig.constants.TransformSha1))
with open(path.join(BASE_DIR, "data/keyStore.p12"), "rb") as key_file:
ctx.load_pkcs12(crypto.load_pkcs12(key_file.read()))
ctx.sign(signature)
from lxml import etree
print(etree.tostring(root))
ctx.verify(signature)
x= TestXadesSignature()
x.test_create()
x.test_create_2()
TRACEBACK:
Exception has occurred: StopIteration
exception: no description
File "/home/sergio/Escritorio/PROYECTOSMAY2018/haciendaPython/src/lxml/lxml.etree.pyx", line 2821, in lxml.etree._ElementMatchIterator.__next__ (src/lxml/lxml.etree.c:75265)
File "/home/sergio/Escritorio/PROYECTOSMAY2018/haciendaPython/haciendaCode/pythoncorella/test/test_xades.py", line 50, in test_create
xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped)
File "/home/sergio/Escritorio/PROYECTOSMAY2018/haciendaPython/haciendaCode/pythoncorella/test/test_xades.py", line 150, in <module>
x.test_create()
File "/usr/lib/python3.6/runpy.py", line 85, in _run_code
exec(code, run_globals)
File "/usr/lib/python3.6/runpy.py", line 96, in _run_module_code
mod_name, mod_spec, pkg_name, script_name)
File "/usr/lib/python3.6/runpy.py", line 263, in run_path
pkg_name=pkg_name, script_name=fname)
Use this imports
import xmlsig
from lxml import etree
from OpenSSL import crypto
from xades import XAdESContext, template, utils
from xades.policy import GenericPolicyId
GLOBAL IS THIS
POLICY_ENDPOINT = "politicadefirma/v2/politicadefirmav2.pdf"
SIGN_POLICY = f"http://facturaelectronica.dian.gov.co/{POLICY_ENDPOINT}"
CERTICAMARA_PFX = os.environ.get(
'CERTICAMARA_PFX',
'/path/to/certificate.pfx')
you need to parse the xml file you want to sign
parsed_file = etree.parse(file_path).getroot()
Create Signature template with the corresponding Transform stuff
signature = xmlsig.template.create(
xmlsig.constants.TransformInclC14N,
xmlsig.constants.TransformRsaSha256,
"Signature",
)
create an uuid and the reference to the signature
signature_id = utils.get_unique_id()
ref = xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha256, uri="", name="REF"
)
Create transform for the signature reference
xmlsig.template.add_transform(ref, xmlsig.constants.TransformEnveloped)
Add the other references
xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha256, uri="#" + signature_id
)
xmlsig.template.add_reference(
signature, xmlsig.constants.TransformSha256, uri="#" + signature_id
)
add the part where the certificate is going to be incorporated
ki = xmlsig.template.ensure_key_info(signature, name="KI")
data = xmlsig.template.add_x509_data(ki)
xmlsig.template.x509_data_add_certificate(data)
serial = xmlsig.template.x509_data_add_issuer_serial(data)
xmlsig.template.x509_issuer_serial_add_issuer_name(serial)
xmlsig.template.x509_issuer_serial_add_serial_number(serial)
xmlsig.template.add_key_value(ki)
qualifying = template.create_qualifying_properties(
signature, name=utils.get_unique_id(), etsi='xades'
)
Add any additional data por the signature
props = template.create_signed_properties(qualifying, name=signature_id)
# Additional data for signature
template.add_claimed_role(props, "Supp2")
template.add_production_place(props, city="Bogotá Colombia")
template.add_production_place(
props, state="BCN", postal_code="08000", country="CO"
)
template.add_claimed_role(props, "SNE")
Add policy info
policy = GenericPolicyId(
SIGN_POLICY,
u"Política de firma para facturas"
" electrónicas de la República de Colombia",
xmlsig.constants.TransformSha256,
)
Append the signature to the parsed document
parsed_file.append(signature)
Open the .pfx or .pem, this has to contain both key and cert
with open(CERTICAMARA_ANDES_PFX, "rb") as key_file:
certificate = crypto.load_pkcs12(key_file.read(), 'filepassword')
Add certificate and policy to ctx for signig in the next step
ctx = XAdESContext(
policy,
certificate.get_certificate().to_cryptography(),
)
Load the certificate to the ctx and perform the signing
ctx.load_pkcs12(certificate)
ctx.sign(signature)
Move the signature to the desired position, you can changed to fit your pourposes.
parsed_file[0][position][0].append(signature)
Create the new xml signed file
et = etree.ElementTree(parsed_file)
nfs_name = 'Name of the signed file'
et.write(nfs_name, pretty_print=True,
encoding='utf-8', xml_declaration=True)
I used this for a project for electronic invoice signing in my country, take into account that the source code of Enric Tobella is ahead compared to the package. With the source code you can add multiple certificates whereas the package only let you use one, that was the only difference I found.
I've taken all the forks of gpg-mailgate and put all the working parts together to get it almost totally working. The last issue I"m having is that attachments come through encrpted as filename.originalextension.pgp BUT are undecryptable.
Here is the full code of the mailgate plugin as I have it working.
#!/usr/bin/python
from ConfigParser import RawConfigParser
from email.mime.base import MIMEBase
import email
import email.message
import re
import GnuPG
import smtplib
import sys
# Read configuration from /etc/gpg-mailgate.conf
_cfg = RawConfigParser()
_cfg.read('/etc/gpg-mailgate.conf')
cfg = dict()
for sect in _cfg.sections():
cfg[sect] = dict()
for (name, value) in _cfg.items(sect):
cfg[sect][name] = value
# Read e-mail from stdin
raw = sys.stdin.read()
raw_message = email.message_from_string( raw )
from_addr = raw_message['From']
to_addrs = sys.argv[1:]
def send_msg( message, recipients = None ):
if recipients == None:
recipients = to_addrs
if cfg.has_key('logging') and cfg['logging'].has_key('file'):
log = open(cfg['logging']['file'], 'a')
log.write("Sending email to: <%s>\n" % '> <'.join( recipients ))
log.close()
relay = (cfg['relay']['host'], int(cfg['relay']['port']))
smtp = smtplib.SMTP(relay[0], relay[1])
smtp.sendmail( from_addr, recipients, message.as_string() )
def encrypt_payload( payload, gpg_to_cmdline ):
gpg = GnuPG.GPGEncryptor( cfg['gpg']['keyhome'], gpg_to_cmdline )
raw_payload = payload.get_payload(decode=True)
gpg.update( raw_payload )
if "-----BEGIN PGP MESSAGE-----" in raw_payload and "-----END PGP MESSAGE-----" in raw_payload:
return payload
payload.set_payload( gpg.encrypt() )
if payload['Content-Disposition']:
payload.replace_header( 'Content-Disposition', re.sub(r'filename="([^"]+)"', r'filename="\1.pgp"', payload['Content-Disposition']) )
if payload['Content-Type']:
payload.replace_header( 'Content-Type', re.sub(r'name="([^"]+)"', r'name="\1.pgp"', payload['Content-Type']) )
# if payload.get_content_type() != 'text/plain' and payload.get_content_type != 'text/html':
if 'name="' in payload['Content-Type']:
payload.replace_header( 'Content-Type', re.sub(r'^[a-z/]+;', r'application/octet-stream;', payload['Content-Type']) )
payload.set_payload( "\n".join( filter( lambda x:re.search(r'^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$',x), payload.get_payload().split("\n") ) ) )
return payload
def encrypt_all_payloads( payloads, gpg_to_cmdline ):
encrypted_payloads = list()
if type( payloads ) == str:
msg = email.message.Message()
msg.set_payload( payloads )
return encrypt_payload( msg, gpg_to_cmdline ).as_string()
for payload in payloads:
if( type( payload.get_payload() ) == list ):
encrypted_payloads.append( encrypt_all_payloads( payload.get_payload(), gpg_to_cmdline ) )
else:
encrypted_payloads.append( [encrypt_payload( payload, gpg_to_cmdline )] )
return sum(encrypted_payloads, [])
def get_msg( message ):
if not message.is_multipart():
return message.get_payload()
return '\n\n'.join( [str(m) for m in message.get_payload()] )
keys = GnuPG.public_keys( cfg['gpg']['keyhome'] )
gpg_to = list()
ungpg_to = list()
for to in to_addrs:
domain = to.split('#')[1]
if domain in cfg['default']['domains'].split(','):
if to in keys:
gpg_to.append( (to, to) )
elif cfg.has_key('keymap') and cfg['keymap'].has_key(to):
gpg_to.append( (to, cfg['keymap'][to]) )
else:
ungpg_to.append(to)
if gpg_to == list():
if cfg['default'].has_key('add_header') and cfg['default']['add_header'] == 'yes':
raw_message['X-GPG-Mailgate'] = 'Not encrypted, public key not found'
send_msg( raw_message )
exit()
if ungpg_to != list():
send_msg( raw_message, ungpg_to )
if cfg.has_key('logging') and cfg['logging'].has_key('file'):
log = open(cfg['logging']['file'], 'a')
log.write("Encrypting email to: %s\n" % ' '.join( map(lambda x: x[0], gpg_to) ))
log.close()
if cfg['default'].has_key('add_header') and cfg['default']['add_header'] == 'yes':
raw_message['X-GPG-Mailgate'] = 'Encrypted by GPG Mailgate'
gpg_to_cmdline = list()
gpg_to_smtp = list()
for rcpt in gpg_to:
gpg_to_smtp.append(rcpt[0])
gpg_to_cmdline.extend(rcpt[1].split(','))
encrypted_payloads = encrypt_all_payloads( raw_message.get_payload(), gpg_to_cmdline )
raw_message.set_payload( encrypted_payloads )
send_msg( raw_message, gpg_to_smtp )
My client ( both roundcube and k-9 ), do not know what to do with the file.
From command line if I do a gpg --decrypt filename.txt.pgp I get:
gpg: no valid OpenPGP data found.
gpg: decrypt_message failed: eof
Headers of email are:
User-Agent: K-9 Mail for Android
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----YR49011GO0MWM753ETYWUA7CBOKGAV"
Subject: New with attachments
From: Bruce Markey <bruce#secryption.com>
Date: Fri, 13 Sep 2013 06:18:03 -0400
To: "bruce#packetaddiction.com" <bruce#packetaddiction.com>
Message-ID: <53178821-6d6c-4b7d-b9c7-5a49034da1ef#email.android.com>
X-GPG-Mailgate: Encrypted by GPG Mailgate
I'm not even sure "what" to debug since everything looks ok, and there is a total lack of errors.
If anyone has any direction I'd appreciate it.
Update:
I came across this Decrypt gpg file attached from email (file.pgp)
I decided to add a line to write raw to a log file.
MIME-Version: 1.0
X-Received: by 10.68.130.1 with SMTP id oa1mr18868651pbb.35.1379162867744;
Sat, 14 Sep 2013 05:47:47 -0700 (PDT)
Received: by 10.68.46.72 with HTTP; Sat, 14 Sep 2013 05:47:47 -0700 (PDT)
Date: Sat, 14 Sep 2013 08:47:47 -0400
Message-ID: <CACRtyey-L9Z5JGNG4bheYqJ7tVK+6qfigmanH9pTUk0ute5gEw#mail.gmail.com>
Subject: Test with attachment - Saturday
From: Bruce Markey <bmarkey#gmail.com>
To: bruce#packetaddiction.com
Content-Type: multipart/mixed; boundary=047d7b10ca15d1c7b904e65760eb
--047d7b10ca15d1c7b904e65760eb
Content-Type: text/plain; charset=ISO-8859-1
Just a simple test with txt attachment
--047d7b10ca15d1c7b904e65760eb
Content-Type: text/plain; charset=US-ASCII; name="TestAttach.txt"
Content-Disposition: attachment; filename="TestAttach.txt"
Content-Transfer-Encoding: base64
X-Attachment-Id: f_hlkty4930
VGhpcyBpcyBqdXN0IGEgdGVzdCBvZiB0aGUgYXR0YWNobWVudHMuIApUaGlzIGlzIGEgc2ltcGxl
IHRleHQgZmlsZS4gCgo=
--047d7b10ca15d1c7b904e65760eb--
Since this is being written raw it's pre encryption. So should I be decoding the base64 prior to encryption?
After staring at this for awhile I don't understand why this line is here.
if 'name="' in payload['Content-Type']:
payload.replace_header( 'Content-Type', re.sub(r'^[a-z/]+;', r'application/octet- stream;', payload['Content-Type']) )
payload.set_payload( "\n".join( filter( lambda x:re.search(r'^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$',x), payload.get_payload().split("\n") ) ) )
Why change to application/octet-stream?
UPDATE:
I think I got it working unless I'm doing something horribly wrong. I changed the following:
def get_msg( message ):
if not message.is_multipart():
return message.get_payload()
return '\n\n'.join( [base64.decodestring(str(m)) for m in message.get_payload()] )
This now allows me to actually run gpg --decrypt filename.txt .
( I assume that most attachments will come through as base64 although I'll probably add a test for all content-transfer-encoding types. )
The attachment needed to be decoded, content-transfer-encoding, prior to encrypting. See code in update of original question.