Created
October 28, 2025 11:28
-
-
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?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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