In .NETFramework 4.8, the System.Security.Cryptography.Pkcs.SignerInfo.UnsignedAttributes property only returns copies of the data, which do not propagate changes back to the root SignedCms object. This limitation prevents the modification of unsigned attributes after the signature has been computed.
In contrast, .NET 8 implements the System.Security.Cryptography.Pkcs.SignerInfo method AddUnsignedAttribute(AsnEncodedData unsignedAttribute), which allows for the addition of data to UnsignedAttributes post-signing.
The ability to manipulate UnsignedAttributes post-signing is crucial for adding the timestamp response from the Time Stamping Authority (TSA) after the signature has been created.
As far as i know, there is a valid workaround using a NuGet Package which uses P/Invoke to manipulate UnsignedAttributes (told in this Issue https://github.com/dotnet/runtime/issues/24222) but i just can't find it or make it work for me.
This is the smallest repro i could create. Its trying to add Data to UnsignedAttributes using the OID for signingTime and NULL as a value to a blankPdf as Content.
To make it run, "THUMBPRINT" needs to be replaced by the actual thumbprint of a Certificate with no extra Security Measures and a Private Key included from the MS Certificate Store.
static void Main(string[] args)
{
const string blankPdfBase64 = "JVBERi0xLjYNJeLjz9MNCjI0IDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA0L0xlbmd0aCAyMTYvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePI9RS8MwFIX/yn1bi9jepCQ6GYNpFBTEMsW97CVLbjWYNpImmz/fVsXXcw/f/c4SEFarepPTe4iFok8dU09DgtDBQx6TMwT74vaLTE7uSPDUdXM0Xe/73r1FnVwYYEtHR6d9WdY3kX4ipRMV6oojSmxQMoGyac5RLBAXf63p38aGA7XPorLewyvFcYaJile8rB+D/YcwiRdMMGScszO8/IW0MdhsaKKYGA46gXKTr/cUQVY4We/cYMNpnLVeXPJUXHs9fECr7kAFk+eZ5Xr9LcAAfKpQrA0KZW5kc3RyZWFtDWVuZG9iag0yNSAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgNC9MZW5ndGggNDkvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjeslAwULCx0XfOL80rUTDU985MKY42NAIKBsXqh1QWpOoHJKanFtvZAQQYAN/6C60NCmVuZHN0cmVhbQ1lbmRvYmoNMjYgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDkvTGVuZ3RoIDQyL04gMi9UeXBlL09ialN0bT4+c3RyZWFtDQpo3jJTMFAwVzC0ULCx0fcrzS2OBnENFIJi7eyAIsH6LnZ2AAEGAI2FCDcNCmVuZHN0cmVhbQ1lbmRvYmoNMjcgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDUvTGVuZ3RoIDEyMC9OIDEvVHlwZS9PYmpTdG0+PnN0cmVhbQ0KaN4yNFIwULCx0XfOzytJzSspVjAyBgoE6TsX5Rc45VdEGwB5ZoZGCuaWRrH6vqkpmYkYogGJRUCdChZgfUGpxfmlRcmpxUAzA4ryk4NTS6L1A1zc9ENSK0pi7ez0g/JLEktSFQz0QyoLUoF601Pt7AACDADYoCeWDQplbmRzdHJlYW0NZW5kb2JqDTIgMCBvYmoNPDwvTGVuZ3RoIDM1MjUvU3VidHlwZS9YTUwvVHlwZS9NZXRhZGF0YT4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjQtYzAwNSA3OC4xNDczMjYsIDIwMTIvMDgvMjMtMTM6MDM6MDMgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnBkZj0iaHR0cDovL25zLmFkb2JlLmNvbS9wZGYvMS4zLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KICAgICAgICAgPHBkZjpQcm9kdWNlcj5BY3JvYmF0IERpc3RpbGxlciA2LjAgKFdpbmRvd3MpPC9wZGY6UHJvZHVjZXI+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDA2LTAzLTA2VDE1OjA2OjMzLTA1OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZVBTNS5kbGwgVmVyc2lvbiA1LjIuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wNy0xNVQxMDoxMjoyMSswODowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDctMTVUMTA6MTI6MjErMDg6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnV1aWQ6ZmYzZGNmZDEtMjNmYS00NzZmLTgzOWEtM2U1Y2FlMmRhMmViPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD51dWlkOjM1OTM1MGIzLWFmNDAtNGQ4YS05ZDZjLTAzMTg2YjRmZmIzNjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5CbGFuayBQREYgRG9jdW1lbnQ8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnRpdGxlPgogICAgICAgICA8ZGM6Y3JlYXRvcj4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGk+RGVwYXJ0bWVudCBvZiBKdXN0aWNlIChFeGVjdXRpdmUgT2ZmaWNlIG9mIEltbWlncmF0aW9uIFJldmlldyk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L2RjOmNyZWF0b3I+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ1lbmRvYmoNMTEgMCBvYmoNPDwvTWV0YWRhdGEgMiAwIFIvUGFnZUxhYmVscyA2IDAgUi9QYWdlcyA4IDAgUi9UeXBlL0NhdGFsb2c+Pg1lbmRvYmoNMjMgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAxMD4+c3RyZWFtDQpIiQIIMAAAAAABDQplbmRzdHJlYW0NZW5kb2JqDTI4IDAgb2JqDTw8L0RlY29kZVBhcm1zPDwvQ29sdW1ucyA0L1ByZWRpY3RvciAxMj4+L0ZpbHRlci9GbGF0ZURlY29kZS9JRFs8REI3Nzc1Q0NFMjI3RjZCMzBDNDQwREY0MjIxREMzOTA+PEJGQ0NDRjNGNTdGNjEzNEFCRDNDMDRBOUU0Q0ExMDZFPl0vSW5mbyA5IDAgUi9MZW5ndGggODAvUm9vdCAxMSAwIFIvU2l6ZSAyOS9UeXBlL1hSZWYvV1sxIDIgMV0+PnN0cmVhbQ0KaN5iYgACJjDByGzIwPT/73koF0wwMUiBWYxA4v9/EMHA9I/hBVCxoDOQeH8DxH2KrIMIglFwIpD1vh5IMJqBxPpArHYgwd/KABBgAP8bEC0NCmVuZHN0cmVhbQ1lbmRvYmoNc3RhcnR4cmVmDQo0NTc2DQolJUVPRg0K";
var contentInfo = new ContentInfo(Convert.FromBase64String(blankPdfBase64));
var signedCms = new SignedCms(contentInfo, true);
var cmsSigner = new CmsSigner
{
IncludeOption = X509IncludeOption.WholeChain,
DigestAlgorithm = new Oid("2.16.840.1.101.3.4.2.1") // SHA 256
};
// Get X509Certificate from CertificateStore
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindByThumbprint, "THUMBPRINT", false);
cmsSigner.Certificate = certificates[0];
signedCms.ComputeSignature(cmsSigner, false);
Debug.WriteLine($"{signedCms.SignerInfos[0].UnsignedAttributes.Count} Count before");
#if NET6_0_OR_GREATER
// Manipulating Root SignedCms Object
signedCms.SignerInfos[0].AddUnsignedAttribute(new AsnEncodedData(new Oid("1.2.840.113549.1.9.16.2.14"), [0x05, 0x00])); // ASN.1 Decodable for NULL
#elif NET48
// Not Manipulating Root SignedCms Object
signedCms.SignerInfos[0].UnsignedAttributes.Add(new Pkcs9AttributeObject(new AsnEncodedData(new Oid("1.2.840.113549.1.9.16.2.14"), [])));
#endif
Debug.WriteLine($"{signedCms.SignerInfos[0].UnsignedAttributes.Count} Count after");
}
I've tried several invocation methods but couldn't get them to work, likely due to a lack of knowledge in those specific methods. I also attempted a workaround using BouncyCastle to decode my computed signature into a new CmsSignedData() to manipulate the UnsignedAttributes using a different Signer. This successfully added the attribute (as verified by decoding the bytes with ASN.1), but it invalidated the signature. Using reflection to override the private properties also didn't work because some properties are missing setters, which I would also need to replace. The timestamp response is always valid, so that shouldn't be the issue.
(BouncyCastle attempt)
var signedData = new CmsSignedData(signedCms.Encode());
var signer = signedData.GetSignerInfos().GetSigners().First();
Asn1EncodableVector asnEncodableTimeStampToken = [new DerOctetString(timeStampToken)];
var timeStampAttribute = new Attribute(new DerObjectIdentifier("1.2.840.113549.1.9.16.2.14"), new DerSet(asnEncodableTimeStampToken));
var unsignedAttributes = signer.UnsignedAttributes ?? new AttributeTable(new Dictionary<DerObjectIdentifier, object>());
var newAttributes = new Dictionary<DerObjectIdentifier, object>(unsignedAttributes.ToDictionary());
newAttributes.Add(timeStampAttribute.AttrType, timeStampAttribute);
var newUnsignedAttr = new AttributeTable(newAttributes);
var newSigner = SignerInformation.ReplaceUnsignedAttributes(signer, newUnsignedAttr);
var newSignerStore = new SignerInformationStore(new List<SignerInformation> { newSigner });
var newSignedData = CmsSignedData.ReplaceSigners(signedData, newSignerStore);
return newSignedData.GetEncoded();
As far as i know, there is a valid workaround [using P/Invokes] to manipulate UnsignedAttributes
You can if you want. It's not super friendly. I tried going that route to show it as a counter-example, and got too frustrated, and went with the pure managed approach. The gist is that you want to use CryptMsgOpenToDecode (then CryptMsgUpdate to give it the contents), then CryptMsgControl with CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR on the properly encoded attribute (which itself involves more P/Invokes) then some other stuff to export the byte[]
again. I think it's clear where I stopped.
SignedCms is an ASN.1/BER data structure, so we can manipulate it using the AsnReader/AsnWriter classes from the System.Formats.Asn1 NuGet package (or, if you're on .NET 5+, they're just built-in).
Pre-req: You need to make sure you're using the TimeStampToken value, not the TimeStampResponse value. So, if you've just issued a TimeStampRequest to a Time Stamping Authority and now have your response, here's how to extract the token. (It's also "nice" in that if the input isn't a response it assumes it's a token and returns it)
private static ReadOnlyMemory<byte> ExtractTimeStampToken(byte[] input, out bool wasTsr)
{
AsnReader reader = new AsnReader(input, AsnEncodingRules.BER);
AsnReader outer = reader.ReadSequence();
reader.ThrowIfNotEmpty();
// TimeStampResponse is SEQUENCE { SEQUENCE { INTEGER, ... }, MaybeTST }
// TimeStampToken is SEQUENCE { OBJECT IDENTIFIER, [0] }
if (outer.PeekTag().HasSameClassAndValue(Asn1Tag.Sequence))
{
AsnReader pkiStatusInfo = outer.ReadSequence();
if (!pkiStatusInfo.TryReadInt32(out int pkiStatus) || pkiStatus is not (0 or 1))
{
throw new Exception("TimeStampResponse indicates non-success");
}
ReadOnlyMemory<byte> tst = outer.ReadEncodedValue();
outer.ThrowIfNotEmpty();
wasTsr = true;
return tst;
}
wasTsr = false;
return new ReadOnlyMemory<byte>(input);
}
OK, so you've extracted your TST, and now you want to add it as an attribute. On .NET 5+, that's easy:
cms.SignerInfos[0].AddUnsignedAttribute(
new Pkcs9AttributeObject(
"1.2.840.113549.1.9.16.2.14",
tst.ToArray()));
cms.Encode();
But you want to know for .NET Framework:
private static byte[] InsertTstWithAsnWriter(byte[] cmsBytes, ReadOnlyMemory<byte> tstBytes)
{
AsnWriter writer = new AsnWriter(AsnEncodingRules.BER, cmsBytes.Length + tstBytes.Length + 50);
AsnReader reader = new AsnReader(cmsBytes, AsnEncodingRules.BER);
// The top structure is
//
// ContentInfo ::= SEQUENCE {
// contentType ContentType,
// content [0] EXPLICIT ANY DEFINED BY contentType }
//
// ContentType ::= OBJECT IDENTIFIER
//
// For a SignedData, contentType is 1.2.840.113549.1.7.2,
// and the value in content is a SignedData value.
//
// SignedData ::= SEQUENCE {
// version CMSVersion,
// digestAlgorithms DigestAlgorithmIdentifiers,
// encapContentInfo EncapsulatedContentInfo,
// certificates [0] IMPLICIT CertificateSet OPTIONAL,
// crls [1] IMPLICIT RevocationInfoChoices OPTIONAL,
// signerInfos SignerInfos }
AsnReader topContent = reader.ReadSequence();
reader.ThrowIfNotEmpty();
using (writer.PushSequence())
{
writer.WriteEncodedValue(topContent.PeekEncodedValue().Span);
if (topContent.ReadObjectIdentifier() != "1.2.840.113549.1.7.2")
{
throw new Exception("cmsBytes is not a SignedData value");
}
Asn1Tag context0 = new Asn1Tag(TagClass.ContextSpecific, 0);
Asn1Tag context1 = new Asn1Tag(TagClass.ContextSpecific, 1);
AsnReader explicit0 = topContent.ReadSequence(context0);
AsnReader content = explicit0.ReadSequence();
explicit0.ThrowIfNotEmpty();
using (writer.PushSequence(context0))
using (writer.PushSequence())
{
// The first 3 fields are required
writer.WriteEncodedValue(content.ReadEncodedValue().Span);
writer.WriteEncodedValue(content.ReadEncodedValue().Span);
writer.WriteEncodedValue(content.ReadEncodedValue().Span);
while (content.PeekTag() != Asn1Tag.SetOf)
{
writer.WriteEncodedValue(content.ReadEncodedValue().Span);
}
AsnReader signerInfos = content.ReadSetOf();
content.ThrowIfNotEmpty();
using (writer.PushSetOf())
{
// SignerInfo ::= SEQUENCE {
// version CMSVersion,
// sid SignerIdentifier,
// digestAlgorithm DigestAlgorithmIdentifier,
// signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL,
// signatureAlgorithm SignatureAlgorithmIdentifier,
// signature SignatureValue,
// unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL }
AsnReader firstSignerInfo = signerInfos.ReadSequence();
using (writer.PushSequence())
{
writer.WriteEncodedValue(firstSignerInfo.ReadEncodedValue().Span);
writer.WriteEncodedValue(firstSignerInfo.ReadEncodedValue().Span);
writer.WriteEncodedValue(firstSignerInfo.ReadEncodedValue().Span);
if (firstSignerInfo.PeekTag().TagClass == TagClass.ContextSpecific)
{
writer.WriteEncodedValue(firstSignerInfo.ReadEncodedValue().Span);
}
writer.WriteEncodedValue(firstSignerInfo.ReadEncodedValue().Span);
writer.WriteEncodedValue(firstSignerInfo.ReadEncodedValue().Span);
using (writer.PushSetOf(context1))
{
if (firstSignerInfo.HasData)
{
AsnReader currentAttrs =
firstSignerInfo.ReadSetOf(context1);
while (currentAttrs.HasData)
{
writer.WriteEncodedValue(currentAttrs.ReadEncodedValue().Span);
}
}
// Attribute ::= SEQUENCE {
// attrType OBJECT IDENTIFIER,
// attrValues SET OF AttributeValue }
//
// AttributeValue ::= ANY
using (writer.PushSequence())
{
writer.WriteObjectIdentifier("1.2.840.113549.1.9.16.2.14");
using (writer.PushSetOf())
{
writer.WriteEncodedValue(tstBytes.Span);
}
}
}
firstSignerInfo.ThrowIfNotEmpty();
while (signerInfos.HasData)
{
writer.WriteEncodedValue(signerInfos.ReadEncodedValue().Span);
}
}
}
}
}
return writer.Encode();
}
Assuming that your TST has the TSA certificate embedded in it, you can then verify that it worked with .NET 5+:
SignedCms cms = new SignedCms();
cms.Decode(blob);
bool decoded = Rfc3161TimestampToken.TryDecode(
cms.SignerInfos[0].UnsignedAttributes[0].Values[0].RawData,
out Rfc3161TimestampToken token,
out _);
bool valid = decoded && token.VerifySignatureForSignerInfo(cms.SignerInfos[0], out _);