~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
598 lines · 15.4 KB Raw History
package market

import (
	"cmp"
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"maps"
	"math"
	"slices"
	"strings"
	"sync"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/b64"

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

const dagCBORCodec = 0x71

type KeyResolver interface {
	ResolveKey(ctx context.Context, did string) (atcrypto.PublicKey, error)
}

type KeyResolverFunc func(ctx context.Context, did string) (atcrypto.PublicKey, error)

func (f KeyResolverFunc) ResolveKey(ctx context.Context, did string) (atcrypto.PublicKey, error) {
	return f(ctx, did)
}

var DefaultKeyResolver KeyResolver = KeyResolverFunc(func(_ context.Context, did string) (atcrypto.PublicKey, error) {
	return atcrypto.ParsePublicDIDKey(did)
})

type KeysForDID interface {
	KeysForDID(ctx context.Context, did string) ([]string, error)
}

type KeysForDIDFunc func(ctx context.Context, did string) ([]string, error)

func (f KeysForDIDFunc) KeysForDID(ctx context.Context, did string) ([]string, error) {
	return f(ctx, did)
}

type DIDDocumentResolver interface {
	ResolveDID(ctx context.Context, did syntax.DID) (*identity.DIDDocument, error)
}

type InlineAttestation struct {
	Type      string
	Key       string
	CID       string
	Signature []byte
	Fields    map[string]any
}

func (e InlineAttestation) Metadata() map[string]any {
	out := make(map[string]any, len(e.Fields)+4)
	maps.Copy(out, e.Fields)
	out["$type"] = e.Type
	out["key"] = e.Key
	out["cid"] = e.CID
	return out
}

func (e InlineAttestation) Map() map[string]any {
	out := e.Metadata()
	out["signature"] = map[string]any{"$bytes": base64.RawStdEncoding.EncodeToString(e.Signature)}
	return out
}

func (e InlineAttestation) IssuerOr(repositoryDID string) string {
	if v, ok := e.Fields["issuer"].(string); ok && v != "" {
		return v
	}
	return repositoryDID
}

type Signer struct {
	PrivateKey atcrypto.PrivateKey
	Issuer     string
}

func (s *Signer) PublicDID() (string, error) {
	if s == nil || s.PrivateKey == nil {
		return "", errors.New("market: nil signer")
	}
	pub, err := s.PrivateKey.PublicKey()
	if err != nil {
		return "", err
	}
	return pub.DIDKey(), nil
}

func NewSigner(privateKey atcrypto.PrivateKey, issuer string) *Signer {
	return &Signer{PrivateKey: privateKey, Issuer: issuer}
}

func NewSignerFromHex(privateKeyHex string) (*Signer, error) {
	raw := strings.TrimPrefix(strings.TrimSpace(privateKeyHex), "0x")
	b, err := hex.DecodeString(raw)
	if err != nil {
		return nil, fmt.Errorf("market: private key hex: %w", err)
	}
	priv, err := atcrypto.ParsePrivateBytesK256(b)
	if err != nil {
		return nil, err
	}
	return &Signer{PrivateKey: priv}, nil
}

func GenerateSigner() (*Signer, error) {
	priv, err := atcrypto.GeneratePrivateKeyK256()
	if err != nil {
		return nil, err
	}
	return &Signer{PrivateKey: priv}, nil
}

func LoadOrGenerateSigner(privateKeyHex string) (*Signer, error) {
	if strings.TrimSpace(privateKeyHex) == "" {
		return GenerateSigner()
	}
	return NewSignerFromHex(privateKeyHex)
}

func SignBytes(payload []byte, privateKey atcrypto.PrivateKey) ([]byte, error) {
	return privateKey.HashAndSign(payload)
}

func normalizeForCBOR(value any) any {
	switch v := value.(type) {
	case float64:
		if !math.IsInf(v, 0) && !math.IsNaN(v) && v == math.Trunc(v) && v >= -9223372036854775808.0 && v < 9223372036854775808.0 {
			return int64(v)
		}
		return v
	case int:
		return int64(v)
	case int32:
		return int64(v)
	case uint32:
		return int64(v)
	case uint64:
		if v <= math.MaxInt64 {
			return int64(v)
		}
		return v
	case []any:
		out := make([]any, len(v))
		for i, item := range v {
			out[i] = normalizeForCBOR(item)
		}
		return out
	case map[string]any:
		out := make(map[string]any, len(v))
		for k, item := range v {
			out[k] = normalizeForCBOR(item)
		}
		return out
	default:
		return v
	}
}

func DagCborCID(value map[string]any) (cid.Cid, error) {
	enc, err := atdata.MarshalCBOR(normalizeForCBOR(value).(map[string]any))
	if err != nil {
		return cid.Undef, fmt.Errorf("market: dag-cbor encode: %w", err)
	}
	prefix := cid.Prefix{Version: 1, Codec: dagCBORCodec, MhType: multihash.SHA2_256, MhLength: 32}
	c, err := prefix.Sum(enc)
	if err != nil {
		return cid.Undef, fmt.Errorf("market: cid: %w", err)
	}
	return c, nil
}

func CreateAttestationCID(record, metadata map[string]any, repository string, fields []string) (cid.Cid, error) {
	recType, _ := record["$type"].(string)
	if recType == "" {
		return cid.Undef, fmt.Errorf("%w: record is missing $type", ErrInvalidRecord)
	}
	metaType, _ := metadata["$type"].(string)
	if metaType == "" {
		return cid.Undef, fmt.Errorf("%w: attestation metadata is missing $type", ErrInvalidRecord)
	}
	preparedMetadata := make(map[string]any, len(metadata)+1)
	for k, v := range metadata {
		if k == "cid" || k == "signature" {
			continue
		}
		preparedMetadata[k] = v
	}
	preparedMetadata["repository"] = repository

	prepared := make(map[string]any, len(record)+1)
	if fields != nil {
		prepared["$type"] = recType
		prepared["$sig"] = preparedMetadata
		for _, f := range fields {
			if v, ok := record[f]; ok && f != "signatures" {
				prepared[f] = v
			}
		}
	} else {
		for k, v := range record {
			if k == "signatures" {
				continue
			}
			prepared[k] = v
		}
		prepared["$sig"] = preparedMetadata
	}
	return DagCborCID(prepared)
}

func IsAttestationCIDString(value string) bool {
	c, err := cid.Parse(value)
	if err != nil {
		return false
	}
	if c.Version() != 1 || c.Type() != dagCBORCodec {
		return false
	}
	decoded, err := multihash.Decode(c.Hash())
	if err != nil {
		return false
	}
	return decoded.Code == multihash.SHA2_256 && len(decoded.Digest) == 32
}

func ParseInlineAttestation(raw map[string]any) (InlineAttestation, bool) {
	if raw == nil {
		return InlineAttestation{}, false
	}
	if _, ok := raw["uri"]; ok {
		return InlineAttestation{}, false
	}
	key, ok := raw["key"].(string)
	if !ok || key == "" {
		return InlineAttestation{}, false
	}
	sig, ok := normalizeSignatureBytes(raw["signature"])
	if !ok || len(sig) == 0 {
		return InlineAttestation{}, false
	}
	out := InlineAttestation{Key: key, Signature: sig, Fields: map[string]any{}}
	if v, ok := raw["$type"].(string); ok {
		out.Type = v
	}
	if v, ok := raw["cid"].(string); ok {
		out.CID = v
	}
	for k, v := range raw {
		switch k {
		case "$type", "key", "cid", "signature":
			continue
		}
		out.Fields[k] = v
	}
	return out, true
}

func InlineEntries(record map[string]any) []InlineAttestation {
	rawSigs, _ := record["signatures"].([]any)
	if rawSigs == nil {
		if typed, ok := record["signatures"].([]map[string]any); ok {
			rawSigs = make([]any, len(typed))
			for i := range typed {
				rawSigs[i] = typed[i]
			}
		}
	}
	out := make([]InlineAttestation, 0, len(rawSigs))
	for _, raw := range rawSigs {
		m, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		if entry, ok := ParseInlineAttestation(m); ok {
			out = append(out, entry)
		}
	}
	return out
}

func normalizeSignatureBytes(value any) ([]byte, bool) {
	switch v := value.(type) {
	case []byte:
		return v, true
	case atdata.Bytes:
		return []byte(v), true
	case map[string]any:
		s, ok := v["$bytes"].(string)
		if !ok {
			return nil, false
		}
		return b64.Loose(s)
	case string:
		return b64.Loose(v)
	case nil:
		return nil, false
	default:
		return nil, false
	}
}

func SignInline(record map[string]any, repository string, signer *Signer) (map[string]any, error) {
	if signer == nil || signer.PrivateKey == nil {
		return nil, errors.New("market: nil signer")
	}
	did, err := signer.PublicDID()
	if err != nil {
		return nil, err
	}
	metadata := map[string]any{"$type": AttestedSignatureNSID, "key": did}
	if signer.Issuer != "" {
		metadata["issuer"] = signer.Issuer
	}
	c, err := CreateAttestationCID(record, metadata, repository, nil)
	if err != nil {
		return nil, err
	}
	sig, err := SignBytes(c.Bytes(), signer.PrivateKey)
	if err != nil {
		return nil, err
	}
	entry := map[string]any{
		"$type":     AttestedSignatureNSID,
		"key":       did,
		"cid":       c.String(),
		"signature": map[string]any{"$bytes": base64.RawStdEncoding.EncodeToString(sig)},
	}
	if signer.Issuer != "" {
		entry["issuer"] = signer.Issuer
	}
	return entry, nil
}

func SignRecord(record map[string]any, repository string, signer *Signer) (map[string]any, error) {
	entry, err := SignInline(record, repository, signer)
	if err != nil {
		return nil, err
	}
	out := make(map[string]any, len(record)+1)
	maps.Copy(out, record)
	existing := []any{}
	switch v := out["signatures"].(type) {
	case []any:
		existing = v
	case []map[string]any:
		for _, item := range v {
			existing = append(existing, item)
		}
	}
	next := make([]any, 0, len(existing)+1)
	next = append(next, existing...)
	next = append(next, entry)
	out["signatures"] = next
	return out, nil
}

type SignedRecord struct {
	URI    string
	CID    string
	Record map[string]any
}

func CreateSignedRecord(ctx context.Context, w RecordWriter, repo, collection string, record map[string]any, signer *Signer) (SignedRecord, error) {
	signed, err := SignRecord(record, repo, signer)
	if err != nil {
		return SignedRecord{}, err
	}
	ref, err := w.CreateRecord(ctx, repo, collection, signed)
	if err != nil {
		return SignedRecord{}, err
	}
	return SignedRecord{URI: ref.URI, CID: ref.CID, Record: signed}, nil
}

func CreateSignedRecordAt(ctx context.Context, w RecordWriter, repo, collection, rkey string, record map[string]any, signer *Signer) (SignedRecord, error) {
	signed, err := SignRecord(record, repo, signer)
	if err != nil {
		return SignedRecord{}, err
	}
	ref, err := w.PutRecord(ctx, repo, collection, rkey, signed)
	if err != nil {
		return SignedRecord{}, err
	}
	return SignedRecord{URI: ref.URI, CID: ref.CID, Record: signed}, nil
}

type RemoteProofSubject struct {
	SubjectRecord        map[string]any
	SubjectRepositoryDID string
}

func CreateRemoteProofRecord(ctx context.Context, w RecordWriter, repo, collection string, metadata map[string]any, subject RemoteProofSubject, signer *Signer) (StrongRef, error) {
	if _, ok := metadata["$type"].(string); !ok {
		return StrongRef{}, fmt.Errorf("%w: remote proof metadata is missing $type", ErrInvalidRecord)
	}
	bind, err := CreateAttestationCID(subject.SubjectRecord, metadata, subject.SubjectRepositoryDID, nil)
	if err != nil {
		return StrongRef{}, err
	}
	value := make(map[string]any, len(metadata)+1)
	maps.Copy(value, metadata)
	value["cid"] = bind.String()
	entry, err := SignInline(value, repo, signer)
	if err != nil {
		return StrongRef{}, err
	}
	value["signatures"] = []any{entry}
	return w.CreateRecord(ctx, repo, collection, value)
}

type ReceiptSubject struct {
	AcceptRecord        map[string]any
	AcceptRepositoryDID string
}

func CreateReceiptRecordAt(ctx context.Context, w RecordWriter, repo, rkey string, refs ReceiptRefs, subject ReceiptSubject, signer *Signer) (StrongRef, error) {
	receipt, err := BuildReceipt(refs, NowISO())
	if err != nil {
		return StrongRef{}, err
	}
	metadata, err := EncodeRecord(receipt)
	if err != nil {
		return StrongRef{}, err
	}
	bind, err := CreateAttestationCID(subject.AcceptRecord, metadata, subject.AcceptRepositoryDID, nil)
	if err != nil {
		return StrongRef{}, err
	}
	receipt.CID = bind.String()
	value, err := EncodeRecord(receipt)
	if err != nil {
		return StrongRef{}, err
	}
	entry, err := SignInline(value, repo, signer)
	if err != nil {
		return StrongRef{}, err
	}
	value["signatures"] = []any{entry}
	return w.PutRecord(ctx, repo, ReceiptNSID, rkey, value)
}

func ReceiptRKey(acceptURI, acceptCID string) string {
	sum := sha256.Sum256([]byte(strings.TrimSpace(acceptURI) + "\n" + strings.TrimSpace(acceptCID)))
	return "receipt-" + hex.EncodeToString(sum[:16])
}

func VerifyInlineAttestation(ctx context.Context, record map[string]any, entry InlineAttestation, repositoryDID string, resolver KeyResolver) bool {
	if resolver == nil {
		resolver = DefaultKeyResolver
	}
	if len(entry.Signature) == 0 || entry.Key == "" {
		return false
	}
	c, err := CreateAttestationCID(record, entry.Metadata(), repositoryDID, nil)
	if err != nil {
		return false
	}
	if entry.CID != "" && entry.CID != c.String() {
		return false
	}
	key, err := resolver.ResolveKey(ctx, entry.Key)
	if err != nil {
		return false
	}
	return key.HashAndVerify(c.Bytes(), entry.Signature) == nil
}

func VerifyRemoteProof(subjectRecord map[string]any, subjectRepositoryDID string, proofRecord map[string]any) bool {
	declared, ok := proofRecord["cid"].(string)
	if !ok || declared == "" {
		return false
	}
	metadata := make(map[string]any, len(proofRecord))
	for k, v := range proofRecord {
		switch k {
		case "cid", "signature", "signatures":
			continue
		}
		metadata[k] = v
	}
	c, err := CreateAttestationCID(subjectRecord, metadata, subjectRepositoryDID, nil)
	if err != nil {
		return false
	}
	return c.String() == declared
}

type VerifyRecordOptions struct {
	KeysForDID             KeysForDID
	KeyBoundByBadgeBlueKey func(ctx context.Context, did, key string) (bool, error)
	KeyResolver            KeyResolver
}

func VerifyRecordSignatures(ctx context.Context, record map[string]any, repositoryDID string, opts VerifyRecordOptions) bool {
	resolver := opts.KeyResolver
	if resolver == nil {
		resolver = DefaultKeyResolver
	}
	for _, entry := range InlineEntries(record) {
		if !VerifyInlineAttestation(ctx, record, entry, repositoryDID, resolver) {
			continue
		}
		if opts.KeysForDID != nil {
			allowed, err := opts.KeysForDID.KeysForDID(ctx, entry.IssuerOr(repositoryDID))
			bound := err == nil && slices.Contains(allowed, entry.Key)
			if !bound {
				if opts.KeyBoundByBadgeBlueKey == nil {
					continue
				}
				ok, berr := opts.KeyBoundByBadgeBlueKey(ctx, repositoryDID, entry.Key)
				if berr != nil || !ok {
					continue
				}
			}
		}
		return true
	}
	return false
}

func NewDIDKeyLister(dir DIDDocumentResolver) KeysForDID {
	var mu sync.Mutex
	cache := map[string][]string{}
	return KeysForDIDFunc(func(ctx context.Context, did string) ([]string, error) {
		mu.Lock()
		cached, ok := cache[did]
		mu.Unlock()
		if ok {
			return cached, nil
		}
		keys := []string{}
		if parsed, err := syntax.ParseDID(did); err == nil {
			if doc, err := dir.ResolveDID(ctx, parsed); err == nil && doc != nil {
				for _, vm := range doc.VerificationMethod {
					if vm.PublicKeyMultibase != "" {
						keys = append(keys, "did:key:"+vm.PublicKeyMultibase)
					}
				}
			}
		}
		mu.Lock()
		cache[did] = keys
		mu.Unlock()
		return keys, nil
	})
}

func NewDirectoryKeyLister(dir identity.Directory) KeysForDID {
	var mu sync.Mutex
	cache := map[string][]string{}
	return KeysForDIDFunc(func(ctx context.Context, did string) ([]string, error) {
		mu.Lock()
		cached, ok := cache[did]
		mu.Unlock()
		if ok {
			return cached, nil
		}
		keys := []string{}
		if parsed, err := syntax.ParseDID(did); err == nil {
			if ident, err := dir.LookupDID(ctx, parsed); err == nil && ident != nil {
				for _, vm := range ident.Keys {
					if vm.PublicKeyMultibase != "" {
						keys = append(keys, "did:key:"+vm.PublicKeyMultibase)
					}
				}
			}
		}
		mu.Lock()
		cache[did] = keys
		mu.Unlock()
		return keys, nil
	})
}

func AttestationVerificationMethod(controller, keypairDID, fragment string) map[string]any {
	fragment = cmp.Or(fragment, "attestation")
	return map[string]any{
		"id":                 controller + "#" + fragment,
		"type":               "Multikey",
		"controller":         controller,
		"publicKeyMultibase": strings.TrimPrefix(keypairDID, "did:key:"),
	}
}