Skip to content

Commit cb8615d

Browse files
committed
feat: support encrypted keys
1 parent 383adc1 commit cb8615d

File tree

5 files changed

+166
-19
lines changed

5 files changed

+166
-19
lines changed

main.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ func main() {
7777
issuerFile = flag.String("issuer", "", "Path to issuer certificate (PEM)")
7878
responderFile = flag.String("responder", "", "Path to responder certificate (PEM)")
7979
keyFile = flag.String("key", "", "Path to responder private key (PEM)")
80+
keyPassword = flag.String("key-password", "", "Password for encrypted private key (if key is encrypted)")
8081
interval = flag.Duration("interval", 24*time.Hour, "OCSP response validity interval")
8182

8283
// CRL source options
@@ -101,12 +102,14 @@ func main() {
101102
fmt.Fprintf(os.Stderr, " # With local CRL file:\n")
102103
fmt.Fprintf(os.Stderr, " %s -issuer ca.pem -responder ocsp.pem -key ocsp-key.pem -crl-file /path/to/crl.der\n\n", os.Args[0])
103104
fmt.Fprintf(os.Stderr, " # Fetch issuer and responder certs from URL:\n")
104-
fmt.Fprintf(os.Stderr, " %s -issuer https://pki.example.com/ca.crt -responder https://pki.example.com/ca.crt -key ocsp-key.pem\n", os.Args[0])
105+
fmt.Fprintf(os.Stderr, " %s -issuer https://pki.example.com/ca.crt -responder https://pki.example.com/ca.crt -key ocsp-key.pem\n\n", os.Args[0])
106+
fmt.Fprintf(os.Stderr, " # With encrypted private key:\n")
107+
fmt.Fprintf(os.Stderr, " %s -issuer ca.pem -responder ocsp.pem -key ocsp-key.pem -key-password 'mypassword'\n", os.Args[0])
105108
os.Exit(1)
106109
}
107110

108111
// Create signer from paths (certs can be files or URLs, key must be file)
109-
signer, err := ocsp.NewSignerFromPaths(*issuerFile, *responderFile, *keyFile, *interval, *insecureSkipTLS)
112+
signer, err := ocsp.NewSignerFromPaths(*issuerFile, *responderFile, *keyFile, *keyPassword, *interval, *insecureSkipTLS)
110113
if err != nil {
111114
log.Fatalf("Failed to create signer: %v", err)
112115
}

ocsp/helpers.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"crypto/x509"
99
"encoding/pem"
1010
"errors"
11+
"fmt"
1112
)
1213

1314
// ParseCertificatePEM parses a PEM-encoded certificate
@@ -24,32 +25,54 @@ func ParseCertificatePEM(data []byte) (*x509.Certificate, error) {
2425
return x509.ParseCertificate(block.Bytes)
2526
}
2627

