最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Ed25519ph key and signature are not compatible between OpenSSL and BouncyCastle C# - Stack Overflow

programmeradmin6浏览0评论

Step 1: generate Ed25519 key, signature and verify using OpenSSL

openssl version
REM output: OpenSSL 3.3.2 3 Sep 2024 (Library: OpenSSL 3.3.2 3 Sep 2024)

REM Generate private key
openssl genpkey -algorithm ED25519 -out ed25519_private.pem

REM Extract public key
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem

REM Create sample SHA512 hash
echo|set /p="HelloEd25519ph" | openssl dgst -sha512 -binary | openssl base64 -A > hash.txt

REM Generate signature for pre-sha512-hashed data
openssl pkeyutl -sign -inkey ed25519_private.pem -rawin -in hash.txt -pkeyopt instance:ed25519ph -out signature.bin

REM Verify signature
openssl pkeyutl -verify -rawin -pubin -inkey ed25519_public.pem -pkeyopt instance:ed25519ph -in hash.txt -sigfile signature.bin
REM output: Signature Verified Successfully

Step 2: generate Ed25519 key, signature and verify using BouncyCastle (C# .NET 8)

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
  </ItemGroup>

</Project>


using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System.Security.Cryptography;
using System.Text;

class Program
{
    static void Main()
    {
        string privateKeyPath = "ed25519_private.pem";
        string publicKeyPath = "ed25519_public.pem";
        string hashFilePath = "hash.txt";
        string signatureFilePath = "signature.bin";

        // 1. Generate Ed25519 Key Pair
        AsymmetricCipherKeyPair keyPair = GenerateEd25519KeyPair();
        Ed25519PrivateKeyParameters privateKey = (Ed25519PrivateKeyParameters)keyPair.Private;
        Ed25519PublicKeyParameters publicKey = (Ed25519PublicKeyParameters)keyPair.Public;

        // 2. Save keys to PEM files
        SavePrivateKeyToPem(privateKeyPath, privateKey);
        SavePublicKeyToPem(publicKeyPath, publicKey);
        Console.WriteLine("Keys saved as PEM files!");

        // 3. Pre-hash the message using SHA-512
        byte[] message = Encoding.UTF8.GetBytes("HelloEd25519ph");
        byte[] preHashedMessage = SHA512.HashData(message);

        // 4. Save pre-hashed message to file (Base64 encoded)
        File.WriteAllText(hashFilePath, Convert.ToBase64String(preHashedMessage));
        Console.WriteLine($"Pre-hashed message saved to {hashFilePath}");

        // 5. Sign with Ed25519ph (using empty context instead of null)
        byte[] signature = SignEd25519ph(privateKey, preHashedMessage, new byte[0]);

        // 6. Save the signature to a binary file
        File.WriteAllBytes(signatureFilePath, signature);
        Console.WriteLine($"Signature saved to {signatureFilePath}");

        // 7. Verify the signature
        bool isValid = VerifyEd25519ph(publicKey, preHashedMessage, signature, new byte[0]);
        Console.WriteLine("Signature valid: " + isValid);
    }

    // Generate Ed25519 Key Pair
    static AsymmetricCipherKeyPair GenerateEd25519KeyPair()
    {
        var keyGen = new Ed25519KeyPairGenerator();
        keyGen.Init(new KeyGenerationParameters(new SecureRandom(), 256));
        return keyGen.GenerateKeyPair();
    }

    // Save Ed25519 Private Key in OpenSSL-compatible PKCS#8 format
    static void SavePrivateKeyToPem(string filePath, Ed25519PrivateKeyParameters privateKey)
    {
        using (TextWriter textWriter = new StreamWriter(filePath))
        {
            var pemWriter = new PemWriter(textWriter);
            pemWriter.WriteObject(privateKey);
        }


    }

    // Save Ed25519 Public Key as PEM
    static void SavePublicKeyToPem(string filePath, Ed25519PublicKeyParameters publicKey)
    {
        using (TextWriter textWriter = new StreamWriter(filePath))
        {
            var pemWriter = new PemWriter(textWriter);
            pemWriter.WriteObject(publicKey);
        }
    }

    // Sign SHA-512 pre-hashed data with Ed25519ph (must provide a non-null context)
    static byte[] SignEd25519ph(Ed25519PrivateKeyParameters privateKey, byte[] preHashedMessage, byte[] context)
    {
        var signer = new Ed25519phSigner(context); // Use an empty array for no context
        signer.Init(true, privateKey);
        signer.BlockUpdate(preHashedMessage, 0, preHashedMessage.Length);
        return signer.GenerateSignature();
    }

    // Verify Ed25519ph signature (must provide a non-null context)
    static bool VerifyEd25519ph(Ed25519PublicKeyParameters publicKey, byte[] preHashedMessage, byte[] signature, byte[] context)
    {
        var verifier = new Ed25519phSigner(context); // Use an empty array for no context
        verifier.Init(false, publicKey);
        verifier.BlockUpdate(preHashedMessage, 0, preHashedMessage.Length);
        return verifier.VerifySignature(signature);
    }
}

Output:

Keys saved as PEM files!
Pre-hashed message saved to hash.txt
Signature saved to signature.bin
Signature valid: True

Problem No.1

Using OpenSSL to view the generated public key/private key

a) For the file generated by OpenSSL itself:

