Fork of github.com/did-method-plc/did-method-plc

initial work on webplc

+4
go-didplc/.gitignore
···
+
./webplc
+
/plan.txt
+
!static/.well-known/
+
!.gitignore
+42
go-didplc/Makefile
···
+
+
SHELL = /bin/bash
+
.SHELLFLAGS = -o pipefail -c
+
+
.PHONY: help
+
help: ## Print info about all commands
+
@echo "Commands:"
+
@echo
+
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[01;32m%-20s\033[0m %s\n", $$1, $$2}'
+
+
.PHONY: build
+
build: ## Build all executables
+
go build ./cmd/webplc
+
+
.PHONY: test
+
test: ## Run all tests
+
go test ./...
+
+
.PHONY: coverage-html
+
coverage-html: ## Generate test coverage report and open in browser
+
go test ./... -coverpkg=./... -coverprofile=test-coverage.out
+
go tool cover -html=test-coverage.out
+
+
.PHONY: lint
+
lint: ## Verify code style and run static checks
+
go vet ./...
+
test -z $(gofmt -l ./...)
+
+
.PHONY: fmt
+
fmt: ## Run syntax re-formatting (modify in place)
+
go fmt ./...
+
+
.PHONY: check
+
check: ## Compile everything, checking syntax (does not output binaries)
+
go build ./...
+
+
.env:
+
if [ ! -f ".env" ]; then cp example.dev.env .env; fi
+
+
.PHONY: run-dev-webplc
+
run-dev-webplc: .env ## Runs 'bskyweb' for local dev
+
GOLOG_LOG_LEVEL=info go run ./cmd/webplc serve --debug
+23
go-didplc/README.md
···
+
+
`go-didplc`: did:plc in golang
+
==============================
+
+
This golang package will eventually be an implementation of the did:plc specification in golang, including at a minimum verification of DID documents from a PLC operation log.
+
+
For now it primarily contains a basic website for the PLC directory, allowing lookup of individual DID documents.
+
+
+
## Developer Quickstart
+
+
Install golang. We are generally using v1.20+.
+
+
In this directory (`go-didplc/`):
+
+
# re-build and run daemon
+
go run ./cmd/webplc serve
+
+
# build and output a binary
+
go build -o webplc ./cmd/webplc/
+
+
The easiest way to configure the daemon is to copy `example.env` to `.env` and
+
fill in auth values there.
+60
go-didplc/cmd/webplc/main.go
···
+
package main
+
+
import (
+
"os"
+
+
_ "github.com/joho/godotenv/autoload"
+
+
logging "github.com/ipfs/go-log"
+
"github.com/urfave/cli/v2"
+
)
+
+
var log = logging.Logger("webplc")
+
+
func init() {
+
logging.SetAllLoggers(logging.LevelDebug)
+
//logging.SetAllLoggers(logging.LevelWarn)
+
}
+
+
func main() {
+
run(os.Args)
+
}
+
+
func run(args []string) {
+
+
app := cli.App{
+
Name: "webplc",
+
Usage: "web server for bsky.app web app (SPA)",
+
}
+
+
app.Commands = []*cli.Command{
+
&cli.Command{
+
Name: "serve",
+
Usage: "run the web server",
+
Action: serve,
+
Flags: []cli.Flag{
+
&cli.StringFlag{
+
Name: "plc-host",
+
Usage: "method, hostname, and port of PLC instance",
+
Value: "https://plc.directory",
+
EnvVars: []string{"ATP_PLC_HOST"},
+
},
+
&cli.StringFlag{
+
Name: "http-address",
+
Usage: "Specify the local IP/port to bind to",
+
Required: false,
+
Value: ":8700",
+
EnvVars: []string{"HTTP_ADDRESS"},
+
},
+
&cli.BoolFlag{
+
Name: "debug",
+
Usage: "Enable debug mode",
+
Value: false,
+
Required: false,
+
EnvVars: []string{"DEBUG"},
+
},
+
},
+
},
+
}
+
app.RunAndExitOnError()
+
}
+82
go-didplc/cmd/webplc/renderer.go
···
+
package main
+
+
import (
+
"bytes"
+
"embed"
+
"errors"
+
"fmt"
+
"io"
+
"path/filepath"
+
+
"github.com/flosch/pongo2/v6"
+
"github.com/labstack/echo/v4"
+
)
+
+
type RendererLoader struct {
+
prefix string
+
fs *embed.FS
+
}
+
+
func NewRendererLoader(prefix string, fs *embed.FS) pongo2.TemplateLoader {
+
return &RendererLoader{
+
prefix: prefix,
+
fs: fs,
+
}
+
}
+
func (l *RendererLoader) Abs(_, name string) string {
+
// TODO: remove this workaround
+
// Figure out why this method is being called
+
// twice on template names resulting in a failure to resolve
+
// the template name.
+
if filepath.HasPrefix(name, l.prefix) {
+
return name
+
}
+
return filepath.Join(l.prefix, name)
+
}
+
+
func (l *RendererLoader) Get(path string) (io.Reader, error) {
+
b, err := l.fs.ReadFile(path)
+
if err != nil {
+
return nil, fmt.Errorf("reading template %q failed: %w", path, err)
+
}
+
return bytes.NewReader(b), nil
+
}
+
+
type Renderer struct {
+
TemplateSet *pongo2.TemplateSet
+
Debug bool
+
}
+
+
func NewRenderer(prefix string, fs *embed.FS, debug bool) *Renderer {
+
return &Renderer{
+
TemplateSet: pongo2.NewSet(prefix, NewRendererLoader(prefix, fs)),
+
Debug: debug,
+
}
+
}
+
+
func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
+
var ctx pongo2.Context
+
+
if data != nil {
+
var ok bool
+
ctx, ok = data.(pongo2.Context)
+
if !ok {
+
return errors.New("no pongo2.Context data was passed")
+
}
+
}
+
+
var t *pongo2.Template
+
var err error
+
+
if r.Debug {
+
t, err = pongo2.FromFile(name)
+
} else {
+
t, err = r.TemplateSet.FromFile(name)
+
}
+
+
if err != nil {
+
return err
+
}
+
+
return t.ExecuteWriter(ctx, w)
+
}
+78
go-didplc/cmd/webplc/resolve.go
···
+
+
package main
+
+
import (
+
"io"
+
"fmt"
+
"net/http"
+
"encoding/json"
+
)
+
+
type VerificationMethod struct {
+
Id string `json:"id"`
+
Type string `json:"type"`
+
Controller string `json:"controller"`
+
PublicKeyMultibase string `json:"publicKeyMultibase"`
+
}
+
+
type DidService struct {
+
Id string `json:"id"`
+
Type string `json:"type"`
+
ServiceEndpoint string `json:"serviceEndpoint"`
+
}
+
+
type DidDoc struct {
+
AlsoKnownAs []string `json:"alsoKnownAs"`
+
VerificationMethod []VerificationMethod `json:"verificationMethod"`
+
Service []DidService `json:"service"`
+
}
+
+
type ResolutionResult struct {
+
Doc *DidDoc
+
DocJson *string
+
StatusCode int
+
}
+
+
func ResolveDidPlc(client *http.Client, plc_host, did string) (*ResolutionResult, error) {
+
result := ResolutionResult{}
+
res, err := client.Get(fmt.Sprintf("%s/%s", plc_host, did))
+
if err != nil {
+
return nil, fmt.Errorf("error making http request: %v", err)
+
}
+
defer res.Body.Close()
+
log.Debugf("PLC resolution result status=%d did=%s", res.StatusCode, did)
+
+
result.StatusCode = res.StatusCode
+
if res.StatusCode == 404 {
+
return nil, fmt.Errorf("DID not found in PLC directory: %s", did)
+
} else if res.StatusCode != 200 {
+
return &result, nil
+
}
+
+
respBytes, err := io.ReadAll(res.Body)
+
if err != nil {
+
return nil, fmt.Errorf("failed to read PLC result body: %v", err)
+
}
+
+
doc := DidDoc{}
+
err = json.Unmarshal(respBytes, &doc)
+
if err != nil {
+
return nil, fmt.Errorf("failed to parse DID Document JSON:", err)
+
}
+
result.Doc = &doc
+
+
// parse and re-serialize JSON in pretty (indent) style
+
var data map[string]interface{}
+
err = json.Unmarshal(respBytes, &data)
+
if err != nil {
+
return nil, fmt.Errorf("failed to parse DID Document JSON:", err)
+
}
+
indentJson, err := json.MarshalIndent(data, "", " ")
+
if err != nil {
+
return nil, fmt.Errorf("failed to parse DID Document JSON:", err)
+
}
+
s := string(indentJson)
+
result.DocJson = &s
+
+
return &result, nil
+
}
+240
go-didplc/cmd/webplc/server.go
···
+
package main
+
+
import (
+
"context"
+
"embed"
+
"errors"
+
"io/fs"
+
"net/http"
+
"os"
+
"os/signal"
+
"strings"
+
"syscall"
+
"time"
+
"fmt"
+
+
"github.com/russross/blackfriday/v2"
+
"github.com/klauspost/compress/gzhttp"
+
"github.com/klauspost/compress/gzip"
+
"github.com/flosch/pongo2/v6"
+
"github.com/labstack/echo/v4"
+
"github.com/labstack/echo/v4/middleware"
+
"github.com/urfave/cli/v2"
+
)
+
+
//go:embed templates/*
+
var TemplateFS embed.FS
+
+
//go:embed static/*
+
var StaticFS embed.FS
+
+
//go:embed spec/v0.1/did-plc.md
+
var specZeroOneMarkdown []byte
+
+
type Server struct {
+
echo *echo.Echo
+
httpd *http.Server
+
client *http.Client
+
plcHost string
+
}
+
+
func serve(cctx *cli.Context) error {
+
debug := cctx.Bool("debug")
+
httpAddress := cctx.String("http-address")
+
+
// Echo
+
e := echo.New()
+
+
// create a new session (no auth)
+
client := http.Client{
+
Transport: &http.Transport{
+
Proxy: http.ProxyFromEnvironment,
+
ForceAttemptHTTP2: true,
+
MaxIdleConns: 100,
+
IdleConnTimeout: 90 * time.Second,
+
TLSHandshakeTimeout: 10 * time.Second,
+
ExpectContinueTimeout: 1 * time.Second,
+
},
+
}
+
+
// httpd variable
+
var (
+
httpTimeout = 2 * time.Minute
+
httpMaxHeaderBytes = 2 * (1024 * 1024)
+
gzipMinSizeBytes = 1024 * 2
+
gzipCompressionLevel = gzip.BestSpeed
+
gzipExceptMIMETypes = []string{"image/png"}
+
)
+
+
// Wrap the server handler in a gzip handler to compress larger responses.
+
gzipHandler, err := gzhttp.NewWrapper(
+
gzhttp.MinSize(gzipMinSizeBytes),
+
gzhttp.CompressionLevel(gzipCompressionLevel),
+
gzhttp.ExceptContentTypes(gzipExceptMIMETypes),
+
)
+
if err != nil {
+
return err
+
}
+
+
server := &Server{
+
echo: e,
+
client: &client,
+
plcHost: cctx.String("plc-host"),
+
}
+
+
server.httpd = &http.Server{
+
Handler: gzipHandler(server),
+
Addr: httpAddress,
+
WriteTimeout: httpTimeout,
+
ReadTimeout: httpTimeout,
+
MaxHeaderBytes: httpMaxHeaderBytes,
+
}
+
+
e.HideBanner = true
+
// SECURITY: Do not modify without due consideration.
+
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
+
ContentTypeNosniff: "nosniff",
+
XFrameOptions: "SAMEORIGIN",
+
HSTSMaxAge: 31536000, // 365 days
+
// TODO:
+
// ContentSecurityPolicy
+
// XSSProtection
+
}))
+
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
+
// Don't log requests for static content.
+
Skipper: func(c echo.Context) bool {
+
return strings.HasPrefix(c.Request().URL.Path, "/static")
+
},
+
}))
+
e.Renderer = NewRenderer("templates/", &TemplateFS, debug)
+
e.HTTPErrorHandler = server.errorHandler
+
+
// redirect trailing slash to non-trailing slash.
+
// all of our current endpoints have no trailing slash.
+
e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{
+
RedirectCode: http.StatusFound,
+
}))
+
+
staticHandler := http.FileServer(func() http.FileSystem {
+
if debug {
+
log.Debugf("serving static file from the local file system")
+
return http.FS(os.DirFS("static"))
+
}
+
fsys, err := fs.Sub(StaticFS, "static")
+
if err != nil {
+
log.Fatal(err)
+
}
+
return http.FS(fsys)
+
}())
+
+
// static file routes
+
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
+
e.GET("/favicon.ico", echo.WrapHandler(staticHandler))
+
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)))
+
e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
+
e.GET("/security.txt", func(c echo.Context) error {
+
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
+
})
+
+
// actual pages/views
+
e.GET("/", server.WebHome)
+
e.GET("/resolve", server.WebResolve)
+
e.GET("/did/:did", server.WebDid)
+
e.GET("/spec/v0.1/did-plc", server.WebSpecZeroOne)
+
// TODO: e.GET("/api/redoc", server.WebSpecZeroOne)
+
// TODO: e.GET("/api/openapi3.json", server.WebSpecZeroOne)
+
+
// Start the server.
+
log.Infof("starting server address=%s", httpAddress)
+
go func() {
+
if err := server.httpd.ListenAndServe(); err != nil {
+
if !errors.Is(err, http.ErrServerClosed) {
+
log.Errorf("HTTP server shutting down unexpectedly: %s", err)
+
}
+
}
+
}()
+
+
// Wait for a signal to exit.
+
log.Info("registering OS exit signal handler")
+
quit := make(chan struct{})
+
exitSignals := make(chan os.Signal, 1)
+
signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM)
+
go func() {
+
sig := <-exitSignals
+
log.Infof("received OS exit signal: %s", sig)
+
+
// Shut down the HTTP server.
+
if err := server.Shutdown(); err != nil {
+
log.Errorf("HTTP server shutdown error: %s", err)
+
}
+
+
// Trigger the return that causes an exit.
+
close(quit)
+
}()
+
<-quit
+
log.Infof("graceful shutdown complete")
+
return nil
+
}
+
+
func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+
srv.echo.ServeHTTP(rw, req)
+
}
+
+
func (srv *Server) Shutdown() error {
+
log.Info("shutting down")
+
+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+
defer cancel()
+
+
return srv.httpd.Shutdown(ctx)
+
}
+
+
func (srv *Server) errorHandler(err error, c echo.Context) {
+
code := http.StatusInternalServerError
+
if he, ok := err.(*echo.HTTPError); ok {
+
code = he.Code
+
}
+
c.Logger().Error(err)
+
data := pongo2.Context{
+
"statusCode": code,
+
}
+
c.Render(code, "error.html", data)
+
}
+
+
func (srv *Server) WebHome(c echo.Context) error {
+
data := pongo2.Context{}
+
return c.Render(http.StatusOK, "templates/home.html", data)
+
}
+
+
func (srv *Server) WebSpecZeroOne(c echo.Context) error {
+
data := pongo2.Context{}
+
data["html_title"] = "did:plc Specification v0.1"
+
data["markdown_html"] = string(blackfriday.Run(specZeroOneMarkdown))
+
return c.Render(http.StatusOK, "templates/markdown.html", data)
+
}
+
+
func (srv *Server) WebResolve(c echo.Context) error {
+
data := pongo2.Context{}
+
did := c.QueryParam("did")
+
if did != "" {
+
return c.Redirect(http.StatusMovedPermanently, "/did/" + did)
+
}
+
return c.Render(http.StatusOK, "templates/resolve.html", data)
+
}
+
+
func (srv *Server) WebDid(c echo.Context) error {
+
data := pongo2.Context{}
+
did := c.Param("did")
+
data["did"] = did
+
if !strings.HasPrefix(did, "did:plc:") {
+
return fmt.Errorf("Not a valid DID PLC identifier: %s", did)
+
}
+
res, err := ResolveDidPlc(srv.client, srv.plcHost, did)
+
if err != nil {
+
return err
+
}
+
data["result"] = res
+
fmt.Println(res.Doc)
+
fmt.Println(res.Doc.VerificationMethod)
+
return c.Render(http.StatusOK, "templates/did.html", data)
+
}
+400
go-didplc/cmd/webplc/spec/v0.1/did-plc.md
···
+
+
DID PLC Specification v0.1
+
==========================
+
+
DID Placeholder is a self-authenticating [DID](https://www.w3.org/TR/did-core/) which is strongly-consistent, recoverable, and allows for key rotation.
+
+
An example DID is: `did:plc:yk4dd2qkboz2yv6tpubpc6co`
+
+
Control over a `did:plc` identity rests in a set of re-configurable "rotation" keys pairs. These keys can sign update "operations" to mutate the identity (including key rotations), with each operation referencing a prior version of the identity state by hash. Each identity starts from an initial "genesis" operation, and the hash of this initial object is what defines the DID itself (that is, the DID URI "identifier" string). A central "directory" server collects and validates operations, and maintains a transparent log of operations for each DID.
+
+
## Motivation
+
+
[Bluesky](https://blueskyweb.xyz/) developed DID Placeholder when designing the [AT Protocol](https://atproto.com) ("atproto") because we were not satisfied with any of the existing DID methods.
+
We wanted a strongly consistent, highly available, recoverable, and cryptographically secure method with fast and cheap propagation of updates.
+
+
We titled the method "Placeholder", because we _don't_ want it to stick around forever in its current form. We are actively hoping to replace it with or evolve it into something less centralized - likely a permissioned DID consortium. That being said, we do intend to support `did:plc` in the current form until after any successor is deployed, with a reasonable grace period. We would also provide a migration route to allow continued use of existing `did:plc` identifiers.
+
+
## How it works
+
+
The core data fields associated with an active `did:plc` identifier at any point in time are listed below. The encoding and structure differs somewhat from DID document formatting and semantics, but this information is sufficient to render a valid DID document.
+
+
- `did` (string): the full DID identifier
+
- `rotationKeys` (array of strings): priority-ordered list of public keys in `did:key` encoding. must include least 1 key and at most 5 keys, with no duplication. control of the DID identifier rests in these keys. not included in DID document.
+
- `verificationMethods` (map with string keys and values): a set service / public key mappings. the values are public keys `did:key` encoding; they get re-encoded in "multibase" form when rendered in DID document. the key strings should not include a `#` prefix; that will be added when rendering the DID document. used to generate `verificationMethods` of DID document. these keys do not have control over the DID document
+
- `alsoKnownAs` (array of strings): priority-ordered list of URIs which indicate other names or aliases associated with the DID identifier
+
- `services` (map with string keys; values are maps with `type` and `endpoint` string fields): a set of service / URL mappings. the key strings should not include a `#` prefix; that will be added when rendering the DID document.
+
+
Every update "operation" to the DID identifier, including the initial creation operation ("genesis" operation), contains all of the above information, except for the `did` field. The DID itself is generated from a hash of the signed genesis operation (details described below), which makes the DID entirely self-certifying. Updates after initial creation contain a pointer to the most-recent previous operation (by hash).
+
+
"Operations" are signed and submitted to the central PLC directory server over an un-authenticated HTTP request. The PLC server validates operations against any and all existing operations on the DID (including signature validation, recovery time windows, etc), and either rejects the operation or accepts and permanently stores the operation, along with a server-generated timestamp.
+
+
A special operation type is a "tombstone", which clears all of the data fields and permanently "de-activates" the DID. Note that the usual recovery time window applies to "tombstone" operations.
+
+
Note that `rotationKeys` and `verificationMethods` ("signing keys") may have public keys which are re-used across many accounts. There is not necessarily a one-to-one mapping between a DID and either "rotation" keys or "signing" keys.
+
+
Only `secp256k1` ("k256") and NIST P-256 ("p256") keys are currently supported, for both "rotation" and "signing" keys.
+
+
### Use with AT Protocol
+
+
The following information should be included for use with atproto:
+
+
- `verificationMethods`: an `atproto` entry with a "blessed" public key type, to be used as a "signing key" for authenticating updates to the account's repository. the signing key does not have any control over the DID identity unless also included in the `rotationKeys` list. best practice is to maintain separation between rotation keys and atproto signing keys
+
- `alsoKnownAs`: should include an `at://` URI indicating a "handle" (hostname) for the account. note that the handle/DID mapping needs to be validated bi-directionally (via handle resolution), and needs to be re-verified periodically
+
- `services`: an `atproto_pds` entry with an `AtprotoPersonalDataServer` type and http/https URL `endpoint` indicating the account's current PDS hostname. for example, `https://pds.example.com` (no `/xrpc/` suffix needed).
+
+
### Operation Serialization, Signing, and Validation
+
+
There are a couple variations on the "operation" data object schema. The operations are also serialized both as simple JSON objects, or binary DAG-CBOR encoding for the purpose of hashing or signing.
+
+
A regular creation or update operation contains the following fields:
+
+
- `type` (string): with fixed value `plc_operation`
+
- `rotationKeys` (array of strings): as described above
+
- `verificationMethods` (mapping of string keys and values): as described above
+
- `alsoKnownAs` (array of strings): as described above
+
- `services` (mapping of string keys and object values): as described above
+
- `prev` (string, nullable): a "CID" hash pointer to a previous operation if an update, or `null` for a creation. if `null`, the key should actually be part of the object, with value `null`, not simply omitted. in DAG-CBOR encoding, the CID is string-encoded, not a binary IPLD "Link"
+
- `sig` (string): signature of the operation in `base64url` encoding
+
+
A tombstone operation contains:
+
+
- `type` (string): with fixed value `plc_tombstone`
+
- `prev` (string): same as above, but not nullable
+
- `sig` (string): signature of the operation (same as above)
+
+
There is also a deprecated legacy operation format, supported *only* for creation ("genesis") operations:
+
+
- `type` (string): with fixed value `create`
+
- `signingKey` (string): single `did:key` value (not an array of strings)
+
- `recoveryKey` (string): single `did:key` value (not an array of strings); and note "recovery" terminology, not "rotation"
+
- `handle` (string): single value, indicating atproto handle, instead of `alsoKnownAs`. bare handle, with no `at://` prefix
+
- `service` (string): single value, http/https URL of atproto PDS
+
- `prev` (null): always include, but always with value `null`
+
- `sig` (string): signature of the operation (same as above)
+
+
Legacy `create` operations are stored in the PLC registry and may be returned in responses, so validating software needs to support that format. Conversion of the legacy format to "regular" operation format is relatively straight-forward, but there exist many `did:plc` identifiers where the DID identifier itself is based on the hash of the old format, so they will unfortunately be around forever.
+
+
The process for signing and hashing operation objects is to first encode them in the DAG-CBOR binary serialization format. [DAG-CBOR](https://ipld.io/specs/codecs/dag-cbor/spec/) is a restricted subset of the Concise Binary Object Representation (CBOR), an IETF standard (RFC 8949), with semantics and value types similar to JSON.
+
+
As an anti-abuse mechanism, operations have a maximum size when encoded as DAG-CBOR. The current limit is 7500 bytes.
+
+
For signatures, the object is first encoded as DAG-CBOR *without* the `sig` field at all (as opposed to a `null` value in that field). Those bytes are signed, and then the signature bytes are encoded as a string using `base64url` encoding. The `sig` value is then populated with the string. In strongly typed programming languages it is a best practice to have distinct "signed" and "unsigned" types.
+
+
When working with signatures, note that ECDSA signatures are not necessarily *deterministic* or *unique*. That is, the same key signing the same bytes *might* generate the same signature every time, or it might generate a *different* signature every time, depending on the cryptographic library and configuration. In some cases it is also easy for a third party to take a valid signature and transform it in to a new, distinct signature, which also validates. Be sure to always use the "validate signature" routine from a cryptographic library, instead of re-signing bytes and directly comparing the signature bytes.
+
+
For `prev` references, the SHA-256 of the previous operation's bytes are encoded as a "[CID](https://github.com/multiformats/cid)", with the following parameters:
+
+
- CIDv1
+
- `base32` multibase encoding (prefix: `b`)
+
- `dag-cbor` multibase type (code: 0x71)
+
- `sha-256` multihash (code: 0x12)
+
+
Rotation keys are serialized as strings using [did:key](https://w3c-ccg.github.io/did-method-key/), and only `secp256k1` ("k256") and NIST P-256 ("p256") are currently supported.
+
+
The signing keys (`verificationMethods`) are also serialized using `did:key` in operations. When rendered in a DID document, signing keys are represented as objects, with the actual keys in multibase encoding, as required by the DID Core specification.
+
+
The DID itself is derived from the hash of the first operation in the log, called the "genesis" operation. The signed operation is encoded in DAG-CBOR; the bytes are hashed with SHA-256; the hash bytes are `base32`-encoded (not hex encoded) as a string; and that string is truncated to 24 chars to yield the "identifier" segment of the DID.
+
+
In pseudo-code:
+
`did:plc:${base32Encode(sha256(createOp)).slice(0,24)}`
+
+
### Identifier Syntax
+
+
The DID Placeholder method name is `plc`. The identifier part is 24 characters
+
long, including only characters from the `base32` encoding set. An example is
+
`did:plc:yk4dd2qkboz2yv6tpubpc6co`. This means:
+
+
- the overall identifier length is 32 characters
+
- the entire identifier is lower-case (and should be normalized to lower-case)
+
- the entire identifier is ASCII, and includes only the characters `a-z`, `0-9`, and `:` (and does not even use digits `0189`)
+
+
+
### Key Rotation & Account Recovery
+
+
Any key specified in `rotationKeys` has the ability to sign operations for the DID document.
+
+
The set of rotation keys for a DID is not included in the DID document. They are an internal detail of PLC, and are stored in the operation log.
+
+
Keys are listed in the `rotationKeys` field of operations in order of descending authority.
+
+
The PLC server provides a 72hr window during which a higher authority rotation key can "rewrite" history, clobbering any operations (or chain of operations) signed by a lower-authority rotation key.
+
+
To do so, that key must sign a new operation that points to the CID of the last "valid" operation - ie the fork point.
+
The PLC server will accept this recovery operation as long as:
+
+
- it is submitted within 72hrs of the referenced operation
+
- the key used for the signature has a lower index in the `rotationKeys` array than the key that signed the to-be-invalidated operation
+
+
+
### Privacy and Security Concerns
+
+
The full history of DID operations and updates, including timestamps, is permanently publicly accessible. This is true even after DID deactivation. It is important to recognize (and communicate to account holders) that any personally identifiable information (PII) encoded in `alsoKnownAs` URIs will be publicly visible even after DID deactivation, and can not be redacted or purged.
+
+
In the context of atproto, this includes the full history of handle updates and PDS locations (URLs) over time. To be explicit, it does not include any other account metadata such as email addresses or IP addresses. Handle history could potentially de-anonymize account holders if they switch handles between a known identity and an anonymous or pseudonymous identity.
+
+
The PLC server does not cross-validate `alsoKnownAs` or `service` entries in operations. This means that any DID can "claim" to have any identity, or to have an active account with any service (identified by URL). This data should *not* be trusted without bi-directionally verification, for example using handle resolution.
+
+
The timestamp metadata encoded in the PLC audit log could be cross-verified against network traffic or other information to de-anonymize account holders. It also makes the "identity creation date" public.
+
+
If "rotation" and "signing" keys are re-used across multiple account, it could reveal non-public identity details or relationships. For example, if two individuals cross-share rotation keys as a trusted backup, that information is public. If device-local recovery or signing keys are uniquely shared by two identifiers, that would indicate that those identities may actually be the same person.
+
+
+
#### PLC Server Trust Model
+
+
The PLC server has a public endpoint to receive operation objects from any client (without authentication). The server verifies operations, orders them according to recovery rules, and makes the log of operations publicly available.
+
+
The operation log is self-certifying, and contains all the information needed to construct (or verify) the the current state of the DID document.
+
+
Some trust is required in the PLC server. Its attacks are limited to:
+
+
- Denial of service: rejecting valid operations, or refusing to serve some information about the DID
+
- Misordering: In the event of a fork in DID document history, the server could choose to serve the "wrong" fork
+
+
+
### DID Creation
+
+
To summarize the process of creating a new `did:plc` identifier:
+
+
- collect values for all of the core data fields, including generating new secure key pairs if necessary
+
- construct an "unsigned" regular operation object. include a `prev` field with `null` value. do not use the deprecated/legacy operation format for new DID creations
+
- serialize the "unsigned" operation with DAG-CBOR, and sign the resulting bytes with one of the initial `rotationKeys`. encode the signature as `base64url`, and use that to construct a "signed" operation object
+
- serialize the "signed" operation with DAG-CBOR, take the SHA-256 hash of those bytes, and encode the hash bytes in `base32`. use the first 24 characters to generate DID value (`did:plc:<hashchars>`)
+
- serialize the "signed" operation as simple JSON, and submit it via HTTP POST to `https://plc.directory/:did`
+
- if the HTTP status code is successful, the DID has been registered
+
+
When "signing" using a "`rotationKey`", what is meant is to sign using the private key associated the public key in the `rotationKey` list.
+
+
### DID Update
+
+
To summarize the process of updating a new `did:plc` identifier:
+
+
- if the current DID state isn't known, fetch the current state from `https://plc.directory/:did/data`
+
- if the most recent valid DID operation CID (hash) isn't known, fetch the audit log from `https://plc.directory/:did/log/audit`, identify the most recent valid operation, and get the `cid` value. if this is a recovery operation, the relevant "valid" operation to fork from may not be the most recent in the audit log
+
- collect updated values for all of the core data fields, including generating new secure key pairs if necessary (eg, key rotation)
+
- construct an "unsigned" regular operation object. include a `prev` field with the CID (hash) of the previous valid operation
+
- serialize the "unsigned" operation with DAG-CBOR, and sign the resulting bytes with one of the previously-existing `rotationKeys`. encode the signature as `base64url`, and use that to construct a "signed" operation object
+
- serialize the "signed" operation as simple JSON, and submit it via HTTP POST to `https://plc.directory/:did`
+
- if the HTTP status code is successful, the DID has been updated
+
- the DID update may be nullified by a "rotation" operation during the recovery window (currently 72hr)
+
+
### DID Deactivation
+
+
To summarize the process of de-activating an existing `did:plc` identifier:
+
+
- if the most recent valid DID operation CID (hash) isn't known, fetch the audit log from `https://plc.directory/:did/log/audit`, identify the most recent valid operation, and get the `cid` value
+
- construct an "unsigned" tombstone operation object. include a `prev` field with the CID (hash) of the previous valid operation
+
- serialize the "unsigned" tombstone operation with DAG-CBOR, and sign the resulting bytes with one of the previously-existing `rotationKeys`. encode the signature as `base64url`, and use that to construct a "signed" tombstone operation object
+
- serialize the "signed" tombstone operation as simple JSON, and submit it via HTTP POST to `https://plc.directory/:did`
+
- if the HTTP status code is successful, the DID has been deactivated
+
- the DID deactivation may be nullified by a "rotation" operation during the recovery window (currently 72hr)
+
+
### DID Resolution
+
+
PLC DIDs are resolved to a DID document (JSON) by making simple HTTP GET request to the PLC server. The resolution endpoint is: `https://plc.directory/:did`
+
+
The PLC-specific state data (based on the most recent operation) can be fetched as a JSON object at: `https://plc.directory/:did/data`
+
+
+
### Audit Logs
+
+
As an additional check against abuse by the PLC server, and to promote resiliency, the set of all identifiers is enumerable, and the set of all operations for all identifiers (even "nullified" operations) can be enumerated and audited.
+
+
The log of currently-valid operations for a given DID, as JSON, can be found at: `https://plc.directory/:did/log/audit`
+
+
The audit history of a given DID (complete with timestamps and invalidated forked histories), as JSON, can be found at: `https://plc.directory/:did/log/audit`
+
+
To fully validate a DID document against the operation log:
+
+
- fetch the full audit log
+
- for the genesis operation, validate the DID
+
- note that the genesis operation may be in deprecated/legacy format, and should be encoded and verified in that format
+
- see the "DID Creation" section above for details
+
- for each operation in the log, validate signatures:
+
- identify the set of valid `rotationKeys` at that point of time: either the initial keys for a "genesis" operation, or the keys in the `prev` operation
+
- remove any `sig` field and serialize the "unsigned" operation with DAG-CBOR, yielding bytes
+
- decode the `base64url` `sig` field to bytes
+
- for each of the `rotationKeys`, attempt to verify the signature against the "unsigned" bytes
+
- if no key matches, there has been a trust violation; the PLC server should never have accepted the operation
+
- verify the correctness of "nullified" operations and the current active operation log using the rules around rotation keys and recovery windows
+
+
The complete log of operations for all DIDs on the PLC server can be enumerated efficiently:
+
+
- HTTP endpoint: `https://plc.directory/export`
+
- output format: [JSON lines](https://jsonlines.org/)
+
- `count` query parameter, as an integer, maximum 1000 lines per request
+
- `after` query parameter, based on `createdAt` timestamp, for pagination
+
+
+
## Example
+
+
```ts
+
// note: we use shorthand for keys for ease of reference, but consider them valid did:keys
+
+
// Genesis operation
+
const genesisOp = {
+
type: 'plc_operation',
+
verificationMethods: {
+
atproto: "did:key:zSigningKey"
+
},
+
rotationKeys: [
+
"did:key:zRecoveryKey",
+
"did:key:zRotationKey"
+
],
+
alsoKnownAs: [
+
"at://alice.test"
+
],
+
services: {
+
atproto_pds: {
+
type: "AtprotoPersonalDataServer",
+
endpoint: "https://example.test"
+
}
+
},
+
prev: null,
+
sig: 'sig_from_did:key:zRotationKey'
+
}
+
+
// Operation to update recovery key
+
const updateKeys = {
+
type: 'plc_operation',
+
verificationMethods: {
+
atproto: "did:key:zSigningKey"
+
},
+
rotationKeys: [
+
"did:key:zNewRecoveryKey",
+
"did:key:zRotationKey"
+
],
+
alsoKnownAs: [
+
"at://alice.test"
+
],
+
services: {
+
atproto_pds: {
+
type: "AtprotoPersonalDataServer",
+
endpoint: "https://example.test"
+
}
+
},
+
prev: CID(genesisOp),
+
sig: 'sig_from_did:key:zRotationKey'
+
}
+
+
// Invalid operation that will be rejected
+
// because did:key:zAttackerKey is not listed in rotationKeys
+
const invalidUpdate = {
+
type: 'plc_operation',
+
verificationMethods: {
+
atproto: "did:key:zAttackerKey"
+
},
+
rotationKeys: [
+
"did:key:zAttackerKey"
+
],
+
alsoKnownAs: [
+
"at://bob.test"
+
],
+
services: {
+
atproto_pds: {
+
type: "AtprotoPersonalDataServer",
+
endpoint: "https://example.test"
+
}
+
},
+
prev: CID(updateKeys),
+
sig: 'sig_from_did:key:zAttackerKey'
+
}
+
+
// Valid recovery operation that "undoes" updateKeys
+
const recoveryOp = {
+
type: 'plc_operation',
+
verificationMethods: {
+
atproto: "did:key:zSigningKey"
+
},
+
rotationKeys: [
+
"did:key:zRecoveryKey"
+
],
+
alsoKnownAs: [
+
"at://alice.test"
+
],
+
services: {
+
atproto_pds: {
+
type: "AtprotoPersonalDataServer",
+
endpoint: "https://example.test"
+
}
+
},
+
prev: CID(genesisOp),
+
sig: 'sig_from_did:key:zRecoveryKey'
+
}
+
```
+
+
## Presentation as DID Document
+
+
The following data:
+
+
```ts
+
{
+
did: 'did:plc:7iza6de2dwap2sbkpav7c6c6',
+
verificationMethods: {
+
atproto: 'did:key:zDnaeh9v2RmcMo13Du2d6pjUf5bZwtauYxj3n9dYjw4EZUAR7'
+
},
+
rotationKeys: [
+
'did:key:zDnaedvvAsDE6H3BDdBejpx9ve2Tz95cymyCAKF66JbyMh1Lt',
+
'did:key:zDnaeh9v2RmcMo13Du2d6pjUf5bZwtauYxj3n9dYjw4EZUAR7'
+
],
+
alsoKnownAs: [
+
'at://alice.test'
+
],
+
services: {
+
atproto_pds: {
+
type: "AtprotoPersonalDataServer",
+
endpoint: "https://example.test"
+
}
+
}
+
}
+
```
+
+
Will be presented as the following DID document:
+
+
```ts
+
{
+
'@context': [
+
'https://www.w3.org/ns/did/v1',
+
'https://w3id.org/security/suites/ecdsa-2019/v1'
+
],
+
id: 'did:plc:7iza6de2dwap2sbkpav7c6c6',
+
alsoKnownAs: [ 'at://alice.test' ],
+
verificationMethod: [
+
{
+
id: '#atproto',
+
type: 'EcdsaSecp256r1VerificationKey2019',
+
controller: 'did:plc:7iza6de2dwap2sbkpav7c6c6',
+
publicKeyMultibase: 'zSSa7w8s5aApu6td45gWTAAFkqCnaWY6ZsJ8DpyzDdYmVy4fARKqbn5F1UYBUMeVvYTBsoSoLvZnPdjd3pVHbmAHP'
+
}
+
],
+
service: [
+
{
+
id: '#atproto_pds',
+
type: 'AtprotoPersonalDataServer',
+
serviceEndpoint: 'https://example2.com'
+
}
+
]
+
}
+
```
+
+
## Possible Future Changes
+
+
The set of allowed ("blessed") public key cryptographic algorithms (aka, curves) may expanded over time, slowly. Likewise, support for additional "blessed" CID types and parameters may be expanded over time, slowly.
+
+
The recovery time window may become configurable, within constraints, as part of the DID metadata itself.
+
+
Support for "DID Controller Delegation" could be useful (eg, in the context of atproto PDS hosts), and may be incorporated.
+
+
In the context of atproto, support for multiple "handles" for the same DID is being considered, with a single "primary" handle. But no final decision has been made yet.
+
+
We welcome proposals for small additions to make `did:plc` more generic and reusable for applications other than atproto. But no promises: atproto will remain the focus for the near future.
+
+
Moving governance of the `did:plc` method, and operation of registry servers, out of the sole control of Bluesky PBLLC is something we are enthusiastic about. Audit log snapshots, mirroring, and automated third-party auditing have all been considered as mechanisms to mitigate the centralized nature of the PLC server.
+
+
The size of the `verificationMethods`, `alsoKnownAs`, and `service` mappings/arrays may be specifically constrained. And the maximum DAG-CBOR size may be constrained.
+
+
As an anti-abuse mechanisms, the PLC server load balancer restricts the number of HTTP requests per time window. The limits are generous, and operating large services or scraping the operation log should not run in to limits. Specific per-DID limits on operation rate may be introduced over time. For example, no more than N operations per DID per rotation key per 24 hour window.
+
+
A "DID PLC history explorer" web interface would make the public nature of the DID audit log more publicly understandable.
+
+
It is concievable that longer DID PLCs, with more of the SHA-256 characters, will be supported in the future. It is also concievable that a different hash algorithm would be allowed. Any such changes would allow existing DIDs in their existing syntax to continue being used.
+1
go-didplc/cmd/webplc/static
···
+
../../static/
+1
go-didplc/cmd/webplc/templates
···
+
../../templates/
+3
go-didplc/example.dev.env
···
+
ATP_PLC_HOST='http://localhost:2582'
+
DEBUG='true'
+
GOLOG_LOG_LEVEL=debug
+4
go-didplc/example.env
···
+
#ATP_PLC_HOST='https://plc.directory'
+
#HTTP_ADDRESS=':8700'
+
#DEBUG='false'
+
#GOLOG_LOG_LEVEL=warn
+35
go-didplc/go.mod
···
+
module github.com/bluesky-social/did-method-plc/webplc
+
+
go 1.20
+
+
require (
+
github.com/flosch/pongo2/v6 v6.0.0
+
github.com/ipfs/go-log v1.0.5
+
github.com/joho/godotenv v1.5.1
+
github.com/klauspost/compress v1.16.7
+
github.com/labstack/echo/v4 v4.11.1
+
github.com/urfave/cli/v2 v2.25.7
+
)
+
+
require (
+
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+
github.com/gogo/protobuf v1.3.2 // indirect
+
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
+
github.com/ipfs/go-log/v2 v2.1.3 // indirect
+
github.com/labstack/gommon v0.4.0 // indirect
+
github.com/mattn/go-colorable v0.1.13 // indirect
+
github.com/mattn/go-isatty v0.0.19 // indirect
+
github.com/opentracing/opentracing-go v1.2.0 // indirect
+
github.com/russross/blackfriday/v2 v2.1.0 // indirect
+
github.com/valyala/bytebufferpool v1.0.0 // indirect
+
github.com/valyala/fasttemplate v1.2.2 // indirect
+
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
+
go.uber.org/atomic v1.7.0 // indirect
+
go.uber.org/multierr v1.6.0 // indirect
+
go.uber.org/zap v1.16.0 // indirect
+
golang.org/x/crypto v0.11.0 // indirect
+
golang.org/x/net v0.12.0 // indirect
+
golang.org/x/sys v0.10.0 // indirect
+
golang.org/x/text v0.11.0 // indirect
+
golang.org/x/time v0.3.0 // indirect
+
)
+136
go-didplc/go.sum
···
+
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
+
github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
+
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
+
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
+
github.com/ipfs/go-log/v2 v2.1.3 h1:1iS3IU7aXRlbgUpN8yTTpJ53NXYjAe37vcI5+5nYrzk=
+
github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
+
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
+
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
+
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
+
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
+
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
+
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
+
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
+
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
+
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
+
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
+
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
+
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
+
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
+
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
+
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
+
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+3
go-didplc/static/.well-known/security.txt
···
+
Contact: mailto:security@bsky.app
+
Preferred-Languages: en
+
Canonical: https://web.plc.directory/.well-known/security.txt
+5
go-didplc/static/robots.txt
···
+
# Hello Friends!
+
+
# By default, may crawl anything on this domain. HTTP 429 ("backoff") status codes are used for rate-limiting. Up to a handful of concurrent requests should be ok.
+
User-Agent: *
+
Allow: /
+44
go-didplc/templates/base.html
···
+
<!doctype html>
+
<html lang="en">
+
<head>
+
<meta charset="utf-8">
+
<meta name="referrer" content="origin-when-cross-origin">
+
<meta name="viewport" content="width=device-width, initial-scale=1">
+
<link rel="stylesheet" href="/static/simple.min.css">
+
<title>{%- block head_title -%}did:plc Directory{%- endblock -%}</title>
+
+
<style>
+
</style>
+
+
<!--
+
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
+
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
+
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
+
-->
+
{% block html_head_extra -%}{%- endblock %}
+
<meta name="application-name" name="did:plc Website">
+
<meta name="generator" name="webplc">
+
</head>
+
<body>
+
{%- block body_all %}
+
+
+
<header>
+
<nav>
+
<a href="/">Home</a>
+
<a href="/resolve" >Lookup</a>
+
<a href="/spec/v0.1/did-plc" >Specification</a>
+
<a href="https://github.com/bluesky-social/did-method-plc">Source Code</a>
+
</nav>
+
<h1>did:plc Identifier Directory</h1>
+
<p>a self-authenticating Decentralized Identifier (DID) system which is strongly-consistent, recoverable, and allows for key rotation</p>
+
</header>
+
<main>
+
{% block main %}{% endblock %}
+
</main>
+
<footer>
+
<p>Developed by <a href="https://blueskyweb.xyz">Bluesky</a> for <a href="https://atproto.com">atproto</a>.</p>
+
</footer>
+
{% endblock -%}
+
</body>
+
</html>
+80
go-didplc/templates/did.html
···
+
{% extends "base.html" %}
+
+
{% block head_title %}
+
{{ did }}
+
{% endblock %}
+
+
{% block main -%}
+
<h3 style="font-family: monospace;">{{ did }}</h3>
+
+
{% if result.StatusCode == 200 %}
+
+
<h4>Names and Aliases</h4>
+
<div style="width: 100%; overflow-x: auto;">
+
<table style="width: 100%;">
+
<thead>
+
<tr>
+
<th>URI</th>
+
</tr>
+
</thead>
+
<tbody style="font-family: monospace;">
+
{% for uri in result.Doc.AlsoKnownAs %}
+
<tr>
+
<td>{{ uri }}</td>
+
</tr>
+
{% endfor %}
+
</tbody>
+
</table>
+
</div>
+
+
<h4>Services</h4>
+
<div style="width: 100%; overflow-x: auto;">
+
<table style="width: 100%;">
+
<thead>
+
<tr>
+
<th>ID</th>
+
<th>Type</th>
+
<th>URL</th>
+
</tr>
+
</thead>
+
<tbody style="font-family: monospace;">
+
{% for vm in result.Doc.Service %}
+
<tr>
+
<td>{{ vm.Id }}</td>
+
<td>{{ vm.Type }}</td>
+
<td>{{ vm.ServiceEndpoint }}</td>
+
</tr>
+
{% endfor %}
+
</tbody>
+
</table>
+
</div>
+
+
<h4>Verification Methods</h4>
+
<div style="width: 100%; overflow-x: auto;">
+
<table style="width: 100%;">
+
<thead>
+
<tr>
+
<th>ID</th>
+
<th>Type</th>
+
<th>Public Key (multibase-encoded)</th>
+
</tr>
+
</thead>
+
<tbody style="font-family: monospace;">
+
{% for vm in result.Doc.VerificationMethod %}
+
<tr>
+
<td>{{ vm.Id }}</td>
+
<td>{{ vm.Type }}</td>
+
<td>{{ vm.PublicKeyMultibase }}</td>
+
</tr>
+
{% endfor %}
+
</tbody>
+
</table>
+
</div>
+
+
<h4>DID Document JSON</h4>
+
<pre><code>
+
{{- result.DocJson -}}
+
</code></pre>
+
{% endif %}
+
+
{%- endblock %}
+9
go-didplc/templates/error.html
···
+
{% extends "base.html" %}
+
+
{% block head_title %}Error {{ statusCode }} - did:plc{% endblock %}
+
+
{%- block body_all %}
+
<h1>{{ statusCode }}: Server Error</h1>
+
<p>Sorry about that! Our <a href="https://status.bsky.app/">Status Page</a> might have more context.
+
{% endif %}
+
{% endblock -%}
+8
go-didplc/templates/home.html
···
+
{% extends "base.html" %}
+
+
{% block main -%}
+
<h2>Motivation</h2>
+
<p>Bluesky developed DID Placeholder when designing the AT Protocol ("atproto") because we were not satisfied with any of the existing DID methods. We wanted a strongly consistent, highly available, recoverable, and cryptographically secure method with fast and cheap propagation of updates.</p>
+
+
<p>We titled the method "Placeholder", because we don't want it to stick around forever in its current form. We are actively hoping to replace it with or evolve it into something less centralized - likely a permissioned DID consortium. That being said, we do intend to support did:plc in the current form until after any successor is deployed, with a reasonable grace period. We would also provide a migration route to allow continued use of existing did:plc identifiers.</p>
+
{%- endblock %}
+9
go-didplc/templates/markdown.html
···
+
{% extends "base.html" %}
+
+
{% block head_title %}
+
{{ html_title }}
+
{% endblock %}
+
+
{% block main -%}
+
{{ markdown_html|safe }}
+
{%- endblock %}
+15
go-didplc/templates/resolve.html
···
+
{% extends "base.html" %}
+
+
{% block head_title %}
+
Resolve did:plc
+
{% endblock %}
+
+
{% block main -%}
+
<h2>Resolve a did:plc Identifier</h2>
+
<form action="/resolve" method="get">
+
<p><label>DID</label>
+
<input type="text" name="did">
+
</p>
+
<input type="submit" value="Submit">
+
</form>
+
{%- endblock %}