27-
// ParsePrivateKeyPEM parses a PEM-encoded private key
28+
// ParsePrivateKeyPEM parses a PEM-encoded private key (unencrypted)
2829
func ParsePrivateKeyPEM(data []byte) (crypto.Signer, error) {
30+
return ParsePrivateKeyPEMWithPassword(data, "")
31+
}
32+
33+
// ParsePrivateKeyPEMWithPassword parses a PEM-encoded private key, decrypting if necessary
34+
func ParsePrivateKeyPEMWithPassword(data []byte, password string) (crypto.Signer, error) {
2935
block, _ := pem.Decode(data)
3036
if block == nil {
3137
return nil, errors.New("failed to decode PEM block")
3238
}
3339

40+
keyBytes := block.Bytes
41+
42+
// Check if the key is encrypted (legacy PEM encryption)
43+
//nolint:staticcheck // x509.IsEncryptedPEMBlock is deprecated but needed for legacy format
44+
if x509.IsEncryptedPEMBlock(block) {
45+
if password == "" {
46+
return nil, errors.New("private key is encrypted but no password provided (use -key-password)")
47+
}
48+
var err error
49+
//nolint:staticcheck // x509.DecryptPEMBlock is deprecated but needed for legacy format
50+
keyBytes, err = x509.DecryptPEMBlock(block, []byte(password))
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to decrypt private key: %w", err)
53+
}
54+
}
55+
3456
switch block.Type {
3557
case "RSA PRIVATE KEY":
36-
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
58+
key, err := x509.ParsePKCS1PrivateKey(keyBytes)
3759
if err != nil {
38-
return nil, err
60+
return nil, fmt.Errorf("failed to parse RSA private key: %w", err)
3961
}
4062
return key, nil
4163

4264
case "EC PRIVATE KEY":
43-
key, err := x509.ParseECPrivateKey(block.Bytes)
65+
key, err := x509.ParseECPrivateKey(keyBytes)
4466
if err != nil {
45-
return nil, err
67+
return nil, fmt.Errorf("failed to parse EC private key: %w", err)
4668
}
4769
return key, nil
4870

4971
case "PRIVATE KEY":
50-
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
72+
// PKCS#8 format (may be encrypted with PBES2, not legacy PEM encryption)
73+
key, err := x509.ParsePKCS8PrivateKey(keyBytes)
5174
if err != nil {
52-
return nil, err
75+
return nil, fmt.Errorf("failed to parse PKCS#8 private key: %w", err)
5376
}
5477
switch k := key.(type) {
5578
case *rsa.PrivateKey:
@@ -59,10 +82,14 @@ func ParsePrivateKeyPEM(data []byte) (crypto.Signer, error) {
5982
case ed25519.PrivateKey:
6083
return k, nil
6184
default:
62-
return nil, errors.New("unsupported private key type")
85+
return nil, errors.New("unsupported private key type in PKCS#8 container")
6386
}
6487

88+
case "ENCRYPTED PRIVATE KEY":
89+
// PKCS#8 encrypted format - requires different handling
90+
return nil, errors.New("PKCS#8 encrypted keys (ENCRYPTED PRIVATE KEY) are not yet supported; use legacy PEM encryption or unencrypted keys")
91+
6592
default:
66-
return nil, errors.New("unsupported PEM block type: " + block.Type)
93+
return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
6794
}
6895
}

