plc

package
v0.0.0-...-cfc49f8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: Apache-2.0, MIT Imports: 22 Imported by: 0

README

did:plc

An implementation of the did:plc DID method in Golang.

did:plc is a self-authenticating, cryptographically verifiable DID method used by the AT Protocol. A DID is created by signing a genesis operation; the DID identifier is derived from the hash of that operation, so it cannot be forged. The operation history is an append-only log hosted by a directory server (the canonical one is https://plc.directory), and the DID can be rotated or updated over time by signing further operations with one of its registered rotation keys.

This package provides:

  • A Resolver that fetches and parses the DID Document for a did:plc DID — it implements the shared did.Resolver interface.
  • A DirectoryClient that additionally fetches the last operation (Last), publishes operations (Update), and deactivates a DID (Deactivate).
  • Operation builders (NewOperation, NewOperationFromPrevious, NewTombstone), signing (New, SignOperation, SignTombstone), and signature verification (VerifyOperationSignature, VerifyTombstoneSignature).

Usage

Resolving an existing PLC DID

Resolve a did:plc DID to its DID Document:

package main

import (
	"context"
	"fmt"
	"net/url"

	"github.com/fil-forge/ucantone/did"
	"github.com/fil-forge/ucantone/did/plc"
)

func main() {
	endpoint, _ := url.Parse("https://plc.directory")
	resolver, err := plc.NewResolver(*endpoint)
	if err != nil {
		panic(err)
	}

	d := did.MustParse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")
	doc, err := resolver.Resolve(context.Background(), d)
	if err != nil {
		panic(err)
	}

	fmt.Println("ID:         ", doc.ID)          // did:plc:ewvi7nxzyoun6zhxrhs64oiz
	fmt.Println("AlsoKnownAs:", doc.AlsoKnownAs) // [at://atproto.com]
	for _, svc := range doc.Service {
		fmt.Println("Service:    ", svc)
	}
}
Creating a new PLC DID

Generate a rotation key, build and sign a genesis operation with New, then publish it to the directory. The DID is derived from the signed genesis operation:

package main

import (
	"context"
	"net/url"

	"github.com/fil-forge/ucantone/did"
	"github.com/fil-forge/ucantone/did/plc"
	"github.com/fil-forge/ucantone/multikey/secp256k1"
)

func main() {
	// The rotation key controls the DID. Keep it secret and back it up — anyone
	// who holds it can rewrite the DID's history.
	signer, err := secp256k1.Generate()
	if err != nil {
		panic(err)
	}
	key := signer.KeyDID()

	d, genesis, err := plc.New(
		signer,
		plc.WithRotationKeys(key),
		plc.WithVerificationMethods(map[string]did.DID{"atproto": key}),
		plc.WithAlsoKnownAs("at://alice.example.com"),
		plc.WithServices(map[string]plc.Service{
			"atproto_pds": {
				Type:     "AtprotoPersonalDataServer",
				Endpoint: "https://pds.example.com",
			},
		}),
	)
	if err != nil {
		panic(err)
	}

	// Publish the genesis operation to register the DID.
	endpoint, _ := url.Parse("https://plc.directory")
	client, err := plc.NewDirectoryClient(*endpoint)
	if err != nil {
		panic(err)
	}
	if err := client.Update(context.Background(), d, genesis); err != nil {
		panic(err)
	}

	println(d.String()) // did:plc:...
}
Updating a PLC DID

Fetch the last operation, derive a new operation from it (inheriting the previous state and merging your changes), sign it with a registered rotation key, and publish it:

package main

import (
	"context"
	"net/url"

	"github.com/fil-forge/ucantone/did"
	"github.com/fil-forge/ucantone/did/plc"
	"github.com/fil-forge/ucantone/multikey/secp256k1"
)

func update(signer secp256k1.Signer, d did.DID) error {
	endpoint, _ := url.Parse("https://plc.directory")
	client, err := plc.NewDirectoryClient(*endpoint)
	if err != nil {
		return err
	}
	ctx := context.Background()

	// Fetch the current head of the operation log.
	last, err := client.Last(ctx, d)
	if err != nil {
		return err
	}

	// Build an operation that updates the handle, carrying over everything else.
	op, err := plc.NewOperationFromPrevious(
		last,
		plc.WithAlsoKnownAs("at://alice.new.example.com"),
	)
	if err != nil {
		return err
	}

	// Sign with a rotation key and publish.
	signed, err := plc.SignOperation(signer, op)
	if err != nil {
		return err
	}
	return client.Update(ctx, d, signed)
}
Rotating a rotation key

To replace a rotation key, add the new key and remove the outgoing one in a single operation. The operation must be signed by a rotation key that is valid in the previous operation, so it is signed by the outgoing key as it is being removed:

package main

import (
	"context"
	"net/url"

	"github.com/fil-forge/ucantone/did"
	"github.com/fil-forge/ucantone/did/plc"
	"github.com/fil-forge/ucantone/multikey/secp256k1"
)

// rotateKey replaces current with a freshly generated rotation key and returns
// the new signer.
func rotateKey(current secp256k1.Signer, d did.DID) (secp256k1.Signer, error) {
	endpoint, _ := url.Parse("https://plc.directory")
	client, err := plc.NewDirectoryClient(*endpoint)
	if err != nil {
		return nil, err
	}
	ctx := context.Background()

	// Generate the replacement rotation key.
	next, err := secp256k1.Generate()
	if err != nil {
		return nil, err
	}

	last, err := client.Last(ctx, d)
	if err != nil {
		return nil, err
	}

	// In a single operation, add the new rotation key and remove the old one.
	op, err := plc.NewOperationFromPrevious(
		last,
		plc.WithRotationKeys(next.KeyDID()),
		plc.WithoutRotationKeys(current.KeyDID()),
	)
	if err != nil {
		return nil, err
	}

	// Sign with the outgoing key — it is still valid in the previous operation,
	// which is what authorizes this change.
	signed, err := plc.SignOperation(current, op)
	if err != nil {
		return nil, err
	}
	if err := client.Update(ctx, d, signed); err != nil {
		return nil, err
	}
	return next, nil
}

To deactivate a DID, build a tombstone from the last operation, sign it, and publish it with Deactivate:

last, _ := client.Last(ctx, d)
tomb, _ := plc.NewTombstoneFromPrevious(last)
tombstone, _ := plc.SignTombstone(signer, tomb)
err := client.Deactivate(ctx, d, tombstone)

When a DID has been deactivated, Last returns a *plc.DeactivatedDIDError — use errors.As to detect it and inspect the tombstone:

var deactivated *plc.DeactivatedDIDError
if _, err := client.Last(ctx, d); errors.As(err, &deactivated) {
	// DID is deactivated; deactivated.Operation is the *SignedTombstone.
}

Contributing

Feel free to join in. All welcome. Please open an issue!

License

Dual-licensed under MIT OR Apache 2.0

Documentation

Index

Constants

View Source
const (
	OperationType = "plc_operation"
	TombstoneType = "plc_tombstone"
)
View Source
const IdentifierLength = 24

IdentifierLength is the length in characters of the method-specific identifier of a did:plc DID.

View Source
const Method = "plc"

Variables

View Source
var ErrMissingRotationKeys = fmt.Errorf("at least one rotation key is required")

Functions

func Parse

func Parse(str string) (did.DID, error)

Parse parses a did:plc DID string, verifying the method is "plc" and the identifier is 24 characters of base32 (lowercase, no padding).

func SumOperation

func SumOperation(op *SignedOperation) (cid.Cid, error)

SumOperation computes the CID of a signed operation, as used to link the next operation in the chain to its predecessor.

func VerifyOperationSignature

func VerifyOperationSignature(verifier Verifier, signedOp *SignedOperation) error

VerifyOperationSignature verifies the signature of a SignedOperation using the provided Verifier.

func VerifyTombstoneSignature

func VerifyTombstoneSignature(verifier Verifier, signedOp *SignedTombstone) error

Types

type Cache

type Cache interface {
	Get(key string) (interface{}, bool)
	Set(key string, value interface{}, ttl time.Duration)
}

Cache is a TTL cache interface used by the Resolver to store resolved documents and their ETags for conditional revalidation. The cache is keyed by the DID string.

type DeactivatedDIDError

type DeactivatedDIDError struct {
	Operation *SignedTombstone
}

func (*DeactivatedDIDError) Error

func (e *DeactivatedDIDError) Error() string

type DirectoryClient

type DirectoryClient struct {
	Resolver
}

func NewDirectoryClient

func NewDirectoryClient(endpoint url.URL, options ...Option) (*DirectoryClient, error)

NewDirectoryClient creates a new DirectoryClient that can be used to fetch, update, and deactivate PLC operations at a directory at the given endpoint. The client can be configured with options such as timeout and transport.

func (*DirectoryClient) Deactivate

func (c *DirectoryClient) Deactivate(ctx context.Context, d did.DID, op *SignedTombstone) error

Deactivate publishes the given signed tombstone to the configured directory, deactivating the DID.

func (*DirectoryClient) Last

Last fetches the last operation for the given did:plc DID from the configured directory.

func (*DirectoryClient) Update

func (c *DirectoryClient) Update(ctx context.Context, d did.DID, op *SignedOperation) error

Update publishes the given signed operation to the configured directory.

type Operation

type Operation struct {
	Type                string             `cborgen:"type,const=plc_operation" dagjsongen:"type,const=plc_operation"`
	VerificationMethods map[string]did.DID `cborgen:"verificationMethods" dagjsongen:"verificationMethods"`
	RotationKeys        []did.DID          `cborgen:"rotationKeys" dagjsongen:"rotationKeys"`
	AlsoKnownAs         []string           `cborgen:"alsoKnownAs" dagjsongen:"alsoKnownAs"`
	Services            map[string]Service `cborgen:"services" dagjsongen:"services"`
	// String encoded CID of the previous operation in the chain, if any. If this
	// is the first operation in the chain, this field is null.
	Previous *string `cborgen:"prev" dagjsongen:"prev"`
}

Operation represents a PLC operation that can be used to create or update a PLC DID.

func NewOperation

func NewOperation(prev *cid.Cid, options ...OperationOption) (*Operation, error)

NewOperation creates a new PLC operation with the given previous operation CID and options.

func NewOperationFromPrevious

func NewOperationFromPrevious(prev *SignedOperation, options ...OperationOption) (*Operation, error)

NewOperationFromPrevious creates a new PLC operation that updates the given previous operation with the provided options. The new operation will have the previous verification methods, rotation keys, also known as, and services as the previous operation, merged with the values passed in the options.

func (*Operation) MarshalCBOR

func (t *Operation) MarshalCBOR(w io.Writer) error

func (*Operation) MarshalDagJSON

func (t *Operation) MarshalDagJSON(w io.Writer) error

func (*Operation) UnmarshalCBOR

func (t *Operation) UnmarshalCBOR(r io.Reader) (err error)

func (*Operation) UnmarshalDagJSON

func (t *Operation) UnmarshalDagJSON(r io.Reader) (err error)

type OperationOption

type OperationOption func(*opConfig)

func WithAlsoKnownAs

func WithAlsoKnownAs(alsoKnownAs ...string) OperationOption

WithAlsoKnownAs adds also known as entries to the PLC operation.

func WithRotationKeys

func WithRotationKeys(keys ...did.DID) OperationOption

WithRotationKeys adds rotation keys to the PLC operation.

func WithServices

func WithServices(services map[string]Service) OperationOption

WithServices adds services to the PLC operation.

func WithVerificationMethods

func WithVerificationMethods(methods map[string]did.DID) OperationOption

WithVerificationMethod adds a verification method to the PLC operation.

func WithoutAlsoKnownAs

func WithoutAlsoKnownAs(alsoKnownAs ...string) OperationOption

WithoutAlsoKnownAs removes the given also known as entries from the PLC operation.

func WithoutRotationKeys

func WithoutRotationKeys(keys ...did.DID) OperationOption

WithoutRotationKeys removes the given rotation keys from the PLC operation.

func WithoutServices

func WithoutServices(services map[string]Service) OperationOption

WithoutServices removes the given services from the PLC operation.

func WithoutVerificationMethods

func WithoutVerificationMethods(methods map[string]did.DID) OperationOption

WithoutVerificationMethods removes the given verification methods from the PLC operation.

type Option

type Option func(*config)

func WithCache

func WithCache(cache Cache) Option

WithCache configures the resolver to cache resolved DID documents. When set, the resolver stores the document alongside the ETag returned by the directory and issues conditional requests (If-None-Match) on subsequent resolutions, returning the cached document when the directory responds 304 Not Modified.

func WithCacheTTL

func WithCacheTTL(ttl time.Duration) Option

WithCacheTTL sets the time to live duration passed to the cache's Set when storing a resolved document. It only has an effect alongside WithCache. The value is passed through to the configured Cache implementation; consult your Cache's documentation for the meaning of 0 or negative durations.

func WithTimeout

func WithTimeout(timeout time.Duration) Option

func WithTransport

func WithTransport(transport http.RoundTripper) Option

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver resolves a did:plc DID to a DID Document by fetching the document from the configured directory.

func NewResolver

func NewResolver(endpoint url.URL, options ...Option) (*Resolver, error)

func (*Resolver) Resolve

func (r *Resolver) Resolve(ctx context.Context, d did.DID) (did.Document, error)

type Service

type Service struct {
	Type     string `cborgen:"type" dagjsongen:"type"`
	Endpoint string `cborgen:"endpoint" dagjsongen:"endpoint"`
}

func (*Service) MarshalCBOR

func (t *Service) MarshalCBOR(w io.Writer) error

func (*Service) MarshalDagJSON

func (t *Service) MarshalDagJSON(w io.Writer) error

func (*Service) UnmarshalCBOR

func (t *Service) UnmarshalCBOR(r io.Reader) (err error)

func (*Service) UnmarshalDagJSON

func (t *Service) UnmarshalDagJSON(r io.Reader) (err error)

type SignedOperation

type SignedOperation struct {
	Type                string             `cborgen:"type,const=plc_operation" dagjsongen:"type,const=plc_operation"`
	VerificationMethods map[string]did.DID `cborgen:"verificationMethods" dagjsongen:"verificationMethods"`
	RotationKeys        []did.DID          `cborgen:"rotationKeys" dagjsongen:"rotationKeys"`
	AlsoKnownAs         []string           `cborgen:"alsoKnownAs" dagjsongen:"alsoKnownAs"`
	Services            map[string]Service `cborgen:"services" dagjsongen:"services"`
	// String encoded CID of the previous operation in the chain, if any. If this
	// is the first operation in the chain, this field is null.
	Previous  *string `cborgen:"prev" dagjsongen:"prev"`
	Signature string  `cborgen:"sig" dagjsongen:"sig"`
}

func New

func New(signer Signer, options ...OperationOption) (did.DID, *SignedOperation, error)

func SignOperation

func SignOperation(signer Signer, op *Operation) (*SignedOperation, error)

SignOperation signs a PLC operation with the given signer and returns a SignedOperation.

func (*SignedOperation) MarshalCBOR

func (t *SignedOperation) MarshalCBOR(w io.Writer) error

func (*SignedOperation) MarshalDagJSON

func (t *SignedOperation) MarshalDagJSON(w io.Writer) error

func (*SignedOperation) UnmarshalCBOR

func (t *SignedOperation) UnmarshalCBOR(r io.Reader) (err error)

func (*SignedOperation) UnmarshalDagJSON

func (t *SignedOperation) UnmarshalDagJSON(r io.Reader) (err error)

type SignedTombstone

type SignedTombstone struct {
	Type      string `cborgen:"type,const=plc_tombstone" dagjsongen:"type,const=plc_tombstone"`
	Previous  string `cborgen:"prev" dagjsongen:"prev"`
	Signature string `cborgen:"sig" dagjsongen:"sig"`
}

func SignTombstone

func SignTombstone(signer Signer, op *Tombstone) (*SignedTombstone, error)

SignTombstone signs a PLC tombstone with the given signer and returns a SignedTombstone.

func (*SignedTombstone) MarshalCBOR

func (t *SignedTombstone) MarshalCBOR(w io.Writer) error

func (*SignedTombstone) MarshalDagJSON

func (t *SignedTombstone) MarshalDagJSON(w io.Writer) error

func (*SignedTombstone) UnmarshalCBOR

func (t *SignedTombstone) UnmarshalCBOR(r io.Reader) (err error)

func (*SignedTombstone) UnmarshalDagJSON

func (t *SignedTombstone) UnmarshalDagJSON(r io.Reader) (err error)

type Signer

type Signer interface {
	// Sign takes a byte encoded message and produces a verifiable signature.
	Sign(msg []byte) []byte
}

Signer is an entity that can sign a payload.

type Tombstone

type Tombstone struct {
	Type     string `cborgen:"type,const=plc_tombstone" dagjsongen:"type,const=plc_tombstone"`
	Previous string `cborgen:"prev" dagjsongen:"prev"`
}

func NewTombstone

func NewTombstone(prev cid.Cid) *Tombstone

NewTombstone creates a new PLC tombstone with the given previous operation CID. The tombstone indicates that the DID has been deactivated and should no longer be used.

func NewTombstoneFromPrevious

func NewTombstoneFromPrevious(prev *SignedOperation) (*Tombstone, error)

NewTombstoneFromPrevious creates a new PLC tombstone that deactivates the DID, linking to the given previous operation by its computed CID. It is a convenience over NewTombstone for the common case where you have fetched the last signed operation (e.g. via DirectoryClient.Last) rather than its CID.

func (*Tombstone) MarshalCBOR

func (t *Tombstone) MarshalCBOR(w io.Writer) error

func (*Tombstone) MarshalDagJSON

func (t *Tombstone) MarshalDagJSON(w io.Writer) error

func (*Tombstone) UnmarshalCBOR

func (t *Tombstone) UnmarshalCBOR(r io.Reader) (err error)

func (*Tombstone) UnmarshalDagJSON

func (t *Tombstone) UnmarshalDagJSON(r io.Reader) (err error)

type Verifier

type Verifier interface {
	// Verify takes a byte encoded message and verifies that it is signed by
	// corresponding signer.
	Verify(msg []byte, sig []byte) bool
}

Verifier represents an entity that can verify signatures.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL