Skip to content

Dispatch DoAsn1Key public keys on key type - #1137

Open
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/f_7504
Open

Dispatch DoAsn1Key public keys on key type#1137
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/f_7504

Conversation

@yosuke-wolfssl

@yosuke-wolfssl yosuke-wolfssl commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Problem

DoAsn1Key()'s public-key branch had exactly two arms: an ML-DSA arm gated on an explicit key-ID test, and an unconditional else that assumed RSA. But IdentifyAsn1Key(in, inSz, 0, ...) also returns ID_ECDSA_SHA2_NISTP256/384/521 and ID_ED25519, which decode into the ecc_key and ed25519_key members of the WS_KeySignature.ks union.

Those IDs fell into the RSA arm and reached wc_RsaFlattenPublicKey(&key->ks.rsa.key, ...), walking RsaKey.n/RsaKey.e mp_int fields that overlay unrelated members of a different key type. Observed behavior: the call returned WS_SUCCESS with *outType set to ecdsa-sha2-nistp256 but a blob whose first string was ssh-rsa, built from unrelated memory. The encode/decode roundtrip cannot hold, and the read is out of the caller's control.

Reachable through the public API wolfSSH_ReadPublicKey_buffer(). The in-tree caller wolfSSH_TPM_InitKey() (examples/client/common.c) feeds a TPM public key exported as ASN.1, so a TPM-resident ECC key trips this.

Fix (src/ssh.c)

