Skip to content

Commit 1e3553c

Browse files
Copilotbbockelm
andauthored
Add CTest-based integration test with JWKS server and TLS infrastructure (#184)
* Add full integration test with JWKS server and TLS support * Bind socket to random port on localhost * Update .gitignore and remove CodeQL build artifacts * Add git safe.directory config to devcontainer postCreateCommand. This fixes git submodule initialization and prevents 'dubious ownership' errors when working in the devcontainer. * Add server log output to teardown and improve TLS compatibility * Add GitHub Copilot instructions for project --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Brian P Bockelman <bockelman@gmail.com>
1 parent 194b710 commit 1e3553c

File tree

11 files changed

+1166
-24
lines changed

11 files changed

+1166
-24
lines changed

.devcontainer/Dockerfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,10 @@ RUN apt-get update \
4747

4848
# Create a non-root user to use
4949
ARG USERNAME=vscode
50-
ARG USER_UID=1000
51-
ARG USER_GID=$USER_UID
5250

53-
RUN groupadd --gid $USER_GID $USERNAME \
54-
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
51+
# Create user and group without forcing specific UID/GID
52+
RUN groupadd $USERNAME \
53+
&& useradd -g $USERNAME -m $USERNAME \
5554
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
5655
&& chmod 0440 /etc/sudoers.d/$USERNAME
5756

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
// "forwardPorts": [],
2727

2828
// Use 'postCreateCommand' to run commands after the container is created.
29-
"postCreateCommand": "git submodule update --init --recursive",
29+
"postCreateCommand": "git config --global --add safe.directory /workspaces/scitokens-cpp && git submodule update --init --recursive",
3030

3131
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
3232
"remoteUser": "vscode",

.github/copilot-instructions.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# GitHub Copilot Instructions for scitokens-cpp
2+
3+
## Project Overview
4+
5+
scitokens-cpp is a C++ library for creating and validating SciTokens (JWT-based authorization tokens for scientific computing). The library uses JWT-cpp for token operations and supports OIDC discovery with JWKS for public key distribution.
6+
7+
## Building the Project
8+
9+
### Prerequisites
10+
11+
- CMake 3.10 or later
12+
- C++11 compatible compiler (gcc, clang)
13+
- OpenSSL 1.1.1 or later (3.0+ recommended)
14+
- libuuid
15+
- sqlite3
16+
- jwt-cpp (included as vendor submodule)
17+
18+
### Build Commands
19+
20+
```bash
21+
# Create build directory
22+
mkdir -p build
23+
cd build
24+
25+
# Configure with CMake (enable tests with -DSCITOKENS_BUILD_UNITTESTS=ON)
26+
cmake .. -DSCITOKENS_BUILD_UNITTESTS=ON
27+
28+
# Build all targets
29+
make
30+
31+
# Install (optional)
32+
sudo make install
33+
```
34+
35+
### CMake Build Options
36+
37+
- Tests are **disabled by default** - use `-DSCITOKENS_BUILD_UNITTESTS=ON` to enable
38+
- Build produces:
39+
- `libSciTokens.so` - Main library
40+
- `scitokens-test` - Unit tests (Google Test)
41+
- `scitokens-integration-test` - Integration tests with real HTTPS server
42+
- `scitokens-generate-jwks` - JWKS generation utility
43+
- Command-line tools: `scitokens-verify`, `scitokens-create`, `scitokens-list-access`, `scitokens-test-access`
44+
45+
## Running Tests
46+
47+
### Unit Tests
48+
49+
```bash
50+
cd build
51+
./scitokens-test
52+
```
53+
54+
Expected: 29 unit tests should pass
55+
56+
### Integration Tests
57+
58+
Integration tests use CTest fixtures with setup/teardown phases:
59+
60+
```bash
61+
cd build/test
62+
ctest --output-on-failure
63+
```
64+
65+
Or run specific test phases:
66+
```bash
67+
ctest -R integration::setup # Start HTTPS JWKS server
68+
ctest -R integration::test # Run integration tests
69+
ctest -R integration::teardown # Stop server
70+
```
71+
72+
**Integration test infrastructure:**
73+
- `test/jwks_server.py` - Python HTTPS server with OIDC discovery and JWKS endpoints
74+
- `test/integration-test-setup.sh` - Generates TLS certificates and starts server
75+
- `test/integration-test-teardown.sh` - Stops server gracefully
76+
- `test/integration_test.cpp` - C++ tests using real HTTPS connections
77+
78+
Expected: 3 integration tests should pass (total time ~1-2 seconds)
79+
80+
### All Tests
81+
82+
```bash
83+
cd build/test
84+
ctest --output-on-failure
85+
```
86+
87+
Expected: 32 total tests (29 unit + 3 integration)
88+
89+
## Code Style
90+
91+
- C++11 standard
92+
- Use `clang-format` for formatting (configuration in project root)
93+
- Format before committing: `clang-format -i src/*.cpp src/*.h`
94+
95+
## Testing Infrastructure Details
96+
97+
### JWKS Server (test/jwks_server.py)
98+
99+
Python HTTPS server that provides:
100+
- `/.well-known/openid-configuration` - OIDC discovery document
101+
- `/oauth2/certs` - JWKS public key endpoint
102+
103+
Server features:
104+
- HTTP/1.1 with keep-alive support
105+
- TLS 1.2+ with self-signed certificates
106+
- Graceful shutdown with SIGTERM
107+
- Logs to `build/tests/integration/server.log`
108+
109+
### Integration Test Flow
110+
111+
1. **Setup**: Generate EC P-256 key pair, create JWKS, generate TLS certificates, start HTTPS server
112+
2. **Test**: Create tokens, verify with JWKS discovery, test dynamic issuer enforcement
113+
3. **Teardown**: Stop server, print logs if tests failed
114+
115+
### Debugging Integration Tests
116+
117+
If integration tests fail:
118+
1. Check server log: `cat build/tests/integration/server.log`
119+
2. Verify server started: `cat build/tests/integration/server_ready`
120+
3. Test HTTPS manually: `curl -k https://localhost:<port>/.well-known/openid-configuration`
121+
122+
## Key Files
123+
124+
- `src/scitokens.cpp`, `src/scitokens.h` - Main library API
125+
- `src/generate_jwks.cpp` - JWKS generation (EC P-256 keys)
126+
- `src/scitokens_internal.cpp` - Token validation and OIDC discovery
127+
- `src/scitokens_cache.cpp` - JWKS caching
128+
- `test/integration_test.cpp` - End-to-end integration tests
129+
- `test/main.cpp` - Google Test unit tests
130+
131+
## Development Workflow
132+
133+
1. Make code changes
134+
2. Build: `cd build && cmake .. && make`
135+
3. Run unit tests: `./scitokens-test`
136+
4. Run integration tests: `cd test && ctest --output-on-failure`
137+
5. Format code: `clang-format -i <modified-files>`
138+
6. Commit with descriptive message
139+
140+
## CI/CD
141+
142+
GitHub Actions runs tests on:
143+
- Ubuntu 22.04 (OpenSSL 3.0.2)
144+
- Ubuntu 24.04 (OpenSSL 3.0.13)
145+
146+
Integration tests verify TLS compatibility across OpenSSL versions.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ build
22
_codeql_build_dir
33
_codeql_detected_source_root
44

5+
*.pyc
6+
__pycache__/

src/generate_jwks.cpp

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -128,29 +128,33 @@ std::string base64url_encode(const unsigned char *data, size_t len) {
128128
bool extract_ec_coordinates(EVP_PKEY *pkey, std::string &x_coord,
129129
std::string &y_coord) {
130130
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
131-
size_t pub_key_len = 0;
132-
133-
if (EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY, nullptr,
134-
0, &pub_key_len) != 1) {
131+
// For OpenSSL 3.0+, use the BIGNUM parameter API which is more reliable
132+
BIGNUM *x_bn = nullptr;
133+
BIGNUM *y_bn = nullptr;
134+
135+
if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_EC_PUB_X, &x_bn) != 1 ||
136+
EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_EC_PUB_Y, &y_bn) != 1) {
137+
BN_free(x_bn);
138+
BN_free(y_bn);
135139
return false;
136140
}
137141

