From 6095ead99248963ae70091c5bb3399eed49c0826 Mon Sep 17 00:00:00 2001
From: Rutger Broekhoff
Date: Wed, 24 Jan 2024 18:58:58 +0100
Subject: Remove Go and C source

The Rust implementation now implements all features I need
---
 cmd/git-lfs-authenticate/main.c      | 253 ----------
 cmd/git-lfs-authenticate/main.go     | 141 ------
 cmd/git-lfs-server/main.go           | 897 -----------------------------------
 cmd/gitolfs3-gen-ed25519-key/main.go |  31 --
 4 files changed, 1322 deletions(-)
 delete mode 100644 cmd/git-lfs-authenticate/main.c
 delete mode 100644 cmd/git-lfs-authenticate/main.go
 delete mode 100644 cmd/git-lfs-server/main.go
 delete mode 100644 cmd/gitolfs3-gen-ed25519-key/main.go

(limited to 'cmd')

diff --git a/cmd/git-lfs-authenticate/main.c b/cmd/git-lfs-authenticate/main.c
deleted file mode 100644
index a7ec031..0000000
--- a/cmd/git-lfs-authenticate/main.c
+++ /dev/null
@@ -1,253 +0,0 @@
-#include <assert.h>
-#include <ctype.h>
-#include <errno.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <string.h>
-
-#include <sys/stat.h>
-
-#include <openssl/evp.h>
-#include <openssl/hmac.h>
-
-void die(const char *format, ...) {
-	fputs("Fatal: ", stderr);
-	va_list ap;
-	va_start(ap, format);
-	vfprintf(stderr, format, ap);
-	va_end(ap);
-	fputc('\n', stderr);
-	exit(EXIT_FAILURE);
-}
-
-#define USAGE "Usage: git-lfs-authenticate <REPO> upload/download"
-
-bool hasprefix(const char *str, const char *prefix) {
-	if (strlen(prefix) > strlen(str))
-		return false;
-	return !strncmp(str, prefix, strlen(prefix));
-}
-
-bool hassuffix(const char *str, const char *suffix) {
-	if (strlen(suffix) > strlen(str))
-		return false;
-	str += strlen(str) - strlen(suffix);
-	return !strcmp(str, suffix);
-}
-
-const char *trimspace(const char *str, size_t *length) {
-	while (*length > 0 && isspace(str[0])) {
-		str++; (*length)--;
-	}
-	while (*length > 0 && isspace(str[*length - 1]))
-		(*length)--;
-	return str;
-}
-
-void printescjson(const char *str) {
-	for (size_t i = 0; i < strlen(str); i++) {
-		switch (str[i]) {
-		case '"':  fputs("\\\"", stdout); break;
-		case '\\': fputs("\\\\", stdout); break;
-		case '\b': fputs("\\b",  stdout); break;
-		case '\f': fputs("\\f",  stdout); break;
-		case '\n': fputs("\\n",  stdout); break;
-		case '\r': fputs("\\r",  stdout); break;
-		case '\t': fputs("\\t",  stdout); break;
-		default:   fputc(str[i], stdout);
-		}
-	}
-}
-
-void checkrepopath(const char *path) {
-	if (strstr(path, "//") || strstr(path, "/./") || strstr(path, "/../")
-	 || hasprefix(path, "./") || hasprefix(path, "../") || hasprefix(path, "/../"))
-		die("Bad repository name: Is unresolved path");
-	if (strlen(path) > 100)
-		die("Bad repository name: Longer than 100 characters");
-	if (hasprefix(path, "/"))
-		die("Bad repository name: Unexpected absolute path");
-	if (!hassuffix(path, ".git"))
-		die("Bad repository name: Expected '.git' repo path suffix");
-
-	struct stat statbuf;
-	if (stat(path, &statbuf)) {
-		if (errno == ENOENT)
-			die("Repo not found");
-		die("Could not stat repo: %s", strerror(errno));
-	}
-	if (!S_ISDIR(statbuf.st_mode)) {
-		die("Repo not found");
-	}
-}
-
-char *readkeyfile(const char *path, size_t *len) {
-	FILE *f = fopen(path, "r");
-	if (!f)
-		die("Cannot read key file: %s", strerror(errno));
-	*len = 0;
-	size_t bufcap = 4096;
-	char *buf = malloc(bufcap);
-	while (!feof(f) && !ferror(f)) {
-		if (*len + 4096 > bufcap) {
-			bufcap *= 2;
-			buf = realloc(buf, bufcap);
-		}
-		*len += fread(buf + *len, sizeof(char), 4096, f);
-	}
-	if (ferror(f) && !feof(f)) {
-		OPENSSL_cleanse(buf, *len);
-		die("Failed to read key file (length: %lu)", *len);
-	}
-	fclose(f);
-	return buf;
-}
-
-#define KEYSIZE 64
-
-void readkey(const char *path, uint8_t dest[KEYSIZE]) {
-	size_t keybuf_len = 0;
-	char *keybuf = readkeyfile(path, &keybuf_len);
-
-	size_t keystr_len = keybuf_len;
-	const char *keystr = trimspace(keybuf, &keystr_len);
-	if (keystr_len != 2 * KEYSIZE) {
-		OPENSSL_cleanse(keybuf, keybuf_len);
-		die("Bad key length");
-	}
-
-	for (size_t i = 0; i < KEYSIZE; i++) {
-		const char c = keystr[i];
-		uint8_t nibble = 0;
-		if (c >= '0' && c <= '9')
-			nibble = c - '0';
-		else if (c >= 'a' && c <= 'f')
-			nibble = c - 'a' + 10;
-		else if (c >= 'A' && c <= 'F')
-			nibble = c - 'A' + 10;
-		else {
-			OPENSSL_cleanse(keybuf, keybuf_len);
-			OPENSSL_cleanse(dest, KEYSIZE);
-			die("Cannot decode key");
-		}
-		size_t ikey = i / 2;
-		if (i % 2) dest[ikey] |= nibble;
-		else dest[ikey] = nibble << 4;
-	}
-
-	OPENSSL_cleanse(keybuf, keybuf_len);
-	free(keybuf);
-}
-
-void u64tobe(uint64_t x, uint8_t b[8]) {
-	b[0] = (uint8_t)(x >> 56);
-	b[1] = (uint8_t)(x >> 48);
-	b[2] = (uint8_t)(x >> 40);
-	b[3] = (uint8_t)(x >> 32);
-	b[4] = (uint8_t)(x >> 24);
-	b[5] = (uint8_t)(x >> 16);
-	b[6] = (uint8_t)(x >>  8);
-	b[7] = (uint8_t)(x >>  0);
-}
-
-void *memcat(void *dest, const void *src, size_t n) {
-	return memcpy(dest, src, n) + n;
-}
-
-#define MAX_TAG_SIZE EVP_MAX_MD_SIZE
-
-typedef struct taginfo {
-	const char    *authtype;
-	const char    *repopath;
-	const char    *operation;
-	const int64_t  expiresat_s;
-} taginfo_t;
-
-void maketag(const taginfo_t info, uint8_t key[KEYSIZE], uint8_t dest[MAX_TAG_SIZE], uint32_t *len) {
-	uint8_t expiresat_b[8];
-	u64tobe(info.expiresat_s, expiresat_b);
-
-	const uint8_t zero[1] = { 0 };
-	const size_t fullsize = strlen(info.authtype) +
-		1 + strlen(info.repopath) +
-		1 + strlen(info.operation) +
-		1 + sizeof(expiresat_b);
-	uint8_t *claimbuf = alloca(fullsize);
-	uint8_t *head = claimbuf;
-	head = memcat(head, info.authtype, strlen(info.authtype));
-	head = memcat(head, zero, 1);
-	head = memcat(head, info.repopath, strlen(info.repopath));
-	head = memcat(head, zero, 1);
-	head = memcat(head, info.operation, strlen(info.operation));
-	head = memcat(head, zero, 1);
-	head = memcat(head, expiresat_b, sizeof(expiresat_b));
-	assert(head == claimbuf + fullsize);
-
-	memset(dest, 0, MAX_TAG_SIZE);
-	*len = 0;
-	if (!HMAC(EVP_sha256(), key, KEYSIZE, claimbuf, fullsize, dest, len)) {
-		OPENSSL_cleanse(key, KEYSIZE);
-		die("Failed to generate tag");
-	}
-}
-
-#define MAX_HEXTAG_STRLEN MAX_TAG_SIZE * 2
-
-void makehextag(const taginfo_t info, uint8_t key[KEYSIZE], char dest[MAX_HEXTAG_STRLEN + 1]) {
-	uint8_t rawtag[MAX_TAG_SIZE];
-	uint32_t rawtag_len;
-	maketag(info, key, rawtag, &rawtag_len);
-
-	memset(dest, 0, MAX_HEXTAG_STRLEN + 1);
-	for (size_t i = 0; i < rawtag_len; i++) {
-		uint8_t b = rawtag[i];
-		dest[i * 2] = (b >> 4) + ((b >> 4) < 10 ? '0' : 'a');
-		dest[i*2 + 1] = (b & 0x0F) + ((b & 0x0F) < 10 ? '0' : 'a');
-	}
-}
-
-int main(int argc, char *argv[]) {
-	if (argc != 3) {
-		puts(USAGE);
-		exit(EXIT_FAILURE);
-	}
-
-	const char *repopath  = argv[1];
-	const char *operation = argv[2];
-	if (strcmp(operation, "download") && strcmp(operation, "upload")) {
-		puts(USAGE);
-		exit(EXIT_FAILURE);
-	}
-	checkrepopath(repopath);
-
-	const char *hrefbase = getenv("GITOLFS3_HREF_BASE");
-	const char *keypath  = getenv("GITOLFS3_KEY_PATH");
-	
-	if (!hrefbase || strlen(hrefbase) == 0)
-		die("Incomplete configuration: Base URL not provided");
-	if (hrefbase[strlen(hrefbase) - 1] != '/')
-		die("Bad configuration: Base URL should end with slash");
-	if (!keypath || strlen(keypath) == 0)
-		die("Incomplete configuration: Key path not provided");
-
-	uint8_t key[64];
-	readkey(keypath, key);
-
-	int64_t expiresin_s = 5 * 60;
-	int64_t expiresat_s = (int64_t)time(NULL) + expiresin_s;
-
-	taginfo_t taginfo = {
-		.authtype    = "git-lfs-authenticate",
-		.repopath    = repopath,
-		.operation   = operation,
-		.expiresat_s = expiresat_s,
-	};
-	char hextag[MAX_HEXTAG_STRLEN + 1];
-	makehextag(taginfo, key, hextag);
-
-	printf("{\"header\":{\"Authorization\":\"Gitolfs3-Hmac-Sha256 %s\"},"
-	       "\"expires_in\":%ld,\"href\":\"", hextag, expiresin_s);
-	printescjson(hrefbase);
-	printescjson(repopath);
-	printf("/info/lfs?p=1&te=%ld\"}\n", expiresat_s);
-}
diff --git a/cmd/git-lfs-authenticate/main.go b/cmd/git-lfs-authenticate/main.go
deleted file mode 100644
index 59ed978..0000000
--- a/cmd/git-lfs-authenticate/main.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"crypto/hmac"
-	"crypto/sha256"
-	"encoding/binary"
-	"encoding/hex"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"io/fs"
-	"os"
-	"path"
-	"strings"
-	"time"
-)
-
-func die(msg string, args ...any) {
-	fmt.Fprint(os.Stderr, "Fatal: ")
-	fmt.Fprintf(os.Stderr, msg, args...)
-	fmt.Fprint(os.Stderr, "\n")
-	os.Exit(1)
-}
-
-type authenticateResponse struct {
-	// When providing href, the Git LFS client will use href as the base URL
-	// instead of building the base URL using the Service Discovery mechanism.
-	// It should end with /info/lfs. See
-	// https://github.com/git-lfs/git-lfs/blob/baf40ac99850a62fe98515175d52df5c513463ec/docs/api/server-discovery.md#ssh
-	HRef   string            `json:"href,omitempty"`
-	Header map[string]string `json:"header"`
-	// In seconds.
-	ExpiresIn int64 `json:"expires_in,omitempty"`
-	// The expires_at (RFC3339) property could also be used, but we leave it
-	// out since we don't use it. The Git LFS docs recommend using expires_in
-	// instead (???)
-}
-
-func wipe(b []byte) {
-	for i := range b {
-		b[i] = 0
-	}
-}
-
-const usage = "Usage: git-lfs-authenticate <REPO> upload/download"
-
-func main() {
-	// Even though not explicitly described in the Git LFS documentation, the
-	// git-lfs-authenticate command is expected to either exit succesfully with
-	// exit code 0 and to then print credentials in the prescribed JSON format
-	// to standard out. On errors, the command should exit with a non-zero exit
-	// code and print the error message in plain text to standard error. See
-	// https://github.com/git-lfs/git-lfs/blob/baf40ac99850a62fe98515175d52df5c513463ec/lfshttp/ssh.go#L76-L117
-
-	if len(os.Args) != 3 {
-		fmt.Println(usage)
-		os.Exit(1)
-	}
-
-	repo := strings.TrimPrefix(path.Clean(os.Args[1]), "/")
-	operation := os.Args[2]
-	if operation != "download" && operation != "upload" {
-		fmt.Println(usage)
-		os.Exit(1)
-	}
-	if repo == ".." || strings.HasPrefix(repo, "../") {
-		die("highly illegal repo name (Anzeige ist raus)")
-	}
-	if !strings.HasSuffix(repo, ".git") {
-		die("expected repo name to have '.git' suffix")
-	}
-
-	repoDir := path.Join(repo)
-	finfo, err := os.Stat(repoDir)
-	if err != nil {
-		if errors.Is(err, fs.ErrNotExist) {
-			die("repo not found")
-		}
-		die("could not stat repo: %s", err)
-	}
-	if !finfo.IsDir() {
-		die("repo not found")
-	}
-
-	hrefBase := os.Getenv("GITOLFS3_HREF_BASE")
-	if hrefBase == "" {
-		die("incomplete configuration: base URL not provided")
-	}
-	if !strings.HasSuffix(hrefBase, "/") {
-		hrefBase += "/"
-	}
-
-	keyPath := os.Getenv("GITOLFS3_KEY_PATH")
-	if keyPath == "" {
-		die("incomplete configuration: key path not provided")
-	}
-
-	keyStr, err := os.ReadFile(keyPath)
-	if err != nil {
-		wipe(keyStr)
-		die("cannot read key")
-	}
-	keyStr = bytes.TrimSpace(keyStr)
-	defer wipe(keyStr)
-	if hex.DecodedLen(len(keyStr)) != 64 {
-		die("bad key length")
-	}
-	key := make([]byte, 64)
-	defer wipe(key)
-	if _, err = hex.Decode(key, keyStr); err != nil {
-		die("cannot decode key")
-	}
-
-	expiresIn := time.Minute * 5
-	expiresAtUnix := time.Now().Add(expiresIn).Unix()
-
-	tag := hmac.New(sha256.New, key)
-	io.WriteString(tag, "git-lfs-authenticate")
-	tag.Write([]byte{0})
-	io.WriteString(tag, repo)
-	tag.Write([]byte{0})
-	io.WriteString(tag, operation)
-	tag.Write([]byte{0})
-	binary.Write(tag, binary.BigEndian, &expiresAtUnix)
-	tagStr := hex.EncodeToString(tag.Sum(nil))
-
-	response := authenticateResponse{
-		Header: map[string]string{
-			"Authorization": "Gitolfs3-Hmac-Sha256 " + tagStr,
-		},
-		ExpiresIn: int64(expiresIn.Seconds()),
-		HRef: fmt.Sprintf("%s%s?p=1&te=%d",
-			hrefBase,
-			path.Join(repo, "/info/lfs"),
-			expiresAtUnix,
-		),
-	}
-	json.NewEncoder(os.Stdout).Encode(response)
-}
diff --git a/cmd/git-lfs-server/main.go b/cmd/git-lfs-server/main.go
deleted file mode 100644
index eec7d00..0000000
--- a/cmd/git-lfs-server/main.go
+++ /dev/null
@@ -1,897 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"context"
-	"crypto/ed25519"
-	"crypto/sha256"
-	"encoding/base64"
-	"encoding/hex"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"hash"
-	"io"
-	"mime"
-	"net"
-	"net/http"
-	"net/url"
-	"os"
-	"os/exec"
-	"path"
-	"regexp"
-	"runtime/debug"
-	"slices"
-	"strconv"
-	"strings"
-	"time"
-	"unicode"
-
-	"github.com/golang-jwt/jwt/v5"
-	"github.com/minio/minio-go/v7"
-	"github.com/minio/minio-go/v7/pkg/credentials"
-	"github.com/rs/xid"
-)
-
-type operation string
-type transferAdapter string
-type hashAlgo string
-
-const (
-	operationDownload    operation       = "download"
-	operationUpload      operation       = "upload"
-	transferAdapterBasic transferAdapter = "basic"
-	hashAlgoSHA256       hashAlgo        = "sha256"
-)
-
-const lfsMIME = "application/vnd.git-lfs+json"
-
-type batchRef struct {
-	Name string `json:"name"`
-}
-
-type batchRequestObject struct {
-	OID  string `json:"oid"`
-	Size int64  `json:"size"`
-}
-
-type batchRequest struct {
-	Operation operation            `json:"operation"`
-	Transfers []transferAdapter    `json:"transfers,omitempty"`
-	Ref       *batchRef            `json:"ref,omitempty"`
-	Objects   []batchRequestObject `json:"objects"`
-	HashAlgo  hashAlgo             `json:"hash_algo,omitempty"`
-}
-
-type batchAction struct {
-	HRef   string            `json:"href"`
-	Header map[string]string `json:"header,omitempty"`
-	// In seconds.
-	ExpiresIn int64 `json:"expires_in,omitempty"`
-	// expires_at (RFC3339) could also be used, but we leave it out since we
-	// don't use it.
-}
-
-type batchError struct {
-	Code    int    `json:"code"`
-	Message string `json:"message"`
-}
-
-type batchResponseObject struct {
-	OID           string                    `json:"oid"`
-	Size          int64                     `json:"size"`
-	Authenticated *bool                     `json:"authenticated"`
-	Actions       map[operation]batchAction `json:"actions,omitempty"`
-	Error         *batchError               `json:"error,omitempty"`
-}
-
-type batchResponse struct {
-	Transfer transferAdapter       `json:"transfer,omitempty"`
-	Objects  []batchResponseObject `json:"objects"`
-	HashAlgo hashAlgo              `json:"hash_algo,omitempty"`
-}
-
-type handler struct {
-	mc                      *minio.Client
-	bucket                  string
-	anonUser                string
-	gitolitePath            string
-	privateKey              ed25519.PrivateKey
-	baseURL                 *url.URL
-	exportAllForwardedHosts []string
-}
-
-func isValidSHA256Hash(hash string) bool {
-	if len(hash) != 64 {
-		return false
-	}
-	for _, c := range hash {
-		if !unicode.Is(unicode.ASCII_Hex_Digit, c) {
-			return false
-		}
-	}
-	return true
-}
-
-type lfsError struct {
-	Message          string `json:"message"`
-	DocumentationURL string `json:"documentation_url,omitempty"`
-	RequestID        string `json:"request_id,omitempty"`
-}
-
-func makeRespError(ctx context.Context, w http.ResponseWriter, message string, code int) {
-	err := lfsError{Message: message}
-	if val := ctx.Value(requestIDKey); val != nil {
-		err.RequestID = val.(string)
-	}
-	w.Header().Set("Content-Type", lfsMIME+"; charset=utf-8")
-	w.WriteHeader(code)
-	json.NewEncoder(w).Encode(err)
-}
-
-func makeObjError(obj parsedBatchObject, message string, code int) batchResponseObject {
-	return batchResponseObject{
-		OID:  obj.fullHash,
-		Size: obj.size,
-		Error: &batchError{
-			Message: message,
-			Code:    code,
-		},
-	}
-}
-
-func sha256AsBase64(hash string) string {
-	raw, err := hex.DecodeString(hash)
-	if err != nil {
-		return ""
-	}
-	return base64.StdEncoding.EncodeToString(raw)
-}
-
-func (h *handler) handleDownloadObject(ctx context.Context, repo string, obj parsedBatchObject) batchResponseObject {
-	fullPath := path.Join(repo+".git", "lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash)
-
-	info, err := h.mc.StatObject(ctx, h.bucket, fullPath, minio.StatObjectOptions{Checksum: true})
-	if err != nil {
-		var resp minio.ErrorResponse
-		if errors.As(err, &resp) && resp.StatusCode == http.StatusNotFound {
-			return makeObjError(obj, "Object does not exist", http.StatusNotFound)
-		}
-		// TODO: consider not making this an object-specific, but rather a
-		// generic error such that the entire Batch API request fails.
-		reqlog(ctx, "Failed to query object information (full path: %s): %s", fullPath, err)
-		return makeObjError(obj, "Failed to query object information", http.StatusInternalServerError)
-	}
-	if info.ChecksumSHA256 != "" && strings.ToLower(info.ChecksumSHA256) != obj.fullHash {
-		return makeObjError(obj, "Object corrupted", http.StatusUnprocessableEntity)
-	}
-	if info.Size != obj.size {
-		return makeObjError(obj, "Incorrect size specified for object or object currupted", http.StatusUnprocessableEntity)
-	}
-
-	expiresIn := time.Minute * 10
-	claims := handleObjectCustomClaims{
-		Gitolfs3: handleObjectGitolfs3Claims{
-			Type:       "basic-transfer",
-			Operation:  operationDownload,
-			Repository: repo,
-			OID:        obj.fullHash,
-			Size:       obj.size,
-		},
-		RegisteredClaims: &jwt.RegisteredClaims{
-			IssuedAt:  jwt.NewNumericDate(time.Now()),
-			ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)),
-		},
-	}
-
-	token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
-	ss, err := token.SignedString(h.privateKey)
-	if err != nil {
-		// TODO: consider not making this an object-specific, but rather a
-		// generic error such that the entire Batch API request fails.
-		reqlog(ctx, "Fatal: failed to generate JWT: %s", err)
-		return makeObjError(obj, "Failed to generate token", http.StatusInternalServerError)
-	}
-	uploadPath := path.Join(repo+".git", "info/lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash)
-
-	authenticated := true
-	return batchResponseObject{
-		OID:           obj.fullHash,
-		Size:          obj.size,
-		Authenticated: &authenticated,
-		Actions: map[operation]batchAction{
-			operationDownload: {
-				Header: map[string]string{
-					"Authorization": "Bearer " + ss,
-				},
-				HRef:      h.baseURL.ResolveReference(&url.URL{Path: uploadPath}).String(),
-				ExpiresIn: int64(expiresIn.Seconds()),
-			},
-		},
-	}
-}
-
-type handleObjectGitolfs3Claims struct {
-	Type       string    `json:"type"`
-	Operation  operation `json:"operation"`
-	Repository string    `json:"repository"`
-	OID        string    `json:"oid"`
-	Size       int64     `json:"size"`
-}
-
-type handleObjectCustomClaims struct {
-	Gitolfs3 handleObjectGitolfs3Claims `json:"gitolfs3"`
-	*jwt.RegisteredClaims
-}
-
-// Return nil when the object already exists
-func (h *handler) handleUploadObject(ctx context.Context, repo string, obj parsedBatchObject) *batchResponseObject {
-	fullPath := path.Join(repo+".git", "lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash)
-	_, err := h.mc.StatObject(ctx, h.bucket, fullPath, minio.GetObjectOptions{})
-	if err == nil {
-		// The object exists
-		return nil
-	}
-
-	var resp minio.ErrorResponse
-	if !errors.As(err, &resp) || resp.StatusCode != http.StatusNotFound {
-		// TODO: consider not making this an object-specific, but rather a
-		// generic error such that the entire Batch API request fails.
-		reqlog(ctx, "Failed to generate action href (full path: %s): %s", fullPath, err)
-		objErr := makeObjError(obj, "Failed to generate action href", http.StatusInternalServerError)
-		return &objErr
-	}
-
-	expiresIn := time.Minute * 10
-	claims := handleObjectCustomClaims{
-		Gitolfs3: handleObjectGitolfs3Claims{
-			Type:       "basic-transfer",
-			Operation:  operationUpload,
-			Repository: repo,
-			OID:        obj.fullHash,
-			Size:       obj.size,
-		},
-		RegisteredClaims: &jwt.RegisteredClaims{
-			IssuedAt:  jwt.NewNumericDate(time.Now()),
-			ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)),
-		},
-	}
-
-	token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
-	ss, err := token.SignedString(h.privateKey)
-	if err != nil {
-		// TODO: consider not making this an object-specific, but rather a
-		// generic error such that the entire Batch API request fails.
-		reqlog(ctx, "Fatal: failed to generate JWT: %s", err)
-		objErr := makeObjError(obj, "Failed to generate token", http.StatusInternalServerError)
-		return &objErr
-	}
-
-	uploadPath := path.Join(repo+".git", "info/lfs/objects", obj.firstByte, obj.secondByte, obj.fullHash)
-	uploadHRef := h.baseURL.ResolveReference(&url.URL{Path: uploadPath}).String()
-	// The object does not exist.
-	authenticated := true
-	return &batchResponseObject{
-		OID:           obj.fullHash,
-		Size:          obj.size,
-		Authenticated: &authenticated,
-		Actions: map[operation]batchAction{
-			operationUpload: {
-				Header: map[string]string{
-					"Authorization": "Bearer " + ss,
-				},
-				HRef:      uploadHRef,
-				ExpiresIn: int64(expiresIn.Seconds()),
-			},
-		},
-	}
-}
-
-type validatingReader struct {
-	promisedSize   int64
-	promisedSha256 []byte
-
-	reader    io.Reader
-	bytesRead int64
-	current   hash.Hash
-	err       error
-}
-
-func newValidatingReader(promisedSize int64, promisedSha256 []byte, r io.Reader) *validatingReader {
-	return &validatingReader{
-		promisedSize:   promisedSize,
-		promisedSha256: promisedSha256,
-		reader:         r,
-		current:        sha256.New(),
-	}
-}
-
-var errTooBig = errors.New("validator: uploaded file bigger than indicated")
-var errTooSmall = errors.New("validator: uploaded file smaller than indicated")
-var errBadSum = errors.New("validator: bad checksum provided or file corrupted")
-
-func (i *validatingReader) Read(b []byte) (int, error) {
-	if i.err != nil {
-		return 0, i.err
-	}
-	n, err := i.reader.Read(b)
-	i.bytesRead += int64(n)
-	if i.bytesRead > i.promisedSize {
-		i.err = errTooBig
-		return 0, i.err
-	}
-	if err != nil && errors.Is(err, io.EOF) {
-		if i.bytesRead < i.promisedSize {
-			i.err = errTooSmall
-			return n, i.err
-		}
-	}
-	// According to the documentation, Hash.Write never returns an error
-	i.current.Write(b[:n])
-	if i.bytesRead == i.promisedSize {
-		if !bytes.Equal(i.promisedSha256, i.current.Sum(nil)) {
-			i.err = errBadSum
-			return 0, i.err
-		}
-	}
-	return n, err
-}
-
-func (h *handler) handlePutObject(w http.ResponseWriter, r *http.Request, repo, oid string) {
-	ctx := r.Context()
-
-	authz := r.Header.Get("Authorization")
-	if authz == "" {
-		makeRespError(ctx, w, "Missing Authorization header", http.StatusBadRequest)
-		return
-	}
-	if !strings.HasPrefix(authz, "Bearer ") {
-		makeRespError(ctx, w, "Invalid Authorization header", http.StatusBadRequest)
-		return
-	}
-	authz = strings.TrimPrefix(authz, "Bearer ")
-
-	var claims handleObjectCustomClaims
-	_, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) {
-		if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
-			return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"])
-		}
-		return h.privateKey.Public(), nil
-	})
-	if err != nil {
-		makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-		return
-	}
-	if claims.Gitolfs3.Type != "basic-transfer" {
-		makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-		return
-	}
-	if claims.Gitolfs3.Repository != repo {
-		makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-		return
-	}
-	if claims.Gitolfs3.OID != oid {
-		makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-		return
-	}
-	if claims.Gitolfs3.Operation != operationUpload {
-		makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-		return
-	}
-
-	// Check with claims
-	if lengthStr := r.Header.Get("Content-Length"); lengthStr != "" {
-		length, err := strconv.ParseInt(lengthStr, 10, 64)
-		if err != nil {
-			makeRespError(ctx, w, "Bad Content-Length format", http.StatusBadRequest)
-			return
-		}
-		if length != claims.Gitolfs3.Size {
-			makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-			return
-		}
-	}
-
-	sha256Raw, err := hex.DecodeString(oid)
-	if err != nil || len(sha256Raw) != sha256.Size {
-		makeRespError(ctx, w, "Invalid OID", http.StatusBadRequest)
-		return
-	}
-
-	reader := newValidatingReader(claims.Gitolfs3.Size, sha256Raw, r.Body)
-
-	fullPath := path.Join(repo+".git", "lfs/objects", oid[:2], oid[2:4], oid)
-	_, err = h.mc.PutObject(ctx, h.bucket, fullPath, reader, int64(claims.Gitolfs3.Size), minio.PutObjectOptions{
-		SendContentMd5: true,
-	})
-	if err != nil {
-		if errors.Is(err, errBadSum) {
-			makeRespError(ctx, w, "Bad checksum (OID does not match contents)", http.StatusBadRequest)
-		} else if errors.Is(err, errTooSmall) {
-			makeRespError(ctx, w, "Uploaded object smaller than expected", http.StatusBadRequest)
-		} else if errors.Is(err, errTooBig) {
-			makeRespError(ctx, w, "Uploaded object bigger than expected", http.StatusBadRequest)
-		} else {
-			reqlog(ctx, "Failed to upload object: %s", err)
-			makeRespError(ctx, w, "Failed to upload object", http.StatusInternalServerError)
-		}
-		return
-	}
-}
-
-func (h *handler) handleGetObject(w http.ResponseWriter, r *http.Request, repo, oid string) {
-	ctx := r.Context()
-
-	authz := r.Header.Get("Authorization")
-	if authz == "" {
-		makeRespError(ctx, w, "Missing Authorization header", http.StatusBadRequest)
-		return
-	}
-	if !strings.HasPrefix(authz, "Bearer ") {
-		makeRespError(ctx, w, "Invalid Authorization header", http.StatusBadRequest)
-		return
-	}
-	authz = strings.TrimPrefix(authz, "Bearer ")
-
-	var claims handleObjectCustomClaims
-	_, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) {
-		if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
-			return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"])
-		}
-		return h.privateKey.Public(), nil
-	})
-	if err != nil {
-		makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-		return
-	}
-	if claims.Gitolfs3.Type != "basic-transfer" {
-		makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-		return
-	}
-	if claims.Gitolfs3.Repository != repo {
-		makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-		return
-	}
-	if claims.Gitolfs3.OID != oid {
-		makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-		return
-	}
-	if claims.Gitolfs3.Operation != operationDownload {
-		makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-		return
-	}
-
-	sha256Raw, err := hex.DecodeString(oid)
-	if err != nil || len(sha256Raw) != sha256.Size {
-		makeRespError(ctx, w, "Invalid OID", http.StatusBadRequest)
-		return
-	}
-
-	fullPath := path.Join(repo+".git", "lfs/objects", oid[:2], oid[2:4], oid)
-	obj, err := h.mc.GetObject(ctx, h.bucket, fullPath, minio.GetObjectOptions{})
-
-	var resp minio.ErrorResponse
-	if errors.As(err, &resp) && resp.StatusCode != http.StatusNotFound {
-		makeRespError(ctx, w, "Not found", http.StatusNotFound)
-		return
-	} else if err != nil {
-		reqlog(ctx, "Failed to get object: %s", err)
-		makeRespError(ctx, w, "Failed to get object", http.StatusInternalServerError)
-		return
-	}
-
-	stat, err := obj.Stat()
-	if err != nil {
-		reqlog(ctx, "Failed to stat: %s", err)
-		makeRespError(ctx, w, "Internal server error", http.StatusInternalServerError)
-		return
-	}
-
-	if stat.Size != claims.Gitolfs3.Size {
-		reqlog(ctx, "Claims size does not match S3 object size")
-		makeRespError(ctx, w, "Internal server error", http.StatusInternalServerError)
-		return
-	}
-
-	w.Header().Set("Content-Length", strconv.FormatInt(claims.Gitolfs3.Size, 10))
-	w.WriteHeader(http.StatusOK)
-
-	vr := newValidatingReader(claims.Gitolfs3.Size, sha256Raw, obj)
-	_, err = io.Copy(w, vr)
-	if errors.Is(err, errBadSum) {
-		reqlog(ctx, "Bad object checksum")
-	}
-}
-
-type parsedBatchObject struct {
-	firstByte  string
-	secondByte string
-	fullHash   string
-	size       int64
-}
-
-func isLFSMediaType(t string) bool {
-	if mediaType, params, err := mime.ParseMediaType(t); err == nil {
-		if mediaType == lfsMIME {
-			if params["charset"] == "" || strings.ToLower(params["charset"]) == "utf-8" {
-				return true
-			}
-		}
-	}
-	return false
-}
-
-var reBatchAPI = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/batch$`)
-var reObjUpload = regexp.MustCompile(`^([a-zA-Z0-9-_/]+)\.git/info/lfs/objects/([0-9a-f]{2})/([0-9a-f]{2})/([0-9a-f]{64})$`)
-
-type requestID struct{}
-
-var requestIDKey requestID
-
-// TODO: make a shared package for this
-type lfsAuthGitolfs3Claims struct {
-	Type       string    `json:"type"`
-	Repository string    `json:"repository"`
-	Permission operation `json:"permission"`
-}
-
-type lfsAuthCustomClaims struct {
-	Gitolfs3 lfsAuthGitolfs3Claims `json:"gitolfs3"`
-	*jwt.RegisteredClaims
-}
-
-// Request to perform <operation> in <repository> [on reference <refspec>]
-type operationRequest struct {
-	operation  operation
-	repository string
-	refspec    *string
-}
-
-func (h *handler) getGitoliteAccess(repo, user, gitolitePerm string, refspec *string) (bool, error) {
-	// gitolite access -q: returns only exit code
-	gitoliteArgs := []string{"access", "-q", repo, user, gitolitePerm}
-	if refspec != nil {
-		gitoliteArgs = append(gitoliteArgs, *refspec)
-	}
-	cmd := exec.Command(h.gitolitePath, gitoliteArgs...)
-	err := cmd.Run()
-	if err != nil {
-		var exitErr *exec.ExitError
-		if !errors.As(err, &exitErr) {
-			return false, fmt.Errorf("(running %s): %w", cmd, err)
-		}
-		return false, nil
-	}
-	return true, nil
-}
-
-func (h *handler) authorizeBatchAPI(w http.ResponseWriter, r *http.Request, or operationRequest) bool {
-	user := h.anonUser
-	ctx := r.Context()
-
-	if or.operation == operationDownload {
-		// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
-		forwardedHost := r.Header.Get("X-Forwarded-Host")
-		if forwardedHost != "" && slices.Contains(h.exportAllForwardedHosts, forwardedHost) {
-			// This is a forwarded host for which all repositories are exported,
-			// regardless of ownership configuration in Gitolite.
-			return true
-		}
-	}
-
-	if authz := r.Header.Get("Authorization"); authz != "" {
-		if !strings.HasPrefix(authz, "Bearer ") {
-			makeRespError(ctx, w, "Invalid Authorization header", http.StatusBadRequest)
-			return false
-		}
-		authz = strings.TrimPrefix(authz, "Bearer ")
-
-		var claims lfsAuthCustomClaims
-		_, err := jwt.ParseWithClaims(authz, &claims, func(token *jwt.Token) (any, error) {
-			if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
-				return nil, fmt.Errorf("expected signing method EdDSA, got %s", token.Header["alg"])
-			}
-			return h.privateKey.Public(), nil
-		})
-		if err != nil {
-			makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-			return false
-		}
-
-		if claims.Gitolfs3.Type != "batch-api" {
-			makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-			return false
-		}
-		if claims.Gitolfs3.Repository != or.repository {
-			makeRespError(ctx, w, "Invalid token", http.StatusUnauthorized)
-			return false
-		}
-		if claims.Gitolfs3.Permission == operationDownload && or.operation == operationUpload {
-			makeRespError(ctx, w, "Forbidden", http.StatusForbidden)
-			return false
-		}
-
-		user = claims.Subject
-	}
-
-	readAccess, err := h.getGitoliteAccess(or.repository, user, "R", or.refspec)
-	if err != nil {
-		reqlog(ctx, "Error checking access info: %s", err)
-		makeRespError(ctx, w, "Failed to query access information", http.StatusInternalServerError)
-		return false
-	}
-	if !readAccess {
-		makeRespError(ctx, w, "Repository not found", http.StatusNotFound)
-		return false
-	}
-	if or.operation == operationUpload {
-		writeAccess, err := h.getGitoliteAccess(or.repository, user, "W", or.refspec)
-		if err != nil {
-			reqlog(ctx, "Error checking access info: %s", err)
-			makeRespError(ctx, w, "Failed to query access information", http.StatusInternalServerError)
-			return false
-		}
-		// User has read access but no write access
-		if !writeAccess {
-			makeRespError(ctx, w, "Forbidden", http.StatusForbidden)
-			return false
-		}
-	}
-
-	return true
-}
-
-func (h *handler) handleBatchAPI(w http.ResponseWriter, r *http.Request, repo string) {
-	ctx := r.Context()
-
-	if !slices.ContainsFunc(r.Header.Values("Accept"), isLFSMediaType) {
-		makeRespError(ctx, w, "Expected "+lfsMIME+" (with UTF-8 charset) in list of acceptable response media types", http.StatusNotAcceptable)
-		return
-	}
-	if !isLFSMediaType(r.Header.Get("Content-Type")) {
-		makeRespError(ctx, w, "Expected request Content-Type to be "+lfsMIME+" (with UTF-8 charset)", http.StatusUnsupportedMediaType)
-		return
-	}
-
-	var body batchRequest
-	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
-		makeRespError(ctx, w, "Failed to parse request body as JSON", http.StatusBadRequest)
-		return
-	}
-	if body.Operation != operationDownload && body.Operation != operationUpload {
-		makeRespError(ctx, w, "Invalid operation specified", http.StatusBadRequest)
-		return
-	}
-
-	or := operationRequest{
-		operation:  body.Operation,
-		repository: repo,
-	}
-	if body.Ref != nil {
-		or.refspec = &body.Ref.Name
-	}
-	if !h.authorizeBatchAPI(w, r.WithContext(ctx), or) {
-		return
-	}
-
-	if body.HashAlgo != hashAlgoSHA256 {
-		makeRespError(ctx, w, "Unsupported hash algorithm specified", http.StatusConflict)
-		return
-	}
-
-	if len(body.Transfers) != 0 && !slices.Contains(body.Transfers, transferAdapterBasic) {
-		makeRespError(ctx, w, "Unsupported transfer adapter specified (supported: basic)", http.StatusConflict)
-		return
-	}
-
-	var objects []parsedBatchObject
-	for _, obj := range body.Objects {
-		oid := strings.ToLower(obj.OID)
-		if !isValidSHA256Hash(oid) {
-			makeRespError(ctx, w, "Invalid hash format in object ID", http.StatusBadRequest)
-			return
-		}
-		objects = append(objects, parsedBatchObject{
-			firstByte:  oid[:2],
-			secondByte: oid[2:4],
-			fullHash:   oid,
-			size:       obj.Size,
-		})
-	}
-
-	resp := batchResponse{
-		Transfer: transferAdapterBasic,
-		HashAlgo: hashAlgoSHA256,
-	}
-	for _, obj := range objects {
-		switch body.Operation {
-		case operationDownload:
-			resp.Objects = append(resp.Objects, h.handleDownloadObject(ctx, repo, obj))
-		case operationUpload:
-			if respObj := h.handleUploadObject(ctx, repo, obj); respObj != nil {
-				resp.Objects = append(resp.Objects, *respObj)
-			}
-		}
-	}
-
-	w.Header().Set("Content-Type", lfsMIME)
-	w.WriteHeader(http.StatusOK)
-	json.NewEncoder(w).Encode(resp)
-}
-
-func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	reqID := xid.New().String()
-	ctx := context.WithValue(r.Context(), requestIDKey, reqID)
-	w.Header().Set("X-Request-Id", reqID)
-
-	defer func() {
-		if r := recover(); r != nil {
-			reqlog(ctx, "Panic when serving request: %s", debug.Stack())
-		}
-	}()
-
-	reqPath := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
-
-	if submatches := reBatchAPI.FindStringSubmatch(reqPath); len(submatches) == 2 {
-		if r.Method != http.MethodPost {
-			makeRespError(ctx, w, "Method not allowed", http.StatusMethodNotAllowed)
-			return
-		}
-
-		repo := strings.TrimPrefix(path.Clean(submatches[1]), "/")
-		reqlog(ctx, "Handling batch API request for repository: %s", repo)
-
-		h.handleBatchAPI(w, r.WithContext(ctx), repo)
-		return
-	}
-
-	if submatches := reObjUpload.FindStringSubmatch(reqPath); len(submatches) == 5 {
-		oid0, oid1, oid := submatches[2], submatches[3], submatches[4]
-
-		if !isValidSHA256Hash(oid) {
-			panic("Regex should only allow valid SHA256 hashes")
-		}
-		if oid0 != oid[:2] || oid1 != oid[2:4] {
-			makeRespError(ctx, w, "Bad URL format: malformed OID pattern", http.StatusBadRequest)
-			return
-		}
-
-		repo := strings.TrimPrefix(path.Clean(submatches[1]), "/")
-		reqlog(ctx, "Handling object PUT for repository: %s, OID: %s", repo, oid)
-
-		switch r.Method {
-		case http.MethodGet:
-			h.handleGetObject(w, r.WithContext(ctx), repo, oid)
-		case http.MethodPut:
-			h.handlePutObject(w, r.WithContext(ctx), repo, oid)
-		default:
-			makeRespError(ctx, w, "Method not allowed", http.StatusMethodNotAllowed)
-		}
-
-		return
-	}
-
-	makeRespError(ctx, w, "Not found", http.StatusNotFound)
-}
-
-func reqlog(ctx context.Context, msg string, args ...any) {
-	if val := ctx.Value(requestIDKey); val != nil {
-		fmt.Fprintf(os.Stderr, "[%s] ", val.(string))
-	}
-	fmt.Fprintf(os.Stderr, msg, args...)
-	fmt.Fprint(os.Stderr, "\n")
-}
-
-func log(msg string, args ...any) {
-	fmt.Fprintf(os.Stderr, msg, args...)
-	fmt.Fprint(os.Stderr, "\n")
-}
-
-func die(msg string, args ...any) {
-	log("Environment variables: (dying)")
-	for _, s := range os.Environ() {
-		log("  %s", s)
-	}
-	log(msg, args...)
-	os.Exit(1)
-}
-
-func loadPrivateKey(path string) ed25519.PrivateKey {
-	raw, err := os.ReadFile(path)
-	if err != nil {
-		die("Failed to open specified public key: %s", err)
-	}
-	raw = bytes.TrimSpace(raw)
-
-	if hex.DecodedLen(len(raw)) != ed25519.SeedSize {
-		die("Specified public key file does not contain key (seed) of appropriate length")
-	}
-	decoded := make([]byte, hex.DecodedLen(len(raw)))
-	if _, err = hex.Decode(decoded, raw); err != nil {
-		die("Failed to decode specified public key: %s", err)
-	}
-	return ed25519.NewKeyFromSeed(decoded)
-}
-
-func wipe(b []byte) {
-	for i := range b {
-		b[i] = 0
-	}
-}
-
-func main() {
-	anonUser := os.Getenv("GITOLFS3_ANON_USER")
-	privateKeyPath := os.Getenv("GITOLFS3_PRIVATE_KEY_PATH")
-	endpoint := os.Getenv("GITOLFS3_S3_ENDPOINT")
-	bucket := os.Getenv("GITOLFS3_S3_BUCKET")
-	accessKeyIDFile := os.Getenv("GITOLFS3_S3_ACCESS_KEY_ID_FILE")
-	secretAccessKeyFile := os.Getenv("GITOLFS3_S3_SECRET_ACCESS_KEY_FILE")
-	gitolitePath := os.Getenv("GITOLFS3_GITOLITE_PATH")
-	baseURLStr := os.Getenv("GITOLFS3_BASE_URL")
-	listenHost := os.Getenv("GITOLFS3_LISTEN_HOST")
-	listenPort := os.Getenv("GITOLFS3_LISTEN_PORT")
-	exportAllForwardedHostsStr := os.Getenv("GITOLFS3_EXPORT_ALL_FORWARDED_HOSTS")
-
-	listenAddr := net.JoinHostPort(listenHost, listenPort)
-	exportAllForwardedHosts := strings.Split(exportAllForwardedHostsStr, ",")
-
-	if gitolitePath == "" {
-		gitolitePath = "gitolite"
-	}
-
-	if anonUser == "" {
-		die("Fatal: expected environment variable GITOLFS3_ANON_USER to be set")
-	}
-	if privateKeyPath == "" {
-		die("Fatal: expected environment variable GITOLFS3_PRIVATE_KEY_PATH to be set")
-	}
-	if listenPort == "" {
-		die("Fatal: expected environment variable GITOLFS3_LISTEN_PORT to be set")
-	}
-	if baseURLStr == "" {
-		die("Fatal: expected environment variable GITOLFS3_BASE_URL to be set")
-	}
-	if endpoint == "" {
-		die("Fatal: expected environment variable GITOLFS3_S3_ENDPOINT to be set")
-	}
-	if bucket == "" {
-		die("Fatal: expected environment variable GITOLFS3_S3_BUCKET to be set")
-	}
-
-	if accessKeyIDFile == "" {
-		die("Fatal: expected environment variable GITOLFS3_S3_ACCESS_KEY_ID_FILE to be set")
-	}
-	if secretAccessKeyFile == "" {
-		die("Fatal: expected environment variable GITOLFS3_S3_SECRET_ACCESS_KEY_FILE to be set")
-	}
-
-	accessKeyID, err := os.ReadFile(accessKeyIDFile)
-	if err != nil {
-		die("Fatal: failed to read access key ID from specified file: %s", err)
-	}
-	secretAccessKey, err := os.ReadFile(secretAccessKeyFile)
-	if err != nil {
-		die("Fatal: failed to read secret access key from specified file: %s", err)
-	}
-
-	privateKey := loadPrivateKey(privateKeyPath)
-	defer wipe(privateKey)
-
-	baseURL, err := url.Parse(baseURLStr)
-	if err != nil {
-		die("Fatal: provided BASE_URL has bad format: %s", err)
-	}
-
-	mc, err := minio.New(endpoint, &minio.Options{
-		Creds:  credentials.NewStaticV4(string(accessKeyID), string(secretAccessKey), ""),
-		Secure: true,
-	})
-	if err != nil {
-		die("Fatal: failed to create S3 client: %s", err)
-	}
-
-	h := &handler{mc, bucket, anonUser, gitolitePath, privateKey, baseURL, exportAllForwardedHosts}
-	if err = http.ListenAndServe(listenAddr, h); err != nil {
-		die("Fatal: failed to serve CGI: %s", err)
-	}
-}
diff --git a/cmd/gitolfs3-gen-ed25519-key/main.go b/cmd/gitolfs3-gen-ed25519-key/main.go
deleted file mode 100644
index 8288fd1..0000000
--- a/cmd/gitolfs3-gen-ed25519-key/main.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package main
-
-import (
-	"crypto/ed25519"
-	"crypto/rand"
-	"encoding/hex"
-	"fmt"
-	"os"
-)
-
-func wipe(b []byte) {
-	for i := range b {
-		b[i] = 0
-	}
-}
-
-func main() {
-	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "Failed to generate ED25519 key: %s", err)
-		os.Exit(1)
-	}
-	defer wipe(privateKey)
-
-	enc := hex.NewEncoder(os.Stdout)
-	print("Public  ")
-	enc.Write(publicKey)
-	print("\nPrivate ")
-	enc.Write(privateKey.Seed())
-	println()
-}
-- 
cgit v1.2.3