lib: add base64 hex encoding; linter fixes.

This commit is contained in:
2025-11-18 23:43:08 -08:00
parent ff34eb4eff
commit bf29d214c5
4 changed files with 134 additions and 126 deletions

View File

@@ -1,135 +1,135 @@
package certlib package certlib
import ( import (
"bytes" "bytes"
"crypto" "crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"os" "os"
) )
// LoadPrivateKey loads a private key from disk. It accepts both PEM and DER // LoadPrivateKey loads a private key from disk. It accepts both PEM and DER
// encodings and supports RSA and ECDSA keys. If the file contains a PEM block, // encodings and supports RSA and ECDSA keys. If the file contains a PEM block,
// the block type must be one of the recognised private key types. // the block type must be one of the recognised private key types.
func LoadPrivateKey(path string) (crypto.Signer, error) { func LoadPrivateKey(path string) (crypto.Signer, error) {
in, err := os.ReadFile(path) in, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
in = bytes.TrimSpace(in) in = bytes.TrimSpace(in)
if p, _ := pem.Decode(in); p != nil { if p, _ := pem.Decode(in); p != nil {
if !validPEMs[p.Type] { if !validPEMs[p.Type] {
return nil, errors.New("invalid private key file type " + p.Type) return nil, errors.New("invalid private key file type " + p.Type)
} }
return ParsePrivateKeyPEM(in) return ParsePrivateKeyPEM(in)
} }
return ParsePrivateKeyDER(in) return ParsePrivateKeyDER(in)
} }
var validPEMs = map[string]bool{ var validPEMs = map[string]bool{
"PRIVATE KEY": true, "PRIVATE KEY": true,
"RSA PRIVATE KEY": true, "RSA PRIVATE KEY": true,
"EC PRIVATE KEY": true, "EC PRIVATE KEY": true,
} }
const ( const (
curveInvalid = iota // any invalid curve curveInvalid = iota // any invalid curve
curveRSA // indicates key is an RSA key, not an EC key curveRSA // indicates key is an RSA key, not an EC key
curveP256 curveP256
curveP384 curveP384
curveP521 curveP521
) )
func getECCurve(pub any) int { func getECCurve(pub any) int {
switch pub := pub.(type) { switch pub := pub.(type) {
case *ecdsa.PublicKey: case *ecdsa.PublicKey:
switch pub.Curve { switch pub.Curve {
case elliptic.P256(): case elliptic.P256():
return curveP256 return curveP256
case elliptic.P384(): case elliptic.P384():
return curveP384 return curveP384
case elliptic.P521(): case elliptic.P521():
return curveP521 return curveP521
default: default:
return curveInvalid return curveInvalid
} }
case *rsa.PublicKey: case *rsa.PublicKey:
return curveRSA return curveRSA
default: default:
return curveInvalid return curveInvalid
} }
} }
// matchRSA compares an RSA public key from certificate against RSA public key from private key. // matchRSA compares an RSA public key from certificate against RSA public key from private key.
// It returns true on match. // It returns true on match.
func matchRSA(certPub *rsa.PublicKey, keyPub *rsa.PublicKey) bool { func matchRSA(certPub *rsa.PublicKey, keyPub *rsa.PublicKey) bool {
return keyPub.N.Cmp(certPub.N) == 0 && keyPub.E == certPub.E return keyPub.N.Cmp(certPub.N) == 0 && keyPub.E == certPub.E
} }
// matchECDSA compares ECDSA public keys for equality and compatible curve. // matchECDSA compares ECDSA public keys for equality and compatible curve.
// It returns match=true when they are on the same curve and have the same X/Y. // It returns match=true when they are on the same curve and have the same X/Y.
// If curves mismatch, match is false. // If curves mismatch, match is false.
func matchECDSA(certPub *ecdsa.PublicKey, keyPub *ecdsa.PublicKey) bool { func matchECDSA(certPub *ecdsa.PublicKey, keyPub *ecdsa.PublicKey) bool {
if getECCurve(certPub) != getECCurve(keyPub) { if getECCurve(certPub) != getECCurve(keyPub) {
return false return false
} }
if keyPub.X.Cmp(certPub.X) != 0 { if keyPub.X.Cmp(certPub.X) != 0 {
return false return false
} }
if keyPub.Y.Cmp(certPub.Y) != 0 { if keyPub.Y.Cmp(certPub.Y) != 0 {
return false return false
} }
return true return true
} }
// MatchKeys determines whether the certificate's public key matches the given private key. // MatchKeys determines whether the certificate's public key matches the given private key.
// It returns true if they match; otherwise, it returns false and a human-friendly reason. // It returns true if they match; otherwise, it returns false and a human-friendly reason.
func MatchKeys(cert *x509.Certificate, priv crypto.Signer) (bool, string) { func MatchKeys(cert *x509.Certificate, priv crypto.Signer) (bool, string) {
switch keyPub := priv.Public().(type) { switch keyPub := priv.Public().(type) {
case *rsa.PublicKey: case *rsa.PublicKey:
switch certPub := cert.PublicKey.(type) { switch certPub := cert.PublicKey.(type) {
case *rsa.PublicKey: case *rsa.PublicKey:
if matchRSA(certPub, keyPub) { if matchRSA(certPub, keyPub) {
return true, "" return true, ""
} }
return false, "public keys don't match" return false, "public keys don't match"
case *ecdsa.PublicKey: case *ecdsa.PublicKey:
return false, "RSA private key, EC public key" return false, "RSA private key, EC public key"
default: default:
return false, fmt.Sprintf("unsupported certificate public key type: %T", cert.PublicKey) return false, fmt.Sprintf("unsupported certificate public key type: %T", cert.PublicKey)
} }
case *ecdsa.PublicKey: case *ecdsa.PublicKey:
switch certPub := cert.PublicKey.(type) { switch certPub := cert.PublicKey.(type) {
case *ecdsa.PublicKey: case *ecdsa.PublicKey:
if matchECDSA(certPub, keyPub) { if matchECDSA(certPub, keyPub) {
return true, "" return true, ""
} }
// Determine a more precise reason // Determine a more precise reason
kc := getECCurve(keyPub) kc := getECCurve(keyPub)
cc := getECCurve(certPub) cc := getECCurve(certPub)
if kc == curveInvalid { if kc == curveInvalid {
return false, "invalid private key curve" return false, "invalid private key curve"
} }
if cc == curveRSA { if cc == curveRSA {
return false, "private key is EC, certificate is RSA" return false, "private key is EC, certificate is RSA"
} }
if kc != cc { if kc != cc {
return false, "EC curves don't match" return false, "EC curves don't match"
} }
return false, "public keys don't match" return false, "public keys don't match"
case *rsa.PublicKey: case *rsa.PublicKey:
return false, "private key is EC, certificate is RSA" return false, "private key is EC, certificate is RSA"
default: default:
return false, fmt.Sprintf("unsupported certificate public key type: %T", cert.PublicKey) return false, fmt.Sprintf("unsupported certificate public key type: %T", cert.PublicKey)
} }
default: default:
return false, fmt.Sprintf("unrecognised private key type: %T", priv.Public()) return false, fmt.Sprintf("unrecognised private key type: %T", priv.Public())
} }
} }

View File

@@ -182,7 +182,7 @@ func main() {
continue continue
} }
if _, err := verify.CertWith(cert, roots, nil, false); err != nil { if _, err = verify.CertWith(cert, roots, nil, false); err != nil {
fmt.Printf("%s: INVALID\n", arg) fmt.Printf("%s: INVALID\n", arg)
} else { } else {
fmt.Printf("%s: OK (expires %s)\n", arg, cert.NotAfter.Format(lib.DateShortFormat)) fmt.Printf("%s: OK (expires %s)\n", arg, cert.NotAfter.Format(lib.DateShortFormat))

View File

@@ -1,33 +1,33 @@
package main package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"git.wntrmute.dev/kyle/goutils/certlib" "git.wntrmute.dev/kyle/goutils/certlib"
"git.wntrmute.dev/kyle/goutils/die" "git.wntrmute.dev/kyle/goutils/die"
) )
// functionality refactored into certlib // functionality refactored into certlib
func main() { func main() {
var keyFile, certFile string var keyFile, certFile string
flag.StringVar(&keyFile, "k", "", "TLS private `key` file") flag.StringVar(&keyFile, "k", "", "TLS private `key` file")
flag.StringVar(&certFile, "c", "", "TLS `certificate` file") flag.StringVar(&certFile, "c", "", "TLS `certificate` file")
flag.Parse() flag.Parse()
cert, err := certlib.LoadCertificate(certFile) cert, err := certlib.LoadCertificate(certFile)
die.If(err) die.If(err)
priv, err := certlib.LoadPrivateKey(keyFile) priv, err := certlib.LoadPrivateKey(keyFile)
die.If(err) die.If(err)
matched, reason := certlib.MatchKeys(cert, priv) matched, reason := certlib.MatchKeys(cert, priv)
if matched { if matched {
fmt.Println("Match.") fmt.Println("Match.")
return return
} }
fmt.Printf("No match (%s).\n", reason) fmt.Printf("No match (%s).\n", reason)
os.Exit(1) os.Exit(1)
} }

