krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

e11b7d29a6aa9e52f63fcb35ee7ed9127850fffe

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-03-12T03:01:24Z

initial commit
 DomainDig.xcodeproj/project.pbxproj                | 343 +++++++++++++++++++++
 .../project.xcworkspace/contents.xcworkspacedata   |   7 +
 .../xcschemes/xcschememanagement.plist             |  14 +
 .../AccentColor.colorset/Contents.json             |  11 +
 .../AppIcon.appiconset/Contents.json               |  36 +++
 .../AppIcon.appiconset/domaindig.png               | Bin 0 -> 441131 bytes
 DomainDig/Assets.xcassets/Contents.json            |   6 +
 DomainDig/ContentView.swift                        | 309 +++++++++++++++++++
 DomainDig/DNSLookupService.swift                   |  89 ++++++
 DomainDig/DomainDigApp.swift                       |  17 +
 DomainDig/DomainViewModel.swift                    | 157 ++++++++++
 DomainDig/Models.swift                             |  63 ++++
 DomainDig/SSLCheckService.swift                    | 322 +++++++++++++++++++
 LICENSE                                            |  21 ++
 claude.md                                          |  79 +++++
 privacypolicy.md                                   |  40 +++
 16 files changed, 1514 insertions(+)

diff --git a/DomainDig.xcodeproj/project.pbxproj b/DomainDig.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..706ba0f
--- /dev/null
+++ b/DomainDig.xcodeproj/project.pbxproj
@@ -0,0 +1,343 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 77;
+	objects = {
+
+/* Begin PBXFileReference section */
+		8B7800692F6090E300933221 /* DomainDig.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DomainDig.app; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+		8B78006B2F6090E300933221 /* DomainDig */ = {
+			isa = PBXFileSystemSynchronizedRootGroup;
+			path = DomainDig;
+			sourceTree = "<group>";
+		};
+/* End PBXFileSystemSynchronizedRootGroup section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		8B7800662F6090E300933221 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		8B7800602F6090E300933221 = {
+			isa = PBXGroup;
+			children = (
+				8B78006B2F6090E300933221 /* DomainDig */,
+				8B78006A2F6090E300933221 /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		8B78006A2F6090E300933221 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				8B7800692F6090E300933221 /* DomainDig.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		8B7800682F6090E300933221 /* DomainDig */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 8B7800742F6090E400933221 /* Build configuration list for PBXNativeTarget "DomainDig" */;
+			buildPhases = (
+				8B7800652F6090E300933221 /* Sources */,
+				8B7800662F6090E300933221 /* Frameworks */,
+				8B7800672F6090E300933221 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			fileSystemSynchronizedGroups = (
+				8B78006B2F6090E300933221 /* DomainDig */,
+			);
+			name = DomainDig;
+			packageProductDependencies = (
+			);
+			productName = DomainDig;
+			productReference = 8B7800692F6090E300933221 /* DomainDig.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		8B7800612F6090E300933221 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 2630;
+				LastUpgradeCheck = 2630;
+				TargetAttributes = {
+					8B7800682F6090E300933221 = {
+						CreatedOnToolsVersion = 26.3;
+					};
+				};
+			};
+			buildConfigurationList = 8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */;
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 8B7800602F6090E300933221;
+			minimizedProjectReferenceProxies = 1;
+			preferredProjectObjectVersion = 77;
+			productRefGroup = 8B78006A2F6090E300933221 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				8B7800682F6090E300933221 /* DomainDig */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		8B7800672F6090E300933221 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		8B7800652F6090E300933221 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		8B7800722F6090E400933221 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				DEVELOPMENT_TEAM = ZCNAX3VL9D;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 26.2;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		8B7800732F6090E400933221 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				DEVELOPMENT_TEAM = ZCNAX3VL9D;
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 26.2;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		8B7800752F6090E400933221 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = ZCNAX3VL9D;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_KEY_CFBundleDisplayName = "Domain Dig";
+				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRING_CATALOG_GENERATE_SYMBOLS = YES;
+				SWIFT_APPROACHABLE_CONCURRENCY = YES;
+				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		8B7800762F6090E400933221 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = ZCNAX3VL9D;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_KEY_CFBundleDisplayName = "Domain Dig";
+				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRING_CATALOG_GENERATE_SYMBOLS = YES;
+				SWIFT_APPROACHABLE_CONCURRENCY = YES;
+				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				8B7800722F6090E400933221 /* Debug */,
+				8B7800732F6090E400933221 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		8B7800742F6090E400933221 /* Build configuration list for PBXNativeTarget "DomainDig" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				8B7800752F6090E400933221 /* Debug */,
+				8B7800762F6090E400933221 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 8B7800612F6090E300933221 /* Project object */;
+}
diff --git a/DomainDig.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/DomainDig.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/DomainDig.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:">
+   </FileRef>
+</Workspace>
diff --git a/DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist b/DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..bd2f39c
--- /dev/null
+++ b/DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>SchemeUserState</key>
+	<dict>
+		<key>DomainDig.xcscheme_^#shared#^_</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>0</integer>
+		</dict>
+	</dict>
+</dict>
+</plist>
diff --git a/DomainDig/Assets.xcassets/AccentColor.colorset/Contents.json b/DomainDig/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/DomainDig/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/DomainDig/Assets.xcassets/AppIcon.appiconset/Contents.json b/DomainDig/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..87ffca2
--- /dev/null
+++ b/DomainDig/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+  "images" : [
+    {
+      "filename" : "domaindig.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "tinted"
+        }
+      ],
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/DomainDig/Assets.xcassets/AppIcon.appiconset/domaindig.png b/DomainDig/Assets.xcassets/AppIcon.appiconset/domaindig.png
new file mode 100644
index 0000000..752f230
Binary files /dev/null and b/DomainDig/Assets.xcassets/AppIcon.appiconset/domaindig.png differ
diff --git a/DomainDig/Assets.xcassets/Contents.json b/DomainDig/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/DomainDig/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift
new file mode 100644
index 0000000..b6588b4
--- /dev/null
+++ b/DomainDig/ContentView.swift
@@ -0,0 +1,309 @@
+import SwiftUI
+
+struct ContentView: View {
+    @State private var viewModel = DomainViewModel()
+    @FocusState private var domainFieldFocused: Bool
+
+    var body: some View {
+        NavigationStack {
+            ScrollView {
+                VStack(spacing: 0) {
+                    inputSection
+                    if viewModel.hasRun {
+                        if viewModel.resultsLoaded {
+                            HStack {
+                                Spacer()
+                                Button {
+                                    shareResults()
+                                } label: {
+                                    Image(systemName: "square.and.arrow.up")
+                                        .font(.system(.body))
+                                        .foregroundStyle(.secondary)
+                                }
+                            }
+                            .padding(.top, 8)
+                        }
+                        dnsResultsSection
+                        sslResultsSection
+                    } else if !viewModel.recentSearches.isEmpty {
+                        recentSearchesSection
+                    }
+                }
+                .padding(.horizontal)
+                .padding(.bottom, 32)
+            }
+            .background(Color.black)
+            .navigationTitle("DomainDig")
+            .toolbarColorScheme(.dark, for: .navigationBar)
+            .preferredColorScheme(.dark)
+        }
+        .onAppear {
+            domainFieldFocused = true
+        }
+    }
+
+    // MARK: - Input
+
+    private var inputSection: some View {
+        VStack(spacing: 12) {
+            TextField("e.g. cleberg.net", text: $viewModel.domain)
+                .font(.system(.title3, design: .monospaced))
+                .textInputAutocapitalization(.never)
+                .autocorrectionDisabled()
+                .keyboardType(.URL)
+                .padding(12)
+                .background(Color(.systemGray6))
+                .cornerRadius(8)
+                .focused($domainFieldFocused)
+                .onSubmit { viewModel.run() }
+
+            Button {
+                domainFieldFocused = false
+                viewModel.run()
+            } label: {
+                Text("Run")
+                    .font(.headline)
+                    .frame(maxWidth: .infinity)
+                    .padding(.vertical, 12)
+            }
+            .buttonStyle(.borderedProminent)
+            .disabled(viewModel.trimmedDomain.isEmpty)
+        }
+        .padding(.vertical, 16)
+    }
+
+    // MARK: - Recent Searches
+
+    private var recentSearchesSection: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            HStack {
+                Text("RECENT")
+                    .font(.system(.caption2, design: .monospaced))
+                    .foregroundStyle(.secondary)
+                Spacer()
+                Button("Clear") {
+                    viewModel.clearRecentSearches()
+                }
+                .font(.system(.caption2, design: .monospaced))
+                .foregroundStyle(.secondary)
+            }
+
+            ForEach(viewModel.recentSearches, id: \.self) { domain in
+                Button {
+                    viewModel.domain = domain
+                    domainFieldFocused = false
+                    viewModel.run()
+                } label: {
+                    Text(domain)
+                        .font(.system(.callout, design: .monospaced))
+                        .foregroundStyle(.primary)
+                        .frame(maxWidth: .infinity, alignment: .leading)
+                        .padding(.vertical, 6)
+                        .padding(.horizontal, 10)
+                        .background(Color(.systemGray6).opacity(0.5))
+                        .cornerRadius(6)
+                }
+            }
+        }
+        .padding(.top, 8)
+    }
+
+    // MARK: - DNS Results
+
+    private var dnsResultsSection: some View {
+        VStack(alignment: .leading, spacing: 12) {
+            sectionHeader("DNS Records")
+
+            if viewModel.dnsLoading {
+                ProgressView("Querying DNS…")
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .padding()
+            } else if let error = viewModel.dnsError {
+                errorLabel(error)
+            } else {
+                ForEach(viewModel.dnsSections) { section in
+                    dnsRecordSection(section)
+                }
+            }
+        }
+        .padding(.top, 8)
+    }
+
+    private func dnsRecordSection(_ section: DNSSection) -> some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(section.recordType.rawValue)
+                .font(.system(.subheadline, design: .monospaced))
+                .fontWeight(.semibold)
+                .foregroundStyle(.cyan)
+
+            if let error = section.error {
+                errorLabel(error)
+            } else if section.records.isEmpty {
+                Text("No records found")
+                    .font(.system(.caption, design: .monospaced))
+                    .foregroundStyle(.secondary)
+            } else {
+                dnsRecordRows(section.records)
+            }
+
+            // Wildcard sub-section (only shown when records exist)
+            if !section.wildcardRecords.isEmpty {
+                Text("*.\(viewModel.searchedDomain)")
+                    .font(.system(.caption, design: .monospaced))
+                    .fontWeight(.medium)
+                    .foregroundStyle(.cyan.opacity(0.7))
+                    .padding(.top, 4)
+
+                dnsRecordRows(section.wildcardRecords)
+            }
+        }
+        .padding(10)
+        .background(Color(.systemGray6).opacity(0.5))
+        .cornerRadius(6)
+    }
+
+    private func dnsRecordRows(_ records: [DNSRecord]) -> some View {
+        ForEach(records) { record in
+            HStack(alignment: .top) {
+                Text(record.value)
+                    .font(.system(.caption, design: .monospaced))
+                    .foregroundStyle(.primary)
+                    .textSelection(.enabled)
+                Spacer()
+                Text("TTL \(record.ttl)")
+                    .font(.system(.caption2, design: .monospaced))
+                    .foregroundStyle(.secondary)
+            }
+        }
+    }
+
+    // MARK: - SSL Results
+
+    private var sslResultsSection: some View {
+        VStack(alignment: .leading, spacing: 12) {
+            sectionHeader("SSL / TLS Certificate")
+
+            if viewModel.sslLoading {
+                ProgressView("Checking certificate…")
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .padding()
+            } else if let error = viewModel.sslError {
+                errorLabel(error)
+            } else if let info = viewModel.sslInfo {
+                sslDetail(info)
+            }
+        }
+        .padding(.top, 16)
+    }
+
+    private func sslDetail(_ info: SSLCertificateInfo) -> some View {
+        VStack(alignment: .leading, spacing: 8) {
+            certRow("Common Name", info.commonName)
+            certRow("Issuer", info.issuer)
+
+            // SANs
+            VStack(alignment: .leading, spacing: 2) {
+                Text("SANs")
+                    .font(.system(.caption2, design: .monospaced))
+                    .foregroundStyle(.secondary)
+                ForEach(info.subjectAltNames, id: \.self) { san in
+                    Text(san)
+                        .font(.system(.caption, design: .monospaced))
+                        .textSelection(.enabled)
+                }
+            }
+
+            let formatter = DateFormatter.certDate
+            certRow("Valid From", formatter.string(from: info.validFrom))
+            certRow("Valid Until", formatter.string(from: info.validUntil))
+
+            HStack {
+                Text("Days Until Expiry")
+                    .font(.system(.caption2, design: .monospaced))
+                    .foregroundStyle(.secondary)
+                Spacer()
+                Text("\(info.daysUntilExpiry)")
+                    .font(.system(.caption, design: .monospaced))
+                    .fontWeight(.bold)
+                    .foregroundStyle(expiryColor(info.daysUntilExpiry))
+            }
+
+            certRow("Chain Depth", "\(info.chainDepth)")
+        }
+        .padding(10)
+        .background(Color(.systemGray6).opacity(0.5))
+        .cornerRadius(6)
+    }
+
+    // MARK: - Helpers
+
+    private func sectionHeader(_ title: String) -> some View {
+        Text(title)
+            .font(.system(.headline, design: .default))
+            .foregroundStyle(.white)
+    }
+
+    private func certRow(_ label: String, _ value: String) -> some View {
+        VStack(alignment: .leading, spacing: 2) {
+            Text(label)
+                .font(.system(.caption2, design: .monospaced))
+                .foregroundStyle(.secondary)
+            Text(value)
+                .font(.system(.caption, design: .monospaced))
+                .textSelection(.enabled)
+        }
+    }
+
+    private func errorLabel(_ message: String) -> some View {
+        Label(message, systemImage: "exclamationmark.triangle.fill")
+            .font(.system(.caption, design: .monospaced))
+            .foregroundStyle(.red)
+            .padding(8)
+    }
+
+    private func expiryColor(_ days: Int) -> Color {
+        if days < 30 { return .red }
+        if days < 60 { return .yellow }
+        return .green
+    }
+
+    private func shareResults() {
+        let text = viewModel.exportText()
+
+        // Write to a named temp file so "Save to Files" uses a proper filename
+        let dateFmt = DateFormatter()
+        dateFmt.dateFormat = "yyyyMMdd_HHmmss"
+        let timestamp = dateFmt.string(from: Date())
+        let filename = "\(timestamp)_domaindigresults.txt"
+        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
+
+        do {
+            try text.write(to: tempURL, atomically: true, encoding: .utf8)
+        } catch {
+            return
+        }
+
+        let activityVC = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil)
+        guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+              let rootVC = windowScene.keyWindow?.rootViewController else { return }
+        var presenter = rootVC
+        while let presented = presenter.presentedViewController {
+            presenter = presented
+        }
+        activityVC.popoverPresentationController?.sourceView = presenter.view
+        presenter.present(activityVC, animated: true)
+    }
+}
+
+private extension DateFormatter {
+    static let certDate: DateFormatter = {
+        let f = DateFormatter()
+        f.dateStyle = .medium
+        f.timeStyle = .short
+        return f
+    }()
+}
+
+#Preview {
+    ContentView()
+}
diff --git a/DomainDig/DNSLookupService.swift b/DomainDig/DNSLookupService.swift
new file mode 100644
index 0000000..85a5275
--- /dev/null
+++ b/DomainDig/DNSLookupService.swift
@@ -0,0 +1,89 @@
+import Foundation
+
+struct DNSLookupService {
+    private static let baseURL = "https://cloudflare-dns.com/dns-query"
+
+    static func lookup(domain: String, recordType: DNSRecordType) async throws -> [DNSRecord] {
+        var components = URLComponents(string: baseURL)!
+        components.queryItems = [
+            URLQueryItem(name: "name", value: domain),
+            URLQueryItem(name: "type", value: String(recordType.queryType))
+        ]
+
+        var request = URLRequest(url: components.url!)
+        request.setValue("application/dns-json", forHTTPHeaderField: "Accept")
+
+        let (data, response) = try await URLSession.shared.data(for: request)
+
+        guard let httpResponse = response as? HTTPURLResponse,
+              httpResponse.statusCode == 200 else {
+            throw URLError(.badServerResponse)
+        }
+
+        let dnsResponse = try JSONDecoder().decode(CloudflareDNSResponse.self, from: data)
+
+        guard let answers = dnsResponse.Answer else {
+            return []
+        }
+
+        // Filter answers to only include the requested type
+        return answers
+            .filter { $0.type == recordType.queryType }
+            .map { answer in
+                let value = answer.data.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
+                return DNSRecord(value: value, ttl: answer.TTL)
+            }
+    }
+
+    /// Record types that support wildcard queries.
+    private nonisolated(unsafe) static let wildcardTypes: Set<DNSRecordType> = [.A, .AAAA, .MX, .TXT]
+
+    static func lookupAll(domain: String) async -> [DNSSection] {
+        // Each task returns (recordType, apex records, wildcard records).
+        typealias Result = (type: DNSRecordType, records: [DNSRecord], wildcard: [DNSRecord], error: String?)
+
+        return await withTaskGroup(of: Result.self, returning: [DNSSection].self) { group in
+            for recordType in DNSRecordType.allCases {
+                group.addTask {
+                    var apexRecords: [DNSRecord] = []
+                    var wildcardRecords: [DNSRecord] = []
+                    var lookupError: String?
+
+                    // Apex query
+                    do {
+                        apexRecords = try await lookup(domain: domain, recordType: recordType)
+                    } catch {
+                        lookupError = error.localizedDescription
+                    }
+
+                    // Wildcard query (only for applicable types, and only if apex didn't fail)
+                    if wildcardTypes.contains(recordType) && lookupError == nil {
+                        do {
+                            wildcardRecords = try await lookup(domain: "*.\(domain)", recordType: recordType)
+                        } catch {
+                            // Wildcard failure is non-fatal; just leave empty
+                        }
+                    }
+
+                    return (recordType, apexRecords, wildcardRecords, lookupError)
+                }
+            }
+
+            var sections: [DNSSection] = []
+            for await result in group {
+                sections.append(DNSSection(
+                    recordType: result.type,
+                    records: result.records,
+                    wildcardRecords: result.wildcard,
+                    error: result.error
+                ))
+            }
+
+            // Sort to maintain consistent order
+            let order = DNSRecordType.allCases
+            return sections.sorted { a, b in
+                (order.firstIndex(of: a.recordType) ?? 0) < (order.firstIndex(of: b.recordType) ?? 0)
+            }
+        }
+    }
+}
diff --git a/DomainDig/DomainDigApp.swift b/DomainDig/DomainDigApp.swift
new file mode 100644
index 0000000..9c881d8
--- /dev/null
+++ b/DomainDig/DomainDigApp.swift
@@ -0,0 +1,17 @@
+//
+//  DomainDigApp.swift
+//  DomainDig
+//
+//  Created by cmc on 2026-03-10.
+//
+
+import SwiftUI
+
+@main
+struct DomainDigApp: App {
+    var body: some Scene {
+        WindowGroup {
+            ContentView()
+        }
+    }
+}
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
new file mode 100644
index 0000000..2965dde
--- /dev/null
+++ b/DomainDig/DomainViewModel.swift
@@ -0,0 +1,157 @@
+import Foundation
+import SwiftUI
+
+@MainActor
+@Observable
+final class DomainViewModel {
+    var domain: String = ""
+
+    var dnsSections: [DNSSection] = []
+    var dnsLoading = false
+    var dnsError: String?
+
+    var sslInfo: SSLCertificateInfo?
+    var sslLoading = false
+    var sslError: String?
+
+    var hasRun = false
+    private(set) var searchedDomain: String = ""
+
+    private static let recentSearchesKey = "recentSearches"
+    private static let maxRecent = 20
+
+    var recentSearches: [String] = UserDefaults.standard.stringArray(forKey: recentSearchesKey) ?? []
+
+    var trimmedDomain: String {
+        domain
+            .trimmingCharacters(in: .whitespacesAndNewlines)
+            .replacingOccurrences(of: "https://", with: "")
+            .replacingOccurrences(of: "http://", with: "")
+            .components(separatedBy: "/").first ?? ""
+    }
+
+    func run() {
+        let target = trimmedDomain
+        guard !target.isEmpty else { return }
+
+        addRecentSearch(target)
+        searchedDomain = target
+        hasRun = true
+        dnsSections = []
+        dnsError = nil
+        dnsLoading = true
+        sslInfo = nil
+        sslError = nil
+        sslLoading = true
+
+        Task {
+            await withTaskGroup(of: Void.self) { group in
+                group.addTask { @MainActor in
+                    await self.runDNS(domain: target)
+                }
+                group.addTask { @MainActor in
+                    await self.runSSL(domain: target)
+                }
+            }
+        }
+    }
+
+    private func runDNS(domain: String) async {
+        do {
+            let sections = await DNSLookupService.lookupAll(domain: domain)
+            dnsSections = sections
+        }
+        dnsLoading = false
+    }
+
+    private func runSSL(domain: String) async {
+        do {
+            let info = try await SSLCheckService.check(domain: domain)
+            sslInfo = info
+        } catch {
+            sslError = error.localizedDescription
+        }
+        sslLoading = false
+    }
+
+    /// True when both lookups have finished (regardless of success/failure).
+    var resultsLoaded: Bool {
+        hasRun && !dnsLoading && !sslLoading
+    }
+
+    // MARK: - Export
+
+    func exportText() -> String {
+        let dateFmt = DateFormatter()
+        dateFmt.dateFormat = "yyyy-MM-dd HH:mm"
+        let now = dateFmt.string(from: Date())
+
+        var lines: [String] = [
+            "DomainDig Export",
+            "Domain: \(searchedDomain)",
+            "Date: \(now)",
+            "",
+            "DNS Records",
+            "-----------"
+        ]
+
+        for section in dnsSections {
+            lines.append(section.recordType.rawValue)
+            if let error = section.error {
+                lines.append("  Error: \(error)")
+            } else if section.records.isEmpty {
+                lines.append("  No records found")
+            } else {
+                for record in section.records {
+                    lines.append("  \(record.value)  TTL \(record.ttl)")
+                }
+            }
+            if !section.wildcardRecords.isEmpty {
+                lines.append("*.\(searchedDomain)")
+                for record in section.wildcardRecords {
+                    lines.append("  \(record.value)  TTL \(record.ttl)")
+                }
+            }
+        }
+
+        if let info = sslInfo {
+            let certDateFmt = DateFormatter()
+            certDateFmt.dateStyle = .medium
+            certDateFmt.timeStyle = .none
+
+            lines.append("")
+            lines.append("SSL / TLS Certificate")
+            lines.append("---------------------")
+            lines.append("Common Name: \(info.commonName)")
+            lines.append("Issuer: \(info.issuer)")
+            lines.append("SANs: \(info.subjectAltNames.joined(separator: ", "))")
+            lines.append("Valid From: \(certDateFmt.string(from: info.validFrom))")
+            lines.append("Valid Until: \(certDateFmt.string(from: info.validUntil))")
+            lines.append("Days Until Expiry: \(info.daysUntilExpiry)")
+            lines.append("Chain Depth: \(info.chainDepth)")
+        } else if let error = sslError {
+            lines.append("")
+            lines.append("SSL / TLS Certificate")
+            lines.append("---------------------")
+            lines.append("Error: \(error)")
+        }
+
+        return lines.joined(separator: "\n")
+    }
+
+    // MARK: - Recent Searches
+
+    private func addRecentSearch(_ domain: String) {
+        recentSearches.removeAll { $0.lowercased() == domain.lowercased() }
+        recentSearches.insert(domain, at: 0)
+        if recentSearches.count > Self.maxRecent {
+            recentSearches = Array(recentSearches.prefix(Self.maxRecent))
+        }
+        UserDefaults.standard.set(recentSearches, forKey: Self.recentSearchesKey)
+    }
+
+    func clearRecentSearches() {
+        recentSearches.removeAll()
+        UserDefaults.standard.removeObject(forKey: Self.recentSearchesKey)
+    }
+}
diff --git a/DomainDig/Models.swift b/DomainDig/Models.swift
new file mode 100644
index 0000000..53b3c39
--- /dev/null
+++ b/DomainDig/Models.swift
@@ -0,0 +1,63 @@
+import Foundation
+
+// MARK: - DNS Models
+
+enum DNSRecordType: String, CaseIterable {
+    case A
+    case AAAA
+    case MX
+    case NS
+    case TXT
+    case CNAME
+
+    var queryType: Int {
+        switch self {
+        case .A: return 1
+        case .AAAA: return 28
+        case .MX: return 15
+        case .NS: return 2
+        case .TXT: return 16
+        case .CNAME: return 5
+        }
+    }
+}
+
+struct DNSRecord: Identifiable {
+    let id = UUID()
+    let value: String
+    let ttl: Int
+}
+
+struct DNSSection: Identifiable {
+    let id = UUID()
+    let recordType: DNSRecordType
+    var records: [DNSRecord]
+    var wildcardRecords: [DNSRecord] = []
+    var error: String?
+}
+
+// MARK: - SSL Models
+
+struct SSLCertificateInfo {
+    let commonName: String
+    let subjectAltNames: [String]
+    let issuer: String
+    let validFrom: Date
+    let validUntil: Date
+    let daysUntilExpiry: Int
+    let chainDepth: Int
+}
+
+// MARK: - Cloudflare DNS-over-HTTPS Response
+
+struct CloudflareDNSResponse: Decodable {
+    let Status: Int
+    let Answer: [CloudflareDNSAnswer]?
+
+    struct CloudflareDNSAnswer: Decodable {
+        let name: String
+        let type: Int
+        let TTL: Int
+        let data: String
+    }
+}
diff --git a/DomainDig/SSLCheckService.swift b/DomainDig/SSLCheckService.swift
new file mode 100644
index 0000000..bff41c0
--- /dev/null
+++ b/DomainDig/SSLCheckService.swift
@@ -0,0 +1,322 @@
+import Foundation
+import Security
+
+struct SSLCheckService {
+
+    static func check(domain: String) async throws -> SSLCertificateInfo {
+        let delegate = SSLSessionDelegate()
+        let session = URLSession(
+            configuration: .ephemeral,
+            delegate: delegate,
+            delegateQueue: nil
+        )
+        defer { session.invalidateAndCancel() }
+
+        let url = URL(string: "https://\(domain)")!
+        let request = URLRequest(url: url, timeoutInterval: 10)
+
+        // We only need to establish the connection to grab the cert
+        _ = try await session.data(for: request)
+
+        guard let trust = delegate.serverTrust else {
+            throw SSLError.noCertificate
+        }
+
+        return try extractCertificateInfo(from: trust)
+    }
+
+    private static func extractCertificateInfo(from trust: SecTrust) throws -> SSLCertificateInfo {
+        let chainCount = SecTrustGetCertificateCount(trust)
+        guard chainCount > 0,
+              let certChain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
+              let leaf = certChain.first else {
+            throw SSLError.noCertificate
+        }
+
+        // Common Name — use subject summary (available on iOS)
+        let commonName = SecCertificateCopySubjectSummary(leaf) as String? ?? "Unknown"
+
+        // Validity dates
+        let validFrom: Date
+        let validUntil: Date
+
+        if let notBefore = SecCertificateCopyNotValidBeforeDate(leaf) as Date? {
+            validFrom = notBefore
+        } else {
+            validFrom = Date.distantPast
+        }
+
+        if let notAfter = SecCertificateCopyNotValidAfterDate(leaf) as Date? {
+            validUntil = notAfter
+        } else {
+            validUntil = 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
+        var issuer = parsed.issuerCommonName ?? "Unknown"
+        if issuer == "Unknown" && certChain.count > 1 {
+            let issuerCert = certChain[1]
+            if let issuerSummary = SecCertificateCopySubjectSummary(issuerCert) as String? {
+                issuer = issuerSummary
+            }
+        }
+
+        return SSLCertificateInfo(
+            commonName: commonName,
+            subjectAltNames: sans,
+            issuer: issuer,
+            validFrom: validFrom,
+            validUntil: validUntil,
+            daysUntilExpiry: daysUntilExpiry,
+            chainDepth: Int(chainCount)
+        )
+    }
+}
+
+// MARK: - Minimal DER/ASN.1 parser for X.509 certificate fields
+
+private enum DERCertificateParser {
+    struct Result {
+        var issuerCommonName: String?
+        var subjectAltNames: [String] = []
+    }
+
+    static func parse(_ data: Data) -> Result {
+        var result = Result()
+        let bytes = [UInt8](data)
+
+        // X.509 structure: SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
+        // tbsCertificate: SEQUENCE { version, serialNumber, signature, issuer, validity, subject, ... extensions }
+        guard let tbsRange = readSequence(bytes, offset: 0),
+              let tbsContent = readSequence(bytes, offset: tbsRange.contentStart) else {
+            return result
+        }
+
+        var offset = tbsContent.contentStart
+
+        // Skip version (explicit tag [0]) if present
+        if offset < bytes.count && (bytes[offset] & 0xE0) == 0xA0 {
+            if let tagLen = readTagAndLength(bytes, offset: offset) {
+                offset = tagLen.contentStart + tagLen.length
+            }
+        }
+
+        // Skip serialNumber
+        if let serial = readTagAndLength(bytes, offset: offset) {
+            offset = serial.contentStart + serial.length
+        }
+
+        // Skip signature algorithm
+        if let sigAlg = readTagAndLength(bytes, offset: offset) {
+            offset = sigAlg.contentStart + sigAlg.length
+        }
+
+        // Issuer — a SEQUENCE of SETs of attribute type-value pairs
+        if let issuerSeq = readTagAndLength(bytes, offset: offset) {
+            result.issuerCommonName = extractCommonName(bytes, sequenceStart: issuerSeq.contentStart, length: issuerSeq.length)
+            offset = issuerSeq.contentStart + issuerSeq.length
+        }
+
+        // Skip validity
+        if let validity = readTagAndLength(bytes, offset: offset) {
+            offset = validity.contentStart + validity.length
+        }
+
+        // Skip subject
+        if let subject = readTagAndLength(bytes, offset: offset) {
+            offset = subject.contentStart + subject.length
+        }
+
+        // Skip subjectPublicKeyInfo
+        if let spki = readTagAndLength(bytes, offset: offset) {
+            offset = spki.contentStart + spki.length
+        }
+
+        // Extensions are in an explicit tag [3]
+        while offset < tbsContent.contentStart + tbsContent.length {
+            if bytes[offset] == 0xA3 {
+                if let extWrapper = readTagAndLength(bytes, offset: offset) {
+                    // Inside is a SEQUENCE of SEQUENCE extensions
+                    if let extsSeq = readTagAndLength(bytes, offset: extWrapper.contentStart) {
+                        result.subjectAltNames = extractSANs(bytes, sequenceStart: extsSeq.contentStart, length: extsSeq.length)
+                    }
+                }
+                break
+            }
+            // Skip optional implicit tags (issuerUniqueID [1], subjectUniqueID [2])
+            if let tl = readTagAndLength(bytes, offset: offset) {
+                offset = tl.contentStart + tl.length
+            } else {
+                break
+            }
+        }
+
+        return result
+    }
+
+    // OID for commonName: 2.5.4.3 = 55 04 03
+    private static let cnOID: [UInt8] = [0x55, 0x04, 0x03]
+
+    // OID for subjectAltName: 2.5.29.17 = 55 1D 11
+    private static let sanOID: [UInt8] = [0x55, 0x1D, 0x11]
+
+    private static func extractCommonName(_ bytes: [UInt8], sequenceStart: Int, length: Int) -> String? {
+        let end = sequenceStart + length
+        var pos = sequenceStart
+        while pos < end {
+            // Each SET in the issuer
+            guard let setTL = readTagAndLength(bytes, offset: pos) else { break }
+            let setEnd = setTL.contentStart + setTL.length
+
+            // Inside the SET is a SEQUENCE with OID + value
+            if let seqTL = readTagAndLength(bytes, offset: setTL.contentStart) {
+                let seqEnd = seqTL.contentStart + seqTL.length
+                if let oidTL = readTagAndLength(bytes, offset: seqTL.contentStart) {
+                    let oidBytes = Array(bytes[oidTL.contentStart..<oidTL.contentStart + oidTL.length])
+                    if oidBytes == cnOID {
+                        let valueStart = oidTL.contentStart + oidTL.length
+                        if let valueTL = readTagAndLength(bytes, offset: valueStart) {
+                            let strBytes = bytes[valueTL.contentStart..<valueTL.contentStart + valueTL.length]
+                            return String(bytes: strBytes, encoding: .utf8)
+                        }
+                    }
+                    _ = seqEnd // suppress unused warning
+                }
+            }
+            pos = setEnd
+        }
+        return nil
+    }
+
+    private static func extractSANs(_ bytes: [UInt8], sequenceStart: Int, length: Int) -> [String] {
+        let end = sequenceStart + length
+        var pos = sequenceStart
+        var sans: [String] = []
+
+        while pos < end {
+            guard let extSeq = readTagAndLength(bytes, offset: pos) else { break }
+            let extEnd = extSeq.contentStart + extSeq.length
+
+            // Each extension is SEQUENCE { OID, [critical], value }
+            if let oidTL = readTagAndLength(bytes, offset: extSeq.contentStart) {
+                let oidBytes = Array(bytes[oidTL.contentStart..<oidTL.contentStart + oidTL.length])
+                if oidBytes == sanOID {
+                    var valuePos = oidTL.contentStart + oidTL.length
+                    // Skip optional critical BOOLEAN
+                    if valuePos < extEnd && bytes[valuePos] == 0x01 {
+                        if let boolTL = readTagAndLength(bytes, offset: valuePos) {
+                            valuePos = boolTL.contentStart + boolTL.length
+                        }
+                    }
+                    // The value is an OCTET STRING wrapping a SEQUENCE of GeneralNames
+                    if let octetTL = readTagAndLength(bytes, offset: valuePos) {
+                        if let sanSeq = readTagAndLength(bytes, offset: octetTL.contentStart) {
+                            let sanEnd = sanSeq.contentStart + sanSeq.length
+                            var sanPos = sanSeq.contentStart
+                            while sanPos < sanEnd {
+                                guard let nameTL = readTagAndLength(bytes, offset: sanPos) else { break }
+                                // Context tag [2] = dNSName (IA5String)
+                                if (bytes[sanPos] & 0x1F) == 2 {
+                                    let nameBytes = bytes[nameTL.contentStart..<nameTL.contentStart + nameTL.length]
+                                    if let name = String(bytes: nameBytes, encoding: .ascii) {
+                                        sans.append(name)
+                                    }
+                                }
+                                sanPos = nameTL.contentStart + nameTL.length
+                            }
+                        }
+                    }
+                }
+            }
+            pos = extEnd
+        }
+        return sans
+    }
+
+    private struct TLV {
+        let contentStart: Int
+        let length: Int
+    }
+
+    private static func readSequence(_ bytes: [UInt8], offset: Int) -> TLV? {
+        guard offset < bytes.count, bytes[offset] == 0x30 else { return nil }
+        return readTagAndLength(bytes, offset: offset)
+    }
+
+    private static func readTagAndLength(_ bytes: [UInt8], offset: Int) -> TLV? {
+        guard offset < bytes.count else { return nil }
+        var pos = offset + 1 // skip tag byte
+        guard pos < bytes.count else { return nil }
+
+        let firstLen = bytes[pos]
+        pos += 1
+
+        let length: Int
+        if firstLen < 0x80 {
+            length = Int(firstLen)
+        } else {
+            let numBytes = Int(firstLen & 0x7F)
+            guard numBytes > 0, numBytes <= 4, pos + numBytes <= bytes.count else { return nil }
+            var len = 0
+            for i in 0..<numBytes {
+                len = (len << 8) | Int(bytes[pos + i])
+            }
+            pos += numBytes
+            length = len
+        }
+
+        return TLV(contentStart: pos, length: length)
+    }
+}
+
+enum SSLError: LocalizedError {
+    case noCertificate
+    case connectionFailed
+
+    var errorDescription: String? {
+        switch self {
+        case .noCertificate:
+            return "No certificate found"
+        case .connectionFailed:
+            return "Failed to connect to server"
+        }
+    }
+}
+
+final class SSLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendable {
+    private let lock = NSLock()
+    private var _serverTrust: SecTrust?
+
+    var serverTrust: SecTrust? {
+        lock.lock()
+        defer { lock.unlock() }
+        return _serverTrust
+    }
+
+    func urlSession(
+        _ session: URLSession,
+        didReceive challenge: URLAuthenticationChallenge,
+        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
+    ) {
+        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
+              let trust = challenge.protectionSpace.serverTrust else {
+            completionHandler(.performDefaultHandling, nil)
+            return
+        }
+
+        lock.lock()
+        _serverTrust = trust
+        lock.unlock()
+
+        let credential = URLCredential(trust: trust)
+        completionHandler(.useCredential, credential)
+    }
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..fd9f61b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Christian Cleberg
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/claude.md b/claude.md
new file mode 100644
index 0000000..4a90660
--- /dev/null
+++ b/claude.md
@@ -0,0 +1,79 @@
+# DomainDig – Project Context
+
+## What this is
+An iOS utility app for querying DNS records and inspecting SSL/TLS certificates
+for any domain. Designed for developers, sysadmins, and anyone who manages or
+troubleshoots domains and servers.
+
+## Target user
+Technically literate adults — developers, IT/sysadmin, homelab enthusiasts.
+Comfortable with terms like A record, CNAME, MX, TTL, and TLS. The UI should be
+clean and information-dense, not hand-holding.
+
+## Design principles
+- Minimal, utilitarian UI — this is a tool, not a consumer app
+- Results should be readable at a glance — use clear labels, monospace where
+  appropriate for IPs and raw values
+- Dark mode preferred as the primary aesthetic
+- No onboarding, no tutorials, no splash screens
+- Fast — results should appear as soon as they're available, not after all
+  lookups complete
+
+## Features
+
+### DNS Lookup
+Query the following record types for any domain via DNS-over-HTTPS (use
+Cloudflare 1.1.1.1 — https://cloudflare-dns.com/dns-query):
+- A
+- AAAA
+- MX
+- NS
+- TXT
+- CNAME
+
+Display each record type in its own section. Show TTL alongside each result.
+If a record type returns no results, show "No records found" for that type
+rather than hiding the section entirely.
+
+### SSL/TLS Certificate Check
+Connect to the domain on port 443 via URLSession and inspect the server's
+certificate chain using URLSession delegate methods. Display:
+- Common name (CN)
+- Subject Alternative Names (SANs)
+- Issuer
+- Valid from / Valid until dates
+- Days until expiry — highlight in red if under 30 days, yellow if under 60
+- Certificate chain depth
+
+### Results layout
+- Domain input at the top — large text field, keyboard shows on launch
+- Run button to trigger both DNS and SSL lookups simultaneously
+- DNS and SSL results displayed in clearly separated sections below
+- Each section loads independently — don't block SSL results waiting for DNS
+  or vice versa
+
+## Technical constraints
+- SwiftUI, iOS only
+- Fully offline except for DNS-over-HTTPS requests and SSL connections
+- No accounts, no analytics, no ads
+- No third-party dependencies — URLSession and Network framework only
+- Targets latest iOS
+
+### Recent searches
+Store the last 20 searched domains locally using UserDefaults. Display them as
+a tappable list below the text field when no results are showing. Tapping a
+recent domain populates the text field and runs the lookup immediately. Include
+a "Clear" button to wipe history. Most recent at the top.
+
+### Share / export
+A share button (SF Symbol: `square.and.arrow.up`) in the top-right of the
+results area. Formats the full DNS and SSL results as plain text and presents
+the iOS share sheet via ShareLink or UIActivityViewController. The export
+should include the domain, timestamp, all DNS records with TTLs, and all SSL
+cert fields.
+
+## What good looks like
+A developer pastes a domain, taps run, and within a couple of seconds sees a
+clean breakdown of every DNS record type and the full SSL cert status. It should
+feel like a native, polished version of running `dig` and `openssl s_client`
+from the terminal.
diff --git a/privacypolicy.md b/privacypolicy.md
new file mode 100644
index 0000000..e887ee7
--- /dev/null
+++ b/privacypolicy.md
@@ -0,0 +1,40 @@
+# Privacy Policy
+
+**DomainDig**
+Last updated: March 11, 2026
+
+## Overview
+
+DomainDig is a children's educational app designed for ages 4–5. This privacy
+policy explains how the app handles data.
+
+## Data Collection
+
+DomainDig does not collect, store, or transmit any personal data. The app has no
+user accounts, no analytics, no advertising, and no internet connection
+requirement.
+
+## Children's Privacy
+
+DomainDig is designed for young children and fully complies with the Children's
+Online Privacy Protection Act (COPPA). No information is collected from any
+user, including children.
+
+## Third-Party Services
+
+DomainDig does not use any third-party services, SDKs, or APIs that collect data.
+
+## Device Permissions
+
+DomainDig does not request access to your camera, microphone, contacts, location,
+or any other device data. Audio is generated on-device using Apple's built-in
+text-to-speech system.
+
+## Changes to This Policy
+
+If this policy changes in a future update, the updated policy will be posted
+here with a revised date.
+
+## Contact
+
+If you have any questions about this policy, contact: privacy@cleberg.net