199 lines
4.5 KiB
Go
199 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
|
|
}
|