Skip to content
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

ECDH option compatibility with OpenSSL generated PEM files #52

Open
KEINOS opened this issue Dec 25, 2024 · 0 comments
Open

ECDH option compatibility with OpenSSL generated PEM files #52

KEINOS opened this issue Dec 25, 2024 · 0 comments

Comments

@KEINOS
Copy link
Owner

KEINOS commented Dec 25, 2024

TL; DR

While enhancing the use of ECDH, ensure compatibility of generated key-pairs between OpenSSL genpkey and golang's crypto/ecdh.

Details

To generate key-pairs of ECDH with X25519 curve in OpenSSL command is as below:

$ openssl genpkey -algorithm X25519 -out x25519-private.pem -outpubkey x25519-public.pem

$ cat x25519-private.pem
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VuBCIEICgJptfzbyGJr1Xk1tPBiZdT3u6ut6hqGuSnGdXp7JFS
-----END PRIVATE KEY-----

$ cat x25519-public.pem
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VuAyEABCS8+49qsm8+5JH+uin9L0ksOl/I5ZtzU1xaL16lTS0=
-----END PUBLIC KEY-----

$ openssl --version
OpenSSL 3.4.0 22 Oct 2024 (Library: OpenSSL 3.4.0 22 Oct 2024)

To parse the file in go would be:

// Read the PEM file containing the public key
pubKeyPEM, err := os.ReadFile(path)
if err != nil {
	return nil, nil, "", errors.Wrap(err, "failed to read public key PEM")
}

// Decode the PEM block
block, _ := pem.Decode(pubKeyPEM)
if block == nil || block.Type != "PUBLIC KEY" {
	return nil, nil, "", errors.New("failed to decode PEM block")
}

fmt.Printf("PubKey: %x\n", block.Bytes)

But the bytes in block are not directly compatible with ecdh.PublicKey.Bytes().

This is due to the format of the decoded bytes from PEM.
The decoded bytes of the contents is typically a DER encoded ASN.1 structure.

Thus, we need to ensure the compatibility of the keys between OpenSSL and crypto/ecdh.

Here's my two cents to test.

$ openssl genpkey -algorithm X25519 -out x25519-private.pem -outpubkey x25519-public.pem

$ cat x25519-private.pem
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VuBCIEICgJptfzbyGJr1Xk1tPBiZdT3u6ut6hqGuSnGdXp7JFS
-----END PRIVATE KEY-----

$ cat x25519-public.pem
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VuAyEABCS8+49qsm8+5JH+uin9L0ksOl/I5ZtzU1xaL16lTS0=
-----END PUBLIC KEY-----

$ go run main.go
Successfully parsed private key: *ecdh.PrivateKey
Private Key (Parsed): 2809a6d7f36f2189af55e4d6d3c1899753deeeaeb7a86a1ae4a719d5e9ec9152
Private Key (DER HEX): 302e020100300506032b656e042204202809a6d7f36f2189af55e4d6d3c1899753deeeaeb7a86a1ae4a719d5e9ec9152
Private Key (DER Base64): MC4CAQAwBQYDK2VuBCIEICgJptfzbyGJr1Xk1tPBiZdT3u6ut6hqGuSnGdXp7JFS
Successfully parsed public key: *ecdh.PublicKey
Public Key (DER Base64): MCowBQYDK2VuAyEABCS8+49qsm8+5JH+uin9L0ksOl/I5ZtzU1xaL16lTS0=
Public Key (DER Base64 from file): MCowBQYDK2VuAyEABCS8+49qsm8+5JH+uin9L0ksOl/I5ZtzU1xaL16lTS0=
Derived Public Key (Base64): BCS8+49qsm8+5JH+uin9L0ksOl/I5ZtzU1xaL16lTS0=
Does OpenSSL public key match derived public key? true

The main.go is:

package main

import (
	"crypto/ecdh"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"os"

	"github.com/pkg/errors"
)

