package main import ( "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "net/http" "net/http/httptest" "path/filepath" "regexp" "strings" "sync" "testing" ) func parseTestCSR(t *testing.T, csrPEM string) *x509.CertificateRequest { t.Helper() blk, _ := pem.Decode([]byte(csrPEM)) if blk == nil { t.Fatalf("not PEM: %q", csrPEM) } csr, err := x509.ParseCertificateRequest(blk.Bytes) if err != nil { t.Fatal(err) } if err := csr.CheckSignature(); err != nil { t.Fatalf("the CSR must be self-signed by the device key: %v", err) } return csr } func csrURIs(csr *x509.CertificateRequest) []string { var out []string for _, u := range csr.URIs { out = append(out, u.String()) } return out } // A platform redeem's CSR names the fingerprint once, under the platform's // name. A redeem with a Host that has no platform names it under the // platform's name first and the Host's second. func TestTheOperatorCSRNamesTheFingerprintForItsSigner(t *testing.T) { for _, c := range []struct { local bool want []string }{ {false, []string{fpSANPrefix + "fp-test"}}, {true, []string{fpSANPrefix + "fp-test", hostFPSANPrefix + "fp-test"}}, } { _, csrPEM, err := genOperatorCSR("fp-test", c.local) if err != nil { t.Fatal(err) } if got := csrURIs(parseTestCSR(t, csrPEM)); strings.Join(got, " ") != strings.Join(c.want, " ") { t.Errorf("local=%v: URIs = %v, want %v", c.local, got, c.want) } } } // platformSignerRefusal is the platform's rule for the SANs of an operator // CSR it signs (the bridge's _checked_operator_csr): one name, a URI, // the device-fingerprint prefix (fpSANPrefix) and a fingerprint of 16 to 128 // base64url characters. Anything else is refused before anything is signed. var platformFPRe = regexp.MustCompile(`^[A-Za-z0-9_-]{16,128}$`) func platformSignerRefusal(csr *x509.CertificateRequest) string { n := len(csr.URIs) + len(csr.DNSNames) + len(csr.EmailAddresses) + len(csr.IPAddresses) if n == 0 { return "" } if n != 1 || len(csr.URIs) != 1 { return "operator_csr may name only its device fingerprint (urn:skema:hwfp:)" } s := csr.URIs[0].String() if !strings.HasPrefix(s, fpSANPrefix) || !platformFPRe.MatchString(strings.TrimPrefix(s, fpSANPrefix)) { return "operator_csr may name only its device fingerprint (urn:skema:hwfp:)" } return "" } // A platform pairing's CSR is one the platform signs. It reached the platform // with a second fingerprint name in 0.52.1, and the platform refuses any // second name, so every platform pair, --replace and claim-anchor failed at // the redeem. The redeem here is served on loopback and refuses as the // platform does. func TestAPlatformRedeemSendsACSRThePlatformSigns(t *testing.T) { uiTestConfigDir(t) old := markerPathOverride markerPathOverride = filepath.Join(t.TempDir(), "machine.json") t.Cleanup(func() { markerPathOverride = old }) var mu sync.Mutex var got []string refusal := "no request" redeem := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req redeemReq _ = json.NewDecoder(r.Body).Decode(&req) mu.Lock() defer mu.Unlock() refusal = "the CSR does not parse" if blk, _ := pem.Decode([]byte(req.OperatorCSR)); blk != nil { if csr, err := x509.ParseCertificateRequest(blk.Bytes); err == nil { got, refusal = csrURIs(csr), platformSignerRefusal(csr) } } w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]string{"error": "the test stops here: " + refusal}) })) defer redeem.Close() oldBase := redeemURLBase redeemURLBase = redeem.URL t.Cleanup(func() { redeemURLBase = oldBase }) token := "skb_" + base64.RawURLEncoding.EncodeToString([]byte("acme.secret")) if _, _, err := pairWithToken(token, false, false); err == nil { t.Fatal("the stand-in redeem answers 400; the pairing must fail") } mu.Lock() defer mu.Unlock() if refusal != "" { t.Fatalf("the platform would refuse this CSR: %s (URIs %v)", refusal, got) } if len(got) != 1 || !strings.HasPrefix(got[0], fpSANPrefix) { t.Fatalf("a platform CSR names the fingerprint once, under %s: %v", fpSANPrefix, got) } }