Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support RFC 5816 and RFC 3161 trusted timestamping #24524

Closed
joshfree opened this issue Oct 9, 2017 · 10 comments

Comments

Projects
None yet
6 participants
@joshfree
Copy link
Member

commented Oct 9, 2017

API proposal:

namespace System.Security.Cryptography.Pkcs
{
    public sealed partial class Rfc3161TimestampRequest
    {
        private Rfc3161TimestampRequest() { }
        public int Version => throw null;
        public ReadOnlyMemory<byte> GetMessageHash() => throw null;
        public Oid HashAlgorithmId => throw null;
        public Oid RequestedPolicyId => throw null;
        public bool RequestSignerCertificate => throw null;
        public ReadOnlyMemory<byte>? GetNonce() => throw null;
        public bool HasExtensions => throw null;
        public X509ExtensionCollection GetExtensions() => throw null;
        public byte[] Encode() => throw null;
        public bool TryEncode(Span<byte> destination, out int bytesWritten) => throw null;
        public Rfc3161TimestampToken ProcessResponse(
                ReadOnlyMemory<byte> responseBytes, out int bytesConsumed) => throw null;
        public static Rfc3161TimestampRequest CreateFromData(
                ReadOnlySpan<byte> data, HashAlgorithmName hashAlgorithm, Oid requestedPolicyId = null, ReadOnlyMemory<byte>? nonce = null, bool requestSignerCertificates = false, X509ExtensionCollection extensions = null) => throw null;
        public static Rfc3161TimestampRequest CreateFromHash(
                ReadOnlyMemory<byte> hash, HashAlgorithmName hashAlgorithm, Oid requestedPolicyId = null, ReadOnlyMemory<byte>? nonce = null, bool requestSignerCertificates = false, X509ExtensionCollection extensions = null) => throw null;
        public static Rfc3161TimestampRequest CreateFromHash(
                ReadOnlyMemory<byte> hash, Oid hashAlgorithmId, Oid requestedPolicyId = null, ReadOnlyMemory<byte>? nonce = null, bool requestSignerCertificates = false, X509ExtensionCollection extensions = null) => throw null;
        public static Rfc3161TimestampRequest CreateFromSignerInfo(
                SignerInfo signerInfo, HashAlgorithmName hashAlgorithm, Oid requestedPolicyId = null, ReadOnlyMemory<byte>? nonce = null, bool requestSignerCertificates = false, X509ExtensionCollection extensions = null) => throw null;
        public static bool TryDecode(
                ReadOnlyMemory<byte> encodedBytes, out Rfc3161TimestampRequest request, out int bytesConsumed) => throw null;
    }
    public sealed partial class Rfc3161TimestampToken
    {
        private Rfc3161TimestampToken() { }
        public Rfc3161TimestampTokenInfo TokenInfo => throw null;
        public SignedCms AsSignedCms() => throw null;
        public bool VerifySignatureForHash(
                ReadOnlySpan<byte> hash, HashAlgorithmName hashAlgorithm, out X509Certificate2 signerCertificate, X509Certificate2Collection extraCandidates = null) => throw null;
        public bool VerifySignatureForHash(
                ReadOnlySpan<byte> hash, Oid hashAlgorithmId, out X509Certificate2 signerCertificate, X509Certificate2Collection extraCandidates = null) => throw null;
        public bool VerifySignatureForData(
                ReadOnlySpan<byte> data, out X509Certificate2 signerCertificate, X509Certificate2Collection extraCandidates = null) => throw null;
        public bool VerifySignatureForSignerInfo(
                SignerInfo signerInfo, out X509Certificate2 signerCertificate, X509Certificate2Collection extraCandidates = null) => throw null;
        public static bool TryDecode(
                ReadOnlyMemory<byte> encodedBytes, out Rfc3161TimestampToken token, out int bytesConsumed) => throw null;
    }
    public sealed partial class Rfc3161TimestampTokenInfo
    {
        public Rfc3161TimestampTokenInfo(
                Oid policyId, Oid hashAlgorithmId, ReadOnlyMemory<byte> messageHash, ReadOnlyMemory<byte> serialNumber, DateTimeOffset timestamp, long? accuracyInMicroseconds=null, bool isOrdering=false, ReadOnlyMemory<byte>? nonce=null, ReadOnlyMemory<byte>? timestampAuthorityName=null, X509ExtensionCollection extensions =null) { throw null; }
        public int Version => throw null;
        public Oid PolicyId=> throw null;
        public Oid HashAlgorithmId => throw null;
        public ReadOnlyMemory<byte> GetMessageHash() { throw null; }
        public ReadOnlyMemory<byte> GetSerialNumber() { throw null; }
        public DateTimeOffset Timestamp => throw null;
        public long? AccuracyInMicroseconds => throw null;
        public bool IsOrdering => throw null;
        public ReadOnlyMemory<byte>? GetNonce() { throw null; }
        public ReadOnlyMemory<byte>? GetTimestampAuthorityName() { throw null; }
        public bool HasExtensions => throw null;
        public X509ExtensionCollection GetExtensions() { throw null; }
        public byte[] Encode() => throw null;
        public bool TryEncode(Span<byte> destination, out int bytesWritten) => throw null;
        public static bool TryDecode(
                ReadOnlyMemory<byte> encodedBytes, out Rfc3161TimestampTokenInfo timestampTokenInfo, out int bytesConsumed) { throw null; }
    }
}

