aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/git-lfs-authenticate
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/git-lfs-authenticate')
-rw-r--r--cmd/git-lfs-authenticate/main.c253
-rw-r--r--cmd/git-lfs-authenticate/main.go141
2 files changed, 0 insertions, 394 deletions
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 @@
1#include <assert.h>
2#include <ctype.h>
3#include <errno.h>
4#include <stdbool.h>
5#include <stdio.h>
6#include <string.h>
7
8#include <sys/stat.h>
9
10#include <openssl/evp.h>
11#include <openssl/hmac.h>
12
13void die(const char *format, ...) {
14 fputs("Fatal: ", stderr);
15 va_list ap;
16 va_start(ap, format);
17 vfprintf(stderr, format, ap);
18 va_end(ap);
19 fputc('\n', stderr);
20 exit(EXIT_FAILURE);
21}
22
23#define USAGE "Usage: git-lfs-authenticate <REPO> upload/download"
24
25bool hasprefix(const char *str, const char *prefix) {
26 if (strlen(prefix) > strlen(str))
27 return false;
28 return !strncmp(str, prefix, strlen(prefix));
29}
30
31bool hassuffix(const char *str, const char *suffix) {
32 if (strlen(suffix) > strlen(str))
33 return false;
34 str += strlen(str) - strlen(suffix);
35 return !strcmp(str, suffix);
36}
37
38const char *trimspace(const char *str, size_t *length) {
39 while (*length > 0 && isspace(str[0])) {
40 str++; (*length)--;
41 }
42 while (*length > 0 && isspace(str[*length - 1]))
43 (*length)--;
44 return str;
45}
46
47void printescjson(const char *str) {
48 for (size_t i = 0; i < strlen(str); i++) {
49 switch (str[i]) {
50 case '"': fputs("\\\"", stdout); break;
51 case '\\': fputs("\\\\", stdout); break;
52 case '\b': fputs("\\b", stdout); break;
53 case '\f': fputs("\\f", stdout); break;
54 case '\n': fputs("\\n", stdout); break;
55 case '\r': fputs("\\r", stdout); break;
56 case '\t': fputs("\\t", stdout); break;
57 default: fputc(str[i], stdout);
58 }
59 }
60}
61
62void checkrepopath(const char *path) {
63 if (strstr(path, "//") || strstr(path, "/./") || strstr(path, "/../")
64 || hasprefix(path, "./") || hasprefix(path, "../") || hasprefix(path, "/../"))
65 die("Bad repository name: Is unresolved path");
66 if (strlen(path) > 100)
67 die("Bad repository name: Longer than 100 characters");
68 if (hasprefix(path, "/"))
69 die("Bad repository name: Unexpected absolute path");
70 if (!hassuffix(path, ".git"))
71 die("Bad repository name: Expected '.git' repo path suffix");
72
73 struct stat statbuf;
74 if (stat(path, &statbuf)) {
75 if (errno == ENOENT)
76 die("Repo not found");
77 die("Could not stat repo: %s", strerror(errno));
78 }
79 if (!S_ISDIR(statbuf.st_mode)) {
80 die("Repo not found");
81 }
82}
83
84char *readkeyfile(const char *path, size_t *len) {
85 FILE *f = fopen(path, "r");
86 if (!f)
87 die("Cannot read key file: %s", strerror(errno));
88 *len = 0;
89 size_t bufcap = 4096;
90 char *buf = malloc(bufcap);
91 while (!feof(f) && !ferror(f)) {
92 if (*len + 4096 > bufcap) {
93 bufcap *= 2;
94 buf = realloc(buf, bufcap);
95 }
96 *len += fread(buf + *len, sizeof(char), 4096, f);
97 }
98 if (ferror(f) && !feof(f)) {
99 OPENSSL_cleanse(buf, *len);
100 die("Failed to read key file (length: %lu)", *len);
101 }
102 fclose(f);
103 return buf;
104}
105
106#define KEYSIZE 64
107
108void readkey(const char *path, uint8_t dest[KEYSIZE]) {
109 size_t keybuf_len = 0;
110 char *keybuf = readkeyfile(path, &keybuf_len);
111
112 size_t keystr_len = keybuf_len;
113 const char *keystr = trimspace(keybuf, &keystr_len);
114 if (keystr_len != 2 * KEYSIZE) {
115 OPENSSL_cleanse(keybuf, keybuf_len);
116 die("Bad key length");
117 }
118
119 for (size_t i = 0; i < KEYSIZE; i++) {
120 const char c = keystr[i];
121 uint8_t nibble = 0;
122 if (c >= '0' && c <= '9')
123 nibble = c - '0';
124 else if (c >= 'a' && c <= 'f')
125 nibble = c - 'a' + 10;
126 else if (c >= 'A' && c <= 'F')
127 nibble = c - 'A' + 10;
128 else {
129 OPENSSL_cleanse(keybuf, keybuf_len);
130 OPENSSL_cleanse(dest, KEYSIZE);
131 die("Cannot decode key");
132 }
133 size_t ikey = i / 2;
134 if (i % 2) dest[ikey] |= nibble;
135 else dest[ikey] = nibble << 4;
136 }
137
138 OPENSSL_cleanse(keybuf, keybuf_len);
139 free(keybuf);
140}
141
142void u64tobe(uint64_t x, uint8_t b[8]) {
143 b[0] = (uint8_t)(x >> 56);
144 b[1] = (uint8_t)(x >> 48);
145 b[2] = (uint8_t)(x >> 40);
146 b[3] = (uint8_t)(x >> 32);
147 b[4] = (uint8_t)(x >> 24);
148 b[5] = (uint8_t)(x >> 16);
149 b[6] = (uint8_t)(x >> 8);
150 b[7] = (uint8_t)(x >> 0);
151}
152
153void *memcat(void *dest, const void *src, size_t n) {
154 return memcpy(dest, src, n) + n;
155}
156
157#define MAX_TAG_SIZE EVP_MAX_MD_SIZE
158
159typedef struct taginfo {
160 const char *authtype;
161 const char *repopath;
162 const char *operation;
163 const int64_t expiresat_s;
164} taginfo_t;
165
166void maketag(const taginfo_t info, uint8_t key[KEYSIZE], uint8_t dest[MAX_TAG_SIZE], uint32_t *len) {
167 uint8_t expiresat_b[8];
168 u64tobe(info.expiresat_s, expiresat_b);
169
170 const uint8_t zero[1] = { 0 };
171 const size_t fullsize = strlen(info.authtype) +
172 1 + strlen(info.repopath) +
173 1 + strlen(info.operation) +
174 1 + sizeof(expiresat_b);
175 uint8_t *claimbuf = alloca(fullsize);
176 uint8_t *head = claimbuf;
177 head = memcat(head, info.authtype, strlen(info.authtype));
178 head = memcat(head, zero, 1);
179 head = memcat(head, info.repopath, strlen(info.repopath));
180 head = memcat(head, zero, 1);
181 head = memcat(head, info.operation, strlen(info.operation));
182 head = memcat(head, zero, 1);
183 head = memcat(head, expiresat_b, sizeof(expiresat_b));
184 assert(head == claimbuf + fullsize);
185
186 memset(dest, 0, MAX_TAG_SIZE);
187 *len = 0;
188 if (!HMAC(EVP_sha256(), key, KEYSIZE, claimbuf, fullsize, dest, len)) {
189 OPENSSL_cleanse(key, KEYSIZE);
190 die("Failed to generate tag");
191 }
192}
193
194#define MAX_HEXTAG_STRLEN MAX_TAG_SIZE * 2
195
196void makehextag(const taginfo_t info, uint8_t key[KEYSIZE], char dest[MAX_HEXTAG_STRLEN + 1]) {
197 uint8_t rawtag[MAX_TAG_SIZE];
198 uint32_t rawtag_len;
199 maketag(info, key, rawtag, &rawtag_len);
200
201 memset(dest, 0, MAX_HEXTAG_STRLEN + 1);
202 for (size_t i = 0; i < rawtag_len; i++) {
203 uint8_t b = rawtag[i];
204 dest[i * 2] = (b >> 4) + ((b >> 4) < 10 ? '0' : 'a');
205 dest[i*2 + 1] = (b & 0x0F) + ((b & 0x0F) < 10 ? '0' : 'a');
206 }
207}
208
209int main(int argc, char *argv[]) {
210 if (argc != 3) {
211 puts(USAGE);
212 exit(EXIT_FAILURE);
213 }
214
215 const char *repopath = argv[1];
216 const char *operation = argv[2];
217 if (strcmp(operation, "download") && strcmp(operation, "upload")) {
218 puts(USAGE);
219 exit(EXIT_FAILURE);
220 }
221 checkrepopath(repopath);
222
223 const char *hrefbase = getenv("GITOLFS3_HREF_BASE");
224 const char *keypath = getenv("GITOLFS3_KEY_PATH");
225
226 if (!hrefbase || strlen(hrefbase) == 0)
227 die("Incomplete configuration: Base URL not provided");
228 if (hrefbase[strlen(hrefbase) - 1] != '/')
229 die("Bad configuration: Base URL should end with slash");
230 if (!keypath || strlen(keypath) == 0)
231 die("Incomplete configuration: Key path not provided");
232
233 uint8_t key[64];
234 readkey(keypath, key);
235
236 int64_t expiresin_s = 5 * 60;
237 int64_t expiresat_s = (int64_t)time(NULL) + expiresin_s;
238
239 taginfo_t taginfo = {
240 .authtype = "git-lfs-authenticate",
241 .repopath = repopath,
242 .operation = operation,
243 .expiresat_s = expiresat_s,
244 };
245 char hextag[MAX_HEXTAG_STRLEN + 1];
246 makehextag(taginfo, key, hextag);
247
248 printf("{\"header\":{\"Authorization\":\"Gitolfs3-Hmac-Sha256 %s\"},"
249 "\"expires_in\":%ld,\"href\":\"", hextag, expiresin_s);
250 printescjson(hrefbase);
251 printescjson(repopath);
252 printf("/info/lfs?p=1&te=%ld\"}\n", expiresat_s);
253}
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 @@
1package main
2
3import (
4 "bytes"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/binary"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "io"
13 "io/fs"
14 "os"
15 "path"
16 "strings"
17 "time"
18)
19
20func die(msg string, args ...any) {
21 fmt.Fprint(os.Stderr, "Fatal: ")
22 fmt.Fprintf(os.Stderr, msg, args...)
23 fmt.Fprint(os.Stderr, "\n")
24 os.Exit(1)
25}
26
27type authenticateResponse struct {
28 // When providing href, the Git LFS client will use href as the base URL
29 // instead of building the base URL using the Service Discovery mechanism.
30 // It should end with /info/lfs. See
31 // https://github.com/git-lfs/git-lfs/blob/baf40ac99850a62fe98515175d52df5c513463ec/docs/api/server-discovery.md#ssh
32 HRef string `json:"href,omitempty"`
33 Header map[string]string `json:"header"`
34 // In seconds.
35 ExpiresIn int64 `json:"expires_in,omitempty"`
36 // The expires_at (RFC3339) property could also be used, but we leave it
37 // out since we don't use it. The Git LFS docs recommend using expires_in
38 // instead (???)
39}
40
41func wipe(b []byte) {
42 for i := range b {
43 b[i] = 0
44 }
45}
46
47const usage = "Usage: git-lfs-authenticate <REPO> upload/download"
48
49func main() {
50 // Even though not explicitly described in the Git LFS documentation, the
51 // git-lfs-authenticate command is expected to either exit succesfully with
52 // exit code 0 and to then print credentials in the prescribed JSON format
53 // to standard out. On errors, the command should exit with a non-zero exit
54 // code and print the error message in plain text to standard error. See
55 // https://github.com/git-lfs/git-lfs/blob/baf40ac99850a62fe98515175d52df5c513463ec/lfshttp/ssh.go#L76-L117
56
57 if len(os.Args) != 3 {
58 fmt.Println(usage)
59 os.Exit(1)
60 }
61
62 repo := strings.TrimPrefix(path.Clean(os.Args[1]), "/")
63 operation := os.Args[2]
64 if operation != "download" && operation != "upload" {
65 fmt.Println(usage)
66 os.Exit(1)
67 }
68 if repo == ".." || strings.HasPrefix(repo, "../") {
69 die("highly illegal repo name (Anzeige ist raus)")
70 }
71 if !strings.HasSuffix(repo, ".git") {
72 die("expected repo name to have '.git' suffix")
73 }
74
75 repoDir := path.Join(repo)
76 finfo, err := os.Stat(repoDir)
77 if err != nil {
78 if errors.Is(err, fs.ErrNotExist) {
79 die("repo not found")
80 }
81 die("could not stat repo: %s", err)
82 }
83 if !finfo.IsDir() {
84 die("repo not found")
85 }
86
87 hrefBase := os.Getenv("GITOLFS3_HREF_BASE")
88 if hrefBase == "" {
89 die("incomplete configuration: base URL not provided")
90 }
91 if !strings.HasSuffix(hrefBase, "/") {
92 hrefBase += "/"
93 }
94
95 keyPath := os.Getenv("GITOLFS3_KEY_PATH")
96 if keyPath == "" {
97 die("incomplete configuration: key path not provided")
98 }
99
100 keyStr, err := os.ReadFile(keyPath)
101 if err != nil {
102 wipe(keyStr)
103 die("cannot read key")
104 }
105 keyStr = bytes.TrimSpace(keyStr)
106 defer wipe(keyStr)
107 if hex.DecodedLen(len(keyStr)) != 64 {
108 die("bad key length")
109 }
110 key := make([]byte, 64)
111 defer wipe(key)
112 if _, err = hex.Decode(key, keyStr); err != nil {
113 die("cannot decode key")
114 }
115
116 expiresIn := time.Minute * 5
117 expiresAtUnix := time.Now().Add(expiresIn).Unix()
118
119 tag := hmac.New(sha256.New, key)
120 io.WriteString(tag, "git-lfs-authenticate")
121 tag.Write([]byte{0})
122 io.WriteString(tag, repo)
123 tag.Write([]byte{0})
124 io.WriteString(tag, operation)
125 tag.Write([]byte{0})
126 binary.Write(tag, binary.BigEndian, &expiresAtUnix)
127 tagStr := hex.EncodeToString(tag.Sum(nil))
128
129 response := authenticateResponse{
130 Header: map[string]string{
131 "Authorization": "Gitolfs3-Hmac-Sha256 " + tagStr,
132 },
133 ExpiresIn: int64(expiresIn.Seconds()),
134 HRef: fmt.Sprintf("%s%s?p=1&te=%d",
135 hrefBase,
136 path.Join(repo, "/info/lfs"),
137 expiresAtUnix,
138 ),
139 }
140 json.NewEncoder(os.Stdout).Encode(response)
141}