Skip to content

Commit

Permalink
Add file backed certificate authority (sigstore#280)
Browse files Browse the repository at this point in the history
Loads private key and certificate from disk. Optionally watches
for file changes and loads updated key pair.

Signed-off-by: Nathan Smith <nathan@nfsmith.ca>
  • Loading branch information
nsmith5 authored Dec 29, 2021
1 parent 754b833 commit f2e24d2
Show file tree
Hide file tree
Showing 20 changed files with 759 additions and 1 deletion.
24 changes: 23 additions & 1 deletion cmd/app/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -42,14 +43,18 @@ 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/<project>/locations/<location>/<name> (only used with --ca googleca)")
cmd.Flags().String("gcp_private_ca_version", "v1", "private ca version: [v1|v1beta1] (only used with --ca googleca)")
cmd.Flags().String("hsm-caroot-id", "", "HSM ID for Root CA (only used with --ca pkcs11ca)")
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")

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,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=
Expand Down Expand Up @@ -575,6 +578,8 @@ github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaak
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
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=
Expand Down Expand Up @@ -817,6 +822,7 @@ github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvW
github.com/secure-systems-lab/go-securesystemslib v0.2.0/go.mod h1:eIjBmIP8LD2MLBL/DkQWayLiz006Q4p+hCu79rvWleY=
github.com/secure-systems-lab/go-securesystemslib v0.3.0/go.mod h1:o8hhjkbNl2gOamKUA/eNW3xUrntHT9L4W89W1nfj43U=
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.1.0 h1:k7skcw5UW8whU9GiFONi3Vp7AkjaiR2dh3w7BAQlIeA=
github.com/sigstore/sigstore v1.1.0/go.mod h1:gDpcHw4VwpoL5C6N1Ud1YtBsc+ikRDwDelDlWRyYoE8=
Expand All @@ -829,6 +835,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=
Expand Down Expand Up @@ -943,6 +951,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=
Expand Down Expand Up @@ -971,12 +981,14 @@ 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-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8=
Expand Down Expand Up @@ -1072,6 +1084,7 @@ 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-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY=
Expand Down Expand Up @@ -1202,6 +1215,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=
Expand Down
110 changes: 110 additions & 0 deletions pkg/ca/fileca/fileca.go
Original file line number Diff line number Diff line change
@@ -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
}
76 changes: 76 additions & 0 deletions pkg/ca/fileca/fileca_test.go
Original file line number Diff line number Diff line change
@@ -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`)
}
}
Loading

0 comments on commit f2e24d2

Please sign in to comment.