fix(acme): 修复挑战验证阶段匿名 {} 载荷在 AOT 下无 JsonTypeInfo 崩溃

ChallengeContext.Validate 使用 new {} 作为空对象载荷,Native AOT 下
CertesJsonContext.GetTypeInfo(匿名类型) 返回 null,JwsSigner.Sign 调用
JsonSerializer.Serialize(payload, null) 抛出 ArgumentNullException,
导致证书申请在挑战验证阶段失败。

- 新增已注册的 EmptyObject 载荷类型,序列化为 {}(RFC 8555 §7.5.1)
- JwsSigner.Sign 对无法解析元数据的载荷抛出可诊断的 InvalidOperationException,
  而非深层 STJ 的裸 ArgumentNullException
- 保留 null 载荷 -> 空串(POST-as-GET)语义

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
token
2026-07-22 18:05:50 +08:00
parent 23e2a25516
commit 01d17e92b4
4 changed files with 38 additions and 4 deletions
+5 -1
View File
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Certes.Acme.Resource;
using Certes.Jws;
namespace Certes.Acme
{
@@ -60,7 +61,10 @@ namespace Certes.Acme
/// </returns>
public async Task<Challenge> Validate()
{
var resp = await Context.HttpClient.Post<Challenge>(Context, Location, new {}, true);
// RFC 8555 §7.5.1: the challenge-trigger POST carries an empty JSON object.
// Must be a concrete, source-gen-registered type (EmptyObject) — an anonymous
// `new {}` has no JsonTypeInfo under Native AOT and throws in JwsSigner.Sign.
var resp = await Context.HttpClient.Post<Challenge>(Context, Location, new EmptyObject(), true);
return resp.Resource;
}
}
+1
View File
@@ -20,6 +20,7 @@ namespace Certes.Json
[JsonSerializable(typeof(EcJsonWebKey))]
[JsonSerializable(typeof(RsaJsonWebKey))]
[JsonSerializable(typeof(KeyChangePayload))]
[JsonSerializable(typeof(EmptyObject))]
[JsonSerializable(typeof(ExternalAccountBinding))]
[JsonSerializable(typeof(ExternalAccountBindingHeader))]
// ACME resources
+10
View File
@@ -45,6 +45,16 @@ namespace Certes.Jws
public object OldKey { get; set; }
}
/// <summary>
/// An empty JWS payload that serializes to <c>{}</c>. Used for requests whose body
/// is an empty JSON object (e.g. the RFC 8555 §7.5.1 challenge-validation trigger).
/// A concrete, source-gen-registered type is required so serialization is AOT-safe;
/// an anonymous <c>new {}</c> has no JsonTypeInfo and throws under Native AOT.
/// </summary>
internal class EmptyObject
{
}
/// <summary>
/// External account binding JWS, embedded into the new-account request.
/// </summary>
+22 -3
View File
@@ -62,9 +62,28 @@ namespace Certes.Jws
Url = url,
};
var entityJson = payload == null
? ""
: JsonSerializer.Serialize(payload, CertesJsonContext.Default.GetTypeInfo(payload.GetType()));
string entityJson;
if (payload == null)
{
// POST-as-GET: RFC 8555 §6.3 requires an empty (not "{}") payload.
entityJson = "";
}
else
{
var payloadType = payload.GetType();
var typeInfo = CertesJsonContext.Default.GetTypeInfo(payloadType);
if (typeInfo == null)
{
// Anonymous/unregistered types have no source-generated metadata and
// would otherwise surface as an opaque ArgumentNullException from
// System.Text.Json under Native AOT. Point at the real cause instead.
throw new InvalidOperationException(
$"No JSON metadata for payload type '{payloadType}'. Add it to " +
$"{nameof(CertesJsonContext)} (anonymous types are not supported under AOT).");
}
entityJson = JsonSerializer.Serialize(payload, typeInfo);
}
var protectedHeaderJson = JsonSerializer.Serialize(protectedHeader, CertesJsonContext.Default.JwsHeader);
var payloadEncoded = JwsConvert.ToBase64String(Encoding.UTF8.GetBytes(entityJson));