> openssl pkey -in ed25519_public.pem -pubin -text -noout
ED25519 Public-Key:
pub:
    76:10:f4:49:ad:cf:8d:2e:df:f2:47:72:85:57:be:
    70:c1:8a:f6:ed:7f:fb:35:98:8a:29:1e:fd:ab:0a:
    41:0b

> openssl pkey -in ed25519_private.pem -text -noout
ED25519 Private-Key:
priv:
    54:46:41:35:c7:b0:28:b4:81:a0:fa:c8:02:2a:22:
    27:b1:d0:fc:39:7e:1e:3d:d6:21:59:88:f3:8a:76:
    ce:fd
pub:
    76:10:f4:49:ad:cf:8d:2e:df:f2:47:72:85:57:be:
    70:c1:8a:f6:ed:7f:fb:35:98:8a:29:1e:fd:ab:0a:
    41:0b

b) For the file generated by BouncyCastle:

> openssl pkey -in ed25519_public.pem -pubin -text -noout
ED25519 Public-Key:
pub:
    0d:4d:a2:f5:b4:03:d0:90:bb:f5:b7:b7:4c:c4:39:
    e5:7b:1e:9b:ad:86:ef:89:81:d1:75:f2:bd:ab:81:
    b8:ee
    
> openssl pkey -in ed25519_private.pem -text -noout
Could not find private key of key from ed25519_private.pem
7C650000:error:1608010C:STORE routines:ossl_store_handle_load_result:unsupported:crypto\store\store_result.c:151:

I use following command to display the ASN.1 structure of both file ed25519_private.pem:

> openssl asn1parse -in ed25519_private.pem -inform PEM

For the file generated by OpenSSL itself:

    0:d=0  hl=2 l=  46 cons: SEQUENCE
    2:d=1  hl=2 l=   1 prim: INTEGER           :00
    5:d=1  hl=2 l=   5 cons: SEQUENCE
    7:d=2  hl=2 l=   3 prim: OBJECT            :ED25519
   12:d=1  hl=2 l=  34 prim: OCTET STRING      [HEX DUMP]:042054464135C7B028B481A0FAC8022A2227B1D0FC397E1E3DD6215988F38A76CEFD

For the file generated by BouncyCastle:

    0:d=0  hl=2 l=  81 cons: SEQUENCE
    2:d=1  hl=2 l=   1 prim: INTEGER           :01
    5:d=1  hl=2 l=   5 cons: SEQUENCE
    7:d=2  hl=2 l=   3 prim: OBJECT            :ED25519
   12:d=1  hl=2 l=  34 prim: OCTET STRING      [HEX DUMP]:0420CDF23CED8CB130D14898628415D127BAA89029DF5FB7BCE0B3ABC1A470B2C49B
   48:d=1  hl=2 l=  33 prim: cont [ 1 ]
   

It seems that BouncyCastle generate file with newer structure version (0x01) rather than 0x00 in case of OpenSSL, and include some additional information (possible the public key data). So, OpenSSL cannot read the file.

Question 1: is there anyway to force BouncyCastle to generate private key PEM that is compatible with OpenSSL ?

Problem No.2

Using OpenSSL command to verify the signature generated by BouncyCastle:

openssl pkeyutl -verify -rawin -pubin -inkey ed25519_public.pem -pkeyopt instance:ed25519ph -in hash.txt -sigfile signature.bin

Output: Signature Verification Failure

Note that all file ed25519_public.pem, hash.txt and signature.bin are generated by BouncyCastle.

Question 2: why OpenSSL verification is failure here? Is there any problem in the sample C# program?

I assume that when using Ed25519ph, the default context is empty string, so I used new byte[0] as the input. Does OpenSSL use the same context here?


Update 1: working version

Based on @Topaco suggestion, I success revised my sample code and it is working now.

Step 1: generate Ed25519 key, signature and verify using OpenSSL

openssl version
REM output: OpenSSL 3.4.1 11 Feb 2025 (Library: OpenSSL 3.4.1 11 Feb 2025)

REM Generate private key
openssl genpkey -algorithm ED25519 -out ed25519_private.pem

REM Extract public key
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem

REM Create sample SHA512 hash
echo|set /p="HelloEd25519ph" | openssl dgst -sha512 -binary > hash.bin

REM Generate signature for pre-sha512-hashed data
openssl pkeyutl -sign -inkey ed25519_private.pem -in hash.bin -pkeyopt instance:ed25519ph -out signature.bin

REM Verify signature
openssl pkeyutl -verify -pubin -inkey ed25519_public.pem -pkeyopt instance:ed25519ph -in hash.bin -sigfile signature.bin
REM output: Signature Verified Successfully