View File

@@ -1,6 +1,7 @@
package lib package lib
import ( import (
"encoding/base64"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"os" "os"
@@ -126,6 +127,8 @@ const (
HexEncodeUpperColon HexEncodeUpperColon
// HexEncodeBytes prints the string as a sequence of []byte. // HexEncodeBytes prints the string as a sequence of []byte.
HexEncodeBytes HexEncodeBytes
// HexEncodeBase64 prints the string as a base64-encoded string.
HexEncodeBase64
) )
func (m HexEncodeMode) String() string { func (m HexEncodeMode) String() string {
@@ -140,6 +143,8 @@ func (m HexEncodeMode) String() string {
return "ucolon" return "ucolon"
case HexEncodeBytes: case HexEncodeBytes:
return "bytes" return "bytes"
case HexEncodeBase64:
return "base64"
default: default:
panic("invalid hex encode mode") panic("invalid hex encode mode")
} }
@@ -157,6 +162,8 @@ func ParseHexEncodeMode(s string) HexEncodeMode {
return HexEncodeUpperColon return HexEncodeUpperColon
case "bytes": case "bytes":
return HexEncodeBytes return HexEncodeBytes
case "base64":
return HexEncodeBase64
} }
panic("invalid hex encode mode") panic("invalid hex encode mode")
@@ -218,21 +225,22 @@ func bytesAsByteSliceString(buf []byte) string {
return sb.String() return sb.String()
} }
// HexEncode encodes the given bytes as a hexadecimal string. // HexEncode encodes the given bytes as a hexadecimal string. It
// also supports a few other binary-encoding formats as well.
func HexEncode(b []byte, mode HexEncodeMode) string { func HexEncode(b []byte, mode HexEncodeMode) string {
str := hexEncode(b)
switch mode { switch mode {
case HexEncodeLower: case HexEncodeLower:
return str return hexEncode(b)
case HexEncodeUpper: case HexEncodeUpper:
return strings.ToUpper(str) return strings.ToUpper(hexEncode(b))
case HexEncodeLowerColon: case HexEncodeLowerColon:
return hexColons(str) return hexColons(hexEncode(b))
case HexEncodeUpperColon: case HexEncodeUpperColon:
return strings.ToUpper(hexColons(str)) return strings.ToUpper(hexColons(hexEncode(b)))
case HexEncodeBytes: case HexEncodeBytes:
return bytesAsByteSliceString(b) return bytesAsByteSliceString(b)
case HexEncodeBase64:
return base64.StdEncoding.EncodeToString(b)
default: default:
panic("invalid hex encode mode") panic("invalid hex encode mode")
} }