You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

200 lines
4.5 KiB
Go

package openssl
import (
"bufio"
"errors"
"fmt"
"math/big"
"os"
"path"
"strings"
"sync"
"time"
"git.cacert.org/cacert-gosigner/x509/revoking"
"git.cacert.org/cacert-gosigner/x509/signing"
)
// The FileRepository stores information about signed and revoked certificates
// in an openssl index.txt compatible file.
//
// A reference for the file format can be found at
// https://pki-tutorial.readthedocs.io/en/latest/cadb.html.
type FileRepository struct {
indexFileName string
lock sync.Locker
}
type indexStatus string
const (
CertificateValid indexStatus = "V"
CertificateRevoked = "R"
CertificateExpired = "E"
)
const opensslTimeSpec = "060102030405Z"
type indexEntry struct {
statusFlag indexStatus
expiresAt time.Time
revokedAt time.Time
revocationReason string
serialNumber *big.Int
fileName string
certificateSubjectDN string
}
func (e *indexEntry) markRevoked(revocationTime time.Time, reason string) {
if e.statusFlag == CertificateValid {
e.statusFlag = CertificateRevoked
e.revokedAt = revocationTime
e.revocationReason = reason
}
}
type IndexFile struct {
entries []*indexEntry
}
func (f *IndexFile) findEntry(number *big.Int) (*indexEntry, error) {
for _, entry := range f.entries {
if entry.serialNumber == number {
return entry, nil
}
}
return nil, fmt.Errorf("no entry for serial number %s found", number)
}
// StoreRevocation records information about a revoked certificate.
func (r *FileRepository) StoreRevocation(revoked *revoking.CertificateRevoked) error {
r.lock.Lock()
defer r.lock.Unlock()
index, err := r.loadIndex()
if err != nil {
return err
}
entry, err := index.findEntry(revoked.SerialNumber())
if err != nil {
return err
}
entry.markRevoked(revoked.RevocationTime(), revoked.Reason())
err = r.writeIndex(index)
return err
}
// StoreCertificate records information about a signed certificate.
func (r *FileRepository) StoreCertificate(signed *signing.CertificateSigned) error {
r.lock.Lock()
defer r.lock.Unlock()
return nil
}
func (r *FileRepository) loadIndex() (*IndexFile, error) {
f, err := os.Open(r.indexFileName)
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()
entries := make([]*indexEntry, 0)
indexScanner := bufio.NewScanner(f)
for indexScanner.Scan() {
indexEntry, err := r.newIndexEntry(indexScanner.Text())
if err != nil {
return nil, err
}
entries = append(entries, indexEntry)
}
if err := indexScanner.Err(); err != nil {
return nil, err
}
return &IndexFile{entries: entries}, nil
}
func (r *FileRepository) writeIndex(index *IndexFile) error {
return errors.New("not implemented")
}
func (r *FileRepository) newIndexEntry(text string) (*indexEntry, error) {
fields := strings.Split(text, "\t")
const expectedFieldNumber = 6
if len(fields) != expectedFieldNumber {
return nil, fmt.Errorf(
"unexpected number of fields %d instead of %d",
len(fields),
expectedFieldNumber,
)
}
expirationParsed, err := time.Parse(opensslTimeSpec, fields[1])
if err != nil {
return nil, err
}
var revocationTimeParsed time.Time
var revocationReason string
if fields[2] != "" {
var timeString string
if strings.Contains(fields[2], ",") {
parts := strings.SplitN(fields[2], ",", 2)
timeString = parts[0]
revocationReason = parts[1]
} else {
timeString = fields[2]
}
revocationTimeParsed, err = time.Parse(opensslTimeSpec, timeString)
if err != nil {
return nil, err
}
}
serialParsed := new(big.Int)
if _, ok := serialParsed.SetString(fields[3], 16); !ok {
return nil, fmt.Errorf("could not parse serial number %s", fields[3])
}
fileNameParsed := "unknown"
if fields[4] != "" {
_, err = os.Stat(path.Join(path.Dir(r.indexFileName), fields[4]))
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else {
fileNameParsed = fields[4]
}
}
subjectDNParsed := fields[5]
return &indexEntry{
statusFlag: indexStatus(fields[0]),
expiresAt: expirationParsed,
revokedAt: revocationTimeParsed,
revocationReason: revocationReason,
serialNumber: serialParsed,
fileName: fileNameParsed,
certificateSubjectDN: subjectDNParsed,
}, nil
}
func NewFileRepository(baseDirectory string) (*FileRepository, error) {
err := os.Chdir(baseDirectory)
if err != nil {
return nil, err
}
return &FileRepository{
indexFileName: path.Join(baseDirectory, "index.txt"),
lock: &sync.Mutex{},
}, nil
}