Step 2: generate Ed25519 key, signature and verify using BouncyCastle (C# .NET 8). In this updated sample, I also added 2 methods to load the public key and private key from file.

using Org.BouncyCastle.Asn1.EdEC;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System.Security.Cryptography;
using System.Text;
using Org.BouncyCastle.Math.EC.Rfc8032;

class Program
{
    static void Main()
    {
        string privateKeyPath = "ed25519_private.pem";
        string publicKeyPath = "ed25519_public.pem";
        string hashFilePath = "hash.bin";
        string signatureFilePath = "signature.bin";

        // 1. Generate Ed25519 Key Pair
        AsymmetricCipherKeyPair keyPair = GenerateEd25519KeyPair();
        Ed25519PrivateKeyParameters privateKey = (Ed25519PrivateKeyParameters)keyPair.Private;
        Ed25519PublicKeyParameters publicKey = (Ed25519PublicKeyParameters)keyPair.Public;

        // 2. Save keys to PEM files
        SavePrivateKeyToPem2(privateKeyPath, privateKey);
        SavePublicKeyToPem(publicKeyPath, publicKey);
        Console.WriteLine("Keys saved as PEM files!");

        // 3. Load the private key from the PEM file
        Ed25519PrivateKeyParameters privateKey2 = LoadPrivateKeyFromPem(privateKeyPath);

        // 4. Load the public key from the PEM file
        Ed25519PublicKeyParameters publicKey2 = LoadPublicKeyFromPem(publicKeyPath);

        // 5. Pre-hash the message using SHA-512
        byte[] message = Encoding.UTF8.GetBytes("HelloEd25519ph");
        byte[] preHashedMessage = SHA512.HashData(message);

        // 6. Save pre-hashed message to file (Base64 encoded)
        File.WriteAllBytes(hashFilePath, preHashedMessage);
        Console.WriteLine($"Pre-hashed message saved to {hashFilePath}");

        // 7. Sign with Ed25519ph (using empty context instead of null)
        byte[] signature = SignEd25519phLowLevel(privateKey2, preHashedMessage, new byte[0]);

        // 8. Save the signature to a binary file
        File.WriteAllBytes(signatureFilePath, signature);
        Console.WriteLine($"Signature saved to {signatureFilePath}");

        // 9. Verify the signature
        bool isValid = VerifyEd25519phLowLevel(publicKey2, preHashedMessage, signature, new byte[0]);
        Console.WriteLine("Signature valid: " + isValid);
    }

    // Generate Ed25519 Key Pair
    static AsymmetricCipherKeyPair GenerateEd25519KeyPair()
    {
        var keyGen = new Ed25519KeyPairGenerator();
        keyGen.Init(new KeyGenerationParameters(new SecureRandom(), 256));
        return keyGen.GenerateKeyPair();
    }

    // Save Ed25519 Private Key in OpenSSL-compatible PKCS#8 format
    static void SavePrivateKeyToPem2(string filePath, Ed25519PrivateKeyParameters privateKey)
    {
        PrivateKeyInfo privateKeyInfo = new PrivateKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), new DerOctetString(privateKey.GetEncoded()));
        using (TextWriter textWriter = new StreamWriter(filePath))
        {
            var pemWriter = new PemWriter(textWriter);
            pemWriter.WriteObject(privateKeyInfo);
        }
    }

    // Load Ed25519 Private Key from PEM file
    static Ed25519PrivateKeyParameters LoadPrivateKeyFromPem(string filePath)
    {
        using (TextReader textReader = new StreamReader(filePath))
        {
            var pemReader = new PemReader(textReader);
            return (Ed25519PrivateKeyParameters)pemReader.ReadObject();
        }
    }

    // Load Ed25519 Public Key from PEM file
    static Ed25519PublicKeyParameters LoadPublicKeyFromPem(string filePath)
    {
        using (TextReader textReader = new StreamReader(filePath))
        {
            var pemReader = new PemReader(textReader);
            return (Ed25519PublicKeyParameters)pemReader.ReadObject();
        }
    }

    // Save Ed25519 Public Key as PEM
    static void SavePublicKeyToPem(string filePath, Ed25519PublicKeyParameters publicKey)
    {
        using (TextWriter textWriter = new StreamWriter(filePath))
        {
            var pemWriter = new PemWriter(textWriter);
            pemWriter.WriteObject(publicKey);
        }
    }

    // Sign SHA-512 pre-hashed data with Ed25519ph (must provide a non-null context)
    static byte[] SignEd25519phLowLevel(Ed25519PrivateKeyParameters privateKey, byte[] preHashedMessage, byte[] context)
    {
        byte[] signature = new byte[64];
        privateKey.Sign(Ed25519.Algorithm.Ed25519ph, context, preHashedMessage, 0, preHashedMessage.Length, signature, 0);
        return signature;
    }

    // Verify Ed25519ph signature (must provide a non-null context)
    static bool VerifyEd25519phLowLevel(Ed25519PublicKeyParameters publicKey, byte[] preHashedMessage, byte[] signature, byte[] context)
    {
        return publicKey.Verify(Ed25519.Algorithm.Ed25519ph, context, preHashedMessage, 0, preHashedMessage.Length, signature, 0);
    }
}

Output:

Keys saved as PEM files!
Pre-hashed message saved to hash.bin
Signature saved to signature.bin
Signature valid: True

Step 3: use OpenSSL to read the public/private key + verify signature generated by BouncyCastle:

openssl pkey -in ed25519_public.pem -pubin -text -noout
ED25519 Public-Key:
pub:
    36:a1:d3:1e:7d:b0:be:64:fe:ad:ec:dc:ef:48:97:
    33:4c:07:ed:1a:e1:d1:be:5e:03:ae:a3:e3:11:d5:
    6a:7f

