test: record partial native bridge import control
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# Native bridge import control — inspected design
|
||||
|
||||
Status: Astra approved the refined ABI/ownership design for implementation. The
|
||||
C child, separate seed and collector are implemented and submitted for actual-code
|
||||
review. The original assumptions below are retained to explain the exact attempt.
|
||||
[Attempt A](NATIVE_IMPORT_ATTEMPT.md) stopped at its first partial operation: the
|
||||
installed wrapper permits null item associations, contrary to the original fixed
|
||||
identity-association expectation. It produced no repeated retention result. The coordinator confirmed a
|
||||
single staged native-bridge control; a handwritten Apple import reproduction is
|
||||
deferred until the result shows whether it is useful. This is not a production
|
||||
routing/crypto change, a mitigation or an acceptance decision.
|
||||
|
||||
## Installed binaries and retained inspection
|
||||
|
||||
Installed managed crypto:
|
||||
`/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App/10.0.14/System.Security.Cryptography.dll`.
|
||||
Informational version `10.0.14+0427b9abbead3fa64c6655eb4290c537397131fa`;
|
||||
SHA-256 `f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a`.
|
||||
|
||||
Installed native bridge:
|
||||
`/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App/10.0.14/libSystem.Security.Cryptography.Native.Apple.dylib`.
|
||||
SHA-256 `0333994e217a3d111d36d7f31460eea69a286f9fc819e2d7c746df13d2aec46c`.
|
||||
Only this exact, unmodified dylib would be loaded by absolute path; required
|
||||
exports and actual loaded-image identity must match. No SDK/CLR may be loaded in
|
||||
the measured C child. Its native bridge dependency is explicitly part of this
|
||||
experiment, unlike the preceding plain-Apple negative control.
|
||||
|
||||
Retained actual-binary inspection files (read-only ILSpy 10.1.0.8386 / otool):
|
||||
|
||||
| File | Relevant locations |
|
||||
|---|---|
|
||||
| `/tmp/opcua2-native-runtime-loader-decompiled.cs` | 836–870 load certs/keys/match, initialize import state and FromCertAndKey; 1555–1581 exportable branch; 1611–1640 keychain initialization and GetPrivateKey |
|
||||
| `/tmp/opcua2-native-runtime-pkcs12key-decompiled.cs` | AsymmetricAlgorithmPkcs12PrivateKey imports PKCS8 into its RSA algorithm |
|
||||
| `/tmp/opcua2-native-runtime-pkcs12key-base-decompiled.cs` | Dispose calls the algorithm's Dispose |
|
||||
| `/tmp/opcua2-native-runtime-rsa-decompiled.cs` | 95–108 ImportParameters/CreateDataKey/CopyPublicKey; 431–458 PKCS1 serialization to CreateDataKey; export uses native external representation |
|
||||
| `/tmp/opcua2-native-runtime-applepal-decompiled.cs` | 339–349 identity constructor/certificate copy and disposal; 555–564 MoveToKeychain |
|
||||
| `/tmp/opcua2-native-runtime-appleinterop-decompiled.cs` | PAL RSA enum 544–549; CreateDataKey/external representation/public copy 1982–2127; keychain create/never-lock 3054/3209/3481; legacy key import 3563/3596; cert import 3697/3944; move 3878/4036; identity cert copy 2629/4087 |
|
||||
| `/tmp/opcua2-native-runtime-certhandle-decompiled.cs` and `identityhandle-decompiled.cs` | Both actual types inherit SafeKeychainItemHandle with no release override |
|
||||
| `/tmp/opcua2-native-runtime-keychainitem-decompiled.cs` | SafeKeychainItemHandle release: UntrackItem copy/release, then CFRelease; certificate and identity inherit this class |
|
||||
| `/tmp/opcua2-native-runtime-keypair-decompiled.cs` | Actual SecKeyPair Dispose: modern private-key owner before modern public-key owner |
|
||||
| `/tmp/opcua2-native-runtime-keyhandle-decompiled.cs` | SafeSecKeyRefHandle release: straight CFRelease, then optional managed parent release; no tracking copy |
|
||||
| `/tmp/opcua2-native-runtime-keychain-decompiled.cs` | TrackItem/UntrackItem each copy/release a keychain; matching dictionary only adds/releases managed SafeHandle ownership |
|
||||
| `/tmp/opcua2-native-runtime-x509contenttype-decompiled.cs` | Actual public Cert enum value 1; Pfx/Pkcs12 value 3 |
|
||||
| `/tmp/opcua2-native-runtime-apple-disassembly.txt` | `_AppleCryptoNative_SecKeyImportEphemeral`, `_AddKeyToKeychain`, `_AppleCryptoNative_X509MoveToKeychain`, all required exports |
|
||||
|
||||
The installed native call sites, not merely the public runtime tag, show:
|
||||
|
||||
- `SecKeyImportEphemeral` at `0x92e8` calls `SecItemImport` at `0x9390`,
|
||||
followed by CF ownership cleanup.
|
||||
- `AddKeyToKeychain` calls `SecItemExport` at `0xa0a8` and `SecItemImport` at
|
||||
`0xa11c`, then retains an optional returned key and releases local containers.
|
||||
- `X509MoveToKeychain` at `0xa180` calls AddKeyToKeychain at `0xa240`,
|
||||
`SecCertificateAddToKeychain` at `0xa278`, and a private-search-list identity
|
||||
`SecItemCopyMatching` at `0xa388`, with CF cleanup and one returned identity.
|
||||
|
||||
Public `v10.0.14` native headers/source are explanatory cross-checks, not asserted
|
||||
as the exact installed source commit. In particular this **Exportable** path is
|
||||
managed PKCS12 decode / RSA materialization → legacy key import → move-to-keychain.
|
||||
A guessed `SecPKCS12Import` control would exercise a different path.
|
||||
|
||||
## ABI and returned ownership map
|
||||
|
||||
All bridge integer return values below are signed 32-bit. The installed generated
|
||||
managed keychain-create declaration takes signed `int utf8PassphraseLength`;
|
||||
the explanatory public native header takes uint32. The control uses the same
|
||||
fixed positive bounded value 129, whose ABI representation is identical. Other
|
||||
byte lengths are signed 32-bit. Native
|
||||
pointers are opaque CF references and must never be printed. Every out location
|
||||
starts null/zero; each non-null owned output must be released once on success or
|
||||
failure. Borrowed CFData byte pointers are valid only while their owned CFData is
|
||||
held and are never separately freed. CFError is an owned output, recorded only
|
||||
by code and released once, without description/key logging.
|
||||
|
||||
| Exact export | Argument/return contract |
|
||||
|---|---|
|
||||
| `AppleCryptoNative_X509ImportCertificate` | `(uint8_t*, int32 len, int32 contentType=1, CFStringRef NULL, SecKeychainRef NULL, int32 exportable=1, SecCertificateRef*, SecIdentityRef*, int32 OSStatus*)`; bridge return 1 and OSStatus 0 required; DER must yield an owned cert and no identity |
|
||||
| `AppleCryptoNative_SecKeyCreateWithData` | `(uint8_t*, int32 len, uint32 PAL_RSA=2, int32 isPublic=0, SecKeyRef*, CFErrorRef*)`; bridge return 1, owned modern RSA key |
|
||||
| `AppleCryptoNative_SecKeyCopyPublicKey` | `(SecKeyRef) -> SecKeyRef`; owned public-key copy, non-null required |
|
||||
| `AppleCryptoNative_SecKeyCopyExternalRepresentation` | `(SecKeyRef, CFDataRef*, CFErrorRef*)`; bridge return 1, owned PKCS1 private-key data, no key bytes logged |
|
||||
| `AppleCryptoNative_SecKeychainCreate` | `(const char*, int32 passwordLength=129 in installed managed ABI, const uint8_t*, SecKeychainRef*)`; OSStatus 0, owned fresh private keychain; same native false prompt flag |
|
||||
| `AppleCryptoNative_SetKeychainNeverLock` | `(SecKeychainRef) -> OSStatus`; 0 required; affects only the generated private keychain, matching installed import state |
|
||||
| `AppleCryptoNative_SecKeyImportEphemeral` | `(uint8_t*, int32 len, int32 isPrivate=1, SecKeyRef*, int32 OSStatus*)`; bridge return 1 / OSStatus 0, owned legacy private key |
|
||||
| `AppleCryptoNative_X509MoveToKeychain` | `(SecCertificateRef, SecKeychainRef, SecKeyRef, SecIdentityRef*, int32 OSStatus*)`; bridge return 1 / OSStatus 0 and owned non-null identity required; native helper scopes its identity query to target keychain and supplied cert |
|
||||
| `AppleCryptoNative_X509CopyCertFromIdentity` | `(SecIdentityRef, SecCertificateRef*) -> OSStatus`; 0 required, owned certificate copy |
|
||||
| `AppleCryptoNative_SecKeychainItemCopyKeychain` | `(CFTypeRef item, SecKeychainRef*) -> int32`; bridge-normalized OSStatus 0 required; non-null returned keychain is owned and CFReleased once. The installed bridge normalizes native -25304/-25294 to 0; null output is valid for an unassociated item. Preserve normalized status and output-present flag, not raw underlying status. |
|
||||
| `AppleCryptoNative_SecKeychainDelete` | `(SecKeychainRef) -> OSStatus`; recorded separately from subsequent CFRelease; only a returned keychain verified against this generated private path may be deleted |
|
||||
|
||||
## Matching native interactions from managed wrapper tracking
|
||||
|
||||
The child will include the bridge-normalized `SecKeychainItemCopyKeychain` calls
|
||||
made by installed wrappers, not merely the primary import/move exports. Every
|
||||
non-null copied keychain is released once. The map is:
|
||||
|
||||
1. After DER import, TrackItem(cert): copy/release its associated keychain.
|
||||
The null identity output is skipped, as the managed TrackItem does.
|
||||
2. Around MoveToKeychain: acquire the outer cert-keychain copy and keep it alive
|
||||
across the entire move wrapper; separately copy/release cert's old keychain
|
||||
for UntrackItem; call Move; copy/release cert's new keychain and identity's
|
||||
keychain for the two TrackItem calls; then release the outer copy.
|
||||
3. After identity certificate copy: copy/release copied cert's keychain for
|
||||
TrackItem.
|
||||
4. Releasing initial cert, copied cert and identity: copy/release each item's
|
||||
keychain first (UntrackItem), then CFRelease the item. This preserves the
|
||||
installed PAL order: initial cert after the replacement PAL is constructed,
|
||||
then replacement cert before identity at disposal.
|
||||
5. Modern public/private and legacy private `SafeSecKeyRefHandle` release maps
|
||||
directly to CFRelease, with no keychain tracking native call. Its optional
|
||||
managed parent lifetime is not used by the inspected GetPrivateKey path.
|
||||
|
||||
No managed dictionary is recreated. A small logical owner counter preserves the
|
||||
known deterministic ownership/deletion order. It starts at one import-state
|
||||
owner. Successful TrackItem of an item associated with the generated keychain
|
||||
adds one; UntrackItem subtracts one. These logical increments are not CF retains.
|
||||
After the replacement PAL is constructed and the original cert is disposed,
|
||||
ImportState.Dispose relinquishes its initial logical ownership. The counter then
|
||||
contains only copied-cert and identity owners. Their Untrack calls subtract one
|
||||
before releasing each copied-keychain reference. The final identity Untrack
|
||||
reaches zero: it calls SecKeychainDelete and releases the initial native keychain
|
||||
owner **while the copied keychain ref and identity are still alive**, then
|
||||
releases the copied keychain ref, then CFReleases the identity. This matches the
|
||||
installed SafeHandle delayed-release ordering; deliberately postponing deletion
|
||||
to after all item CFReleases would be a different reduced sequence. The legacy
|
||||
private key is released explicitly after this sequence (the managed helper lacks
|
||||
an explicit local using scope); exact managed finalizer timing is not emulated.
|
||||
The installed bridge at `0x8a04` checks item type, calls the public Apple copy API,
|
||||
and normalizes -25304/-25294 to success. The control calls that pinned export
|
||||
rather than interpreting a raw public return differently. Any other returned
|
||||
status fails the operation but is retained in the receipt.
|
||||
|
||||
## Proposed one-child sequence and boundaries
|
||||
|
||||
Prepare one fresh normal SDK-generated RSA fixture in a **separate seed process**,
|
||||
not in the measured child. Export its public DER certificate and RSA PKCS1 private
|
||||
key through public APIs into a private directory with files mode 0600. Preserve
|
||||
only public identity/source/binary hashes, never private bytes/passwords. The seed
|
||||
and measured child must be reaped before their key material is overwritten and
|
||||
unlinked; cleanup is confined to their fresh owned roots. No existing PKI is used.
|
||||
|
||||
One plain C child would then repeat exactly 200 times, without count/retry
|
||||
extension:
|
||||
|
||||
1. Import public DER through the pinned bridge. Materialize the modern RSA key
|
||||
from PKCS1 through CreateWithData and obtain the owned public-key copy, matching
|
||||
the inspected RSA loader prelude without reproducing managed ASN1/PFX matching.
|
||||
2. Create one generated unrelated private keychain path and set its never-lock
|
||||
behavior through the pinned bridge. Use generated password bytes and no prompt;
|
||||
verify its returned path is exactly the generated private filename/backing file.
|
||||
3. Export modern private-key representation, import the legacy ephemeral key,
|
||||
then release exported CFData and modern RSA private/public owners in that order, as the
|
||||
managed Pkcs12Key is disposed before MoveToKeychain.
|
||||
4. Move certificate/key to that private keychain; require a typed identity.
|
||||
Copy its certificate as the AppleCertificatePal identity constructor does;
|
||||
release the original certificate. At first/last only, check returned public
|
||||
certificate identity against the input; no secret/private-key logging.
|
||||
5. Relinquish the initial import-state logical owner after original cert
|
||||
disposal. Release copied cert (Untrack/copy-release then cert CFRelease), then
|
||||
identity. Its final Untrack deletes/releases the verified initial keychain
|
||||
owner before releasing that Untrack copy and identity, as mapped above.
|
||||
Release the explicitly held legacy key. Preserve every bridge
|
||||
return, OSStatus, CFError code, owned-output/release count and failure path.
|
||||
|
||||
The modern/legacy distinction is explicit. The installed managed helper's local
|
||||
legacy SafeSecKeyRef has no explicit using scope in FromCertAndKey; the native
|
||||
control uses balanced explicit CF owners and does **not** claim that this is a
|
||||
proven GC bug, or exactly emulate managed finalizer timing.
|
||||
|
||||
Use the already accepted before/first/200/five-second-idle checkpoints, raw native
|
||||
heap inventory, Mach RSS, total/owned-keychain FDs and backing-file counts, and
|
||||
read-only successful default/search-list fingerprint equality. No list setter or
|
||||
restore call. Keep fixed parent/process/probe/reap bounds, complete JSONL parsing,
|
||||
private generated paths, fail-closed ABI/hash/image checks and child reaping.
|
||||
|
||||
A positive result would show that managed caller participation is not necessary
|
||||
for this observed import-path retention, while retaining the exact installed
|
||||
native bridge code. It would **not** distinguish a bridge-internal native owner
|
||||
from Apple caching; a later independently owned public-Apple sequence could be
|
||||
justified then. A flat result would not by itself prove a managed bug: omitted
|
||||
managed parsing/matching, registry bookkeeping or lifetime differences remain. No
|
||||
production implementation or additional experiment is approved by this design.
|
||||
Reference in New Issue
Block a user