Azure App Service 使用客户端证书 POST 时出现 Security Error
结论
能够从 CurrentUser\My 读取证书,且 HasPrivateKey == true,说明应用已经找到证书和私钥,但无法据此判断 TLS 双向认证是否成功。
日志中的关键字节是:
15 03 03 00 02 02 28
其中,02 28 表示对端返回了 fatal handshake_failure(TLS Alert 40)。根据调用顺序,Azure 实例向 Swish 发送客户端证书后,Swish 终止了握手。问题出在 TLS 握手阶段,此时 HTTP POST 请求尚未发送。
常见原因是 Azure 与本地环境中的证书链不同。App Service 加载了叶证书和私钥,却缺少中间 CA,或者 Swish 不信任该证书的根 CA。如果证书用途、测试或生产环境、私钥提供程序、证书有效期等不符合 Swish 的要求,也会触发相同的 handshake_failure。
ServerCertificateCustomValidationCallback 负责客户端对 Swish 服务器证书的验证,无法让 Swish 接受客户端证书。
为什么本地可以,Azure 不可以
本地计算机的证书存储通常已经安装了完整证书链,包括:
- 带私钥的客户端叶证书;
- 一个或多个中间 CA 证书;
- 根 CA 证书。
将 PFX 上传到 App Service 后,平台通常只保证指定证书可以从 CurrentUser\My 加载。本地计算机上的中间 CA 和根 CA 不一定也存在于 Azure 实例中。
日志显示客户端发送了 TLS Certificate 消息,但无法证明其中包含完整证书链。Swish 可能因为以下原因拒绝握手:
- 客户端没有发送必要的中间 CA 证书。
- Swish 不信任该客户端证书的签发根。
- 使用测试证书访问了生产地址,或使用生产证书访问了测试地址。
- 证书已过期、尚未生效或已被吊销。
- 证书缺少
Client AuthenticationEKU,或密钥用途不允许数字签名。 - Azure 进程可以看到私钥,但 Schannel 无法使用该私钥完成握手签名。
- Swish 绑定了来源 IP、商户号、
payeeAlias或证书,而 Azure 的出站信息尚未登记。
TLS Alert 40 是通用错误。仅凭客户端日志无法判断 Swish 拒绝了哪一项,需要结合 Swish 服务端的握手日志确认具体原因。
Azure App Service 的证书配置
先在 App Service 中上传包含私钥的 PFX,再添加以下应用设置:
WEBSITE_LOAD_CERTIFICATES=800CDE7BC219D53812FC164DFF0FE73DF8A8D7B6
排查时也可以临时使用:
WEBSITE_LOAD_CERTIFICATES=*
修改设置后重启 App Service。排查结束后,生产环境应恢复为明确的指纹列表。
代码应从 StoreLocation.CurrentUser 下的 StoreName.My 查找证书,并在发送请求前检查证书状态:
private static X509Certificate2 LoadClientCertificate(string thumbprint)
{
string normalizedThumbprint = thumbprint
.Replace(" ", string.Empty)
.ToUpperInvariant();
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(
X509FindType.FindByThumbprint,
normalizedThumbprint,
validOnly: false);
if (certificates.Count == 0)
{
throw new InvalidOperationException(
$"Client certificate {normalizedThumbprint} was not found.");
}
var certificate = certificates[0];
if (!certificate.HasPrivateKey)
{
throw new InvalidOperationException(
"The client certificate does not contain an accessible private key.");
}
var now = DateTime.UtcNow;
if (now < certificate.NotBefore.ToUniversalTime() ||
now > certificate.NotAfter.ToUniversalTime())
{
throw new InvalidOperationException(
$"The client certificate is not valid at {now:O}.");
}
return certificate;
}
}
检查证书链
在 Azure 实例上显式构建证书链,可以判断是否缺少中间证书:
private static string CheckCertificateChain(X509Certificate2 certificate)
{
using (var chain = new X509Chain())
{
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
bool valid = chain.Build(certificate);
var status = string.Join(
Environment.NewLine,
chain.ChainStatus.Select(x =>
$"{x.Status}: {x.StatusInformation?.Trim()}"));
return $"Chain valid: {valid}{Environment.NewLine}{status}";
}
}
如果结果中出现 PartialChain、UntrustedRoot 等状态,应检查 PFX 是否包含中间证书,以及相关 CA 证书是否通过当前 App Service 环境支持的方式加载到正确的证书存储中。
中间 CA 通常应放入:
CurrentUser\CertificateAuthority
根 CA 对应:
CurrentUser\Root
不要直接将根证书添加到 handler.ClientCertificates。TLS 客户端通常发送叶证书和必要的中间证书,不会发送根证书。根证书必须已经得到 Swish 服务端信任。在客户端安装根证书只会影响客户端自身的证书链验证,无法改变 Swish 的信任配置。
如果当前 App Service 环境无法按需配置 CA 存储,可以考虑改用支持自定义证书存储的 Windows 容器、虚拟机或其他托管环境。
推荐的请求代码
确认链和证书用途正确后,只启用 Swish 支持的 TLS 版本。此类接口应优先使用 TLS 1.2,无需同时启用旧版 Tls 和 Tls11:
var certificate = LoadClientCertificate(
"800CDE7BC219D53812FC164DFF0FE73DF8A8D7B6");
var handler = new HttpClientHandler
{
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = SslProtocols.Tls12,
AutomaticDecompression =
DecompressionMethods.Deflate | DecompressionMethods.GZip
};
handler.ClientCertificates.Add(certificate);
using (var client = new HttpClient(handler))
using (var content = new StringContent(
JsonConvert.SerializeObject(post),
Encoding.UTF8,
"application/json"))
{
HttpResponseMessage response = await client.PostAsync(
"https://mss.swicpc.bankgirot.se/swish-cpcapi/api/v1/paymentrequests/",
content);
string responseBody = await response.Content.ReadAsStringAsync();
if (response.StatusCode != HttpStatusCode.Created)
{
throw new HttpRequestException(
$"Swish returned {(int)response.StatusCode} " +
$"{response.ReasonPhrase}: {responseBody}");
}
}
实际项目应复用 HttpClient,避免为每个请求创建并销毁实例。可以在应用启动时加载证书并创建客户端,也可以通过 IHttpClientFactory 管理客户端生命周期。
不要绕过服务器证书验证
以下代码会接受任何服务器证书:
handler.ServerCertificateCustomValidationCallback =
(message, certificate, chain, errors) => true;
这段代码完全关闭了服务器证书验证,会在生产环境中留下中间人攻击风险,也无法解决 Swish 拒绝客户端证书的问题。
如果 Swish 测试环境使用私有 CA,应将相应 CA 正确配置到信任存储中,或者在回调中严格校验证书指纹、名称、有效期和证书链。回调不能直接返回 true。
建议的排查顺序
- 确认
WEBSITE_LOAD_CERTIFICATES已设置,并在重启后生效。 - 输出证书的
Thumbprint、Subject、Issuer、NotBefore、NotAfter和HasPrivateKey,不要输出私钥或 PFX 密码。 - 在 Azure 实例上执行
X509Chain.Build,检查是否缺少中间 CA。 - 检查证书是否包含
Client AuthenticationEKU,其 OID 为1.3.6.1.5.5.7.3.2。 - 确认证书、API 地址、商户号和
payeeAlias属于同一个 Swish 环境。 - 确认 Swish 是否要求登记 Azure App Service 的出站 IP。App Service 可能有多个出站 IP,应按对方规则全部登记。
- 将协议限定为
SslProtocols.Tls12,避免旧协议参与协商。 - 如果仍收到 fatal Alert 40,请 Swish 根据请求时间、客户端证书序列号或指纹查询服务端 TLS 日志。
原代码还存在一个问题:它在 finally 中释放了 handler,之后又访问 handler.ClientCertificates[0]。这可能引发额外异常。应在释放前保存所需信息,避免将后续错误与 TLS 错误混在一起。
备注:内容仅供参考。
