Closed joshfree closed 4 years ago
NuGet has a dependency on this work. /cc @dtivel
Added some initial API thoughts.
byte[]
instead of (ReadOnly)Span<byte>
because of .NET Framework timing considerations.
AsnEncodedData
the objects are a bit malleable, so Oid
, X509ExtensionCollection
and X509Extension
(AsnEncodedData
) are not unreasonably mutable.SignedCms
?@bartonjs estimates 3 weeks remaining
Some example usage:
private static bool? CheckSignerInfo(SignerInfo signerInfo, DateTimeOffset? notBefore, DateTimeOffset? notAfter)
{
const string TimeStampTokenOid = "1.2.840.113549.1.9.16.2.14";
bool found = false;
byte[] signatureBytes = null;
foreach (CryptographicAttributeObject attr in signerInfo.UnsignedAttributes)
{
if (attr.Oid.Value == TimeStampTokenOid)
{
foreach (AsnEncodedData attrInst in attr.Values)
{
byte[] attrData = attrInst.RawData;
Rfc3161TimestampToken token;
// New API starts here:
if (!Rfc3161TimestampToken.TryParse(attrData, out int bytesRead, out token))
{
return false;
}
if (bytesRead != attrData.Length)
{
return false;
}
signatureBytes = signatureBytes ?? signerInfo.GetSignature();
// Check that the token was issued based on the SignerInfo's signature value
if (!token.VerifyData(signatureBytes))
{
return false;
}
DateTimeOffset timestamp = token.TokenInfo.Timestamp;
// Check that the signed timestamp is within the provided policy range
// (which may be (signerInfo.Certificate.NotBefore, signerInfo.Certificate.NotAfter);
// or some other policy decision)
if (timestamp < notBefore.GetValueOrDefault(timestamp) ||
timestamp > notAfter.GetValueOrDefault(timestamp))
{
return false;
}
X509Certificate2 tokenSignerCert = token.AsSignedCms().SignerInfos[0].Certificate;
// Implicit policy decision: Tokens required embedded certificates (since this method has
// no resolver)
if (tokenSignerCert == null)
{
return false;
}
// Check that the claimed certificate validly signed the token and that it conforms to
// the baseline policy from the RFC.
if (!token.CheckCertificate(tokenSignerCert))
{
return false;
}
// New API ends here.
found = true;
}
}
}
// If we found any attributes and none of them returned an early false, then the SignerInfo is
// conformant to policy.
if (found)
{
return true;
}
// Inconclusive, as no signed timestamps were found
return null;
}
private static async Task SignDocumentWithSignedTimestamp(
SignedCms toSign,
CmsSigner newSigner,
Uri timeStampAuthorityUri,
TimeSpan timeout)
{
// This example figures out which signer is new by it being "the only signer"
if (toSign.SignerInfos.Count > 0)
throw new ArgumentException();
toSign.ComputeSignature(newSigner);
SignerInfo newSignerInfo = toSign.SignerInfos[0];
byte[] nonce = new byte[8];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(nonce);
}
Rfc3161TimestampRequest request = Rfc3161TimestampRequest.BuildForSignerInfo(
newSignerInfo,
HashAlgorithmName.SHA384,
requestSignerCertificates: true,
nonce: nonce);
Rfc3161TimestampToken token =
await request.SubmitRequestAsync(timeStampAuthorityUri, timeout);
AsnEncodedData tokenAttribute = new AsnEncodedData(
"1.2.840.113549.1.9.16.2.14",
token.AsSignedCms().Encode());
// Exercise left to the reader
AddUnsignedAttributeToExistingSignerInfo(newSignerInfo, tokenAttribute);
}
private static byte[] InitiateTimestampRequest(SignerInfo signerInfo, byte[] nonce)
{
Rfc3161TimestampRequest request = Rfc3161TimestampRequest.BuildForSignerInfo(
signerInfo,
HashAlgorithmName.SHA512,
requestSignerCertificates: true,
nonce: nonce);
return request.Encode();
}
private static Rfc3161TimestampToken AcceptTimestampResponse(SignerInfo signerInfo, byte[] nonce, byte[] responseBytes)
{
Rfc3161TimestampRequest request = Rfc3161TimestampRequest.BuildForSignerInfo(
signerInfo,
HashAlgorithmName.SHA512,
requestSignerCertificates: true,
nonce: nonce);
#if STRONG_MATCH_ONLY
return request.AcceptResponse(responseBytes, out _);
#else
Rfc3161RequestResponseStatus status;
Rfc3161TimestampToken token;
if (request.TryAcceptResponse(responseBytes, out _, out status, out token))
{
// The token was accepted with no issues.
return token;
}
switch (status)
{
// EXAMPLE ONLY: This application has decided that if the TSA doesn't
// send the certificate that it's okay
case Rfc3161RequestResponseStatus.RequestedCertificatesMissing:
return token;
// Fail all other reasons
default:
throw new Exception($"The token was not accepted due to status {status}");
}
#endif
}
private static Rfc3161TimestampToken OverlySimplisticTSA(byte[] requestBytes)
{
Rfc3161TimestampRequest request;
if (!Rfc3161TimestampRequest.TryParse(requestBytes, out int bytesRead, out request) ||
bytesRead != requestBytes.Length)
{
return null;
}
// A UUID-based OID, doesn't really mean anything.
const string MyPolicyOid = "2.255.329800735698586629295641978511506172919.1.4.17";
if (request.RequestedPolicyId != null && request.RequestedPolicyId.Value != MyPolicyOid)
{
throw new Exception(nameof(Rfc3161TimestampRequest.RequestedPolicyId));
}
switch (request.HashAlgorithmId.Value)
{
// SHA-2-256
case "2.16.840.1.101.3.4.2.1":
// SHA-2-384
case "2.16.840.1.101.3.4.2.2":
break;
default:
throw new Exception(nameof(Rfc3161TimestampRequest.HashAlgorithmId));
}
long serial = Interlocked.Increment(ref s_serialNumber);
byte[] serialBytes = BitConverter.GetBytes(serial);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(serialBytes);
}
Rfc3161TimestampTokenInfo tokenInfo = new Rfc3161TimestampTokenInfo(
new Oid(MyPolicyOid, MyPolicyOid),
request.HashAlgorithmId,
request.GetMessageHash(),
serialBytes,
DateTimeOffset.UtcNow,
// Probably right, +/- 3 seconds.
accuracyInMicroseconds: 3 * 1_000_000,
nonce: request.GetNonce());
ContentInfo contentInfo = new ContentInfo(
new Oid("1.2.840.113549.1.9.16.1.4", "id-ct-TSTInfo"),
tokenInfo.RawData);
SignedCms cms = new SignedCms(contentInfo);
CmsSigner signer = new CmsSigner(s_tsaCertificate)
{
IncludeOption = request.RequestSignerCertificate
? X509IncludeOption.EndCertOnly
: X509IncludeOption.None,
};
// Exercise left to the reader
signer.SignedAttributes.Add(BuildSigningCertificateV2Attribute(s_tsaCertificate));
cms.ComputeSignature(signer);
Rfc3161TimestampToken token;
if (!Rfc3161TimestampToken.TryParse(cms.Encode(), out bytesRead, out token))
{
throw new InvalidOperationException();
}
return token;
}
Span<T>
/ReadOnlySpan<T>
and T[]
. However, in this case we there are instances where we know we'll allocated new memory (such as Encode()
) and returning a more specific type is useful for the caller.SubmitRequestAsync
as it dependends on HttpClientsource
should be responseBytes
bytesRead
might be bytesConsumed
BuildForXxx
should be CreateFromXxx
TryParse
the bytesRead
should be lastRfc3161TimestampTokenInfo
.tsaName
should be timestampAuthorityName
Is this a reasonable interpretation of what SubmitRequestAsync()
would of done?
var client = GetHttpClient();
var httpResponse = await client.PostAsync(timeStampAuthorityUri, new ByteArrayContent(request.Encode())).ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
var data = await httpResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
if (!Rfc3161TimestampToken.TryDecode(data, out var token, out int bytesConsumed))
{
throw new InvalidOperationException("Could not get a valid response from the time authority server.");
}
return token;
Old issue I know so hopefully you guys don't mind.
@glennawatson Not quite. The response isn't a token, it's its own thing. https://github.com/bartonjs/corefx/blob/918f38842698d44e5ddd98ba73fdfb73d5f9df2d/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Rfc3161TimestampRequest.cs#L61-L203 is the most recent version I could (quickly) find.
@glennawatson The modern version, though, is something like
HttpClient client = GetHttpClient();
HttpResponse httpResponse = await client.PostAsync(timeStampAuthorityUri, new ByteArrayContent(request.Encode())).ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
byte[] data = await httpResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
// returns a token, or throws
return Rfc3161TimestampTokenRequest.ProcessResponse(data, out _);
Awesome thanks @bartonjs -- pretty helpful :)
My final version was
private static async Task<Rfc3161TimestampToken> GetTimestamp(SignedCms toSign, CmsSigner newSigner, Uri timeStampAuthorityUri)
{
if (timeStampAuthorityUri == null)
{
throw new ArgumentNullException(nameof(timeStampAuthorityUri));
}
// This example figures out which signer is new by it being "the only signer"
if (toSign.SignerInfos.Count > 0)
{
throw new ArgumentException("We must have only one signer", nameof(toSign));
}
toSign.ComputeSignature(newSigner);
SignerInfo newSignerInfo = toSign.SignerInfos[0];
byte[] nonce = new byte[8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(nonce);
}
var request = Rfc3161TimestampRequest.CreateFromSignerInfo(
newSignerInfo,
HashAlgorithmName.SHA384,
requestSignerCertificates: true,
nonce: nonce);
var client = new HttpClient();
var content = new ReadOnlyMemoryContent(request.Encode());
content.Headers.ContentType = new MediaTypeHeaderValue("application/timestamp-query");
var httpResponse = await client.PostAsync(timeStampAuthorityUri, content).ConfigureAwait(false);
if (!httpResponse.IsSuccessStatusCode)
{
throw new CryptographicException(
$"There was a error from the timestamp authority. It responded with {httpResponse.StatusCode} {(int)httpResponse.StatusCode}: {httpResponse.Content}");
}
if (httpResponse.Content.Headers.ContentType.MediaType != "application/timestamp-reply")
{
throw new CryptographicException("The reply from the time stamp server was in a invalid format.");
}
var data = await httpResponse.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
return request.ProcessResponse(data, out var _);
}
API proposal:
https://tools.ietf.org/html/rfc5816#section-1
https://tools.ietf.org/html/rfc3161
https://en.wikipedia.org/wiki/Trusted_timestamping