Repository navigation
fix(platform-wallet): support HASH160 DashPay contact signing keys #5546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/platform-wallet-public-signing-key-selection
Are you sure you want to change the base?
Changes from 11 commits
d341fba
aa1df0a
0289c50
c424d19
26850a3
c756f54
7ff3acd
c3bd078
e25ae6f
27d145e
f0e0508
3a0b82a
3469dd0
ebcb38e
c047d5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,6 +58,12 @@ class KeystoreSigner( | |
| * Room rows and re-seed `pendingIdentityKeys`. | ||
| */ | ||
| private val onSigningKeyInvalidated: (suspend (pubkeyHex: String) -> Unit)? = null, | ||
| /** | ||
| * Whether Rust signs an identity key of a DPP `KeyType` discriminant. | ||
| * Rust owns that set; the parameter exists so a JVM unit test can stub | ||
| * it without the native library. | ||
| */ | ||
| private val supportsIdentityKeyType: (Int) -> Boolean = SignerNative::resolverSupportsKeyType, | ||
|
Comment on lines
+61
to
+66
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Do not expose an unchecked signer-capability override in the public constructor
source: |
||
| ) : NativeSignerBridge(), AutoCloseable { | ||
|
|
||
| private val handleRef = | ||
|
|
@@ -303,21 +309,23 @@ class KeystoreSigner( | |
| } | ||
|
|
||
| override fun canSignWith(pubkeyBytes: ByteArray, keyType: Int): Boolean = try { | ||
| if (keyType == PLATFORM_ADDRESS_HASH_KEY_TYPE) { | ||
| // Prerequisites for the derive-and-sign path: a row with a | ||
| // non-empty derivation path plus a stored wallet mnemonic. | ||
| // Existence-only — hasMnemonic never decrypts, so no plaintext | ||
| // is materialized for a mere capability check. | ||
| runBlocking { | ||
| platformAddressDao.getAllByAddressHash(pubkeyBytes).any { row -> | ||
| row.derivationPath.isNotEmpty() && storage.hasMnemonic(row.walletId) | ||
| when { | ||
| keyType == PLATFORM_ADDRESS_HASH_KEY_TYPE -> | ||
| // Prerequisites for the derive-and-sign path: a row with a | ||
| // non-empty derivation path plus a stored wallet mnemonic. | ||
| // Existence-only — hasMnemonic never decrypts, so no plaintext | ||
| // is materialized for a mere capability check. | ||
| runBlocking { | ||
| platformAddressDao.getAllByAddressHash(pubkeyBytes).any { row -> | ||
| row.derivationPath.isNotEmpty() && storage.hasMnemonic(row.walletId) | ||
| } | ||
| } | ||
| else -> identityKeySignable(keyType, supportsIdentityKeyType) { | ||
| // Ciphertext presence is not signing capability after Android | ||
| // replaces KEYS_ALIAS: the stale blob remains in DataStore but | ||
| // can never be decrypted by the replacement private key. | ||
| runBlocking { storage.isPrivateKeyDecryptable(storageKeyFor(pubkeyBytes)) } | ||
| } | ||
| } else { | ||
| // Ciphertext presence is not signing capability after Android | ||
| // replaces KEYS_ALIAS: the stale blob remains in DataStore but | ||
| // can never be decrypted by the replacement private key. | ||
| runBlocking { storage.isPrivateKeyDecryptable(storageKeyFor(pubkeyBytes)) } | ||
| } | ||
| } catch (_: Exception) { | ||
| false | ||
|
|
@@ -372,6 +380,21 @@ class KeystoreSigner( | |
| */ | ||
| private const val PLATFORM_ADDRESS_HASH_KEY_TYPE: Int = 0xFF | ||
|
|
||
| /** | ||
| * Whether [canSignWith] reports an identity key of DPP `KeyType` | ||
| * discriminant [keyType] signable. Rust picks signing keys by this | ||
| * answer: claiming a key the secp256k1 sign path cannot sign (BLS, | ||
| * EdDSA) would get it chosen and then signed with the wrong | ||
| * algorithm, so [supportsKeyType] (Rust's predicate) is asked first | ||
| * and [privateKeyDecryptable] only for a type it accepts. Factored | ||
| * pure so it is unit-testable without the native signer handle. | ||
| */ | ||
| internal fun identityKeySignable( | ||
| keyType: Int, | ||
| supportsKeyType: (Int) -> Boolean, | ||
| privateKeyDecryptable: () -> Boolean, | ||
| ): Boolean = supportsKeyType(keyType) && privateKeyDecryptable() | ||
|
|
||
| /** | ||
| * Structured completion code for a sign-path failure [t]: | ||
| * [KeyPermanentlyInvalidatedException] is a "key unavailable until | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package org.dashfoundation.dashsdk.security | ||
|
|
||
| import org.junit.Assert.assertEquals | ||
| import org.junit.Assert.assertFalse | ||
| import org.junit.Assert.assertTrue | ||
| import org.junit.Test | ||
|
|
||
| /** | ||
| * Pins how [KeystoreSigner.canSignWith] answers for an identity key. Rust | ||
| * chooses the signing key by that answer, so a key type Rust's predicate | ||
| * refuses (BLS, EdDSA) must not be reported signable even with a stored | ||
| * private key. Which types Rust signs is pinned by the Rust tests of | ||
| * `dash_sdk_resolver_supports_key_type`; this JVM test cannot load the | ||
| * native library, so it stubs the predicate. | ||
| */ | ||
| class KeystoreSignerKeyTypeTest { | ||
|
|
||
| @Test | ||
| fun refusesAKeyTypeRustDoesNotSignWithoutConsultingStorage() { | ||
| var storageConsulted = false | ||
| val signable = KeystoreSigner.identityKeySignable( | ||
| keyType = 1, | ||
| supportsKeyType = { false }, | ||
| ) { | ||
| storageConsulted = true | ||
| true | ||
| } | ||
| assertFalse(signable) | ||
| assertFalse(storageConsulted) | ||
| } | ||
|
|
||
| @Test | ||
| fun asksRustAboutTheKeyTypeItWasGiven() { | ||
| var askedFor: Int? = null | ||
| KeystoreSigner.identityKeySignable( | ||
| keyType = 2, | ||
| supportsKeyType = { | ||
| askedFor = it | ||
| true | ||
| }, | ||
| ) { true } | ||
| assertEquals(2, askedFor) | ||
| } | ||
|
|
||
| @Test | ||
| fun reportsAKeyTypeRustSignsSignableOnlyWhenItsKeyDecrypts() { | ||
| assertTrue(KeystoreSigner.identityKeySignable(0, { true }) { true }) | ||
| assertFalse(KeystoreSigner.identityKeySignable(0, { true }) { false }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Suggestion: Preserve the existing KeystoreSigner constructor ABI when adding the capability hook
Appending
supportsIdentityKeyTypeto the public primary constructor changes the JVM constructor from five arguments to six. Kotlin's default-argument machinery does not emit the previous five-argument constructor, so already-compiled consumers can fail withNoSuchMethodError; the default value is not binary compatibility. The change also makes the former lastonSigningKeyInvalidatedparameter no longer the trailing lambda target, so source callers usingKeystoreSigner(...){ ... }can bind the lambda to the new(Int) -> Booleanparameter and fail to compile. Add an explicit five-argument compatibility constructor and keep the capability injection behind an internal or test-only entry point.source:
gpt-6.1-sol(phase2-reviewer: general)