https://tools.ietf.org/html/rfc5816#section-1

https://tools.ietf.org/html/rfc3161

https://en.wikipedia.org/wiki/Trusted_timestamping

@joshfree joshfree added this to the 2.1.0 milestone Oct 9, 2017

@joshfree joshfree changed the title Support RFC 3161 trusted timestamping Support RFC 5816 and RFC 3161 trusted timestamping Oct 13, 2017

@rrelyea

This comment has been minimized.

Copy link

commented Oct 17, 2017

NuGet has a dependency on this work. /cc @dtivel

@bartonjs

This comment has been minimized.

Copy link
Member

commented Oct 17, 2017

Added some initial API thoughts.

  • It uses byte[] instead of (ReadOnly)Span<byte> because of .NET Framework timing considerations.
    • For .NET Core there can/probably-should be Span-based overloads.
  • By extending AsnEncodedData the objects are a bit malleable, so Oid, X509ExtensionCollection and X509Extension (AsnEncodedData) are not unreasonably mutable.
  • The key considerations were
    • What data model comes out of the RFC
    • What does Win32 CryptRetrieveTimeStamp, CryptVerifyTimeStamp expose
    • What policy decisions might someone want to make on the TimestampToken without needing to cross-load it into SignedCms?
@danmosemsft

This comment has been minimized.

Copy link
Member

commented Dec 1, 2017

@bartonjs estimates 3 weeks remaining

@bartonjs

This comment has been minimized.

Copy link
Member

commented Jan 29, 2018

Some example usage:

Check a SignedCms's SignerInfo for date-based compliance

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;
}

Sign a document including a cryptographic timestamp

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);
}

Get a cryptographic timestamp a way that SubmitRequestAsync cannot handle

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
}

An overly simplistic timestamp issuance authority

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;
}
@terrajobst

This comment has been minimized.

Copy link
Member

commented Jan 30, 2018

Rfc3161TimestampRequest

  • We should be consistent with accepting 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.
  • Remove SubmitRequestAsync as it dependends on HttpClient
  • source should be responseBytes
  • bytesRead might be bytesConsumed
  • BuildForXxx should be CreateFromXxx

Rfc3161TimestampToken

  • TryParse the bytesRead should be last

Rfc3161TimestampTokenInfo

  • Rfc3161TimestampTokenInfo.tsaName should be timestampAuthorityName
@glennawatson

This comment has been minimized.

Copy link
Contributor

commented Jun 2, 2019

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.

@bartonjs

This comment has been minimized.

Copy link
Member

commented Jun 2, 2019

@bartonjs

This comment has been minimized.

Copy link
Member

commented Jun 2, 2019

@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 _);
@glennawatson

This comment has been minimized.

Copy link
Contributor

commented Jun 2, 2019

Awesome thanks @bartonjs -- pretty helpful :)

@glennawatson

This comment has been minimized.

Copy link
Contributor

commented Jun 2, 2019

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 _);
        }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.