~sircmpwn/sr.ht-dev

hub.sr.ht: well-known: support openpgp web key discovery v1 NEEDS REVISION

oliverpool: 1
 well-known: support openpgp web key discovery

 6 files changed, 313 insertions(+), 3 deletions(-)
#1054600 alpine.yml success
#1054601 archlinux.yml failed
#1054602 debian.yml success
https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/40799#%3CCT18V3ZK9ZW7.2VDLU3CMFNATN@taiga%3E
Next
I think we can go ahead and take it, yeah.
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/44509/mbox | git am -3
Learn more about email & git

[PATCH hub.sr.ht] well-known: support openpgp web key discovery Export this patch

will allow clients to discover the public key automatically, using:
gpg --locate-keys outgoing@sr.ht

Internet-Draft https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service#name-web-key-directory

---

Initial discussion: https://lists.sr.ht/~sircmpwn/sr.ht-discuss/%3Cdd965bea-a130-4675-8cdf-742286607079%40app.fastmail.com%3E
First patch (on meta.sr.ht frontend): https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/40794
Second patch (on meta.sr.ht backend): https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/40799

Current patch: hub.sr.ht backend.

It fixes the discrepancy noted by ~bitfehler (with an added test)

To be publicly accessible, adjustements must be made to sr.ht-nginx/hub.sr.ht.conf, like (untested):

	location /.well-known/openpgpkey {
		proxy_pass http://127.0.0.1:5114;
	}
---
 api/openpgpkey/opengpgkey_test.go           |  78 +++++++++++
 api/openpgpkey/openpgpkey.go                | 141 ++++++++++++++++++++
 api/openpgpkey/testdata/expected_served_key | Bin 0 -> 1169 bytes
 api/openpgpkey/testdata/pubkey              |  80 +++++++++++
 api/server.go                               |   8 ++
 go.mod                                      |   9 +-
 6 files changed, 313 insertions(+), 3 deletions(-)
 create mode 100644 api/openpgpkey/opengpgkey_test.go
 create mode 100644 api/openpgpkey/openpgpkey.go
 create mode 100644 api/openpgpkey/testdata/expected_served_key
 create mode 100644 api/openpgpkey/testdata/pubkey

diff --git a/api/openpgpkey/opengpgkey_test.go b/api/openpgpkey/opengpgkey_test.go
new file mode 100644
index 0000000..67d6306
--- /dev/null
+++ b/api/openpgpkey/opengpgkey_test.go
@@ -0,0 +1,78 @@
package openpgpkey

import (
	"net/http"
	"net/http/httptest"
	"os"
	"testing"

	"github.com/go-chi/chi"
	"github.com/stretchr/testify/assert"
	"github.com/vaughan0/go-ini"
)

func TestNewWellKnown(t *testing.T) {
	router := chi.NewRouter()
	err := MountWebKeyDirectoryRoutes(ini.File{
		"sr.ht": ini.Section{
			"global-domain": "sr.ht",
		},
		"mail": ini.Section{
			"pgp-pubkey": "testdata/pubkey",
		},
	}, router)
	assert.NoError(t, err)

	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, httptest.NewRequest("GET", "/.well-known/openpgpkey/hu/4y36rkzdjnzmk3oxaekyi5biowgr5kcz", nil)) // admin in zbase32
	assert.Equal(t, http.StatusOK, resp.Result().StatusCode)
	assert.Equal(t, "application/octet-stream", resp.Header().Get("Content-Type"))

	// reference file created with $(gpgconf --list-dirs libexecdir)/gpg-wks-client -v --install-key -C . ./pubkey admin@sr.ht
	expectedKey, err := os.ReadFile("testdata/expected_served_key")
	assert.NoError(t, err)
	servedKey := resp.Body.Bytes()
	assert.NoError(t, os.WriteFile("testdata/served_key", servedKey, 0o666))
	if assert.Len(t, servedKey, len(expectedKey)) {
		// 13 bytes are different because gpg used the "new-format tag byte"
		// https://superuser.com/a/1610202
		diff := 0
		for i, got := range servedKey {
			expected := expectedKey[i]
			if got != expected {
				diff++
			}
		}
		if assert.Equal(t, 13, diff) {
			// everything look fine, remove the testdata/served_key
			assert.NoError(t, os.Remove("testdata/served_key"))
		} else {
			// assert display a nice hex diff
			assert.Equal(t, expectedKey, servedKey)
		}
	}
}

