func (h *HKPFront) ServeHTTP(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() if r.Method != "GET" || r.URL.Path != "/pks/lookup" || len(q["op"]) != 1 || q["op"][0] != "get" || len(q["search"]) != 1 { http.Error(w, `this server only supports queries of the form "/pks/lookup?op=get&search=<EMAIL>"`, 501) return } user := q["search"][0] ctx := context.Background() var requiredSignatures *proto.QuorumExpr if !h.InsecureSkipVerify { realm, err := coname.GetRealmByUser(h.Config, user) if err != nil { http.Error(w, err.Error(), 400) return } requiredSignatures = realm.VerificationPolicy.Quorum } pf, err := h.Lookup(ctx, &proto.LookupRequest{UserId: user, QuorumRequirement: requiredSignatures}) if err != nil { http.Error(w, err.Error(), 503) return } if !h.InsecureSkipVerify { coname.VerifyLookup(h.Config, user, pf, h.Clk.Now()) } if pf.Profile.Keys == nil { http.Error(w, `No results found: No keys found: unknown email`, 404) return } pgpKey, present := pf.Profile.Keys["pgp"] if !present { http.Error(w, `No results found: No keys found: the email is known to the keyserver, but the profile does not include an OpenPGP key`, 404) return } if _, mr := q["mr"]; mr { w.Header().Set("Content-Type", "application/pgp-keys") } aw, err := armor.Encode(w, "PGP PUBLIC KEY BLOCK", nil) if err != nil { http.Error(w, err.Error(), 500) return } _, err = aw.Write(pgpKey) if err != nil { http.Error(w, err.Error(), 500) return } if err := aw.Close(); err != nil { http.Error(w, err.Error(), 500) return } }
func TestKeyserverRoundtrip(t *testing.T) { nReplicas := 3 cfgs, gks, ck, clientConfig, _, caPool, _, teardown := setupKeyservers(t, nReplicas) defer teardown() logs, dbs, clks, _, teardown2 := setupRaftLogCluster(t, nReplicas, 0) defer teardown2() kss := []*Keyserver{} for i := range cfgs { ks, err := Open(cfgs[i], dbs[i], logs[i], clientConfig.Realms[0].VerificationPolicy, clks[i], gks[i], nil) if err != nil { t.Fatal(err) } ks.insecureSkipEmailProof = true ks.Start() defer ks.Stop() kss = append(kss, ks) } stop := stoppableSyncedClocks(clks) defer close(stop) waitForFirstEpoch(kss[0], clientConfig.Realms[0].VerificationPolicy.GetQuorum()) clientTLS, err := clientConfig.Realms[0].ClientTLS.Config(ck) if err != nil { t.Fatal(err) } _, _, _, profile := doRegister(t, kss[0], clientConfig, clientTLS, caPool, clks[0].Now(), alice, 0, proto.Profile{ Nonce: []byte("noncenoncenonceNONCE"), Keys: map[string][]byte{"abc": []byte{1, 2, 3}, "xyz": []byte("TEST 456")}, }) conn, err := grpc.Dial(kss[0].publicListen.Addr().String(), grpc.WithTransportCredentials(credentials.NewTLS(clientTLS))) if err != nil { t.Fatal(err) } c := proto.NewE2EKSPublicClient(conn) proof, err := c.Lookup(context.Background(), &proto.LookupRequest{ UserId: alice, QuorumRequirement: clientConfig.Realms[0].VerificationPolicy.GetQuorum(), }) if err != nil { t.Fatal(err) } if got, want := proof.Profile.Encoding, profile.Encoding; !bytes.Equal(got, want) { t.Errorf("profile didn't roundtrip: %x != %x", got, want) } _, err = coname.VerifyLookup(clientConfig, alice, proof, clks[0].Now()) if err != nil { t.Fatal(err) } }
func TestKeyserverAbsentLookup(t *testing.T) { dieOnCtrlC() pprof() nReplicas := 3 cfgs, gks, ck, clientConfig, _, _, _, teardown := setupKeyservers(t, nReplicas) defer teardown() logs, dbs, clks, _, teardown2 := setupRaftLogCluster(t, nReplicas, 0) defer teardown2() kss := []*Keyserver{} for i := range cfgs { ks, err := Open(cfgs[i], dbs[i], logs[i], clientConfig.Realms[0].VerificationPolicy, clks[i], gks[i], nil) if err != nil { t.Fatal(err) } ks.Start() defer ks.Stop() kss = append(kss, ks) } stop := stoppableSyncedClocks(clks) defer close(stop) waitForFirstEpoch(kss[0], clientConfig.Realms[0].VerificationPolicy.GetQuorum()) clientTLS, err := clientConfig.Realms[0].ClientTLS.Config(ck) if err != nil { t.Fatal(err) } conn, err := grpc.Dial(kss[0].publicListen.Addr().String(), grpc.WithTransportCredentials(credentials.NewTLS(clientTLS))) if err != nil { t.Fatal(err) } c := proto.NewE2EKSPublicClient(conn) proof, err := c.Lookup(context.Background(), &proto.LookupRequest{ UserId: alice, QuorumRequirement: clientConfig.Realms[0].VerificationPolicy.GetQuorum(), }) if err != nil { t.Fatal(err) } keys, err := coname.VerifyLookup(clientConfig, alice, proof, clks[0].Now()) if err != nil { t.Fatal(err) } if keys != nil { t.Fatalf("Got back keys for a nonexistent profile") } }
func TestKeyserverLookupSpecificEpoch(t *testing.T) { dieOnCtrlC() kss, caPool, clks, verifiers, ck, clientConfig, teardown := setupRealm(t, 3, 3) defer teardown() stop := stoppableSyncedClocks(clks) defer close(stop) waitForFirstEpoch(kss[0], clientConfig.Realms[0].VerificationPolicy.GetQuorum()) clientTLS, err := clientConfig.Realms[0].ClientTLS.Config(ck) if err != nil { t.Fatal(err) } _, _, _, profile := doRegister(t, kss[0], clientConfig, clientTLS, caPool, clks[0].Now(), alice, 0, proto.Profile{ Nonce: []byte("noncenoncenonceNONCE"), Keys: map[string][]byte{"abc": []byte{1, 2, 3}, "xyz": []byte("TEST 456")}, }) epoch, err := getLatestEpoch(kss[0], clientConfig.Realms[0].VerificationPolicy.GetQuorum()) if err != nil { t.Fatal(err) } conn, err := grpc.Dial(kss[0].publicListen.Addr().String(), grpc.WithTransportCredentials(credentials.NewTLS(clientTLS))) if err != nil { t.Fatal(err) } c := proto.NewE2EKSPublicClient(conn) proof, err := c.Lookup(context.Background(), &proto.LookupRequest{ Epoch: epoch, UserId: alice, QuorumRequirement: clientConfig.Realms[0].VerificationPolicy.GetQuorum(), }) if err != nil { t.Fatal(err) } if got, want := proof.Profile.Encoding, profile.Encoding; !bytes.Equal(got, want) { t.Errorf("profile didn't roundtrip: %x != %x", got, want) } if got, want := len(proof.Ratifications), majority(len(kss))+len(verifiers); got < want { t.Errorf("expected at least %d sehs, got %d", got, want) } _, err = coname.VerifyLookup(clientConfig, alice, proof, clks[0].Now()) if err != nil { t.Fatal(err) } }
func doUpdate( t *testing.T, ks *Keyserver, clientConfig *proto.Config, clientTLS *tls.Config, caPool *x509.CertPool, now time.Time, name string, sk *[ed25519.PrivateKeySize]byte, pk *proto.PublicKey, version uint64, profileContents proto.Profile, ) (*proto.EncodedEntry, *proto.EncodedProfile) { conn, err := grpc.Dial(ks.publicListen.Addr().String(), grpc.WithTransportCredentials(credentials.NewTLS(clientTLS))) if err != nil { t.Fatal(err) } publicC := proto.NewE2EKSPublicClient(conn) // First, do a lookup to retrieve the index lookup, err := publicC.Lookup(context.Background(), &proto.LookupRequest{ UserId: name, // We don't care about any signatures here; the server just needs to tell us the index. QuorumRequirement: &proto.QuorumExpr{ Threshold: 0, Candidates: []uint64{}, Subexpressions: []*proto.QuorumExpr{}, }, }) if err != nil { t.Fatal(err) } index := lookup.Index // Do the update var keyidBytes [8]byte sha3.ShakeSum256(keyidBytes[:], proto.MustMarshal(pk)) keyid := binary.BigEndian.Uint64(keyidBytes[:8]) profile := proto.EncodedProfile{ Profile: profileContents, } profile.UpdateEncoding() var commitment [64]byte sha3.ShakeSum256(commitment[:], profile.Encoding) entry := proto.EncodedEntry{ Entry: proto.Entry{ Index: index, Version: version, UpdatePolicy: &proto.AuthorizationPolicy{ PublicKeys: map[uint64]*proto.PublicKey{keyid: pk}, PolicyType: &proto.AuthorizationPolicy_Quorum{Quorum: &proto.QuorumExpr{ Threshold: 1, Candidates: []uint64{keyid}, Subexpressions: []*proto.QuorumExpr{}, }, }}, ProfileCommitment: commitment[:], }, } entry.UpdateEncoding() proof, err := publicC.Update(context.Background(), &proto.UpdateRequest{ Update: &proto.SignedEntryUpdate{ NewEntry: entry, Signatures: map[uint64][]byte{keyid: ed25519.Sign(sk, entry.Encoding)[:]}, }, Profile: profile, LookupParameters: &proto.LookupRequest{ UserId: name, QuorumRequirement: clientConfig.Realms[0].VerificationPolicy.GetQuorum(), }, }) if err != nil { t.Fatal(err) } if got, want := proof.Profile.Encoding, profile.Encoding; !bytes.Equal(got, want) { t.Errorf("updated profile didn't roundtrip: %x != %x", got, want) } _, err = coname.VerifyLookup(clientConfig, name, proof, now) if err != nil { t.Fatal(err) } return &entry, &profile }
func main() { configPathPtr := flag.String("config", "clientconfig.json", "path to config file") name := flag.String("name", "*****@*****.**", "name to be looked up") lookupOnly := flag.Bool("lookup", false, "only lookup the name") flag.Parse() timeOut := 10 * time.Second configReader, err := os.Open(*configPathPtr) if err != nil { log.Fatalf("Failed to open configuration file: %s", err) } cfg := &proto.Config{} err = jsonpb.Unmarshal(configReader, cfg) if err != nil { log.Fatalf("Failed to parse configuration file: %s", err) } certFile := "ca.crt.pem" caCertPEM, err := ioutil.ReadFile(certFile) if err != nil { log.Fatalf("couldn't read certs from %s", certFile) } caCertDER, caCertPEM := pem.Decode(caCertPEM) if caCertDER == nil { log.Fatalf("failed to parse key PEM") } caCert, err := x509.ParseCertificate(caCertDER.Bytes) if err != nil { log.Fatal(err) } caPool := x509.NewCertPool() caPool.AddCert(caCert) realm := cfg.Realms[0] clientTLS, err := realm.ClientTLS.Config(getKey) if err != nil { log.Fatal(err) } conn, err := grpc.Dial(realm.Addr, grpc.WithTransportCredentials(credentials.NewTLS(clientTLS)), grpc.WithTimeout(timeOut)) if err != nil { log.Fatal(err) } publicC := proto.NewE2EKSPublicClient(conn) // First, do a lookup to retrieve the index lookup, err := publicC.Lookup(context.Background(), &proto.LookupRequest{ UserId: *name, // We don't care about any signatures here; the server just needs to tell us the index. // We could just give an empty quorum requirement if we wanted (although I guess the // spec actually disallows that). QuorumRequirement: realm.VerificationPolicy.GetQuorum(), }) if err != nil { log.Fatal(err) } fmt.Printf("looking up %s:\n", *name) keys, err := coname.VerifyLookup(cfg, *name, lookup, time.Now()) if err != nil { log.Fatal(err) } if keys == nil { fmt.Printf("not present\n") } else { fmt.Printf("keys: %s\n", keys) } index := lookup.Index if *lookupOnly { return } // Then, do the actual update nonce := make([]byte, 16) _, err = rand.Read(nonce) if err != nil { log.Fatal(err) } profile := proto.EncodedProfile{ Profile: proto.Profile{ Nonce: nonce, Keys: map[string][]byte{"abc": []byte("foo bar"), "xyz": []byte("TEST 456")}, }, } profile.UpdateEncoding() var commitment [64]byte sha3.ShakeSum256(commitment[:], profile.Encoding) var version uint64 if lookup.Entry != nil { version = lookup.Entry.Version + 1 } entry := proto.EncodedEntry{ Entry: proto.Entry{ Index: index, Version: version, UpdatePolicy: &proto.AuthorizationPolicy{ PublicKeys: make(map[uint64]*proto.PublicKey), PolicyType: &proto.AuthorizationPolicy_Quorum{ Quorum: &proto.QuorumExpr{ Threshold: 0, Candidates: []uint64{}, Subexpressions: []*proto.QuorumExpr{}, }, }, }, ProfileCommitment: commitment[:], }, } entry.UpdateEncoding() var entryHash [32]byte sha3.ShakeSum256(entryHash[:], entry.Encoding) fmt.Printf("updating profile:\n") proof, err := publicC.Update(context.Background(), &proto.UpdateRequest{ Update: &proto.SignedEntryUpdate{ NewEntry: entry, Signatures: make(map[uint64][]byte), }, Profile: profile, LookupParameters: &proto.LookupRequest{ UserId: *name, QuorumRequirement: realm.VerificationPolicy.GetQuorum(), }, EmailProof: &proto.EmailProof{ProofType: &proto.EmailProof_DKIMProof{DKIMProof: []byte("X-Apparently-To: [email protected]; Mon, 31 Aug 2015 20:22:05 +0000\r\nReturn-Path: <*****@*****.**>\r\nReceived-SPF: pass (domain of yahoo-inc.com designates 216.145.54.155 as permitted sender)\r\nX-YMailISG: 1EhwNaYWLDv2gTUu.9nt0jxdqITV9b92O6Cwv1fdJI0Znuzw\r\n JiRKT62YsHZWJxcnlzt4SKtiChHTSAwHSy9.w97NwmcUQYqFkJkXmsqAjscY\r\n 0KW9efirs1q3YEvUtmN3BkIhbXujDW5L0Ne0YQtZnK.DF_Yi6dp7RCvfzUZf\r\n RskRPc2qP.tEMNji9_S1kSiPCtn8AwFsPHgGZtQOTtEYsTYUCsD4oEnjAJy1\r\n 332kCwDAOk1wj5l7.PwstEHF438ER_KZaMzfFq8UNrhdEwoFYZGSOfYeFV8W\r\n d2XMmN4vAJRV4D0FV9_cYwJRktzNvKqq.WddXZLuc96QR4.hxOOsq4gEEfJ8\r\n rkw8VodZEu_GlY_2lmyW8UBqmZLhO9lFuNsjfPVSVRyTxt4mMFBG_5XQudBO\r\n xudVy7LzRGiwH.3UlpR4Vl7smmSRC2bf4OrR0bZthRngty1VipzNCRVAE8Od\r\n ZjoR092JxYCF.B94f6ZF5FKqbB2QCjns3WKF0aExw.lzX8_Ral0DsXpBdan5\r\n aIU8cGBQuJBoaAjbaEanoVUXlvz0qXHLEDisvKKsc3w.igAQy01OCSik.2Gx\r\n m2XkGfeZ37mSdZhzAVKMR8eM8WkXl_O.uf7kq6vEyWXuhaMQonTjFYcdAtZX\r\n FMxFdkDTLp4bdEn1gu9JWyRYZTYnj3igBJxc0z_tIBlMrVpcf_NFpjBn1di2\r\n _sfojiKsnhUoYHwbjX7KT7yblcK8Re5Fp57g7XWIc2_tmRbY_iaiGhK0qr4x\r\n 72_hfaF0OBgArvDUPYWF50HYIP597qv1ucNyymAPVGxOx1UNOFWej494R6N1\r\n C8VTghtzKCbklLGGnmSIqPRZCZntvEsWHmmabhFwxRiTZQeu_HfN8CA_Gqoq\r\n k4qQgMbq9o22ekLsAj_jDEnUW343npUbD0XAYDOBCKuSRxZubpmXAu1HqmWL\r\n OTbpjEw8lvF_71EyG5qKVZoEiNCxXMUxia1FBp6RS_uDb4E73TgPxe1v1Ctw\r\n dzqBdg4G0DKkeMqDmDUzQUynHnonJpO4M5Bl6cdr2OfvYK7iDpPf1xeqoexb\r\n McFitNfwU6kax4VRMGahTFv2L56ovtsgV.NMuNEAwad_p3zI0kwi3OMfan9K\r\n _5pIOwQYuKc5vD.5Twq9KSOdYHGYZp_8cBv6dZ4UXvc9k1M5491nJ164VQo.\r\n v.qr6ufI2qhXdMTNxzsLw372.iYBA_5xPeT3dvaYt35sjzmjZnpN\r\nX-Originating-IP: [216.145.54.155]\r\nAuthentication-Results: mta2002.corp.mail.gq1.yahoo.com from=yahoo-inc.com; domainkeys=neutral (no sig); from=yahoo-inc.com; dkim=pass (ok)\r\nReceived: from 127.0.0.1 (EHLO mrout6.yahoo.com) (216.145.54.155)\r\n by mta2002.corp.mail.gq1.yahoo.com with SMTPS; Mon, 31 Aug 2015 20:22:05 +0000\r\nReceived: from omp1018.mail.ne1.yahoo.com (omp1018.mail.ne1.yahoo.com [98.138.89.162])\r\n by mrout6.yahoo.com (8.14.9/8.14.9/y.out) with ESMTP id t7VKLum2007505\r\n (version=TLSv1/SSLv3 cipher=DHE-RSA-CAMELLIA256-SHA bits=256 verify=NO)\r\n for <*****@*****.**>; Mon, 31 Aug 2015 13:21:56 -0700 (PDT)\r\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=yahoo-inc.com;\r\n s=cobra; t=1441052517;\r\n bh=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=;\r\n h=Date:From:Reply-To:To:Subject;\r\n b=bzbQnVqgZoDSfLYqlVlyv++8CntLQ2AQJR84uPzh2OF/mnz+m+H+YfzAzYQc7b+GU\r\n lGgG0DX89hnWgmp4U1LlzNqwUFrhmL8muanSejVHWkrX48grrG1OwNd5z04oO0hFJE\r\n 20ql0lI4tkgSNR7JLoedMVNm/YdrmCiHbuMj2cxg=\r\nReceived: (qmail 99407 invoked by uid 1000); 31 Aug 2015 20:21:56 -0000\r\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yahoo-inc.com; s=ginc1024; t=1441052516; bh=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=; h=Date:From:Reply-To:To:Message-ID:Subject:MIME-Version:Content-Type:Content-Transfer-Encoding; b=r+c21Nf78CMLaL0K1FmgE/CQz70nUJyVsSgQWqu9OlGdIf2GrY9OmwueQ4xHiLu4A5Jd0CAFgxLS5ZkBkPf4xUCvaLmQOYyY+8bfM2JhuG+g2dOMmjjajNqs+iyXfyx+Ak0T2Kahom/b7cpmr5/PiAb2JpL3O0p2StukvGAolp0=\r\nX-YMail-OSG: 0DtuWrwLUzvKrUMVVf5jtWesdteRmLR6oLTuKAGepU.3rTsZeR6zFmacnp0O_Dj\r\n RWZU-\r\nReceived: by 98.138.105.210; Mon, 31 Aug 2015 20:21:55 +0000 \r\nDate: Mon, 31 Aug 2015 20:21:55 +0000 (UTC)\r\nFrom: Daniel Ziegler <*****@*****.**>\r\nReply-To: Daniel Ziegler <*****@*****.**>\r\nTo: Daniel Ziegler <*****@*****.**>\r\nMessage-ID: <*****@*****.**>\r\nSubject: _YAHOO_E2E_KEYSERVER_PROOF_Y4ncLhJeE/qmdXlzCRp5JtgFHy4F4NbKPawk9U8vJ1E=\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\nContent-Length: 1\r\n\r\n")}}}) if err != nil { log.Fatalf("update failed on %s: %s", realm.Addr, err) } if got, want := proof.Profile.Encoding, profile.Encoding; !bytes.Equal(got, want) { log.Fatalf("created profile didn't roundtrip: %x != %x", got, want) } _, err = coname.VerifyLookup(cfg, *name, proof, time.Now()) if err != nil { log.Fatal(err) } fmt.Printf("success\n") }
func TestKeyserverHTTPFrontLookup(t *testing.T) { kss, caPool, clks, verifiers, ck, clientConfig, teardown := setupRealm(t, 3, 3) ks := kss[0] defer teardown() stop := stoppableSyncedClocks(clks) defer close(stop) waitForFirstEpoch(kss[0], clientConfig.Realms[0].VerificationPolicy.GetQuorum()) clientTLS, err := clientConfig.Realms[0].ClientTLS.Config(ck) if err != nil { t.Fatal(err) } pgpKeyRef := []byte("this-is-alices-pgp-key") _, _, _, profile := doRegister(t, ks, clientConfig, clientTLS, caPool, clks[0].Now(), alice, 0, proto.Profile{ Nonce: []byte("definitely used only once"), Keys: map[string][]byte{"pgp": pgpKeyRef}, }) url := "https://" + ks.httpFrontListen.Addr().String() + "/lookup" lr := &proto.LookupRequest{UserId: alice, QuorumRequirement: clientConfig.Realms[0].VerificationPolicy.GetQuorum()} var b bytes.Buffer mr := &jsonpb.Marshaler{OrigName: true} err = mr.Marshal(&b, lr) if err != nil { t.Fatal(err) } req, err := http.NewRequest("POST", url, &b) tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } c := &http.Client{Transport: tr} resp, err := c.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() body, e := ioutil.ReadAll(resp.Body) if resp.Status != "200 OK" { t.Fatalf("%s (%s)", body, e) } proof := &proto.LookupProof{} err = jsonpb.UnmarshalString(string(body), proof) if err != nil { t.Fatal(err) } if got, want := proof.Profile.Encoding, profile.Encoding; !bytes.Equal(got, want) { t.Errorf("profile didn't roundtrip: %x != %x", got, want) } if got, want := len(proof.Ratifications), majority(len(kss))+len(verifiers); got < want { t.Errorf("expected at least %d sehs, got %d", got, want) } _, err = coname.VerifyLookup(clientConfig, alice, proof, clks[0].Now()) if err != nil { t.Fatal(err) } // To verify json response preserves the original field names // as specified in https://github.com/yahoo/coname/blob/master/proto/client.proto#L63-L89 type lp struct { Entry string `json:"entry"` Index string `json:"index"` IndexProof string `json:"index_proof"` Profile string `json:"profile"` UserId string `json:"user_id"` } l := &lp{} err = json.Unmarshal(body, &l) if err != nil { t.Fatal(err) } if l.Entry == "" { t.Errorf("entry not found in the response") } if l.Index == "" { t.Errorf("index not found in the response") } if l.IndexProof == "" { t.Errorf("index_proof not found in the response") } if l.Profile == "" { t.Errorf("profile not found in the response") } if l.UserId == "" { t.Errorf("user_id not found in the response") } }