func main() {
	// Read and parse the private key from PEM file
	privKey, privKeyDER, privKeyBase64, err := PEMtoECDHprivate("x25519-private.pem")
	panicOnError(err)

	// Print the parsed private key information
	fmt.Printf("Successfully parsed private key: %T\n", privKey)
	fmt.Printf("Private Key (Parsed): %x\n", privKey.Bytes())
	fmt.Printf("Private Key (DER HEX): %x\n", privKeyDER)
	fmt.Printf("Private Key (DER Base64): %s\n", privKeyBase64)

	// Read and parse the public key from PEM file
	pubKey, pubKeyDER, pubKeyBase64, err := PEMtoECDHpublic("x25519-public.pem")
	panicOnError(err)

	// Print the parsed public key information
	fmt.Printf("Successfully parsed public key: %T\n", pubKey)
	fmt.Printf("Public Key (DER Base64): %s\n", pubKeyBase64)

	// Print the DER-encoded public key in Base64
	fmt.Printf("Public Key (DER Base64 from file): %s\n", base64.StdEncoding.EncodeToString(pubKeyDER))

	// Derive the public key from the private key and compare it with the parsed public key
	derivedPubKey := privKey.PublicKey()

	// Print the derived public key in Base64
	fmt.Printf("Derived Public Key (Base64): %s\n", base64.StdEncoding.EncodeToString(derivedPubKey.Bytes()))

	// Compare the parsed public key with the derived public key
	isMatch := base64.StdEncoding.EncodeToString(pubKey.Bytes()) == base64.StdEncoding.EncodeToString(derivedPubKey.Bytes())
	fmt.Printf("Does OpenSSL public key match derived public key? %v\n", isMatch)
}

// PEMtoECDHprivate reads a PEM file containing an ECDH private key and returns:
// - the parsed key in ecdh.PrivateKey type
// - the raw private key bytes with DER encoded ASN.1 structure
// - the Base64 encoded of the raw private key
// - an error if any
func PEMtoECDHprivate(path string) (*ecdh.PrivateKey, []byte, string, error) {
	// Read the PEM file containing the private key
	privKeyPEM, err := os.ReadFile(path)
	if err != nil {
		return nil, nil, "", errors.Wrap(err, "failed to read private key PEM")
	}

	// Decode the PEM block
	block, _ := pem.Decode(privKeyPEM)
	if block == nil || block.Type != "PRIVATE KEY" {
		return nil, nil, "", errors.New("failed to decode PEM block")
	}

	// Parse the PKCS#8 private key to get *ecdh.PrivateKey
	parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, nil, "", errors.Wrap(err, "failed to parse PKCS#8 private key")
	}

	// Ensure the key is of type *ecdh.PrivateKey
	privKey, ok := parsedKey.(*ecdh.PrivateKey)
	if !ok {
		return nil, nil, "", errors.Errorf("unexpected private key type: %T", parsedKey)
	}

	// Base64 encode the raw private key (DER format)
	privKeyBase64 := base64.StdEncoding.EncodeToString(block.Bytes)
	return privKey, block.Bytes, privKeyBase64, nil
}

// PEMtoECDHpublic reads a PEM file containing an ECDH public key and returns:
// - the parsed key in ecdh.PublicKey type
// - the raw public key bytes with DER encoded ASN.1 structure
// - the Base64 encoded of the raw public key
// - an error if any
func PEMtoECDHpublic(path string) (*ecdh.PublicKey, []byte, string, error) {
	// Read the PEM file containing the public key
	pubKeyPEM, err := os.ReadFile(path)
	if err != nil {
		return nil, nil, "", errors.Wrap(err, "failed to read public key PEM")
	}

	// Decode the PEM block
	block, _ := pem.Decode(pubKeyPEM)
	if block == nil || block.Type != "PUBLIC KEY" {
		return nil, nil, "", errors.New("failed to decode PEM block")
	}

	// Parse the public key from the PEM
	parsedKey, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, nil, "", errors.Wrap(err, "failed to parse PKIX public key")
	}

	// Ensure the key is of type *ecdh.PublicKey
	pubKey, ok := parsedKey.(*ecdh.PublicKey)
	if !ok {
		return nil, nil, "", errors.Errorf("unexpected public key type: %T", parsedKey)
	}

	// Base64 encode the raw public key (DER format)
	pubKeyBase64 := base64.StdEncoding.EncodeToString(block.Bytes)
	return pubKey, block.Bytes, pubKeyBase64, nil
}

// Panic if an error occurs
func panicOnError(err error) {
	if err != nil {
		panic(err)
	}
}
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

No branches or pull requests

1 participant