Skip to content

Commit 643f687

Browse files
committed
add derivePublicKey function
Signed-off-by: Vasilii Bulatov <[email protected]>
1 parent 8cb06fe commit 643f687

File tree

4 files changed

+158
-6
lines changed

4 files changed

+158
-6
lines changed

crypto.go

+40-6
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ import (
2828
"io"
2929
"math/big"
3030
"net"
31-
"time"
32-
3331
"strings"
32+
"time"
3433

3534
"github.com/google/uuid"
3635
bcrypt_lib "golang.org/x/crypto/bcrypt"
@@ -198,17 +197,17 @@ func generatePrivateKey(typ string) string {
198197
return fmt.Sprintf("failed to generate private key: %s", err)
199198
}
200199

201-
return string(pem.EncodeToMemory(pemBlockForKey(priv)))
200+
return string(pem.EncodeToMemory(pemBlockForPrivKey(priv)))
202201
}
203202

204203
// DSAKeyFormat stores the format for DSA keys.
205-
// Used by pemBlockForKey
204+
// Used by pemBlockForPrivKey
206205
type DSAKeyFormat struct {
207206
Version int
208207
P, Q, G, Y, X *big.Int
209208
}
210209

211-
func pemBlockForKey(priv interface{}) *pem.Block {
210+
func pemBlockForPrivKey(priv interface{}) *pem.Block {
212211
switch k := priv.(type) {
213212
case *rsa.PrivateKey:
214213
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
@@ -232,6 +231,14 @@ func pemBlockForKey(priv interface{}) *pem.Block {
232231
}
233232
}
234233

234+
func pemBlockForPubKey(pub interface{}) (*pem.Block, error) {
235+
b, err := x509.MarshalPKIXPublicKey(pub)
236+
if err != nil {
237+
return nil, err
238+
}
239+
return &pem.Block{Type: "PUBLIC KEY", Bytes: b}, nil
240+
}
241+
235242
func parsePrivateKeyPEM(pemBlock string) (crypto.PrivateKey, error) {
236243
block, _ := pem.Decode([]byte(pemBlock))
237244
if block == nil {
@@ -293,6 +300,33 @@ func getPublicKey(priv crypto.PrivateKey) (crypto.PublicKey, error) {
293300
}
294301
}
295302

303+
func derivePublicKey(privPEM string) (string, error) {
304+
priv, err := parsePrivateKeyPEM(privPEM)
305+
if err != nil {
306+
return "", fmt.Errorf("error parsing private key: %s", err)
307+
}
308+
309+
pub, err := getPublicKey(priv)
310+
if err != nil {
311+
return "", fmt.Errorf("error getting public key: %s", err)
312+
}
313+
314+
pubBlock, err := pemBlockForPubKey(pub)
315+
if err != nil {
316+
return "", fmt.Errorf("error getting public key PEM block: %s", err)
317+
}
318+
319+
pubBuffer := bytes.Buffer{}
320+
if err := pem.Encode(
321+
&pubBuffer,
322+
pubBlock,
323+
); err != nil {
324+
return "", fmt.Errorf("error pem-encoding public key: %s", err)
325+
}
326+
327+
return pubBuffer.String(), nil
328+
}
329+
296330
type certificate struct {
297331
Cert string
298332
Key string
@@ -534,7 +568,7 @@ func getCertAndKey(
534568
keyBuffer := bytes.Buffer{}
535569
if err := pem.Encode(
536570
&keyBuffer,
537-
pemBlockForKey(signeeKey),
571+
pemBlockForPrivKey(signeeKey),
538572
); err != nil {
539573
return "", "", fmt.Errorf("error pem-encoding key: %s", err)
540574
}

crypto_test.go

+107
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,113 @@ func TestGenPrivateKey(t *testing.T) {
186186
}
187187
}
188188

189+
func TestDerivePublicKey(t *testing.T) {
190+
tpl := `{{genPrivateKey "rsa" | derivePublicKey}}`
191+
out, err := runRaw(tpl, nil)
192+
if err != nil {
193+
t.Error(err)
194+
}
195+
if !strings.Contains(out, "PUBLIC KEY") {
196+
t.Error("Expected PUBLIC KEY", out)
197+
}
198+
tpl = `{{genPrivateKey "dsa" | derivePublicKey}}`
199+
out, err = runRaw(tpl, nil)
200+
// x509.MarshalPKIXPublicKey() does not support DSA keys
201+
if err == nil || !strings.Contains(err.Error(), "x509: unsupported public key type") {
202+
t.Error("Expected error to contain 'x509: unsupported public key type'", err)
203+
}
204+
tpl = `{{genPrivateKey "ecdsa" | derivePublicKey}}`
205+
out, err = runRaw(tpl, nil)
206+
if err != nil {
207+
t.Error(err)
208+
}
209+
if !strings.Contains(out, "PUBLIC KEY") {
210+
t.Error("Expected PUBLIC KEY", out)
211+
}
212+
tpl = `{{genPrivateKey "ed25519" | derivePublicKey}}`
213+
out, err = runRaw(tpl, nil)
214+
if err != nil {
215+
t.Error(err)
216+
}
217+
if !strings.Contains(out, "PUBLIC KEY") {
218+
t.Error("Expected PUBLIC KEY", out)
219+
}
220+
testPrivateKey := `-----BEGIN RSA PRIVATE KEY-----
221+
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
270+
-----END RSA PRIVATE KEY-----`
271+
expectedPublicKey := `-----BEGIN PUBLIC KEY-----
272+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjrVUJpXK13XN7o+B1OdP
273+
rOWt8xpDld3q9GX5f7HlxF98KDBrLzxLcI4nFE1XriEZSCitG7cSC9jvoiU4yA59
274+
t+fIcmU5fLAwBmkNmWnTPD2YkH7GauenL2+LaGQ/6oc3/KqhHACQ7Sj+tzOwkMiv
275+
hw7MMdrP1NEXsYQw1ht/o38EcdRf+G9w4d/YU7aKIxM7XX3evDFpada7RhBsMXoO
276+
tHA/mE4KuztIFZ6e2McB+4fVNPsYN/k9f5ta/iCBdOkG1WdZvDj7KZfLUWno6emD
277+
3oE6I1crrXeuz/tabjHuQoWhxCV2OZStDMdxFg1rAjZBsq9325kO3N6PiH+pyHrd
278+
kRZvbQBiFjlJBa+/YMJzU3dDjpCxVBGlIXuT4/22JhdPBxvGwRx9ZLKup2qbfkCx
279+
tquWMCQN+7SE3mNXxrGxBfMsFtCgVQvkVuDaGhiYQ98DgmR/sXSZ/0okWWIockoX
280+
WOrnMrXzvhMkF4zsd1CqhF6ctN4SYADCiR2VmeN2brzB3JZHh97DHWDlkWmDALSy
281+
Izka65Tg7xaEYvAluaKAkbjMYBP9t9rSE3Z5w4BkwTypAnyJkOd4NwzGon3OhEjD
282+
zHXuCEHwBPZgEcNFbYS1kezDB7QkuzbyTZvDhY+jOnJZuD6JxDLox20LBEGB1dFH
283+
cn2WwZpyAtzWCqPvtFqKpA0CAwEAAQ==
284+
-----END PUBLIC KEY-----
285+
`
286+
tpl = `{{derivePublicKey .}}`
287+
out, err = runRaw(tpl, testPrivateKey)
288+
if err != nil {
289+
t.Error(err)
290+
}
291+
if out != expectedPublicKey {
292+
t.Error("Got incorrect public key", out)
293+
}
294+
}
295+
189296
func TestRandBytes(t *testing.T) {
190297
tpl := `{{randBytes 12}}`
191298
out, err := runRaw(tpl, nil)

docs/crypto.md

+10
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ It takes one of the values for its first param:
9191
- `rsa`: Generate an RSA 4096 key
9292
- `ed25519`: Generate an Ed25519 key
9393

94+
## derivePublicKey
95+
96+
The `derivePublicKey` function takes a PEM-encoded private key and returns the
97+
corresponding public key in PEM format.
98+
99+
Supported private key types are:
100+
- ECDSA: elliptic curve DSA key (P256)
101+
- RSA: RSA 4096 key
102+
- Ed25519: Ed25519 key
103+
94104
## buildCustomCert
95105

96106
The `buildCustomCert` function allows customizing the certificate.

functions.go

+1
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ var genericMap = map[string]interface{}{
342342
"bcrypt": bcrypt,
343343
"htpasswd": htpasswd,
344344
"genPrivateKey": generatePrivateKey,
345+
"derivePublicKey": derivePublicKey,
345346
"derivePassword": derivePassword,
346347
"buildCustomCert": buildCustomCertificate,
347348
"genCA": generateCertificateAuthority,

0 commit comments

Comments
 (0)