138-
std::unique_ptr<unsigned char[]> pub_key_buf(
139-
new unsigned char[pub_key_len]);
142+
std::unique_ptr<BIGNUM, decltype(&BN_free)> x(x_bn, BN_free);
143+
std::unique_ptr<BIGNUM, decltype(&BN_free)> y(y_bn, BN_free);
140144

141-
if (EVP_PKEY_get_octet_string_param(pkey, OSSL_PKEY_PARAM_PUB_KEY,
142-
pub_key_buf.get(), pub_key_len,
143-
&pub_key_len) != 1) {
144-
return false;
145-
}
145+
// Convert BIGNUMs to fixed-size byte arrays (32 bytes for P-256)
146+
unsigned char x_buf[32] = {0};
147+
unsigned char y_buf[32] = {0};
146148

147-
// For uncompressed EC point format: 0x04 || X || Y
148-
if (pub_key_len != 65 || pub_key_buf[0] != 0x04) {
149-
return false;
150-
}
149+
int x_len = BN_num_bytes(x.get());
150+
int y_len = BN_num_bytes(y.get());
151151

152-
x_coord = base64url_encode(pub_key_buf.get() + 1, 32);
153-
y_coord = base64url_encode(pub_key_buf.get() + 33, 32);
152+
// Pad with zeros on the left if necessary
153+
BN_bn2bin(x.get(), x_buf + (32 - x_len));
154+
BN_bn2bin(y.get(), y_buf + (32 - y_len));
155+
156+
x_coord = base64url_encode(x_buf, 32);
157+
y_coord = base64url_encode(y_buf, 32);
154158
#else
155159
std::unique_ptr<EC_KEY, decltype(&EC_KEY_free)> ec_key(
156160
EVP_PKEY_get1_EC_KEY(pkey), EC_KEY_free);
@@ -167,8 +171,18 @@ bool extract_ec_coordinates(EVP_PKEY *pkey, std::string &x_coord,
167171
std::unique_ptr<BIGNUM, decltype(&BN_free)> x(BN_new(), BN_free);
168172
std::unique_ptr<BIGNUM, decltype(&BN_free)> y(BN_new(), BN_free);
169173

170-
if (!EC_POINT_get_affine_coordinates_GFp(group, pub_key, x.get(), y.get(),
171-
nullptr)) {
174+
// Use EC_POINT_get_affine_coordinates for OpenSSL 1.1.1+
175+
// or EC_POINT_get_affine_coordinates_GFp for older versions
176+
int result = 0;
177+
#if OPENSSL_VERSION_NUMBER >= 0x10101000L
178+
result = EC_POINT_get_affine_coordinates(group, pub_key, x.get(), y.get(),
179+
nullptr);
180+
#else
181+
result = EC_POINT_get_affine_coordinates_GFp(group, pub_key, x.get(),
182+
y.get(), nullptr);
183+
#endif
184+
185+
if (result != 1) {
172186
return false;
173187
}
174188

test/CMakeLists.txt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,52 @@ add_test(
1919
COMMAND
2020
${CMAKE_CURRENT_BINARY_DIR}/scitokens-gtest
2121
)
22+
23+
# Integration test executable
24+
add_executable(scitokens-integration-test integration_test.cpp)
25+
if( NOT SCITOKENS_EXTERNAL_GTEST )
26+
add_dependencies(scitokens-integration-test gtest)
27+
endif()
28+
target_link_libraries(scitokens-integration-test SciTokens "${LIBGTEST}" pthread)
29+
30+
# Integration test fixture - setup
31+
add_test(
32+
NAME
33+
integration::setup
34+
COMMAND
35+
${CMAKE_CURRENT_SOURCE_DIR}/integration-test-setup.sh integration
36+
)
37+
38+
set_tests_properties(integration::setup
39+
PROPERTIES
40+
FIXTURES_SETUP integration
41+
ENVIRONMENT "BINARY_DIR=${CMAKE_BINARY_DIR};SOURCE_DIR=${PROJECT_SOURCE_DIR}"
42+
)
43+
44+
# Integration test fixture - teardown
45+
add_test(
46+
NAME
47+
integration::teardown
48+
COMMAND
49+
${CMAKE_CURRENT_SOURCE_DIR}/integration-test-teardown.sh integration
50+
)
51+
52+
set_tests_properties(integration::teardown
53+
PROPERTIES
54+
FIXTURES_CLEANUP integration
55+
ENVIRONMENT "BINARY_DIR=${CMAKE_BINARY_DIR}"
56+
)
57+
58+
# Integration test
59+
add_test(
60+
NAME
61+
integration::test
62+
COMMAND
63+
${CMAKE_CURRENT_BINARY_DIR}/scitokens-integration-test
64+
)
65+
66+
set_tests_properties(integration::test
67+
PROPERTIES
68+
FIXTURES_REQUIRED integration
69+
ENVIRONMENT "BINARY_DIR=${CMAKE_BINARY_DIR};CURL_CA_BUNDLE=${CMAKE_BINARY_DIR}/tests/integration/current/ca-cert.pem"
70+
)

0 commit comments

Comments
 (0)