dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.29k stars 4.74k forks source link

Support RFC 5816 and RFC 3161 trusted timestamping #23783

Closed joshfree closed 4 years ago

joshfree commented 7 years ago

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

rrelyea commented 7 years ago

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

bartonjs commented 7 years ago

Added some initial API thoughts.

danmoseley commented 6 years ago

@bartonjs estimates 3 weeks remaining

bartonjs commented 6 years ago

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 commented 6 years ago

Rfc3161TimestampRequest

Rfc3161TimestampToken

Rfc3161TimestampTokenInfo

glennawatson commented 5 years ago

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 commented 5 years ago

@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.

bartonjs commented 5 years ago

@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 commented 5 years ago

Awesome thanks @bartonjs -- pretty helpful :)

glennawatson commented 5 years ago

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