{"action":"created","comment":{"url":"https://api.github.com/repos/cloudflare/gokeyless/pulls/comments/128612650","pull_request_review_id":51309498,"id":128612650,"diff_hunk":"@@ -0,0 +1,825 @@\n+package server\n+\n+import (\n+\t\"crypto\"\n+\t\"crypto/ecdsa\"\n+\t\"crypto/elliptic\"\n+\t\"crypto/rand\"\n+\t\"crypto/rsa\"\n+\t\"crypto/tls\"\n+\t\"crypto/x509\"\n+\t\"errors\"\n+\t\"fmt\"\n+\t\"io\"\n+\t\"io/ioutil\"\n+\t\"net\"\n+\t\"os\"\n+\t\"path/filepath\"\n+\t\"regexp\"\n+\t\"runtime\"\n+\t\"sync\"\n+\t\"time\"\n+\n+\t\"github.com/cloudflare/cfssl/log\"\n+\t\"github.com/cloudflare/gokeyless/protocol\"\n+\tbuf_ecdsa \"github.com/cloudflare/gokeyless/server/internal/ecdsa\"\n+\t\"github.com/cloudflare/gokeyless/server/internal/worker\"\n+)\n+\n+var keyExt = regexp.MustCompile(`.+\\.key`)\n+\n+// Keystore is an abstract container for a server's private keys, allowing\n+// lookup of keys based on incoming `Operation` requests.\n+type Keystore interface {\n+\tGet(*protocol.Operation) (crypto.Signer, bool)\n+}\n+\n+// DefaultKeystore is a simple in-memory Keystore.\n+type DefaultKeystore struct {\n+\tmtx  sync.RWMutex\n+\tskis map[protocol.SKI]crypto.Signer\n+}\n+\n+// NewDefaultKeystore returns a new DefaultKeystore.\n+func NewDefaultKeystore() *DefaultKeystore {\n+\treturn &DefaultKeystore{skis: make(map[protocol.SKI]crypto.Signer)}\n+}\n+\n+// NewKeystoreFromDir creates a keystore populated from all of the \".key\" files\n+// in dir. For each \".key\" file, LoadKey is called to parse the file's contents\n+// into a crypto.Signer, which is stored in the Keystore.\n+func NewKeystoreFromDir(dir string, LoadKey func([]byte) (crypto.Signer, error)) (Keystore, error) {\n+\tkeys := NewDefaultKeystore()\n+\terr := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\n+\t\tif !info.IsDir() && keyExt.MatchString(info.Name()) {\n+\t\t\tlog.Infof(\"loading %s...\", path)\n+\n+\t\t\tin, err := ioutil.ReadFile(path)\n+\t\t\tif err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\n+\t\t\tpriv, err := LoadKey(in)\n+\t\t\tif err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\n+\t\t\treturn keys.Add(nil, priv)\n+\t\t}\n+\t\treturn nil\n+\t})\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\treturn keys, nil\n+}\n+\n+// Add adds a new key to the server's internal store. Stores in maps by SKI and\n+// (if possible) Digest, SNI, Server IP, and Client IP.\n+func (keys *DefaultKeystore) Add(op *protocol.Operation, priv crypto.Signer) error {\n+\tski, err := protocol.GetSKI(priv.Public())\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tkeys.mtx.Lock()\n+\tdefer keys.mtx.Unlock()\n+\n+\tkeys.skis[ski] = priv\n+\n+\tlog.Debugf(\"add key with SKI: %02x\", ski)\n+\treturn nil\n+}\n+\n+// Get returns a key from keys, mapped from SKI.\n+func (keys *DefaultKeystore) Get(op *protocol.Operation) (crypto.Signer, bool) {\n+\tkeys.mtx.RLock()\n+\tdefer keys.mtx.RUnlock()\n+\n+\tski := op.SKI\n+\tif ski.Valid() {\n+\t\tpriv, found := keys.skis[ski]\n+\t\tif found {\n+\t\t\tlog.Infof(\"fetch key with SKI: %s\", ski)\n+\t\t\treturn priv, found\n+\t\t}\n+\t}\n+\n+\tlog.Errorf(\"couldn't fetch key for %s.\", op)\n+\treturn nil, false\n+}\n+\n+// Server is a Keyless Server capable of performing opaque key operations.\n+type Server struct {\n+\t// config is initialized with the auth configuration used for communicating with keyless clients.\n+\tconfig *tls.Config\n+\t// keys contains the private keys and certificates for the server.\n+\tkeys Keystore\n+\t// stats stores statistics about keyless requests.\n+\tstats *statistics\n+\t// getCert is used for loading certificates.\n+\tgetCert GetCert\n+\t// sealer is called for Seal and Unseal operations.\n+\tsealer Sealer\n+\n+\t// TODO(joshlf): Re-architect so that we don't need to store listeners in the\n+\t// struct (which is totally not thread safe)\n+\n+\t// unixListener is the listener serving unix://[UnixAddr]\n+\tunixListener net.Listener\n+\t// tcpListener is the listener serving tcp://[Addr]\n+\ttcpListener net.Listener\n+}\n+\n+// NewServer prepares a TLS server capable of receiving connections from keyless clients.\n+func NewServer(cert tls.Certificate, keylessCA *x509.CertPool) *Server {\n+\treturn &Server{\n+\t\tconfig: &tls.Config{\n+\t\t\tClientCAs:    keylessCA,\n+\t\t\tClientAuth:   tls.RequireAndVerifyClientCert,\n+\t\t\tCertificates: []tls.Certificate{cert},\n+\t\t\tCipherSuites: []uint16{\n+\t\t\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n+\t\t\t\ttls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n+\t\t\t},\n+\t\t},\n+\t\tkeys:  NewDefaultKeystore(),\n+\t\tstats: newStatistics(),\n+\t}\n+}\n+\n+// NewServerFromFile reads certificate, key, and CA files in order to create a Server.\n+func NewServerFromFile(certFile, keyFile, caFile string) (*Server, error) {\n+\tcert, err := tls.LoadX509KeyPair(certFile, keyFile)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tpemCerts, err := ioutil.ReadFile(caFile)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tkeylessCA := x509.NewCertPool()\n+\tif !keylessCA.AppendCertsFromPEM(pemCerts) {\n+\t\treturn nil, errors.New(\"gokeyless: failed to read keyless CA from PEM\")\n+\t}\n+\treturn NewServer(cert, keylessCA), nil\n+}\n+\n+// TLSConfig returns the Server's TLS configuration.\n+func (s *Server) TLSConfig() *tls.Config {\n+\treturn s.config\n+}\n+\n+// SetGetCert sets the function that the Server uses to get certificates.\n+func (s *Server) SetGetCert(f GetCert) {\n+\ts.getCert = f\n+}\n+\n+// SetKeystore sets the Keystore used by s. It is NOT safe to call concurrently\n+// with any other methods.\n+func (s *Server) SetKeystore(keys Keystore) {\n+\ts.keys = keys\n+}\n+\n+// SetSealer sets the Sealer used by s. It is NOT safe to call concurrently with\n+// any other methods.\n+func (s *Server) SetSealer(sealer Sealer) {\n+\ts.sealer = sealer\n+}\n+\n+// GetCert is a function that returns a certificate given a request.\n+type GetCert func(op *protocol.Operation) (certChain []byte, err error)\n+\n+// Sealer is an interface for an handler for OpSeal and OpUnseal. Seal and\n+// Unseal can return a protocol.Error to send a custom error code.\n+type Sealer interface {\n+\tSeal(*protocol.Operation) ([]byte, error)\n+\tUnseal(*protocol.Operation) ([]byte, error)\n+}\n+\n+type req struct {\n+\tconn   *tls.Conn\n+\tmtx    *sync.Mutex\n+\tpacket *protocol.Packet\n+}\n+\n+type response struct {\n+\tid uint32\n+\top protocol.Operation\n+}\n+\n+func makeRespondResponse(id uint32, payload []byte) response {\n+\treturn response{id: id, op: protocol.MakeRespondOp(payload)}\n+}\n+\n+func makePongResponse(id uint32, payload []byte) response {\n+\treturn response{id: id, op: protocol.MakePongOp(payload)}\n+}\n+\n+func makeErrResponse(id uint32, err protocol.Error) response {\n+\treturn response{id: id, op: protocol.MakeErrorOp(err)}\n+}\n+\n+// otherWorker performs all non-ECDSA requests\n+type otherWorker struct {\n+\ts    *Server\n+\tname string\n+}\n+\n+func newOtherWorker(s *Server, name string) *otherWorker {\n+\treturn &otherWorker{s: s, name: name}\n+}\n+\n+func (w *otherWorker) Do(job interface{}) interface{} {\n+\tpkt := job.(*protocol.Packet)\n+\n+\trequestBegin := time.Now()\n+\tlog.Debugf(\"Worker %v: version:%d.%d id:%d body:%s\", w.name, pkt.MajorVers, pkt.MinorVers, pkt.ID, pkt.Operation)\n+\n+\tvar opts crypto.SignerOpts\n+\tvar key crypto.Signer\n+\tvar ok bool\n+\tswitch pkt.Operation.Opcode {\n+\tcase protocol.OpPing:\n+\t\tw.s.stats.logRequestDuration(requestBegin)\n+\t\treturn makePongResponse(pkt.ID, pkt.Operation.Payload)\n+\n+\tcase protocol.OpGetCertificate:\n+\t\tif w.s.getCert == nil {\n+\t\t\tlog.Errorf(\"Worker %v: GetCertificate is nil\", w.name)\n+\t\t\tw.s.stats.logInvalid(requestBegin)\n+\t\t\treturn makeErrResponse(pkt.ID, protocol.ErrCertNotFound)\n+\t\t}\n+\n+\t\tcertChain, err := w.s.getCert(&pkt.Operation)\n+\t\tif err != nil {\n+\t\t\tlog.Errorf(\"Worker %v: GetCertificate: %v\", w.name, err)\n+\t\t\tw.s.stats.logInvalid(requestBegin)\n+\t\t\treturn makeErrResponse(pkt.ID, protocol.ErrInternal)\n+\t\t}\n+\t\tw.s.stats.logRequestDuration(requestBegin)\n+\t\treturn makeRespondResponse(pkt.ID, certChain)\n+\n+\tcase protocol.OpSeal, protocol.OpUnseal:\n+\t\tif w.s.sealer == nil {\n+\t\t\tlog.Errorf(\"Worker %v: Sealer is nil\", w.name)\n+\t\t\tw.s.stats.logInvalid(requestBegin)\n+\t\t\treturn makeErrResponse(pkt.ID, protocol.ErrInternal)\n+\t\t}\n+\n+\t\tvar res []byte\n+\t\tvar err error\n+\t\tif pkt.Operation.Opcode == protocol.OpSeal {\n+\t\t\tres, err = w.s.sealer.Seal(&pkt.Operation)\n+\t\t} else {\n+\t\t\tres, err = w.s.sealer.Unseal(&pkt.Operation)\n+\t\t}\n+\t\tif err != nil {\n+\t\t\tlog.Errorf(\"Worker %v: Sealer: %v\", w.name, err)\n+\t\t\tcode := protocol.ErrInternal\n+\t\t\tif err, ok := err.(protocol.Error); ok {\n+\t\t\t\tcode = err\n+\t\t\t}\n+\t\t\tw.s.stats.logInvalid(requestBegin)\n+\t\t\treturn makeErrResponse(pkt.ID, code)\n+\t\t}\n+\t\tw.s.stats.logRequestDuration(requestBegin)\n+\t\treturn makeRespondResponse(pkt.ID, res)\n+\n+\tcase protocol.OpRSADecrypt:\n+\t\tif key, ok = w.s.keys.Get(&pkt.Operation); !ok {\n+\t\t\tlog.Error(protocol.ErrKeyNotFound)\n+\t\t\tw.s.stats.logInvalid(requestBegin)\n+\t\t\treturn makeErrResponse(pkt.ID, protocol.ErrKeyNotFound)\n+\t\t}\n+\n+\t\tif _, ok = key.Public().(*rsa.PublicKey); !ok {\n+\t\t\tlog.Errorf(\"Worker %v: %s: Key is not RSA\", w.name, protocol.ErrCrypto)\n+\t\t\tw.s.stats.logInvalid(requestBegin)\n+\t\t\treturn makeErrResponse(pkt.ID, protocol.ErrCrypto)\n+\t\t}\n+\n+\t\trsaKey, ok := key.(crypto.Decrypter)\n+\t\tif !ok {\n+\t\t\tlog.Errorf(\"Worker %v: %s: Key is not Decrypter\", w.name, protocol.ErrCrypto)\n+\t\t\tw.s.stats.logInvalid(requestBegin)\n+\t\t\treturn makeErrResponse(pkt.ID, protocol.ErrCrypto)\n+\t\t}\n+\n+\t\tptxt, err := rsaKey.Decrypt(nil, pkt.Operation.Payload, nil)\n+\t\tif err != nil {\n+\t\t\tlog.Errorf(\"Worker %v: %s: Decryption error: %v\", w.name, protocol.ErrCrypto, err)\n+\t\t\tw.s.stats.logInvalid(requestBegin)\n+\t\t\treturn makeErrResponse(pkt.ID, protocol.ErrCrypto)\n+\t\t}\n+\n+\t\tw.s.stats.logRequestDuration(requestBegin)\n+\t\treturn makeRespondResponse(pkt.ID, ptxt)\n+\tcase protocol.OpRSASignMD5SHA1:\n+\t\topts = crypto.MD5SHA1\n+\tcase protocol.OpRSASignSHA1:\n+\t\topts = crypto.SHA1\n+\tcase protocol.OpRSASignSHA224:\n+\t\topts = crypto.SHA224\n+\tcase protocol.OpRSASignSHA256, protocol.OpRSAPSSSignSHA256:\n+\t\topts = crypto.SHA256\n+\tcase protocol.OpRSASignSHA384, protocol.OpRSAPSSSignSHA384:\n+\t\topts = crypto.SHA384\n+\tcase protocol.OpRSASignSHA512, protocol.OpRSAPSSSignSHA512:\n+\t\topts = crypto.SHA512\n+\tcase protocol.OpPong, protocol.OpResponse, protocol.OpError:\n+\t\tlog.Errorf(\"Worker %v: %s: %s is not a valid request Opcode\\n\", w.name, protocol.ErrUnexpectedOpcode, pkt.Operation.Opcode)\n+\t\tw.s.stats.logInvalid(requestBegin)\n+\t\treturn makeErrResponse(pkt.ID, protocol.ErrUnexpectedOpcode)\n+\tdefault:\n+\t\tw.s.stats.logInvalid(requestBegin)\n+\t\treturn makeErrResponse(pkt.ID, protocol.ErrBadOpcode)\n+\t}\n+\n+\tswitch pkt.Operation.Opcode {\n+\tcase protocol.OpRSAPSSSignSHA256, protocol.OpRSAPSSSignSHA384, protocol.OpRSAPSSSignSHA512:\n+\t\topts = &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash, Hash: opts.HashFunc()}\n+\t}\n+\n+\tif key, ok = w.s.keys.Get(&pkt.Operation); !ok {\n+\t\tlog.Error(protocol.ErrKeyNotFound)\n+\t\tw.s.stats.logInvalid(requestBegin)\n+\t\treturn makeErrResponse(pkt.ID, protocol.ErrKeyNotFound)\n+\t}\n+\n+\t// Ensure we don't perform an RSA sign for an ECDSA request.\n+\tif _, ok := key.Public().(*rsa.PublicKey); !ok {\n+\t\tlog.Errorf(\"Worker %v: %s: request is RSA, but key isn't\\n\", w.name, protocol.ErrCrypto)\n+\t\tw.s.stats.logInvalid(requestBegin)\n+\t\treturn makeErrResponse(pkt.ID, protocol.ErrCrypto)\n+\t}\n+\n+\tsig, err := key.Sign(rand.Reader, pkt.Operation.Payload, opts)\n+\tif err != nil {\n+\t\tlog.Errorf(\"Worker %v: %s: Signing error: %v\\n\", w.name, protocol.ErrCrypto, err)\n+\t\tw.s.stats.logInvalid(requestBegin)\n+\t\treturn makeErrResponse(pkt.ID, protocol.ErrCrypto)\n+\t}\n+\n+\tw.s.stats.logRequestDuration(requestBegin)\n+\treturn makeRespondResponse(pkt.ID, sig)\n+}\n+\n+const randBufferLen = 1024 * 1024\n+\n+type ecdsaWorker struct {\n+\tbuf  *buf_ecdsa.SyncRandBuffer\n+\ts    *Server\n+\tname string\n+}\n+\n+func newECDSAWorker(s *Server, buf *buf_ecdsa.SyncRandBuffer, name string) *ecdsaWorker {\n+\treturn &ecdsaWorker{\n+\t\tbuf:  buf,\n+\t\ts:    s,\n+\t\tname: name,\n+\t}\n+}\n+\n+func (w *ecdsaWorker) Do(job interface{}) interface{} {\n+\tpkt := job.(*protocol.Packet)\n+\n+\trequestBegin := time.Now()\n+\tlog.Debugf(\"Worker %v: version:%d.%d id:%d body:%s\", w.name, pkt.MajorVers, pkt.MinorVers, pkt.ID, pkt.Operation)\n+\n+\tvar opts crypto.SignerOpts\n+\tvar key crypto.Signer\n+\tvar ok bool\n+\tswitch pkt.Operation.Opcode {\n+\tcase protocol.OpECDSASignMD5SHA1:\n+\t\topts = crypto.MD5SHA1\n+\tcase protocol.OpECDSASignSHA1:\n+\t\topts = crypto.SHA1\n+\tcase protocol.OpECDSASignSHA224:\n+\t\topts = crypto.SHA224\n+\tcase protocol.OpECDSASignSHA256:\n+\t\topts = crypto.SHA256\n+\tcase protocol.OpECDSASignSHA384:\n+\t\topts = crypto.SHA384\n+\tcase protocol.OpECDSASignSHA512:\n+\t\topts = crypto.SHA512\n+\tdefault:\n+\t\t// It's the client's responsibility to send all non-ECDSA requests to the\n+\t\t// pool of otherWorkers.\n+\t\tpanic(fmt.Sprintf(\"internal error: got unexpected opcode %v\", pkt.Operation.Opcode))\n+\t}\n+\n+\tif key, ok = w.s.keys.Get(&pkt.Operation); !ok {\n+\t\tlog.Error(protocol.ErrKeyNotFound)\n+\t\tw.s.stats.logInvalid(requestBegin)\n+\t\treturn makeErrResponse(pkt.ID, protocol.ErrKeyNotFound)\n+\t}\n+\n+\t// Ensure we don't perform an RSA sign for an ECDSA request.\n+\tif _, ok := key.Public().(*ecdsa.PublicKey); !ok {\n+\t\tlog.Errorf(\"Worker %v: %s: request is ECDSA, but key isn't\\n\", w.name, protocol.ErrCrypto)\n+\t\tw.s.stats.logInvalid(requestBegin)\n+\t\treturn makeErrResponse(pkt.ID, protocol.ErrCrypto)\n+\t}\n+\n+\tvar sig []byte\n+\tvar err error\n+\tif k, ok := key.(*ecdsa.PrivateKey); ok && k.Curve == elliptic.P256() {\n+\t\tsig, err = buf_ecdsa.Sign(rand.Reader, k, pkt.Operation.Payload, opts, w.buf)\n+\t} else {\n+\t\tsig, err = key.Sign(rand.Reader, pkt.Operation.Payload, opts)\n+\t\tlog.Debugf(\"Worker %v: Computed ECDSA signature without buffer\", w.name)\n+\t}\n+\tif err != nil {\n+\t\tlog.Errorf(\"Worker %v: %s: Signing error: %v\\n\", w.name, protocol.ErrCrypto, err)\n+\t\tw.s.stats.logInvalid(requestBegin)\n+\t\treturn makeErrResponse(pkt.ID, protocol.ErrCrypto)\n+\t}\n+\n+\tw.s.stats.logRequestDuration(requestBegin)\n+\treturn makeRespondResponse(pkt.ID, sig)\n+}\n+\n+type backgroundWorker struct {\n+\tbuf *buf_ecdsa.SyncRandBuffer\n+}\n+\n+func newBackgroundWorker(buf *buf_ecdsa.SyncRandBuffer) *backgroundWorker {\n+\treturn &backgroundWorker{buf: buf}\n+}\n+\n+func (w *backgroundWorker) Do() {\n+\terr := w.buf.Fill(rand.Reader)\n+\tif err != nil {\n+\t\tpanic(err)\n+\t}\n+}\n+\n+// client implements the worker.Client interface. One is created to handle each\n+// connection from clients over the network. See the documentation in the worker\n+// package for details.\n+type client struct {\n+\tconn net.Conn\n+\t// name used to identify this client in logs\n+\tname                 string\n+\ttimeout              time.Duration\n+\tecdsaPool, otherPool *worker.Pool\n+\t// used by the Log method\n+\terr sync.Once","path":"server/server.go","position":474,"original_position":474,"commit_id":"80351126051280e0fed3f10a028e37234e2dc809","original_commit_id":"678bc557dd0277632b9f553e187b44f1aa573236","user":{"login":"lziest","id":1222706,"avatar_url":"https://avatars3.githubusercontent.com/u/1222706?v=4","gravatar_id":"","url":"https://api.github.com/users/lziest","html_url":"https://github.com/lziest","followers_url":"https://api.github.com/users/lziest/followers","following_url":"https://api.github.com/users/lziest/following{/other_user}","gists_url":"https://api.github.com/users/lziest/gists{/gist_id}","starred_url":"https://api.github.com/users/lziest/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lziest/subscriptions","organizations_url":"https://api.github.com/users/lziest/orgs","repos_url":"https://api.github.com/users/lziest/repos","events_url":"https://api.github.com/users/lziest/events{/privacy}","received_events_url":"https://api.github.com/users/lziest/received_events","type":"User","site_admin":false},"body":"agree","created_at":"2017-07-20T19:42:11Z","updated_at":"2017-07-20T19:42:11Z","html_url":"https://github.com/cloudflare/gokeyless/pull/175#discussion_r128612650","pull_request_url":"https://api.github.com/repos/cloudflare/gokeyless/pulls/175","_links":{"self":{"href":"https://api.github.com/repos/cloudflare/gokeyless/pulls/comments/128612650"},"html":{"href":"https://github.com/cloudflare/gokeyless/pull/175#discussion_r128612650"},"pull_request":{"href":"https://api.github.com/repos/cloudflare/gokeyless/pulls/175"}}},"pull_request":{"url":"https://api.github.com/repos/cloudflare/gokeyless/pulls/175","id":130975554,"html_url":"https://github.com/cloudflare/gokeyless/pull/175","diff_url":"https://github.com/cloudflare/gokeyless/pull/175.diff","patch_url":"https://github.com/cloudflare/gokeyless/pull/175.patch","issue_url":"https://api.github.com/repos/cloudflare/gokeyless/issues/175","number":175,"state":"open","locked":false,"title":"Rewrite server algorithm to improve performance, refactor APIs","user":{"login":"joshlf","id":1046063,"avatar_url":"https://avatars1.githubusercontent.com/u/1046063?v=4","gravatar_id":"","url":"https://api.github.com/users/joshlf","html_url":"https://github.com/joshlf","followers_url":"https://api.github.com/users/joshlf/followers","following_url":"https://api.github.com/users/joshlf/following{/other_user}","gists_url":"https://api.github.com/users/joshlf/gists{/gist_id}","starred_url":"https://api.github.com/users/joshlf/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joshlf/subscriptions","organizations_url":"https://api.github.com/users/joshlf/orgs","repos_url":"https://api.github.com/users/joshlf/repos","events_url":"https://api.github.com/users/joshlf/events{/privacy}","received_events_url":"https://api.github.com/users/joshlf/received_events","type":"User","site_admin":false},"body":"## API modifications\r\n* Move `server` and `protocol` packages out of `internal`\r\n* Reduce API surface of `server` to be as minimal as possible, clean up API to be more usable\r\n\r\n## Performance\r\n* Redesigned server to use worker pool model\r\n  * A fixed number of worker goroutines perform requested operations\r\n  * Two separate pools - one for ECDSA ops, one for everything else\r\n  * Each connection has a pair of reader/writer goroutines that submit jobs to the pool and read responses from the pool\r\n* Pre-compute ECDSA random values ","created_at":"2017-07-17T23:47:52Z","updated_at":"2017-07-20T19:42:11Z","closed_at":null,"merged_at":null,"merge_commit_sha":"903474c71ee47cafe0c296dfb6cbe7e63a929b83","assignee":null,"assignees":[],"requested_reviewers":[],"milestone":null,"commits_url":"https://api.github.com/repos/cloudflare/gokeyless/pulls/175/commits","review_comments_url":"https://api.github.com/repos/cloudflare/gokeyless/pulls/175/comments","review_comment_url":"https://api.github.com/repos/cloudflare/gokeyless/pulls/comments{/number}","comments_url":"https://api.github.com/repos/cloudflare/gokeyless/issues/175/comments","statuses_url":"https://api.github.com/repos/cloudflare/gokeyless/statuses/80351126051280e0fed3f10a028e37234e2dc809","head":{"label":"joshlf:performance","ref":"performance","sha":"80351126051280e0fed3f10a028e37234e2dc809","user":{"login":"joshlf","id":1046063,"avatar_url":"https://avatars1.githubusercontent.com/u/1046063?v=4","gravatar_id":"","url":"https://api.github.com/users/joshlf","html_url":"https://github.com/joshlf","followers_url":"https://api.github.com/users/joshlf/followers","following_url":"https://api.github.com/users/joshlf/following{/other_user}","gists_url":"https://api.github.com/users/joshlf/gists{/gist_id}","starred_url":"https://api.github.com/users/joshlf/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joshlf/subscriptions","organizations_url":"https://api.github.com/users/joshlf/orgs","repos_url":"https://api.github.com/users/joshlf/repos","events_url":"https://api.github.com/users/joshlf/events{/privacy}","received_events_url":"https://api.github.com/users/joshlf/received_events","type":"User","site_admin":false},"repo":{"id":94468745,"name":"gokeyless","full_name":"joshlf/gokeyless","owner":{"login":"joshlf","id":1046063,"avatar_url":"https://avatars1.githubusercontent.com/u/1046063?v=4","gravatar_id":"","url":"https://api.github.com/users/joshlf","html_url":"https://github.com/joshlf","followers_url":"https://api.github.com/users/joshlf/followers","following_url":"https://api.github.com/users/joshlf/following{/other_user}","gists_url":"https://api.github.com/users/joshlf/gists{/gist_id}","starred_url":"https://api.github.com/users/joshlf/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joshlf/subscriptions","organizations_url":"https://api.github.com/users/joshlf/orgs","repos_url":"https://api.github.com/users/joshlf/repos","events_url":"https://api.github.com/users/joshlf/events{/privacy}","received_events_url":"https://api.github.com/users/joshlf/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/joshlf/gokeyless","description":"Go implementation of the keyless protocol","fork":true,"url":"https://api.github.com/repos/joshlf/gokeyless","forks_url":"https://api.github.com/repos/joshlf/gokeyless/forks","keys_url":"https://api.github.com/repos/joshlf/gokeyless/keys{/key_id}","collaborators_url":"https://api.github.com/repos/joshlf/gokeyless/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/joshlf/gokeyless/teams","hooks_url":"https://api.github.com/repos/joshlf/gokeyless/hooks","issue_events_url":"https://api.github.com/repos/joshlf/gokeyless/issues/events{/number}","events_url":"https://api.github.com/repos/joshlf/gokeyless/events","assignees_url":"https://api.github.com/repos/joshlf/gokeyless/assignees{/user}","branches_url":"https://api.github.com/repos/joshlf/gokeyless/branches{/branch}","tags_url":"https://api.github.com/repos/joshlf/gokeyless/tags","blobs_url":"https://api.github.com/repos/joshlf/gokeyless/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/joshlf/gokeyless/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/joshlf/gokeyless/git/refs{/sha}","trees_url":"https://api.github.com/repos/joshlf/gokeyless/git/trees{/sha}","statuses_url":"https://api.github.com/repos/joshlf/gokeyless/statuses/{sha}","languages_url":"https://api.github.com/repos/joshlf/gokeyless/languages","stargazers_url":"https://api.github.com/repos/joshlf/gokeyless/stargazers","contributors_url":"https://api.github.com/repos/joshlf/gokeyless/contributors","subscribers_url":"https://api.github.com/repos/joshlf/gokeyless/subscribers","subscription_url":"https://api.github.com/repos/joshlf/gokeyless/subscription","commits_url":"https://api.github.com/repos/joshlf/gokeyless/commits{/sha}","git_commits_url":"https://api.github.com/repos/joshlf/gokeyless/git/commits{/sha}","comments_url":"https://api.github.com/repos/joshlf/gokeyless/comments{/number}","issue_comment_url":"https://api.github.com/repos/joshlf/gokeyless/issues/comments{/number}","contents_url":"https://api.github.com/repos/joshlf/gokeyless/contents/{+path}","compare_url":"https://api.github.com/repos/joshlf/gokeyless/compare/{base}...{head}","merges_url":"https://api.github.com/repos/joshlf/gokeyless/merges","archive_url":"https://api.github.com/repos/joshlf/gokeyless/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/joshlf/gokeyless/downloads","issues_url":"https://api.github.com/repos/joshlf/gokeyless/issues{/number}","pulls_url":"https://api.github.com/repos/joshlf/gokeyless/pulls{/number}","milestones_url":"https://api.github.com/repos/joshlf/gokeyless/milestones{/number}","notifications_url":"https://api.github.com/repos/joshlf/gokeyless/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/joshlf/gokeyless/labels{/name}","releases_url":"https://api.github.com/repos/joshlf/gokeyless/releases{/id}","deployments_url":"https://api.github.com/repos/joshlf/gokeyless/deployments","created_at":"2017-06-15T18:42:10Z","updated_at":"2017-06-15T18:42:12Z","pushed_at":"2017-07-20T19:05:23Z","git_url":"git://github.com/joshlf/gokeyless.git","ssh_url":"git@github.com:joshlf/gokeyless.git","clone_url":"https://github.com/joshlf/gokeyless.git","svn_url":"https://github.com/joshlf/gokeyless","homepage":null,"size":1131,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"cloudflare:master","ref":"master","sha":"1fb9eb19f175db2ccd5bc7b54c96ca7e603db1ca","user":{"login":"cloudflare","id":314135,"avatar_url":"https://avatars3.githubusercontent.com/u/314135?v=4","gravatar_id":"","url":"https://api.github.com/users/cloudflare","html_url":"https://github.com/cloudflare","followers_url":"https://api.github.com/users/cloudflare/followers","following_url":"https://api.github.com/users/cloudflare/following{/other_user}","gists_url":"https://api.github.com/users/cloudflare/gists{/gist_id}","starred_url":"https://api.github.com/users/cloudflare/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cloudflare/subscriptions","organizations_url":"https://api.github.com/users/cloudflare/orgs","repos_url":"https://api.github.com/users/cloudflare/repos","events_url":"https://api.github.com/users/cloudflare/events{/privacy}","received_events_url":"https://api.github.com/users/cloudflare/received_events","type":"Organization","site_admin":false},"repo":{"id":37894193,"name":"gokeyless","full_name":"cloudflare/gokeyless","owner":{"login":"cloudflare","id":314135,"avatar_url":"https://avatars3.githubusercontent.com/u/314135?v=4","gravatar_id":"","url":"https://api.github.com/users/cloudflare","html_url":"https://github.com/cloudflare","followers_url":"https://api.github.com/users/cloudflare/followers","following_url":"https://api.github.com/users/cloudflare/following{/other_user}","gists_url":"https://api.github.com/users/cloudflare/gists{/gist_id}","starred_url":"https://api.github.com/users/cloudflare/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cloudflare/subscriptions","organizations_url":"https://api.github.com/users/cloudflare/orgs","repos_url":"https://api.github.com/users/cloudflare/repos","events_url":"https://api.github.com/users/cloudflare/events{/privacy}","received_events_url":"https://api.github.com/users/cloudflare/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/cloudflare/gokeyless","description":"Go implementation of the keyless protocol","fork":false,"url":"https://api.github.com/repos/cloudflare/gokeyless","forks_url":"https://api.github.com/repos/cloudflare/gokeyless/forks","keys_url":"https://api.github.com/repos/cloudflare/gokeyless/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cloudflare/gokeyless/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cloudflare/gokeyless/teams","hooks_url":"https://api.github.com/repos/cloudflare/gokeyless/hooks","issue_events_url":"https://api.github.com/repos/cloudflare/gokeyless/issues/events{/number}","events_url":"https://api.github.com/repos/cloudflare/gokeyless/events","assignees_url":"https://api.github.com/repos/cloudflare/gokeyless/assignees{/user}","branches_url":"https://api.github.com/repos/cloudflare/gokeyless/branches{/branch}","tags_url":"https://api.github.com/repos/cloudflare/gokeyless/tags","blobs_url":"https://api.github.com/repos/cloudflare/gokeyless/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cloudflare/gokeyless/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cloudflare/gokeyless/git/refs{/sha}","trees_url":"https://api.github.com/repos/cloudflare/gokeyless/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cloudflare/gokeyless/statuses/{sha}","languages_url":"https://api.github.com/repos/cloudflare/gokeyless/languages","stargazers_url":"https://api.github.com/repos/cloudflare/gokeyless/stargazers","contributors_url":"https://api.github.com/repos/cloudflare/gokeyless/contributors","subscribers_url":"https://api.github.com/repos/cloudflare/gokeyless/subscribers","subscription_url":"https://api.github.com/repos/cloudflare/gokeyless/subscription","commits_url":"https://api.github.com/repos/cloudflare/gokeyless/commits{/sha}","git_commits_url":"https://api.github.com/repos/cloudflare/gokeyless/git/commits{/sha}","comments_url":"https://api.github.com/repos/cloudflare/gokeyless/comments{/number}","issue_comment_url":"https://api.github.com/repos/cloudflare/gokeyless/issues/comments{/number}","contents_url":"https://api.github.com/repos/cloudflare/gokeyless/contents/{+path}","compare_url":"https://api.github.com/repos/cloudflare/gokeyless/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cloudflare/gokeyless/merges","archive_url":"https://api.github.com/repos/cloudflare/gokeyless/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cloudflare/gokeyless/downloads","issues_url":"https://api.github.com/repos/cloudflare/gokeyless/issues{/number}","pulls_url":"https://api.github.com/repos/cloudflare/gokeyless/pulls{/number}","milestones_url":"https://api.github.com/repos/cloudflare/gokeyless/milestones{/number}","notifications_url":"https://api.github.com/repos/cloudflare/gokeyless/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cloudflare/gokeyless/labels{/name}","releases_url":"https://api.github.com/repos/cloudflare/gokeyless/releases{/id}","deployments_url":"https://api.github.com/repos/cloudflare/gokeyless/deployments","created_at":"2015-06-23T03:09:09Z","updated_at":"2017-07-19T19:18:51Z","pushed_at":"2017-07-20T19:05:25Z","git_url":"git://github.com/cloudflare/gokeyless.git","ssh_url":"git@github.com:cloudflare/gokeyless.git","clone_url":"https://github.com/cloudflare/gokeyless.git","svn_url":"https://github.com/cloudflare/gokeyless","homepage":null,"size":1089,"stargazers_count":102,"watchers_count":102,"language":"Go","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":23,"mirror_url":null,"open_issues_count":5,"forks":23,"open_issues":5,"watchers":102,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/cloudflare/gokeyless/pulls/175"},"html":{"href":"https://github.com/cloudflare/gokeyless/pull/175"},"issue":{"href":"https://api.github.com/repos/cloudflare/gokeyless/issues/175"},"comments":{"href":"https://api.github.com/repos/cloudflare/gokeyless/issues/175/comments"},"review_comments":{"href":"https://api.github.com/repos/cloudflare/gokeyless/pulls/175/comments"},"review_comment":{"href":"https://api.github.com/repos/cloudflare/gokeyless/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/cloudflare/gokeyless/pulls/175/commits"},"statuses":{"href":"https://api.github.com/repos/cloudflare/gokeyless/statuses/80351126051280e0fed3f10a028e37234e2dc809"}}}}
{"actor":{"display_login":"lziest"}}