Files
backrest/internal/cryptoutil/identity.go
Gareth 86e624bb73
Some checks failed
Release Please / release-please (push) Has been cancelled
Build Snapshot Release / build (push) Has been cancelled
Test / test-nix (push) Has been cancelled
Test / test-win (push) Has been cancelled
Update Restic / update-restic-version (push) Has been cancelled
chore: simplify sync impl by abstracting bidirectional transport (#844)
2025-07-21 21:20:16 -07:00

151 lines
3.8 KiB
Go

package cryptoutil
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
v1 "github.com/garethgeorge/backrest/gen/go/v1"
"google.golang.org/protobuf/proto"
)
var (
curve = elliptic.P256() // ed25519
)
type PublicKey struct {
proto *v1.PublicKey
publicCryptoKey ecdsa.PublicKey
}
func NewPublicKey(pubkey *v1.PublicKey) (*PublicKey, error) {
pubKeyBlock, _ := pem.Decode([]byte(pubkey.Ed25519Pub))
if pubKeyBlock == nil {
return nil, errors.New("no public key found in pem")
}
pkixPubKey, err := x509.ParsePKIXPublicKey(pubKeyBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse public key: %w", err)
}
ecdsaPubKey, ok := pkixPubKey.(*ecdsa.PublicKey)
if !ok {
return nil, errors.New("not an ECDSA public key")
}
if derived := deriveKeyId(ecdsaPubKey); derived != pubkey.Keyid {
return nil, fmt.Errorf("public key_id provided does not match the derived key: %s != %s", derived, pubkey.Keyid)
}
return &PublicKey{
proto: pubkey,
publicCryptoKey: *ecdsaPubKey,
}, nil
}
func (pk *PublicKey) KeyID() string {
return pk.proto.Keyid
}
func (pk *PublicKey) PublicKeyProto() *v1.PublicKey {
return proto.Clone(pk.proto).(*v1.PublicKey)
}
// VerifySignature verifies the signature of a message
func (pk *PublicKey) Verify(message, sig []byte) error {
hash := sha256.Sum256(message)
if !ecdsa.VerifyASN1(&pk.publicCryptoKey, hash[:], sig) {
return errors.New("signature verification failed")
}
return nil
}
type PrivateKey struct {
*PublicKey
proto *v1.PrivateKey
privateCryptoKey *ecdsa.PrivateKey
}
func NewPrivateKey(privkey *v1.PrivateKey) (*PrivateKey, error) {
privKeyBlock, _ := pem.Decode([]byte(privkey.Ed25519Priv))
if privKeyBlock == nil {
return nil, errors.New("no private key found in pem")
}
ecdsaPrivKey, err := x509.ParseECPrivateKey(privKeyBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("parse private key: %w", err)
}
pubKey, err := NewPublicKey(&v1.PublicKey{
Keyid: privkey.Keyid,
Ed25519Pub: privkey.Ed25519Pub,
})
if err != nil {
return nil, err
}
if ecdsaPrivKey.PublicKey.X.Cmp(pubKey.publicCryptoKey.X) != 0 ||
ecdsaPrivKey.PublicKey.Y.Cmp(pubKey.publicCryptoKey.Y) != 0 {
return nil, errors.New("private key does not match public key")
}
return &PrivateKey{
PublicKey: pubKey,
proto: privkey,
privateCryptoKey: ecdsaPrivKey,
}, nil
}
func GeneratePrivateKey() (*v1.PrivateKey, error) {
privKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, err
}
privateKeyBytes, err := x509.MarshalECPrivateKey(privKey)
if err != nil {
return nil, fmt.Errorf("marshal private key: %w", err)
}
pemPrivateKeyBytes := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE", Bytes: privateKeyBytes})
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey)
if err != nil {
return nil, fmt.Errorf("marshal public key: %w", err)
}
pemPublicKeyBytes := pem.EncodeToMemory(&pem.Block{Type: "EC PUBLIC", Bytes: publicKeyBytes})
return &v1.PrivateKey{
Keyid: deriveKeyId(&privKey.PublicKey),
Ed25519Priv: string(pemPrivateKeyBytes),
Ed25519Pub: string(pemPublicKeyBytes),
}, nil
}
func (pk *PrivateKey) PrivateKeyProto() *v1.PrivateKey {
return proto.Clone(pk.proto).(*v1.PrivateKey)
}
// SignMessage signs a message using the private key
func (pk *PrivateKey) Sign(message []byte) ([]byte, error) {
hash := sha256.Sum256(message)
sig, err := ecdsa.SignASN1(rand.Reader, pk.privateCryptoKey, hash[:])
if err != nil {
return nil, fmt.Errorf("sign message: %w", err)
}
return sig, nil
}
func deriveKeyId(key *ecdsa.PublicKey) string {
shasum := sha256.New()
shasum.Write(key.X.Bytes())
shasum.Write(key.Y.Bytes())
return "ecdsa." + base64.RawURLEncoding.EncodeToString(shasum.Sum(nil))
}