a73x

5d3b98b1

test(api): a CA line that was already canonical proves no canonicalization

a73x   2026-08-23 11:11

Commit message
test(api): a CA line that was already canonical proves no canonicalization

TestUploadUserCAStoresCanonicalLine posted a line straight out of
AuthorizedKeyLine, so storing req.PublicKey verbatim passed. It now
posts what a real upload looks like — indented, carrying the operator's
comment, newline-terminated — and resolves the CA back by the canonical
form, the only form TenantForUserCA is ever asked with.

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