Skip to content

Instantly share code, notes, and snippets.

@kabakaev
Created October 28, 2025 11:28
Show Gist options
  • Select an option

  • Save kabakaev/c499f638e9c533b306f6286303c198a1 to your computer and use it in GitHub Desktop.

Select an option

Save kabakaev/c499f638e9c533b306f6286303c198a1 to your computer and use it in GitHub Desktop.
Is it possible to sign an X.509 certificate by `openssl dgst` command?
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"fmt"
"math/big"
"os"
"os/exec"
"time"
)
// This example program demonstrates how to generate a custom X.509 certificate.
// It takes a CSR, constructs a TBSCertificate, signs it using `openssl dgst`,
// and then wraps it into a final X.509 certificate structure.
//
// This approach is useful when direct access to the CA's private key is not
// available, and signing must be delegated to an external tool like `openssl dgst`.
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func run() error {
// In a real application, you would receive a CSR.
// For this example, we will generate a new private key and a CSR.
// The CSR will be provided as a PEM-encoded byte slice.
csrPEM, privateKey, err := createTestCSR()
if err != nil {
return fmt.Errorf("failed to create test CSR: %w", err)
}
// For the signing process via `openssl dgst`, we need a CA key.
// In a real scenario, this key would be managed by the system that `openssl dgst` runs on.
// For this example, we'll generate one and save it to a temporary file.
caKeyFile, err := createTempCAKeyFile()
if err != nil {
return fmt.Errorf("failed to create temporary CA key file: %w", err)
}
defer os.Remove(caKeyFile.Name())
// Create a CA certificate (self-signed) using openssl so we can verify the
// generated leaf certificate against it. We create a temporary file for
// the CA certificate and call `openssl req -new -x509 -key <caKey>`.
caCertFile, err := os.CreateTemp("", "ca-cert-*.pem")
if err != nil {
return fmt.Errorf("failed to create temp CA cert file: %w", err)
}
// remove the file on exit
defer os.Remove(caCertFile.Name())
caCertFile.Close()
// Use openssl to create a self-signed CA certificate readable by `openssl verify`.
cmd := exec.Command("openssl", "req", "-new", "-x509", "-key", caKeyFile.Name(), "-subj", "/CN=My Test CA", "-days", "365", "-out", caCertFile.Name())
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to create CA certificate with openssl: %s\n%w", out, err)
}
// The main process: generate a certificate from the CSR.
certificateBytes, err := generateCertificateFromCSR(csrPEM, caKeyFile.Name())
if err != nil {
return fmt.Errorf("failed to generate certificate: %w", err)
}
// Write the generated certificate to a temp file so openssl can read it.
certFile, err := os.CreateTemp("", "cert-*.pem")
if err != nil {
return fmt.Errorf("failed to create temp cert file: %w", err)
}
certPath := certFile.Name()
if _, err := certFile.Write(certificateBytes); err != nil {
certFile.Close()
return fmt.Errorf("failed to write certificate to temp file: %w", err)
}
certFile.Close()
defer os.Remove(certPath)
// Validate the generated certificate against the CA certificate using openssl.
// `openssl verify -CAfile <caCert> <cert>` returns exit code 0 and prints
// "<cert>: OK" on success.
verifyCmd := exec.Command("openssl", "verify", "-CAfile", caCertFile.Name(), certPath)
verifyOut, err := verifyCmd.CombinedOutput()
if err != nil {
fmt.Printf("certificate verification failed: %s\n%v\n", verifyOut, err)
} else {
fmt.Printf("Certificate verification succeeded: %s\n", string(verifyOut))
}
// Output the resulting certificate
fmt.Println("Successfully generated certificate:")
fmt.Println(string(certificateBytes))
// For demonstration, we can also show the private key that was generated for the CSR.
// In a real application, this key would be kept private by the requester.
fmt.Println("\nPrivate key for the certificate:")
fmt.Println(string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKey})))
return err
}
// generateCertificateFromCSR orchestrates the certificate creation process.
func generateCertificateFromCSR(csrPEM []byte, caKeyPath string) ([]byte, error) {
// Step 1: Parse the CSR.
csr, err := parseCSR(csrPEM)
if err != nil {
return nil, fmt.Errorf("failed to parse CSR: %v", err)
}
// Step 2: Construct the TBSCertificate bytes.
tbsCertBytes, signatureAlgorithm, err := createTBSCertificate(csr)
if err != nil {
return nil, fmt.Errorf("failed to create TBSCertificate: %w", err)
}
// Step 3: Sign the TBSCertificate bytes using `openssl dgst`.
signature, err := signWithOpenSSL(tbsCertBytes, caKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to sign TBSCertificate: %w", err)
}
// Step 4: Assemble the final certificate.
return assembleCertificate(tbsCertBytes, signatureAlgorithm, signature)
}
// createTestCSR generates a new private key and a corresponding CSR for demonstration purposes.
func createTestCSR() (csrPEM []byte, privateKeyBytes []byte, err error) {
// Generate a new private key
keyBytes, err := x509.MarshalPKCS8PrivateKey(generatePrivateKey())
if err != nil {
return nil, nil, err
}
privateKey, err := x509.ParsePKCS8PrivateKey(keyBytes)
if err != nil {
return nil, nil, err
}
// Create a CSR template
csrTemplate := x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "example.com",
},
SignatureAlgorithm: x509.ECDSAWithSHA256,
}
// Create the CSR
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, privateKey)
if err != nil {
return nil, nil, err
}
csrPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
return csrPEM, keyBytes, nil
}
// createTempCAKeyFile creates a temporary file with a new private key to act as the CA key.
func createTempCAKeyFile() (*os.File, error) {
caKey := generatePrivateKey()
caKeyBytes, err := x509.MarshalPKCS8PrivateKey(caKey)
if err != nil {
return nil, err
}
caKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: caKeyBytes})
tmpfile, err := os.CreateTemp("", "ca-key-*.pem")
if err != nil {
return nil, err
}
if _, err := tmpfile.Write(caKeyPEM); err != nil {
tmpfile.Close()
return nil, err
}
return tmpfile, nil
}
// generatePrivateKey creates a new RSA private key.
func generatePrivateKey() interface{} {
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
return privateKey
}
// parseCSR parses a PEM-encoded CSR.
func parseCSR(csrPEM []byte) (*x509.CertificateRequest, error) {
block, _ := pem.Decode(csrPEM)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block containing CSR")
}
return x509.ParseCertificateRequest(block.Bytes)
}
// createTBSCertificate constructs the "To-Be-Signed" certificate structure.
func createTBSCertificate(csr *x509.CertificateRequest) ([]byte, pkix.AlgorithmIdentifier, error) {
// Define the certificate template.
// In a real CA, you would have a proper issuer, and a robust serial number generation.
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Issuer: pkix.Name{
CommonName: "My Test CA",
},
Subject: csr.Subject,
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
SignatureAlgorithm: csr.SignatureAlgorithm,
PublicKey: csr.PublicKey,
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
}
// The `x509.CreateCertificate` function would normally do this, but we are doing it manually.
// This is a simplified version of what happens inside `x509.CreateCertificate`.
// We are creating the TBS (To-Be-Signed) part of the certificate.
tbsCert, err := createTBSCertificateBytes(&template)
if err != nil {
return nil, pkix.AlgorithmIdentifier{}, err
}
return tbsCert, getSignatureAlgorithm(csr.SignatureAlgorithm), nil
}
// signWithOpenSSL signs the given data using `openssl dgst`.
func signWithOpenSSL(data []byte, keyPath string) ([]byte, error) {
// Write the data to be signed to a temporary file.
tbsFile, err := os.CreateTemp("", "tbs-*.bin")
if err != nil {
return nil, err
}
defer os.Remove(tbsFile.Name())
if _, err := tbsFile.Write(data); err != nil {
tbsFile.Close()
return nil, err
}
tbsFile.Close()
// Prepare the output file for the signature.
sigFile, err := os.CreateTemp("", "sig-*.bin")
if err != nil {
return nil, err
}
defer os.Remove(sigFile.Name())
sigFile.Close() // Close it so openssl can write to it.
// Execute the openssl command.
cmd := exec.Command("openssl", "dgst", "-sha256", "-sign", keyPath, "-out", sigFile.Name(), tbsFile.Name())
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("openssl command failed: %s\n%v", output, err)
}
// Read the signature from the output file.
return os.ReadFile(sigFile.Name())
}
// assembleCertificate combines the TBSCertificate, signature algorithm, and signature into a final certificate.
func assembleCertificate(tbsCertBytes []byte, sigAlgo pkix.AlgorithmIdentifier, signature []byte) ([]byte, error) {
// Marshal signature algorithm and signature BIT STRING.
sigAlgoDER, err := asn1.Marshal(sigAlgo)
if err != nil {
return nil, err
}
sigBitDER, err := asn1.Marshal(asn1.BitString{Bytes: signature, BitLength: len(signature) * 8})
if err != nil {
return nil, err
}
// Certificate is SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
content := make([]byte, 0, len(tbsCertBytes)+len(sigAlgoDER)+len(sigBitDER))
content = append(content, tbsCertBytes...)
content = append(content, sigAlgoDER...)
content = append(content, sigBitDER...)
// Wrap content in a top-level SEQUENCE (0x30) with DER length encoding.
certDER := make([]byte, 0, 2+len(content))
certDER = append(certDER, 0x30) // SEQUENCE tag
// encode length
switch {
case len(content) < 128:
certDER = append(certDER, byte(len(content)))
case len(content) < 256:
certDER = append(certDER, 0x81, byte(len(content)))
case len(content) < 65536:
certDER = append(certDER, 0x82, byte(len(content)>>8), byte(len(content)&0xff))
default:
// large sizes not expected in this demo
return nil, fmt.Errorf("certificate too large")
}
certDER = append(certDER, content...)
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}), nil
}
// This is a simplified version of the internal `createTBSCertificateBytes` from Go's `crypto/x509` package.
func createTBSCertificateBytes(template *x509.Certificate) ([]byte, error) {
// This struct mirrors the TBSCertificate structure in RFC 5280.
type tbsCertificate struct {
Version int `asn1:"optional,explicit,default:0,tag:0"`
SerialNumber *big.Int
SignatureAlgorithm pkix.AlgorithmIdentifier
Issuer asn1.RawValue
Validity struct {
NotBefore, NotAfter time.Time
}
Subject asn1.RawValue
PublicKey asn1.RawValue
Extensions []pkix.Extension `asn1:"optional,explicit,tag:3"`
}
// Marshal issuer and subject names.
issuerBytes, err := asn1.Marshal(template.Issuer.ToRDNSequence())
if err != nil {
return nil, err
}
subjectBytes, err := asn1.Marshal(template.Subject.ToRDNSequence())
if err != nil {
return nil, err
}
// Build the public key info.
publicKeyBytes, err := x509.MarshalPKIXPublicKey(template.PublicKey)
if err != nil {
return nil, err
}
tbs := tbsCertificate{
Version: 2, // X.509 v3
SerialNumber: template.SerialNumber,
SignatureAlgorithm: getSignatureAlgorithm(template.SignatureAlgorithm),
Issuer: asn1.RawValue{FullBytes: issuerBytes},
Validity: struct{ NotBefore, NotAfter time.Time }{template.NotBefore.UTC(), template.NotAfter.UTC()},
Subject: asn1.RawValue{FullBytes: subjectBytes},
PublicKey: asn1.RawValue{FullBytes: publicKeyBytes},
}
return asn1.Marshal(tbs)
}
// getSignatureAlgorithm returns the ASN.1 OID for a given signature algorithm.
func getSignatureAlgorithm(alg x509.SignatureAlgorithm) pkix.AlgorithmIdentifier {
// This is a simplified map. A real implementation would be more comprehensive.
switch alg {
case x509.SHA256WithRSA:
return pkix.AlgorithmIdentifier{
Algorithm: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11},
Parameters: asn1.RawValue{Tag: 5}, // ASN.1 NULL
}
case x509.ECDSAWithSHA256:
return pkix.AlgorithmIdentifier{
Algorithm: asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2},
}
default:
// Fallback or error for unsupported algorithms
return pkix.AlgorithmIdentifier{}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment