Skip to content

Commit

Permalink
added parser for openssl ca db index file
Browse files Browse the repository at this point in the history
  • Loading branch information
kemsta committed Aug 31, 2023
1 parent 3dc9739 commit d107191
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 12 deletions.
107 changes: 107 additions & 0 deletions internal/compilantStorage/index.go
Original file line number Diff line number Diff line change
@@ -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
}
228 changes: 228 additions & 0 deletions internal/compilantStorage/index_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
6 changes: 3 additions & 3 deletions internal/compilantStorage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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")
}
Expand Down
4 changes: 2 additions & 2 deletions internal/fsStorage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions internal/fsStorage/storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
Expand Down
Loading

0 comments on commit d107191

Please sign in to comment.