[d2f2f62] | 1 | #!/usr/bin/python3 |
---|
| 2 | # Python 3 wrapper to use "openssl ocsp" as a simple OCSP responder |
---|
| 3 | # |
---|
| 4 | # Copyright 2020 Krista Karppinen, Fiona Klute |
---|
| 5 | # |
---|
| 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you |
---|
| 7 | # may not use this file except in compliance with the License. You |
---|
| 8 | # may obtain a copy of the License at |
---|
| 9 | # |
---|
| 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
---|
| 11 | # |
---|
| 12 | # Unless required by applicable law or agreed to in writing, software |
---|
| 13 | # distributed under the License is distributed on an "AS IS" BASIS, |
---|
| 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
---|
| 15 | # implied. See the License for the specific language governing |
---|
| 16 | # permissions and limitations under the License. |
---|
| 17 | |
---|
| 18 | # This is a CGI script to run the OpenSSL OCSP responder from a web |
---|
| 19 | # server. The CGI environment must provide the following four |
---|
| 20 | # variables to configure the OCSP responder: |
---|
| 21 | # |
---|
| 22 | # CA_CERT: CA certificate of the CA that issued the certificates this |
---|
| 23 | # OCSP reponder should provide status information for |
---|
| 24 | # |
---|
| 25 | # OCSP_INDEX: CA index file in the format used by OpenSSL |
---|
| 26 | # |
---|
| 27 | # OCSP_CERT: Certificate that should be used to sign OCSP reponses |
---|
| 28 | # (either CA_CERT or a dedicated OCSP signer certificate, see RFC |
---|
| 29 | # 6960, Section 4.2.2.2) |
---|
| 30 | # |
---|
| 31 | # OCSP_KEY: Private key for OCSP_CERT |
---|
| 32 | # |
---|
| 33 | # Additionally, the OpenSSL binary to use can be configured through |
---|
| 34 | # the OPENSSL environment variable. If it is not set, the PATH will be |
---|
| 35 | # searched. |
---|
| 36 | |
---|
| 37 | from http import HTTPStatus |
---|
[199acff] | 38 | import base64 |
---|
[d2f2f62] | 39 | import os |
---|
| 40 | import shutil |
---|
| 41 | import subprocess |
---|
| 42 | import sys |
---|
| 43 | |
---|
| 44 | |
---|
| 45 | REQUEST_TYPE = 'application/ocsp-request' |
---|
| 46 | RESPONSE_TYPE = 'application/ocsp-response' |
---|
| 47 | |
---|
| 48 | |
---|
| 49 | def stdout(data): |
---|
| 50 | sys.stdout.buffer.write(data) |
---|
| 51 | |
---|
| 52 | def stdout_line(line): |
---|
| 53 | stdout(line.encode('utf-8')) |
---|
| 54 | stdout(b'\n') |
---|
| 55 | |
---|
| 56 | def stdout_status(status, content_type='text/plain'): |
---|
| 57 | stdout_line(f'Status: {status.value} {status.phrase}') |
---|
| 58 | stdout_line(f'Content-Type: {content_type}\n') |
---|
| 59 | |
---|
| 60 | def stdout_response(status, response): |
---|
| 61 | stdout_status(status, content_type=RESPONSE_TYPE) |
---|
| 62 | stdout(response) |
---|
| 63 | |
---|
| 64 | |
---|
| 65 | def handle_get(): |
---|
| 66 | # GET OCSP requests are allowed by RFC 6960, Appendix A.1, but |
---|
| 67 | # not implemented here. It should be possible to extract a GET |
---|
| 68 | # request from the PATH_INFO CGI variable. |
---|
| 69 | stdout_status(HTTPStatus.METHOD_NOT_ALLOWED) |
---|
| 70 | stdout_line('OCSP GET request not implemented.') |
---|
| 71 | |
---|
| 72 | def handle_post(): |
---|
| 73 | content_type = os.getenv('CONTENT_TYPE') |
---|
| 74 | content_length = os.getenv('CONTENT_LENGTH') |
---|
| 75 | if content_type != REQUEST_TYPE or not content_length: |
---|
| 76 | stdout_status(HTTPStatus.UNSUPPORTED_MEDIA_TYPE) |
---|
| 77 | stdout_line(f'POST request must contain {REQUEST_TYPE} data.') |
---|
| 78 | return |
---|
| 79 | |
---|
| 80 | try: |
---|
[de81c0f] | 81 | req = sys.stdin.buffer.read(int(content_length)) |
---|
[199acff] | 82 | print(f'Received OCSP request: \'{base64.b64encode(req).decode()}\'', |
---|
| 83 | file=sys.stderr, flush=True) |
---|
[d2f2f62] | 84 | openssl = os.getenv('OPENSSL') or shutil.which('openssl') |
---|
| 85 | openssl_run = subprocess.run([openssl, 'ocsp', |
---|
| 86 | '-index', os.getenv('OCSP_INDEX'), |
---|
| 87 | '-CA', os.getenv('CA_CERT'), |
---|
| 88 | '-rsigner', os.getenv('OCSP_CERT'), |
---|
| 89 | '-rkey', os.getenv('OCSP_KEY'), |
---|
| 90 | '-nmin', os.getenv('OCSP_VALID_MIN', '5'), |
---|
| 91 | '-reqin', '-', '-respout', '-'], |
---|
[de81c0f] | 92 | input=req, capture_output=True) |
---|
[d2f2f62] | 93 | |
---|
| 94 | if openssl_run.returncode == 0: |
---|
| 95 | stdout_response(HTTPStatus.OK, openssl_run.stdout) |
---|
| 96 | sys.stderr.buffer.write(openssl_run.stderr) |
---|
| 97 | else: |
---|
| 98 | raise Exception('openssl process exited with return code ' |
---|
| 99 | f'{openssl_run.returncode}, stdout: ' |
---|
| 100 | f'{openssl_run.stdout}, stderr: ' |
---|
| 101 | f'{openssl_run.stderr}') |
---|
| 102 | except: |
---|
| 103 | stdout_status(HTTPStatus.INTERNAL_SERVER_ERROR) |
---|
| 104 | raise |
---|
| 105 | |
---|
| 106 | |
---|
| 107 | if __name__ == '__main__': |
---|
| 108 | method = os.getenv('REQUEST_METHOD') |
---|
| 109 | if method == 'GET': |
---|
| 110 | handle_get() |
---|
| 111 | elif method == 'POST': |
---|
| 112 | handle_post() |
---|
| 113 | else: |
---|
| 114 | stdout_status(HTTPStatus.METHOD_NOT_ALLOWED) |
---|