Appearance
RabbitMQ 客户端证书验证
概述
客户端证书验证是 mTLS 的核心组成部分,通过验证客户端证书来确认客户端身份的合法性。本文档详细介绍 RabbitMQ 客户端证书验证的配置方法、验证流程和最佳实践。
核心知识点
客户端证书验证流程
┌─────────────────────────────────────────────────────────────┐
│ 客户端证书验证流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. TLS 握手阶段 │
│ ├── 客户端发起连接 │
│ ├── 服务器发送证书请求 │
│ ├── 客户端发送证书链 │
│ └── 服务器验证证书有效性 │
│ │
│ 2. 证书验证阶段 │
│ ├── 验证证书签名 │
│ ├── 验证证书有效期 │
│ ├── 验证证书用途 (clientAuth) │
│ ├── 验证证书链完整性 │
│ └── 检查证书撤销状态 │
│ │
│ 3. 身份映射阶段 │
│ ├── 从证书提取用户名 │
│ ├── 查找用户权限 │
│ └── 建立连接 │
│ │
└─────────────────────────────────────────────────────────────┘验证级别配置
| 验证级别 | 配置值 | 说明 |
|---|---|---|
| 不验证 | verify_none | 不验证客户端证书(不安全) |
| 验证证书 | verify_peer | 验证客户端证书有效性 |
| 强制证书 | verify_peer + fail_if_no_peer_cert | 必须提供有效证书 |
用户名提取方式
┌─────────────────────────────────────────────────────────────┐
│ 用户名提取方式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Common Name (CN) │
│ ssl_cert_login_from = common_name │
│ 证书 CN: client.example.com │
│ 用户名: client.example.com │
│ │
│ 2. Subject DN │
│ ssl_cert_login_from = subject │
│ 证书主题: CN=client,O=Company,C=CN │
│ 用户名: CN=client,O=Company,C=CN │
│ │
│ 3. Subject Alternative Name (SAN) │
│ ssl_cert_login_from = san │
│ ssl_cert_login_san_type = dns │
│ SAN DNS: client.example.com │
│ 用户名: client.example.com │
│ │
└─────────────────────────────────────────────────────────────┘配置示例
基础客户端证书验证
conf
# /etc/rabbitmq/rabbitmq.conf
# TLS 监听器
listeners.ssl.default = 5671
# 服务器证书
ssl_options.cacertfile = /etc/rabbitmq/ssl/ca_certificate.pem
ssl_options.certfile = /etc/rabbitmq/ssl/server_certificate.pem
ssl_options.keyfile = /etc/rabbitmq/ssl/server_key.pem
# 客户端证书验证
ssl_options.verify = verify_peer
ssl_options.fail_if_no_peer_cert = true
# 用户名提取
ssl_cert_login_from = common_name高级验证配置
conf
# 证书链深度
ssl_options.depth = 2
# 证书撤销列表
ssl_options.crl_check = peer
ssl_options.crl_file = /etc/rabbitmq/ssl/crl.pem
# 证书用途验证
ssl_options.verify_fun = {ssl_verify_client, []}
# SAN 类型配置
ssl_cert_login_from = san
ssl_cert_login_san_type = dns
ssl_cert_login_san_index = 1混合认证配置
conf
# 同时支持证书认证和用户名密码认证
auth_mechanisms.1 = EXTERNAL
auth_mechanisms.2 = PLAIN
auth_mechanisms.3 = AMQPLAIN
# 证书验证(可选)
ssl_options.verify = verify_peer
ssl_options.fail_if_no_peer_cert = false
# 证书用户名提取
ssl_cert_login_from = common_name证书认证插件配置
bash
# 启用证书认证插件
rabbitmq-plugins enable rabbitmq_auth_mechanism_ssl
# 配置文件
# /etc/rabbitmq/rabbitmq.conf
auth_mechanisms.1 = EXTERNAL
ssl_cert_login_from = common_name
ssl_options.verify = verify_peer
ssl_options.fail_if_no_peer_cert = truePHP 代码示例
客户端证书连接类
php
<?php
use PhpAmqpLib\Connection\AMQPSSLConnection;
class ClientCertConnection
{
private $config;
public function __construct(array $config)
{
$this->config = $config;
}
public function connect(): AMQPSSLConnection
{
$sslOptions = $this->buildSSLOptions();
return new AMQPSSLConnection(
$this->config['host'],
$this->config['port'] ?? 5671,
'',
'',
$this->config['vhost'] ?? '/',
$sslOptions,
$this->config['options'] ?? []
);
}
private function buildSSLOptions(): array
{
$options = [
'cafile' => $this->config['ca_cert'],
'verify_peer' => true,
'verify_peer_name' => true,
'allow_self_signed' => $this->config['allow_self_signed'] ?? false
];
if (isset($this->config['client_cert'])) {
$options['local_cert'] = $this->config['client_cert'];
}
if (isset($this->config['client_key'])) {
$options['local_pk'] = $this->config['client_key'];
}
if (isset($this->config['passphrase'])) {
$options['passphrase'] = $this->config['passphrase'];
}
if (isset($this->config['ciphers'])) {
$options['ciphers'] = $this->config['ciphers'];
}
return $options;
}
public function getCertificateInfo(): array
{
if (!isset($this->config['client_cert'])) {
return ['error' => '未配置客户端证书'];
}
$content = file_get_contents($this->config['client_cert']);
$cert = openssl_x509_read($content);
if ($cert === false) {
return ['error' => '无法解析证书'];
}
$info = openssl_x509_parse($cert);
return [
'subject' => $info['subject'],
'issuer' => $info['issuer'],
'valid_from' => date('Y-m-d H:i:s', $info['validFrom_time_t']),
'valid_to' => date('Y-m-d H:i:s', $info['validTo_time_t']),
'serial_number' => $info['serialNumber'],
'fingerprint' => openssl_x509_fingerprint($cert, 'sha256')
];
}
}
// 使用示例
$connection = new ClientCertConnection([
'host' => 'rabbitmq.example.com',
'port' => 5671,
'vhost' => '/',
'ca_cert' => '/path/to/ca_certificate.pem',
'client_cert' => '/path/to/client_certificate.pem',
'client_key' => '/path/to/client_key.pem',
'passphrase' => 'optional_passphrase',
'ciphers' => 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256'
]);
$conn = $connection->connect();
echo "客户端证书认证连接成功\n";
$certInfo = $connection->getCertificateInfo();
print_r($certInfo);证书用户管理服务
php
<?php
class CertificateUserService
{
private $apiUrl;
private $credentials;
public function __construct(string $host, int $port, string $user, string $password)
{
$this->apiUrl = "http://{$host}:{$port}/api";
$this->credentials = [$user, $password];
}
public function createCertificateUser(string $commonName, array $permissions = []): bool
{
$username = $this->normalizeUsername($commonName);
$response = $this->httpRequest(
"{$this->apiUrl}/users/{$username}",
'PUT',
[
'password' => '',
'tags' => ''
]
);
if ($response['status'] !== 204) {
return false;
}
if (!empty($permissions)) {
return $this->setPermissions($username, $permissions);
}
return true;
}
public function setPermissions(string $username, array $permissions): bool
{
$vhost = urlencode($permissions['vhost'] ?? '/');
$url = "{$this->apiUrl}/permissions/{$vhost}/{$username}";
$response = $this->httpRequest($url, 'PUT', [
'configure' => $permissions['configure'] ?? '.*',
'write' => $permissions['write'] ?? '.*',
'read' => $permissions['read'] ?? '.*'
]);
return $response['status'] === 204;
}
public function deleteCertificateUser(string $commonName): bool
{
$username = $this->normalizeUsername($commonName);
$response = $this->httpRequest(
"{$this->apiUrl}/users/{$username}",
'DELETE'
);
return $response['status'] === 204;
}
public function listCertificateUsers(): array
{
$response = $this->httpRequest("{$this->apiUrl}/users", 'GET');
if ($response['status'] !== 200) {
return [];
}
$users = json_decode($response['body'], true) ?: [];
return array_filter($users, function ($user) {
return empty($user['password_hash']) || $user['password_hash'] === '';
});
}
public function getUserPermissions(string $commonName): array
{
$username = $this->normalizeUsername($commonName);
$response = $this->httpRequest(
"{$this->apiUrl}/users/{$username}/permissions",
'GET'
);
if ($response['status'] !== 200) {
return [];
}
return json_decode($response['body'], true) ?: [];
}
private function normalizeUsername(string $commonName): string
{
return strtolower(preg_replace('/[^a-zA-Z0-9._-]/', '_', $commonName));
}
private function httpRequest(string $url, string $method, array $data = null): array
{
$ch = curl_init($url);
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_USERPWD => implode(':', $this->credentials),
CURLOPT_HTTPHEADER => ['Content-Type: application/json']
];
if ($data !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($data);
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['status' => $status, 'body' => $body];
}
}
// 使用示例
$service = new CertificateUserService('localhost', 15672, 'admin', 'password');
// 创建证书用户
$service->createCertificateUser('client.example.com', [
'vhost' => '/',
'configure' => '^client_.*',
'write' => '^client_.*',
'read' => '^client_.*'
]);
// 列出所有证书用户
$users = $service->listCertificateUsers();
print_r($users);证书验证服务
php
<?php
class ClientCertificateValidator
{
private $caCertPath;
public function __construct(string $caCertPath)
{
$this->caCertPath = $caCertPath;
}
public function validate(string $certPath, string $keyPath = null): array
{
$results = [
'valid' => false,
'checks' => []
];
$results['checks']['exists'] = $this->checkExists($certPath);
$results['checks']['parseable'] = $this->checkParseable($certPath);
$results['checks']['validity_period'] = $this->checkValidityPeriod($certPath);
$results['checks']['chain'] = $this->checkChain($certPath);
$results['checks']['purpose'] = $this->checkClientAuthPurpose($certPath);
if ($keyPath) {
$results['checks']['key_match'] = $this->checkKeyMatch($certPath, $keyPath);
}
$results['valid'] = !in_array(false, array_column($results['checks'], 'passed'), true);
return $results;
}
private function checkExists(string $path): array
{
return [
'passed' => file_exists($path),
'message' => file_exists($path) ? '文件存在' : '文件不存在'
];
}
private function checkParseable(string $path): array
{
$content = file_get_contents($path);
$cert = @openssl_x509_read($content);
return [
'passed' => $cert !== false,
'message' => $cert !== false ? '证书格式正确' : '无法解析证书'
];
}
private function checkValidityPeriod(string $path): array
{
$content = file_get_contents($path);
$cert = openssl_x509_read($content);
$info = openssl_x509_parse($cert);
$now = time();
$isExpired = $info['validTo_time_t'] < $now;
$notYetValid = $info['validFrom_time_t'] > $now;
$daysRemaining = floor(($info['validTo_time_t'] - $now) / 86400);
return [
'passed' => !$isExpired && !$notYetValid,
'message' => $isExpired ? '证书已过期' :
($notYetValid ? '证书尚未生效' : "证书有效,剩余 {$daysRemaining} 天"),
'days_remaining' => max(0, $daysRemaining)
];
}
private function checkChain(string $path): array
{
$cert = file_get_contents($path);
$ca = file_get_contents($this->caCertPath);
$certResource = openssl_x509_read($cert);
$result = @openssl_x509_checkpurpose($certResource, X509_PURPOSE_ANY, [$ca]);
return [
'passed' => $result === true,
'message' => $result === true ? '证书链验证通过' : '证书链验证失败'
];
}
private function checkClientAuthPurpose(string $path): array
{
$cert = file_get_contents($path);
$certResource = openssl_x509_read($cert);
$result = @openssl_x509_checkpurpose($certResource, X509_PURPOSE_SSL_CLIENT);
return [
'passed' => $result === true,
'message' => $result === true ? '证书可用于客户端认证' : '证书不可用于客户端认证'
];
}
private function checkKeyMatch(string $certPath, string $keyPath): array
{
$cert = file_get_contents($certPath);
$key = file_get_contents($keyPath);
$certResource = openssl_x509_read($cert);
$keyResource = openssl_pkey_get_private($key);
$match = @openssl_x509_check_private_key($certResource, $keyResource);
return [
'passed' => $match,
'message' => $match ? '证书与私钥匹配' : '证书与私钥不匹配'
];
}
public function extractUsername(string $certPath, string $method = 'cn'): ?string
{
$content = file_get_contents($certPath);
$cert = openssl_x509_read($content);
$info = openssl_x509_parse($cert);
switch ($method) {
case 'cn':
return $info['subject']['CN'] ?? null;
case 'subject':
return $this->formatDN($info['subject']);
case 'email':
return $info['subject']['emailAddress'] ?? null;
default:
return null;
}
}
private function formatDN(array $dn): string
{
$parts = [];
foreach ($dn as $key => $value) {
$parts[] = "{$key}={$value}";
}
return implode(',', $parts);
}
}
// 使用示例
$validator = new ClientCertificateValidator('/path/to/ca_certificate.pem');
$result = $validator->validate(
'/path/to/client_certificate.pem',
'/path/to/client_key.pem'
);
if ($result['valid']) {
echo "客户端证书验证通过\n";
$username = $validator->extractUsername('/path/to/client_certificate.pem');
echo "提取的用户名: {$username}\n";
} else {
echo "客户端证书验证失败\n";
print_r($result['checks']);
}证书撤销检查
php
<?php
class CertificateRevocationChecker
{
private $crlPath;
private $caCertPath;
public function __construct(string $crlPath, string $caCertPath)
{
$this->crlPath = $crlPath;
$this->caCertPath = $caCertPath;
}
public function isRevoked(string $certPath): bool
{
if (!file_exists($this->crlPath)) {
return false;
}
$cert = file_get_contents($certPath);
$certResource = openssl_x509_read($cert);
$certInfo = openssl_x509_parse($certResource);
$crl = file_get_contents($this->crlPath);
$crlResource = openssl_crl_read($crl);
if ($crlResource === false) {
return false;
}
$crlInfo = openssl_crl_parse($crlResource);
$serialNumber = $certInfo['serialNumber'];
$serialNumberHex = strtoupper(ltrim($serialNumber, '0x'));
return $this->checkSerialInCRL($serialNumberHex, $crlInfo);
}
private function checkSerialInCRL(string $serial, array $crlInfo): bool
{
if (!isset($crlInfo['revoked'])) {
return false;
}
foreach ($crlInfo['revoked'] as $revoked) {
if (strtoupper($revoked['serial']) === $serial) {
return true;
}
}
return false;
}
public function getCRLInfo(): array
{
if (!file_exists($this->crlPath)) {
return ['error' => 'CRL 文件不存在'];
}
$crl = file_get_contents($this->crlPath);
$crlResource = openssl_crl_read($crl);
if ($crlResource === false) {
return ['error' => '无法解析 CRL'];
}
$info = openssl_crl_parse($crlResource);
return [
'issuer' => $info['issuer'] ?? null,
'last_update' => isset($info['lastUpdate']) ? date('Y-m-d H:i:s', $info['lastUpdate']) : null,
'next_update' => isset($info['nextUpdate']) ? date('Y-m-d H:i:s', $info['nextUpdate']) : null,
'revoked_count' => count($info['revoked'] ?? [])
];
}
}实际应用场景
场景一:服务间证书认证
php
<?php
class ServiceCertificateAuth
{
private $serviceName;
private $certConfig;
public function __construct(string $serviceName, array $certConfig)
{
$this->serviceName = $serviceName;
$this->certConfig = $certConfig;
}
public function authenticate(): ?AMQPSSLConnection
{
$validator = new ClientCertificateValidator($this->certConfig['ca_cert']);
$validation = $validator->validate(
$this->certConfig['client_cert'],
$this->certConfig['client_key']
);
if (!$validation['valid']) {
throw new RuntimeException("服务证书验证失败: " . json_encode($validation['checks']));
}
$connection = new ClientCertConnection($this->certConfig);
return $connection->connect();
}
public function getServiceIdentity(): array
{
$validator = new ClientCertificateValidator($this->certConfig['ca_cert']);
return [
'service_name' => $this->serviceName,
'certificate_cn' => $validator->extractUsername($this->certConfig['client_cert']),
'certificate_valid' => $validator->validate($this->certConfig['client_cert'])['valid']
];
}
}场景二:证书自动注册
php
<?php
class CertificateAutoRegistration
{
private $userService;
private $validator;
public function __construct(CertificateUserService $userService, ClientCertificateValidator $validator)
{
$this->userService = $userService;
$this->validator = $validator;
}
public function registerCertificate(string $certPath, array $permissions = []): array
{
$validation = $this->validator->validate($certPath);
if (!$validation['valid']) {
return [
'success' => false,
'error' => '证书验证失败',
'details' => $validation['checks']
];
}
$username = $this->validator->extractUsername($certPath);
if (!$username) {
return [
'success' => false,
'error' => '无法从证书提取用户名'
];
}
$created = $this->userService->createCertificateUser($username, $permissions);
return [
'success' => $created,
'username' => $username,
'message' => $created ? '证书用户注册成功' : '证书用户注册失败'
];
}
}常见问题与解决方案
问题 1:证书用户不存在
错误信息:
ACCESS_REFUSED - Login was refused using authentication mechanism EXTERNAL解决方案:
bash
# 创建证书对应的用户
rabbitmqctl add_user client_cn ""
# 设置权限
rabbitmqctl set_permissions -p / client_cn ".*" ".*" ".*"问题 2:证书用途错误
错误信息:
Certificate purpose does not match解决方案:
bash
# 检查证书扩展用途
openssl x509 -in client_certificate.pem -noout -text | grep -A1 "Extended Key Usage"
# 重新生成证书时添加 clientAuth 用途
openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key \
-CAcreateserial -out client_certificate.pem -days 365 \
-extfile <(echo "extendedKeyUsage=clientAuth")问题 3:证书链验证失败
解决方案:
bash
# 检查证书链
openssl verify -CAfile ca_certificate.pem client_certificate.pem
# 如果有中间证书,合并验证
cat intermediate.pem ca.pem > full_chain.pem
openssl verify -CAfile full_chain.pem client_certificate.pem最佳实践建议
1. 证书权限模板
php
<?php
class CertificatePermissionTemplates
{
const TEMPLATES = [
'producer' => [
'configure' => '^$',
'write' => '.*',
'read' => '^$'
],
'consumer' => [
'configure' => '^$',
'write' => '^$',
'read' => '.*'
],
'full_access' => [
'configure' => '.*',
'write' => '.*',
'read' => '.*'
],
'isolated' => [
'configure' => '^{{prefix}}_.*',
'write' => '^{{prefix}}_.*',
'read' => '^{{prefix}}_.*'
]
];
public static function applyTemplate(string $template, array $variables = []): array
{
$permissions = self::TEMPLATES[$template] ?? self::TEMPLATES['isolated'];
foreach ($permissions as $key => $value) {
foreach ($variables as $var => $val) {
$permissions[$key] = str_replace('{{' . $var . '}}', $val, $permissions[$key]);
}
}
return $permissions;
}
}2. 证书监控脚本
php
<?php
class CertificateMonitor
{
private $certDir;
private $warningDays;
public function checkAll(): array
{
$certificates = glob($this->certDir . '/*_certificate.pem');
$results = [];
foreach ($certificates as $certPath) {
$name = basename($certPath, '_certificate.pem');
$results[$name] = $this->checkCertificate($certPath);
}
return $results;
}
private function checkCertificate(string $path): array
{
$content = file_get_contents($path);
$cert = openssl_x509_read($content);
$info = openssl_x509_parse($cert);
$now = time();
$daysRemaining = floor(($info['validTo_time_t'] - $now) / 86400);
return [
'valid' => $info['validTo_time_t'] > $now,
'days_remaining' => $daysRemaining,
'needs_renewal' => $daysRemaining <= $this->warningDays,
'expires_at' => date('Y-m-d H:i:s', $info['validTo_time_t']),
'subject_cn' => $info['subject']['CN'] ?? 'Unknown'
];
}
}安全注意事项
重要警告
- 强制证书验证:生产环境必须设置
fail_if_no_peer_cert = true - 证书用途验证:确保证书包含
clientAuth扩展用途 - 证书撤销机制:配置 CRL 或 OCSP 进行证书撤销检查
- 定期审计:检查证书用户权限是否合理
- 证书有效期监控:设置告警监控证书过期
相关链接
- SSL/TLS 加密 - TLS 加密配置详解
- 证书配置 - 证书生成与配置
- 双向认证 - mTLS 双向认证配置
- 认证机制概述 - RabbitMQ 认证体系
