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 @@ public ChallengeContext( /// 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 @@ internal class KeyChangePayload 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 @@ public JwsPayload Sign( 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));