$!-----------------------------------------------------------------'f$verify(0) $! CLIENT_CERT_REQUEST.COM $! $! WASD VMS Web Services, Copyright (C) 1996-2010 Mark G.Daniel. $! This program comes with ABSOLUTELY NO WARRANTY. $! This is free software, and you are welcome to redistribute it under the $! conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or later version. $! http://www.gnu.org/licenses/gpl.txt $! $! NOTE: This procedure is for experimental purposes only! $! It is not intended as a production script for a CA!! $! Use at your own risk!!! $! $! The server account requires read+write access to the OpenSSL configuration $! and certificate directories. The easiest way to provide this with WASD $! v7.1 and following is to enable /PERSONA and adding an HTTPD$MAP entry $! directing this script should be executed using the SYSTEM account. $! $! set /cgi-bin/client_cert_request/* script=as=SYSTEM $! $! This script will operate in one of two ways depending on configuration. $! $! AUTOMATIC CERT, ON-DEMAND, ON-LINE FOR ANY USER $! ----------------------------------------------- $! Automatically generating client certificates for any user, on-demand, $! online, over the Web using a Netscape browser. $! $! When the user accesses the script via a GET request it provides an HTML form $! that allows a private-key and CSR to be submitted back to the script as a $! POSTed request. This is then processed by the OpenSSL CA utility to create $! the certificate, which is the returned direct to the browser as a $! "application/x-x509-user-cert" response. The user would access the script $! using something like the following URL $! $! https://host.name/cgi-bin/client_cert_request $! $! Setup $! ----- $! Copy the procedure into WASD_ROOT:[SCRIPT_LOCAL] with appropriate permissions. $! $! In HTTPD$MAP $! $! set /cgi-bin/client_cert_request script=as=SYSTEM $! $! $! AUTOMATIC CSR FOR ANY USER, SITE ADMIN GENERATED CERT $! ----------------------------------------------------- $! By providing an interface and script for a user to generate a CSR and $! send that via email to the site administrator. Then for that administrator $! to use the same script as an interface and script (under authorization $! controls) to generate the certificate. $! $! When the user accesses the script via a GET request it provides an HTML $! form that allows a private-key and CSR to be submitted back to the script $! as a POSTed request. The fields are collated and this is then emailed to $! a specified address. The user would access the script using something like $! the following URL $! $! https://host.name/cgi-bin/client_cert_request $! $! The site administartor access the script with an additional, authorization $! controlled path. For example $! $! https://host.name/cgi-bin/client_cert_request/admin/client_cert_request $! $! The admin is provided with an HTML form that allows cut-and-paste of the $! user CSR into a text area, duration in days is set, and the CA passphrase $! entered, and then the form submitted. The OpenSSL CA utility is used to $! generate the certificate, which is placed in PEM format into the site's $! certificate storage directory. $! $! A page containing this PEM certificate is returned to he site administrator. $! There is a URL at the top of the page. This allows the user to download $! the certificate into the browser. This page may be emailed to the user. $! $! Setup $! ----- $! Copy the procedure into WASD_ROOT:[SCRIPT_LOCAL] with appropriate permissions. $! $! In HTTPD$AUTH $! $! [whatever-realm] $! /admin/client_cert_request/* r+w,whatever-account $! $! In HTTPD$MAP $! $! set /cgi-bin/client_cert_request/* script=as=SYSTEM $! $! $! OTHER CONSIDERATIONS $! -------------------- $! When generating certificates the procedure requires write access to scratch $! space. By default either WASD_ROOT:[SCRATCH] (v7.1.1 or later HT_SCRATCH) or $! WASD_ROOT:[LOGS.SERVER] is used. $! $! This script may be easily cloned - just copy it to a new procedure name! $! To allow procedure problem resultuion define a logical name $! $DEBUG. For instance, for this original procedure the $! logical could be defined using $! $! $ DEFINE /SYSTEM CLIENT_CERT_REQUEST$DEBUG 1 $! $! ***** THIS HAS BEEN TESTED IN THE DEFAULT WASD OPENSSL DIRECTORY TREE ***** $! MILEAGE MAY VARY USING OTHER SETUPS! $! $! VERSION HISTORY $! --------------- $! 04-APR-2010 MGD OpenSSL v1.0.0 use OPENSSL_CONF instead of -config $! 18-DEC-2000 MGD initial $!----------------------------------------------------------------------------- $! $! Configurable Parameters ... $! $! Enabling this value allows users to generate client certificates on-line $! (provided the script is correctly configured of course). $! Disabling this value allows them to generate a CSR which can then $! optionally be emailed to the site administrator who can then use this $! same script to complete the process. $! $ onDemandGenerate = 0 $ CSRemailAddress = "SYSTEM" $! $! Leave these empty to have the procedure configure itself for the standard $! WASD OpenSSL setup (WASD_ROOT:[SRC.OPENSSL-n_n_n]). If one is explicitly set $! all must be set. $! $ OpenSSLdir = "" !top-level location of OpenSSL pacakge $ certBinaryDir = "" !location of generated binary certificates $ defaultConfig = "" !location of configuration file $ opensslEXE = "" !foreign verb for OPENSSL.EXE program $ scratchDir = "" !directory the script can read+write to $ CApassphrase = "" !CA certificate password (only set for on-demand) $ durationDays = "365" !duration of client certificate $! $!----------------------------------------------------------------------------- $! $ set noon $ procName = f$parse(f$environment("procedure"),,,"name") $ debug = procName + "$DEBUG" $ debug = f$trnlnm(debug) $ if debug $ then $ type sys$input Content-Type: text/plain $ show sym * $ set verify $ endif $! $ say = "write sys$output" $ saysym = "write/symbol sys$output" $ lf[0,8] = 10 $ CGIutl = "$HT_EXE:CGIUTL" $ sysOutput = f$trnlnm("SYS$OUTPUT") $ sysError = f$trnlnm("SYS$ERROR") $ NORMAL_STATUS = %x00000001 $ ERROR_STATUS = %x10000004 $! $ pathInfoAdmin = "/admin/" + f$edit(procName,"lowercase") + "/" $! $ if onDemandGenerate $ then adminGenerate = 0 $ else adminGenerate = 1 $ endif $! $!(search for latest version of OpenSSL in default WASD location) $ if OpenSSLdir .eqs. "" $ then $ sslDirLoop: $ tmpDir = f$search("WASD_ROOT:[SRC]OPENSSL-*_*_*.DIR") $ if tmpDir .eqs. "" then goto end_sslDirLoop $ sslDirPos = f$locate("]OPENSSL",tmpDir) $ OpenSSLdir = f$extract(0,sslDirPos,tmpDir) + "." + - f$extract(sslDirPos+1,999,tmpDir) - ".DIR;1" + "]" $ goto sslDirLoop $ end_sslDirLoop: $! $ if OpenSSLdir .eqs. "" $ then $ call certRequestFailed "Cannot locate WASD OpenSSL directory structure." $ exit $ endif $! $ parentDir = f$extract(0,f$length(openSSLdir)-1,openSSLdir) $ defaultDir = parentDir + ".WASD]" $ defaultConfig = parentDir + ".WASD]DEFAULT.CNF" $ PEMcertDir = parentDir + ".WASD.CERT]" $ opensslEXE = "$" + parentDir + ".AXP.EXE.APPS]OPENSSL" $! $ endif $! $!(add the CA utility parameter) $ opensslCA = opensslEXE + " CA" $ opensslX509 = opensslEXE + " X509" $! $ if scratchDir .eqs. "" $ then $ if f$trnlnm("HT_SCRATCH") .eqs. "" $ then scratchDir = "HT_SCRATCH:" $ else scratchDir = "SYS$SCRATCH:" $ endif $ endif $! $!(set up the file names at top-level, so accessable to all subroutines) $ CSRfileName = scratchDir + procName + "_REQ." + WWW_UNIQUE_ID $ CAoutputFileName = scratchDir + procName + "_CAO." + WWW_UNIQUE_ID $ randFileName = scratchDir + procName + "_RND." + WWW_UNIQUE_ID $ certFileName = scratchDir + procName + "_DER." + WWW_UNIQUE_ID $ PEMfileName = PEMcertDir + procName + "." + WWW_UNIQUE_ID $! $!----------------------------------------------------------------------------- $! $!(end of configuration, begin main execution routine) $ if WWW_PATH_INFO .eqs. pathInfoAdmin $ then $! (site admin phase) $ clientPhase = 0 $ if WWW_REMOTE_USER .eqs. "" $ then $ call certRequestFailed - "Access to ""''pathInfoAdmin'"" must be authorization controlled!" $ exit $ endif $ if WWW_REQUEST_METHOD .eqs. "GET" $ then $ call adminCertRequestForm $ else $! (POSTed form) $ call adminSaveCSR $ if .not. $status then exit $ durationDays = WWW_FORM_DURATION $ if f$integer(durationDays) .le 0 then durationDays = 365 $ if f$type(CApassphrase) .eqs. "" then delete/symbol CApassphrase $ CApassphrase = WWW_FORM_CAPASSPHRASE $ call generateCert $ if .not. $status then exit $ call adminReturnCert $ if .not. $status then exit $ endif $ else $! (client phase) $ clientPhase = 1 $ if f$length(WWW_PATH_INFO) - 1 .eq. f$length(WWW_UNIQUE_ID) $ then $ call userReturnAdminCert "''f$extract(1,999,WWW_PATH_INFO)'" $ else $ if WWW_REQUEST_METHOD .eqs. "GET" $ then $ call userCertRequestForm $ if .not. $status then exit $ else $! (POSTed form) $ if onDemandGenerate $ then $ call userBuildCSR $ if .not. $status then exit $ call generateCert $ if .not. $status then exit $ call userReturnCert $ if .not. $status then exit $ else $ if CSRemailAddress .eqs. "" $ then $ call certRequestFailed "CSR email address not set." $ exit NORMAL_STATUS $ endif $ call userBuildCSR $ if .not. $status then exit $ call userEmailCSR $ if .not. $status then exit $ endif $ endif $ endif $ endif $ call cleanup $! $!(exit from procedure) $ exit $! $!----------------------------------------------------------------------------- $! $!(build Certificate Signing Request in file) $ userBuildCSR: subroutine $ set noon $! $!(use CGIutl to extract the POSTed form body into DCL symbols) $ CGIutl /urldecode /symbol /prefix="WWW_FORM" $ if debug then show symbol WWW_FORM_* $! $! (fail if not Netscape browser) $ if f$type(WWW_FORM_SPKAC) .eqs. "" $ then $ call certRequestFailed "No Key provided (SPKAC). Netscape required." $ exit ERROR_STATUS $ endif $! $!(check that we've got all the essentials) $ ok = 1 $ if f$type(WWW_FORM_COMMONNAME) .eqs. "" then ok = 0 $ if f$type(WWW_FORM_EMAILADDRESS) .eqs. "" then ok = 0 $ if f$type(WWW_FORM_ORGANIZATIONNAME) .eqs. "" then ok = 0 $ if f$type(WWW_FORM_ORGANIZATIONALUNITNAME) .eqs. "" then ok = 0 $ if f$type(WWW_FORM_LOCALITYNAME) .eqs. "" then ok = 0 $ if f$type(WWW_FORM_STATEORPROVINCENAME) .eqs. "" then ok = 0 $ if f$type(WWW_FORM_COUNTRYNAME) .eqs. "" then ok = 0 $ if .not. ok $ then $ call certRequestFailed "All the required information is not available." $ exit ERROR_STATUS $ endif $! $ if .not. debug then define sys$output nl: $ if .not. debug then define sys$error nl: $ create 'CSRfileName' /fdl=SYS$INPUT: FILE ORGANIZATION sequential RECORD CARRIAGE_CONTROL carriage_return FORMAT stream_LF $ open /append CSRfile 'CSRfileName' $ WWW_FORM_COMMONNAME = "commonName = " + WWW_FORM_COMMONNAME $ write /symbol CSRfile WWW_FORM_COMMONNAME $ WWW_FORM_EMAILADDRESS = "emailAddress = " + WWW_FORM_EMAILADDRESS $ write /symbol CSRfile WWW_FORM_EMAILADDRESS $ WWW_FORM_ORGANIZATIONNAME = "organizationName = " + WWW_FORM_ORGANIZATIONNAME $ write /symbol CSRfile WWW_FORM_ORGANIZATIONNAME $ WWW_FORM_ORGANIZATIONALUNITNAME = "organizationalUnitName = " + WWW_FORM_ORGANIZATIONALUNITNAME $ write /symbol CSRfile WWW_FORM_ORGANIZATIONALUNITNAME $ WWW_FORM_LOCALITYNAME = "localityName = " + WWW_FORM_LOCALITYNAME $ write /symbol CSRfile WWW_FORM_LOCALITYNAME $ WWW_FORM_STATEORPROVINCENAME = "stateOrProvinceName = " + WWW_FORM_STATEORPROVINCENAME $ write /symbol CSRfile WWW_FORM_STATEORPROVINCENAME $ WWW_FORM_COUNTRYNAME = "countryName = " + WWW_FORM_COUNTRYNAME $ write /symbol CSRfile WWW_FORM_COUNTRYNAME $!(the SPKAC has embedded newlines, it need transforming into ASN.1 syntax) $ write CSRfile "SPKAC = \" $ SPKACloop: $ lfpos = f$locate(lf,WWW_FORM_SPKAC) $ if lfpos+1 .ge. f$length(WWW_FORM_SPKAC) $ then $! (must be the last and non-newline delimited extract, no continuation!) $ line = f$extract(0,lfpos,WWW_FORM_SPKAC) $ WWW_FORM_SPKAC = "" $ else $! (append a continuation character to this newline-delimited extract) $ line = f$extract(0,lfpos,WWW_FORM_SPKAC) + "\" $ WWW_FORM_SPKAC = f$extract(lfpos+1,999,WWW_FORM_SPKAC) $ endif $ write /symbol CSRfile line $ if WWW_FORM_SPKAC .nes. "" then goto SPKACloop $ SPKACloopEnd: $ close CSRfile $ define /nolog sys$output 'sysOutput' $ define /nolog sys$error 'sysError' $! $ if f$search(CSRfileName) .eqs. "" $ then $ call certRequestFailed "Error saving CSR file." $ exit ERROR_STATUS $ endif $ if f$file(CSRfileName,"EOF") .eqs. 0 $ then $ call certRequestFailed "Error populating CSR file." $ exit $ endif $! $ exit NORMAL_STATUS $ endsubroutine $! $!----------------------------------------------------------------------------- $! $ adminSaveCSR: subroutine $ set noon $! $!(use CGIutl to extract the POSTed form body into DCL symbols and file) $ CGIutl /quiet /urldecode /symbols /prefix="WWW_FORM" $ if debug $ then $ show sym www_form_* $ write sys$output f$length(WWW_FORM_CSR) $ endif $! $ if f$type(WWW_FORM_CSR) .eqs. "" $ then $ call certRequestFailed "CSR not available." $ exit $ endif $ if f$length(WWW_FORM_CSR) .lt. 512 $ then $ call certRequestFailed "CSR looks doubtful." $ exit $ endif $! $ if .not. debug then define sys$output nl: $ if .not. debug then define sys$error nl: $ create 'CSRfileName' /fdl=SYS$INPUT: FILE ORGANIZATION sequential RECORD CARRIAGE_CONTROL carriage_return FORMAT stream_LF $ open /append CSRfile 'CSRfileName' $ write /symbol CSRfile WWW_FORM_CSR $ close CSRfile $ define /nolog sys$output 'sysOutput' $ define /nolog sys$error 'sysError' $! $ if f$search(CSRfileName) .eqs. "" $ then $ call certRequestFailed "Error saving CSR file." $ exit ERROR_STATUS $ endif $ if f$file(CSRfileName,"EOF") .eqs. 0 $ then $ call certRequestFailed "Error populating CSR file." $ exit ERROR_STATUS $ endif $! $ exit NORMAL_STATUS $ endsubroutine $! $!----------------------------------------------------------------------------- $! $ generateCert: subroutine $ set noon $! $ if CApassphrase .eqs. "" $ then $ call certRequestFailed "CA passphrase not specified." $ exit ERROR_STATUS $ endif $! $ if f$type(defaultDir) .nes "" then set default 'defaultDir' $ if .not. debug then define sys$output nl: $ if .not. debug then define sys$error nl: $ directory *.* $ if .not. $status $ then $ call certRequestFailed "Directory setup problem." $ exit ERROR_STATUS $ endif $! $!(initialize the random number seed file) $ create 'randFileName' /fdl=SYS$INPUT: FILE ORGANIZATION sequential RECORD CARRIAGE_CONTROL carriage_return FORMAT stream_LF $ open /append randFile 'randFileName' $ show system /output=randFile $ close randFile $ define /nolog sys$output 'sysOutput' $ define /nolog sys$error 'sysError' $! $ if f$search(randFileName) .eqs. "" $ then $ call certRequestFailed "Error creating random seed file." $ exit ERROR_STATUS $ endif $ if f$file(randFileName,"EOF") .eqs. 0 $ then $ call certRequestFailed "Error populating random seed file." $ exit ERROR_STATUS $ endif $! $!(execute the CA utility, capturing its output) $ define /nolog sys$output 'CAoutputFileName' $ define /nolog sys$error 'CAoutputFileName' $ RANDFILE = randFileName $ define /user openssl_conf 'defaultConfig' $ opensslCA -batch -days 'durationDays' -key "''CApassphrase'" - -spkac 'CSRfileName' -out 'certFileName' $ define /nolog sys$output 'sysOutput' $ define /nolog sys$error 'sysError' $! $ if f$search(certFileName) .eqs. "" $ then $ call certRequestFailed "Error when CA processing CSR." $ exit ERROR_STATUS $ endif $ if f$file(certFileName,"EOF") .eqs. 0 $ then $ call certRequestFailed "Error when CA processing CSR." $ exit ERROR_STATUS $ endif $! $ exit NORMAL_STATUS $ endsubroutine $! $!----------------------------------------------------------------------------- $! $ userReturnCert: subroutine $ set noon $! $!(on-demand, return the certificate directly to the user's browser) $ CGIutl /response /content="application/x-x509-user-cert" 'certFileName' $! $ exit NORMAL_STATUS $ endsubroutine $! $!----------------------------------------------------------------------------- $! $ userReturnAdminCert: subroutine $ set noon $! $ PEMfileName = PEMcertDir + procName + "." + P1 $ if .not. debug then define sys$output nl: $ if .not. debug then define sys$error nl: $ directory 'PEMfileName' $ dirStatus = $status $ define /nolog sys$output 'sysOutput' $ define /nolog sys$error 'sysError' $ if .not. dirStatus $ then $ call certRequestFailed "Certificate not found." $ exit ERROR_STATUS $ endif $! $ if .not. debug then define sys$output nl: $ if .not. debug then define sys$error nl: $ define /user openssl_conf 'defaultConfig' $ OpenSSLX509 -inform PEM -in 'PEMfileName' -outform DER -out 'certFileName' $ define /nolog sys$output 'sysOutput' $ define /nolog sys$error 'sysError' $! $ CGIutl /response /content="application/x-x509-user-cert" 'certFileName' $! $ exit NORMAL_STATUS $ endsubroutine $! $!----------------------------------------------------------------------------- $! $ adminReturnCert: subroutine $ set noon $! $ define /nolog sys$output 'PEMfileName' $ define /nolog sys$error 'PEMfileName' $ type sys$input This certificate can be obtained in binary at any time by accessing the following URL: $ say " " + WWW_REQUEST_SCHEME + "//" + WWW_SERVER_NAME +- WWW_SCRIPT_NAME + "/" + F$EDIT(WWW_UNIQUE_ID,"upcase") $ say "" $ define /user openssl_conf 'defaultConfig' $ OpenSSLX509 -inform DER -in 'certFileName' -text $ define /nolog sys$output 'sysOutput' $ define /nolog sys$error 'sysError' $ CGIutl /response /content="text/plain" 'PEMfileName' $! $ exit NORMAL_STATUS $ endsubroutine $! $!----------------------------------------------------------------------------- $! $ userEmailCSR: subroutine $ set noon $! $!(email the CSR to the site administrator's email address) $ if .not. debug then define sys$output nl: $ if .not. debug then define sys$error nl: $ mail 'CSRfileName' "''CSRemailAddress'" - /subject="CSR from ''WWW_FORM_EMAILADDRESS'" $ mailStatus = $STATUS $ define /nolog sys$output 'sysOutput' $ define /nolog sys$error 'sysError' $ if mailStatus $ then $ type sys$input Content-Type: text/html Certificate Signing Request Successfully Sent Certificate Signing Request

Certificate Signing Request was sent successfully!  Thankyou. $ if f$type(WWW_SERVER_SIGNATURE) .nes. "" $ then $ say "


" $ say WWW_SERVER_SIGNATURE $ endif $ type sys$input $ else $ call certRequestFailed "A problem occured when sending.
Please contact the site administrator." $ endif $ exit NORMAL_STATUS $ endsubroutine $! $!----------------------------------------------------------------------------- $! $ cleanup: subroutine $ set noon $! $exit NORMAL_STATUS $ if .not. debug then define sys$output nl: $ if .not. debug then define sys$error nl: $ delete /noconfirm 'scratchDir'*.'WWW_UNIQUE_ID';* $ define /nolog sys$output 'sysOutput' $ define /nolog sys$error 'sysError' $! $ exit NORMAL_STATUS $ endsubroutine $! $!----------------------------------------------------------------------------- $! $ certRequestFailed: subroutine $ set noon $! $ type sys$input Status: 403 Content-Type: text/html Certificate Signing Request Failed Certificate Signing Request Failed

$ say P1 $ if f$type(CAoutputFileName) .nes. "" $ then $ if f$search(CAoutputFileName) .nes. "" $ then $ say "

"
$       CGIutl 'CAoutputFileName'
$       say "
" $ endif $ endif $! $ if clientPhase $ then $ type sys$input

Request Details:

$ if f$type(WWW_FORM_COMMONNAME) .nes. "" then saysym WWW_FORM_COMMONNAME
$ if f$type(WWW_FORM_EMAILADDRESS) .nes. "" then saysym WWW_FORM_EMAILADDRESS
$ if f$type(WWW_FORM_ORGANIZATIONNAME) .nes. "" then saysym WWW_FORM_ORGANIZATIONNAME
$ if f$type(WWW_FORM_ORGANIZATIONALUNITNAME) .nes. "" then saysym WWW_FORM_ORGANIZATIONALUNITNAME
$ if f$type(WWW_FORM_LOCALITYNAME) .nes. "" then saysym WWW_FORM_LOCALITYNAME
$ if f$type(WWW_FORM_STATEORPROVINCENAME) .nes. "" then saysym WWW_FORM_STATEORPROVINCENAME
$ if f$type(WWW_FORM_COUNTRYNAME) .nes. "" then saysym WWW_FORM_COUNTRYNAME
$    say "
" $ endif $! $ if f$type(WWW_SERVER_SIGNATURE) .nes. "" $ then $ say "


" $ say WWW_SERVER_SIGNATURE $ endif $ type sys$input $! $ call cleanup $ exit NORMAL_STATUS $ endsubroutine $! $!----------------------------------------------------------------------------- $! $ userCertRequestForm: subroutine $ set noon $! $ type sys$input Content-Type: text/html Client Certificate Signing Request Client Certificate Signing Request $ say "
" $ type sys$input
Common Name:
Email Address:
Organization:
Organizational Unit:
Locality (City):
State or Province:
Country:
$ if onDemandGenerate $ then $ type sys$input

Submitting this request will activate a service to automatically generate a client certificate.  Please be patient, it may take some time (perhaps tens of seconds).  When complete you be notified of any problems.  If successful the certificate will be returned directly to your browser for installation in its database. $ else $ type sys$input

Submitting this request will generate a Certificate Signing Request (CSR) which will then be forwarded to the site administrator.  The certificate will then at some time in the immediate future be signed at the adminstrator's discretion.  You will be notified by email using the address specified by the form. $ endif $ if f$type(WWW_SERVER_SIGNATURE) .nes. "" $ then $ say "


" $ say WWW_SERVER_SIGNATURE $ endif $ type sys$input $! $ exit NORMAL_STATUS $ endsubroutine $! $!----------------------------------------------------------------------------- $! $ adminCertRequestForm: subroutine $ set noon $! $ type sys$input Content-Type: text/html Admin Generate Client Certificate

Admin Generate Client Certificate

$ say "
" $ type sys$input

Cut and paste CSR from source into this text area:
Duration: days   CA passphrase:   

$ if f$type(WWW_SERVER_SIGNATURE) .nes. "" $ then $ say "


" $ say WWW_SERVER_SIGNATURE $ endif $! $ exit NORMAL_STATUS $ endsubroutine $! $!-----------------------------------------------------------------------------