>openssl pkey -in ed25519_private.pem -text -noout
ED25519 Private-Key:
priv:
    3a:18:7a:f5:48:2f:91:f1:49:3d:cf:06:e8:3e:6b:
    61:17:8a:34:52:2a:87:f2:75:27:ef:65:b7:0f:74:
    18:76
pub:
    36:a1:d3:1e:7d:b0:be:64:fe:ad:ec:dc:ef:48:97:
    33:4c:07:ed:1a:e1:d1:be:5e:03:ae:a3:e3:11:d5:
    6a:7f

>openssl pkeyutl -verify -pubin -inkey ed25519_public.pem -pkeyopt instance:ed25519ph -in hash.bin -sigfile signature.bin
Signature Verified Successfully

Step 1: generate Ed25519 key, signature and verify using OpenSSL

openssl version
REM output: OpenSSL 3.3.2 3 Sep 2024 (Library: OpenSSL 3.3.2 3 Sep 2024)

REM Generate private key
openssl genpkey -algorithm ED25519 -out ed25519_private.pem

REM Extract public key
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem

REM Create sample SHA512 hash
echo|set /p="HelloEd25519ph" | openssl dgst -sha512 -binary | openssl base64 -A > hash.txt

REM Generate signature for pre-sha512-hashed data
openssl pkeyutl -sign -inkey ed25519_private.pem -rawin -in hash.txt -pkeyopt instance:ed25519ph -out signature.bin

REM Verify signature
openssl pkeyutl -verify -rawin -pubin -inkey ed25519_public.pem -pkeyopt instance:ed25519ph -in hash.txt -sigfile signature.bin
REM output: Signature Verified Successfully

Step 2: generate Ed25519 key, signature and verify using BouncyCastle (C# .NET 8)

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
  </ItemGroup>

</Project>


using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System.Security.Cryptography;
using System.Text;

class Program
{
    static void Main()
    {
        string privateKeyPath = "ed25519_private.pem";
        string publicKeyPath = "ed25519_public.pem";
        string hashFilePath = "hash.txt";
        string signatureFilePath = "signature.bin";

        // 1. Generate Ed25519 Key Pair
        AsymmetricCipherKeyPair keyPair = GenerateEd25519KeyPair();
        Ed25519PrivateKeyParameters privateKey = (Ed25519PrivateKeyParameters)keyPair.Private;
        Ed25519PublicKeyParameters publicKey = (Ed25519PublicKeyParameters)keyPair.Public;

        // 2. Save keys to PEM files
        SavePrivateKeyToPem(privateKeyPath, privateKey);
        SavePublicKeyToPem(publicKeyPath, publicKey);
        Console.WriteLine("Keys saved as PEM files!");

        // 3. Pre-hash the message using SHA-512
        byte[] message = Encoding.UTF8.GetBytes("HelloEd25519ph");
        byte[] preHashedMessage = SHA512.HashData(message);

        // 4. Save pre-hashed message to file (Base64 encoded)
        File.WriteAllText(hashFilePath, Convert.ToBase64String(preHashedMessage));
        Console.WriteLine($"Pre-hashed message saved to {hashFilePath}");

        // 5. Sign with Ed25519ph (using empty context instead of null)
        byte[] signature = SignEd25519ph(privateKey, preHashedMessage, new byte[0]);

        // 6. Save the signature to a binary file
        File.WriteAllBytes(signatureFilePath, signature);
        Console.WriteLine($"Signature saved to {signatureFilePath}");

        // 7. Verify the signature
        bool isValid = VerifyEd25519ph(publicKey, preHashedMessage, signature, new byte[0]);
        Console.WriteLine("Signature valid: " + isValid);
    }

    // Generate Ed25519 Key Pair
    static AsymmetricCipherKeyPair GenerateEd25519KeyPair()
    {
        var keyGen = new Ed25519KeyPairGenerator();
        keyGen.Init(new KeyGenerationParameters(new SecureRandom(), 256));
        return keyGen.GenerateKeyPair();
    }

    // Save Ed25519 Private Key in OpenSSL-compatible PKCS#8 format
    static void SavePrivateKeyToPem(string filePath, Ed25519PrivateKeyParameters privateKey)
    {
        using (TextWriter textWriter = new StreamWriter(filePath))
        {
            var pemWriter = new PemWriter(textWriter);
            pemWriter.WriteObject(privateKey);
        }


    }

    // Save Ed25519 Public Key as PEM
    static void SavePublicKeyToPem(string filePath, Ed25519PublicKeyParameters publicKey)
    {
        using (TextWriter textWriter = new StreamWriter(filePath))
        {
            var pemWriter = new PemWriter(textWriter);
            pemWriter.WriteObject(publicKey);
        }
    }

    // Sign SHA-512 pre-hashed data with Ed25519ph (must provide a non-null context)
    static byte[] SignEd25519ph(Ed25519PrivateKeyParameters privateKey, byte[] preHashedMessage, byte[] context)
    {
        var signer = new Ed25519phSigner(context); // Use an empty array for no context
        signer.Init(true, privateKey);
        signer.BlockUpdate(preHashedMessage, 0, preHashedMessage.Length);
        return signer.GenerateSignature();
    }

    // Verify Ed25519ph signature (must provide a non-null context)
    static bool VerifyEd25519ph(Ed25519PublicKeyParameters publicKey, byte[] preHashedMessage, byte[] signature, byte[] context)
    {
        var verifier = new Ed25519phSigner(context); // Use an empty array for no context
        verifier.Init(false, publicKey);
        verifier.BlockUpdate(preHashedMessage, 0, preHashedMessage.Length);
        return verifier.VerifySignature(signature);
    }
}

Output:

Keys saved as PEM files!
Pre-hashed message saved to hash.txt
Signature saved to signature.bin
Signature valid: True

Problem No.1

Using OpenSSL to view the generated public key/private key

a) For the file generated by OpenSSL itself:

