4
client-enroll.py - certificate enrollment with mainstream web browsers
5
(c) by Michael Stroeder <michael@stroeder.com>
10
########################################################################
11
# CGI-BIN for creating certificate requests
12
########################################################################
14
import os, sys, types, string, re, socket, \
16
cgiforms, cgihelper, charset, ipadr, htmlbase, certhelper
18
from pycacnf import opensslcnf, pyca_section
19
from charset import iso2utf, iso2html4
20
from cgihelper import BrowserType,known_browsers,known_browsers_rev
22
########################################################################
23
# "Internet"-Funktionen
24
########################################################################
26
# Aus einer Mailadresse die Domain herausloesen
27
# Ergebnis ist Teilstring hinter letztem(!) @
28
# falls kein @ vorhanden, dann Leerstring
29
def DomainAdr(MailAdr=""):
30
splitted=string.split(MailAdr,'@')
36
# String der Hilfe-URL zu bestimmtem Parameter zurueckgeben
37
def HelpURL(HelpUrl,name,text):
38
return '<A HREF="%s.html#%s">%s</A>' % \
39
(HelpUrl,name,iso2html4(text))
41
# Print list of possible cert types
42
def PrintCertTypes(ca_names):
43
htmlbase.PrintHeader('Start enrollment for certificate request')
44
htmlbase.PrintHeading('Start enrollment for certificate request')
45
print """This certificate authority issues several types
46
of client certificates.<BR>Please choose the appropriate certificate
48
<TABLE CELLSPACING=10%%>"""
49
for ca_name in ca_names:
50
ca = opensslcnf.getcadata(ca_name)
53
nsCaPolicyUrlStr = '<A HREF="%s%s">(view policy)' % (ca.nsBaseUrl,ca.nsCaPolicyUrl)
55
nsCaPolicyUrlStr = ' '
56
print '<TR><TD><A HREF="%s/%s">%s</A></TD><TD>%s</TD><TD>%s</TD></TR>' % (os.environ.get('SCRIPT_NAME','client-enroll.py'),ca_name,ca_name,ca.nsComment,nsCaPolicyUrlStr)
58
htmlbase.PrintFooter()
60
# Ausgabe eines leeren Eingabeformulars
61
def PrintEmptyForm(form,ca_name,scriptmethod='POST'):
62
print '<FORM ACTION="%s/%s" METHOD="%s" ACCEPT-CHARSET="iso-8859-1">' % (os.environ.get('SCRIPT_NAME','client-enroll.py'),ca_name,scriptmethod)
63
print '<TABLE WIDTH=100% BORDER>'
65
for j in form.field[i]:
66
print '<TR><TD>%s%s</TD><TD>%s</TD></TR>' % (HelpURL(HelpUrlBase,j.name,j.text),' <FONT SIZE="+2">*</FONT>'*j.required,j.inputfield())
68
print '<INPUT TYPE="submit" VALUE="Send">'
69
print '<INPUT TYPE="reset" VALUE="Reset">'
73
def PrintInput(form,cellpadding=5,width=100):
74
print '<TABLE BORDER CELLPADDING=%d%% WIDTH=%d%%>' % (cellpadding,width)
75
formKeys = form.inputkeys[:]
77
formKeys.remove('challenge')
82
for j in form.field[i]:
83
print '<TR><TD WIDTH=35%%>%s</TD><TD>%s</TD></TR>' % \
84
(HelpURL(HelpUrlBase,j.name,j.text),j.contentprint())
88
# Ausgabe eines Formulars zur Schuesselerzeugung
89
def PrintKeygenForm(form,ca_name,ca,browsertype,scriptmethod='POST'):
91
print """Content-type: text/html\n
94
<TITLE>Create key pair and certificate request</TITLE>
96
if browsertype=='MSIE':
98
vbs.PrintVBSXenrollObject()
99
print '<SCRIPT Language=VBSCRIPT>\n<!-- '
100
vbs.PrintVBSCryptoProvider()
101
vbs.PrintVBSKeyGenCode(form)
102
print ' -->\n</SCRIPT>'
103
print '</HEAD><BODY onLoad=KeySizeSelectList() %s>' % htmlbase.bodyPARAM
104
htmlbase.PrintHeading('Create key pair and certificate request')
105
print 'Your key pair and certificate request can be generated now.<BR>'
106
print 'Please have a look below to check if your input data was correct.<P>'
107
# Print input given by user as readable table and hidden input fields
109
print '<FORM name="KeyGenForm" ACTION="%s/%s" METHOD=%s ACCEPT-CHARSET="iso-8859-1">' % (os.environ.get('SCRIPT_NAME','client-enroll.py'),ca_name,scriptmethod)
110
for i in form.inputkeys:
111
for j in form.field[i]:
112
print '<INPUT TYPE="hidden" NAME="%s" VALUE="%s">' % (j.name,j.content)
114
# Print hint about minimum key size
115
if ca.min_key_size>0:
116
print """Please note:<BR>
117
The certificate type <STRONG>%s</STRONG> requires a minimum key size of <STRONG>%d</STRONG> bits!
118
If you are not able to choose a key length equal or greater than <STRONG>%d</STRONG> the
119
certificate authority will refuse to issue a certificate for your certificate request!<P>
120
""" % (ca_name,ca.min_key_size,ca.min_key_size)
122
if browsertype=='MSIE':
123
print '<P>Key size: <SELECT NAME="KeySize"></SELECT></P><INPUT TYPE="hidden" NAME="PKCS10" VALUE="">'
124
print '<INPUT TYPE="BUTTON" onClick="GenTheKeyPair()" VALUE="Generate key pair"></FORM>'
126
print '<P>%s:%s</P><INPUT TYPE="submit" VALUE="Generate key pair"></FORM>' % ( \
127
HelpURL(HelpUrlBase,form.field['SPKAC'][0].name,form.field['SPKAC'][0].text),\
128
form.field['SPKAC'][0].inputfield(form.field['challenge'][0].content) \
130
htmlbase.PrintFooter()
132
########################################################################
134
########################################################################
136
# Read several parameters from config
138
MailRelay = pyca_section.get('MailRelay','localhost')
139
TmpDir = pyca_section.get('TmpDir','/tmp')
141
caCertReqMailAdr = pyca_section.get('caCertReqMailAdr','')
142
caPendCertReqValid = string.atoi(pyca_section.get('caPendCertReqValid','0'))
144
caInternalCertTypes = pyca_section.get('caInternalCertTypes',[])
145
if type(caInternalCertTypes)!=types.ListType:
146
caInternalCertTypes = [caInternalCertTypes]
148
caInternalIPAdr = pyca_section.get('caInternalIPAdr',['127.0.0.1/255.255.255.255'])
149
if type(caInternalIPAdr)!=types.ListType:
150
caInternalIPAdr = [caInternalIPAdr]
152
caInternalDomains = pyca_section.get('caInternalDomains','')
153
if type(caInternalDomains)!=types.ListType:
154
caInternalDomains = [caInternalDomains]
156
ScriptMethod = pyca_section.get('ScriptMethod','POST')
158
# Read wanted certificate type from PATH_INFO
159
ca_name = os.environ.get('PATH_INFO','')[1:]
161
# Get list of possible certificate type from config
162
ca_names = opensslcnf.sectionkeys.get('ca',[])
164
# Check for valid certificate type
166
htmlbase.PrintErrorMsg('No certificate authorities found.')
169
PrintCertTypes(ca_names)
171
if not ca_name in ca_names:
172
# CA-Definition nicht in openssl-Konfiguration enthalten
173
htmlbase.PrintErrorMsg('Unknown certificate authority "%s".' % ca_name)
176
# Check for "internal" IP address of client
177
if (ca_name in caInternalCertTypes) and \
178
not ipadr.MatchIPAdrList(os.environ.get('REMOTE_ADDR',''),caInternalIPAdr):
179
htmlbase.PrintErrorMsg('This type of certificate request is restricted to internal hosts!')
182
ca = opensslcnf.getcadata(ca_name)
184
HelpUrlBase = '%s%s%s' % ( \
186
pyca_section.get('HelpUrl',''), \
187
os.path.splitext(os.path.basename(os.environ.get('SCRIPT_NAME','')))[0] \
190
policy_section = opensslcnf.data.get(ca.policy,{})
191
req_section = opensslcnf.data.get(ca.req,{})
193
if req_section and req_section.has_key('distinguished_name'):
194
req_distinguished_name_section = opensslcnf.data.get(req_section['distinguished_name'],{})
195
req_distinguished_name_keys = opensslcnf.sectionkeys.get(req_section['distinguished_name'],[])
197
htmlbase.PrintErrorMsg('Request section for "%s" not found.' % ca_name)
200
# Hier Verwendungszweck der Zertifikate pruefen
201
if not ca.isclientcert():
202
htmlbase.PrintErrorMsg('Certificate authority "%s" does not issue client certificates.' % ca_name)
205
# form initialisieren
206
form = cgiforms.formClass(charset='iso-8859-1')
208
# Die gueltigen Inputattribute setzen
209
alphanumregex = r'[0-9a-zA-Z\344\366\374\304\326\334\337ļæ½/\'"._ -]*'
210
# telephoneregex = r'^\+[0-9][0-9]-[0-9]*-[0-9]*'
212
# Check which browser is used
213
http_browsertype,http_browserversion = BrowserType(os.environ.get('HTTP_USER_AGENT',''))
214
key_gen_browsers = {'Microsoft Internet Explorer':('PKCS10','pem'),'Netscape Navigator':('SPKAC','spkac'),'Opera':('SPKAC','spkac')}
215
if not known_browsers.get(http_browsertype,http_browsertype) in key_gen_browsers.keys():
218
form.add(cgiforms.formSelectClass('browsertype','Browser Software',key_gen_browsers.keys(),known_browsers.get(http_browsertype,''),required=1))
219
form.add(cgiforms.formPasswordClass('challenge','Initial Master Secret',30,alphanumregex,required=1))
221
# The form is build by looking at a [req] section in openssl.cnf
225
for i in req_distinguished_name_keys:
226
l = string.split(i,'_')
227
attr_name = string.strip(l[0])
228
if not attr_name in dn_attr_keys:
229
dn_attr_keys.append(attr_name)
230
dn_attr[attr_name]={'comment':'','max':'40','regex':alphanumregex,'default':''}
232
dn_attr[attr_name][l[1]]=req_distinguished_name_section.get(i,'')
234
dn_attr[attr_name]['comment']=req_distinguished_name_section.get(i,attr_name)
236
for i in dn_attr_keys:
237
imaxlength=string.atoi(dn_attr[i].get('max','40'))
242
policy_field = policy_section.get(i,'optional')
243
if policy_field=='match':
244
if type(dn_attr[i]['default'])==types.ListType:
245
dn_attr[i]['default']=dn_attr[i]['default'][0]
246
form.add(cgiforms.formHiddenInputClass(i,dn_attr[i]['comment'],imaxlength,dn_attr[i]['regex'],dn_attr[i]['default'],required=1,show=1))
248
if type(dn_attr[i]['default'])==types.ListType:
249
dn_attr[i]['default'].sort()
250
form.add(cgiforms.formSelectClass(i,dn_attr[i]['comment'],dn_attr[i]['default'],required=policy_field=='supplied'))
252
form.add(cgiforms.formInputClass(i,dn_attr[i]['comment'],imaxlength,dn_attr[i]['regex'],dn_attr[i]['default'],required=policy_field=='supplied',size=isize))
254
# Schon Parameter vorhanden?
255
if not form.contentlength:
259
# Aufruf erfolgte ohne Parameter =>
260
# 0. Schritt: leeres Eingabeformular ausgeben
263
ca.nsComment = 'No comment'
265
nsCommentStr = '<A HREF="%s%s">%s</A>' % (ca.nsBaseUrl,ca.nsCaPolicyUrl,ca.nsComment)
267
nsCommentStr = ca.nsComment
269
htmlbase.PrintHeader('Input form for certificate request')
270
htmlbase.PrintHeading('Input form for certificate request')
272
if not http_browsertype:
273
print '<P><STRONG>Your browser type could not be automatically determined.<BR>Please choose the browser you are using.</STRONG></P>'
276
<TR><TD>Certificate authority:</TD><TD><STRONG>%s</STRONG></TD></TR>
277
<TR><TD>Certificate type:</TD><TD><STRONG>%s</STRONG></TD></TR>
278
<TR><TD>Certificate comment:</TD><TD><STRONG>%s</STRONG></TD></TR>
281
Certificates of this type will be valid for <STRONG>%d days</STRONG>, approximately until <STRONG>%s</STRONG>.
287
time.strftime('%Y-%m-%d',time.gmtime(time.time()+86400*ca.default_days))
289
print """You can apply for a certificate by filling out the input form below.
290
Click on the names of the parameters to get further informations about the
291
usage and format restrictions of the input data.<P>
292
Required input parameters are marked with *.
294
PrintEmptyForm(form,ca_name)
295
htmlbase.PrintFooter()
298
# 1. und 2. Schritt haben Schluesselfeld
299
form.add(cgiforms.formInputClass('KeySize','Key Size',100,alphanumregex))
301
cgiforms.formInputClass(
310
form.add(cgiforms.formKeygenClass('SPKAC','Public Key and Challenge',6000))
312
# Aufruf erfolgte mit Parametern
314
form.getparams(ignoreemptyparams=1)
315
except cgiforms.formContentLengthException,e:
316
htmlbase.PrintErrorMsg('Content length invalid.')
318
except cgiforms.formParamNameException,e:
319
htmlbase.PrintErrorMsg('Unknown parameter "%s".' % (e.name))
321
except cgiforms.formParamsMissing,e:
322
htmlbase.PrintHeader('Error')
323
htmlbase.PrintHeading('Error')
324
print """The following parameter(s) is/are missing:<P>
328
Required input parameters are marked with *.
329
""" % (string.join(map(lambda x: x[1],e.missing),'<LI>'))
330
for k in ['PKCS10','KeySize','SPKAC']:
335
for i in form.inputkeys:
336
form.field[i][0].default=form.field[i][0].content
337
PrintEmptyForm(form,ca_name)
338
htmlbase.PrintFooter()
340
except cgiforms.formParamContentException,e:
341
htmlbase.PrintHeader('Error')
342
htmlbase.PrintHeading('Error')
343
print 'Content of field "%s" has invalid format.<P>' % (e.text)
344
form.keys.remove(RequestDataKey)
345
for i in form.inputkeys:
346
form.field[i][0].default=form.field[i][0].content
347
PrintEmptyForm(form,ca_name)
348
htmlbase.PrintFooter()
350
except cgiforms.formParamStructException,e:
351
htmlbase.PrintErrorMsg('Too many (%d) parameters for field "%s".' % (e.count,e.name))
353
except cgiforms.formParamLengthException,e:
354
htmlbase.PrintErrorMsg('Content too long. Field "%s" has %d characters.' % (e.text,e.length))
357
if 'browsertype' in form.inputkeys and \
358
form.field['browsertype'][0].content in key_gen_browsers.keys():
359
browsertype = known_browsers_rev[form.field['browsertype'][0].content]
361
browsertype = http_browsertype
363
RequestDataKey,request_filenamesuffix = key_gen_browsers[known_browsers[browsertype]]
365
##############################################################################
366
# Zusaetzliche Ueberpruefungen diverser Parameter
367
##############################################################################
369
if 'commonName' in form.inputkeys:
370
commonName = form.field.get('commonName',[''])[0].content
373
if 'emailAddress' in form.inputkeys:
374
emailAddress = form.field.get('emailAddress',[''])[0].content
378
# Check for "internal" mail domain
379
if (ca_name in caInternalCertTypes) and \
380
not (DomainAdr(emailAddress) in caInternalDomains):
381
htmlbase.PrintErrorMsg('This type of certificate request is restricted to internal address domains!')
384
if not (RequestDataKey in form.inputkeys and form.field[RequestDataKey][0].content):
386
# Aufruf erfolgte mit Parametern ohne Schluessel =>
387
# 1. Schritt: Eingegebene Daten anzeigen und
388
# Benutzer zur Schluesselerzeugung auffordern
390
PrintKeygenForm(form,ca_name,ca,browsertype)
393
# Aufruf erfolgte mit Parametern inkl. Schluessel =>
394
# 2. Schritt: Forminhalt bearbeiten
396
# Check the required key length if min_key_size was defined
397
if ca.min_key_size>0:
399
# This is a very primitive and falsy key length checking!!!
400
# Only useful for SPKAC
401
minbytes={512:200,768:300,1024:400}
402
if minbytes.has_key(ca.min_key_size) and len(form.field[RequestDataKey][0].content)<minbytes[ca.min_key_size]:
403
htmlbase.PrintErrorMsg('The key length you submitted was too weak!<BR>The certificate type <STRONG>%s</STRONG> requires a minimum key size of <STRONG>%d</STRONG> bits!' % (ca_name,ca.min_key_size))
406
# Zufaellige ChallengeID erzeugen und daraus eindeutige, noch nicht
407
# existierende Dateinamen mittels MD5-Hash basteln
409
import random, md5, binascii
411
# Zufaellige ID fuer Antwort vom Benutzer
412
caChallengeId = md5.new('%d' % random.randint(0,99999999))
413
formKeys = form.inputkeys[:]
415
for j in form.field[i]:
416
caChallengeId.update(j.content)
418
# ca_name und ChallengeId fuer Mail-Subjects und Dateinamen
419
camailSubject = 'cert-req-%s.%s.%s' % (RequestDataKey,ca_name,string.replace(binascii.b2a_base64(caChallengeId.digest()),'/','_')[:-1])
420
request_filename = os.path.join(ca.pend_reqs_dir,'%s.%s' % (camailSubject,request_filenamesuffix))
422
if os.path.exists(request_filename):
423
# Versuch nicht existierenden Dateinamen zu basteln schlug fehl.
424
# Duerfte eigentlich nicht passieren.
425
htmlbase.PrintErrorMsg('Error generating a random ID or creating temporary files.')
428
##############################################################################
430
##############################################################################
432
request_file = open(request_filename,'w')
434
if RequestDataKey=='PKCS10':
436
request_file.write("""-----BEGIN CERTIFICATE REQUEST-----
438
-----END CERTIFICATE REQUEST-----
439
""" % (form.field['PKCS10'][0].content))
441
elif RequestDataKey=='SPKAC':
443
# FIX ME! This won't work with additional parameters of [ new_oids ] section
444
# CertRequestKeys = ['countryName','stateOrProvinceName','localityName','organizationName','organizationalUnitName','commonName','initials','uid','emailAddress','SPKAC']
445
CertRequestKeys = filter(
446
lambda i: not i in ['challenge','browsertype'],
449
for i in CertRequestKeys:
450
if (i in form.inputkeys) and form.field[i][0].content:
451
request_file.write('%s = %s\n' % (i,form.field[i][0].content))
454
os.chmod(request_filename,0444)
456
##############################################################################
457
# Send a nice e-mail with random ID to user to initiate mail dialogue
458
##############################################################################
460
if caCertReqMailAdr and emailAddress:
462
import smtplib, mimify
465
to_addr = '%s <%s>' % (mimify.mime_encode_header(commonName),emailAddress)
467
to_addr = '%s' % (emailAddress)
470
mail_msg = """From: %s
474
Someone (maybe you) has sent a certificate request
475
to our certificate authority.
477
Please answer this e-mail with the same subject
478
if this was really you and the data below is correct.
480
If someone abused your name / e-mail address simply
481
forget about this message and delete it. %s
483
------------- Identity Information -------------
484
""" % (caCertReqMailAdr,\
487
(caPendCertReqValid>0)*(' The certificate\nrequest will be removed automatically after %d hours.' % caPendCertReqValid)
489
# Hier den eigentlichen Cert-Req an Mailbody anhaengen
490
formKeys = form.keys[:]
491
for unwantedkey in ['challenge','browsertype',RequestDataKey]:
493
formKeys.remove(unwantedkey)
497
mail_msg_paramlist = []
499
if (i in form.inputkeys) and form.field[i][0].content:
500
mail_msg_paramlist.append('%s = %s' % (i,form.field[i][0].content))
501
mail_msg = '%s%s' % (mail_msg,string.join(mail_msg_paramlist,'\n'))
504
smtpconn=smtplib.SMTP(MailRelay)
505
smtpconn.set_debuglevel(0)
507
smtpconn.sendmail(caCertReqMailAdr,[to_addr],mail_msg)
509
htmlbase.PrintErrorMsg(
510
'Unable to send an e-mail to <B>%s</B>!<BR>Please provide your correct and valid %s or ask your system administrator.' % \
512
charset.escapeHTML(to_addr),
513
HelpURL(HelpUrlBase,'emailAddress','e-mail address')
519
htmlbase.PrintErrorMsg('Unable to contact default mail server!')
522
# Schliesslich und endlich...
523
htmlbase.PrintHeader('Certificate request stored.')
524
htmlbase.PrintHeading('Certificate request is stored.')
525
print """Your certificate request was stored for
526
further processing."""
528
if caCertReqMailAdr and emailAddress:
529
print """<P>You will get an e-mail message with a random ID.
530
Please answer this e-mail to confirm your certificate request."""
532
if caPendCertReqValid:
533
print """Otherwise your certificate request will be
534
removed automatically after %d hour(s).<P>""" % (caPendCertReqValid)
536
print '<P>Once again the data you gave to us:<P>'
538
htmlbase.PrintFooter()