commit 3affc704d87f7a758b63cb2e547db49183d3d56f Author: Jan Dittberner Date: Mon Aug 23 20:53:43 2021 +0200 First DDD based signer implementation parts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..da32d53 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.go text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..da17bb5 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module git.cacert.org/cacert-gosigner + +go 1.17 + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.1.0 // indirect + github.com/stretchr/testify v1.7.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7b4dc41 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/openpgp/signing/repository.go b/openpgp/signing/repository.go new file mode 100644 index 0000000..23021ae --- /dev/null +++ b/openpgp/signing/repository.go @@ -0,0 +1,5 @@ +package signing + +type Repository interface { + StoreSignature(key *SignedPublicKey) error +} diff --git a/openpgp/signing/signing.go b/openpgp/signing/signing.go new file mode 100644 index 0000000..7ac9b7c --- /dev/null +++ b/openpgp/signing/signing.go @@ -0,0 +1,11 @@ +package signing + +type OpenPGPSigning struct{} + +type RequestInformation struct{} + +type SignedPublicKey struct{} + +func (o *OpenPGPSigning) Sign(signingRequest *RequestInformation) (*SignedPublicKey, error) { + return &SignedPublicKey{}, nil +} diff --git a/x509/openssl/repository.go b/x509/openssl/repository.go new file mode 100644 index 0000000..f05867c --- /dev/null +++ b/x509/openssl/repository.go @@ -0,0 +1,199 @@ +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 +} diff --git a/x509/openssl/repository_test.go b/x509/openssl/repository_test.go new file mode 100644 index 0000000..7f03559 --- /dev/null +++ b/x509/openssl/repository_test.go @@ -0,0 +1,32 @@ +package openssl_test + +import ( + "path" + "testing" + + "git.cacert.org/cacert-gosigner/x509/openssl" + "git.cacert.org/cacert-gosigner/x509/revoking" + "git.cacert.org/cacert-gosigner/x509/signing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStoreRevocation(t *testing.T) { + fr, err := openssl.NewFileRepository(t.TempDir()) + require.NoError(t, err) + + err = fr.StoreRevocation(&revoking.CertificateRevoked{}) + assert.NoError(t, err) + + assert.FileExists(t, path.Join(t.TempDir(), "index.txt")) +} + +func TestStoreCertificate(t *testing.T) { + fr, err := openssl.NewFileRepository(t.TempDir()) + require.NoError(t, err) + + err = fr.StoreCertificate(&signing.CertificateSigned{}) + assert.NoError(t, err) + + assert.FileExists(t, path.Join(t.TempDir(), "index.txt")) +} diff --git a/x509/revoking/repository.go b/x509/revoking/repository.go new file mode 100644 index 0000000..3aa593e --- /dev/null +++ b/x509/revoking/repository.go @@ -0,0 +1,7 @@ +package revoking + +// A Repository for storing certificate status information +type Repository interface { + // StoreRevocation stores information about a revoked certificate. + StoreRevocation(*CertificateRevoked) error +} diff --git a/x509/revoking/revoking.go b/x509/revoking/revoking.go new file mode 100644 index 0000000..b3b61c5 --- /dev/null +++ b/x509/revoking/revoking.go @@ -0,0 +1,57 @@ +package revoking + +import ( + "math/big" + "time" +) + +type X509Revoking struct { + repository Repository +} + +type RevokeCertificate struct { + serialNumber *big.Int + reason string +} + +type CertificateRevoked struct { + serialNumber *big.Int + revocationTime time.Time + reason string +} + +type CRLInformation struct{} + +func (r *X509Revoking) Revoke(revokeCertificate *RevokeCertificate) (*CertificateRevoked, error) { + revoked := &CertificateRevoked{ + serialNumber: revokeCertificate.serialNumber, + revocationTime: time.Now(), + reason: revokeCertificate.reason, + } + + if err := r.repository.StoreRevocation(revoked); err != nil { + return nil, err + } + + return revoked, nil +} + +func (r *X509Revoking) CreateCRL() (*CRLInformation, error) { + return &CRLInformation{}, nil +} + +func (r *CertificateRevoked) SerialNumber() *big.Int { + return r.serialNumber +} + +func (r *CertificateRevoked) RevocationTime() time.Time { + return r.revocationTime +} + +func (r *CertificateRevoked) Reason() string { + return r.reason +} + +func NewX509Revoking(repo Repository) *X509Revoking { + return &X509Revoking{repository: repo} +} diff --git a/x509/revoking/revoking_test.go b/x509/revoking/revoking_test.go new file mode 100644 index 0000000..8840c30 --- /dev/null +++ b/x509/revoking/revoking_test.go @@ -0,0 +1,36 @@ +package revoking + +import ( + "math/big" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type testRepo struct { + revoked []big.Int +} + +func (t *testRepo) StoreRevocation(revoked *CertificateRevoked) error { + t.revoked = append(t.revoked, *revoked.serialNumber) + return nil +} + +func TestRevoking(t *testing.T) { + testRepository := testRepo{revoked: make([]big.Int, 0)} + r := NewX509Revoking(&testRepository) + + rand.Seed(time.Now().Unix()) + + serial := big.NewInt(rand.Int63()) + + revoke, err := r.Revoke(&RevokeCertificate{serialNumber: serial, reason: "for testing"}) + assert.NoError(t, err) + + assert.Equal(t, "for testing", revoke.reason) + assert.Equal(t, serial, revoke.serialNumber) + + assert.Contains(t, testRepository.revoked, *serial) +} diff --git a/x509/signing/repository.go b/x509/signing/repository.go new file mode 100644 index 0000000..37cf7f3 --- /dev/null +++ b/x509/signing/repository.go @@ -0,0 +1,5 @@ +package signing + +type Repository interface { + StoreCertificate(*CertificateSigned) error +} diff --git a/x509/signing/signing.go b/x509/signing/signing.go new file mode 100644 index 0000000..447623a --- /dev/null +++ b/x509/signing/signing.go @@ -0,0 +1,11 @@ +package signing + +type X509Signing struct{} + +type RequestInformation struct{} + +type CertificateSigned struct{} + +func (x *X509Signing) Sign(signingRequest *RequestInformation) (*CertificateSigned, error) { + return &CertificateSigned{}, nil +}