From 1f58092dcb67cded75a850db364f3b46d86181f3 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Tue, 21 Apr 2026 21:53:16 -0500 Subject: feat(v2.2.0): add foreground monitoring, notifications, and smarter diffs - implement “Check All” sweep for tracked domains - add local notifications for changes and certificate expiration - introduce change severity filtering (low/medium/high) - enhance change summaries and diff readability - add certificate expiration tracking and alerts - improve watchlist indicators for changes - add sweep summary screen - optimize performance for larger watchlists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DomainDig/SSLCheckService.swift | 73 +++++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 13 deletions(-) (limited to 'DomainDig/SSLCheckService.swift') diff --git a/DomainDig/SSLCheckService.swift b/DomainDig/SSLCheckService.swift index e180d4e..73c00bd 100644 --- a/DomainDig/SSLCheckService.swift +++ b/DomainDig/SSLCheckService.swift @@ -65,24 +65,28 @@ struct SSLCheckService { let validFrom: Date let validUntil: Date - if let notBefore = SecCertificateCopyNotValidBeforeDate(leaf) as Date? { - validFrom = notBefore - } else { - validFrom = Date.distantPast - } + let derData = SecCertificateCopyData(leaf) as Data + let parsed = DERCertificateParser.parse(derData) + + if #available(iOS 18.0, *) { + if let notBefore = SecCertificateCopyNotValidBeforeDate(leaf) as Date? { + validFrom = notBefore + } else { + validFrom = parsed.notBefore ?? Date.distantPast + } - if let notAfter = SecCertificateCopyNotValidAfterDate(leaf) as Date? { - validUntil = notAfter + if let notAfter = SecCertificateCopyNotValidAfterDate(leaf) as Date? { + validUntil = notAfter + } else { + validUntil = parsed.notAfter ?? Date.distantFuture + } } else { - validUntil = Date.distantFuture + validFrom = parsed.notBefore ?? Date.distantPast + validUntil = parsed.notAfter ?? Date.distantFuture } let daysUntilExpiry = Calendar.current.dateComponents([.day], from: Date(), to: validUntil).day ?? 0 - // Parse the DER-encoded certificate to extract SANs and Issuer - let derData = SecCertificateCopyData(leaf) as Data - let parsed = DERCertificateParser.parse(derData) - let sans = parsed.subjectAltNames.isEmpty ? [commonName] : parsed.subjectAltNames // Issuer: prefer parsed issuer, fall back to chain's next cert summary @@ -116,6 +120,7 @@ struct SSLCheckService { chain: chain ) } + } fileprivate struct TLSMetadata { @@ -133,6 +138,8 @@ private enum DERCertificateParser { struct Result { var issuerCommonName: String? var subjectAltNames: [String] = [] + var notBefore: Date? + var notAfter: Date? } static func parse(_ data: Data) -> Result { @@ -171,8 +178,11 @@ private enum DERCertificateParser { offset = issuerSeq.contentStart + issuerSeq.length } - // Skip validity + // Validity if let validity = readTagAndLength(bytes, offset: offset) { + let (notBefore, notAfter) = extractValidity(bytes, sequenceStart: validity.contentStart, length: validity.length) + result.notBefore = notBefore + result.notAfter = notAfter offset = validity.contentStart + validity.length } @@ -287,6 +297,43 @@ private enum DERCertificateParser { return sans } + private static func extractValidity(_ bytes: [UInt8], sequenceStart: Int, length: Int) -> (Date?, Date?) { + let end = sequenceStart + length + var position = sequenceStart + var dates: [Date] = [] + + while position < end, dates.count < 2 { + guard let timeTL = readTagAndLength(bytes, offset: position) else { break } + let raw = String(bytes: bytes[timeTL.contentStart.. Date { + let utcFormatter = DateFormatter() + utcFormatter.locale = Locale(identifier: "en_US_POSIX") + utcFormatter.timeZone = TimeZone(secondsFromGMT: 0) + utcFormatter.dateFormat = "yyMMddHHmmss'Z'" + + if let date = utcFormatter.date(from: string) { + return date + } + + let generalizedFormatter = DateFormatter() + generalizedFormatter.locale = Locale(identifier: "en_US_POSIX") + generalizedFormatter.timeZone = TimeZone(secondsFromGMT: 0) + generalizedFormatter.dateFormat = "yyyyMMddHHmmss'Z'" + + return generalizedFormatter.date(from: string) ?? Date.distantFuture + } + private struct TLV { let contentStart: Int let length: Int -- cgit v1.2.3