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 }