5d3b98b1
test(api): a CA line that was already canonical proves no canonicalization
a73x 2026-08-23 11:11
Commit message
internal/server/api/usercas_test.go
| Old | New | ||
|---|---|---|---|
| @@ -13,29 +13,39 @@ import ( | |||
| 13 | "github.com/stretchr/testify/require" | 13 | "github.com/stretchr/testify/require" |
| 14 | ) | 14 | ) |
| 15 | 15 | ||
| 16 | // TestUploadUserCAStoresCanonicalLine posts a BYO user-CA public key and asserts | 16 | // TestUploadUserCAStoresCanonicalLine posts a BYO user-CA public key in the |
| 17 | // eitri stores its canonical authorized_keys line under the tenant — the pubkey | 17 | // shape a real upload arrives in — copied out of a file, so indented, carrying |
| 18 | // is registered (never a signing key), resolvable back to the tenant. | 18 | // the operator's comment and a trailing newline — and asserts eitri stores the |
| 19 | // CANONICAL line. Lookup is by exact line match (TenantForUserCA), so storing | ||
| 20 | // what was posted would mean a CA that resolves for nobody: every guest signed | ||
| 21 | // by it is unreachable, and re-uploading the same key from a different tool | ||
| 22 | // mints a second row instead of finding the first. The pubkey is registered, | ||
| 23 | // never a signing key. | ||
| 19 | func TestUploadUserCAStoresCanonicalLine(t *testing.T) { | 24 | func TestUploadUserCAStoresCanonicalLine(t *testing.T) { |
| 20 | ts, st, _, _, _ := newServer(t) | 25 | ts, st, _, _, _ := newServer(t) |
| 21 | 26 | ||
| 22 | // A throwaway CA: eitri only ever sees the public key. | 27 | // A throwaway CA: eitri only ever sees the public key. |
| 23 | _, signer, err := sshca.GenerateHostKey() | 28 | _, signer, err := sshca.GenerateHostKey() |
| 24 | require.NoError(t, err) | 29 | require.NoError(t, err) |
| 25 | line := sshca.AuthorizedKeyLine(signer.PublicKey()) | 30 | canonical := sshca.AuthorizedKeyLine(signer.PublicKey()) |
| 31 | posted := " " + canonical + " alex@laptop\n" | ||
| 32 | require.NotEqual(t, canonical, posted, "the posted line must differ from the canonical one or this test proves nothing") | ||
| 26 | 33 | ||
| 27 | resp := do(t, "POST", ts.URL+"/api/v1/tenants/"+testTenant+"/user-cas", testPAT, | 34 | resp := do(t, "POST", ts.URL+"/api/v1/tenants/"+testTenant+"/user-cas", testPAT, |
| 28 | map[string]any{"public_key": line, "label": "yubikey-ca"}) | 35 | map[string]any{"public_key": posted, "label": "yubikey-ca"}) |
| 29 | require.Equal(t, http.StatusCreated, resp.StatusCode) | 36 | require.Equal(t, http.StatusCreated, resp.StatusCode) |
| 30 | 37 | ||
| 31 | var out map[string]string | 38 | var out map[string]string |
| 32 | require.NoError(t, json.NewDecoder(resp.Body).Decode(&out)) | 39 | require.NoError(t, json.NewDecoder(resp.Body).Decode(&out)) |
| 33 | assert.NotEmpty(t, out["fingerprint"], "upload must echo the CA fingerprint") | 40 | assert.NotEmpty(t, out["fingerprint"], "upload must echo the CA fingerprint") |
| 34 | 41 | ||
| 35 | tenant, ok, err := st.TenantForUserCA(line) | 42 | tenant, ok, err := st.TenantForUserCA(canonical) |
| 36 | require.NoError(t, err) | 43 | require.NoError(t, err) |
| 37 | require.True(t, ok, "the uploaded CA line must resolve back to a tenant") | 44 | require.True(t, ok, "the uploaded CA must resolve back to a tenant by its canonical line — the only form a lookup ever presents") |
| 38 | assert.Equal(t, testTenant, tenant) | 45 | assert.Equal(t, testTenant, tenant) |
| 46 | |||
| 47 | assert.True(t, tenantlessListHas(t, ts.URL, testPAT, canonical), | ||
| 48 | "the stored line must be the canonical one, comment and whitespace stripped") | ||
| 39 | } | 49 | } |
| 40 | 50 | ||
| 41 | // TestUploadUserCATenantlessLandsOnCallerTenant posts to the tenant-less | 51 | // TestUploadUserCATenantlessLandsOnCallerTenant posts to the tenant-less |