Hoisted the *outType/*outTypeSz assignment above the chain, gated the RSA arm on its own ID, and added the missing arms. Unknown types now fail closed with WS_UNIMPLEMENTED_E instead of falling through to RSA.

Key ID Blob layout
ID_SSH_RSA ssh-rsa, e, n (unchanged)
ID_ECDSA_SHA2_NISTP256/384/521 ecdsa-sha2-nistpXXX, nistpXXX, Q via wc_ecc_export_x963()
ID_ED25519 ssh-ed25519, A via wc_ed25519_export_public()
ID_MLDSA44/65/87 unchanged
anything else WS_UNIMPLEMENTED_E

The fallback also closes a second instance of the same defect: IdentifyAsn1Key assigns ID_MLDSA44/65/87 without per-level guards, while the ML-DSA arm is per-level guarded, so in a WOLFSSH_NO_MLDSA44 build such a key previously hit the RSA arm too.

Closes f-7504.

Tests (tests/unit.c)

test_ReadPublicKeyAsn1() covers RSA, ECDSA P-256/384/521, Ed25519 and ML-DSA 44/65/87. Each case reads a public key back through wolfSSH_ReadPublicKey_buffer() and checks outType plus every blob string against the key's own exported value, requiring the blob to be exactly consumed — content, not just framing. The RSA case reproduces the leading zero byte DoAsn1Key prepends when the modulus MSB is set.

Mutation-tested rather than assumed: corrupting a modulus byte, forcing nMsb = 0, and corrupting the ML-DSA raw key are each caught by the corresponding case. The whole test fails before the fix.

Verification

  • make check: 10 pass, 1 skip (external.test, needs network), 0 fail, in both a standard build and one against wolfSSL --enable-mldsa.
  • Linked and run against a lean wolfSSL matching the CI configuration, default and --enable-smallstack.
  • Preflight sweep clean: lint plus 6 configs under gcc-13 -Werror.
  • src/ssh.c compiles -Werror clean across all eight on/off combinations of WOLFSSH_NO_RSA/ECDSA/ED25519, and with ML-DSA compiled in. tests/unit.c was checked over the same matrix and is clean for every configuration it supports, including an ML-DSA-only build with the other three key types disabled.

Reviewer notes

Two points have been raised repeatedly by automated review. The rationale is recorded here so it does not need re-deriving.

The WS_UNIMPLEMENTED_E fallback cannot be reached by any input. DoAsn1Key guards the entire dispatch chain on ret > 0. Every keyId that IdentifyAsn1Key can assign (ID_SSH_RSA, the three ECDSA curves, the three ML-DSA levels, ID_ED25519) has a matching arm, and anything unrecognised returns a negative error, so the block including the fallback is skipped.

This was checked, not assumed. Feeding an ECC SPKI whose curve OID is an unsupported curve returns WS_UNIMPLEMENTED_E with out == NULL; changing the fallback to return a different code and rebuilding yields the same result, proving the line never executes. Any test written against it would pass identically with the else deleted. The only configuration that can execute it is WOLFSSH_NO_MLDSA44 (or the 65/87 equivalents) with ML-DSA otherwise enabled, reaching it through the unguarded level mapping described below, and CI does not build that combination. The branch is deliberate defence in depth against future drift between the two functions.

Public-key blobs are allocated with DYNTYPE_PRIVKEY. That reads oddly for a public key, but it matches the callers: examples/client/common.c and apps/wolfssh/common.c both free wolfSSH_ReadPublicKey_buffer() output with DYNTYPE_PRIVKEY, as do DoSshPubKey, DoPemKey, DoOpenSshKey and the RSA arm. The tag is advisory rather than functional: without WOLFSSL_STATIC_MEMORY wolfSSL discards it before any custom allocator callback sees it, and with static memory the free path selects its bucket by size, consulting the type only for the IO-pool types, which wolfSSH's 500-based enum can never collide with. Aligning the tag is a consistency fix, not a routing fix.

Not in this PR

The unguarded ID_MLDSA44/65/87 assignment in IdentifyAsn1Key reports a key type the build has disabled. This change makes it fail closed rather than confuse types, but the ID arguably should not be produced at all. Left for a separate finding.

@yosuke-wolfssl yosuke-wolfssl self-assigned this Aug 3, 2026
Copilot AI review requested due to automatic review settings August 3, 2026 00:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes incorrect ASN.1 public-key dispatch in DoAsn1Key() so non-RSA public keys (ECDSA/Ed25519 and ML-DSA under some build flags) no longer fall through the RSA encoding path and produce malformed/unsafe public-key blobs.

Changes:

  • Dispatch ASN.1 public-key encoding by identified key ID (RSA vs ECDSA vs Ed25519 vs ML-DSA), and fail closed with WS_UNIMPLEMENTED_E for unsupported types.
  • Add unit tests validating ASN.1 SPKI → SSH public-key blob conversion for RSA, ECDSA P-256/384/521, and Ed25519.
  • Adjust the public-key path to set outType/outTypeSz consistently before encoding.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/ssh.c Dispatch ASN.1 public-key encoding by key type and add ECDSA/Ed25519 handling; fail closed for unsupported types.
tests/unit.c Add regression/unit tests for reading ASN.1 public keys into SSH-wire blobs for multiple algorithms.
Suppressed comments (4)

src/ssh.c:2014

  • The returned buffer from wolfSSH_ReadPublicKey_buffer() is typically allocated/freed as DYNTYPE_PRIVKEY in this codebase (see DoSshPubKey() and examples/client/common.c). This ECDSA ASN.1 path allocates the output blob with DYNTYPE_PUBKEY, which can cause mismatched free/memory tracking for callers following the existing convention.
                    *outSz = LENGTH_SZ + *outTypeSz + LENGTH_SZ + curveNameSz +
                        LENGTH_SZ + qSz;
                    newKey = (byte*)WMALLOC(*outSz, heap, DYNTYPE_PUBKEY);
                    if (newKey == NULL) {

tests/unit.c:9479

  • wolfSSH_ReadPublicKey_buffer() output buffers are freed as DYNTYPE_PRIVKEY elsewhere in-tree (e.g., examples/client/common.c). Freeing this blob with DYNTYPE_PUBKEY can mismatch the allocator tag (and will also mismatch if the implementation allocates with DYNTYPE_PRIVKEY as other key-read paths do).
    if (blob != NULL) {
        WFREE(blob, NULL, DYNTYPE_PUBKEY);
    }

tests/unit.c:9579

  • wolfSSH_ReadPublicKey_buffer() output buffers are freed as DYNTYPE_PRIVKEY elsewhere in-tree (e.g., examples/client/common.c). Freeing this blob with DYNTYPE_PUBKEY can mismatch the allocator tag (and will also mismatch if the implementation allocates with DYNTYPE_PRIVKEY as other key-read paths do).
    if (blob != NULL) {
        WFREE(blob, NULL, DYNTYPE_PUBKEY);
    }

src/ssh.c:2052

  • The returned buffer from wolfSSH_ReadPublicKey_buffer() is typically allocated/freed as DYNTYPE_PRIVKEY in this codebase (see DoSshPubKey() and examples/client/common.c). This Ed25519 ASN.1 path allocates the output blob with DYNTYPE_PUBKEY, which can cause mismatched free/memory tracking for callers following the existing convention.
                *outSz = LENGTH_SZ + *outTypeSz + LENGTH_SZ + qSz;
                newKey = (byte*)WMALLOC(*outSz, heap, DYNTYPE_PUBKEY);
                if (newKey == NULL) {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/unit.c
Comment thread src/ssh.c
@yosuke-wolfssl
yosuke-wolfssl force-pushed the fix/f_7504 branch 2 times, most recently from 0bda802 to 7573588 Compare August 3, 2026 02:45

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fenrir Automated Review — PR #1137

Scan targets checked: wolfssh-bugs, wolfssh-src

Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Findings are non-blocking.

Comment thread tests/unit.c
Comment thread tests/unit.c
Comment thread tests/unit.c
Comment thread tests/unit.c

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fenrir Automated Review — PR #1137

Scan targets checked: wolfssh-bugs, wolfssh-src

Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Findings are non-blocking.

Comment thread tests/unit.c
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.

4 participants