func TestEncodeLocalPart(t *testing.T) {
	assert.Equal(t, "4y36rkzdjnzmk3oxaekyi5biowgr5kcz", encodeLocalPart("admin"))
	assert.Equal(t, "4y36rkzdjnzmk3oxaekyi5biowgr5kcz", encodeLocalPart("Admin"))
	assert.Equal(t, "e5a4bxki1ktx1jncwco5nkcofedmkxod", encodeLocalPart("git"))
}

func TestGetGlobalDomain(t *testing.T) {
	globalDomain, err := getGlobalDomain(ini.File{
		"sr.ht": ini.Section{
			"global-domain": "sr.ht",
		},
	}, "meta.sr.ht")
	assert.NoError(t, err)
	assert.Equal(t, "sr.ht", globalDomain)

	globalDomain, err = getGlobalDomain(ini.File{
		"meta.sr.ht": ini.Section{
			"origin": "http://meta.sr.ht.local",
		},
	}, "meta.sr.ht")
	assert.NoError(t, err)
	assert.Equal(t, "sr.ht.local", globalDomain)
}
diff --git a/api/openpgpkey/openpgpkey.go b/api/openpgpkey/openpgpkey.go
new file mode 100644
index 0000000..a43aeed
--- /dev/null
+++ b/api/openpgpkey/openpgpkey.go
@@ -0,0 +1,141 @@
package openpgpkey

import (
	"crypto/sha1"
	"encoding/base32"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"

	"github.com/ProtonMail/go-crypto/openpgp"
	"github.com/go-chi/chi"
	"github.com/vaughan0/go-ini"
)

// MountWebKeyDirectoryRoutes mounts the routes to serve the public key (binary encoded) under the well-known URLs:
//   - /.well-known/openpgpkey/hu/<zbase32-hash>
//   - /.well-known/openpgpkey/hu/policy
//
// See https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service
func MountWebKeyDirectoryRoutes(conf ini.File, router chi.Router) error {
	pubKeyPath, ok := conf.Get("mail", "pgp-pubkey")
	if !ok {
		return errors.New("expected [mail]pgp-pubkey in config")
	}

	pubKeyFile, err := os.Open(pubKeyPath)
	if err != nil {
		return fmt.Errorf("failed to open [mail]pgp-pubkey: %v", err)
	}
	defer pubKeyFile.Close()

	keyring, err := openpgp.ReadArmoredKeyRing(pubKeyFile)
	if err != nil {
		return err
	}

	globalDomain, err := getGlobalDomain(conf, "meta.sr.ht")
	if err != nil {
		return err
	}

	for _, entity := range keyring {
		for _, identidy := range entity.Identities {
			local, domain, found := stringsCut(identidy.UserId.Email, "@")
			if !found || domain != globalDomain {
				continue
			}
			serializePublicKey := wkdSerializer(entity, identidy)
			router.Get("/.well-known/openpgpkey/hu/"+encodeLocalPart(local), func(w http.ResponseWriter, r *http.Request) {
				w.Header().Set("Content-Type", "application/octet-stream")
				serializePublicKey(w)
			})
		}
	}
	router.Get("/.well-known/openpgpkey/hu/policy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain")
		w.Write([]byte("# Policy flags for the OpenPGP Web Key Directory"))
	}))
	return nil
}

func wkdSerializer(entity *openpgp.Entity, identidy *openpgp.Identity) func(io.Writer) error {
	return func(w io.Writer) error {
		if err := entity.PrimaryKey.Serialize(w); err != nil {
			return err
		}
		if err := identidy.UserId.Serialize(w); err != nil {
			return err
		}
		for _, sig := range identidy.Signatures {
			if err := sig.Serialize(w); err != nil {
				return err
			}
		}
		for _, subkey := range entity.Subkeys {
			if err := subkey.PublicKey.Serialize(w); err != nil {
				return err
			}
			for _, revocation := range subkey.Revocations {
				if err := revocation.Serialize(w); err != nil {
					return err
				}
			}
			if err := subkey.Sig.Serialize(w); err != nil {
				return err
			}

		}
		return nil
	}
}

