HTTPS and TLS Configuration¶
The Danish Parliament API implements strong TLS encryption to protect data in transit. This guide covers TLS configuration details, certificate management, and client-side implementation best practices.
TLS Configuration Analysis¶
Current TLS Setup¶
Based on comprehensive security testing (Phase 15 investigation), the API uses:
TLS Version: 1.2
Cipher Suite: ECDHE-RSA-AES256-SHA384
Certificate Authority: GlobalSign RSA OV SSL CA 2018
Certificate Validity: Until August 3, 2026
Wildcard Coverage: *.ft.dk (covers oda.ft.dk)
Certificate Details¶
# Verify current certificate status
curl -I -v "https://oda.ft.dk/api/Sag" 2>&1 | grep -E "(SSL|TLS|Certificate)"
# Expected output:
# * TLSv1.2 (OUT), TLS handshake, Client hello (1):
# * TLSv1.2 (IN), TLS handshake, Server hello (2):
# * TLSv1.2 (IN), TLS handshake, Certificate (11):
# * SSL certificate verify ok.
Security Strength Assessment¶
Strengths: - ✅ Strong cipher suite (AES-256, SHA-384) - ✅ Forward secrecy (ECDHE key exchange) - ✅ Valid certificate chain - ✅ Wildcard certificate properly configured - ✅ Certificate from trusted CA (GlobalSign)
Limitations: - No HTTP/2 support (uses HTTP/1.1) - Missing security headers (HSTS, CSP) - Aggressive no-cache policies (no ETag support)
Client Configuration¶
Python Implementation¶
import requests
import ssl
import urllib3
from requests.adapters import HTTPAdapter
class SecureDanishParliamentClient:
def __init__(self):
self.session = requests.Session()
self.base_url = "https://oda.ft.dk/api"
self._configure_tls()
def _configure_tls(self):
"""Configure secure TLS settings"""
# Create SSL context with strong security
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = True
ssl_context.verify_mode = ssl.CERT_REQUIRED
# Disable weak protocols
ssl_context.options |= ssl.OP_NO_SSLv2
ssl_context.options |= ssl.OP_NO_SSLv3
ssl_context.options |= ssl.OP_NO_TLSv1
ssl_context.options |= ssl.OP_NO_TLSv1_1
# Set minimum TLS version to 1.2
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
# Configure adapter
adapter = HTTPAdapter()
self.session.mount('https://', adapter)
# Verify certificate
self.session.verify = True
def verify_connection_security(self):
"""Verify TLS connection security"""
try:
response = self.session.get(f"{self.base_url}/Sag?$top=1")
# Check response headers for security info
connection_info = {
'status_code': response.status_code,
'url': response.url,
'headers': dict(response.headers),
'tls_verified': response.url.startswith('https://'),
'certificate_valid': True # If no SSL error occurred
}
return connection_info
except requests.exceptions.SSLError as e:
return {'error': f'TLS verification failed: {e}'}
def get_with_security_check(self, endpoint, params=None):
"""Make API request with security validation"""
try:
response = self.session.get(
f"{self.base_url}/{endpoint}",
params=params,
timeout=30 # Prevent hanging connections
)
response.raise_for_status()
return response.json()
except requests.exceptions.SSLError as e:
raise SecurityError(f"TLS security check failed: {e}")
except requests.exceptions.RequestException as e:
raise APIError(f"Request failed: {e}")
# Usage example
client = SecureDanishParliamentClient()
security_status = client.verify_connection_security()
print(f"Connection security: {security_status}")
JavaScript/Node.js Implementation¶
const https = require('https');
const crypto = require('crypto');
class SecureDanishParliamentClient {
constructor() {
this.baseURL = 'https://oda.ft.dk/api';
this.httpsAgent = this.createSecureAgent();
}
createSecureAgent() {
return new https.Agent({
// Enforce TLS 1.2+
secureProtocol: 'TLSv1_2_method',
// Certificate verification
checkServerIdentity: (host, cert) => {
if (host !== 'oda.ft.dk') {
throw new Error('Invalid hostname in certificate');
}
// Verify certificate chain
const now = new Date();
const notAfter = new Date(cert.valid_to);
if (now > notAfter) {
throw new Error('Certificate has expired');
}
return undefined; // Certificate is valid
},
// Cipher suite restrictions
ciphers: [
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-SHA384',
'ECDHE-RSA-AES128-GCM-SHA256'
].join(':'),
// Security options
honorCipherOrder: true,
rejectUnauthorized: true
});
}
async makeSecureRequest(endpoint, params = {}) {
const url = new URL(endpoint, this.baseURL);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
return new Promise((resolve, reject) => {
const options = {
agent: this.httpsAgent,
headers: {
'User-Agent': 'Danish-Parliament-Client/1.0',
'Accept': 'application/json'
}
};
https.get(url, options, (response) => {
let data = '';
response.on('data', chunk => data += chunk);
response.on('end', () => {
if (response.statusCode === 200) {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error('Invalid JSON response'));
}
} else {
reject(new Error(`HTTP ${response.statusCode}`));
}
});
}).on('error', (error) => {
if (error.code === 'CERT_UNTRUSTED') {
reject(new Error('Certificate trust verification failed'));
} else if (error.code === 'EPROTO') {
reject(new Error('TLS protocol error'));
} else {
reject(error);
}
});
});
}
async verifyCertificate() {
return new Promise((resolve, reject) => {
const options = {
host: 'oda.ft.dk',
port: 443,
agent: this.httpsAgent
};
const socket = crypto.connect(options, () => {
const cert = socket.getPeerCertificate(true);
const result = {
subject: cert.subject,
issuer: cert.issuer,
valid_from: cert.valid_from,
valid_to: cert.valid_to,
fingerprint: cert.fingerprint,
serialNumber: cert.serialNumber,
is_valid: socket.authorized
};
socket.end();
resolve(result);
});
socket.on('error', reject);
});
}
}
// Usage
const client = new SecureDanishParliamentClient();
// Verify certificate before making requests
client.verifyCertificate()
.then(cert => {
console.log('Certificate verified:', cert);
return client.makeSecureRequest('Sag', {'$top': '5'});
})
.then(data => console.log('API data:', data))
.catch(error => console.error('Security error:', error));
Browser Implementation¶
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self' https://oda.ft.dk;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';">
<title>Secure Danish Parliament API Client</title>
</head>
<body>
<script>
class BrowserSecureClient {
constructor() {
this.baseURL = 'https://oda.ft.dk/api';
}
async makeSecureRequest(endpoint, params = {}) {
const url = new URL(endpoint, this.baseURL);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
try {
const response = await fetch(url, {
method: 'GET',
mode: 'cors',
credentials: 'omit', // No credentials needed
headers: {
'Accept': 'application/json',
'Cache-Control': 'no-cache'
},
// Browser automatically handles TLS
redirect: 'follow'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// Verify we're still on HTTPS
if (!response.url.startsWith('https://')) {
throw new Error('Response was not served over HTTPS');
}
return await response.json();
} catch (error) {
console.error('Secure request failed:', error);
throw error;
}
}
checkConnectionSecurity() {
return {
protocol: location.protocol,
is_secure: location.protocol === 'https:',
mixed_content_blocked: !window.isSecureContext,
tls_version: 'Managed by browser'
};
}
}
// Initialize secure client
const client = new BrowserSecureClient();
// Check security status
console.log('Connection security:', client.checkConnectionSecurity());
// Make secure API request
client.makeSecureRequest('Sag', {'$top': '3'})
.then(data => console.log('Secure API response:', data))
.catch(error => console.error('Security error:', error));
</script>
</body>
</html>
Certificate Management¶
Monitoring Certificate Expiry¶
import ssl
import socket
from datetime import datetime
def check_certificate_expiry(hostname🔧oda.ft.dk', port=443):
"""Monitor certificate expiry"""
context = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
# Parse expiry date
expiry_date = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')
days_until_expiry = (expiry_date - datetime.now()).days
return {
'certificate_subject': cert['subject'],
'issuer': cert['issuer'],
'expires': expiry_date.isoformat(),
'days_until_expiry': days_until_expiry,
'is_valid': days_until_expiry > 0
}
# Example monitoring
cert_status = check_certificate_expiry()
print(f"Certificate expires in {cert_status['days_until_expiry']} days")
if cert_status['days_until_expiry'] < 30:
print("WARNING: Certificate expires soon!")
Certificate Pinning (Advanced)¶
import hashlib
import ssl
# Expected certificate fingerprints (update when certificates change)
EXPECTED_CERT_FINGERPRINTS = [
'sha256:expected_fingerprint_here' # Update with actual fingerprint
]
def verify_certificate_pin(hostname🔧oda.ft.dk', port=443):
"""Verify certificate against known fingerprints"""
context = ssl.create_default_context()
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert_der = ssock.getpeercert_chain()[0].encode('DER')
cert_fingerprint = hashlib.sha256(cert_der).hexdigest()
if f"sha256:{cert_fingerprint}" in EXPECTED_CERT_FINGERPRINTS:
return True
else:
raise SecurityError(f"Certificate fingerprint mismatch: {cert_fingerprint}")
Troubleshooting TLS Issues¶
Common TLS Problems¶
1. Certificate Verification Errors¶
# Problem: Certificate verification fails
# Solution: Update certificate store or check system time
import certifi
import requests
# Use latest certificate bundle
response = requests.get(
'https://oda.ft.dk/api/Sag',
verify=certifi.where()
)
2. TLS Version Mismatches¶
# Problem: Client using outdated TLS version
# Solution: Explicitly configure TLS 1.2+
import ssl
import urllib3
# Disable warnings for testing only
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Create context with specific TLS version
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.minimum_version = ssl.TLSVersion.TLSv1_2
3. Cipher Suite Compatibility¶
# Test supported cipher suites
openssl s_client -connect oda.ft.dk:443 -cipher 'ECDHE-RSA-AES256-SHA384'
# Expected output should include:
# Cipher : ECDHE-RSA-AES256-SHA384
# Verification: OK
Diagnostic Commands¶
# Comprehensive TLS analysis
curl -I -v "https://oda.ft.dk/api/Sag" 2>&1 | grep -E "(TLS|SSL|Certificate|Cipher)"
# Test specific TLS version
openssl s_client -connect oda.ft.dk:443 -tls1_2
# Certificate chain analysis
openssl s_client -connect oda.ft.dk:443 -showcerts < /dev/null
# Check certificate expiry
openssl s_client -connect oda.ft.dk:443 < /dev/null 2>/dev/null | openssl x509 -noout -dates
Best Practices Summary¶
Security Requirements¶
- Always use HTTPS: Never make API requests over HTTP
- Verify certificates: Enable certificate verification in all clients
- Use TLS 1.2+: Disable older protocol versions
- Monitor expiry: Track certificate expiration dates
- Handle errors: Properly handle TLS-related exceptions
Performance Considerations¶
- Connection pooling: Reuse TLS connections when possible
- Timeout settings: Set appropriate connection timeouts
- Certificate caching: Cache certificate validation results
- HTTP/1.1 limitation: API doesn't support HTTP/2, optimize accordingly
Compliance Integration¶
- GDPR: Ensure encrypted data transmission for personal information
- Data Quality: Verify data integrity through secure connections
- Monitoring: Log TLS-related security events
The Danish Parliament API's TLS implementation provides strong encryption for data in transit. While it lacks some modern features like HTTP/2 and security headers, the core TLS configuration is robust and suitable for production use.