Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External policy hook #252

Merged
merged 17 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/approve-list-svc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
approve-list-svc
17 changes: 17 additions & 0 deletions cmd/approve-list-svc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Approve List Service

The reference implementation of an external policy service.

For configuration see [an example](config_example.yaml)

## Start

```
approve-list-svc -c config serve
```

## Print the public key

```
approve-list-svc -c config pub
```
91 changes: 91 additions & 0 deletions cmd/approve-list-svc/approve_list_svc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package main

import (
"errors"
"fmt"
"os"

"github.com/ecadlabs/signatory/cmd/approve-list-svc/server"
"github.com/ecadlabs/signatory/pkg/tezos"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
Use: "approve-list-svc",
Short: "Example IP approve list external policy service",
}

var confFile string

var pubCmd = &cobra.Command{
Use: "pub",
Short: "Print the authentication public key",
RunE: func(cmd *cobra.Command, args []string) error {
conf, err := ReadConfig(confFile)
if err != nil {
return err
}
pk, err := conf.GetPrivateKey()
if err != nil {
return err
}
if pk == nil {
return errors.New("private key is not specified")
}

pub, err := tezos.EncodePublicKey(pk.Public())
if err != nil {
return err
}

fmt.Printf("Public key: %s\n", pub)
return nil
},
}

var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the server",
RunE: func(cmd *cobra.Command, args []string) error {
conf, err := ReadConfig(confFile)
if err != nil {
return err
}

pk, err := conf.GetPrivateKey()
if err != nil {
return err
}

ips, nets, err := conf.Addresses()
if err != nil {
return err
}

srv := server.Server{
Address: conf.Address,
PrivateKey: pk,
Addresses: ips,
Nets: nets,
}

s := srv.New()
log.Printf("HTTP server is listening for connections on %s", srv.Address)
log.Println(s.ListenAndServe())

return nil
},
}

func init() {
rootCmd.PersistentFlags().StringVarP(&confFile, "config", "c", "", "Config file")
rootCmd.AddCommand(pubCmd)
rootCmd.AddCommand(serveCmd)
}

func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
87 changes: 87 additions & 0 deletions cmd/approve-list-svc/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net"

"github.com/ecadlabs/signatory/pkg/cryptoutils"
"github.com/ecadlabs/signatory/pkg/tezos"
yaml "gopkg.in/yaml.v3"
)

type Config struct {
Address string `yaml:"address"`
PrivateKey string `yaml:"private_key"`
PrivateKeyFile string `yaml:"private_key_file"`
List []string `yaml:"list"`
}

func (conf *Config) Addresses() ([]net.IP, []*net.IPNet, error) {
var (
ips []net.IP
nets []*net.IPNet
)
for _, addr := range conf.List {
if _, n, err := net.ParseCIDR(addr); err == nil {
nets = append(nets, n)
} else {
if ip := net.ParseIP(addr); ip != nil {
ips = append(ips, ip)
} else {
return nil, nil, fmt.Errorf("invalid address: %s", addr)
}
}
}
return ips, nets, nil
}

func (conf *Config) GetPrivateKey() (cryptoutils.PrivateKey, error) {
var keyData []byte
if conf.PrivateKey != "" {
if pk, err := tezos.ParsePrivateKey(conf.PrivateKey, nil); err == nil {
return pk, nil
} else {
keyData = []byte(conf.PrivateKey)
}
} else {
if conf.PrivateKeyFile == "" {
return nil, nil
}
var err error
if keyData, err = ioutil.ReadFile(conf.PrivateKeyFile); err != nil {
return nil, err
}
}

b, _ := pem.Decode(keyData)
if b == nil {
return nil, errors.New("can't parse private key PEM block")
}

key, err := x509.ParsePKCS8PrivateKey(b.Bytes)
if err != nil {
return nil, err
}
pk, ok := key.(cryptoutils.PrivateKey)
if !ok {
return nil, errors.New("unexpected private key type")
}
return pk, nil
}

func ReadConfig(file string) (*Config, error) {
yamlFile, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
var c Config
if err = yaml.Unmarshal(yamlFile, &c); err != nil {
return nil, err
}

return &c, nil
}
13 changes: 13 additions & 0 deletions cmd/approve-list-svc/config_example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# host:port to listen on
address: :6733

# Optional private key for authenticated replies. Can be either Base58 encoded private key (see `signatory-tools gen-key`)
# of PEM encoded PKCS8 key data
private_key: edsk3YfMTdSzkFqLtmiZoFtEN6sR9jp64zin7f4jUTJiKbTLCqkkcJ

# Alternatively PEM file can be stored separately
#private_key_file: path_to_pem_file

# List of allowed addresses which can be IPv4/IPv6 addresses or CIDR network addresses
list:
- 192.168.88.254
119 changes: 119 additions & 0 deletions cmd/approve-list-svc/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package server

import (
"encoding/json"
"fmt"
"net"
"net/http"

"github.com/ecadlabs/signatory/pkg/cryptoutils"
"github.com/ecadlabs/signatory/pkg/signatory"
"github.com/ecadlabs/signatory/pkg/tezos"
"github.com/ecadlabs/signatory/pkg/tezos/utils"
)

type Server struct {
Address string
PrivateKey cryptoutils.PrivateKey
Addresses []net.IP
Nets []*net.IPNet
}

func (s *Server) Handler() (http.Handler, error) {
pub := s.PrivateKey.Public()
hash, err := tezos.EncodePublicKeyHash(pub)
if err != nil {
return nil, err
}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req signatory.PolicyHookRequest
dec := json.NewDecoder(r.Body)
if err := dec.Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

var ok bool
for _, n := range s.Nets {
if n.Contains(req.Source) {
ok = true
break
}
}
if !ok {
for _, a := range s.Addresses {
if a.Equal(req.Source) {
ok = true
break
}
}
}

if s.PrivateKey != nil {
var status int
if ok {
status = http.StatusOK
} else {
status = http.StatusForbidden
}

replyPl := signatory.PolicyHookReplyPayload{
Status: status,
PublicKeyHash: hash,
Nonce: req.Nonce,
}

if !ok {
replyPl.Error = fmt.Sprintf("address %s is not allowed", req.Source)
}

buf, err := json.Marshal(&replyPl)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

digest := utils.DigestFunc(buf)
sig, err := cryptoutils.Sign(s.PrivateKey, digest[:])
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

s, err := tezos.EncodeGenericSignature(sig)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

reply := signatory.PolicyHookReply{
Payload: buf,
Signature: s,
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(&reply)
} else {
var status int
if ok {
status = http.StatusNoContent
} else {
status = http.StatusForbidden
}
w.WriteHeader(status)
}
}), nil
}

func (s *Server) New() *http.Server {
h, err := s.Handler()
if err != nil {
return nil
}
return &http.Server{
Handler: h,
Addr: s.Address,
}
}
Loading