diff --git a/cmd/app/serve.go b/cmd/app/serve.go index 4bb00ce7f..2c4a9fbe9 100644 --- a/cmd/app/serve.go +++ b/cmd/app/serve.go @@ -24,6 +24,7 @@ import ( "github.com/sigstore/fulcio/pkg/api" certauth "github.com/sigstore/fulcio/pkg/ca" "github.com/sigstore/fulcio/pkg/ca/ephemeralca" + "github.com/sigstore/fulcio/pkg/ca/fileca" googlecav1 "github.com/sigstore/fulcio/pkg/ca/googleca/v1" googlecav1beta1 "github.com/sigstore/fulcio/pkg/ca/googleca/v1beta1" "github.com/sigstore/fulcio/pkg/ca/x509ca" @@ -42,7 +43,7 @@ func newServeCmd() *cobra.Command { } cmd.Flags().String("log_type", "dev", "logger type to use (dev/prod)") - cmd.Flags().String("ca", "", "googleca | pkcs11ca | ephemeralca (for testing)") + cmd.Flags().String("ca", "", "googleca | pkcs11ca | fileca | ephemeralca (for testing)") cmd.Flags().String("aws-hsm-root-ca-path", "", "Path to root CA on disk (only used with AWS HSM)") cmd.Flags().String("gcp_private_ca_parent", "", "private ca parent: /projects//locations// (only used with --ca googleca)") cmd.Flags().String("gcp_private_ca_version", "v1", "private ca version: [v1|v1beta1] (only used with --ca googleca)") @@ -50,6 +51,10 @@ func newServeCmd() *cobra.Command { cmd.Flags().String("ct-log-url", "http://localhost:6962/test", "host and path (with log prefix at the end) to the ct log") cmd.Flags().String("config-path", "/etc/fulcio-config/config.json", "path to fulcio config json") cmd.Flags().String("pkcs11-config-path", "config/crypto11.conf", "path to fulcio pkcs11 config file") + cmd.Flags().String("fileca-cert", "", "Path to CA certificate") + cmd.Flags().String("fileca-key", "", "Path to CA encrypted private key") + cmd.Flags().String("fileca-key-passwd", "", "Password to decrypt CA private key") + cmd.Flags().Bool("fileca-watch", true, "Watch filesystem for updates") cmd.Flags().String("host", "0.0.0.0", "The host on which to serve requests") cmd.Flags().String("port", "8080", "The port on which to serve requests") @@ -77,6 +82,17 @@ func runServeCmd(cmd *cobra.Command, args []string) { log.Logger.Fatal("gcp_private_ca_parent must be set when using googleca") } + case "fileca": + if !viper.IsSet("fileca-cert") { + log.Logger.Fatal("fileca-cert must be set to certificate path when using fileca") + } + if !viper.IsSet("fileca-key") { + log.Logger.Fatal("fileca-key must be set to private key path when using fileca") + } + if !viper.IsSet("fileca-key-passwd") { + log.Logger.Fatal("fileca-key-passwd must be set to encryption password for private key file when using fileca") + } + case "ephemeralca": // this is a no-op since this is a self-signed in-memory CA for testing default: @@ -116,6 +132,12 @@ func runServeCmd(cmd *cobra.Command, args []string) { params.CAPath = &path } baseca, err = x509ca.NewX509CA(params) + case "fileca": + certFile := viper.GetString("fileca-cert") + keyFile := viper.GetString("fileca-key") + keyPass := viper.GetString("fileca-key-passwd") + watch := viper.GetBool("fileca-watch") + baseca, err = fileca.NewFileCA(certFile, keyFile, keyPass, watch) case "ephemeralca": baseca, err = ephemeralca.NewEphemeralCA() default: diff --git a/go.mod b/go.mod index 9c135759e..34363df6c 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/PaesslerAG/jsonpath v0.1.1 github.com/ThalesIgnite/crypto11 v1.2.5 github.com/coreos/go-oidc/v3 v3.1.0 + github.com/fsnotify/fsnotify v1.5.1 github.com/go-chi/chi v4.1.2+incompatible github.com/google/go-cmp v0.5.6 github.com/google/uuid v1.3.0 @@ -20,6 +21,7 @@ require ( github.com/spf13/cobra v1.3.0 github.com/spf13/viper v1.10.1 github.com/stretchr/testify v1.7.0 + go.step.sm/crypto v0.13.0 go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.19.1 google.golang.org/api v0.63.0 diff --git a/go.sum b/go.sum index 36b292070..4b0b440c5 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,9 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -556,6 +559,8 @@ github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aU github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -563,6 +568,7 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= @@ -641,6 +647,7 @@ github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= @@ -656,6 +663,7 @@ github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGg github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= @@ -789,6 +797,7 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/secure-systems-lab/go-securesystemslib v0.1.0/go.mod h1:eIjBmIP8LD2MLBL/DkQWayLiz006Q4p+hCu79rvWleY= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sigstore/sigstore v1.0.1 h1:AiJAuz309uei26tRtvzV1XQorns2UogZsgs4ZQ2cYiA= github.com/sigstore/sigstore v1.0.1/go.mod h1:1+krIdtuf81/fLC8mHPt/7uwYiOg7W8k/PAR7lzKW3w= @@ -801,6 +810,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -914,6 +925,8 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.step.sm/crypto v0.13.0 h1:mQuP9Uu2FNmqCJNO0OTbvolnYXzONy4wdUBtUVcP1s8= +go.step.sm/crypto v0.13.0/go.mod h1:5YzQ85BujYBu6NH18jw7nFjwuRnDch35nLzH0ES5sKg= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -943,14 +956,16 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 h1:3erb+vDS8lU1sxfDHF4/hhWyaXnhIaO+7RgL4fDZORA= +golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1042,8 +1057,9 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8= +golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1169,6 +1185,7 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/ca/fileca/fileca.go b/pkg/ca/fileca/fileca.go new file mode 100644 index 000000000..32e92d65a --- /dev/null +++ b/pkg/ca/fileca/fileca.go @@ -0,0 +1,110 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package fileca + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/sigstore/fulcio/pkg/ca" + "github.com/sigstore/fulcio/pkg/ca/x509ca" + "github.com/sigstore/fulcio/pkg/challenges" +) + +type fileCA struct { + sync.RWMutex + + cert *x509.Certificate + key crypto.Signer +} + +// NewFileCA returns a file backed certificate authority. Expects paths to a +// certificate and key that are PEM encoded. The key must be encrypted +// according to RFC 1423 +func NewFileCA(certPath, keyPath, keyPass string, watch bool) (ca.CertificateAuthority, error) { + var fca fileCA + + var err error + fca.cert, fca.key, err = loadKeyPair(certPath, keyPath, keyPass) + if err != nil { + return nil, err + } + + if watch { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + err = watcher.Add(certPath) + if err != nil { + return nil, err + } + err = watcher.Add(keyPath) + if err != nil { + return nil, err + } + + go ioWatch(certPath, keyPath, keyPass, watcher, fca.updateX509KeyPair) + } + + return &fca, err +} + +func (fca *fileCA) updateX509KeyPair(cert *x509.Certificate, key crypto.Signer) { + fca.Lock() + defer fca.Unlock() + + // NB: We use the RWLock to unsure a reading thread can't get a mismatching + // cert / key pair by reading the attributes halfway through the update + // below. + fca.cert = cert + fca.key = key +} + +func (fca *fileCA) getX509KeyPair() (*x509.Certificate, crypto.Signer) { + fca.RLock() + defer fca.RUnlock() + return fca.cert, fca.key +} + +// CreateCertificate issues code signing certificates +func (fca *fileCA) CreateCertificate(_ context.Context, subject *challenges.ChallengeResult) (*ca.CodeSigningCertificate, error) { + cert, err := x509ca.MakeX509(subject) + if err != nil { + return nil, err + } + + rootCA, privateKey := fca.getX509KeyPair() + + finalCertBytes, err := x509.CreateCertificate(rand.Reader, cert, rootCA, subject.PublicKey, privateKey) + if err != nil { + return nil, err + } + + return ca.CreateCSCFromDER(subject, finalCertBytes, nil) +} + +func (fca *fileCA) Root(ctx context.Context) ([]byte, error) { + return pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: fca.cert.Raw, + }), nil +} diff --git a/pkg/ca/fileca/fileca_test.go b/pkg/ca/fileca/fileca_test.go new file mode 100644 index 000000000..d8dd267eb --- /dev/null +++ b/pkg/ca/fileca/fileca_test.go @@ -0,0 +1,76 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package fileca + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "testing" +) + +const testKeyPass = `password123` + +func TestNewFileCA(t *testing.T) { + _, err := NewFileCA( + `testdata/ed25519-cert.pem`, + `testdata/ed25519-key.pem`, + testKeyPass, + false, + ) + if err != nil { + t.Error(`Failed to load file CA from disk`) + } +} + +func TestCertUpdate(t *testing.T) { + oldCert := `testdata/ed25519-cert.pem` + oldKey := `testdata/ed25519-key.pem` + newCert := `testdata/ecdsa-cert.pem` + newKey := `testdata/ecdsa-key.pem` + watch := false + + ca, err := NewFileCA( + oldCert, + oldKey, + testKeyPass, + watch, + ) + if err != nil { + t.Fatal(`Failed to load file CA from disk`) + } + + fca, ok := ca.(*fileCA) + if !ok { + t.Fatal(`Bad CA type`) + } + + _, key := fca.getX509KeyPair() + if _, ok = key.(ed25519.PrivateKey); !ok { + t.Error(`first key should have been an ed25519 key`) + } + + cert, key, err := loadKeyPair(newCert, newKey, testKeyPass) + if err != nil { + t.Fatal(`Failed to load new keypair`) + } + + fca.updateX509KeyPair(cert, key) + _, key = fca.getX509KeyPair() + + if _, ok = key.(*ecdsa.PrivateKey); !ok { + t.Fatal(`file CA should have been updated with ecdsa key`) + } +} diff --git a/pkg/ca/fileca/load.go b/pkg/ca/fileca/load.go new file mode 100644 index 000000000..61b84c136 --- /dev/null +++ b/pkg/ca/fileca/load.go @@ -0,0 +1,104 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package fileca + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "errors" + + "go.step.sm/crypto/pemutil" +) + +func loadKeyPair(certPath, keyPath, keyPass string) (*x509.Certificate, crypto.Signer, error) { + + var ( + cert *x509.Certificate + err error + key crypto.Signer + ) + + // TODO: Load chain of certs (intermediates and root) instead of just one + // certificate. + cert, err = pemutil.ReadCertificate(certPath) + if err != nil { + return nil, nil, err + } + + { + opaqueKey, err := pemutil.Read(keyPath, pemutil.WithPassword([]byte(keyPass))) + if err != nil { + return nil, nil, err + } + + var ok bool + key, ok = opaqueKey.(crypto.Signer) + if !ok { + return nil, nil, errors.New(`fileca: loaded private key can't be used to sign`) + } + } + + if !valid(cert, key) { + return nil, nil, errors.New(`fileca: certificate public key and private key don't match`) + } + + if !cert.IsCA { + return nil, nil, errors.New(`fileca: certificate is not a CA`) + } + + return cert, key, nil +} + +func valid(cert *x509.Certificate, key crypto.Signer) bool { + if cert == nil || key == nil { + return false + } + + switch pub := cert.PublicKey.(type) { + case *rsa.PublicKey: + priv, ok := key.(*rsa.PrivateKey) + if !ok { + return false + } + if pub.N.Cmp(priv.N) != 0 { + return false + } + case *ecdsa.PublicKey: + priv, ok := key.(*ecdsa.PrivateKey) + if !ok { + return false + } + if pub.X.Cmp(priv.X) != 0 || pub.Y.Cmp(priv.Y) != 0 { + return false + } + case ed25519.PublicKey: + priv, ok := key.(ed25519.PrivateKey) + if !ok { + return false + } + if !bytes.Equal(priv.Public().(ed25519.PublicKey), pub) { + return false + } + default: + return false + } + + return true +} diff --git a/pkg/ca/fileca/load_test.go b/pkg/ca/fileca/load_test.go new file mode 100644 index 000000000..7da90e1e6 --- /dev/null +++ b/pkg/ca/fileca/load_test.go @@ -0,0 +1,56 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package fileca + +import ( + "fmt" + "testing" +) + +func TestValidLoadKeyPair(t *testing.T) { + keypairs := []string{ + "ecdsa", + "ed25519", + "rsa4096", + } + + for _, keypair := range keypairs { + keyPath := fmt.Sprintf("testdata/%s-key.pem", keypair) + certPath := fmt.Sprintf("testdata/%s-cert.pem", keypair) + + _, _, err := loadKeyPair(certPath, keyPath, testKeyPass) + if err != nil { + t.Errorf("Failed to load key pair of type %s", keypair) + } + } +} + +func TestInvalidLoadKeyPair(t *testing.T) { + keypairs := []string{ + "notca", + "mismatch", + } + + for _, keypair := range keypairs { + keyPath := fmt.Sprintf("testdata/%s-key.pem", keypair) + certPath := fmt.Sprintf("testdata/%s-cert.pem", keypair) + + _, _, err := loadKeyPair(certPath, keyPath, testKeyPass) + if err == nil { + t.Errorf("Expected invalid key pair of type %s to fail to load", keypair) + } + } +} diff --git a/pkg/ca/fileca/testdata/ecdsa-cert.pem b/pkg/ca/fileca/testdata/ecdsa-cert.pem new file mode 100644 index 000000000..476ac642f --- /dev/null +++ b/pkg/ca/fileca/testdata/ecdsa-cert.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBxzCCAU6gAwIBAgIUbQqE8rDPWDqJexmvpaeamgZe/HIwCgYIKoZIzj0EAwIw +EDEOMAwGA1UEAwwFZWNkc2EwIBcNMjExMjIxMTkxMzI2WhgPMjEyMTExMjcxOTEz +MjZaMBAxDjAMBgNVBAMMBWVjZHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEMV9i +0e3Ld1eQy9UXII5MOymw2IFBo288zuOMeH+7w0ejJlY0PFowY4rItKIhqRIWOqFA +luSaVC59sKsqjsiLdQQW2CV19eYFhVvYQS1S2QaROpFA5Zt8ALOACyp5s+6+o2cw +ZTAdBgNVHQ4EFgQU3QF9mKDrefmeiE3lqC46PSmhEOkwHwYDVR0jBBgwFoAU3QF9 +mKDrefmeiE3lqC46PSmhEOkwDwYDVR0TAQH/BAUwAwEB/zASBgNVHRMBAf8ECDAG +AQH/AgEBMAoGCCqGSM49BAMCA2cAMGQCMAQ/g18eRvqITDZEKdzf4bI4qKF/ZbVL +GTZ+2HHZYwDvsuHeznTl1Uq1stzmySi4owIwV1jCF8f4gikxT0XCF+u1CJlVYiZP +tyRnLdZaKl/seNUmBO0RRR72tsRd/X1QR3NK +-----END CERTIFICATE----- diff --git a/pkg/ca/fileca/testdata/ecdsa-key.pem b/pkg/ca/fileca/testdata/ecdsa-key.pem new file mode 100644 index 000000000..785f2a20f --- /dev/null +++ b/pkg/ca/fileca/testdata/ecdsa-key.pem @@ -0,0 +1,8 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIBEzBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIGCeFiJ2rs3wCAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECGZQSOBsvcjqBIHAGdU9wj136ikM +QUqJqDt5oSDKJAU2Yrv+pCRLz6VENGUvxFrPsn9fbjSt69fSZMm09mITfEg1eLO4 +9LDl7PW1Bza803IXKQnZ0xnRUkkY1GQfDtQEGCYFaojRpYNLmmSiHpeFrqAPz83K ++oXsRTwuRkDrABNpwTCEXYVmcHUmk9NqC6E2qjmYOyDx0ktA4HG62H3/cpGZleBs +l58oyOg658erxF1rnASN15lw9/1g0lWACsXsMgbkjDnY51LU71pR +-----END ENCRYPTED PRIVATE KEY----- diff --git a/pkg/ca/fileca/testdata/ed25519-cert.pem b/pkg/ca/fileca/testdata/ed25519-cert.pem new file mode 100644 index 000000000..03c292c9c --- /dev/null +++ b/pkg/ca/fileca/testdata/ed25519-cert.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBTzCCAQGgAwIBAgIUeObF4LopbObr0zVOX7BAZbvy4MswBQYDK2VwMBIxEDAO +BgNVBAMMB2VkMjU1MTkwIBcNMjExMjIxMTkxMzI2WhgPMjEyMTExMjcxOTEzMjZa +MBIxEDAOBgNVBAMMB2VkMjU1MTkwKjAFBgMrZXADIQBNNJP9Ys+Sx0Cx/c5pQNAF +cuECdESA0vB2IqXVAG5OiaNnMGUwHQYDVR0OBBYEFJEGm0OzRNsdBVLdDBCcx21i +nEySMB8GA1UdIwQYMBaAFJEGm0OzRNsdBVLdDBCcx21inEySMA8GA1UdEwEB/wQF +MAMBAf8wEgYDVR0TAQH/BAgwBgEB/wIBATAFBgMrZXADQQD6quk/tnnZpFgabR2Q +4WCweJfZ4NfrhMOVvAPdECW/P57NH0P2BUSOK+/DktOBFIjLUWG6ptRExHDcRsFm +WTsA +-----END CERTIFICATE----- diff --git a/pkg/ca/fileca/testdata/ed25519-key.pem b/pkg/ca/fileca/testdata/ed25519-key.pem new file mode 100644 index 000000000..5386d0801 --- /dev/null +++ b/pkg/ca/fileca/testdata/ed25519-key.pem @@ -0,0 +1,5 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGKME4GCSqGSIb3DQEFDTBBMCkGCSqGSIb3DQEFDDAcBAggWEDSFylYswICCAAw +DAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIaecFy/8IbAYEOHb3xUdAVad3ZcXk +dkwJjtPNP2t2PA/6ngVgfsx2dgqKBhjg9JXG98Yw2eeYqJsbZ4jrHAJK0l8E +-----END ENCRYPTED PRIVATE KEY----- diff --git a/pkg/ca/fileca/testdata/generate.sh b/pkg/ca/fileca/testdata/generate.sh new file mode 100755 index 000000000..34cb868a6 --- /dev/null +++ b/pkg/ca/fileca/testdata/generate.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# Copyright 2021 The Sigstore Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +password=password123 +duration=36500 # 100 years + +# ed25519 +openssl req -x509 \ + -newkey ed25519 \ + -sha256 \ + -keyout ed25519-key.pem \ + -out ed25519-cert.pem \ + -subj "/CN=ed25519" \ + -days $duration \ + -addext basicConstraints=critical,CA:TRUE,pathlen:1 \ + -passout pass:"$password" + +# ecdsa +openssl req -x509 \ + -newkey ec \ + -pkeyopt ec_paramgen_curve:secp384r1 \ + -sha256 \ + -keyout ecdsa-key.pem \ + -out ecdsa-cert.pem \ + -subj "/CN=ecdsa" \ + -days $duration \ + -addext basicConstraints=critical,CA:TRUE,pathlen:1 \ + -passout pass:"$password" + +# RSA 4096 +openssl req -x509 \ + -newkey rsa:4096 \ + -sha256 \ + -keyout rsa4096-key.pem \ + -out rsa4096-cert.pem \ + -subj "/CN=rsa4096" \ + -days $duration \ + -addext basicConstraints=critical,CA:TRUE,pathlen:1 \ + -passout pass:"$password" + +# mismatch cert (key doesn't match cert) +openssl req -x509 \ + -newkey ed25519 \ + -sha256 \ + -keyout mismatch-key.pem \ + -out mismatch-cert.pem \ + -subj "/CN=mismatch" \ + -days $duration \ + -addext basicConstraints=critical,CA:TRUE,pathlen:1 \ + -passout pass:"$password" + +# Mess up the keys +cp ed25519-key.pem mismatch-key.pem + +# Not a CA +openssl req -x509 \ + -newkey ed25519 \ + -sha256 \ + -keyout notca-key.pem \ + -out notca-cert.pem \ + -subj "/CN=notca" \ + -days $duration \ + -addext basicConstraints=critical,CA:FALSE,pathlen:1 \ + -passout pass:"$password" diff --git a/pkg/ca/fileca/testdata/mismatch-cert.pem b/pkg/ca/fileca/testdata/mismatch-cert.pem new file mode 100644 index 000000000..c49513a15 --- /dev/null +++ b/pkg/ca/fileca/testdata/mismatch-cert.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBUTCCAQOgAwIBAgIUfHFDsb53zUPiJ5/nlknv5eU3DpkwBQYDK2VwMBMxETAP +BgNVBAMMCG1pc21hdGNoMCAXDTIxMTIyMTE5MTMyN1oYDzIxMjExMTI3MTkxMzI3 +WjATMREwDwYDVQQDDAhtaXNtYXRjaDAqMAUGAytlcAMhAKTb9T96TtiHEC4cLs2y +gobJmI51zTGIKzjR+T+yMddLo2cwZTAdBgNVHQ4EFgQUM/1OrVpNmA8JvPwp47o4 +cS/NLdUwHwYDVR0jBBgwFoAUM/1OrVpNmA8JvPwp47o4cS/NLdUwDwYDVR0TAQH/ +BAUwAwEB/zASBgNVHRMBAf8ECDAGAQH/AgEBMAUGAytlcANBAHZ7HB9H/qh1xqC+ +ih5XmVYPBbec8qOez3i5JSYy+05C6cjsMdBookbAY5qpUtaYeBwcJ9SW7JeP18R8 +JPQ/rg4= +-----END CERTIFICATE----- diff --git a/pkg/ca/fileca/testdata/mismatch-key.pem b/pkg/ca/fileca/testdata/mismatch-key.pem new file mode 100644 index 000000000..5386d0801 --- /dev/null +++ b/pkg/ca/fileca/testdata/mismatch-key.pem @@ -0,0 +1,5 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGKME4GCSqGSIb3DQEFDTBBMCkGCSqGSIb3DQEFDDAcBAggWEDSFylYswICCAAw +DAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIaecFy/8IbAYEOHb3xUdAVad3ZcXk +dkwJjtPNP2t2PA/6ngVgfsx2dgqKBhjg9JXG98Yw2eeYqJsbZ4jrHAJK0l8E +-----END ENCRYPTED PRIVATE KEY----- diff --git a/pkg/ca/fileca/testdata/notca-cert.pem b/pkg/ca/fileca/testdata/notca-cert.pem new file mode 100644 index 000000000..ee83bafe5 --- /dev/null +++ b/pkg/ca/fileca/testdata/notca-cert.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBRzCB+qADAgECAhQDX4GfTfK6ck56Yn7yT8hrJn1SyDAFBgMrZXAwEDEOMAwG +A1UEAwwFbm90Y2EwIBcNMjExMjIxMTkxMzI3WhgPMjEyMTExMjcxOTEzMjdaMBAx +DjAMBgNVBAMMBW5vdGNhMCowBQYDK2VwAyEAH/poanRWlO3G2v7TojRWEpmJLVLX +0zxmyTA1EbOQuTGjZDBiMB0GA1UdDgQWBBS6S5FO2LmyK9GjNdWt4lBUeAq0LjAf +BgNVHSMEGDAWgBS6S5FO2LmyK9GjNdWt4lBUeAq0LjAPBgNVHRMBAf8EBTADAQH/ +MA8GA1UdEwEB/wQFMAMCAQEwBQYDK2VwA0EABvr4fVmVtWGtYVxPEhANWPmf3oFS +ukFOmBetWsTYmrH8HWx7P73MCPo9aXkkla9s5p/oITqD7h2RvF9nNimqAg== +-----END CERTIFICATE----- diff --git a/pkg/ca/fileca/testdata/notca-key.pem b/pkg/ca/fileca/testdata/notca-key.pem new file mode 100644 index 000000000..b298c768a --- /dev/null +++ b/pkg/ca/fileca/testdata/notca-key.pem @@ -0,0 +1,5 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGKME4GCSqGSIb3DQEFDTBBMCkGCSqGSIb3DQEFDDAcBAjwTlExAYUMxwICCAAw +DAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIHg80StF7iN8EOLK5NCgrUFxs7p9Z +oxcp1G7Bh2qswvz6eGef4yL6boxGEuCg4zcUzec+0lFJ8nyOw3bb1Jwwi5JO +-----END ENCRYPTED PRIVATE KEY----- diff --git a/pkg/ca/fileca/testdata/rsa4096-cert.pem b/pkg/ca/fileca/testdata/rsa4096-cert.pem new file mode 100644 index 000000000..6427e2e67 --- /dev/null +++ b/pkg/ca/fileca/testdata/rsa4096-cert.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFGzCCAwOgAwIBAgIUNNrNtETw6r3oJIDH+quQ01QYxlQwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHcnNhNDA5NjAgFw0yMTEyMjExOTEzMjdaGA8yMTIxMTEy +NzE5MTMyN1owEjEQMA4GA1UEAwwHcnNhNDA5NjCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAM9mSsC1QpNg9i4ZRd9tNZ3bD50CNpDdmy5+Axbhvd9PKgX9 +Krn0YubMbipyWeqlzV8PKTAplnaXSOpDlVfCiTBACw68YxIa7Hp1Qan/UGd5l5qQ +NID35Lyfy5UqiRgxtEJp15W59RepxJlc4ks2WShQVmpRjs4uUtg84fOhzZJt/HST +nbpoSkC8sJJIPRVnlaGMGjTKuLurVaSFXNdVbnSLOwsajgaWS/Sc3hmg2NGEgLPa +u9qhaJ2ckCg7J8yiA81Www5KWiz6PeREaeSdJquN+RT1FY7JRYZ1BAaGnsndP8OA +rgpGgL+r8Dcy/cy28HcCEyQnDirBcONxoP7Wh6PudgJAJwaVxYvFm8adm8puvbWK +NFtBqd9Sl6t8clBf63Y/WQMzWvIBBxS1bd2152VQsCrDEkACCGtVyoOlLcUrUFOQ +GXChRFWfbvkldb4vxW7zKKoNXS+B6lhKH5AbxoY//16WAAkT+capnUQZ0X0323rA +zF/HcKAVdDDr/oh+WxU42fEhmzbbvZYL1MECfwfju8TvPNIk2gbO8s7nXzvTqO4x +TBpElSWDdOlI16h1FIHA0rkLYhX+BiAuOHVJhOO49GBF7xmWsdo4qDXG6OtW+mGJ +5YQyIlgjzzEKqX9/MC+24dNZCAVoLqAhuRV3UBf7LfBTxHcc2Hi9QxuUEmGdAgMB +AAGjZzBlMB0GA1UdDgQWBBSrJb6lgTBKm36AWKx12MRM2Nd6HTAfBgNVHSMEGDAW +gBSrJb6lgTBKm36AWKx12MRM2Nd6HTAPBgNVHRMBAf8EBTADAQH/MBIGA1UdEwEB +/wQIMAYBAf8CAQEwDQYJKoZIhvcNAQELBQADggIBAADYzYrv05CHLjKWZRj6+ufW +V5gKZr4KSjAgES2EpBBon1sY1HHulgDDE6yNgPJgkOl8S5g+BYBW1bIldhOLhe0A +MN26e4gW1/CSiqEUYqZHs8qdmqNaNCvODgJtZS+UIaqEUOFkHhBd73RK5RHq+H9J +hbHqSn1P8/UPvRv/lWRPFUEP7o6IrDHC8NcdV6Hy4kIv0VwiJQdI19h1IslAcGwq +Z/R6Cuuyqm5YUIfGTZSMRmx4V4FsFyoL6+U6Ujoa7vGSUDChqZYZQdbNSLSBPrv8 +7ejCkh+BAHz05nKABAIWJnSdKs15JcM3wAIAKW+W0QIxh4qn/n4ochQh077nkRdv +pWEEh3bvCIJRgnpPufdGF5GIZQrCUwiYPGRUDP1IzVM/NRRtFMhfzkl3brXoMtrv +qb3A+AWP9xw4aC+AHb+fK7fZB8Ag54lhn/2PH/vxrmMhhTovLF7ccE5f2sZuiV9i +Ak/bfqfmYIckxIW/4NwUKn9mlWLB4NNuNnjfcdADzxHS6vqMAY56KOdFYLq3GW4y +xK1NdzEk19Np6rtBGImydBBUqx4auN9b0hm/+zooRkuTmJbg8cUmnatSKkHC24ZR +zqi0j2yJfkstNCn+K8Y+6cvU6WeoFses+n408JVqNVmw6jnd5u76yxXNdBqF+2sw +HQHYjVyVGoWRiL5+3VwI +-----END CERTIFICATE----- diff --git a/pkg/ca/fileca/testdata/rsa4096-key.pem b/pkg/ca/fileca/testdata/rsa4096-key.pem new file mode 100644 index 000000000..2c8187459 --- /dev/null +++ b/pkg/ca/fileca/testdata/rsa4096-key.pem @@ -0,0 +1,54 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +SltOTT/4oMIVEu3INXhiAw== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/pkg/ca/fileca/watch.go b/pkg/ca/fileca/watch.go new file mode 100644 index 000000000..b509bf990 --- /dev/null +++ b/pkg/ca/fileca/watch.go @@ -0,0 +1,39 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package fileca + +import ( + "crypto" + "crypto/x509" + + "github.com/fsnotify/fsnotify" +) + +func ioWatch(certPath, keyPath, keyPass string, watcher *fsnotify.Watcher, callback func(*x509.Certificate, crypto.Signer)) { + for event := range watcher.Events { + if event.Op&fsnotify.Write == fsnotify.Write { + cert, key, err := loadKeyPair(certPath, keyPath, keyPass) + if err != nil { + // Don't sweat it if this errors out. One file might + // have updated and the other isn't causing a key-pair + // mismatch + continue + } + + callback(cert, key) + } + } +} diff --git a/pkg/ca/fileca/watch_test.go b/pkg/ca/fileca/watch_test.go new file mode 100644 index 000000000..76d11442b --- /dev/null +++ b/pkg/ca/fileca/watch_test.go @@ -0,0 +1,111 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package fileca + +import ( + "crypto" + "crypto/ecdsa" + "crypto/x509" + "os" + "path/filepath" + "testing" + "time" + + "github.com/fsnotify/fsnotify" +) + +func cp(src, dst string) error { + data, err := os.ReadFile(src) + if err != nil { + return err + } + return os.WriteFile(dst, data, 0644) +} + +func TestIOWatch(t *testing.T) { + dir, err := os.MkdirTemp("", "fileca") + if err != nil { + t.Fatal(`Failed to create temp dir`) + } + // defer os.RemoveAll(dir) + + keyPath := filepath.Join(dir, "key.pem") + certPath := filepath.Join(dir, "cert.pem") + + // Copy initial certs into place + err = cp("testdata/ed25519-key.pem", keyPath) + if err != nil { + t.Fatal(`Couldn't copy test data to temp file`) + } + err = cp("testdata/ed25519-cert.pem", certPath) + if err != nil { + t.Fatal(`Couldn't copy test data to temp file`) + } + + // Set up callback trap + var received []struct { + cert *x509.Certificate + key crypto.Signer + } + callback := func(cert *x509.Certificate, key crypto.Signer) { + received = append(received, struct { + cert *x509.Certificate + key crypto.Signer + }{cert, key}) + } + + // Set up watcher + watcher, err := fsnotify.NewWatcher() + if err != nil { + t.Fatal(err) + } + + err = watcher.Add(certPath) + if err != nil { + t.Fatal(err) + } + err = watcher.Add(keyPath) + if err != nil { + t.Fatal(err) + } + + go ioWatch(certPath, keyPath, testKeyPass, watcher, callback) + + // Change the certs in place + err = cp("testdata/ecdsa-key.pem", keyPath) + if err != nil { + t.Fatal(`Couldn't copy test data to temp file`) + } + err = cp("testdata/ecdsa-cert.pem", certPath) + if err != nil { + t.Fatal(`Couldn't copy test data to temp file`) + } + + // Sleep for a bit to make sure that iowatch thread + // does its thing. + // TODO: This is hacky. Find a better way + time.Sleep(1 * time.Second) + + // Test that we noticed the update and loaded the new + // certificate + if len(received) == 0 { + t.Error("iowatcher should have seen at least 1 update") + } + + if _, ok := received[0].key.(*ecdsa.PrivateKey); !ok { + t.Error("Should have loaded an ecdsa private key on update") + } +}