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 |
---|
38 | import base64 |
---|
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 | |
---|
53 | def stdout_line(line): |
---|
54 | stdout(line.encode('utf-8')) |
---|
55 | stdout(b'\n') |
---|
56 | |
---|
57 | |
---|
58 | def stdout_status(status, content_type='text/plain'): |
---|
59 | stdout_line(f'Status: {status.value} {status.phrase}') |
---|
60 | stdout_line(f'Content-Type: {content_type}\n') |
---|
61 | |
---|
62 | |
---|
63 | def stdout_response(status, response): |
---|
64 | stdout_status(status, content_type=RESPONSE_TYPE) |
---|
65 | stdout(response) |
---|
66 | |
---|
67 | |
---|
68 | def handle_get(): |
---|
69 | # GET OCSP requests are allowed by RFC 6960, Appendix A.1, but |
---|
70 | # not implemented here. It should be possible to extract a GET |
---|
71 | # request from the PATH_INFO CGI variable. |
---|
72 | stdout_status(HTTPStatus.METHOD_NOT_ALLOWED) |
---|
73 | stdout_line('OCSP GET request not implemented.') |
---|
74 | |
---|
75 | |
---|
76 | def handle_post(): |
---|
77 | content_type = os.getenv('CONTENT_TYPE') |
---|
78 | content_length = os.getenv('CONTENT_LENGTH') |
---|
79 | if content_type != REQUEST_TYPE or not content_length: |
---|
80 | stdout_status(HTTPStatus.UNSUPPORTED_MEDIA_TYPE) |
---|
81 | stdout_line(f'POST request must contain {REQUEST_TYPE} data.') |
---|
82 | return |
---|
83 | |
---|
84 | try: |
---|
85 | req = sys.stdin.buffer.read(int(content_length)) |
---|
86 | print(f'Received OCSP request: \'{base64.b64encode(req).decode()}\'', |
---|
87 | file=sys.stderr, flush=True) |
---|
88 | openssl = os.getenv('OPENSSL') or shutil.which('openssl') |
---|
89 | openssl_run = subprocess.run( |
---|
90 | [openssl, 'ocsp', |
---|
91 | '-index', os.getenv('OCSP_INDEX'), |
---|
92 | '-CA', os.getenv('CA_CERT'), |
---|
93 | '-rsigner', os.getenv('OCSP_CERT'), |
---|
94 | '-rkey', os.getenv('OCSP_KEY'), |
---|
95 | '-nmin', os.getenv('OCSP_VALID_MIN', '5'), |
---|
96 | '-reqin', '-', '-respout', '-'], |
---|
97 | input=req, capture_output=True) |
---|
98 | |
---|
99 | if openssl_run.returncode == 0: |
---|
100 | stdout_response(HTTPStatus.OK, openssl_run.stdout) |
---|
101 | sys.stderr.buffer.write(openssl_run.stderr) |
---|
102 | else: |
---|
103 | raise Exception('openssl process exited with return code ' |
---|
104 | f'{openssl_run.returncode}, stdout: ' |
---|
105 | f'{openssl_run.stdout}, stderr: ' |
---|
106 | f'{openssl_run.stderr}') |
---|
107 | except: |
---|
108 | stdout_status(HTTPStatus.INTERNAL_SERVER_ERROR) |
---|
109 | raise |
---|
110 | |
---|
111 | |
---|
112 | if __name__ == '__main__': |
---|
113 | method = os.getenv('REQUEST_METHOD') |
---|
114 | if method == 'GET': |
---|
115 | handle_get() |
---|
116 | elif method == 'POST': |
---|
117 | handle_post() |
---|
118 | else: |
---|
119 | stdout_status(HTTPStatus.METHOD_NOT_ALLOWED) |
---|