From 72cd3ef3e26c9cb7c80031a42d61220c4e40f475 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 16 Mar 2026 19:50:46 -0500 Subject: [PATCH] Fix PublicSign with full key pair embedding private key material in assembly When PublicSign=true with a full .snk key pair, the PublicKey property of PublicKeyOptionsSigner returned the raw .snk bytes verbatim, including private key material (PRIVATEKEYBLOB). This caused: 1. Private key material embedded in the compiled assembly's metadata 2. Malformed public key blob (bType=0x07 instead of 0x06) that fails IsValidPublicKey validation during ResolveAssemblyReference The fix detects when PublicKeyOptionsSigner holds a key pair blob and converts it to a public-key-only CLR blob via getPublicKeyForKeyPair, matching what the KeyPair case already does. Also strengthened the existing PublicSign test to verify no RSA2 (private key) magic appears in the output, and added a regression test that validates AssemblyName can load the public key without SecurityException. Fixes #19441 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Compiler/AbstractIL/ilsign.fs | 14 ++++- .../CompilerOptions/fsc/misc/PublicSign.fs | 55 +++++++++++++++++-- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/Compiler/AbstractIL/ilsign.fs b/src/Compiler/AbstractIL/ilsign.fs index aef49313f9f..36d5b2d3563 100644 --- a/src/Compiler/AbstractIL/ilsign.fs +++ b/src/Compiler/AbstractIL/ilsign.fs @@ -331,6 +331,13 @@ let getPublicKeyForKeyPair keyBlob = let rsaParameters = rsa.ExportParameters false toCLRKeyBlob rsaParameters CALG_RSA_KEYX +// Detect whether a byte array is a raw CAPI PRIVATEKEYBLOB (full key pair). +// A raw key pair blob starts with bType=0x07 (PRIVATEKEYBLOB), bVersion=0x02. +let isKeyPairBlob (blob: byte array) = + blob.Length > 8 + && int blob.[0] = PRIVATEKEYBLOB + && int blob.[1] = BLOBHEADER_CURRENT_BVERSION + // Key signing type keyContainerName = string type keyPair = byte array @@ -375,7 +382,12 @@ type ILStrongNameSigner = | PublicKeySigner pk -> pk | PublicKeyOptionsSigner pko -> let pk, _ = pko - pk + // If the blob is a full key pair (PRIVATEKEYBLOB), extract the public key + // to avoid embedding private key material in the assembly. + if isKeyPairBlob pk then + signerGetPublicKeyForKeyPair pk + else + pk | KeyPair kp -> signerGetPublicKeyForKeyPair kp | KeyContainer _ -> failWithContainerSigningUnsupportedOnThisPlatform () diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/misc/PublicSign.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/misc/PublicSign.fs index 5a4a75d6369..a753fecac3f 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/misc/PublicSign.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/misc/PublicSign.fs @@ -72,11 +72,54 @@ let x = 42 found <- true found - // Verify that the compiled DLL contains RSA magic bytes, confirming the public key blob was embedded - let hasRSAMagic: bool = - containsRSAMagic dllBytes rsa1Magic || containsRSAMagic dllBytes rsa2Magic - + // Verify that the compiled DLL contains RSA1 (public key) magic and NOT RSA2 (private key) magic + let hasRSA1: bool = containsRSAMagic dllBytes rsa1Magic + let hasRSA2: bool = containsRSAMagic dllBytes rsa2Magic + Assert.True( - hasRSAMagic, - "Compiled DLL should contain RSA magic bytes (RSA1 or RSA2) indicating public key blob was embedded by compiler with --publicsign" + hasRSA1, + "Compiled DLL should contain RSA1 magic bytes indicating a public key blob was embedded" + ) + + Assert.False( + hasRSA2, + "Compiled DLL must NOT contain RSA2 magic bytes — private key material must not be embedded in the assembly" ) + + /// + /// Tests that --publicsign with a full key pair (.snk) produces an assembly with a valid + /// public key blob that can be loaded by AssemblyName without throwing SecurityException. + /// This is the regression test for https://github.com/dotnet/fsharp/issues/19441. + /// + [] + let ``--publicsign with full key pair produces valid public key blob`` () = + let source = + """ +module TestModule +let x = 42 +""" + + let snkPath: string = + Path.Combine(__SOURCE_DIRECTORY__, "..", "..", "..", "..", "fsharp", "core", "signedtests", "sha1full.snk") + + let result = + source + |> FSharp + |> asLibrary + |> withFileName "PublicSignValidKey.fs" + |> withOptions ["--publicsign+"; sprintf "--keyfile:%s" snkPath] + |> compile + + result |> shouldSucceed |> ignore + + let outputDll: string = + match result.OutputPath with + | Some path -> path + | None -> failwith "Compilation did not produce an output DLL" + + // Loading the assembly name should not throw SecurityException for invalid public key + let assemblyName = System.Reflection.AssemblyName.GetAssemblyName(outputDll) + let publicKeyToken = assemblyName.GetPublicKeyToken() + + Assert.NotNull(publicKeyToken) + Assert.True(publicKeyToken.Length > 0, "PublicKeyToken should be non-empty for a public-signed assembly")