Skip to content

Commit 3f69ef4

Browse files
CopilotFlickdm
andcommitted
Implement DBX certificate validation pipeline with tests and CI integration
Co-authored-by: Flickdm <8979761+Flickdm@users.noreply.github.com>
1 parent 050bb37 commit 3f69ef4

9 files changed

Lines changed: 385 additions & 1 deletion

.github/workflows/prepare-binaries.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ jobs:
6161
- name: Run Unit Tests
6262
run: pytest scripts/
6363

64+
- name: Validate DBX Certificate References
65+
run: python scripts/validate_dbx_references.py PreSignedObjects/DBX
66+
6467
- name: Build Microsoft Only Defaults Template (2023 MSFT)
6568
run: python scripts/secure_boot_default_keys.py --keystore Templates/MicrosoftOnly.toml -o FirmwareArtifacts
6669

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ Pipfile
44
.ruff_cache/*
55
Artifacts/*
66
ReleaseArtifacts/*
7-
Scripts/__pycache__/*
7+
Scripts/__pycache__/*
8+
scripts/__pycache__/*
9+
**/__pycache__/*
-5.49 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-12.6 KB
Binary file not shown.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# @file
2+
#
3+
# Copyright (c) Microsoft Corporation.
4+
# SPDX-License-Identifier: BSD-2-Clause-Patent
5+
##
6+
"""Test the validate_dbx_references.py script.
7+
8+
This module contains unit tests for the DBX certificate reference validation functionality.
9+
"""
10+
import json
11+
import pathlib
12+
import tempfile
13+
14+
import pytest
15+
from validate_dbx_references import validate_certificate_references
16+
17+
18+
def test_validate_certificate_references_no_certificates_section() -> None:
19+
"""Test validation when JSON has no certificates section."""
20+
with tempfile.TemporaryDirectory() as temp_dir:
21+
temp_path = pathlib.Path(temp_dir)
22+
23+
# Create JSON file without certificates section
24+
json_file = temp_path / "dbx_info_msft_01_01_24.json"
25+
with json_file.open("w") as f:
26+
json.dump({"images": {"x64": []}}, f)
27+
28+
# Create empty certificates directory
29+
certs_dir = temp_path / "Certificates"
30+
certs_dir.mkdir()
31+
32+
# Should pass validation
33+
errors = validate_certificate_references(json_file, certs_dir)
34+
assert errors == []
35+
36+
37+
def test_validate_certificate_references_empty_certificates() -> None:
38+
"""Test validation when certificates section is empty."""
39+
with tempfile.TemporaryDirectory() as temp_dir:
40+
temp_path = pathlib.Path(temp_dir)
41+
42+
# Create JSON file with empty certificates section
43+
json_file = temp_path / "dbx_info_msft_01_01_24.json"
44+
with json_file.open("w") as f:
45+
json.dump({"certificates": []}, f)
46+
47+
# Create empty certificates directory
48+
certs_dir = temp_path / "Certificates"
49+
certs_dir.mkdir()
50+
51+
# Should pass validation
52+
errors = validate_certificate_references(json_file, certs_dir)
53+
assert errors == []
54+
55+
56+
def test_validate_certificate_references_valid_certificates() -> None:
57+
"""Test validation when all certificate references are valid."""
58+
with tempfile.TemporaryDirectory() as temp_dir:
59+
temp_path = pathlib.Path(temp_dir)
60+
61+
# Create certificates directory with test files
62+
certs_dir = temp_path / "Certificates"
63+
certs_dir.mkdir()
64+
(certs_dir / "cert1.cer").touch()
65+
(certs_dir / "cert2.der").touch()
66+
67+
# Create JSON file referencing these certificates
68+
json_file = temp_path / "dbx_info_msft_01_01_24.json"
69+
json_data = {
70+
"certificates": [
71+
{
72+
"value": "cert1.cer",
73+
"subjectName": "Test Subject 1",
74+
"issuerName": "Test Issuer 1",
75+
"thumbprint": "abc123",
76+
"description": "Test certificate 1",
77+
"dateOfAddition": "2024-01-01"
78+
},
79+
{
80+
"value": "cert2.der",
81+
"subjectName": "Test Subject 2",
82+
"issuerName": "Test Issuer 2",
83+
"thumbprint": "def456",
84+
"description": "Test certificate 2",
85+
"dateOfAddition": "2024-01-01"
86+
}
87+
]
88+
}
89+
with json_file.open("w") as f:
90+
json.dump(json_data, f)
91+
92+
# Should pass validation
93+
errors = validate_certificate_references(json_file, certs_dir)
94+
assert errors == []
95+
96+
97+
def test_validate_certificate_references_missing_certificates() -> None:
98+
"""Test validation when some certificate references are missing."""
99+
with tempfile.TemporaryDirectory() as temp_dir:
100+
temp_path = pathlib.Path(temp_dir)
101+
102+
# Create certificates directory with only one file
103+
certs_dir = temp_path / "Certificates"
104+
certs_dir.mkdir()
105+
(certs_dir / "cert1.cer").touch()
106+
107+
# Create JSON file referencing missing certificate
108+
json_file = temp_path / "dbx_info_msft_01_01_24.json"
109+
json_data = {
110+
"certificates": [
111+
{
112+
"value": "cert1.cer",
113+
"subjectName": "Test Subject 1",
114+
"issuerName": "Test Issuer 1",
115+
"thumbprint": "abc123",
116+
"description": "Test certificate 1",
117+
"dateOfAddition": "2024-01-01"
118+
},
119+
{
120+
"value": "missing_cert.cer",
121+
"subjectName": "Test Subject 2",
122+
"issuerName": "Test Issuer 2",
123+
"thumbprint": "def456",
124+
"description": "Test certificate 2",
125+
"dateOfAddition": "2024-01-01"
126+
}
127+
]
128+
}
129+
with json_file.open("w") as f:
130+
json.dump(json_data, f)
131+
132+
# Should fail validation
133+
errors = validate_certificate_references(json_file, certs_dir)
134+
assert len(errors) == 1
135+
assert "missing_cert.cer" in errors[0]
136+
assert "not found" in errors[0]
137+
138+
139+
def test_validate_certificate_references_missing_value_field() -> None:
140+
"""Test validation when certificate entry is missing value field."""
141+
with tempfile.TemporaryDirectory() as temp_dir:
142+
temp_path = pathlib.Path(temp_dir)
143+
144+
# Create certificates directory
145+
certs_dir = temp_path / "Certificates"
146+
certs_dir.mkdir()
147+
148+
# Create JSON file with malformed certificate entry
149+
json_file = temp_path / "dbx_info_msft_01_01_24.json"
150+
json_data = {
151+
"certificates": [
152+
{
153+
"subjectName": "Test Subject",
154+
"issuerName": "Test Issuer",
155+
"thumbprint": "abc123",
156+
"description": "Test certificate",
157+
"dateOfAddition": "2024-01-01"
158+
# Missing "value" field
159+
}
160+
]
161+
}
162+
with json_file.open("w") as f:
163+
json.dump(json_data, f)
164+
165+
# Should fail validation
166+
errors = validate_certificate_references(json_file, certs_dir)
167+
assert len(errors) == 1
168+
assert "missing 'value' field" in errors[0]
169+
170+
171+
def test_validate_certificate_references_file_not_found() -> None:
172+
"""Test validation when JSON file doesn't exist."""
173+
with tempfile.TemporaryDirectory() as temp_dir:
174+
temp_path = pathlib.Path(temp_dir)
175+
176+
# Create certificates directory
177+
certs_dir = temp_path / "Certificates"
178+
certs_dir.mkdir()
179+
180+
# Reference non-existent JSON file
181+
json_file = temp_path / "nonexistent.json"
182+
183+
# Should raise FileNotFoundError
184+
with pytest.raises(FileNotFoundError):
185+
validate_certificate_references(json_file, certs_dir)
186+
187+
188+
def test_validate_certificate_references_invalid_json() -> None:
189+
"""Test validation when JSON file is malformed."""
190+
with tempfile.TemporaryDirectory() as temp_dir:
191+
temp_path = pathlib.Path(temp_dir)
192+
193+
# Create certificates directory
194+
certs_dir = temp_path / "Certificates"
195+
certs_dir.mkdir()
196+
197+
# Create malformed JSON file
198+
json_file = temp_path / "dbx_info_msft_01_01_24.json"
199+
with json_file.open("w") as f:
200+
f.write("{ invalid json }")
201+
202+
# Should raise json.JSONDecodeError
203+
with pytest.raises(json.JSONDecodeError):
204+
validate_certificate_references(json_file, certs_dir)

scripts/validate_dbx_references.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# @file
2+
#
3+
# Copyright (c) Microsoft Corporation.
4+
# SPDX-License-Identifier: BSD-2-Clause-Patent
5+
##
6+
"""Script to validate that DBX JSON files reference existing certificate files.
7+
8+
This script reads the latest DBX JSON file and validates that all certificate
9+
files referenced in the "certificates" array actually exist in the
10+
PreSignedObjects/DBX/Certificates folder.
11+
"""
12+
import argparse
13+
import json
14+
import logging
15+
import pathlib
16+
import sys
17+
from typing import List
18+
19+
20+
def get_latest_dbx_info_file(dbx_directory: pathlib.Path) -> pathlib.Path:
21+
"""Get the latest DBX info JSON file from the specified directory.
22+
23+
Args:
24+
dbx_directory (pathlib.Path): The directory path to search for DBX JSON files.
25+
26+
Returns:
27+
pathlib.Path: The path to the latest DBX info JSON file.
28+
29+
Raises:
30+
FileNotFoundError: If no DBX info JSON files are found in the specified directory.
31+
"""
32+
# Look specifically for dbx_info_msft_*.json files
33+
dbx_files = list(dbx_directory.glob("dbx_info_msft_*.json"))
34+
if not dbx_files:
35+
raise FileNotFoundError("No DBX info JSON files found in the specified directory.")
36+
37+
# Parse the date components from the filename (month_day_year format)
38+
try:
39+
latest_file = max(dbx_files, key=lambda f: list(map(int, f.stem.split("_")[-3:])))
40+
return latest_file
41+
except (ValueError, IndexError) as e:
42+
raise FileNotFoundError(f"Could not parse date from DBX info filenames: {e}")
43+
44+
45+
def validate_certificate_references(dbx_json_path: pathlib.Path, certificates_dir: pathlib.Path) -> List[str]:
46+
"""Validate that certificate references in DBX JSON exist in the certificates directory.
47+
48+
Args:
49+
dbx_json_path (pathlib.Path): Path to the DBX JSON file
50+
certificates_dir (pathlib.Path): Path to the certificates directory
51+
52+
Returns:
53+
List[str]: List of error messages for missing certificates (empty if all exist)
54+
55+
Raises:
56+
FileNotFoundError: If the DBX JSON file doesn't exist
57+
json.JSONDecodeError: If the JSON file is malformed
58+
"""
59+
errors = []
60+
61+
# Load the DBX JSON file
62+
with open(dbx_json_path, 'r') as f:
63+
dbx_data = json.load(f)
64+
65+
# Check if certificates section exists
66+
if 'certificates' not in dbx_data:
67+
logging.info("No 'certificates' section found in DBX JSON file - validation passed")
68+
return errors
69+
70+
certificates = dbx_data['certificates']
71+
if not certificates:
72+
logging.info("Empty 'certificates' section found in DBX JSON file - validation passed")
73+
return errors
74+
75+
logging.info(f"Found {len(certificates)} certificate references to validate")
76+
77+
# Validate each certificate reference
78+
for i, cert_entry in enumerate(certificates):
79+
if 'value' not in cert_entry:
80+
errors.append(f"Certificate entry {i} missing 'value' field")
81+
continue
82+
83+
cert_filename = cert_entry['value']
84+
cert_path = certificates_dir / cert_filename
85+
86+
if not cert_path.exists():
87+
errors.append(f"Certificate file '{cert_filename}' referenced in JSON but not found in {certificates_dir}")
88+
logging.error(f"Missing certificate: {cert_filename}")
89+
else:
90+
logging.info(f"Certificate found: {cert_filename}")
91+
92+
return errors
93+
94+
95+
def main() -> None:
96+
"""Main function to handle command-line arguments and validate DBX certificate references."""
97+
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
98+
99+
parser = argparse.ArgumentParser(
100+
description="Validate that DBX JSON files reference existing certificate files."
101+
)
102+
parser.add_argument(
103+
"dbx_directory",
104+
help="Path to the PreSignedObjects/DBX directory",
105+
type=pathlib.Path
106+
)
107+
parser.add_argument(
108+
"--json-file",
109+
help="Specific DBX JSON file to validate (default: latest dbx_info_msft_*.json)",
110+
type=pathlib.Path
111+
)
112+
113+
args = parser.parse_args()
114+
115+
# Validate input directory
116+
if not args.dbx_directory.is_dir():
117+
logging.error(f"DBX directory does not exist: {args.dbx_directory}")
118+
sys.exit(1)
119+
120+
certificates_dir = args.dbx_directory / "Certificates"
121+
if not certificates_dir.is_dir():
122+
logging.error(f"Certificates directory does not exist: {certificates_dir}")
123+
sys.exit(1)
124+
125+
# Determine which JSON file to validate
126+
if args.json_file:
127+
dbx_json_path = args.json_file
128+
if not dbx_json_path.is_absolute():
129+
dbx_json_path = args.dbx_directory / dbx_json_path
130+
else:
131+
try:
132+
dbx_json_path = get_latest_dbx_info_file(args.dbx_directory)
133+
logging.info(f"Using latest DBX JSON file: {dbx_json_path.name}")
134+
except FileNotFoundError as e:
135+
logging.error(f"No DBX JSON files found in {args.dbx_directory}: {e}")
136+
sys.exit(1)
137+
138+
# Validate the JSON file exists
139+
if not dbx_json_path.exists():
140+
logging.error(f"DBX JSON file does not exist: {dbx_json_path}")
141+
sys.exit(1)
142+
143+
try:
144+
# Perform validation
145+
errors = validate_certificate_references(dbx_json_path, certificates_dir)
146+
147+
if errors:
148+
logging.error("Certificate reference validation failed:")
149+
for error in errors:
150+
logging.error(f" - {error}")
151+
152+
# List available certificate files for debugging
153+
available_certs = list(certificates_dir.glob("*"))
154+
if available_certs:
155+
logging.info("Available certificate files:")
156+
for cert_file in available_certs:
157+
logging.info(f" - {cert_file.name}")
158+
else:
159+
logging.warning("No certificate files found in the certificates directory")
160+
161+
sys.exit(1)
162+
else:
163+
logging.info("All certificate references validated successfully!")
164+
165+
except (FileNotFoundError, json.JSONDecodeError) as e:
166+
logging.error(f"Error reading DBX JSON file: {e}")
167+
sys.exit(1)
168+
except Exception as e:
169+
logging.error(f"Unexpected error during validation: {e}")
170+
sys.exit(1)
171+
172+
173+
if __name__ == "__main__":
174+
main()
175+

0 commit comments

Comments
 (0)