From 01d17e92b4725e4067812318185965fd2e8f06af Mon Sep 17 00:00:00 2001 From: token <61819790+239573049@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:05:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(acme):=20=E4=BF=AE=E5=A4=8D=E6=8C=91?= =?UTF-8?q?=E6=88=98=E9=AA=8C=E8=AF=81=E9=98=B6=E6=AE=B5=E5=8C=BF=E5=90=8D?= =?UTF-8?q?=20{}=20=E8=BD=BD=E8=8D=B7=E5=9C=A8=20AOT=20=E4=B8=8B=E6=97=A0?= =?UTF-8?q?=20JsonTypeInfo=20=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/Certes/Acme/ChallengeContext.cs | 6 +++++- src/Certes/Json/CertesJsonContext.cs | 1 + src/Certes/Jws/JwsHeader.cs | 10 ++++++++++ src/Certes/Jws/JwsSigner.cs | 25 ++++++++++++++++++++++--- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/Certes/Acme/ChallengeContext.cs b/src/Certes/Acme/ChallengeContext.cs index 026c296..3ad8780 100644 --- a/src/Certes/Acme/ChallengeContext.cs +++ b/src/Certes/Acme/ChallengeContext.cs @@ -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 /// public async Task Validate() { - var resp = await Context.HttpClient.Post(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(Context, Location, new EmptyObject(), true); return resp.Resource; } } diff --git a/src/Certes/Json/CertesJsonContext.cs b/src/Certes/Json/CertesJsonContext.cs index 215d172..cb19965 100644 --- a/src/Certes/Json/CertesJsonContext.cs +++ b/src/Certes/Json/CertesJsonContext.cs @@ -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 diff --git a/src/Certes/Jws/JwsHeader.cs b/src/Certes/Jws/JwsHeader.cs index ee8de03..2bcebdf 100644 --- a/src/Certes/Jws/JwsHeader.cs +++ b/src/Certes/Jws/JwsHeader.cs @@ -45,6 +45,16 @@ namespace Certes.Jws public object OldKey { get; set; } } + /// + /// An empty JWS payload that serializes to {}. 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 new {} has no JsonTypeInfo and throws under Native AOT. + /// + internal class EmptyObject + { + } + /// /// External account binding JWS, embedded into the new-account request. /// diff --git a/src/Certes/Jws/JwsSigner.cs b/src/Certes/Jws/JwsSigner.cs index 9e0b743..1c7e6bb 100644 --- a/src/Certes/Jws/JwsSigner.cs +++ b/src/Certes/Jws/JwsSigner.cs @@ -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));