Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 91 additions & 26 deletions Lib/Collectors/WindowsFileSystemUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Serilog;
using System;
using System.Collections.Generic;
using System.Formats.Asn1;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
Expand Down Expand Up @@ -201,11 +202,27 @@ private static List<DLLCHARACTERISTICS> CharacteristicsTypeToListOfCharacteristi
return null;
}

/// <summary>
/// Microsoft's Authenticode RFC 3161 timestamp unsigned attribute (szOID_RFC3161_counterSign).
/// This is what modern Windows binaries are timestamped with.
/// </summary>
private const string MicrosoftRfc3161TimestampOid = "1.3.6.1.4.1.311.3.3.1";

/// <summary>
/// The standard RFC 3161 signature timestamp unsigned attribute (id-aa-signatureTimeStampToken).
/// </summary>
private const string Rfc3161TimestampOid = "1.2.840.113549.1.9.16.2.14";

/// <summary>
/// The content type of the TSTInfo encapsulated in an RFC 3161 timestamp token (id-ct-TSTInfo).
/// </summary>
private const string TstInfoContentTypeOid = "1.2.840.113549.1.9.16.1.4";

/// <summary>
/// Extracts the signing timestamp from a PE file's Authenticode signature.
/// The method first attempts to use the countersigner info in the PKCS#7 data,
/// then checks for RFC3161 timestamp tokens in the signer's UnsignedAttributes,
/// and finally falls back to the signer's own SignedAttributes if needed.
/// The method first attempts to use the countersigner info in the PKCS#7 data (the legacy
/// Authenticode timestamp format), then checks for RFC 3161 timestamp tokens in the signer's
/// UnsignedAttributes, and finally falls back to the signer's own SignedAttributes if needed.
/// </summary>
internal static DateTime? GetSigningTime(PeFile peFile)
{
Expand All @@ -222,42 +239,33 @@ private static List<DLLCHARACTERISTICS> CharacteristicsTypeToListOfCharacteristi

foreach (var signerInfo in signedCms.SignerInfos)
{
// Check counter-signers for the Authenticode timestamp
// Legacy Authenticode timestamps are PKCS#9 countersignatures carrying a signingTime attribute
foreach (var counterSigner in signerInfo.CounterSignerInfos)
{
var time = GetPkcs9SigningTime(counterSigner.SignedAttributes);
if (time.HasValue)
return time;
}

// Check unsigned attributes for RFC 3161 timestamp tokens
// Modern Authenticode timestamps are RFC 3161 tokens carried in an unsigned attribute
foreach (var attr in signerInfo.UnsignedAttributes)
{
// RFC 3161 timestamp token OID: 1.2.840.113549.1.9.16.2.14 (signatureTimeStampToken)
if (string.Equals(attr.Oid?.Value, "1.2.840.113549.1.9.16.2.14", StringComparison.Ordinal))
if (!string.Equals(attr.Oid?.Value, MicrosoftRfc3161TimestampOid, StringComparison.Ordinal) &&
!string.Equals(attr.Oid?.Value, Rfc3161TimestampOid, StringComparison.Ordinal))
{
foreach (var val in attr.Values)
{
try
{
var tokenCms = new SignedCms();
tokenCms.Decode(val.RawData);
foreach (var tokenSigner in tokenCms.SignerInfos)
{
var time = GetPkcs9SigningTime(tokenSigner.SignedAttributes);
if (time.HasValue)
return time;
}
}
catch (CryptographicException)
{
// Not a valid CMS structure, skip
}
}
continue;
}

foreach (var val in attr.Values)
{
var time = GetRfc3161TimestampTime(val.RawData);
if (time.HasValue)
return time;
}
}

// Fallback: check the signer's own signed attributes
// Fallback: check the signer's own signed attributes. This time is asserted by the
// signer rather than by a trusted timestamp authority.
var signerTime = GetPkcs9SigningTime(signerInfo.SignedAttributes);
if (signerTime.HasValue)
return signerTime;
Expand All @@ -270,6 +278,63 @@ private static List<DLLCHARACTERISTICS> CharacteristicsTypeToListOfCharacteristi
return null;
}

/// <summary>
/// Extracts the trusted time from an RFC 3161 timestamp token. The authoritative value is the
/// genTime of the encapsulated TSTInfo; timestamp authorities are not required to also place a
/// PKCS#9 signingTime attribute on the token's signer, so that is only used as a fallback.
/// </summary>
private static DateTime? GetRfc3161TimestampTime(byte[] tokenData)
{
try
{
var tokenCms = new SignedCms();
tokenCms.Decode(tokenData);

if (string.Equals(tokenCms.ContentInfo.ContentType?.Value, TstInfoContentTypeOid, StringComparison.Ordinal))
{
var genTime = GetTstInfoGenTime(tokenCms.ContentInfo.Content);
if (genTime.HasValue)
return genTime;
}

foreach (var tokenSigner in tokenCms.SignerInfos)
{
var time = GetPkcs9SigningTime(tokenSigner.SignedAttributes);
if (time.HasValue)
return time;
}
}
catch (CryptographicException)
{
// Not a valid CMS structure, skip
}
catch (AsnContentException)
{
// Malformed ASN.1, skip
}
return null;
}

/// <summary>
/// Reads the genTime field out of a DER encoded RFC 3161 TSTInfo structure.
/// </summary>
private static DateTime? GetTstInfoGenTime(byte[] tstInfo)
{
try
{
var reader = new AsnReader(tstInfo, AsnEncodingRules.BER).ReadSequence();
reader.ReadInteger(); // version
reader.ReadObjectIdentifier(); // policy
reader.ReadSequence(); // messageImprint
reader.ReadInteger(); // serialNumber
return reader.ReadGeneralizedTime().UtcDateTime;
}
catch (AsnContentException)
{
return null;
}
}

private static DateTime? GetPkcs9SigningTime(CryptographicAttributeObjectCollection attributes)
{
foreach (var attr in attributes)
Expand Down
22 changes: 19 additions & 3 deletions Lib/Objects/Signature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,24 @@ public Signature()
{
}

/// <summary>
/// True when the signature carries a signing time which falls inside the signing
/// certificate's validity period. A binary that was correctly signed and timestamped stays
/// valid here even after its certificate expires. False when the signing time could not be
/// determined, so callers that need to distinguish "signed outside validity" from "signing
/// time unknown" should also inspect <see cref="SigningTime"/>.
/// </summary>
public bool IsTimeValid
{
get
{
if (SigningCertificate != null && SigningTime is DateTime signingTime)
if (SigningCertificate is SerializableCertificate certificate && SigningTime is DateTime signingTime)
{
return signingTime >= SigningCertificate.NotBefore && signingTime <= SigningCertificate.NotAfter;
// Signing times are recovered as UTC while certificate validity comes back from
// X509Certificate2 as local time, so both sides are normalized before comparing.
var signedAtUtc = signingTime.ToUniversalTime();
return signedAtUtc >= certificate.NotBefore.ToUniversalTime() &&
signedAtUtc <= certificate.NotAfter.ToUniversalTime();
}
return false;
}
Expand All @@ -54,6 +65,11 @@ public bool IsTimeValid
public string? SignedHash { get; set; }
public string? SignerSerialNumber { get; set; }
public SerializableCertificate? SigningCertificate { get; set; }

/// <summary>
/// The time the binary was signed, recovered from the Authenticode timestamp when one is
/// present. Null when the signature is absent or carries no recoverable timestamp.
/// </summary>
public DateTime? SigningTime { get; set; }
}
}
}
Loading