Files
brianmcgee c7840edca5
buildbot/nix-eval Build done.
PR Size Review Check / pr-size-review-gate (pull_request) Successful in 56s
sizelint / sizelint (pull_request) Successful in 1m13s
buildbot/nix-build Build done.
gitea-mq/buildbot/nix-eval Build done.
gitea-mq/buildbot/nix-build Build done.
gitea-mq Merge queue passed
crypto,config: guard against nil PublicKey in decoded signatures
`PublicKey.String()`, `URLEncoded()`, `Bytes()`, and `Equal()` all panic when called on a `nil` receiver. A peer can exploit this by sending a Manifest with a missing `network_id` or `signed_by`, causing a nil-pointer panic in the stream-handler goroutine that crashes the daemon.

Add nil-receiver guards to `PublicKey` methods and an early nil-check
in `SignatureAuth.Authorize()` so malformed signatures are rejected
with an error rather than crashing the process.
2026-04-08 12:10:37 +01:00

341 lines
9.4 KiB
Go

package crypto
import (
"crypto/ed25519"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
"sync"
p2pcrypto "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
)
// PublicKey represents an ED25519 public key with added utility methods for encoding, comparison, and verification.
type PublicKey struct {
key ed25519.PublicKey
str string
strOnce *sync.Once
}
// Bytes returns the byte representation of the public key.
func (p *PublicKey) Bytes() []byte {
if p == nil {
return nil
}
return p.key
}
// Verify checks if the provided signature is valid for the given message using the public key.
// Returns true if valid.
func (p *PublicKey) Verify(msg []byte, sig []byte) bool {
return ed25519.Verify(p.key, msg, sig)
}
// String returns the base64-encoded string representation of the public key.
// The result is computed once and cached for future calls.
func (p *PublicKey) String() string {
if p == nil {
return ""
}
p.strOnce.Do(func() {
p.str = base64.StdEncoding.EncodeToString(p.key)
})
return p.str
}
// URLEncoded returns a filesystem-safe representation of the public key using unpadded base64url encoding (RFC 4648).
// The result contains only alphanumeric characters, hyphens, and underscores.
func (p *PublicKey) URLEncoded() string {
if p == nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(p.key)
}
// PEM returns the PEM-encoded representation of the public key.
func (p *PublicKey) PEM() ([]byte, error) {
der, err := x509.MarshalPKIXPublicKey(p.key)
if err != nil {
return nil, fmt.Errorf("failed to marshal public key: %w", err)
}
return pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: der,
}), nil
}
// WriteToFile writes the PEM-encoded representation of the public key to the specified file path.
func (p *PublicKey) WriteToFile(path string) error {
bytes, err := p.PEM()
if err != nil {
return fmt.Errorf("failed to encode public key to PEM: %w", err)
}
if err := os.WriteFile(path, bytes, 0o600); err != nil {
return fmt.Errorf("failed to write public key to file: %w", err)
}
return nil
}
// ToLibP2P converts the public key to a libp2p crypto public key.
func (p *PublicKey) ToLibP2P() (p2pcrypto.PubKey, error) {
result, err := p2pcrypto.UnmarshalEd25519PublicKey(p.key)
if err != nil {
return nil, fmt.Errorf("failed to convert key: %w", err)
}
return result, nil
}
// PeerID derives the libp2p peer ID from this public key.
func (p *PublicKey) PeerID() (peer.ID, error) {
key, err := p.ToLibP2P()
if err != nil {
return "", err
}
id, err := peer.IDFromPublicKey(key)
if err != nil {
return "", fmt.Errorf("failed to derive peer ID: %w", err)
}
return id, nil
}
// Equal compares this PublicKey with another PublicKey and returns true if they represent the same key, false
// otherwise.
func (p *PublicKey) Equal(other *PublicKey) bool {
if p == nil || other == nil {
return p == other
}
return slices.Equal(p.key, other.key)
}
// MarshalJSON serializes the PublicKey to a JSON-encoded base64 representation of the key.
// It returns the serialized data or an error.
func (p *PublicKey) MarshalJSON() ([]byte, error) {
buf, err := json.Marshal(p.String())
if err != nil {
return nil, fmt.Errorf("failed to marshal public key: %w", err)
}
return buf, nil
}
// UnmarshalJSON implements the json.Unmarshaler interface for the PublicKey type.
// It decodes a base64-encoded JSON string into the PublicKey's internal representation.
// Returns an error if decoding fails or if the decoded key length is incorrect.
func (p *PublicKey) UnmarshalJSON(b []byte) error {
var (
err error
encoded string
)
if err = json.Unmarshal(b, &encoded); err != nil {
return fmt.Errorf("failed to unmarshal public key: %w", err)
}
// decode the base64 string
if p.key, err = base64.StdEncoding.DecodeString(encoded); err != nil {
return fmt.Errorf("failed to decode public key: %w", err)
}
// check the decoded bytes are the correct length
if len(p.key) != ed25519.PublicKeySize {
return fmt.Errorf("decoded public key has %d bytes, expected %d", len(p.key), ed25519.PublicKeySize)
}
// pre-set the string value since we have it here
p.strOnce = &sync.Once{}
p.strOnce.Do(func() {
p.str = encoded
})
return nil
}
// MarshalBinary implements encoding.BinaryMarshaler for PublicKey.
// Returns the raw 32-byte Ed25519 public key.
func (p *PublicKey) MarshalBinary() ([]byte, error) {
result := make([]byte, ed25519.PublicKeySize)
copy(result, p.key)
return result, nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler for PublicKey.
// Expects exactly 32 bytes of raw Ed25519 public key data.
func (p *PublicKey) UnmarshalBinary(b []byte) error {
if len(b) != ed25519.PublicKeySize {
return fmt.Errorf("public key has %d bytes, expected %d", len(b), ed25519.PublicKeySize)
}
p.key = make([]byte, ed25519.PublicKeySize)
copy(p.key, b)
p.strOnce = &sync.Once{}
return nil
}
// newPublicKey creates a new PublicKey instance from the provided byte slice if it satisfies the required length.
// Returns an error if the byte slice length does not match the expected ed25519.PublicKeySize.
func newPublicKey(buf []byte) (*PublicKey, error) {
if len(buf) != ed25519.PublicKeySize {
return nil, fmt.Errorf("buf has %d bytes, expected %d", len(buf), ed25519.PublicKeySize)
}
return &PublicKey{
key: buf,
strOnce: &sync.Once{},
}, nil
}
// PublicKeyList represents a list of pointers to PublicKey objects, enabling batch operations on multiple keys.
type PublicKeyList []*PublicKey
// String returns a comma-separated string representation of the PublicKeyList.
// Returns an empty string if the list is empty.
func (p PublicKeyList) String() string {
if len(p) == 0 {
return ""
}
var b strings.Builder
for _, key := range p {
b.WriteString(",")
b.WriteString(key.String())
}
return b.String()[1:]
}
// LoadPublicKey loads a public key from a file path or parses it from a base64/PEM string.
func LoadPublicKey(pathOrData string) (*PublicKey, error) {
// first try to read as a file
data, err := os.ReadFile(pathOrData) //nolint:gosec
if err == nil {
// file exists, parse its contents
return ParsePublicKey(data)
}
// not a file, try to parse as base64/PEM string
return ParsePublicKey([]byte(pathOrData))
}
// ReadPublicKey reads an ED25519 public key from a file at the specified path and returns a PublicKey or an error.
func ReadPublicKey(path string) (*PublicKey, error) {
f, err := os.Open(path) //nolint:gosec
if err != nil {
return nil, fmt.Errorf("failed to open public key file: %w", err)
}
defer f.Close() //nolint:errcheck
buf, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read public key: %w", err)
}
return ParsePublicKey(buf)
}
// ParsePublicKey parses a byte slice into a PublicKey.
// It wraps data in a PEM block if not already formatted as one.
// It ensures the key is ED25519 and returns an error for other types, invalid PEM, or incorrect key format.
func ParsePublicKey(buf []byte) (*PublicKey, error) {
var parsed ed25519.PublicKey
switch {
case len(buf) == ed25519.PublicKeySize:
// raw 32-byte key
return newPublicKey(buf)
case len(buf) == 44:
// we assume we're dealing with a simple base64 encoded key, as 32 bytes encoded in base64 is 44
parsed = make([]byte, ed25519.PublicKeySize)
_, err := base64.StdEncoding.Decode(parsed, buf)
if err != nil {
return nil, fmt.Errorf("failed to decode public key: %w", err)
}
default:
// otherwise, we assume the key was generated directly with openssl in PEM format
// wrap in a PEM block if it isn't already
// this is a convenience allowing users to just copy the main line from the key file
if !strings.HasPrefix(string(buf), "-----BEGIN PUBLIC KEY-----") {
buf = []byte("-----BEGIN PUBLIC KEY-----\n" + string(buf) + "\n-----END PUBLIC KEY-----")
}
// Decode the PEM block
block, _ := pem.Decode(buf)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
// Ensure the block type is "PUBLIC KEY"
if block.Type != "PUBLIC KEY" {
return nil, fmt.Errorf("unexpected block type: %s, expected PUBLIC KEY", block.Type)
}
// Parse the public key
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %w", err)
}
// Ensure the key is an ED25519 public key
ed25519Key, ok := key.(ed25519.PublicKey)
if !ok {
return nil, errors.New("key is not an ED25519 public key")
}
parsed = ed25519Key
}
return newPublicKey(parsed)
}
// ParsePublicKeyURLEncoded decodes an unpadded base64url-encoded string (as produced by URLEncoded) into a PublicKey.
func ParsePublicKeyURLEncoded(encoded string) (*PublicKey, error) {
buf, err := base64.RawURLEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("failed to decode public key dir name: %w", err)
}
return newPublicKey(buf)
}
// UnmarshalParam implements the echo.BindUnmarshaler interface for binding from headers/query params.
func (p *PublicKey) UnmarshalParam(param string) error {
buf, err := base64.StdEncoding.DecodeString(param)
if err != nil {
return fmt.Errorf("failed to decode public key: %w", err)
}
if len(buf) != ed25519.PublicKeySize {
return fmt.Errorf("decoded public key has %d bytes, expected %d", len(buf), ed25519.PublicKeySize)
}
p.key = buf
p.strOnce = &sync.Once{}
p.strOnce.Do(func() {
p.str = param
})
return nil
}