ocsp/helpers_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,119 @@ func TestParsePrivateKeyPEM_InvalidPKCS8Key(t *testing.T) {
208208
t.Error("Expected error for invalid PKCS8 key")
209209
}
210210
}
211+
212+
func TestParsePrivateKeyPEMWithPassword_EncryptedEC(t *testing.T) {
213+
// Generate an EC key
214+
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
215+
keyDER, _ := x509.MarshalECPrivateKey(key)
216+
217+
// Encrypt the PEM block with a password
218+
password := []byte("testpassword123")
219+
//nolint:staticcheck // x509.EncryptPEMBlock is deprecated but needed for testing legacy format
220+
encryptedBlock, err := x509.EncryptPEMBlock(rand.Reader, "EC PRIVATE KEY", keyDER, password, x509.PEMCipherAES256)
221+
if err != nil {
222+
t.Fatalf("Failed to encrypt PEM block: %v", err)
223+
}
224+
225+
encryptedPEM := pem.EncodeToMemory(encryptedBlock)
226+
227+
// Test successful decryption with correct password
228+
parsed, err := ParsePrivateKeyPEMWithPassword(encryptedPEM, "testpassword123")
229+
if err != nil {
230+
t.Fatalf("Failed to parse encrypted EC key: %v", err)
231+
}
232+
if _, ok := parsed.(*ecdsa.PrivateKey); !ok {
233+
t.Error("Expected ECDSA private key")
234+
}
235+
}
236+
237+
func TestParsePrivateKeyPEMWithPassword_WrongPassword(t *testing.T) {
238+
// Generate an EC key
239+
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
240+
keyDER, _ := x509.MarshalECPrivateKey(key)
241+
242+
// Encrypt the PEM block with a password
243+
password := []byte("correctpassword")
244+
//nolint:staticcheck // x509.EncryptPEMBlock is deprecated but needed for testing legacy format
245+
encryptedBlock, err := x509.EncryptPEMBlock(rand.Reader, "EC PRIVATE KEY", keyDER, password, x509.PEMCipherAES256)
246+
if err != nil {
247+
t.Fatalf("Failed to encrypt PEM block: %v", err)
248+
}
249+
250+
encryptedPEM := pem.EncodeToMemory(encryptedBlock)
251+
252+
// Test with wrong password
253+
_, err = ParsePrivateKeyPEMWithPassword(encryptedPEM, "wrongpassword")
254+
if err == nil {
255+
t.Error("Expected error for wrong password")
256+
}
257+
}
258+
259+
func TestParsePrivateKeyPEMWithPassword_EncryptedNoPassword(t *testing.T) {
260+
// Generate an EC key
261+
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
262+
keyDER, _ := x509.MarshalECPrivateKey(key)
263+
264+
// Encrypt the PEM block with a password
265+
password := []byte("testpassword")
266+
//nolint:staticcheck // x509.EncryptPEMBlock is deprecated but needed for testing legacy format
267+
encryptedBlock, err := x509.EncryptPEMBlock(rand.Reader, "EC PRIVATE KEY", keyDER, password, x509.PEMCipherAES256)
268+
if err != nil {
269+
t.Fatalf("Failed to encrypt PEM block: %v", err)
270+
}
271+
272+
encryptedPEM := pem.EncodeToMemory(encryptedBlock)
273+
274+
// Test without providing password
275+
_, err = ParsePrivateKeyPEMWithPassword(encryptedPEM, "")
276+
if err == nil {
277+
t.Error("Expected error when no password provided for encrypted key")
278+
}
279+
if err.Error() != "private key is encrypted but no password provided (use -key-password)" {
280+
t.Errorf("Unexpected error message: %v", err)
281+
}
282+
}
283+
284+
func TestParsePrivateKeyPEMWithPassword_UnencryptedWithPassword(t *testing.T) {
285+
// Generate an EC key (unencrypted)
286+
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
287+
keyDER, _ := x509.MarshalECPrivateKey(key)
288+
keyPEM := pem.EncodeToMemory(&pem.Block{
289+
Type: "EC PRIVATE KEY",
290+
Bytes: keyDER,
291+
})
292+
293+
// Providing a password for unencrypted key should still work (password is ignored)
294+
parsed, err := ParsePrivateKeyPEMWithPassword(keyPEM, "unnecessarypassword")
295+
if err != nil {
296+
t.Fatalf("Failed to parse unencrypted key with password: %v", err)
297+
}
298+
if _, ok := parsed.(*ecdsa.PrivateKey); !ok {
299+
t.Error("Expected ECDSA private key")
300+
}
301+
}
302+
303+
func TestParsePrivateKeyPEMWithPassword_EncryptedRSA(t *testing.T) {
304+
// Generate an RSA key
305+
key, _ := rsa.GenerateKey(rand.Reader, 2048)
306+
keyDER := x509.MarshalPKCS1PrivateKey(key)
307+
308+
// Encrypt the PEM block with a password
309+
password := []byte("rsapassword")
310+
//nolint:staticcheck // x509.EncryptPEMBlock is deprecated but needed for testing legacy format
311+
encryptedBlock, err := x509.EncryptPEMBlock(rand.Reader, "RSA PRIVATE KEY", keyDER, password, x509.PEMCipherAES256)
312+
if err != nil {
313+
t.Fatalf("Failed to encrypt PEM block: %v", err)
314+
}
315+
316+
encryptedPEM := pem.EncodeToMemory(encryptedBlock)
317+
318+
// Test successful decryption
319+
parsed, err := ParsePrivateKeyPEMWithPassword(encryptedPEM, "rsapassword")
320+
if err != nil {
321+
t.Fatalf("Failed to parse encrypted RSA key: %v", err)
322+
}
323+
if _, ok := parsed.(*rsa.PrivateKey); !ok {
324+
t.Error("Expected RSA private key")
325+
}
326+
}

