I'm using @bartonjs's example, An overly simplistic timestamp issuance authority at https://github.com/dotnet/corefx/issues/24524#issuecomment-361337499 to timestamp a request generated by signtool timestamp, and having trouble getting it to work. Here is what I did.
One-time certificate setup
Run this PowerShell script to generate a root CA and a timestamping certificate:
$root = New-SelfSignedCertificate `
-Type Custom `
-HashAlgorithm SHA256 `
-KeyExportPolicy Exportable `
-Subject 'CN=_TestCA' `
-KeySpec Signature `
-KeyUsage 'None' `
-KeyUsageProperty All `
-TextExtension @("2.5.29.19={text}cA=true") `
-CertStoreLocation Cert:\CurrentUser\My `
-NotAfter '02/01/2029 00:00:00'
$thumbprint = $root.Thumbprint
Write-Host "Created RootCA cert: ${thumbprint}"
$tscert = New-SelfSignedCertificate `
-Type Custom `
-HashAlgorithm SHA256 `
-KeyExportPolicy Exportable `
-Subject 'CN=_TestTimestamp' `
-KeyUsage DigitalSignature,NonRepudiation `
-KeyUsageProperty Sign `
-CertStoreLocation Cert:CurrentUser\My `
-TextExtension @("2.5.29.37={critical}{text}1.3.6.1.5.5.7.3.8") `
-Signer "Cert:CurrentUser\My\${thumbprint}" `
-NotAfter '01/01/2029 00:00:00'
$thumbprint = $tscert.Thumbprint
Write-Host "Created timestamping cert: ${thumbprint}"
The output should be similar to:
Created RootCA cert: 3E235901DC62D29F451EB3251B612AADCAEA6005
Created timestamping cert: 3877B460311CC3F281B037198493518AF14C62B5
Note/copy the timestamping certificate thumbprint (i.e. 3877B460311CC3F281B037198493518AF14C62B5).
Open certmgr.msc, Cut the _TestCA certificate from Personal, and Paste it to Trusted Root Certification Authorities.
In a command prompt, run:
> dotnet --version
2.1.401
> mkdir repro && cd repro
> dotnet new console
> dotnet add package System.Security.Cryptography.Pkcs --version 4.5.0
Replace Program.cs with:
using System;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
namespace Repro
{
class Program
{
const string CertificateThumbprint = "3877B460311CC3F281B037198493518AF14C62B5";
static void Main(string[] args)
{
const string requestBase64 = "MDkCAQEwMTANBglghkgBZQMEAgEFAAQgZKFY5VI7xuWHNNeFb17Q60DPLlqoFtctpEHhsq1qgPwBAf8=";
byte[] requestBytes = Convert.FromBase64String(requestBase64);
Debug.Assert(Rfc3161TimestampRequest.TryDecode(requestBytes, out Rfc3161TimestampRequest request, out int bytesConsumed));
Debug.Assert(bytesConsumed == requestBytes.Length);
var tokenInfo = new Rfc3161TimestampTokenInfo(
new Oid("2.255.329800735698586629295641978511506172919.1.4.17"), // UUID policy (no meaning)
request.HashAlgorithmId,
request.GetMessageHash(),
Guid.NewGuid().ToByteArray(),
DateTimeOffset.UtcNow,
accuracyInMicroseconds: 3_000_000,
nonce: request.GetNonce()
);
var contentInfo = new ContentInfo(new Oid("1.2.840.113549.1.9.16.1.4", "id-ct-TSTInfo"), tokenInfo.Encode());
X509Certificate2 certificate;
using (var store = new X509Store(StoreLocation.CurrentUser))
{
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
certificate = store.Certificates
.Find(X509FindType.FindByThumbprint, CertificateThumbprint, true)
.Cast<X509Certificate2>()
.Single();
}
var cms = new SignedCms(contentInfo);
var signer = new CmsSigner(certificate)
{
DigestAlgorithm = request.HashAlgorithmId,
IncludeOption = request.RequestSignerCertificate
? X509IncludeOption.EndCertOnly
: X509IncludeOption.None,
};
// Exercise left to the reader
//signer.SignedAttributes.Add(BuildSigningCertificateV2Attribute(certificate));
cms.ComputeSignature(signer);
byte[] response = cms.Encode();
Rfc3161TimestampToken token;
Debug.Assert(Rfc3161TimestampToken.TryDecode(response, out token, out int _));
}
}
}
Replace the certificate thumbprint with the value obtained in the One-time certificate setup step.
const string CertificateThumbprint = "3877B460311CC3F281B037198493518AF14C62B5";
dotnet run fails with:
Assertion Failed
at Repro.Program.Main(String[] args) in Program.cs:line 60
at:
Debug.Assert(Rfc3161TimestampToken.TryDecode(response, out token, out int _));
Replace line 60 with:
- Debug.Assert(Rfc3161TimestampToken.TryDecode(response, out token, out int _));
+ token = request.ProcessResponse(response, out int _);
dotnet run now fails with:
Unhandled Exception: System.Security.Cryptography.CryptographicException: Unable to set field Status on type System.Security.Cryptography.Pkcs.Asn1.Rfc3161TimeStampResp. ---> System.Security.Cryptography.CryptographicException: ASN1 corrupted data.
at System.Security.Cryptography.Asn1.AsnReader.CheckExpectedTag(Asn1Tag tag, Asn1Tag expectedTag, UniversalTagNumber tagNumber)
at System.Security.Cryptography.Asn1.AsnReader.ReadSequence(Asn1Tag expectedTag)
at System.Security.Cryptography.Asn1.AsnSerializer.DeserializeCustomType(AsnReader reader, Type typeT, Asn1Tag expectedTag)
at System.Security.Cryptography.Asn1.AsnSerializer.DeserializeCustomType(AsnReader reader, Type typeT, Asn1Tag expectedTag)
--- End of inner exception stack trace ---
at System.Security.Cryptography.Asn1.AsnSerializer.DeserializeCustomType(AsnReader reader, Type typeT, Asn1Tag expectedTag)
at System.Security.Cryptography.Asn1.AsnSerializer.Deserialize[T](ReadOnlyMemory`1 source, AsnEncodingRules ruleSet, Int32& bytesRead)
at System.Security.Cryptography.Pkcs.Rfc3161TimestampRequest.ProcessResponse(ReadOnlyMemory`1 source, Rfc3161TimestampToken& token, Rfc3161RequestResponseStatus& status, Int32& bytesConsumed, Boolean shouldThrow)
at System.Security.Cryptography.Pkcs.Rfc3161TimestampRequest.ProcessResponse(ReadOnlyMemory`1 source, Int32& bytesConsumed)
at Repro.Program.Main(String[] args) in Program.cs:line 61
I'm very likely missing out some subtle detail, I just can't figure out what it is. Or perhaps it's a actual bug. I would appreciate feedback in either case 馃槂
Building corefx locally and debugging through the Rfc3161TimestampToken.TryDecode code, it turns out the decode fails at:
because the expected signed attributes are missing: SigningCertificate and/or SigningCertificateV2.
So it looks like what I am missing is how to fill-in this blank:
// Exercise left to the reader
// signer.SignedAttributes.Add(BuildSigningCertificateV2Attribute(certificate));
And here I seem to run into a wall: the seemingly-relevant structs/classes to build these attributes are all internal, including CryptographicAttributeObject, SigningCertificateAsn, EssCertId, AsnSerializer, AsnWriter, etc.
The only example I could find builds them from hard-coded byte arrays, in the test code, here:
Is there a higher-level API available (or planned to be exposed) that allows building the SigningCertificate and SigningCertificateV2 attributes? /cc @bartonjs @filipnavara
Is there a higher-level API available (or planned to be exposed) that allows building the SigningCertificate and SigningCertificateV2 attributes?
Not presently, no. The API was designed to read/verify/process timestamps, building timestamps is mainly expected to be done by trusted timestamping authorities. (Though test scenarios certainly do exist.)
The linked test code there builds a valid SigningCertificate (v1) attribute. Slightly lower is code to build a V2 attribute, though it's certainly a lot trickier.
Thank you @bartonjs, that's understandable given that 99% of scenarios would be covered by reading them.
I'll see what I can piece together from this point on. It would be really helpful if at least AsnWriter would become a publicly exposed API.
Using AsnSerializer is a joy, but probably that would require exposing too many internals to be realistic.
Making AsnReader/AsnWriter public is on my TODO list, though not with a specific timeline. We've probably stopped changing it enough that it's almost time to go forward.
AsnSerializer has a lot of value, but a lot of cost, too. We've just removed the last usage of it in .NET Core, using generated code instead (solves some perf problems, some reflection/runtime problems, and some debuggability problems). It probably won't survive to be public.
@bartonjs sounds good, looking forward to that (you're likely referring to https://github.com/dotnet/corefx/issues/21833).
Most helpful comment
Making AsnReader/AsnWriter public is on my TODO list, though not with a specific timeline. We've probably stopped changing it enough that it's almost time to go forward.
AsnSerializer has a lot of value, but a lot of cost, too. We've just removed the last usage of it in .NET Core, using generated code instead (solves some perf problems, some reflection/runtime problems, and some debuggability problems). It probably won't survive to be public.