package x509_ops import ( "bufio" "crypto" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/x509" "crypto/x509/pkix" "encoding/hex" "encoding/pem" "fmt" "io/ioutil" "math/big" "os" "path" "path/filepath" "strconv" "strings" "time" log "github.com/sirupsen/logrus" "git.cacert.org/cacert-gosigner/shared" "git.cacert.org/cacert-gosigner/signer/common" ) const crlLifetime = time.Hour * 24 * 7 type Root struct { Name string privateKeyFile string certificateFile string databaseFile string crlNumberFile string crlFileName string crlHashDir string } func (x *Root) String() string { return x.Name } type Profile struct { Name string } func (x *Root) loadCertificate() (*x509.Certificate, error) { certificateFile := x.certificateFile pemBytes, err := ioutil.ReadFile(certificateFile) if err != nil { return nil, fmt.Errorf( "could not load certificate %s: %v", certificateFile, err, ) } pemBlock, _ := pem.Decode(pemBytes) if pemBlock.Type != "CERTIFICATE" { log.Warnf( "PEM in %s is probably not a certificate. PEM block has type %s", certificateFile, pemBlock.Type, ) } certificate, err := x509.ParseCertificate(pemBlock.Bytes) if err != nil { return nil, fmt.Errorf( "could no parse certificate from %s: %v", certificateFile, err, ) } return certificate, nil } func (x *Root) getPrivateKey() (crypto.Signer, error) { privateKeyFile := x.privateKeyFile pemBytes, err := ioutil.ReadFile(privateKeyFile) if err != nil { return nil, fmt.Errorf( "could not load private key %s: %v", privateKeyFile, err, ) } pemBlock, _ := pem.Decode(pemBytes) if pemBlock.Type != "PRIVATE KEY" { log.Warnf( "PEM in %s is probably not a private key. PEM block has type %s", privateKeyFile, pemBlock.Type, ) } privateKey, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes) if err != nil { return nil, fmt.Errorf( "could no parse private key from %s: %v", privateKeyFile, err, ) } return privateKey.(*rsa.PrivateKey), nil } func (x *Root) getNextCRLNumber() (*big.Int, error) { crlNumberFile := x.crlNumberFile _, err := os.Stat(crlNumberFile) if err != nil { log.Warnf("CRL number file %s does not exist: %v", crlNumberFile, err) return big.NewInt(1), nil } data, err := ioutil.ReadFile(crlNumberFile) if err != nil { return nil, fmt.Errorf("could not read CRL number file %s", crlNumberFile) } result, err := common.StringAsBigInt(data) if err != nil { return nil, fmt.Errorf("could not parse content of %s as CRL number: %v", crlNumberFile, err) } return result, nil } func (x *Root) bumpCRLNumber(current *big.Int) error { serial := current.Int64() + 1 crlNumberFile := x.crlNumberFile outFile, err := ioutil.TempFile(path.Dir(crlNumberFile), "*.txt") defer func() { _ = outFile.Close() }() _, err = outFile.WriteString(fmt.Sprintf( "%s\n", strings.ToUpper(strconv.FormatInt(serial, 16)), )) if err != nil { return fmt.Errorf("could not write new CRL number %d to %s: %v", serial, outFile.Name(), err) } if err = outFile.Close(); err != nil { return fmt.Errorf("could not close temporary file %s: %v", outFile.Name(), err) } if err = os.Rename(crlNumberFile, fmt.Sprintf("%s.old", crlNumberFile)); err != nil { return fmt.Errorf("could not rename %s to %s.old: %v", crlNumberFile, crlNumberFile, err) } if err = os.Rename(outFile.Name(), crlNumberFile); err != nil { return fmt.Errorf("could not rename %s to %s: %v", outFile.Name(), crlNumberFile, err) } return nil } func (x *Root) loadRevokedCertificatesFromDatabase() ([]pkix.RevokedCertificate, error) { databaseFile := x.databaseFile _, err := os.Stat(databaseFile) if err != nil { log.Warnf("openssl certificate database file %s does not exist: %v", databaseFile, err) return []pkix.RevokedCertificate{}, nil } file, err := os.Open(databaseFile) if err != nil { return nil, fmt.Errorf("could not open openssl certificate database file %s: %v", databaseFile, err) } defer func() { _ = file.Close() }() result := make([]pkix.RevokedCertificate, 0) scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.Split(scanner.Text(), "\t") if line[0] == "R" { serialNumber, err := common.StringAsBigInt([]byte(line[3])) if err != nil { return nil, fmt.Errorf("could not parse serial number %s as big int: %v", line[3], err) } revokeTs, err := strconv.ParseInt(line[2][:len(line[2])-1], 10, 64) result = append(result, pkix.RevokedCertificate{ SerialNumber: serialNumber, RevocationTime: time.Unix(revokeTs, 0), Extensions: nil, }) } } return result, nil } func (x *Root) recordRevocation(certificate *x509.Certificate) (*pkix.RevokedCertificate, error) { databaseFile := x.databaseFile _, err := os.Stat(databaseFile) if err != nil { return nil, fmt.Errorf("openssl certificate database file %s does not exist: %v", databaseFile, err) } inFile, err := os.Open(databaseFile) if err != nil { return nil, fmt.Errorf("could not open openssl certificate database file %s: %v", databaseFile, err) } defer func() { _ = inFile.Close() }() outFile, err := ioutil.TempFile(path.Dir(databaseFile), "*.txt") defer func() { _ = outFile.Close() }() scanner := bufio.NewScanner(inFile) writer := bufio.NewWriter(outFile) found := false revocationTime := time.Now() for scanner.Scan() { line := scanner.Text() parts := strings.Split(line, "\t") serialNumber, err := common.StringAsBigInt([]byte(parts[3])) if err != nil { return nil, fmt.Errorf("could not parse serial number %s as big int: %v", parts[3], err) } if serialNumber == certificate.SerialNumber { line = strings.Join( []string{"R", parts[1], strconv.FormatInt(revocationTime.Unix(), 10) + "Z", parts[3], parts[4]}, "\t", ) found = true } if _, err = writer.WriteString(fmt.Sprintf("%s\n", line)); err != nil { return nil, fmt.Errorf("could not write '%s' to %s: %v", line, outFile.Name(), err) } } if err = outFile.Close(); err != nil { return nil, fmt.Errorf("could not close temporary file %s: %v", outFile.Name(), err) } if err = inFile.Close(); err != nil { return nil, fmt.Errorf("could not close %s: %v", databaseFile, err) } if err = os.Rename(databaseFile, fmt.Sprintf("%s.old", databaseFile)); err != nil { return nil, fmt.Errorf("could not rename %s to %s.old: %v", databaseFile, databaseFile, err) } if err = os.Rename(outFile.Name(), databaseFile); err != nil { return nil, fmt.Errorf("could not rename temporary file %s to %s: %v", outFile.Name(), databaseFile, err) } if !found { log.Warnf("entry not found in database") } return &pkix.RevokedCertificate{ SerialNumber: certificate.SerialNumber, RevocationTime: revocationTime, }, nil } func (x *Root) RevokeCertificate(request []byte) (*pkix.RevokedCertificate, error) { pemBlock, _ := pem.Decode(request) if pemBlock.Type != "CERTIFICATE" { if pemBlock.Type != "CERTIFICATE" { log.Warnf( "PEM structure is probably not a certificate. PEM block has type %s", pemBlock.Type, ) log.Trace(request) } } certificate, err := x509.ParseCertificate(pemBlock.Bytes) if err != nil { return nil, fmt.Errorf( "could no parse certificate: %v", err, ) } return x.recordRevocation(certificate) } func (x *Root) GenerateCrl(algorithm x509.SignatureAlgorithm) ([]byte, *[20]byte, error) { caCertificate, err := x.loadCertificate() if err != nil { return nil, nil, err } caPrivateKey, err := x.getPrivateKey() if err != nil { return nil, nil, err } certificatesToRevoke, err := x.loadRevokedCertificatesFromDatabase() if err != nil { return nil, nil, err } nextCrlNumber, err := x.getNextCRLNumber() if err != nil { return nil, nil, err } crlTemplate := &x509.RevocationList{ SignatureAlgorithm: algorithm, RevokedCertificates: certificatesToRevoke, Number: nextCrlNumber, ThisUpdate: time.Now(), NextUpdate: time.Now().Add(crlLifetime), ExtraExtensions: nil, } defer func() { if err = x.bumpCRLNumber(nextCrlNumber); err != nil { log.Errorf("could not bump CRL number: %v", err) } }() crlBytes, err := x509.CreateRevocationList( rand.Reader, crlTemplate, caCertificate, caPrivateKey, ) if err != nil { return nil, nil, fmt.Errorf("could not create new CRL: %v", err) } if err = ioutil.WriteFile(x.crlFileName, crlBytes, 0644); err != nil { return nil, nil, fmt.Errorf("could not write new CRL to %s: %v", x.crlFileName, err) } newCrlHash := sha1.Sum(crlBytes) hashedCrlFileName := path.Join( x.crlHashDir, fmt.Sprintf("%s.crl", hex.EncodeToString(newCrlHash[:])), ) if err = ioutil.WriteFile(hashedCrlFileName, crlBytes, 0644); err != nil { return nil, nil, fmt.Errorf("could not write new CRL to %s: %v", hashedCrlFileName, err) } return crlBytes, &newCrlHash, nil } func (x *Root) DeleteOldCRLs(keepHashes ...string) error { log.Debugf("will look for CRLs in %s", x.crlHashDir) found, err := filepath.Glob(path.Join(x.crlHashDir, "*.crl")) if err != nil { log.Warnf("could not match files: %v", err) return nil } nextFound: for _, filename := range found { for _, keepHash := range keepHashes { if x.GetCrlFileName(keepHash) == filename { continue nextFound } } if err := os.Remove(filename); err != nil { return fmt.Errorf("could not delete %s: %v", filename, err) } } return nil } func (x *Root) GetCrlFileName(hash string) string { return path.Join(x.crlHashDir, fmt.Sprintf("%s.crl", hash)) } func (x *Root) checkPreconditions() { results := []bool{ x.checkFile(x.privateKeyFile, "private key"), x.checkFile(x.certificateFile, "certificate"), x.checkFile(x.databaseFile, "database"), x.checkFile(x.crlNumberFile, "CRL serial number"), x.checkFile(x.crlFileName, "CRL file"), x.checkDir(x.crlHashDir, "directory for hash indexed CRLs"), } for _, success := range results { if !success { log.Warnf("preconditions for %s failed, operations may fail too", x) break } } } func (x *Root) checkFile(path, prefix string) bool { ok := true if s, e := os.Stat(path); e != nil { log.Warnf("%s file %s of %s has issues: %v", prefix, path, x, e) ok = false } else if s.IsDir() { log.Warnf("%s file %s of %s is a directory", prefix, path, x) ok = false } return ok } func (x *Root) checkDir(path, prefix string) bool { ok := true if s, e := os.Stat(path); e != nil { log.Warnf("%s %s of %s has issues: %v", prefix, path, x, e) if err := os.MkdirAll(path, 0755); err != nil { log.Warnf("could not create %s %s of %s: %v", prefix, path, x, err) ok = false } ok = false } else if !s.IsDir() { log.Warnf("%s %s of %s is not a directory", prefix, path, x) ok = false } return ok } func NewRoot(basedir, name, subdir string, id shared.CryptoSystemRootId) *Root { root := &Root{ Name: name, privateKeyFile: path.Join(basedir, subdir, "private", "ca.key.pem"), certificateFile: path.Join(basedir, subdir, "ca.crt.pem"), databaseFile: path.Join(basedir, subdir, "index.txt"), crlNumberFile: path.Join(basedir, subdir, "crlnumber"), crlFileName: fmt.Sprintf("revoke-root%d.crl", id), crlHashDir: path.Join( basedir, "currentcrls", fmt.Sprintf("%d", id), ), } root.checkPreconditions() return root }