From 59f892ff7d01b5fbe3c1dc38807e83517bef44f1 Mon Sep 17 00:00:00 2001 From: Stanislav Kem Date: Sun, 3 Sep 2023 16:45:41 +0200 Subject: [PATCH] temp --- internal/compliantStorage/index.go | 107 ++++++++ internal/compliantStorage/index_test.go | 228 ++++++++++++++++++ internal/compliantStorage/storage.go | 123 ++++++++++ internal/compliantStorage/storage_test.go | 77 ++++++ internal/compliantStorage/test/.gitignore | 1 + .../test/dir_keystorage/ca.crt | 1 + .../dir_keystorage/certs_by_serial/42.crt | 1 + .../dir_keystorage/certs_by_serial/9A.crt | 1 + .../test/dir_keystorage/issued/good_cert.crt | 1 + .../test/dir_keystorage/private/ca.key | 1 + .../test/dir_keystorage/private/good_cert.key | 1 + 11 files changed, 542 insertions(+) create mode 100644 internal/compliantStorage/index.go create mode 100644 internal/compliantStorage/index_test.go create mode 100644 internal/compliantStorage/storage.go create mode 100644 internal/compliantStorage/storage_test.go create mode 100644 internal/compliantStorage/test/.gitignore create mode 100644 internal/compliantStorage/test/dir_keystorage/ca.crt create mode 100644 internal/compliantStorage/test/dir_keystorage/certs_by_serial/42.crt create mode 100644 internal/compliantStorage/test/dir_keystorage/certs_by_serial/9A.crt create mode 100644 internal/compliantStorage/test/dir_keystorage/issued/good_cert.crt create mode 100644 internal/compliantStorage/test/dir_keystorage/private/ca.key create mode 100644 internal/compliantStorage/test/dir_keystorage/private/good_cert.key diff --git a/internal/compliantStorage/index.go b/internal/compliantStorage/index.go new file mode 100644 index 0000000..ce80a8e --- /dev/null +++ b/internal/compliantStorage/index.go @@ -0,0 +1,107 @@ +package compliantStorage + +import ( + "bufio" + "fmt" + "io" + "strings" + "time" + "unicode/utf8" +) + +const dateLayout = "060102150405Z" + +type Index struct { + records []Record +} + +//https://pki-tutorial.readthedocs.io/en/latest/cadb.html +//https://www.openssl.org/docs/man1.0.2/man1/openssl-ca.html + +type Record struct { + statusFlag rune //Certificate status flag (V=valid, R=revoked, E=expired) + expirationDate *time.Time //Certificate expiration date + revocationDate *time.Time //Certificate revocation date, empty if not revoked + revocationReason string //Certificate revocation reason if presented + certSerialHex string //Certificate serial number in hex + certFileName string //Certificate filename or literal string ‘unknown’ + certDN string //Certificate distinguished name +} + +func (r Record) String() string { + var revString string + if r.revocationDate != nil { + revString = r.revocationDate.Format(dateLayout) + if r.revocationReason != "" { + revString = fmt.Sprintf("%v,%v", r.revocationDate.Format(dateLayout), r.revocationReason) + } + } + + return fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v", string(r.statusFlag), r.expirationDate.Format(dateLayout), revString, + r.certSerialHex, r.certFileName, r.certDN) +} + +func (i *Index) Len() int { + return len(i.records) +} + +func (i *Index) Decode(r io.Reader) error { + br := bufio.NewReader(r) + for { + line, _, err := br.ReadLine() + if err != nil { + if err == io.EOF { + break + } + return fmt.Errorf("couldn't read line from index: %w", err) + } + + record, err := parseLine(line) + if err != nil { + return fmt.Errorf("couldn't parse record %s from index: %w", line, err) + } + i.records = append(i.records, *record) + } + return nil +} + +func parseLine(line []byte) (*Record, error) { + split := strings.Split(string(line), "\t") + if len(split) != 6 { + return nil, fmt.Errorf("wrong records format: %v", string(line)) + } + rec := new(Record) + rec.statusFlag, _ = utf8.DecodeRuneInString(split[0]) + parsedDate, err := time.Parse(dateLayout, split[1]) + if err != nil { + return nil, fmt.Errorf("couldn't parse date from %v : %w", split[1], err) + } + rec.expirationDate = &parsedDate + if split[2] != "" { + revoc := strings.Split(split[2], ",") + parsedDate, err = time.Parse(dateLayout, revoc[0]) + if err != nil { + return nil, fmt.Errorf("couldn't parse date from %v : %w", split[2], err) + } + rec.revocationDate = &parsedDate + if len(revoc) == 2 { + rec.revocationReason = revoc[1] + } + } + + rec.certSerialHex = split[3] + rec.certFileName = split[4] + rec.certDN = split[5] + + return rec, nil +} + +func (i *Index) Encode(w io.Writer) error { + for _, r := range i.records { + _, err := w.Write([]byte(r.String())) + if err != nil { + return fmt.Errorf("couldn't write encoded index: %w", err) + } + } + return nil +} diff --git a/internal/compliantStorage/index_test.go b/internal/compliantStorage/index_test.go new file mode 100644 index 0000000..c4f2257 --- /dev/null +++ b/internal/compliantStorage/index_test.go @@ -0,0 +1,228 @@ +package compliantStorage + +import ( + "bytes" + "fmt" + "github.com/stretchr/testify/assert" + "io" + "strings" + "testing" + "time" +) + +type fakeReader struct { +} + +func (f fakeReader) Read(p []byte) (n int, err error) { + return 0, io.ErrClosedPipe +} + +type fakeWriter struct { +} + +func (f fakeWriter) Write(p []byte) (n int, err error) { + return 0, io.ErrClosedPipe +} + +func TestIndex_Decode(t *testing.T) { + type args struct { + r io.Reader + } + tests := []struct { + name string + i *Index + args args + wantErr bool + funcV func(index *Index, t *testing.T) bool + }{ + { + name: "mt", + i: new(Index), + args: args{ + r: strings.NewReader(""), + }, + wantErr: false, + funcV: func(index *Index, t *testing.T) bool { + return true + }, + }, + { + name: "oneline", + i: new(Index), + args: args{ + r: strings.NewReader("V\t240830094439Z\t\tA687897D709E441C85A0B2EF9C02C80D\tunknown\t/CN=test1"), + }, + wantErr: false, + funcV: func(index *Index, t *testing.T) bool { + return assert.Equal(t, 1, index.Len()) + }, + }, + { + name: "multiline", + i: new(Index), + args: args{ + r: strings.NewReader("V\t240830094439Z\t\tA687897D709E441C85A0B2EF9C02C80D\tunknown\t/CN=test1\nR\t240831190001Z\t220529195720Z\tB2B9D80AE52F4E739FB1A4D696417D30\tunknown\t/CN=client\nR\t240831190253Z\t220618182903Z,keyCompromise\tCBF3370F0AB460655DF6FA60FFCA421F\tunknown\t/CN=client2\nV\t240831190819Z\t\tC3B12A550081FB41EF0F67C3678EA4BC\tunknown\t/CN=server\n"), + }, + wantErr: false, + funcV: func(index *Index, t *testing.T) bool { + return assert.Equal(t, 4, index.Len()) + }, + }, + { + name: "fakereader", + i: new(Index), + args: args{ + r: new(fakeReader), + }, + wantErr: true, + funcV: func(index *Index, t *testing.T) bool { + return true + }, + }, + { + name: "brokenrecord", + i: new(Index), + args: args{ + r: strings.NewReader("V\t240830094439Z\t\tA687897D709E441C85A0B2EF9C02C80D\tunknown"), + }, + wantErr: true, + funcV: func(index *Index, t *testing.T) bool { + return true + }, + }, + { + name: "wrong exp date", + i: new(Index), + args: args{ + r: strings.NewReader("R\t241331190253Z\t220630182903Z,keyCompromise\tCBF3370F0AB460655DF6FA60FFCA421F\tunknown\t/CN=client2"), + }, + wantErr: true, + funcV: func(index *Index, t *testing.T) bool { + return true + }, + }, + { + name: "wrong revoc date", + i: new(Index), + args: args{ + r: strings.NewReader("R\t240831190253Z\t220632182903Z,keyCompromise\tCBF3370F0AB460655DF6FA60FFCA421F\tunknown\t/CN=client2"), + }, + wantErr: true, + funcV: func(index *Index, t *testing.T) bool { + return true + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.i.Decode(tt.args.r) + if (err != nil) != tt.wantErr { + t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) + } else { + tt.funcV(tt.i, t) + } + }) + } +} + +func TestIndex_Encode(t *testing.T) { + dt := time.Date(2020, 01, 06, 12, 24, 24, 00, time.UTC) + type fields struct { + records []Record + } + tests := []struct { + name string + fields fields + wantW string + wantErr assert.ErrorAssertionFunc + writer io.Writer + }{ + { + name: "mt", + fields: fields{}, + wantW: "", + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return true + }, + writer: nil, + }, + { + name: "good", + fields: fields{ + records: []Record{ + { + statusFlag: 86, + expirationDate: &dt, + revocationDate: nil, + revocationReason: "", + certSerialHex: "AB12", + certFileName: "unknown", + certDN: "/CN=client3", + }, + { + statusFlag: 86, + expirationDate: &dt, + revocationDate: &dt, + revocationReason: "keyCompromise", + certSerialHex: "AB12", + certFileName: "unknown", + certDN: "/CN=client3", + }, + }, + }, + wantW: "V\t200106122424Z\t\tAB12\tunknown\t/CN=client3V\t200106122424Z\t200106122424Z,keyCompromise\tAB12\tunknown\t/CN=client3", + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return assert.NoError(t, err) + }, + }, + { + name: "good", + fields: fields{ + records: []Record{ + { + statusFlag: 86, + expirationDate: &dt, + revocationDate: nil, + revocationReason: "", + certSerialHex: "AB12", + certFileName: "unknown", + certDN: "/CN=client3", + }, + { + statusFlag: 86, + expirationDate: &dt, + revocationDate: &dt, + revocationReason: "keyCompromise", + certSerialHex: "AB12", + certFileName: "unknown", + certDN: "/CN=client3", + }, + }, + }, + wantW: "", + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return assert.Error(t, err) + }, + writer: fakeWriter{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var err error + i := &Index{ + records: tt.fields.records, + } + w := &bytes.Buffer{} + if tt.writer != nil { + err = i.Encode(tt.writer) + } else { + err = i.Encode(w) + } + + if !tt.wantErr(t, err, fmt.Sprintf("Encode(%v)", w)) { + return + } + assert.Equalf(t, tt.wantW, w.String(), "Encode(%v)", w) + }) + } +} diff --git a/internal/compliantStorage/storage.go b/internal/compliantStorage/storage.go new file mode 100644 index 0000000..82cd5bf --- /dev/null +++ b/internal/compliantStorage/storage.go @@ -0,0 +1,123 @@ +package compliantStorage + +import ( + "bytes" + "fmt" + "github.com/kemsta/go-easyrsa/internal/utils" + "github.com/kemsta/go-easyrsa/pkg/pair" + "math/big" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +const ( + LockPeriod = time.Millisecond * 100 + LockTimeout = time.Second * 10 + CertFileExtension = ".crt" // certificate file extension +) + +// DirKeyStorage is an easyrsa v3 compliant storage. It can be used as a drop replacement on the PKI created with easyrsa v3 +type DirKeyStorage struct { + pkidir string +} + +func NewDirKeyStorage(pkidir string) *DirKeyStorage { + return &DirKeyStorage{pkidir: pkidir} +} + +func (s *DirKeyStorage) initDir() error { + var once sync.Once + var err error + once.Do(func() { + for _, dir := range []string{ + s.pkidir, + filepath.Join(s.pkidir, "certs_by_serial"), + filepath.Join(s.pkidir, "issued"), + filepath.Join(s.pkidir, "private"), + filepath.Join(s.pkidir, "reqs"), + filepath.Join(s.pkidir, "revoked"), + filepath.Join(s.pkidir, "revoked", "certs_by_serial"), + filepath.Join(s.pkidir, "revoked", "private_by_serial"), + filepath.Join(s.pkidir, "revoked", "reqs_by_serial"), + } { + err = os.MkdirAll(dir, 0750) + } + }) + return err +} + +func (s *DirKeyStorage) Put(pair *pair.X509Pair) error { + err := s.initDir() + if err != nil { + return fmt.Errorf("can`t make pki paths in %s: %w", s.pkidir, err) + } + + var certPath, keyPath, serialPath string + + _, err = os.Stat(certPath) + if err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("%s already exists. Cancel writing to avoid overwriting this file", certPath) + } + } + + certPath = filepath.Join(s.pkidir, "issued", fmt.Sprintf("%s.crt", pair.CN())) + keyPath = filepath.Join(s.pkidir, "private", fmt.Sprintf("%s.key", pair.CN())) + serialPath = filepath.Join(s.pkidir, "certs_by_serial", fmt.Sprintf("%s.crt", strings.ToUpper(pair.Serial().Text(16)))) + if pair.CN() == "ca" { + certPath = filepath.Join(s.pkidir, "ca.crt") + } + if err := utils.WriteFileAtomic(certPath, bytes.NewReader(pair.CertPemBytes()), 0644); err != nil { + return fmt.Errorf("can`t write cert %v: %w", certPath, err) + } + if err := utils.WriteFileAtomic(serialPath, bytes.NewReader(pair.CertPemBytes()), 0644); err != nil { + return fmt.Errorf("can`t write cert %v: %w", certPath, err) + } + + if err := utils.WriteFileAtomic(keyPath, bytes.NewReader(pair.KeyPemBytes()), 0644); err != nil { + return fmt.Errorf("can`t write key %v: %w", keyPath, err) + } + return nil +} + +func (s *DirKeyStorage) GetByCN(cn string) ([]*pair.X509Pair, error) { + res := make([]*pair.X509Pair, 0) + certBytes, err := os.ReadFile(filepath.Join(s.pkidir, "issued", fmt.Sprintf("%s.crt", cn))) + if err != nil { + return nil, fmt.Errorf("can't read cert by cn %s: %w", cn, err) + } + keyBytes, err := os.ReadFile(filepath.Join(s.pkidir, "private", fmt.Sprintf("%s.key", cn))) + if err != nil { + return nil, fmt.Errorf("can't read key by cn %s: %w", cn, err) + } + res = append(res, pair.ImportX509(keyBytes, certBytes, cn, big.NewInt(0))) + return res, err +} + +func (s *DirKeyStorage) GetLastByCn(cn string) (*pair.X509Pair, error) { + //TODO implement me + panic("implement me") +} + +func (s *DirKeyStorage) GetBySerial(serial *big.Int) (*pair.X509Pair, error) { + //TODO implement me + panic("implement me") +} + +func (s *DirKeyStorage) DeleteByCN(cn string) error { + //TODO implement me + panic("implement me") +} + +func (s *DirKeyStorage) DeleteBySerial(serial *big.Int) error { + //TODO implement me + panic("implement me") +} + +func (s *DirKeyStorage) GetAll() ([]*pair.X509Pair, error) { + //TODO implement me + panic("implement me") +} diff --git a/internal/compliantStorage/storage_test.go b/internal/compliantStorage/storage_test.go new file mode 100644 index 0000000..73b0c25 --- /dev/null +++ b/internal/compliantStorage/storage_test.go @@ -0,0 +1,77 @@ +package compliantStorage + +import ( + "bytes" + "github.com/kemsta/go-easyrsa/pkg/pair" + "math/big" + "os" + "path/filepath" + "testing" +) + +func getTestDir() string { + res, _ := filepath.Abs("test") + return res +} + +func TestDirKeyStorage_Put(t *testing.T) { + type fields struct { + keydir string + } + type args struct { + pair *pair.X509Pair + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "good", + fields: fields{ + keydir: filepath.Join(getTestDir(), "dir_keystorage"), + }, + args: args{ + pair: pair.ImportX509([]byte("keybytes"), []byte("certbytes"), "good_cert", big.NewInt(66)), + }, + wantErr: false, + }, + { + name: "ca", + fields: fields{ + keydir: filepath.Join(getTestDir(), "dir_keystorage"), + }, + args: args{ + pair: pair.ImportX509([]byte("keybytes"), []byte("certbytes"), "ca", big.NewInt(154)), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &DirKeyStorage{ + pkidir: tt.fields.keydir, + } + if err := s.Put(tt.args.pair); (err != nil) != tt.wantErr { + t.Errorf("DirKeyStorage.Put() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } + certBytes, _ := os.ReadFile(filepath.Join(getTestDir(), "dir_keystorage", "issued/good_cert.crt")) + if !bytes.Equal(certBytes, []byte("certbytes")) { + t.Errorf("DirKeyStorage.Put() wrong cert bytes in result file") + } + certBytes, _ = os.ReadFile(filepath.Join(getTestDir(), "dir_keystorage", "certs_by_serial/9A.crt")) + if !bytes.Equal(certBytes, []byte("certbytes")) { + t.Errorf("DirKeyStorage.Put() wrong cert bytes in result file") + } + certBytes, _ = os.ReadFile(filepath.Join(getTestDir(), "dir_keystorage", "ca.crt")) + if !bytes.Equal(certBytes, []byte("certbytes")) { + t.Errorf("DirKeyStorage.Put() wrong cert bytes in result file") + } + keyBytes, _ := os.ReadFile(filepath.Join(getTestDir(), "dir_keystorage", "private/good_cert.key")) + if !bytes.Equal(keyBytes, []byte("keybytes")) { + t.Errorf("DirKeyStorage.Put() wrong key bytes in result file") + } +} diff --git a/internal/compliantStorage/test/.gitignore b/internal/compliantStorage/test/.gitignore new file mode 100644 index 0000000..63f1fef --- /dev/null +++ b/internal/compliantStorage/test/.gitignore @@ -0,0 +1 @@ +*.lock diff --git a/internal/compliantStorage/test/dir_keystorage/ca.crt b/internal/compliantStorage/test/dir_keystorage/ca.crt new file mode 100644 index 0000000..0b8a88f --- /dev/null +++ b/internal/compliantStorage/test/dir_keystorage/ca.crt @@ -0,0 +1 @@ +certbytes \ No newline at end of file diff --git a/internal/compliantStorage/test/dir_keystorage/certs_by_serial/42.crt b/internal/compliantStorage/test/dir_keystorage/certs_by_serial/42.crt new file mode 100644 index 0000000..0b8a88f --- /dev/null +++ b/internal/compliantStorage/test/dir_keystorage/certs_by_serial/42.crt @@ -0,0 +1 @@ +certbytes \ No newline at end of file diff --git a/internal/compliantStorage/test/dir_keystorage/certs_by_serial/9A.crt b/internal/compliantStorage/test/dir_keystorage/certs_by_serial/9A.crt new file mode 100644 index 0000000..0b8a88f --- /dev/null +++ b/internal/compliantStorage/test/dir_keystorage/certs_by_serial/9A.crt @@ -0,0 +1 @@ +certbytes \ No newline at end of file diff --git a/internal/compliantStorage/test/dir_keystorage/issued/good_cert.crt b/internal/compliantStorage/test/dir_keystorage/issued/good_cert.crt new file mode 100644 index 0000000..0b8a88f --- /dev/null +++ b/internal/compliantStorage/test/dir_keystorage/issued/good_cert.crt @@ -0,0 +1 @@ +certbytes \ No newline at end of file diff --git a/internal/compliantStorage/test/dir_keystorage/private/ca.key b/internal/compliantStorage/test/dir_keystorage/private/ca.key new file mode 100644 index 0000000..923717d --- /dev/null +++ b/internal/compliantStorage/test/dir_keystorage/private/ca.key @@ -0,0 +1 @@ +keybytes \ No newline at end of file diff --git a/internal/compliantStorage/test/dir_keystorage/private/good_cert.key b/internal/compliantStorage/test/dir_keystorage/private/good_cert.key new file mode 100644 index 0000000..923717d --- /dev/null +++ b/internal/compliantStorage/test/dir_keystorage/private/good_cert.key @@ -0,0 +1 @@ +keybytes \ No newline at end of file