Documentation
Runtime Enforcement of Validation Library
Overview
Runtime enforcement ensures that validation is always performed, even if a developer tries to bypass it. This is achieved through:
- Mandatory wrapper functions that all compilers must use
- Validation metadata embedded in compilation results
- Verification signatures that prove validation occurred
- Runtime checks that prevent unvalidated compilation
Architecture
┌─────────────────────────────────────────┐
│ Compiler Code │
│ (TypeScript, .NET, Python, Rust) │
└────────────────┬────────────────────────┘
│
│ MUST USE
▼
┌─────────────────────────────────────────┐
│ compile_with_validation() │
│ (Runtime Enforcement Wrapper) │
│ │
│ ✓ Validates all local files │
│ ✓ Validates all remote URLs │
│ ✓ Creates validation metadata │
│ ✓ Generates verification signature │
│ ✓ Embeds proof in result │
└────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ @adguard/hostlist-compiler │
└─────────────────────────────────────────┘
How It Works
1. Mandatory Wrapper Function
All compilers MUST use compile_with_validation() instead of calling @adguard/hostlist-compiler directly:
❌ FORBIDDEN - Direct compilation bypass:
// This bypasses validation and is NOT ALLOWED
const result = await hostlistCompiler.compile(config);
✅ REQUIRED - Use enforcement wrapper:
import { compile_with_validation, CompilationInput } from '@adguard/validation';
const input = {
local_files: ['data/input/rules.txt'],
remote_urls: ['https://example.com/list.txt'],
expected_hashes: new Map([
['https://example.com/list.txt', 'sha384hash...']
])
};
const options = {
validation_config: ValidationConfig.default(),
output_path: 'data/output/filter.txt',
create_archive: true
};
// This is the ONLY allowed way to compile
const result = await compile_with_validation(input, options);
2. Validation Metadata
Every compilation result includes proof that validation was performed:
interface EnforcedCompilationResult {
success: boolean;
rule_count: number;
output_hash: string;
elapsed_ms: number;
output_path: string;
// PROOF OF VALIDATION
validation_metadata: {
validation_timestamp: string; // When validation occurred
local_files_validated: number; // Count of files validated
remote_urls_validated: number; // Count of URLs validated
hash_database_entries: number; // Hash DB size
validation_library_version: string; // Version used
strict_mode: boolean; // Security level
archive_created: string | null; // Archive path if created
signature: string; // SHA-384 verification signature
}
}
3. Verification Signature
The validation metadata includes a cryptographic signature that proves it wasn't forged:
function generateSignature(metadata: ValidationMetadata): string {
const data = `${metadata.validation_timestamp}:${metadata.local_files_validated}:${metadata.remote_urls_validated}:${metadata.validation_library_version}:${metadata.strict_mode}`;
return sha384(data); // 96 hex characters
}
To verify a result:
import { verify_compilation_was_validated } from '@adguard/validation';
// This throws an error if validation is missing or invalid
verify_compilation_was_validated(result);
4. Runtime Checks
The wrapper function performs these mandatory checks:
function compile_with_validation(input, options) {
const validator = new Validator(options.validation_config);
const metadata = createMetadata();
// CHECK 1: Validate all local files (CANNOT BE SKIPPED)
for (const file of input.local_files) {
const syntaxResult = validator.validate_local_file(file);
if (!syntaxResult.is_valid) {
throw new Error(`Validation failed: ${file}`);
}
metadata.local_files_validated++;
}
// CHECK 2: Validate all remote URLs (CANNOT BE SKIPPED)
for (const url of input.remote_urls) {
const urlResult = validator.validate_remote_url(url, expectedHash);
if (!urlResult.is_valid) {
throw new Error(`URL validation failed: ${url}`);
}
metadata.remote_urls_validated++;
}
// CHECK 3: Verify at least one source was validated
if (metadata.local_files_validated === 0 && metadata.remote_urls_validated === 0) {
throw new Error("No sources provided for compilation");
}
// Only AFTER validation passes, call the actual compiler
const output = await hostlistCompiler.compile(...);
// Generate verification signature
metadata.signature = generateSignature(metadata);
return { ...output, validation_metadata: metadata };
}
Enforcement Mechanisms
Mechanism 1: Type System Enforcement
TypeScript:
// Return type forces inclusion of validation metadata
export function compile(config: Config): Promise<EnforcedCompilationResult> {
// Can only return EnforcedCompilationResult which requires validation_metadata
return compile_with_validation(input, options);
}
.NET:
// Interface enforces validation metadata
public interface ICompilationResult {
bool Success { get; }
int RuleCount { get; }
ValidationMetadata ValidationMetadata { get; } // REQUIRED
}
Mechanism 2: CI/CD Verification
GitHub Actions verify that results include validation:
- name: Verify compilation includes validation
run: |
# Run compilation
OUTPUT=$(npm run compile --silent)
# Verify validation metadata exists
echo "$OUTPUT" | jq '.validation_metadata' || exit 1
# Verify signature is present and correct length (96 chars)
SIG=$(echo "$OUTPUT" | jq -r '.validation_metadata.signature')
if [ ${#SIG} -ne 96 ]; then
echo "Invalid validation signature"
exit 1
fi
# Verify files were actually validated
LOCAL_COUNT=$(echo "$OUTPUT" | jq '.validation_metadata.local_files_validated')
REMOTE_COUNT=$(echo "$OUTPUT" | jq '.validation_metadata.remote_urls_validated')
TOTAL=$((LOCAL_COUNT + REMOTE_COUNT))
if [ $TOTAL -eq 0 ]; then
echo "No sources were validated!"
exit 1
fi
Mechanism 3: Integration Tests
Required test that verifies enforcement:
describe('Runtime Enforcement', () => {
test('compilation result must include validation metadata', async () => {
const result = await compile(config);
// Metadata must exist
expect(result.validation_metadata).toBeDefined();
// Must have validated at least one source
const total = result.validation_metadata.local_files_validated +
result.validation_metadata.remote_urls_validated;
expect(total).toBeGreaterThan(0);
// Signature must be valid SHA-384 (96 hex chars)
expect(result.validation_metadata.signature).toMatch(/^[a-f0-9]{96}$/);
// Verification must pass
expect(() => verify_compilation_was_validated(result)).not.toThrow();
});
test('cannot forge validation metadata', async () => {
const result = await compile(config);
// Try to forge metadata
const forged = {
...result,
validation_metadata: {
...result.validation_metadata,
local_files_validated: 0, // Fake it
remote_urls_validated: 0, // Fake it
}
};
// Verification should fail because signature won't match
expect(() => verify_compilation_was_validated(forged)).toThrow();
});
});
Mechanism 4: Code Review Checklist
PR template includes:
## Runtime Enforcement Checklist
- [ ] Uses `compile_with_validation()` wrapper
- [ ] Does NOT call `hostlist-compiler` directly
- [ ] Returns `EnforcedCompilationResult` type
- [ ] Includes validation metadata in output
- [ ] Verification test passes
- [ ] CI validation check passes
Mechanism 5: Static Analysis
ESLint/Clippy rules detect bypass attempts:
// .eslintrc.js
module.exports = {
rules: {
'no-restricted-imports': ['error', {
patterns: [{
group: ['@adguard/hostlist-compiler'],
message: 'Do not import hostlist-compiler directly. Use compile_with_validation() from @adguard/validation instead.'
}]
}]
}
};
Language-Specific Implementation
TypeScript
import {
compile_with_validation,
CompilationInput,
CompilationOptions,
EnforcedCompilationResult
} from '@adguard/validation';
export async function compileRules(
sources: Source[]
): Promise<EnforcedCompilationResult> {
const input: CompilationInput = {
local_files: sources.filter(s => s.isLocal()).map(s => s.path),
remote_urls: sources.filter(s => s.isRemote()).map(s => s.url),
expected_hashes: new Map(
sources
.filter(s => s.expectedHash)
.map(s => [s.url, s.expectedHash!])
)
};
const options: CompilationOptions = {
validation_config: getValidationConfig(),
output_path: getOutputPath(),
create_archive: shouldCreateArchive()
};
return compile_with_validation(input, options);
}
.NET
using AdGuard.Validation;
public class RulesCompiler {
public async Task<EnforcedCompilationResult> CompileAsync(
IEnumerable<Source> sources) {
var input = new CompilationInput {
LocalFiles = sources.Where(s => s.IsLocal).Select(s => s.Path).ToList(),
RemoteUrls = sources.Where(s => !s.IsLocal).Select(s => s.Url).ToList(),
ExpectedHashes = sources
.Where(s => s.ExpectedHash != null)
.ToDictionary(s => s.Url, s => s.ExpectedHash)
};
var options = new CompilationOptions {
ValidationConfig = GetValidationConfig(),
OutputPath = GetOutputPath(),
CreateArchive = ShouldCreateArchive()
};
return await ValidationLibrary.CompileWithValidation(input, options);
}
}
Python
from adguard_validation import (
compile_with_validation,
CompilationInput,
CompilationOptions,
verify_compilation_was_validated
)
def compile_rules(sources: list[Source]) -> EnforcedCompilationResult:
input_data = CompilationInput(
local_files=[s.path for s in sources if s.is_local],
remote_urls=[s.url for s in sources if not s.is_local],
expected_hashes={
s.url: s.expected_hash
for s in sources
if s.expected_hash
}
)
options = CompilationOptions(
validation_config=get_validation_config(),
output_path=get_output_path(),
create_archive=should_create_archive()
)
result = compile_with_validation(input_data, options)
# Verify before returning
verify_compilation_was_validated(result)
return result
Rust
use adguard_validation::{
compile_with_validation,
CompilationInput,
CompilationOptions,
EnforcedCompilationResult,
};
pub fn compile_rules(sources: &[Source]) -> Result<EnforcedCompilationResult> {
let input = CompilationInput {
local_files: sources
.iter()
.filter(|s| s.is_local())
.map(|s| s.path.clone())
.collect(),
remote_urls: sources
.iter()
.filter(|s| !s.is_local())
.map(|s| s.url.clone())
.collect(),
expected_hashes: sources
.iter()
.filter_map(|s| s.expected_hash.as_ref().map(|h| (s.url.clone(), h.clone())))
.collect(),
};
let options = CompilationOptions {
validation_config: get_validation_config(),
output_path: get_output_path(),
create_archive: should_create_archive(),
};
compile_with_validation(input, options)
}
Audit Trail
Every compilation creates an audit log:
{
"timestamp": "2024-12-27T10:30:00Z",
"compiler": "typescript",
"compiler_version": "1.0.0",
"validation_library_version": "1.0.0",
"sources_validated": {
"local": 5,
"remote": 3
},
"validation_mode": "strict",
"output_hash": "abc123...",
"validation_signature": "def456...",
"archive_created": "data/archive/2024-12-27_10-30-00"
}
Bypassing Prevention
What Happens if Someone Tries to Bypass?
Scenario 1: Direct compilation
// Attempt to bypass
const result = await hostlistCompiler.compile(config);
Prevention:
- ESLint error: "Do not import hostlist-compiler directly"
- TypeScript error: Return type mismatch (no validation_metadata)
- CI fails: "Validation metadata missing from result"
- Code review: Automatic rejection
Scenario 2: Fake metadata
const result = {
...compilerOutput,
validation_metadata: { /* fake data */ }
};
Prevention:
- Signature verification fails
verify_compilation_was_validated()throws error- CI verification step fails
- Audit log shows mismatch
Scenario 3: Skip validation in wrapper
function compile_with_validation() {
// Skip actual validation
return { validation_metadata: makeFakeMetadata() };
}
Prevention:
- Integration tests fail (no actual files validated)
- Signature won't match expected pattern
- Code review catches missing validation calls
- Function is in external library (can't be modified)
Summary
Runtime enforcement ensures validation cannot be bypassed through:
- Mandatory wrapper - Only entry point for compilation
- Cryptographic signatures - Proof of validation that can't be forged
- Type system - Compiler enforces validation metadata presence
- CI/CD checks - Automated verification of every compilation
- Integration tests - Tests verify validation occurred
- Code review - Human verification of compliance
- Static analysis - Lint rules prevent direct compiler access
- Audit logs - Permanent record of all compilations
Result: It's easier to do validation correctly than to bypass it.