~sircmpwn/sr.ht-dev

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
4 4

[PATCH meta.sr.ht v2] well-known: support openpgp webkey discovery

Details
Message ID
<20230503134639.84474-1-git@olivier.pfad.fr>
DKIM signature
pass
Download raw message
Patch: +256 -0
as documented on https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service#name-web-key-directory

---

I rewrote my initial patch in go so that it lives under "api".
To be publicly accessible adjustements must be made to sr.ht-nginx/meta.sr.ht.conf, like (untested):

	location /.well-known/openpgpkey {
		proxy_pass http://127.0.0.1:5100;
	}

It does not conflict anymore with https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/40325 (removal of pgpy dependency).

This is my first substantial contribution to sr.ht and I appreciate any feedback!
---
 api/openpgpkey/opengpgkey_test.go |  53 ++++++++++++++
 api/openpgpkey/openpgpkey.go      | 116 ++++++++++++++++++++++++++++++
 api/openpgpkey/testdata/pubkey    |  80 +++++++++++++++++++++
 api/server.go                     |   5 ++
 go.mod                            |   2 +
 5 files changed, 256 insertions(+)
 create mode 100644 api/openpgpkey/opengpgkey_test.go
 create mode 100644 api/openpgpkey/openpgpkey.go
 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..7a4cb19
--- /dev/null
+++ b/api/openpgpkey/opengpgkey_test.go
@@ -0,0 +1,53 @@
package openpgpkey

import (
	"net/http"
	"net/http/httptest"
	"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 := MountWellKnownRoutes(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"))
}

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..f702979
--- /dev/null
+++ b/api/openpgpkey/openpgpkey.go
@@ -0,0 +1,116 @@
package openpgpkey

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

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

// MountWellKnownRoutes 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 MountWellKnownRoutes(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 {
		var buf bytes.Buffer
		err = entity.PrimaryKey.Serialize(&buf)
		if err != nil {
			return fmt.Errorf("could not serialize primary key: %v", err)
		}
		pubKey := buf.Bytes()
		servePubkey := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/octet-stream")
			w.Write(pubKey)
		})
		for _, identidy := range entity.Identities {
			local, domain, found := stringsCut(identidy.UserId.Email, "@")
			if !found || domain != globalDomain {
				continue
			}
			router.Get("/.well-known/openpgpkey/hu/"+encodeLocalPart(local), servePubkey)
		}
	}
	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
}

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/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 5d7b482..1738498 100644
--- a/api/server.go
+++ b/api/server.go
@@ -22,6 +22,7 @@ import (
	"git.sr.ht/~sircmpwn/meta.sr.ht/api/graph/model"
	"git.sr.ht/~sircmpwn/meta.sr.ht/api/invoice"
	"git.sr.ht/~sircmpwn/meta.sr.ht/api/loaders"
	"git.sr.ht/~sircmpwn/meta.sr.ht/api/openpgpkey"
	"git.sr.ht/~sircmpwn/meta.sr.ht/api/webhooks"
)

@@ -134,5 +135,9 @@ func main() {
			panic(err)
		}
	})
	err := openpgpkey.MountWellKnownRoutes(appConfig, srv.Router())
	if err != nil {
		panic(fmt.Errorf("openpgpkey.MountWellKnownRoutes failed: %v", err))
	}
	srv.Run()
}
diff --git a/go.mod b/go.mod
index 7751d72..4b3452a 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,9 @@ require (
	github.com/mitchellh/mapstructure v1.3.2 // indirect
	github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
	github.com/stretchr/testify v1.7.1
	github.com/urfave/cli/v2 v2.20.2 // indirect
	github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec
	github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e
	github.com/vektah/gqlparser v1.3.1
	github.com/vektah/gqlparser/v2 v2.5.1
-- 
2.40.1

[meta.sr.ht/patches] build failed

builds.sr.ht <builds@sr.ht>
Details
Message ID
<CSCOWDGXW0RB.1AA32URC75ZKY@cirno2>
In-Reply-To
<20230503134639.84474-1-git@olivier.pfad.fr> (view parent)
DKIM signature
missing
Download raw message
meta.sr.ht/patches: FAILED in 3m53s

[well-known: support openpgp webkey discovery][0] v2 from [oliverpool][1]

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

✗ #983809 FAILED  meta.sr.ht/patches/archlinux.yml https://builds.sr.ht/~sircmpwn/job/983809
✓ #983808 SUCCESS meta.sr.ht/patches/alpine.yml    https://builds.sr.ht/~sircmpwn/job/983808
✓ #983810 SUCCESS meta.sr.ht/patches/debian.yml    https://builds.sr.ht/~sircmpwn/job/983810
Details
Message ID
<CT18V3ZK9ZW7.2VDLU3CMFNATN@taiga>
In-Reply-To
<20230503134639.84474-1-git@olivier.pfad.fr> (view parent)
DKIM signature
pass
Download raw message
Honestly not sure about this patch, it's unclear if it belongs in the
frontend or the backend. Might be more clear post GQL finalization and
the frontend overhauls. Will sit on it for a bit and think it over.
Details
Message ID
<6a08c719-321e-4125-9855-844161344ead@bitfehler.net>
In-Reply-To
<CT18V3ZK9ZW7.2VDLU3CMFNATN@taiga> (view parent)
DKIM signature
pass
Download raw message
On 6/1/23 12:33, Drew DeVault wrote:
> Honestly not sure about this patch, it's unclear if it belongs in the
> frontend or the backend. Might be more clear post GQL finalization and
> the frontend overhauls. Will sit on it for a bit and think it over.

I'd say backend is fine, but not sure if it should be hub instead of 
meta, as the key would be requested from sr.ht/.well-known/... unless we 
want to set up a dedicated openpgpkey.sr.ht DNS record, which could then 
point to either one.

But the real reason I am responding is that there is problem in the 
current implementation. You are only writing the primary key in the HTTP 
response, which is not enough, e.g. compare

curl http://localhost:5100/.well-known/openpgpkey/hu/...?l=srht -Ss | 
gpg --list-packets
# off=0 ctb=c6 tag=6 hlen=2 plen=51 new-ctb
:public key packet:
	version 4, algo 22, created 1643793955, expires 0
	pkey[0]: [80 bits] ed25519 (1.3.6.1.4.1.11591.15.1)
	pkey[1]: [263 bits]
	keyid: 4268671F07D1A61E

to the output of `cat <PUBKEY>.asc | gpg --list-packets`, which will 
show a lot more packets. Specifically, the RFC explicitly says:

 > The HTTP GET method MUST return the binary representation of the
 > OpenPGP key for the given mail address. The key needs to carry a User
 > ID packet ([RFC4880]) with that mail address.

This would have to be fixed before merging, but I'll leave it to Drew if 
and where we really want this.

Cheers,
Conrad
Details
Message ID
<d796df3d-a387-434e-a8cb-6c9d13842860@app.fastmail.com>
In-Reply-To
<6a08c719-321e-4125-9855-844161344ead@bitfehler.net> (view parent)
DKIM signature
pass
Download raw message
Superseded by https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/44509 (ported to hub.sr.ht - fixed packet number)
Reply to thread Export thread (mbox)