Skip to content

Conversation

@dantecatalfamo
Copy link
Member

@dantecatalfamo dantecatalfamo commented Jan 7, 2026

Related issue: Resolves #37567

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

Summary by CodeRabbit

Release Notes

  • New Features
    • Certificate tracking now includes additional metadata: validity dates and serial numbers for enhanced certificate lifecycle management and more detailed status reporting.

✏️ Tip: You can customize this high-level summary in your review settings.

@codecov
Copy link

codecov bot commented Jan 7, 2026

Codecov Report

❌ Patch coverage is 70.78652% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.80%. Comparing base (74b3288) to head (5a58671).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
.../java/com/fleetdm/agent/CertificateOrchestrator.kt 77.35% 6 Missing and 6 partials ⚠️
...d/app/src/main/java/com/fleetdm/agent/ApiClient.kt 23.07% 10 Missing ⚠️
...pp/src/main/java/com/fleetdm/agent/MainActivity.kt 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #38006      +/-   ##
==========================================
- Coverage   65.86%   65.80%   -0.06%     
==========================================
  Files        2389     2384       -5     
  Lines      190544   189022    -1522     
  Branches     8326     8402      +76     
==========================================
- Hits       125493   124390    -1103     
+ Misses      53641    53290     -351     
+ Partials    11410    11342      -68     
Flag Coverage Δ
android 42.64% <70.78%> (+1.49%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dantecatalfamo dantecatalfamo marked this pull request as ready for review January 8, 2026 16:08
@dantecatalfamo
Copy link
Member Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

Walkthrough

This PR extends Android certificate handling to capture and propagate certificate metadata (expiration dates and serial number) throughout the enrollment and status reporting pipeline. Metadata is extracted during SCEP enrollment, persisted in orchestrator state, and included when reporting certificate installation status to the server. UUID change handling is also added to detect and re-enroll certificates when server-assigned identifiers change.

Changes

Cohort / File(s) Summary
API and Data Models
android/app/src/main/java/com/fleetdm/agent/ApiClient.kt
Extended updateCertificateStatus() method signature to accept three optional date/serial metadata parameters (notAfter, notBefore, serialNumber). Updated UpdateCertificateStatusRequest data class to include corresponding string-serialized fields mapped to JSON keys not_valid_after, not_valid_before, and serial. Date values converted to ISO8601 format before transmission.
Enrollment Result
android/app/src/main/java/com/fleetdm/agent/CertificateEnrollmentHandler.kt
Extended EnrollmentResult.Success to carry certificate metadata fields (notAfter: Date, notBefore: Date, serialNumber: BigInteger) alongside existing alias.
Certificate Orchestration
android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt
Added ISO8601 date parsing/formatting utilities. Expanded CertificateState to persist metadata fields. Enhanced markCertificateUnreported() to accept and store metadata as ISO8601 strings. Updated cleanup logic to remove certificates when host UUID differs from stored UUID. Modified enrollment flow to propagate metadata from enrollment results through state storage. Enhanced status reporting retry path to reconstruct and pass metadata to API.
SCEP Client
android/app/src/main/java/com/fleetdm/agent/scep/ScepResult.kt, android/app/src/main/java/com/fleetdm/agent/scep/ScepClientImpl.kt
Extended ScepResult public data class with three new fields for certificate metadata. Updated ScepClientImpl to extract notBefore, notAfter, and serialNumber from the leaf certificate in SCEP response and populate these fields in returned ScepResult.
UI and Debugging
android/app/src/main/java/com/fleetdm/agent/MainActivity.kt
Added rendering of UUID field in DebugCertificateList alongside existing certificate display fields.
Test Utilities and Tests
android/app/src/test/java/com/fleetdm/agent/testutil/FakeCertificateApiClient.kt, android/app/src/test/java/com/fleetdm/agent/scep/MockScepClient.kt, android/app/src/test/java/com/fleetdm/agent/CertificateOrchestratorTest.kt
Updated test doubles to propagate metadata fields. FakeCertificateApiClient.updateCertificateStatus() extended to capture notAfter, notBefore, serialNumber in UpdateStatusCall. MockScepClient extracts metadata from generated certificates. Added test scenario for UUID change-triggered certificate re-enrollment.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PR #36139: Extends SCEP-related types (ScepClientImpl, ScepResult, CertificateEnrollmentHandler) to propagate certificate metadata, providing foundational data structures built upon in this PR.
  • PR #37198: Modifies certificate status reporting and orchestrator cleanup logic (ApiClient.updateCertificateStatus, certificate removal); this PR extends the same API surface to include metadata parameters.
  • PR #37640: Touches overlapping certificate orchestration, API surfaces, and test utilities (CertificateOrchestrator flows, ApiClient methods, test fake clients).

Suggested reviewers

  • mostlikelee
  • getvictor
  • sharon-fdm
🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning PR description is incomplete and lacks required sections from the repository template. Add missing checklist items including changes file, database migrations (if applicable), and verification steps for Android platform compatibility and auto-update functionality.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Android certificate renewal' clearly and concisely summarizes the primary change—implementing certificate renewal functionality on the Android platform.
Linked Issues check ✅ Passed The PR successfully implements the core coding requirements from #37567: UUID handling with certificate re-installation on UUID changes, extraction and sending of certificate metadata (notAfter, notBefore, serialNumber), and updated API signatures to support these features.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the #37567 objectives. The UI update to display UUID in the debug screen, test infrastructure updates, and data model expansions all support the core certificate renewal and UUID-based re-enrollment requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 37567-android-renewal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt (1)

615-622: Missing error handling for parsing operations.

If notAfter, notBefore, or serialNumber contain malformed data (e.g., corrupted storage), parseISO8601() throws IllegalArgumentException and BigInteger() throws NumberFormatException. This would crash the entire retry loop, preventing status reporting for other certificates.

🐛 Suggested fix with error handling
             val result = apiClient.updateCertificateStatus(
                 certificateId = certId,
                 status = UpdateCertificateStatusStatus.VERIFIED,
                 operationType = operationType,
-                notAfter = state.notAfter?.let { parseISO8601(it) },
-                notBefore = state.notBefore?.let { parseISO8601(it) },
-                serialNumber = state.serialNumber?.let { BigInteger(it) },
+                notAfter = state.notAfter?.let { 
+                    runCatching { parseISO8601(it) }.getOrNull() 
+                },
+                notBefore = state.notBefore?.let { 
+                    runCatching { parseISO8601(it) }.getOrNull() 
+                },
+                serialNumber = state.serialNumber?.let { 
+                    runCatching { BigInteger(it) }.getOrNull() 
+                },
             )
🤖 Fix all issues with AI agents
In @android/app/src/main/java/com/fleetdm/agent/scep/ScepClientImpl.kt:
- Line 20: Remove the unused import kotlin.math.exp from ScepClientImpl.kt;
locate the import statement at the top of the file and delete the line importing
kotlin.math.exp so there are no unused imports in the ScepClientImpl class.
🧹 Nitpick comments (2)
android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt (2)

74-83: Potential parse exception handling issue.

SimpleDateFormat.parse() throws ParseException on invalid input rather than returning null. The null-coalescing operator won't catch parsing failures - the exception will propagate.

Additionally, the format "yyyy-MM-dd'T'HH:mm:ss'Z'" doesn't handle fractional seconds (e.g., "2025-01-01T12:00:00.123Z"), which could cause parsing failures if the server sends dates with milliseconds.

🛠️ Suggested fix with proper exception handling and flexible parsing
     private fun parseISO8601(dateString: String): Date {
-        val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
-        dateFormat.timeZone = TimeZone.getTimeZone("UTC")
-        return dateFormat.parse(dateString) ?: throw IllegalArgumentException("Invalid ISO8601 date: $dateString")
+        val formats = listOf(
+            "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+            "yyyy-MM-dd'T'HH:mm:ss'Z'"
+        )
+        for (pattern in formats) {
+            try {
+                val dateFormat = SimpleDateFormat(pattern, Locale.US)
+                dateFormat.timeZone = TimeZone.getTimeZone("UTC")
+                return dateFormat.parse(dateString)!!
+            } catch (e: Exception) {
+                // Try next format
+            }
+        }
+        throw IllegalArgumentException("Invalid ISO8601 date: $dateString")
     }

675-680: Consider documenting or handling placeholder values more explicitly.

Returning Date(0) (epoch: 1970-01-01) and BigInteger.ZERO as placeholders when skipping enrollment is functional but could lead to confusion. Callers receiving these values might not know they're placeholders rather than actual certificate metadata.

Consider either:

  1. Documenting in the EnrollmentResult.Success class that these values may be placeholders
  2. Using nullable types in EnrollmentResult.Success for optional metadata
  3. Adding a boolean flag like isPlaceholder to distinguish skip vs. actual enrollment
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5059107 and 16b813e.

📒 Files selected for processing (9)
  • android/app/src/main/java/com/fleetdm/agent/ApiClient.kt
  • android/app/src/main/java/com/fleetdm/agent/CertificateEnrollmentHandler.kt
  • android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt
  • android/app/src/main/java/com/fleetdm/agent/MainActivity.kt
  • android/app/src/main/java/com/fleetdm/agent/scep/ScepClientImpl.kt
  • android/app/src/main/java/com/fleetdm/agent/scep/ScepResult.kt
  • android/app/src/test/java/com/fleetdm/agent/CertificateOrchestratorTest.kt
  • android/app/src/test/java/com/fleetdm/agent/scep/MockScepClient.kt
  • android/app/src/test/java/com/fleetdm/agent/testutil/FakeCertificateApiClient.kt
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: getvictor
Repo: fleetdm/fleet PR: 36139
File: android/app/src/main/java/com/fleetdm/agent/scep/ScepClientImpl.kt:75-76
Timestamp: 2025-11-26T18:58:18.865Z
Learning: In Fleet's Android MDM agent SCEP implementation (android/app/src/main/java/com/fleetdm/agent/scep/ScepClientImpl.kt), OptimisticCertificateVerifier is intentionally used because: (1) SCEP URL is provided by authenticated MDM server, (2) challenge password authenticates enrollment, (3) enterprise SCEP servers use internal CAs not in system trust stores, (4) enrolled certificate is validated when used.
Learnt from: getvictor
Repo: fleetdm/fleet PR: 37640
File: android/app/src/main/java/com/fleetdm/agent/ApiClient.kt:518-522
Timestamp: 2026-01-02T22:48:09.865Z
Learning: In the Fleet Android app's SCEP proxy URL format (android/app/src/main/java/com/fleetdm/agent/ApiClient.kt), the `g` prefix before the certificate ID (e.g., `g$id`) is intentional and stands for "Google Android" to differentiate from other platforms like Apple and Windows.
📚 Learning: 2025-11-26T18:58:18.865Z
Learnt from: getvictor
Repo: fleetdm/fleet PR: 36139
File: android/app/src/main/java/com/fleetdm/agent/scep/ScepClientImpl.kt:75-76
Timestamp: 2025-11-26T18:58:18.865Z
Learning: In Fleet's Android MDM agent SCEP implementation (android/app/src/main/java/com/fleetdm/agent/scep/ScepClientImpl.kt), OptimisticCertificateVerifier is intentionally used because: (1) SCEP URL is provided by authenticated MDM server, (2) challenge password authenticates enrollment, (3) enterprise SCEP servers use internal CAs not in system trust stores, (4) enrolled certificate is validated when used.

Applied to files:

  • android/app/src/test/java/com/fleetdm/agent/CertificateOrchestratorTest.kt
  • android/app/src/main/java/com/fleetdm/agent/scep/ScepResult.kt
  • android/app/src/main/java/com/fleetdm/agent/CertificateEnrollmentHandler.kt
  • android/app/src/test/java/com/fleetdm/agent/scep/MockScepClient.kt
  • android/app/src/main/java/com/fleetdm/agent/scep/ScepClientImpl.kt
  • android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt
📚 Learning: 2026-01-02T22:48:09.865Z
Learnt from: getvictor
Repo: fleetdm/fleet PR: 37640
File: android/app/src/main/java/com/fleetdm/agent/ApiClient.kt:518-522
Timestamp: 2026-01-02T22:48:09.865Z
Learning: In the Fleet Android app's SCEP proxy URL format (android/app/src/main/java/com/fleetdm/agent/ApiClient.kt), the `g` prefix before the certificate ID (e.g., `g$id`) is intentional and stands for "Google Android" to differentiate from other platforms like Apple and Windows.

Applied to files:

  • android/app/src/main/java/com/fleetdm/agent/scep/ScepResult.kt
  • android/app/src/main/java/com/fleetdm/agent/CertificateEnrollmentHandler.kt
  • android/app/src/main/java/com/fleetdm/agent/ApiClient.kt
  • android/app/src/main/java/com/fleetdm/agent/scep/ScepClientImpl.kt
  • android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt
📚 Learning: 2026-01-02T23:15:16.518Z
Learnt from: getvictor
Repo: fleetdm/fleet PR: 37640
File: android/app/src/main/java/com/fleetdm/agent/AgentApplication.kt:76-84
Timestamp: 2026-01-02T23:15:16.518Z
Learning: In the Fleet Android agent (android/app/src/main/java/com/fleetdm/agent/AgentApplication.kt), the enrollSecret is designed as a one-time credential. If an API key becomes invalid, the intended workflow is for customers to manually unenroll and re-enroll the device rather than automatically re-enrolling when credentials change. The logic that skips enrollment when ApiClient.getApiKey() != null is intentional.

Applied to files:

  • android/app/src/main/java/com/fleetdm/agent/CertificateEnrollmentHandler.kt
  • android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt
🧬 Code graph analysis (1)
android/app/src/test/java/com/fleetdm/agent/CertificateOrchestratorTest.kt (1)
server/datastore/mysql/migrations/tables/20251124140138_CreateTableCertifcatesTemplates_test.go (1)
  • CertificateTemplateResult (33-39)
🔇 Additional comments (18)
android/app/src/main/java/com/fleetdm/agent/ApiClient.kt (4)

31-40: LGTM! ISO8601 date formatting looks correct.

The extension function correctly formats dates to UTC ISO8601 format. Creating a new SimpleDateFormat instance per call avoids thread-safety issues with the class.


55-63: Interface extension looks good.

The new optional parameters with null defaults maintain backward compatibility for any existing callers.


270-288: Implementation correctly propagates certificate metadata.

The null-safe conversions (?.toISO8601String(), ?.toString()) properly handle optional values before sending to the API.


470-476: Data class fields correctly mapped to API contract.

The @SerialName annotations properly map Kotlin field names to the expected API field names (not_valid_after, not_valid_before, serial).

android/app/src/main/java/com/fleetdm/agent/MainActivity.kt (1)

233-238: LGTM! Debug display addition is straightforward.

The UUID field is correctly added to the debug certificate list view, providing visibility into the certificate identifier for debugging purposes.

android/app/src/test/java/com/fleetdm/agent/scep/MockScepClient.kt (1)

63-75: LGTM! Mock correctly mirrors production metadata extraction.

The mock implementation properly extracts certificate metadata from the generated self-signed certificate, ensuring tests exercise the same data flow as production code.

android/app/src/main/java/com/fleetdm/agent/scep/ScepClientImpl.kt (1)

101-113: LGTM! Metadata extraction is well-structured.

The cast to X509Certificate is safe since SCEP servers return X.509 certificates, and the empty-list check on lines 97-99 ensures certificates.first() won't throw.

android/app/src/main/java/com/fleetdm/agent/CertificateEnrollmentHandler.kt (2)

31-35: LGTM! EnrollmentResult.Success properly extended.

The sealed class properly captures certificate metadata for propagation to the orchestrator layer. The fields match the non-null ScepResult properties.


51-57: Clean metadata passthrough from SCEP result to enrollment result.

The values are correctly extracted from the SCEP result and forwarded without transformation.

android/app/src/main/java/com/fleetdm/agent/scep/ScepResult.kt (1)

17-23: LGTM! Well-documented data class expansion.

The ScepResult now captures essential certificate metadata with proper KDoc documentation. Non-null fields are appropriate since a successful SCEP enrollment always yields a complete certificate with all these attributes.

android/app/src/test/java/com/fleetdm/agent/testutil/FakeCertificateApiClient.kt (2)

13-21: LGTM! Test utility correctly updated.

The UpdateStatusCall data class properly captures the new metadata fields for test assertions. Nullable types correctly match the production interface.


44-64: LGTM! Method signature and call recording correctly extended.

The fake client properly records all parameters including the new metadata fields, enabling comprehensive test assertions.

android/app/src/test/java/com/fleetdm/agent/CertificateOrchestratorTest.kt (2)

927-932: LGTM! Clear comment explaining the updated cleanup behavior.

The comment accurately explains why 2 cleanup results are expected: certificate 14 has operation="remove", and certificate 20 has a UUID mismatch (null != "new-uuid"). The assertions correctly verify both certificates are processed.


959-1073: LGTM! Comprehensive test for certificate renewal via UUID change.

This test thoroughly validates the UUID-based reinstallation flow:

  1. Certificate is already installed with old UUID
  2. Cleanup detects UUID mismatch and removes the certificate
  3. Enrollment reinstalls with new UUID
  4. All API calls and state transitions are verified

Good coverage of the critical renewal scenario described in issue #37567.

android/app/src/main/java/com/fleetdm/agent/CertificateOrchestrator.kt (4)

351-354: LGTM! Core logic for UUID-based certificate renewal.

The filter correctly identifies certificates for removal when:

  1. operation == "remove" (explicit removal request), OR
  2. Stored UUID doesn't match requested UUID (triggers reinstallation)

This enables the renewal flow where a UUID change causes the certificate to be uninstalled during cleanup and reinstalled during enrollment.


528-551: LGTM! Clean extension to persist certificate metadata for retry.

The optional parameters allow storing certificate metadata (expiration dates, serial number) alongside the unreported status. This enables retryUnreportedStatuses to include the metadata when retrying the API call, fulfilling the PR objective to include certificate expiration date in status reports.


736-759: LGTM! Proper metadata propagation for successful enrollment.

The code correctly:

  1. Converts certificate metadata to strings for DataStore persistence
  2. Stores metadata with the unreported status for retry capability
  3. Passes the original typed values (Date, BigInteger) to the API call

This fulfills the PR objective to include notAfter, notBefore, and serialNumber when reporting certificate installation status.


889-894: LGTM! Clean data model extension for certificate metadata.

The new optional fields properly extend CertificateState while maintaining backward compatibility with existing stored data (via null defaults and explicitNulls = false in the JSON configuration). Using String? for date storage is appropriate given the ISO8601 serialization approach.

Copy link
Member

@getvictor getvictor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. My main concern is that we don't want to remove a cert before we fetch its replacement.

@dantecatalfamo
Copy link
Member Author

@getvictor Changes made 👍

@dantecatalfamo dantecatalfamo merged commit 2c962f0 into main Jan 9, 2026
16 checks passed
@dantecatalfamo dantecatalfamo deleted the 37567-android-renewal branch January 9, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android renewal: Android app

3 participants