> openssl pkey -in ed25519_public.pem -pubin -text -noout
ED25519 Public-Key:
pub:
    76:10:f4:49:ad:cf:8d:2e:df:f2:47:72:85:57:be:
    70:c1:8a:f6:ed:7f:fb:35:98:8a:29:1e:fd:ab:0a:
    41:0b

> openssl pkey -in ed25519_private.pem -text -noout
ED25519 Private-Key:
priv:
    54:46:41:35:c7:b0:28:b4:81:a0:fa:c8:02:2a:22:
    27:b1:d0:fc:39:7e:1e:3d:d6:21:59:88:f3:8a:76:
    ce:fd
pub:
    76:10:f4:49:ad:cf:8d:2e:df:f2:47:72:85:57:be:
    70:c1:8a:f6:ed:7f:fb:35:98:8a:29:1e:fd:ab:0a:
    41:0b

b) For the file generated by BouncyCastle:

> openssl pkey -in ed25519_public.pem -pubin -text -noout
ED25519 Public-Key:
pub:
    0d:4d:a2:f5:b4:03:d0:90:bb:f5:b7:b7:4c:c4:39:
    e5:7b:1e:9b:ad:86:ef:89:81:d1:75:f2:bd:ab:81:
    b8:ee
    
> openssl pkey -in ed25519_private.pem -text -noout
Could not find private key of key from ed25519_private.pem
7C650000:error:1608010C:STORE routines:ossl_store_handle_load_result:unsupported:crypto\store\store_result.c:151:

I use following command to display the ASN.1 structure of both file ed25519_private.pem:

> openssl asn1parse -in ed25519_private.pem -inform PEM

For the file generated by OpenSSL itself:

    0:d=0  hl=2 l=  46 cons: SEQUENCE
    2:d=1  hl=2 l=   1 prim: INTEGER           :00
    5:d=1  hl=2 l=   5 cons: SEQUENCE
    7:d=2  hl=2 l=   3 prim: OBJECT            :ED25519
   12:d=1  hl=2 l=  34 prim: OCTET STRING      [HEX DUMP]:042054464135C7B028B481A0FAC8022A2227B1D0FC397E1E3DD6215988F38A76CEFD

For the file generated by BouncyCastle:

    0:d=0  hl=2 l=  81 cons: SEQUENCE
    2:d=1  hl=2 l=   1 prim: INTEGER           :01
    5:d=1  hl=2 l=   5 cons: SEQUENCE
    7:d=2  hl=2 l=   3 prim: OBJECT            :ED25519
   12:d=1  hl=2 l=  34 prim: OCTET STRING      [HEX DUMP]:0420CDF23CED8CB130D14898628415D127BAA89029DF5FB7BCE0B3ABC1A470B2C49B
   48:d=1  hl=2 l=  33 prim: cont [ 1 ]
   

It seems that BouncyCastle generate file with newer structure version (0x01) rather than 0x00 in case of OpenSSL, and include some additional information (possible the public key data). So, OpenSSL cannot read the file.

Question 1: is there anyway to force BouncyCastle to generate private key PEM that is compatible with OpenSSL ?

Problem No.2

Using OpenSSL command to verify the signature generated by BouncyCastle:

openssl pkeyutl -verify -rawin -pubin -inkey ed25519_public.pem -pkeyopt instance:ed25519ph -in hash.txt -sigfile signature.bin

Output: Signature Verification Failure

Note that all file ed25519_public.pem, hash.txt and signature.bin are generated by BouncyCastle.

Question 2: why OpenSSL verification is failure here? Is there any problem in the sample C# program?

I assume that when using Ed25519ph, the default context is empty string, so I used new byte[0] as the input. Does OpenSSL use the same context here?


Update 1: working version

Based on @Topaco suggestion, I success revised my sample code and it is working now.

Step 1: generate Ed25519 key, signature and verify using OpenSSL

openssl version
REM output: OpenSSL 3.4.1 11 Feb 2025 (Library: OpenSSL 3.4.1 11 Feb 2025)

REM Generate private key
openssl genpkey -algorithm ED25519 -out ed25519_private.pem

REM Extract public key
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem

REM Create sample SHA512 hash
echo|set /p="HelloEd25519ph" | openssl dgst -sha512 -binary > hash.bin

REM Generate signature for pre-sha512-hashed data
openssl pkeyutl -sign -inkey ed25519_private.pem -in hash.bin -pkeyopt instance:ed25519ph -out signature.bin

REM Verify signature
openssl pkeyutl -verify -pubin -inkey ed25519_public.pem -pkeyopt instance:ed25519ph -in hash.bin -sigfile signature.bin
REM output: Signature Verified Successfully

