diff --git a/cmd/w3k/go.mod b/cmd/w3k/go.mod deleted file mode 100644 index 4317db0e..00000000 --- a/cmd/w3k/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/cisco-open/wasm-kernel-module/cmd/w3k - -go 1.18 - -require github.com/cristalhq/acmd v0.11.1 diff --git a/cmd/w3k/go.sum b/cmd/w3k/go.sum deleted file mode 100644 index b4f1b710..00000000 --- a/cmd/w3k/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/cristalhq/acmd v0.11.1 h1:DJ4fh2Pv0nPKmqT646IU/0Vh5FNdGblxvF+3/W3NAUI= -github.com/cristalhq/acmd v0.11.1/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ= diff --git a/cmd/w3k/main.go b/cmd/w3k/main.go index d6bab178..8bbc5222 100644 --- a/cmd/w3k/main.go +++ b/cmd/w3k/main.go @@ -34,15 +34,26 @@ import ( "syscall" cli "github.com/cristalhq/acmd" + + "github.com/cisco-open/wasm-kernel-module/pkg/tls" ) +type CommandContext struct { + UID int `json:"uid,omitempty"` + GID int `json:"gid,omitempty"` + PID int `json:"pid,omitempty"` + CommandName string `json:"command_name,omitempty"` + CommandPath string `json:"command_path,omitempty"` +} + type Command struct { - ID string `json:"id,omitempty"` - Command string `json:"command"` - Name string `json:"name,omitempty"` - Code []byte `json:"code,omitempty"` - Entrypoint string `json:"entrypoint,omitempty"` - Data string `json:"data,omitempty"` + Context CommandContext `json:"context,omitempty"` + ID string `json:"id,omitempty"` + Command string `json:"command"` + Name string `json:"name,omitempty"` + Code []byte `json:"code,omitempty"` + Entrypoint string `json:"entrypoint,omitempty"` + Data string `json:"data,omitempty"` } type Answer struct { @@ -66,6 +77,10 @@ func AcceptOk(Command) (string, error) { return "ok", nil } +func ConnectOk(Command) (string, error) { + return "ok", nil +} + type loadFlags struct { File string Name string @@ -77,6 +92,18 @@ func (c *loadFlags) Flags() *flag.FlagSet { fs.StringVar(&c.File, "file", "my-module.wasm", "the file path of the loaded Wasm module") fs.StringVar(&c.Name, "name", "", "how to name the loaded Wasm module") fs.StringVar(&c.Entrypoint, "entrypoint", "", "initial function to invoke after loading the Wasm module") + + return fs +} + +type serverFlags struct { + CAPemFileName string +} + +func (c *serverFlags) Flags() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.StringVar(&c.CAPemFileName, "ca-pem-filename", "", "root CA pem location for CA signer") + return fs } @@ -86,7 +113,8 @@ var commandHandlers map[string]CommandHandler func init() { commandHandlers = map[string]CommandHandler{ - "accept": CommandHandlerFunc(AcceptOk), + "accept": CommandHandlerFunc(AcceptOk), + "connect": CommandHandlerFunc(ConnectOk), } } @@ -144,7 +172,59 @@ var cmds = []cli.Command{ Name: "server", Description: "run the support server for the kernel module", Alias: "s", + FlagSet: &serverFlags{}, ExecFunc: func(ctx context.Context, args []string) error { + var cfg serverFlags + if err := cfg.Flags().Parse(args); err != nil { + return err + } + + signerCA, err := tls.NewSignerCA(cfg.CAPemFileName) + if err != nil { + return err + } + _ = signerCA.Certificate + + commandHandlers["csr_sign"] = CommandHandlerFunc(func(c Command) (string, error) { + var data struct { + CSR string `json:"csr"` + } + + if err := json.Unmarshal([]byte(c.Data), &data); err != nil { + return "jsonerror", err + } + + containers, err := tls.ParsePEMs([]byte(data.CSR)) + if err != nil { + return "error", err + } + + if len(containers) != 1 { + return "error", errors.New("invalid csr") + } + + certificate, err := signerCA.SignCertificateRequest(containers[0].GetX509CertificateRequest().CertificateRequest) + if err != nil { + return "error", err + } + + caCertificate := signerCA.GetCaCertificate() + + var response struct { + Certificate *tls.X509Certificate `json:"certificate"` + TrustAnchors []*tls.X509Certificate `json:"trust_anchors"` + } + + response.Certificate = certificate + response.TrustAnchors = append(response.TrustAnchors, caCertificate) + + j, err := json.Marshal(response) + if err != nil { + return "error", err + } + + return string(j), nil + }) dev, err := os.OpenFile("/dev/wasm", os.O_RDWR, 0666) if err != nil { @@ -165,7 +245,7 @@ var cmds = []cli.Command{ return err } - log.Printf("received command: %+v", command) + log.Printf("received command: (%s) %+v", scanner.Bytes(), command) if handler, ok := commandHandlers[command.Command]; ok { answer, err = handler.HandleCommand(command) diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..d35bd27b --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/cisco-open/wasm-kernel-module + +go 1.18 + +require ( + emperror.dev/errors v0.8.1 + github.com/cristalhq/acmd v0.11.1 +) + +require ( + github.com/pkg/errors v0.9.1 // indirect + github.com/stretchr/testify v1.8.1 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.9.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..3e212de0 --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= +emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= +github.com/cristalhq/acmd v0.11.1 h1:DJ4fh2Pv0nPKmqT646IU/0Vh5FNdGblxvF+3/W3NAUI= +github.com/cristalhq/acmd v0.11.1/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work index f877c820..34ff9565 100644 --- a/go.work +++ b/go.work @@ -1,5 +1,5 @@ go 1.20 -use ./cmd/w3k +use . use ./samples/dns-go diff --git a/go.work.sum b/go.work.sum index b1944ebd..8e5bc733 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,3 +1,127 @@ +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/logging v1.4.2/go.mod h1:jco9QZSx8HiVVqLJReq7z7bVdj0P1Jb9PDFs63T+axo= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +github.com/banzaicloud/proxy-wasm-go-host v1.0.2/go.mod h1:ceg5fs685OwGAGl7gnSdse2YKMiMJBxKuSK8uMmxTqM= +github.com/banzaicloud/proxy-wasm-go-host/runtime/wasmer v1.0.4-c1/go.mod h1:LLCsxoNbU88BofPbOWbvxsmnKnqdsv6RVGvYAfKirZc= +github.com/banzaicloud/proxy-wasm-go-host/runtime/wasmtime/v3 v3.0.2-c1/go.mod h1:MCrrCIxSKM3IUTJjHDFYIg4tf8LWKv4PS+mr7inZBzU= +github.com/banzaicloud/proxy-wasm-go-host/runtime/wazero v1.0.0-pre.8-c1/go.mod h1:OARL2liDcdMVi0IvSPQoBmq7/x2EzjupYyWgkFZukCc= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20220520190051-1e77728a1eaa/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= +github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.10.3-0.20220719090109-b024c36d9935/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gohobby/deepcopy v1.0.1/go.mod h1:J/RNFAQlLyrU0ZfJ86LUh9o7vdvn9C6HRvHfIsV1YFk= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/cel-go v0.11.4/go.mod h1:Av7CU6r6X3YmcHR9GXqVDaEJYfEtSxl6wvIjUQTriCw= +github.com/google/flatbuffers v2.0.7+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/sethvargo/go-envconfig v0.8.2/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tetratelabs/wazero v1.0.0-pre.8/go.mod h1:u8wrFmpdrykiFK0DFPiFm5a4+0RzsdmXYVtijBKqUVo= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/wasmerio/wasmer-go v1.0.4/go.mod h1:0gzVdSfg6pysA6QVp6iVRPTagC6Wq9pOE8J86WKb2Fk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.18.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +istio.io/api v0.0.0-20220826132550-04f2b20dc284/go.mod h1:hQkF0Q19MCmfOTre/Sg4KvrwwETq45oaFplnBm2p4j8= +istio.io/istio v0.0.0-20220831174539-e3364ab424b7/go.mod h1:tsGtNnps8oWEagCnOQ6gBgeQhpZf1J6KVNrKihaPrW0= +istio.io/pkg v0.0.0-20220815202617-8f42645c6e49/go.mod h1:kcBYN5TiyGFM2bs4b7K81j+YeDZ4JrINP+brV9ehZe0= +k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg= +k8s.io/apiextensions-apiserver v0.24.2/go.mod h1:e5t2GMFVngUEHUd0wuCJzw8YDwZoqZfJiGOW6mm2hLQ= +k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/client-go v0.24.2/go.mod h1:zg4Xaoo+umDsfCWr4fCnmLEtQXyCNXCvJuSsglNcV30= +k8s.io/klog/v2 v2.90.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8/go.mod h1:mbJ+NSUoAhuR14N0S63bPkh8MGVSo3VYSGZtH/mfMe0= +k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +sigs.k8s.io/controller-runtime v0.12.3/go.mod h1:qKsk4WE6zW2Hfj0G4v10EnNB2jMG1C+NTb8h+DwCoU0= +sigs.k8s.io/gateway-api v0.5.1-0.20220815164014-854e2bfc5276/go.mod h1:x0AP6gugkFV8fC/oTlnOMU0pnmuzIR8LfIPRVUjxSqA= +sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/tls/parser.go b/pkg/tls/parser.go new file mode 100644 index 00000000..6788f72f --- /dev/null +++ b/pkg/tls/parser.go @@ -0,0 +1,482 @@ +/* + * The MIT License (MIT) + * Copyright (c) 2023 Cisco and/or its affiliates. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package tls + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "math/big" + "net" + "net/url" + "os" + "time" + + "emperror.dev/errors" +) + +type SupportedPEMType string + +const ( + CertificateRequestSupportedPEMType SupportedPEMType = "CERTIFICATE REQUEST" + CertificateSupportedPEMType SupportedPEMType = "CERTIFICATE" + PublicKeySupportedPEMType SupportedPEMType = "PUBLIC KEY" + PrivateKeySupportedPEMType SupportedPEMType = "PRIVATE KEY" + RSAPrivateKeySupportedPEMType SupportedPEMType = "RSA PRIVATE KEY" + ECPrivateKeySupportedPEMType SupportedPEMType = "EC PRIVATE KEY" +) + +type X509Certificate struct { + *CertificateCommon `json:",inline"` + IsCA bool `json:"isCA,omitempty"` + + Certificate *x509.Certificate `json:"-"` +} + +type X509CertificateRequest struct { + *CertificateCommon `json:",inline"` + + CertificateRequest *x509.CertificateRequest `json:"-"` +} + +type CertificateCommon struct { + PublicKey *PublicKey `json:"publicKey,omitempty"` + + SerialNumber string `json:"serialNumber,omitempty"` + NotBefore *time.Time `json:"notBefore,omitempty"` + NotAfter *time.Time `json:"notAfter,omitempty"` + NotBeforeUnix uint64 `json:"notBeforeUnix,omitempty"` + NotAfterUnix uint64 `json:"notAfterUnix,omitempty"` + + Subject string `json:"subject,omitempty"` + Issuer string `json:"issuer,omitempty"` + + DNSNames []string `json:"dnsNames,omitempty"` + EmailAddresses []string `json:"emailAddresses,omitempty"` + IPAddresses []string `json:"ipAddresses,omitempty"` + URIs []string `json:"urIs,omitempty"` + + Signature []byte `json:"signature,omitempty"` + SignatureAlgorithm string `json:"signatureAlgorithm,omitempty"` + + Raw []byte `json:"raw,omitempty"` + RawSubject []byte `json:"rawSubject,omitempty"` + RawIssuer []byte `json:"rawIssuer,omitempty"` +} + +func (c *CertificateCommon) copyIPsAndURIs(ipAddresses []net.IP, uris []*url.URL) { + if l := len(ipAddresses); l > 0 { + c.IPAddresses = make([]string, l) + for k, v := range ipAddresses { + c.IPAddresses[k] = v.String() + } + } + if l := len(uris); l > 0 { + c.URIs = make([]string, l) + for k, v := range uris { + c.URIs[k] = v.String() + } + } +} + +type PublicKey struct { + Type string `json:"type,omitempty"` + BitSize int32 `json:"bitSize,omitempty"` + + RSA_N []byte `json:"RSA_N,omitempty"` + RSA_E []byte `json:"RSA_E,omitempty"` + + Curve string `json:"curve,omitempty"` + EC_Q []byte `json:"EC_Q,omitempty"` + + Raw []byte `json:"raw,omitempty"` + Key any `json:"-"` +} + +type PrivateKey struct { + Type string `json:"type,omitempty"` + Size int `json:"size,omitempty"` + + RSA_P []byte `json:"RSA_P,omitempty"` + RSA_Q []byte `json:"RSA_Q,omitempty"` + RSA_DP []byte `json:"RSA_DP,omitempty"` + RSA_DQ []byte `json:"RSA_DQ,omitempty"` + RSA_IQ []byte `json:"RSA_IQ,omitempty"` + + Curve string `json:"curve,omitempty"` + EC_D []byte `json:"EC_D,omitempty"` + + PublicKey *PublicKey `json:"publicKey,omitempty"` + + Raw []byte `json:"raw,omitempty"` + Key any `json:"-"` +} + +type ContainerType string + +const ( + X509CertificateContainerType ContainerType = "X509Certificate" + X509CertificateRequestContainerType ContainerType = "X509CertificateRequest" + PublicKeyContainerType ContainerType = "PublicKey" + PrivateKeyContainerType ContainerType = "PrivateKey" +) + +type Container struct { + Type ContainerType `json:"type,omitempty"` + Object any `json:"object,omitempty"` +} + +func (c *Container) GetX509Certificate() *X509Certificate { + if o, ok := c.Object.(*X509Certificate); ok { + return o + } + + return &X509Certificate{} +} + +func (c *Container) GetX509CertificateRequest() *X509CertificateRequest { + if o, ok := c.Object.(*X509CertificateRequest); ok { + return o + } + + return &X509CertificateRequest{} +} + +func (c *Container) GetPublicKey() *PublicKey { + if o, ok := c.Object.(*PublicKey); ok { + return o + } + + return &PublicKey{} +} + +func (c *Container) GetPrivateKey() *PrivateKey { + if o, ok := c.Object.(*PrivateKey); ok { + return o + } + + return &PrivateKey{} +} + +func ParsePEMFromFile(pemFileName ...string) ([]*Container, error) { + var content []byte + + for _, file := range pemFileName { + fileContent, err := os.ReadFile(file) + if err != nil { + return nil, err + } + + content = append(content, fileContent...) + } + + return ParsePEMs(content) +} + +func ParsePEMs(content []byte) ([]*Container, error) { + containers := []*Container{} + + var block *pem.Block + var rest []byte + var multierr error + for { + block, rest = pem.Decode(content) + if block == nil { + break + } + + content = rest + + container, err := ParsePEM(block) + if err != nil { + multierr = errors.Append(multierr, err) + continue + } + + containers = append(containers, container) + + if len(rest) == 0 { + break + } + } + + return containers, multierr +} + +func ParsePEM(block *pem.Block) (*Container, error) { + switch SupportedPEMType(block.Type) { + case CertificateRequestSupportedPEMType: + obj, err := ParseX509CertificateRequestFromDER(block.Bytes) + + return &Container{ + Type: X509CertificateRequestContainerType, + Object: obj, + }, err + case CertificateSupportedPEMType: + obj, err := ParseX509CertificateFromDER(block.Bytes) + + return &Container{ + Type: X509CertificateContainerType, + Object: obj, + }, err + case PublicKeySupportedPEMType: + obj, err := ParseX509PublicKey(block.Bytes) + + return &Container{ + Type: PublicKeyContainerType, + Object: obj, + }, err + case PrivateKeySupportedPEMType, RSAPrivateKeySupportedPEMType, ECPrivateKeySupportedPEMType: + obj, err := ParseX509PrivateKey(block.Bytes) + + return &Container{ + Type: PrivateKeyContainerType, + Object: obj, + }, err + default: + return nil, errors.New("unsupported PEM type") + } +} + +func ParseX509CertificateFromDER(der []byte) (*X509Certificate, error) { + x509cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + + return ConvertX509Certificate(x509cert) +} + +func ConvertX509Certificate(x509cert *x509.Certificate) (*X509Certificate, error) { + cert := &X509Certificate{ + Certificate: x509cert, + IsCA: x509cert.IsCA, + CertificateCommon: &CertificateCommon{ + SerialNumber: hex.EncodeToString(x509cert.SerialNumber.Bytes()), + NotBefore: &x509cert.NotBefore, + NotAfter: &x509cert.NotAfter, + NotBeforeUnix: uint64(x509cert.NotBefore.Unix()), + NotAfterUnix: uint64(x509cert.NotAfter.Unix()), + + Subject: x509cert.Subject.String(), + Issuer: x509cert.Issuer.String(), + + DNSNames: x509cert.DNSNames, + EmailAddresses: x509cert.EmailAddresses, + + Signature: x509cert.Signature, + SignatureAlgorithm: x509cert.SignatureAlgorithm.String(), + + Raw: x509cert.Raw, + RawSubject: x509cert.RawSubject, + RawIssuer: x509cert.RawIssuer, + }, + } + + cert.CertificateCommon.copyIPsAndURIs(x509cert.IPAddresses, x509cert.URIs) + + var err error + cert.PublicKey, err = parseX509PublicKey(x509cert.PublicKey) + if err != nil { + return nil, err + } + cert.PublicKey.Raw = x509cert.RawSubjectPublicKeyInfo + + return cert, nil +} + +func ParseX509CertificateRequestFromDER(der []byte) (*X509CertificateRequest, error) { + x509req, err := x509.ParseCertificateRequest(der) + if err != nil { + return nil, err + } + + return ConvertX509CertificateRequest(x509req) +} + +func ConvertX509CertificateRequest(x509req *x509.CertificateRequest) (*X509CertificateRequest, error) { + req := &X509CertificateRequest{ + CertificateRequest: x509req, + CertificateCommon: &CertificateCommon{ + Subject: x509req.Subject.String(), + DNSNames: x509req.DNSNames, + EmailAddresses: x509req.EmailAddresses, + + Signature: x509req.Signature, + SignatureAlgorithm: x509req.SignatureAlgorithm.String(), + + RawSubject: x509req.RawSubject, + Raw: x509req.Raw, + }, + } + + req.CertificateCommon.copyIPsAndURIs(x509req.IPAddresses, x509req.URIs) + + var err error + req.PublicKey, err = parseX509PublicKey(x509req.PublicKey) + if err != nil { + return nil, err + } + req.PublicKey.Raw = x509req.RawSubjectPublicKeyInfo + + return req, nil +} + +func ParseX509PublicKey(der []byte) (*PublicKey, error) { + x509key, err := parsePublicKey(der) + if err != nil { + return nil, err + } + + pk, err := parseX509PublicKey(x509key) + if err != nil { + return nil, err + } + + pk.Raw = der + + return pk, nil +} + +func parseX509PublicKey(pk any) (*PublicKey, error) { + switch key := pk.(type) { + case *rsa.PublicKey: + return convertRSAPublicKey(*key), nil + + case *ecdsa.PublicKey: + return convertECDSPublicKey(*key), nil + } + + return nil, errors.New("unsupported public key") +} + +func parsePublicKey(der []byte) (crypto.PublicKey, error) { + if key, err := x509.ParsePKCS1PublicKey(der); err == nil { + return key, nil + } + + if key, err := x509.ParsePKIXPublicKey(der); err == nil { + return key, nil + } + + return nil, errors.New("tls: failed to parse public key") +} + +func convertRSAPublicKey(key rsa.PublicKey) *PublicKey { + return &PublicKey{ + Type: "RSA", + BitSize: int32(key.Size() * 8), + + RSA_N: key.N.Bytes(), + RSA_E: big.NewInt(0).SetInt64(int64(key.E)).Bytes(), + + Key: key, + } +} + +func convertECDSPublicKey(key ecdsa.PublicKey) *PublicKey { + return &PublicKey{ + Type: "ECDSA", + BitSize: int32(key.Params().BitSize), + + Curve: key.Params().Name, + EC_Q: append([]byte{0x04}, append(key.X.Bytes(), key.Y.Bytes()...)...), + + Key: key, + } +} + +func ParseX509PrivateKey(der []byte) (*PrivateKey, error) { + x509key, err := parsePrivateKey(der) + if err != nil { + return nil, err + } + + pk, err := parseX509PrivateKey(x509key) + if err != nil { + return nil, err + } + + return pk, nil +} + +func parseX509PrivateKey(pk any) (*PrivateKey, error) { + switch key := pk.(type) { + case *rsa.PrivateKey: + return convertRSAPrivateKey(*key), nil + case *ecdsa.PrivateKey: + return convertECDSPrivateKey(*key), nil + } + + return nil, errors.New("unsupported private key") +} + +func parsePrivateKey(der []byte) (crypto.PrivateKey, error) { + if key, err := x509.ParsePKCS1PrivateKey(der); err == nil { + return key, nil + } + + if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { + switch key := key.(type) { + case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: + return key, nil + default: + return nil, errors.New("found unknown private key type in PKCS#8 wrapping") + } + } + + if key, err := x509.ParseECPrivateKey(der); err == nil { + return key, nil + } + + return nil, errors.New("unsupported private key") +} + +func convertRSAPrivateKey(key rsa.PrivateKey) *PrivateKey { + return &PrivateKey{ + Type: "RSA", + Size: key.Size() * 8, + RSA_P: key.Primes[0].Bytes(), + RSA_Q: key.Primes[1].Bytes(), + RSA_DP: key.Precomputed.Dp.Bytes(), + RSA_DQ: key.Precomputed.Dq.Bytes(), + RSA_IQ: key.Precomputed.Qinv.Bytes(), + + PublicKey: convertRSAPublicKey(key.PublicKey), + + Key: key, + } +} + +func convertECDSPrivateKey(key ecdsa.PrivateKey) *PrivateKey { + return &PrivateKey{ + Type: "ECDSA", + Curve: key.Params().Name, + EC_D: key.D.Bytes(), + + PublicKey: convertECDSPublicKey(key.PublicKey), + + Key: key, + } +} diff --git a/pkg/tls/signer.go b/pkg/tls/signer.go new file mode 100644 index 00000000..d95a5ec7 --- /dev/null +++ b/pkg/tls/signer.go @@ -0,0 +1,154 @@ +/* + * The MIT License (MIT) + * Copyright (c) 2023 Cisco and/or its affiliates. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package tls + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "time" + + "emperror.dev/errors" +) + +type SignerCA struct { + PrivateKey *PrivateKey + Certificate *X509Certificate +} + +func NewSignerCA(caPEMFileName string) (*SignerCA, error) { + signer := &SignerCA{} + + var containers []*Container + var err error + + if caPEMFileName == "" { + if caCertPEM, err := signer.createCACertPEM(); err != nil { + return nil, errors.WrapIf(err, "could not create self signed CA PEM") + } else { + containers, err = ParsePEMs(caCertPEM) + } + } else { + containers, err = ParsePEMFromFile(caPEMFileName) + } + if err != nil { + return nil, err + } + + if len(containers) != 2 { + return nil, errors.New("invalid PEM content") + } + + for _, v := range containers { + switch v.Type { + case PrivateKeyContainerType: + signer.PrivateKey = v.GetPrivateKey() + case X509CertificateContainerType: + signer.Certificate = v.GetX509Certificate() + } + } + + if signer.PrivateKey == nil { + return nil, errors.New("missing CA private key") + } + + if signer.Certificate == nil { + return nil, errors.New("missing CA certificate") + } + + return signer, nil +} + +func (s *SignerCA) createCACertPEM() (caPEM []byte, err error) { + serial, err := rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) + if err != nil { + return nil, err + } + + ca := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + caPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, errors.WrapIf(err, "could not generate RSA key for the CA") + } + + caPrivKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: string(RSAPrivateKeySupportedPEMType), + Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + }) + + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + return nil, errors.WrapIf(err, "could not create CA certificate") + } + + caCertPEM := pem.EncodeToMemory(&pem.Block{ + Type: string(CertificateSupportedPEMType), + Bytes: caBytes, + }) + + return bytes.Join([][]byte{caCertPEM, caPrivKeyPEM}, nil), nil +} + +func (s *SignerCA) GetCaCertificate() *X509Certificate { + return s.Certificate +} + +func (s *SignerCA) SignCertificateRequest(req *x509.CertificateRequest) (*X509Certificate, error) { + serial, err := rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) + if err != nil { + return nil, err + } + + pkey := s.PrivateKey.Key.(rsa.PrivateKey) + + certByte, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ + SerialNumber: serial, + Subject: req.Subject, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + IsCA: false, + DNSNames: req.DNSNames, + IPAddresses: req.IPAddresses, + EmailAddresses: req.EmailAddresses, + URIs: req.URIs, + ExtraExtensions: req.ExtraExtensions, + }, s.Certificate.Certificate, req.PublicKey, &pkey) + if err != nil { + return nil, err + } + + return ParseX509CertificateFromDER(certByte) +}