ocsp/loader_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func TestNewSignerFromPaths_Files(t *testing.T) {
110110
files := createTestFiles(t)
111111
defer files.cleanup()
112112

113-
signer, err := NewSignerFromPaths(files.issuerFile, files.responderFile, files.keyFile, 0, false)
113+
signer, err := NewSignerFromPaths(files.issuerFile, files.responderFile, files.keyFile, "", 0, false)
114114
if err != nil {
115115
t.Fatalf("Failed to create signer: %v", err)
116116
}
@@ -137,7 +137,7 @@ func TestNewSignerFromPaths_IssuerFromURL(t *testing.T) {
137137
defer server.Close()
138138

139139
// Load issuer from URL, responder and key from files
140-
signer, err := NewSignerFromPaths(server.URL+"/issuer.pem", files.responderFile, files.keyFile, 0, false)
140+
signer, err := NewSignerFromPaths(server.URL+"/issuer.pem", files.responderFile, files.keyFile, "", 0, false)
141141
if err != nil {
142142
t.Fatalf("Failed to create signer: %v", err)
143143
}
@@ -172,7 +172,7 @@ func TestNewSignerFromPaths_BothCertsFromURL(t *testing.T) {
172172
defer server.Close()
173173

174174
// Load both certs from URL, key from file
175-
signer, err := NewSignerFromPaths(server.URL+"/issuer.pem", server.URL+"/responder.pem", files.keyFile, 0, false)
175+
signer, err := NewSignerFromPaths(server.URL+"/issuer.pem", server.URL+"/responder.pem", files.keyFile, "", 0, false)
176176
if err != nil {
177177
t.Fatalf("Failed to create signer: %v", err)
178178
}
@@ -191,7 +191,7 @@ func TestNewSignerFromPaths_InvalidIssuerURL(t *testing.T) {
191191
}))
192192
defer server.Close()
193193

194-
_, err := NewSignerFromPaths(server.URL+"/issuer.pem", files.responderFile, files.keyFile, 0, false)
194+
_, err := NewSignerFromPaths(server.URL+"/issuer.pem", files.responderFile, files.keyFile, "", 0, false)
195195
if err == nil {
196196
t.Error("Expected error for 404 on issuer URL")
197197
}
@@ -207,7 +207,7 @@ func TestNewSignerFromPaths_InvalidCertData(t *testing.T) {
207207
}))
208208
defer server.Close()
209209

210-
_, err := NewSignerFromPaths(server.URL+"/issuer.pem", files.responderFile, files.keyFile, 0, false)
210+
_, err := NewSignerFromPaths(server.URL+"/issuer.pem", files.responderFile, files.keyFile, "", 0, false)
211211
if err == nil {
212212
t.Error("Expected error for invalid certificate data")
213213
}
@@ -217,7 +217,7 @@ func TestNewSignerFromPaths_MissingKeyFile(t *testing.T) {
217217
files := createTestFiles(t)
218218
defer files.cleanup()
219219

220-
_, err := NewSignerFromPaths(files.issuerFile, files.responderFile, "/nonexistent/key.pem", 0, false)
220+
_, err := NewSignerFromPaths(files.issuerFile, files.responderFile, "/nonexistent/key.pem", "", 0, false)
221221
if err == nil {
222222
t.Error("Expected error for missing key file")
223223
}

ocsp/signer.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ func NewSignerFromFile(issuerFile, responderFile, keyFile string, interval time.
8585
// NewSignerFromPaths loads certs from file paths or URLs, and key from file only.
8686
// For -issuer and -responder: paths starting with http:// or https:// are fetched via HTTP GET.
8787
// For -key: only file paths are supported (private keys should not be fetched over network).
88-
func NewSignerFromPaths(issuerPath, responderPath, keyFile string, interval time.Duration, insecureSkipVerify bool) (Signer, error) {
88+
// If keyPassword is non-empty, it will be used to decrypt an encrypted private key.
89+
func NewSignerFromPaths(issuerPath, responderPath, keyFile, keyPassword string, interval time.Duration, insecureSkipVerify bool) (Signer, error) {
8990
// Load issuer certificate (file or URL)
9091
issuerBytes, err := LoadPEM(issuerPath, insecureSkipVerify)
9192
if err != nil {
@@ -114,7 +115,7 @@ func NewSignerFromPaths(issuerPath, responderPath, keyFile string, interval time
114115
return nil, fmt.Errorf("failed to parse responder certificate: %w", err)
115116
}
116117

117-
key, err := ParsePrivateKeyPEM(keyBytes)
118+
key, err := ParsePrivateKeyPEMWithPassword(keyBytes, keyPassword)
118119
if err != nil {
119120
return nil, fmt.Errorf("failed to parse responder key: %w", err)
120121
}

0 commit comments

Comments
 (0)