Step 2: generate Ed25519 key, signature and verify using BouncyCastle (C# .NET 8). In this updated sample, I also added 2 methods to load the public key and private key from file.

using Org.BouncyCastle.Asn1.EdEC;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System.Security.Cryptography;
using System.Text;
using Org.BouncyCastle.Math.EC.Rfc8032;

class Program
{
    static void Main()
    {
        string privateKeyPath = "ed25519_private.pem";
        string publicKeyPath = "ed25519_public.pem";
        string hashFilePath = "hash.bin";
        string signatureFilePath = "signature.bin";

        // 1. Generate Ed25519 Key Pair
        AsymmetricCipherKeyPair keyPair = GenerateEd25519KeyPair();
        Ed25519PrivateKeyParameters privateKey = (Ed25519PrivateKeyParameters)keyPair.Private;
        Ed25519PublicKeyParameters publicKey = (Ed25519PublicKeyParameters)keyPair.Public;

        // 2. Save keys to PEM files
        SavePrivateKeyToPem2(privateKeyPath, privateKey);
        SavePublicKeyToPem(publicKeyPath, publicKey);
        Console.WriteLine("Keys saved as PEM files!");

        // 3. Load the private key from the PEM file
        Ed25519PrivateKeyParameters privateKey2 = LoadPrivateKeyFromPem(privateKeyPath);

        // 4. Load the public key from the PEM file
        Ed25519PublicKeyParameters publicKey2 = LoadPublicKeyFromPem(publicKeyPath);

        // 5. Pre-hash the message using SHA-512
        byte[] message = Encoding.UTF8.GetBytes("HelloEd25519ph");
        byte[] preHashedMessage = SHA512.HashData(message);

        // 6. Save pre-hashed message to file (Base64 encoded)
        File.WriteAllBytes(hashFilePath, preHashedMessage);
        Console.WriteLine($"Pre-hashed message saved to {hashFilePath}");

        // 7. Sign with Ed25519ph (using empty context instead of null)
        byte[] signature = SignEd25519phLowLevel(privateKey2, preHashedMessage, new byte[0]);

        // 8. Save the signature to a binary file
        File.WriteAllBytes(signatureFilePath, signature);
        Console.WriteLine($"Signature saved to {signatureFilePath}");

        // 9. Verify the signature
        bool isValid = VerifyEd25519phLowLevel(publicKey2, preHashedMessage, signature, new byte[0]);
        Console.WriteLine("Signature valid: " + isValid);
    }

    // Generate Ed25519 Key Pair
    static AsymmetricCipherKeyPair GenerateEd25519KeyPair()
    {
        var keyGen = new Ed25519KeyPairGenerator();
        keyGen.Init(new KeyGenerationParameters(new SecureRandom(), 256));
        return keyGen.GenerateKeyPair();
    }

    // Save Ed25519 Private Key in OpenSSL-compatible PKCS#8 format
    static void SavePrivateKeyToPem2(string filePath, Ed25519PrivateKeyParameters privateKey)
    {
        PrivateKeyInfo privateKeyInfo = new PrivateKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), new DerOctetString(privateKey.GetEncoded()));
        using (TextWriter textWriter = new StreamWriter(filePath))
        {
            var pemWriter = new PemWriter(textWriter);
            pemWriter.WriteObject(privateKeyInfo);
        }
    }

    // Load Ed25519 Private Key from PEM file
    static Ed25519PrivateKeyParameters LoadPrivateKeyFromPem(string filePath)
    {
        using (TextReader textReader = new StreamReader(filePath))
        {
            var pemReader = new PemReader(textReader);
            return (Ed25519PrivateKeyParameters)pemReader.ReadObject();
        }
    }

    // Load Ed25519 Public Key from PEM file
    static Ed25519PublicKeyParameters LoadPublicKeyFromPem(string filePath)
    {
        using (TextReader textReader = new StreamReader(filePath))
        {
            var pemReader = new PemReader(textReader);
            return (Ed25519PublicKeyParameters)pemReader.ReadObject();
        }
    }

    // Save Ed25519 Public Key as PEM
    static void SavePublicKeyToPem(string filePath, Ed25519PublicKeyParameters publicKey)
    {
        using (TextWriter textWriter = new StreamWriter(filePath))
        {
            var pemWriter = new PemWriter(textWriter);
            pemWriter.WriteObject(publicKey);
        }
    }

    // Sign SHA-512 pre-hashed data with Ed25519ph (must provide a non-null context)
    static byte[] SignEd25519phLowLevel(Ed25519PrivateKeyParameters privateKey, byte[] preHashedMessage, byte[] context)
    {
        byte[] signature = new byte[64];
        privateKey.Sign(Ed25519.Algorithm.Ed25519ph, context, preHashedMessage, 0, preHashedMessage.Length, signature, 0);
        return signature;
    }

    // Verify Ed25519ph signature (must provide a non-null context)
    static bool VerifyEd25519phLowLevel(Ed25519PublicKeyParameters publicKey, byte[] preHashedMessage, byte[] signature, byte[] context)
    {
        return publicKey.Verify(Ed25519.Algorithm.Ed25519ph, context, preHashedMessage, 0, preHashedMessage.Length, signature, 0);
    }
}

Output:

Keys saved as PEM files!
Pre-hashed message saved to hash.bin
Signature saved to signature.bin
Signature valid: True

