diff --git a/depot/mongodb/mongodb.go b/depot/mongodb/mongodb.go new file mode 100644 index 0000000..432a7b8 --- /dev/null +++ b/depot/mongodb/mongodb.go @@ -0,0 +1,289 @@ +package mongodb + +import ( + "context" + "crypto/rsa" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "math/big" + "strconv" + "time" + "unicode/utf8" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type MongoDBStorage struct { + MongoClient *mongo.Client + CACollection *mongo.Collection + IssuedCertsCollection *mongo.Collection + SerialNumberCollection *mongo.Collection +} + +// TODO - Enable configuration of these names +const ( + databaseName = "scep" + + caStoreName = "ca_store" + issuedCertStoreName = "issued_certificates_store" + serialNumberStoreName = "serial_number_store" +) + +func New(ctx context.Context, uri string, username string, password string) (*MongoDBStorage, error) { + var err error + storage := &MongoDBStorage{} + + mongoOpts := options.Client().ApplyURI(uri) + mongoOpts.SetAuth(options.Credential{Username: username, Password: password}) + + storage.MongoClient, err = mongo.NewClient(mongoOpts) + if err != nil { + return nil, err + } + + err = storage.MongoClient.Connect(ctx) + if err != nil { + return nil, err + } + + storage.CACollection = storage.MongoClient.Database(databaseName).Collection(caStoreName) + storage.IssuedCertsCollection = storage.MongoClient.Database(databaseName).Collection(issuedCertStoreName) + storage.SerialNumberCollection = storage.MongoClient.Database(databaseName).Collection(serialNumberStoreName) + + return storage, nil +} + +type CAEntry struct { + Certificates []string `bson:"certificates,omitempty"` + PrivateKey string `bson:"private_key,omitempty"` +} + +func (m MongoDBStorage) SeedCA(certs []string, key string) error { + + upsert := true + filter := bson.M{} + update := bson.M{ + "$set": CAEntry{ + Certificates: certs, + PrivateKey: key, + }, + } + + _, err := m.CACollection.UpdateOne(context.TODO(), filter, update, options.Update().SetUpsert(upsert)) + + return err +} + +func (m *MongoDBStorage) CA(pass []byte) ([]*x509.Certificate, *rsa.PrivateKey, error) { + certs := []*x509.Certificate{} + + latestSort := bson.M{ + "$natural": -1, + } + filter := bson.M{} + + res := CAEntry{} + err := m.CACollection.FindOne(context.TODO(), filter, options.FindOne().SetSort(latestSort)).Decode(&res) + if err != nil { + return certs, nil, err + } + + for _, v := range res.Certificates { + pemBlock, _ := pem.Decode([]byte(v)) + if pemBlock == nil { + return certs, nil, errors.New("PEM decode failed") + } + if pemBlock.Type != "CERTIFICATE" { + return certs, nil, errors.New("unmatched type or headers") + } + cert, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + return certs, nil, err + } + + certs = append(certs, cert) + } + + pemBlock, _ := pem.Decode([]byte(res.PrivateKey)) + if pemBlock == nil { + return certs, nil, errors.New("PEM decode failed") + } + if pemBlock.Type != "RSA PRIVATE KEY" { + return certs, nil, errors.New("unmatched type or headers") + } + + privateKey, err := x509.ParsePKCS1PrivateKey([]byte(pemBlock.Bytes)) + if err != nil { + return certs, nil, err + } + + return certs, privateKey, nil +} + +type IssuedCertificateStatus string + +const ( + ValidIssuedCertificateStatus IssuedCertificateStatus = "Valid" + RevokedIssuedCertificateStatus IssuedCertificateStatus = "Revoked" +) + +type IssuedCertificateEntry struct { + CommonName string `bson:"common_name,omitempty"` + Certificate string `bson:"certificate,omitempty"` + SerialHex string `bson:"serial_hex,omitempty"` + Status IssuedCertificateStatus `bson:"status,omitempty"` + IssueTimeStamp string `bson:"issue_timestamp,omitempty"` + RevocationTimeStamp string `bson:"revocation_timestamp,omitempty"` +} + +func (m MongoDBStorage) Put(name string, crt *x509.Certificate) error { + if crt == nil { + return errors.New("crt is nil") + } + if crt.Raw == nil { + return errors.New("data is nil") + } + if crt.SerialNumber == nil { + return errors.New("serial number is nil") + } + + serialHex := fmt.Sprintf("%X", crt.SerialNumber) + if len(serialHex)%2 == 1 { + serialHex = fmt.Sprintf("0%s", serialHex) + } + + certEntry := IssuedCertificateEntry{ + Certificate: string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: crt.Raw, + })), + CommonName: certName(crt), + SerialHex: serialHex, + Status: ValidIssuedCertificateStatus, + IssueTimeStamp: strconv.FormatInt(crt.NotBefore.Unix(), 10), + } + + _, err := m.HasCN(certName(crt), 0, crt, true) + if err != nil { + return err + } + + _, err = m.IssuedCertsCollection.InsertOne(context.TODO(), certEntry) + if err != nil { + return err + } + + return m.incrementSerial(crt.SerialNumber) +} + +type SerialNumberEntry struct { + CurrentMaxSerialHex string `bson:"current_max_serial,omitempty"` +} + +func (m MongoDBStorage) incrementSerial(s *big.Int) error { + serialHex := fmt.Sprintf("%X", s.Add(s, big.NewInt(1))) + if len(serialHex)%2 == 1 { + serialHex = fmt.Sprintf("0%s", serialHex) + } + + upsert := true + filter := bson.M{} + update := bson.M{ + "$set": SerialNumberEntry{ + CurrentMaxSerialHex: serialHex, + }, + } + _, err := m.SerialNumberCollection.UpdateOne(context.TODO(), filter, update, options.Update().SetUpsert(upsert)) + + return err +} + +func (m MongoDBStorage) Serial() (*big.Int, error) { + filter := bson.M{} + s := big.NewInt(2) + + res := SerialNumberEntry{} + err := m.SerialNumberCollection.FindOne(context.TODO(), filter).Decode(&res) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return s, nil + } + return nil, err + } + + s, ok := s.SetString(res.CurrentMaxSerialHex, 16) + if !ok { + return nil, errors.New("could not convert " + res.CurrentMaxSerialHex + " to serial number") + } + + return s, nil +} + +func (m MongoDBStorage) HasCN(cn string, allowTime int, cert *x509.Certificate, revokeOldCertificate bool) (bool, error) { + // TODO - implement allowTime + + filter := bson.M{ + "$or": bson.A{ + bson.M{ + "certificate": string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: cert.Raw, + })), + }, + bson.M{ + "common_name": utf8Check(cn), + }, + }, + } + res := IssuedCertificateEntry{} + err := m.IssuedCertsCollection.FindOne(context.TODO(), filter).Decode(&res) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return false, nil + } + return false, err + } + if revokeOldCertificate { + upsert := true + update := bson.M{ + "$set": IssuedCertificateEntry{ + Certificate: string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Headers: nil, + Bytes: cert.Raw, + })), + Status: RevokedIssuedCertificateStatus, + RevocationTimeStamp: strconv.FormatInt(time.Now().Unix(), 10), + }, + } + + _, err := m.IssuedCertsCollection.UpdateOne(context.TODO(), filter, update, options.Update().SetUpsert(upsert)) + if err != nil { + return true, err + } + } + + return true, nil +} + +func certName(crt *x509.Certificate) string { + if crt.Subject.CommonName != "" { + return crt.Subject.CommonName + } + return utf8Check(string(crt.Signature)) +} + +func utf8Check(input string) string { + if utf8.Valid([]byte(input)) { + return input + } + + return hex.EncodeToString([]byte(input)) +} diff --git a/go.mod b/go.mod index 0d81571..0f4d8fc 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,7 @@ require ( github.com/gorilla/mux v1.4.0 github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect - github.com/pkg/errors v0.8.0 + github.com/pkg/errors v0.9.1 + go.mongodb.org/mongo-driver v1.10.3 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 - golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 // indirect - golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5 // indirect ) diff --git a/go.sum b/go.sum index 80502af..3d8ad30 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,70 @@ github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-kit/kit v0.4.0 h1:KeVK+Emj3c3S4eRztFuzbFYb2BAgf2jmwDwyXEri7Lo= github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-stack/stack v1.6.0 h1:MmJCxYVKTJ0SplGKqFVX3SBnmaUhODHZrrFF6jMbpZk= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f h1:9oNbS1z4rVpbnkHBdPZU4jo9bSmrLpII768arSyMFgk= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.4.0 h1:N6R8isjoRv7IcVVlf0cTBbo0UDc9V6ZXWEm0HQoQmLo= github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda h1:5ikpG9mYCMFiZX0nkxoV6aU2IpCHPdws3gCNgdZeEV0= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +go.mongodb.org/mongo-driver v1.10.3 h1:XDQEvmh6z1EUsXuIkXE9TaVeqHw6SwS1uf93jFs0HBA= +go.mongodb.org/mongo-driver v1.10.3/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7 h1:1Pw+ZX4dmGORIwGkTwnUr7RFuMhfpCYHXRZNF04XPYs= golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5 h1:NAjcSWsnFBcOQGn/lxvHouhL7iPC53X8+znVzzQkAEg= golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=