Skip to content

Commit 1a120fd

Browse files
authored
test: OCSP E2E (#1172)
Added OCSP E2E test cases: valid, revoked and unknown OCSP fallback to CRL test cases have been included in #1079 Resolves #638 Resolves #572 --------- Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
1 parent 4808e08 commit 1a120fd

22 files changed

Lines changed: 677 additions & 0 deletions

File tree

test/e2e/internal/notation/host.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,15 @@ func CRLOptions() []utils.HostOption {
192192
)
193193
}
194194

195+
func OCSPOptions() []utils.HostOption {
196+
return Opts(
197+
AuthOption("", ""),
198+
AddKeyOption(filepath.Join(NotationE2EConfigPath, "ocsp", "leaf.key"), filepath.Join(NotationE2EConfigPath, "ocsp", "certchain_with_ocsp.pem")),
199+
AddTrustStoreOption("e2e", filepath.Join(NotationE2EConfigPath, "ocsp", "root.crt")),
200+
AddTrustPolicyOption("trustpolicy.json"),
201+
)
202+
}
203+
195204
func BaseOptionsWithExperimental() []utils.HostOption {
196205
return Opts(
197206
AuthOption("", ""),

test/e2e/internal/utils/ocsp.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright The Notary Project Authors.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package utils
15+
16+
import (
17+
"fmt"
18+
"net/http"
19+
)
20+
21+
func LeafOCSPRevoke() error {
22+
url := "http://localhost:10088/revoke"
23+
resp, err := http.Post(url, "application/json", nil)
24+
if err != nil {
25+
return err
26+
}
27+
defer resp.Body.Close()
28+
fmt.Printf("OCSP of leaf certificate revoked with status code: %d\n", resp.StatusCode)
29+
return nil
30+
}
31+
32+
func LeafOCSPUnrevoke() error {
33+
url := "http://localhost:10088/unrevoke"
34+
resp, err := http.Post(url, "application/json", nil)
35+
if err != nil {
36+
return err
37+
}
38+
defer resp.Body.Close()
39+
fmt.Printf("OCSP of leaf certificate unrevoked with status code: %d\n", resp.StatusCode)
40+
return nil
41+
}
42+
43+
func LeafOCSPUnknown() error {
44+
url := "http://localhost:10088/unknown"
45+
resp, err := http.Post(url, "application/json", nil)
46+
if err != nil {
47+
return err
48+
}
49+
defer resp.Body.Close()
50+
fmt.Printf("OCSP of leaf certificate with unknown status with status code: %d\n", resp.StatusCode)
51+
return nil
52+
}

test/e2e/run.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,18 @@ setup_registry
9999
python3 ./scripts/crl_server.py &
100100
CRL_SERVER_PID=$!
101101

102+
# run the OCSP server in the background
103+
python3 ./scripts/ocsp_server.py --config-dir ./testdata/config/ocsp &
104+
OCSP_SERVER_PID=$!
105+
102106
# defer cleanup registry
103107
function cleanup {
104108
echo "Cleaning up..."
105109
cleanup_registry
106110
echo "Stopping CRL server..."
107111
kill $CRL_SERVER_PID
112+
echo "Stopping OCSP server..."
113+
kill $OCSP_SERVER_PID
108114
}
109115
trap cleanup EXIT
110116

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/bin/bash -ex
2+
# Copyright The Notary Project Authors.
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Create root configuration with CA and OCSP extensions
16+
cat > root_ocsp.cnf <<EOF
17+
[ req ]
18+
default_bits = 2048
19+
prompt = no
20+
distinguished_name = root_distinguished_name
21+
x509_extensions = v3_ca
22+
23+
[ root_distinguished_name ]
24+
C = US
25+
ST = State
26+
L = City
27+
O = Organization
28+
OU = OrgUnit
29+
CN = RootCA
30+
31+
[ v3_ca ]
32+
basicConstraints = critical,CA:TRUE
33+
keyUsage = critical,keyCertSign,cRLSign
34+
subjectKeyIdentifier = hash
35+
36+
[ v3_ocsp ]
37+
extendedKeyUsage = critical,OCSPSigning
38+
keyUsage = critical,digitalSignature
39+
subjectKeyIdentifier = hash
40+
EOF
41+
42+
# Set up OpenSSL CA directory structure for root
43+
mkdir -p demoCA/newcerts
44+
touch demoCA/index.txt
45+
echo '1000' > demoCA/serial
46+
47+
# Generate root private key and self-signed certificate with CA extensions
48+
openssl genrsa -out root.key 2048
49+
openssl req -x509 -new -key root.key -sha256 -days 36500 -out root.crt \
50+
-config root_ocsp.cnf -extensions v3_ca
51+
52+
# Create OCSP responder configuration
53+
cat > ocsp.cnf <<EOF
54+
[ ca ]
55+
default_ca = CA_default
56+
57+
[ CA_default ]
58+
dir = demoCA
59+
database = demoCA/index.txt
60+
new_certs_dir = demoCA/newcerts
61+
certificate = root.crt
62+
serial = demoCA/serial
63+
private_key = root.key
64+
default_days = 36500
65+
default_md = sha256
66+
policy = policy_strict
67+
68+
[ policy_strict ]
69+
countryName = match
70+
stateOrProvinceName = match
71+
organizationName = match
72+
organizationalUnitName = optional
73+
commonName = supplied
74+
emailAddress = optional
75+
76+
[ req ]
77+
default_bits = 2048
78+
prompt = no
79+
distinguished_name = ocsp_distinguished_name
80+
req_extensions = v3_ocsp
81+
82+
[ ocsp_distinguished_name ]
83+
C = US
84+
ST = State
85+
L = City
86+
O = Organization
87+
OU = OrgUnit
88+
CN = OCSPResponder
89+
90+
[ v3_ocsp ]
91+
extendedKeyUsage = critical,OCSPSigning
92+
keyUsage = critical,digitalSignature
93+
subjectKeyIdentifier = hash
94+
EOF
95+
96+
# Generate OCSP private key and CSR, then sign it with the root certificate
97+
openssl genrsa -out ocsp.key 2048
98+
openssl req -new -key ocsp.key -out ocsp.csr -config ocsp.cnf
99+
openssl x509 -req -in ocsp.csr -CA root.crt -CAkey root.key -CAcreateserial \
100+
-out ocsp.crt -days 36500 -extfile ocsp.cnf -extensions v3_ocsp
101+
102+
# Create leaf configuration with OCSP URL
103+
cat > leaf_ocsp.cnf <<EOF
104+
[ req ]
105+
default_bits = 2048
106+
prompt = no
107+
distinguished_name = leaf_distinguished_name
108+
req_extensions = v3_req
109+
110+
[ leaf_distinguished_name ]
111+
C = US
112+
ST = State
113+
L = City
114+
O = Organization
115+
OU = OrgUnit
116+
CN = LeafCert
117+
118+
[ v3_req ]
119+
basicConstraints = critical,CA:FALSE
120+
keyUsage = critical,digitalSignature
121+
authorityInfoAccess = OCSP;URI:http://localhost:10087
122+
subjectKeyIdentifier = hash
123+
EOF
124+
125+
# Generate leaf key and CSR then sign directly with the root certificate instead of an intermediate
126+
openssl genrsa -out leaf.key 2048
127+
openssl req -new -key leaf.key -out leaf.csr -config leaf_ocsp.cnf
128+
openssl x509 -req -in leaf.csr -CA root.crt -CAkey root.key -CAcreateserial \
129+
-out leaf.crt -days 36500 -extfile leaf_ocsp.cnf -extensions v3_req
130+
131+
# Cleanup and final message
132+
rm -f *.csr root_occrt.srl
133+
echo "OCSP testing certificates generated successfully."
134+

test/e2e/scripts/ocsp_server.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Copyright The Notary Project Authors.
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
import os
15+
import argparse
16+
import subprocess
17+
import threading
18+
from http.server import BaseHTTPRequestHandler
19+
from socketserver import TCPServer
20+
21+
# Global variable to hold the OCSP server process
22+
ocsp_process = None
23+
ocsp_lock = threading.Lock()
24+
25+
def start_ocsp_server(config_dir):
26+
global ocsp_process
27+
# Start OCSP server in background in config_dir
28+
cmd = [
29+
"openssl", "ocsp",
30+
"-port", "10087",
31+
"-index", "demoCA/index.txt",
32+
"-CA", "root.crt",
33+
"-rkey", "ocsp.key",
34+
"-rsigner", "ocsp.crt",
35+
"-nmin", "5",
36+
"-text",
37+
]
38+
ocsp_process = subprocess.Popen(cmd, cwd=config_dir)
39+
print("OCSP server started with PID:", ocsp_process.pid)
40+
41+
def stop_ocsp_server():
42+
global ocsp_process
43+
if ocsp_process:
44+
ocsp_process.terminate()
45+
ocsp_process.wait()
46+
print("OCSP server with PID", ocsp_process.pid, "terminated")
47+
ocsp_process = None
48+
49+
def restart_ocsp_server(config_dir):
50+
with ocsp_lock:
51+
stop_ocsp_server()
52+
start_ocsp_server(config_dir)
53+
54+
def update_index_file(config_dir, new_content):
55+
index_path = os.path.join(config_dir, "demoCA", "index.txt")
56+
with open(index_path, "w") as f:
57+
f.write(new_content + "\n")
58+
print("Updated", index_path)
59+
60+
class OCSPRequestHandler(BaseHTTPRequestHandler):
61+
def do_POST(self):
62+
response = ""
63+
# Ensure path is processed without query parameters.
64+
path = self.path.split("?")[0]
65+
if path == "/revoke":
66+
# Revoke: update index.txt with revoked entry
67+
revoke_content = ("R 21250121012109Z 250214013720Z 520D9B1364D98367711DA2B6A0A0F34B23E3D02A unknown /C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=LeafCert")
68+
update_index_file(self.server.config_dir, revoke_content)
69+
restart_ocsp_server(self.server.config_dir)
70+
response = "OCSP server restarted with index updated (revoke)."
71+
elif path == "/unrevoke":
72+
# Unrevoke: update index.txt with valid entry
73+
unrevoke_content = ("V 21250121012109Z 250214013720Z 520D9B1364D98367711DA2B6A0A0F34B23E3D02A unknown /C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=LeafCert")
74+
update_index_file(self.server.config_dir, unrevoke_content)
75+
restart_ocsp_server(self.server.config_dir)
76+
response = "OCSP server restarted with index updated (unrevoke)."
77+
elif path == "/unknown":
78+
# Unknown endpoint
79+
empty_content = ""
80+
update_index_file(self.server.config_dir, empty_content)
81+
restart_ocsp_server(self.server.config_dir)
82+
response = "OCSP server restarted with empty index."
83+
else:
84+
response = "Invalid endpoint. Use /revoke or /unrevoke."
85+
86+
self.send_response(200)
87+
self.send_header("Content-type", "text/plain")
88+
self.end_headers()
89+
self.wfile.write(response.encode("utf-8"))
90+
91+
class ReusableTCPServer(TCPServer):
92+
allow_reuse_address = True
93+
94+
def run_server(config_dir, host="localhost", port=10088):
95+
server_address = (host, port)
96+
httpd = ReusableTCPServer(server_address, OCSPRequestHandler)
97+
httpd.config_dir = config_dir # attach config directory to server instance
98+
print(f"HTTP control server running on {host}:{port}")
99+
httpd.serve_forever()
100+
101+
if __name__ == "__main__":
102+
parser = argparse.ArgumentParser(
103+
description="Start OCSP server control HTTP server."
104+
)
105+
parser.add_argument(
106+
"--config-dir",
107+
required=True,
108+
help="Path to OCSP configuration folder."
109+
)
110+
args = parser.parse_args()
111+
config_dir = os.path.abspath(args.config_dir)
112+
# Change to config_dir to ensure all commands run there (optional)
113+
os.chdir(config_dir)
114+
# Start the OCSP server in background
115+
start_ocsp_server(config_dir)
116+
# Run HTTP control server on port 10088
117+
run_server(config_dir)

0 commit comments

Comments
 (0)