Step 3: use OpenSSL to read the public/private key + verify signature generated by BouncyCastle:

openssl pkey -in ed25519_public.pem -pubin -text -noout
ED25519 Public-Key:
pub:
    36:a1:d3:1e:7d:b0:be:64:fe:ad:ec:dc:ef:48:97:
    33:4c:07:ed:1a:e1:d1:be:5e:03:ae:a3:e3:11:d5:
    6a:7f

>openssl pkey -in ed25519_private.pem -text -noout
ED25519 Private-Key:
priv:
    3a:18:7a:f5:48:2f:91:f1:49:3d:cf:06:e8:3e:6b:
    61:17:8a:34:52:2a:87:f2:75:27:ef:65:b7:0f:74:
    18:76
pub:
    36:a1:d3:1e:7d:b0:be:64:fe:ad:ec:dc:ef:48:97:
    33:4c:07:ed:1a:e1:d1:be:5e:03:ae:a3:e3:11:d5:
    6a:7f

>openssl pkeyutl -verify -pubin -inkey ed25519_public.pem -pkeyopt instance:ed25519ph -in hash.bin -sigfile signature.bin
Signature Verified Successfully
Share Improve this question edited 2 days ago phibao37 asked Apr 1 at 3:08 phibao37phibao37 2,3824 gold badges27 silver badges38 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 2

Question 1: is there anyway to force BouncyCastle to generate private key PEM that is compatible with OpenSSL ?

The current C#/BouncyCastle code generates the key in OneAsymmetricKey format as described in RFC 5958, sec. 2. In contrast, the OpenSSL statement applies the PKCS#8 format, which contains the PrivateKeyInfo, as described in RFC5208, sec. 5.
Both formats are explained in more detail here. OneAsymmetricKey is actually very rarely used, so it's a little strange that C#/BouncyCastle defaults to this format.

To export the key in PKCS#8 format, the following change can be made in SavePrivateKeyToPem():

static void SavePrivateKeyToPem(string filePath, Ed25519PrivateKeyParameters privateKey)
{
    PrivateKeyInfo privateKeyInfo = new PrivateKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), new DerOctetString(privateKey.GetEncoded())); 
    using (TextWriter textWriter = new StreamWriter(filePath))
    {
        var pemWriter = new PemWriter(textWriter);
        pemWriter.WriteObject(privateKeyInfo);
    }
}

The PKCS#8 key generated in this way can now be output with openssl pkey -in ed25519_private.pem -text -noout.


Test:

using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.EdEC;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System;
using System.IO;
                    
public class Program
{
    public static void Main()
    {
        AsymmetricCipherKeyPair keyPair = GenerateEd25519KeyPair();
        Ed25519PrivateKeyParameters privateKey = (Ed25519PrivateKeyParameters)keyPair.Private;
        SavePrivateKeyToPem(privateKey);
        SavePrivateKeyToPemPkcs8(privateKey);
    }
    
    static void SavePrivateKeyToPem(Ed25519PrivateKeyParameters privateKey)
    {
        using (TextWriter textWriter = new StringWriter())
        {
            var pemWriter = new PemWriter(textWriter);
            pemWriter.WriteObject(privateKey);
            Console.WriteLine(textWriter.ToString());
        }
    }
    
    static void SavePrivateKeyToPemPkcs8(Ed25519PrivateKeyParameters privateKey)
    {
        PrivateKeyInfo privateKeyInfo = new PrivateKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), new DerOctetString(privateKey.GetEncoded())); // fix: add this line
        using (TextWriter textWriter = new StringWriter())
        {
            var pemWriter = new PemWriter(textWriter);
            pemWriter.WriteObject(privateKeyInfo);
            Console.WriteLine(textWriter.ToString());
        }
    }
    
    static AsymmetricCipherKeyPair GenerateEd25519KeyPair()
    {
        var keyGen = new Ed25519KeyPairGenerator();
        keyGen.Init(new KeyGenerationParameters(new SecureRandom(), 256));
        return keyGen.GenerateKeyPair();
    }
}

Sample output:

-----BEGIN PRIVATE KEY-----
MFECAQEwBQYDK2VwBCIEIDkH9EEKYaV7qKQ/PGqkEBy/ql7ZroBq7xP+Ew5nSfVK
gSEA9e6BNsrMnRMHrf1wNL1tFvfX58Dz8hqe3aUqKAEZYqY=
-----END PRIVATE KEY-----

-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIDkH9EEKYaV7qKQ/PGqkEBy/ql7ZroBq7xP+Ew5nSfVK
-----END PRIVATE KEY-----

Question 2: why OpenSSL verification is failure here? Is there any problem in the sample C# program?

hash.txt contains the Base64 encoded hash. The flaw in the current code is that the Base64 encoded hash is used in the OpenSSL statement. This is wrong, instead, the raw hash is required.
You must therefore either Base64 decode the data before using it in the OpenSSL statement or store the raw hash in the C# code from the outset, e.g. with File.WriteAllBytes(hashFilePath, preHashedMessage).
With this change, the verification with the OpenSSL statement is successful.


Note that actually not the message hash but the message itself should be passed to Ed25519phSigner#BlockUpdate(). In the current code, the message hash is passed, which means that it is hashed twice (this is not wrong per se, but unnecessary).
If it is required to pass the message hash (e.g. if only this is known), the low level functions Ed25519PrivateKeyParameters#Sign() and Ed25519PublicKeyParameters.Verify() can be used.
This can be verified most conveniently with an Ed25519ph test vector from RFC8032, section 7.3:

