Skip to content

Conversation

@aj-rosado
Copy link
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-22479

📔 Objective

Added method to return signature from current certificate and past signing certificates from signingCertificateHistory when hasMultipleSigners returns false.
Using it to validate AssetLinks as some .well-known might not have the most recent certificate.

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@claude
Copy link
Contributor

claude bot commented Nov 18, 2025

Claude finished @aj-rosado's task —— View job


Code Review Complete

Overall Assessment: APPROVE

This PR correctly implements certificate history validation for asset links, addressing scenarios where apps have rotated their signing certificates. The implementation is secure, well-tested, and follows established patterns.

Key Strengths:

  • Proper handling of certificate rotation via signingCertificateHistory
  • Comprehensive test coverage including edge cases
  • Maintains existing error differentiation logic
  • Efficient MessageDigest reuse in the new extension function

Findings:

Finding 1: Consider adding a clarifying comment about certificate ordering assumptions in OriginManagerImpl.kt:50

Finding 2: The assetLinkFound flag logic correctly differentiates between "asset link found but no match" vs "asset link not found" - verify this distinction provides value in error messaging

PR Description Quality:

  • ✅ Clear objective explaining the change
  • ✅ Jira reference included
  • ✅ Reminders checklist present
  • ℹ️ Consider adding: Brief explanation of why .well-known files might not have the most recent certificate (helps future reviewers understand the use case)

Security Considerations:
The change enhances security by properly supporting certificate rotation, allowing apps to maintain valid asset links during key rotation periods. The implementation correctly:

  • Validates against the signing certificate history (not just current certificate)
  • Returns early on first match (performance optimization)
  • Maintains proper error states for security failures

Technical Compliance:

  • ✅ Follows MVVM + manager patterns per architecture guidelines
  • ✅ Uses Result types and sealed classes for error handling
  • ✅ Proper KDoc documentation on new public extension function
  • ✅ Hilt DI patterns maintained
  • ✅ Comprehensive JUnit 5 + MockK tests

See inline comments for specific details.


b.toHexString(HexFormat.UpperCase)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨 Code Quality Suggestion

Consider caching MessageDigest instance

Creating a new MessageDigest instance inside the loop means it's recreated for each signature in the history. Since MessageDigest instances are reusable after calling digest(), consider creating it once outside the map function:

@OptIn(ExperimentalStdlibApi::class)
fun CallingAppInfo.getAllSignatureFingerprintsAsHexStrings(): List<String> {
    if (signingInfo.hasMultipleSigners()) return emptyList()

    val md = MessageDigest.getInstance(SHA_ALGORITHM)
    return signingInfo.signingCertificateHistory.map { signature ->
        val hash = md.digest(signature.toByteArray())
        hash.joinToString(":") { b ->
            b.toHexString(HexFormat.UpperCase)
        }
    }
}

This is already implemented correctly - the md instance is created once and reused. This is more efficient than the previous single-signature approach that created the digest inline.

return ValidateOriginResult.Error.PasskeyNotSupportedForApp
}

var assetLinkFound = false
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 Question about error differentiation logic

Why track assetLinkFound separately?

The assetLinkFound flag differentiates between two scenarios:

  1. Asset link API succeeded but none matched (linked = false) → PasskeyNotSupportedForApp
  2. Asset link API failed (network/server error) for all attempts → AssetLinkNotFound

Question: Is this distinction important for the user experience? Both scenarios result in validation failure.

The logic appears correct, but consider:

  • If the asset link service returns linked = false, it means the .well-known file was found but doesn't contain a matching certificate
  • If the service throws an exception, it means the .well-known file wasn't found or is inaccessible

This distinction helps provide more accurate error messages to the user about why passkey authentication failed.

var assetLinkFound = false

// Check each fingerprint in the signing certificate history
return fingerprints
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Security consideration: Certificate ordering

Verify certificate history ordering assumptions

The code iterates through signingCertificateHistory using firstNotNullOfOrNull, which returns as soon as a match is found. This assumes the certificate history is ordered in a specific way.

Questions:

  1. What is the ordering of signingInfo.signingCertificateHistory? (oldest-to-newest or newest-to-oldest?)
  2. Should we prioritize checking the current/newest certificate first for performance?

According to Android documentation, signingCertificateHistory contains the signing certificates with the first entry being the current signer. The current implementation will check certificates in order from current to historical, which is correct.

Recommendation: Add a comment clarifying this ordering assumption:

// Check each fingerprint in the signing certificate history
// Note: signingCertificateHistory is ordered from current to historical certificates
return fingerprints

Copy link
Contributor

Choose a reason for hiding this comment

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

Something about this is making my Spidey senses tingle. 🤔

I think checking every cert in the chain is going to make us susceptible to APK signing replay attacks.

Since the first certificate is the latest, it may be more secure to only check it and ignore others in the chain. That would also make this solution simpler. We can remove the if (signingInfo.hasMultipleSigners()) return null statement from getAppSigningSignatureFingerprint and be done. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is what we are currently doing and some apps do not have the most recent certificate fingerprint on the well-known file, but have an older one, that is the cause of this issue.

getSigningCertificateHistory should only be used if hasMultipleSigners is false. We were already doing this hasMultipleSigners validation.

Checking all the signatures looks like the intended behaviour from android

When determining if a package is signed by a desired certificate, the returned array should be checked to determine if it is one of the entries.

🤔 might be worth to also check getApkContentsSigners when multipleSigners returns true

Copy link
Contributor

Choose a reason for hiding this comment

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

Discussed offline. There are concerns about the mismatch between WhatsApp's assetlinks.json and the app signature being detected.

Accepting a valid certificate signature from anywhere within the chain is a potential vulnerability. The suggested solution at this time is to improve our user-facing error message in this scenario, and reject the request. Additionally, we are reaching out to WhatsApp so we can better understand why key rotation was performed and why their assetlinks.json has not been updated to include the latest key signature.

ValidateOriginResult.Success(null),
result,
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 Excellent test coverage

The test suite comprehensively covers the new functionality:

  • ✅ Multiple certificates in history with fallback validation (line 249)
  • ✅ Early return on first match (line 320)
  • ✅ All certificates fail validation (line 381)
  • ✅ Multiple signers edge case (line 451)

This demonstrates thorough thinking about edge cases and validation scenarios.

@github-actions
Copy link
Contributor

Logo
Checkmarx One – Scan Summary & Details994bfe1c-0014-4783-8d08-2eccd44e17ad

Great job! No new security vulnerabilities introduced in this pull request

@github-actions
Copy link
Contributor

Warning

@aj-rosado Uploading code coverage report failed. Please check the "Upload to codecov.io" step of Process Test Reports job for more details.

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.

3 participants