~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
170 lines · 4.4 KB Raw History
package plc

import (
	"crypto/sha256"
	"encoding/base32"
	"encoding/base64"
	"fmt"
	"maps"
	"strings"

	"github.com/bluesky-social/indigo/atproto/atcrypto"
	"github.com/bluesky-social/indigo/atproto/atdata"
	"github.com/ipfs/go-cid"
	"github.com/multiformats/go-multihash"
)

type GenesisOptions struct {
	RotationKeys        []string
	VerificationMethods map[string]string
	AlsoKnownAs         []string
	Services            map[string]Service
	Signer              atcrypto.PrivateKey
}

func CreateGenesis(opts GenesisOptions) (string, *Operation, error) {
	if opts.Signer == nil {
		return "", nil, fmt.Errorf("plc: a genesis operation needs a signer")
	}
	if len(opts.RotationKeys) == 0 {
		return "", nil, fmt.Errorf("plc: a genesis operation needs at least one rotation key")
	}
	op := &Operation{
		Type:                TypeOperation,
		RotationKeys:        append([]string{}, opts.RotationKeys...),
		VerificationMethods: copyStringMap(opts.VerificationMethods),
		AlsoKnownAs:         append([]string{}, opts.AlsoKnownAs...),
		Services:            copyServiceMap(opts.Services),
	}
	if err := SignOperation(op, opts.Signer); err != nil {
		return "", nil, err
	}
	did, err := OperationDID(op)
	if err != nil {
		return "", nil, err
	}
	return did, op, nil
}

func CreateTombstone(prev string, signer atcrypto.PrivateKey) (*Operation, error) {
	if signer == nil {
		return nil, fmt.Errorf("plc: a tombstone operation needs a signer")
	}
	p := prev
	op := &Operation{Type: TypeTombstone, Prev: &p}
	if err := SignOperation(op, signer); err != nil {
		return nil, err
	}
	return op, nil
}

func SignOperation(op *Operation, signer atcrypto.PrivateKey) error {
	if op == nil {
		return fmt.Errorf("plc: nil operation")
	}
	op.raw = nil
	unsigned, err := atdata.MarshalCBOR(op.object())
	if err != nil {
		return fmt.Errorf("plc: encode unsigned operation: %w", err)
	}
	sig, err := signer.HashAndSign(unsigned)
	if err != nil {
		return fmt.Errorf("plc: sign operation: %w", err)
	}
	op.Sig = base64.RawURLEncoding.EncodeToString(sig)
	return nil
}

func OperationCID(op *Operation) (string, error) {
	if op == nil {
		return "", fmt.Errorf("plc: nil operation")
	}
	b, err := atdata.MarshalCBOR(op.SignedObject())
	if err != nil {
		return "", fmt.Errorf("plc: encode operation: %w", err)
	}
	sum := sha256.Sum256(b)
	mh, err := multihash.Encode(sum[:], multihash.SHA2_256)
	if err != nil {
		return "", err
	}
	return cid.NewCidV1(cid.DagCBOR, mh).String(), nil
}

func OperationDID(op *Operation) (string, error) {
	if op == nil {
		return "", fmt.Errorf("plc: nil operation")
	}
	b, err := atdata.MarshalCBOR(op.SignedObject())
	if err != nil {
		return "", fmt.Errorf("plc: encode operation: %w", err)
	}
	sum := sha256.Sum256(b)
	enc := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(sum[:15])
	return "did:plc:" + strings.ToLower(enc), nil
}

func VerifyOperationSignature(op *Operation, rotationKeys []string) (bool, error) {
	if op == nil {
		return false, fmt.Errorf("plc: nil operation")
	}
	sig, err := decodeSignature(op.Sig)
	if err != nil {
		return false, err
	}
	unsigned, err := atdata.MarshalCBOR(op.UnsignedObject())
	if err != nil {
		return false, fmt.Errorf("plc: encode unsigned operation: %w", err)
	}
	var lastErr error
	for _, key := range rotationKeys {
		pub, err := didKeyPublic(key)
		if err != nil {
			lastErr = err
			continue
		}
		if err := pub.HashAndVerify(unsigned, sig); err == nil {
			return true, nil
		} else {
			lastErr = err
		}
	}
	if lastErr == nil {
		lastErr = fmt.Errorf("plc: no rotation keys")
	}
	return false, lastErr
}

func didKeyPublic(key string) (atcrypto.PublicKey, error) {
	if !strings.HasPrefix(key, "did:key:") {
		return nil, fmt.Errorf("plc: rotation key %q is not a did:key", key)
	}
	return atcrypto.ParsePublicDIDKey(key)
}

func decodeSignature(sig string) ([]byte, error) {
	if sig == "" {
		return nil, fmt.Errorf("plc: empty signature")
	}
	b, err := base64.RawURLEncoding.DecodeString(sig)
	if err == nil {
		return b, nil
	}
	b, err = base64.URLEncoding.DecodeString(sig)
	if err != nil {
		return nil, fmt.Errorf("plc: signature is not base64url: %w", err)
	}
	return b, nil
}

func copyStringMap(in map[string]string) map[string]string {
	out := make(map[string]string, len(in))
	maps.Copy(out, in)
	return out
}

func copyServiceMap(in map[string]Service) map[string]Service {
	out := make(map[string]Service, len(in))
	maps.Copy(out, in)
	return out
}