using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using System.Security.Cryptography;
using System.Text;
using Org.BouncyCastle.Math.EC.Rfc8032;
using Org.BouncyCastle.OpenSsl;

class Program
{
    static void Main(string[] args)
    {
        // test evctor for for Ed25519ph from RFC8032, section 7.3 
        string ed25519pkcs8 = @"-----BEGIN PRIVATE KEY-----
                    MC4CAQAwBQYDK2VwBCIEIIM/5iQJI3udYux3WHUgkR6adZzsHRl1W32pAbltyj1C
                    -----END PRIVATE KEY-----";
        PemReader pemReaderPrivate = new PemReader(new StringReader(ed25519pkcs8));
        Ed25519PrivateKeyParameters privateKey = (Ed25519PrivateKeyParameters)pemReaderPrivate.ReadObject();
        Ed25519PublicKeyParameters publicKey = privateKey.GeneratePublicKey();
        
        byte[] message = Encoding.UTF8.GetBytes("abc");
        byte[] preHashedMessage = SHA512.HashData(message);

        {
            byte[] signature = SignEd25519ph(privateKey, message, new byte[0]);
            bool isValid = VerifyEd25519ph(publicKey, message, signature, new byte[0]);
            Console.WriteLine(Convert.ToHexString(signature));
            Console.WriteLine(isValid);
        }
        {
            byte[] signature = SignEd25519phLowLevel(privateKey, preHashedMessage, new byte[0]);
            bool isValid = VerifyEd25519phLowLevel(publicKey, preHashedMessage, signature, new byte[0]);
            Console.WriteLine(Convert.ToHexString(signature));
            Console.WriteLine(isValid);
        }
    }

    static byte[] SignEd25519ph(Ed25519PrivateKeyParameters privateKey, byte[] message, byte[] context)
    {
        var signer = new Ed25519phSigner(context); 
        signer.Init(true, privateKey);
        signer.BlockUpdate(message, 0, message.Length);
        return signer.GenerateSignature();
    }

    static bool VerifyEd25519ph(Ed25519PublicKeyParameters publicKey, byte[] message, byte[] signature, byte[] context)
    {
        var verifier = new Ed25519phSigner(context); 
        verifier.Init(false, publicKey);
        verifier.BlockUpdate(message, 0, message.Length);
        return verifier.VerifySignature(signature);
    }

    static byte[] SignEd25519phLowLevel(Ed25519PrivateKeyParameters privateKey, byte[] preHashedMessage, byte[] context)
    {
        byte[] signature = new byte[64];
        privateKey.Sign(Ed25519.Algorithm.Ed25519ph, new byte[0], preHashedMessage, 0, preHashedMessage.Length, signature, 0);
        return signature;
    }

    static bool VerifyEd25519phLowLevel(Ed25519PublicKeyParameters publicKey, byte[] preHashedMessage, byte[] signature, byte[] context)
    {
        return publicKey.Verify(Ed25519.Algorithm.Ed25519ph, new byte[0], preHashedMessage, 0, preHashedMessage.Length, signature, 0);
    }
}

Output:

98A70222F0B8121AA9D30F813D683F809E462B469C7FF87639499BB94E6DAE4131F85042463C2A355A2003D062ADF5AAA10B8C61E636062AAAD11C2A26083406
True
98A70222F0B8121AA9D30F813D683F809E462B469C7FF87639499BB94E6DAE4131F85042463C2A355A2003D062ADF5AAA10B8C61E636062AAAD11C2A26083406
True

This signature matches the signature of the test vector. Furthermore, the signature can be verified with the following OpenSSL statement (v3.4):

openssl pkeyutl -verify -rawin -pubin -inkey <path to public key> -pkeyopt instance:ed25519ph -in <path to message> -sigfile <path to signature>

using the following data:

  • public key:

    -----BEGIN PUBLIC KEY-----
    MCowBQYDK2VwAyEA7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8=
    -----END PUBLIC KEY-----
    
  • message:

    abc
    
  • signature:

    0x98A70222F0B8121AA9D30F813D683F809E462B469C7FF87639499BB94E6DAE4131F85042463C2A355A2003D062ADF5AAA10B8C61E636062AAAD11C2A26083406
    

Note that in the OpenSSL statement, with the option rawin set, the actual message must be passed, and with the option rawin not set, the (SHA512) hash of the message must be passed (tested on v3.4). The rawin option has been available since 3.0.


Difference between Ed25519ph and Ed25519:
With Ed25519ph, the message is additionally hashed with SHA-512 before it is signed with Ed25519, see RFC8031, sec. 5.1. This does not mean that the implementations of the respective libraries generally require the hashed message instead of the actual message. Many implementations support both, e.g. OpenSSL v3.4-pkeyutl (with rawin option the actual message is to be passed, without rawin the message hash) or C#/BouncyCastle (with Ed25519phSigner#BlockUpdate() the actual message is to be passed, with Ed25519PrivateKeyParameters#Sign() and Ed25519PublicKeyParameters.Verify() the message hash).

发布评论

评论列表(0)

  1. 暂无评论