var zbase32Encoding = base32.NewEncoding("ybndrfg8ejkmcpqxot1uwisza345h769").WithPadding(base32.NoPadding)

func encodeLocalPart(s string) string {
	hash := sha1.New()
	hash.Write([]byte(strings.ToLower(s)))
	shaOne := hash.Sum(nil)
	return zbase32Encoding.EncodeToString(shaOne)
}

// Gets the global domain from the config. If it's not defined, assume that
// the given site is a sub-domain of the global domain, i.e. it is of the
// form `blah.globaldomain.com`.
func getGlobalDomain(conf ini.File, site string) (string, error) {
	globalDomain, ok := conf.Get("sr.ht", "global-domain")
	if ok {
		return globalDomain, nil
	}
	serviceOrigin, ok := conf.Get(site, "origin")
	if !ok {
		return "", fmt.Errorf("expected [sr.ht]global-domain or [%s]origin in config", site)
	}
	serviceURL, err := url.Parse(serviceOrigin)
	if err != nil {
		return "", fmt.Errorf("failed to parse URL [%s]origin in config: %v", site, err)
	}
	_, globalDomain, _ = stringsCut(serviceURL.Hostname(), ".")
	return globalDomain, nil
}

// strings.Cut is available starting with go1.18
// Copied from https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/strings/strings.go;l=1262
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license.
//
// stringsCut slices s around the first instance of sep,
// returning the text before and after sep.
// The found result reports whether sep appears in s.
// If sep does not appear in s, cut returns s, "", false.
func stringsCut(s, sep string) (before, after string, found bool) {
	if i := strings.Index(s, sep); i >= 0 {
		return s[:i], s[i+len(sep):], true
	}
	return s, "", false
}
diff --git a/api/openpgpkey/testdata/expected_served_key b/api/openpgpkey/testdata/expected_served_key
new file mode 100644
index 0000000000000000000000000000000000000000..0650ef3865a8a907fe71bdb19ca7b0978a29be49
GIT binary patch
literal 1169
zcmV;C1aAA80SyFL3eeO62mrjW#%(2aeVVDQPfuTXRMLH@P(mU}#bq0>%LAM3x7F^G
zqdA!-Si!~^Wa|bYx3q)|3_*%I9n@y&8r4d3h6QDY_n=p)^E&W2@haX9GVNb$u5C&E
zB?g+PWoh-$0|6M))e2^douh}W<|U?ERnPdG3g6hf3Z+Q*wFEPRNxH5b(7t+A9=ikf
znWz`*@Zxl17j>FPb|!F9S;7Ig{<DXcgF+f$-XvP(0w=1y$~MAAT<=N?bF^Dy!8Xf~
zas-@J_hNmmPEUISx$z-jN&e6vk_LjGlOZt32Cc(BkC)&wg)7>N;#rjhrW$N|R0u%;
z(vqEtwp-V~BaS7Kaq9pP0RREC6LWGdXmlVvVPtJ-Za{N#E@*T<i2*kR69EVSAq4_h
z3eeO78v_Lk2?z%Q1{DYi3JU@R76JnS0v-VZ7k~f?2@qwM1kt04ldgtb2melFzvfjw
zBtWw@<nKoHfnx4jhG}$eB_on1#h6I?QR{=ex%nG)4+j)TjWtAE^+;9=qeDJqk@>!7
zd`q&{!R7ijoK^6OKzr}v<XFV%Tq!alqmu9-ak79QA(p16_kSyvGZI~@A4uBfRmlH1
z<eMouju`q~p@U?f#>#0t>!l<k1u{px{=(lCLn?2c-zNW}@=ce{+-CCcg}`Y&7eay`
zUQz3=VM8e{rXw86D`oeH#Y%p;CxmX3#8XC-yZyzH{=ttnXrtmgZmJ;B#x3kQ>?N!W
zuwJ^#<@a8JQaK~GVP6x5R~-j_LMmO%Q#x<;NKmHx&A-M6Ik8z0KO4CL4Fp&U(9{74
z0LoH4T40{<vdU#z^HiLEVUg42rlOrgnP+5kB=$zCM-d`;JLP-Hdml(b)RFqzQ3-_)
zrPy9)j1?JHVL~(-3f8=Uz0VlfO}=urF)*L2@0YDKyk`RUsgNcg@|z|32-@F<>wSru
zPKg$NQAB}}cs;nthIYY3dCMpzhtB>bw%n{!vS<|dwK=8uWiam3(l|aOiEqcG6##;9
z)fV;scF>B&)w~fP{?LVWlVU`9ky2V2ozGk3HUaE~!_n3ZLI7gNwk&8mzLVa*dN9{U
zZ@D|$cTcKYGbgS0DqCz-=|5m*^t_wJ)gk-dU*PgDgq;JM1Gt^A-pz6^C6?3P0{{^L
z00D^s9|RZy2mlEM0$2*r)B+m}0162ZWtRleql%NR466tLv!lJb9I<Jq{Luu;loJ|Z
zh}>!~WHMUPL}#cGM~|zes^MIzRL^aJC|75$)JK<zwloRMQDTfBqu`fp1Z6+H>mRXd
zSUI<r{h!tVVtQ{PxZqx3FQSQI|3C7bw+lEKz0RR@U~}=u-Afx8L72=VmCKJOgVN+b
zLvk~j2X~vA{7B?&LL@aWiy)J;#5ndtH|M@w<}&E%P2~5_6k&mpg96vRZ@%FOhfa+8
zbt;z^18lqOdkE$qY@-(Jj0B%^yI-{ii;50GO72Y8m{Dq2B%MVyVdv4boas<VFVjY{
j{Cw%3E7_NImj#fO)ZM0?L)bW-EI|b3r%2Loooy2*?miwD

literal 0
HcmV?d00001

diff --git a/api/openpgpkey/testdata/pubkey b/api/openpgpkey/testdata/pubkey
new file mode 100644
index 0000000..27c1a2c
--- /dev/null
+++ b/api/openpgpkey/testdata/pubkey
@@ -0,0 +1,80 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBFgK0NQBCAC8sMZtJXV9mqmtT09feFTSfahQQiJJxWUbr8sDm+231e6TozmZ
JljBxhhk6wYht7SEDAxBijod1GbpGtVKc4YFZYb3oFep8zrwOPEq3g4y7V9rrm1J
/SUGmqhlafXQAwEY0tUKZoudo4es5iWmW1XP+JwK39i7CqVI97UEM4NJuq4d0L56
VR67A/aZqBfr8OJ0Yxd1mkd2JnBRWcIBt/6zh5aDQhpg3iRa5QInqr3KNsJGXO9K
C3O0W2PBNsuQcgScVfdifa1OT3sDufEhYEn+0CCSBoKfkyEwyAatwz6Pl+AyhSva
i+JZlQSmGmx6VAhBANKSnYm2W9e/I44lknHrABEBAAG0E3NyLmh0IDxhZG1pbkBz
ci5odD6JATcEEwEIACEFAlgK0NQCGwMFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AA
CgkQZZcE0aOKk66GXAf/TmS/5lU+JECzNeTvRvWBYu5ahml0biUjkibFmEj6UeuD
vLn5G3UPBxRIjTVEXPVIVgujQz5kkfm+Z3xLstbB5fo1nFXwikB77+LkWMTpXCky
IqOS8CBxsoAgIZamp/d/K5YzEl2qH0ja5lXI/zjkmyk5jhj6XaGDZJ/Gymk866Uk
IwUyR7z+wt8VQypvnt8m/6LyTZfO3Gby74XAaT0XQoIfXlHrrmFDKS6mIxzKK2X3
iMVKfrknhG6TxFNGk7v9xZH+wY82aKPiO26qINHGLew57CWsDLBeusrl916BUjkj
tmFfE4ZXHQd+QipdzFM6b/VIUKb7zb/GBzmxWRE/G7RIc3IuaHQgb3V0Z29pbmcg
KEdlbmVyYWwgcHVycG9zZSBvdXRnb2luZyBzci5odCBhZGRyZXNzKSA8b3V0Z29p
bmdAc3IuaHQ+iQFOBBMBCAA4FiEERHtp5LNL6QvIKaDpZZcE0aOKk64FAlwreIwC
GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQZZcE0aOKk64s7Af/RHO4hxQi
rx4+W1Kqe3f6An9KcrFQ1hDUzWDm5KKZE7RztD1/9BoFXK4UOyPB1F+Ng0gCknLI
65ISSsiC9CbS0adOf5nAO5cUbwSv2A1D/TgznVf1dZvdBQ1YuCyV53UXFryZJ4d7
NgPQPOrzaQE7rQutbk/wlNSKn7t3rkJjVaSPAofdjG0HCb2Uc/oFSCNG6aY3SqBv
yr0ys57CrdhKqg2DVQcsZk8elSEQu4DIFZm5xsM7wxN57qA3Xshm347t4tWH+IpT
i9sqjIBCa+TEy5frhkgj/ykIl9AKrIWs27KEvCogXy6aiwbQS8K9+Mp4d8TKmv12
nO79CM5j7vFLa7RGc3IuaHQgdG9kbyAoQWRkcmVzcyBmb3IgYXV0b21hdGVkIGVt
YWlscyBmcm9tIHRvZG8uc3IuaHQpIDx0b2RvQHNyLmh0PokBTgQTAQgAOBYhBER7
aeSzS+kLyCmg6WWXBNGjipOuBQJcK3ihAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B
AheAAAoJEGWXBNGjipOuexMH/jBckRZTSVT9CciFzAslsuqRRehJ4Tae5EW4X5Ou
JGnmFVDb+nn1T14I+I+GhA3URYuCe1pEFiJfg368pXqGDbh/9JSmzWd9PZtGGlI6
JApQlPYxapfkv3UrXtF0T7Rj0yTTgf6vkjSaHdp05bCvatTO7UAD01s9/TCHloeP
Mxkx9e4BKfhO2y/aVeNFW9EASKG46q/2c5UyUEjoVCYCV14L3vMcbXNO0hQOeY6o
WQD245tfNRPUMNumCfdE+FF8PWYGkhypPf9Os4FfsHQsj12bi3CNEa7wrVuMwRU3
qTWqkc+0c/HMU471DIh6YH7dxFgLTqDtZ7SqoFufmwJ+qTS0THNyLmh0IGJ1aWxk
cyAoQWRkcmVzcyBmb3IgYXV0b21hdGVkIGVtYWlscyBmcm9tIGJ1aWxkcy5zci5o
dCkgPGJ1aWxkc0Bzci5odD6JAU4EEwEIADgWIQREe2nks0vpC8gpoOlllwTRo4qT
rgUCXCt4sgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBllwTRo4qTrjzo
B/oCCFAGAhrQJADDH7ALYd6K7kQgtHjOE++u2ZAjbbu5CBgIcao5UKe0t3u8xVDK
E0gy/v2Y/vyu1VacaXWhvV52dm+YA6UYMDzlyTngQ6zXP2MmmOXYm9o7hks83Acj
8zyVxtuoSiTEcXBxyGbcFa4TN8ptuiEJEKnRnpi67xMrniaXfBTAE4BDgD3qpfRd
zh3Ya+/pqd/f6yWCok8rQeHennqbyrdSrjzlwrpwHj63aTp0CsbLjsv7JToo8AIo
GoyGgXaMj1991DJFE2kxmd3aSFVEPGH2bgLk21wadjSMBfVgWQy/d0hBpU0h77+e
iGkNWsmQXE0TrNKs96YSPktUtENzci5odCBnaXQgKEFkZHJlc3MgZm9yIGF1dG9t
YXRlZCBlbWFpbHMgZnJvbSBnaXQuc3IuaHQpIDxnaXRAc3IuaHQ+iQFOBBMBCAA4
FiEERHtp5LNL6QvIKaDpZZcE0aOKk64FAlwreMMCGwMFCwkIBwIGFQoJCAsCBBYC
AwECHgECF4AACgkQZZcE0aOKk67CfggAlkf1xyVLFBNC/UyaYq6iYotcVQ9oW82O
dLZ00NckXllixvmcd9s+l5xmKluGPOivY2/tM2ongxfe3orPrr4/JIQXAeZIcq7z
uz8OJDI/efPFC+x/BMb2eb0bpDGamcTMp6AiJGZd4fF2IcBGcWUpFSbaXzy8fE/d
iVEIk970HGSxB8NchA7TY1AKzGty5qy4lslfGc7eA9G8ZfoNQ96MGJFIPvY4jT4L
71tH+baqRN3/8By0mmxLCiW8rQDVGgcPicZENWWioCIYLlj7TixqTLLtK8nCLQ8a
y8uc/9Yk+whJDah+t8wBAuIjCysa/lZPcxoWekDyZ6pLGyI2T/QRSbRGc3IuaHQg
bWV0YSAoQWRkcmVzcyBmb3IgYXV0b21hdGVkIGVtYWlscyBmcm9tIG1ldGEuc3Iu
aHQpIDxtZXRhQHNyLmh0PokBTgQTAQgAOBYhBER7aeSzS+kLyCmg6WWXBNGjipOu
BQJcK3jVAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEGWXBNGjipOuS9AH
/RVimfI35uuI4zGu5v4T/FNO7uSvLiKtG64iYoQCBJSOw0wx/rU/wWfEZ5w8wPZB
TdcFVB/qEVCkE8vISmvK70fLiBtOKq8tmZvgFd9f7V9LWUDyoh4xOShFM+nPWpJW
aweKK/tfclXAU4NIfgtFEn+XuSqrtt3VmQWeYiR8+ToRb3lcO8CxLW/AFZrVzRZu
X9nQMyCr6mK6Xxg8c17CXSHJRSfFEvRzES7q/spSJT26xlaY8X1V82GFRpBPdeIV
EEd/6vbPqNJ6kyZioYB/PyEkTxMLh7sRM8l2xKYcqLgW7vXKW2L82vmFdzdPCQMg
xxWhdUhYWD9RIdoT/n8CEHW0UnNyLmh0IGRpc3BhdGNoIChBZGRyZXNzIGZvciBh
dXRvbWF0ZWQgZW1haWxzIGZyb20gZGlzcGF0Y2guc3IuaHQpIDxkaXNwYXRjaEBz
ci5odD6JAU4EEwEIADgWIQREe2nks0vpC8gpoOlllwTRo4qTrgUCXCt46gIbAwUL
CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBllwTRo4qTruoEB/9UJapNcs9gci37
XJCEHB/G0+9HL7lcGjbBSfMhA2BCuirfS3k347Ug+hLzld23wqFSP2wbr+muKHxL
1rQ9CIJ5lVwWOmZQLovl/8W/irQRh4SRnJtxWpCpXKoYhDH0bPoUbYjR9rezWWb0
rKw6KpWiEhYAOZWZ/vnv9uT70P7RP0+D5biI5snuS/O2sJLVqt0YDpOPcyF3nFg5
KHre7DzzBNoIZjrMOld8KbQ9YjOG14kokhXFQRbkrPhisx2uQdaCJN1fn4nbn8EU
DMXk28ZtutEAJjoGnaWksYi1VtYm/4Z8dWMhRByAx0fNKbl3FhzEUuL8ef2nxfcA
2KMtUu2IuQENBFgK0NQBCADKUjxaYJ7vssplWvNUnH5hkdPlpqKdQ5lnZHMk9kaq
RxEieDvle8l7H0hC1JH621EJhQ+l2F5njBUZVmFCNBoK1ryAvc8Y2E2+crYxMJ+r
75etNLxnAvepkCYf8psl+Qja34brfYmaTokWflFEgZF4PbjIhnbBRHnLKCWHzv4l
ttysU7JoFPe1OaX4ZTDu09I4PiSJb8ekFQCCcdUW9f520IrF1bwRIP7QhXWTYkR5
kVJaGZ3PW+Q2AeyFw9HWDEIAYse2LGg6vpPevnow10ZvuTvbd0+qWzMnrfcqW2xV
6T9gZvS8m8XVIfveX+DyL4SdA5sDuJ2w3s1yLyWW094DABEBAAGJAR8EGAEIAAkF
AlgK0NQCGwwACgkQZZcE0aOKk64MqwgAs6O9uhyxaaf80QTKlBMaYYjcai9kMlrS
RGeoEUePq6Wq4VypVM9tgShXZ67UR5eJtjQJzFFijCCj4JdsBGU/vesfsWpYObeW
/Z/WAGJ6byK44F5gL6KJYf8/8p23CzgZvc6hdGBz8cfdSxsZQZjMI5XLjyeD0uQ/
Q3IzmQd3m5r8SORtQiQ1L4sgk7PEOPZDN+e+XOYy6OlN5PfPFGGBkYMC171vvuEI
h06M+nUqlxcDbLvsewjmH2yjFuyMBJ9zu1+1BouKDkFK7kzXmFFqWCSdRTVh59G0
nOlQSC/TRrL8fOmfK9mXdZcFkJXU3aadQ9g4nSxBBOanSNJvnW0TJw==
=zmAw
-----END PGP PUBLIC KEY BLOCK-----
diff --git a/api/server.go b/api/server.go
index 3a38b97..7684f5e 100644
--- a/api/server.go
+++ b/api/server.go
@@ -1,6 +1,8 @@
package main

import (
	"log"

	"git.sr.ht/~sircmpwn/core-go/config"
	"git.sr.ht/~sircmpwn/core-go/server"
	work "git.sr.ht/~sircmpwn/dowork"
@@ -8,6 +10,7 @@ import (
	"git.sr.ht/~sircmpwn/hub.sr.ht/api/account"
	"git.sr.ht/~sircmpwn/hub.sr.ht/api/graph"
	"git.sr.ht/~sircmpwn/hub.sr.ht/api/graph/api"
	"git.sr.ht/~sircmpwn/hub.sr.ht/api/openpgpkey"
)

func main() {
@@ -30,5 +33,10 @@ func main() {
		WithSchema(schema, scopes).
		WithQueues(accountQueue)

	err := openpgpkey.MountWebKeyDirectoryRoutes(appConfig, gsrv.Router())
	if err != nil {
		log.Fatalf("openpgpkey.MountWebKeyDirectoryRoutes failed: %v", err)
	}

	gsrv.Run()
}
diff --git a/go.mod b/go.mod
index 0010975..d2afaae 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,10 @@ require (
	git.sr.ht/~sircmpwn/core-go v0.0.0-20221025082458-3e69641ef307
	git.sr.ht/~sircmpwn/dowork v0.0.0-20210820133136-d3970e97def3
	github.com/99designs/gqlgen v0.17.20
	github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3
	github.com/go-chi/chi v4.1.2+incompatible
	github.com/stretchr/testify v1.7.1
	github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec
	github.com/vektah/gqlparser/v2 v2.5.1
)

@@ -13,11 +17,11 @@ require (
	git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3 // indirect
	git.sr.ht/~sircmpwn/go-bare v0.0.0-20210227202403-5dae5c48f917 // indirect
	github.com/Masterminds/squirrel v1.4.0 // indirect
	github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
	github.com/agnivade/levenshtein v1.1.1 // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cespare/xxhash/v2 v2.1.2 // indirect
	github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
	github.com/emersion/go-message v0.15.0 // indirect
	github.com/emersion/go-pgpmail v0.2.0 // indirect
@@ -25,7 +29,6 @@ require (
	github.com/emersion/go-smtp v0.15.1-0.20211103212524-30169acc42e7 // indirect
	github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
	github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001 // indirect
	github.com/go-chi/chi v4.1.2+incompatible // indirect
	github.com/go-redis/redis/v8 v8.11.4 // indirect
	github.com/golang/protobuf v1.5.2 // indirect
	github.com/gorilla/websocket v1.5.0 // indirect
@@ -36,13 +39,13 @@ require (
	github.com/lib/pq v1.8.0 // indirect
	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
	github.com/mitchellh/mapstructure v1.3.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/prometheus/client_golang v1.7.1 // indirect
	github.com/prometheus/client_model v0.2.0 // indirect
	github.com/prometheus/common v0.10.0 // indirect
	github.com/prometheus/procfs v0.7.3 // indirect
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
	github.com/urfave/cli/v2 v2.8.1 // indirect
	github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec // indirect
	github.com/vektah/gqlparser v1.3.1 // indirect
	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
	golang.org/x/crypto v0.0.0-20211202192323-5770296d904e // indirect
-- 
2.42.0
hub.sr.ht/patches: FAILED in 3m39s

[well-known: support openpgp web key discovery][0] from [oliverpool][1]

[0]: https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/44509
[1]: mailto:git@olivier.pfad.fr

✗ #1054601 FAILED  hub.sr.ht/patches/archlinux.yml https://builds.sr.ht/~sircmpwn/job/1054601
✓ #1054600 SUCCESS hub.sr.ht/patches/alpine.yml    https://builds.sr.ht/~sircmpwn/job/1054600
✓ #1054602 SUCCESS hub.sr.ht/patches/debian.yml    https://builds.sr.ht/~sircmpwn/job/1054602