From 7151124493b690771501fa6fcbfbb228447f0108 Mon Sep 17 00:00:00 2001 From: Stanislav Kem Date: Sun, 19 Jun 2022 00:08:37 +0300 Subject: [PATCH] added parser for openssl ca db index file --- internal/compilantStorage/index.go | 107 +++++++++++ internal/compilantStorage/index_test.go | 228 ++++++++++++++++++++++++ internal/compilantStorage/storage.go | 6 +- internal/fsStorage/storage.go | 4 +- internal/fsStorage/storage_test.go | 4 +- pkg/pki/struct.go | 10 +- 6 files changed, 347 insertions(+), 12 deletions(-) create mode 100644 internal/compilantStorage/index.go create mode 100644 internal/compilantStorage/index_test.go diff --git a/internal/compilantStorage/index.go b/internal/compilantStorage/index.go new file mode 100644 index 0000000..003e4d0 --- /dev/null +++ b/internal/compilantStorage/index.go @@ -0,0 +1,107 @@ +package compilantStorage + +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 from index: %w", 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/compilantStorage/index_test.go b/internal/compilantStorage/index_test.go new file mode 100644 index 0000000..7e2317f --- /dev/null +++ b/internal/compilantStorage/index_test.go @@ -0,0 +1,228 @@ +package compilantStorage + +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/compilantStorage/storage.go b/internal/compilantStorage/storage.go index 5ed187c..4fc8dd8 100644 --- a/internal/compilantStorage/storage.go +++ b/internal/compilantStorage/storage.go @@ -20,7 +20,7 @@ const ( CertFileExtension = ".crt" // certificate file extension ) -//DirKeyStorage is easyrsa v3 compilant sotrage. It can be used as a drop-off replacement on the created with easyrsa v3 pki +//DirKeyStorage is easyrsa v3 compilant storage. It can be used as a drop-off replacement on the created with easyrsa v3 pki type DirKeyStorage struct { pkidir string } @@ -61,7 +61,7 @@ func (s *DirKeyStorage) Put(pair *pair.X509Pair) error { _, err = os.Stat(certPath) if err != nil { if !os.IsNotExist(err) { - return fmt.Errorf("%s already exist. Aborting writing to avoid overwriting this file", certPath) + return fmt.Errorf("%s already exist. Abort writing to avoid overwriting this file", certPath) } } @@ -108,7 +108,7 @@ func (s *DirKeyStorage) GetBySerial(serial *big.Int) (*pair.X509Pair, error) { panic("implement me") } -func (s *DirKeyStorage) DeleteByCn(cn string) error { +func (s *DirKeyStorage) DeleteByCN(cn string) error { //TODO implement me panic("implement me") } diff --git a/internal/fsStorage/storage.go b/internal/fsStorage/storage.go index 724bec7..d22eeb6 100644 --- a/internal/fsStorage/storage.go +++ b/internal/fsStorage/storage.go @@ -152,8 +152,8 @@ func (s *DirKeyStorage) Put(pair *pair.X509Pair) error { return nil } -// DeleteByCn delete all pairs by CN -func (s *DirKeyStorage) DeleteByCn(cn string) error { +// DeleteByCN delete all pairs by CN +func (s *DirKeyStorage) DeleteByCN(cn string) error { err := os.Remove(filepath.Join(s.keydir, cn)) if err != nil { return fmt.Errorf("can`t delete by cn %v in %v: %w", cn, s.keydir, err) diff --git a/internal/fsStorage/storage_test.go b/internal/fsStorage/storage_test.go index 6f90cf1..35e9c66 100644 --- a/internal/fsStorage/storage_test.go +++ b/internal/fsStorage/storage_test.go @@ -211,8 +211,8 @@ func TestDirKeyStorage_DeleteByCn(t *testing.T) { s := &DirKeyStorage{ keydir: tt.fields.keydir, } - if err := s.DeleteByCn(tt.args.cn); (err != nil) != tt.wantErr { - t.Errorf("DirKeyStorage.DeleteByCn() error = %v, wantErr %v", err, tt.wantErr) + if err := s.DeleteByCN(tt.args.cn); (err != nil) != tt.wantErr { + t.Errorf("DirKeyStorage.DeleteByCN() error = %v, wantErr %v", err, tt.wantErr) } }) } diff --git a/pkg/pki/struct.go b/pkg/pki/struct.go index 4d17e14..43c939c 100644 --- a/pkg/pki/struct.go +++ b/pkg/pki/struct.go @@ -6,23 +6,23 @@ import ( "math/big" ) -// Key storage interface +// KeyStorage storage interface type KeyStorage interface { Put(pair *pair.X509Pair) error // Put new pair to KeyStorage. Overwrite if already exist. GetByCN(cn string) ([]*pair.X509Pair, error) // Get all keypairs by CN. GetLastByCn(cn string) (*pair.X509Pair, error) // Get last pair by CN. GetBySerial(serial *big.Int) (*pair.X509Pair, error) // Get one keypair by serial. - DeleteByCn(cn string) error // Delete all keypairs by CN. + DeleteByCN(cn string) error // Delete all keypairs by CN. DeleteBySerial(serial *big.Int) error // Delete one keypair by serial. GetAll() ([]*pair.X509Pair, error) // Get all keypair } -// Serial provider interface +// SerialProvider provider interface type SerialProvider interface { - Next() (*big.Int, error) // Next return next uniq serial + Next() (*big.Int, error) // Next return uniq serial } -// Certificate revocation list holder interface +// CRLHolder is a certificate revocation list holder interface type CRLHolder interface { Put([]byte) error // Put file content for crl Get() (*pkix.CertificateList, error) // Get current revoked cert list