├── .gitignore ├── .gitattributes ├── testdata ├── crl1.crl ├── testcert.der ├── crl1.pem ├── invalidcrl1.pem └── crlpadding.pem ├── core ├── crlstructures.go ├── checker.go ├── hashing │ ├── hashes_test.go │ ├── hashes.go │ ├── hashingreaderwrapper.go │ └── hashingreaderwrapper_test.go ├── signatureverify │ ├── signatureverifystrategy.go │ ├── rsasignatureverifystrategy.go │ ├── ecdsasignatureverifystrategy.go │ ├── hashandverifystrategieslookup.go │ ├── hashandverifystrategieslookup_test.go │ ├── rsasignatureverifystrategy_test.go │ └── ecdsasignatureverifystrategy_test.go ├── testutils │ └── ZapLoggerTestUtil.go ├── utils │ ├── utils.go │ └── utils_test.go ├── pemreader │ ├── pemreader.go │ └── pemreader_test.go ├── certificatechains.go └── asn1parser │ └── asn1parser.go ├── testhelper ├── testdatahelper.go └── testdatahelper_test.go ├── crl ├── crlstore │ ├── storetype.go │ ├── storetype_test.go │ ├── serializer.go │ ├── crlpesisterprocessor.go │ ├── crlstore.go │ ├── crlstore_test.go │ ├── asn1serializer.go │ ├── map.go │ ├── leveldb.go │ ├── map_test.go │ └── asn1serializer_test.go ├── crlloader │ ├── crlloader.go │ ├── crlloaderfactory.go │ ├── filecrlloader.go │ ├── multischemescrlloader.go │ ├── urlcrlloader.go │ ├── crlloaderfactory_test.go │ ├── urlcrlloader_test.go │ ├── filecrlloader_test.go │ └── multischemescrlloader_test.go ├── crlreader │ ├── crlreader_test.go │ ├── extensionsupport │ │ └── extensionsupport.go │ └── crlreader.go ├── crlrevocationchecker.go └── crlrepository │ └── crlrepository_test.go ├── test.json ├── LICENSE.md ├── .github └── workflows │ ├── ci.yml │ └── sonarcloud.yml ├── caddyfile.go ├── config └── config.go ├── revocation.go ├── go.mod ├── ocsp └── ocsprevocationchecker.go └── configparser.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | coverage.out 3 | testReport.json -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Autodetect text files 2 | * text=auto 3 | testdata/* binary -------------------------------------------------------------------------------- /testdata/crl1.crl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gr33nbl00d/caddy-revocation-validator/HEAD/testdata/crl1.crl -------------------------------------------------------------------------------- /testdata/testcert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gr33nbl00d/caddy-revocation-validator/HEAD/testdata/testcert.der -------------------------------------------------------------------------------- /core/crlstructures.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type CRLLocations struct { 4 | CRLDistributionPoints []string 5 | CRLUrl string 6 | CRLFile string 7 | } 8 | -------------------------------------------------------------------------------- /core/checker.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "crypto/x509/pkix" 5 | "golang.org/x/crypto/ocsp" 6 | ) 7 | 8 | type RevocationStatus struct { 9 | Revoked bool 10 | CRLRevokedCertEntry *pkix.RevokedCertificate 11 | OcspResponse *ocsp.Response 12 | } 13 | -------------------------------------------------------------------------------- /core/hashing/hashes_test.go: -------------------------------------------------------------------------------- 1 | package hashing 2 | 3 | import ( 4 | "github.com/smallstep/assert" 5 | "testing" 6 | ) 7 | 8 | func TestSum64(t *testing.T) { 9 | result := Sum64("helloworld") 10 | expectedResult := []byte{129, 85, 74, 146, 94, 49, 217, 16} 11 | assert.Equals(t, expectedResult, result, "fnva hash result does not match") 12 | } 13 | -------------------------------------------------------------------------------- /testhelper/testdatahelper.go: -------------------------------------------------------------------------------- 1 | package testhelper 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "runtime" 7 | ) 8 | 9 | func GetTestDataFilePath(fileName string) (resultFilePath string) { 10 | _, curPath, _, ok := runtime.Caller(0) 11 | if !ok { 12 | panic(fmt.Errorf("could not find current working directory")) 13 | } 14 | return path.Join(curPath, "../../testdata", fileName) 15 | } 16 | -------------------------------------------------------------------------------- /crl/crlstore/storetype.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import "fmt" 4 | 5 | type StoreType int32 6 | 7 | const ( 8 | Map StoreType = 0 9 | LevelDB StoreType = 1 10 | ) 11 | 12 | func StoreTypeToString(storeType StoreType) string { 13 | switch storeType { 14 | case LevelDB: 15 | return "Level DB" 16 | case Map: 17 | return "Map" 18 | default: 19 | return fmt.Sprintf("unknown store type %d", storeType) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/signatureverify/signatureverifystrategy.go: -------------------------------------------------------------------------------- 1 | package signatureverify 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | ) 7 | 8 | type SignatureVerifyStrategy interface { 9 | VerifySignature(hash crypto.Hash, key interface{}, calculatedSignature []byte, signature []byte) error 10 | GetAlgorithmID() x509.PublicKeyAlgorithm 11 | } 12 | 13 | type HashAndVerifyStrategies struct { 14 | VerifyStrategy SignatureVerifyStrategy 15 | HashStrategy crypto.Hash 16 | } 17 | -------------------------------------------------------------------------------- /core/hashing/hashes.go: -------------------------------------------------------------------------------- 1 | package hashing 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | const ( 8 | // offset64 FNVa offset 9 | offset64 = 14695981039346656037 10 | // prime64 FNVa prime value. 11 | prime64 = 1099511628211 12 | ) 13 | 14 | // Sum64 fnva hash function 15 | func Sum64(key string) []byte { 16 | var hash uint64 = offset64 17 | for i := 0; i < len(key); i++ { 18 | hash ^= uint64(key[i]) 19 | hash *= prime64 20 | } 21 | b := make([]byte, 8) 22 | binary.LittleEndian.PutUint64(b, hash) 23 | return b 24 | } 25 | -------------------------------------------------------------------------------- /crl/crlloader/crlloader.go: -------------------------------------------------------------------------------- 1 | package crlloader 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "time" 7 | ) 8 | 9 | type CRLLoader interface { 10 | LoadCRL(filePath string) error 11 | GetCRLLocationIdentifier() (string, error) 12 | GetDescription() string 13 | } 14 | 15 | const CRLLoaderRetryCount = 5 16 | const CRLLoaderRetryDelay = 500 * time.Millisecond 17 | 18 | func calculateHashHexString(normalizedUrl string) string { 19 | hash := sha256.New() 20 | hash.Write([]byte(normalizedUrl)) 21 | sum := hash.Sum(nil) 22 | return hex.EncodeToString(sum) 23 | } 24 | -------------------------------------------------------------------------------- /crl/crlstore/storetype_test.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestStoreTypeToString(t *testing.T) { 10 | tests := []struct { 11 | storeType StoreType 12 | expected string 13 | }{ 14 | {LevelDB, "Level DB"}, 15 | {Map, "Map"}, 16 | {StoreType(2), "unknown store type 2"}, 17 | {StoreType(-1), "unknown store type -1"}, 18 | } 19 | 20 | for _, test := range tests { 21 | t.Run(test.expected, func(t *testing.T) { 22 | result := StoreTypeToString(test.storeType) 23 | assert.Equal(t, test.expected, result) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /testhelper/testdatahelper_test.go: -------------------------------------------------------------------------------- 1 | package testhelper 2 | 3 | import ( 4 | "fmt" 5 | "github.com/smallstep/assert" 6 | "path" 7 | "path/filepath" 8 | "runtime" 9 | "testing" 10 | ) 11 | 12 | func TestGetTestDataFilePath(t *testing.T) { 13 | result := GetTestDataFilePath("crl1") 14 | _, curPath, _, ok := runtime.Caller(0) 15 | if !ok { 16 | panic(fmt.Errorf("could not find current working directory")) 17 | } 18 | curPath = path.Join(curPath, "../../") 19 | resultRelative, err := filepath.Rel(curPath, result) 20 | assert.Nil(t, err) 21 | resultRelative = filepath.ToSlash(resultRelative) 22 | assert.Equals(t, "testdata/crl1", resultRelative) 23 | } 24 | -------------------------------------------------------------------------------- /core/signatureverify/rsasignatureverifystrategy.go: -------------------------------------------------------------------------------- 1 | package signatureverify 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "errors" 8 | ) 9 | 10 | type RSASignatureVerifyStrategy struct { 11 | } 12 | 13 | func (t RSASignatureVerifyStrategy) VerifySignature(hash crypto.Hash, key interface{}, calculatedSignature []byte, signature []byte) error { 14 | var rsaKey *rsa.PublicKey 15 | var ok bool 16 | 17 | if rsaKey, ok = key.(*rsa.PublicKey); !ok { 18 | return errors.New("not an rsa key") 19 | } 20 | err := rsa.VerifyPKCS1v15(rsaKey, hash, calculatedSignature, signature) 21 | return err 22 | } 23 | 24 | func (t RSASignatureVerifyStrategy) GetAlgorithmID() x509.PublicKeyAlgorithm { 25 | return x509.RSA 26 | } 27 | -------------------------------------------------------------------------------- /core/testutils/ZapLoggerTestUtil.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "go.uber.org/zap" 6 | "go.uber.org/zap/zaptest/observer" 7 | "testing" 8 | ) 9 | 10 | type LogObserver struct { 11 | Logger *zap.Logger 12 | Logs *observer.ObservedLogs 13 | } 14 | 15 | func (o LogObserver) AssertLogSize(t *testing.T, i int) { 16 | assert.Equal(t, i, o.Logs.Len()) 17 | } 18 | 19 | func (o LogObserver) AssertMessageEqual(t *testing.T, messageId int, message string) { 20 | if o.Logs.Len()-1 < messageId { 21 | t.Fatalf("messageid too low") 22 | } 23 | assert.Equal(t, message, o.Logs.All()[messageId].Message) 24 | } 25 | 26 | func CreateAllLevelLogObserver() LogObserver { 27 | zapCore, logs := observer.New(zap.DebugLevel) 28 | observedLogger := zap.New(zapCore) 29 | return LogObserver{Logger: observedLogger, Logs: logs} 30 | 31 | } 32 | -------------------------------------------------------------------------------- /crl/crlstore/serializer.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import ( 4 | "crypto/x509" 5 | "crypto/x509/pkix" 6 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 7 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader" 8 | ) 9 | 10 | type Serializer interface { 11 | DeserializeMetaInfo([]byte) (*crlreader.CRLMetaInfo, error) 12 | SerializeMetaInfo(*crlreader.CRLMetaInfo) ([]byte, error) 13 | SerializeRevokedCert(*pkix.RevokedCertificate) ([]byte, error) 14 | DeserializeRevokedCert([]byte) (*pkix.RevokedCertificate, error) 15 | SerializeMetaInfoExt(*crlreader.ExtendedCRLMetaInfo) ([]byte, error) 16 | DeserializeMetaInfoExt([]byte) (*crlreader.ExtendedCRLMetaInfo, error) 17 | DeserializeSignatureCert([]byte) (*x509.Certificate, error) 18 | SerializeCRLLocations(*core.CRLLocations) ([]byte, error) 19 | DeserializeCRLLocations([]byte) (*core.CRLLocations, error) 20 | } 21 | -------------------------------------------------------------------------------- /core/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/zap" 6 | "log" 7 | "time" 8 | ) 9 | 10 | func Retry(attempts int, sleep time.Duration, logger *zap.Logger, f func() error) (err error) { 11 | for i := 0; ; i++ { 12 | err = f() 13 | if err == nil { 14 | return 15 | } 16 | 17 | if i >= (attempts - 1) { 18 | break 19 | } 20 | time.Sleep(sleep) 21 | logger.Debug("retrying after error", zap.Error(err)) 22 | } 23 | return fmt.Errorf("after %d attempts, last error: %s", attempts, err) 24 | } 25 | 26 | func CloseWithErrorHandling(closers ...func() error) { 27 | var err error 28 | for _, closeFn := range closers { 29 | if cerr := closeFn(); cerr != nil { 30 | if err == nil { 31 | err = cerr 32 | } else { 33 | err = fmt.Errorf("%v; %v", err, cerr) 34 | } 35 | } 36 | } 37 | if err != nil { 38 | log.Printf("error(s) occurred while closing files: %v", err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_authentication": { 3 | "trusted_ca_certs_pem_files": [ 4 | "./certificates/ca.crt", 5 | "./certificates/ca2.crt" 6 | ], 7 | "mode": "require_and_verify", 8 | "revocation_check": { 9 | "mode": "prefer_ocsp,prefer_crl", 10 | "crl_config": { 11 | "crl_work_dir": "./crls", 12 | "crl_storage": "memory,disk", 13 | "crl_signature_validation_mode": "none,verify_log,verify", 14 | "trusted_signature_cert_files": [ 15 | "./certificates/crlca.crt" 16 | ], 17 | "crl_url_locations": [ 18 | "http://server/my.crl" 19 | ], 20 | "crl_file_locations": [ 21 | "./crls/my.crl" 22 | ], 23 | "crl_update_interval": "10m", 24 | "cdp_support": { 25 | "cdp_fetch": "never,fetch_actively,fetch_background", 26 | "cdp_crl_required": true 27 | } 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Henrique Dias hacdias@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /crl/crlstore/crlpesisterprocessor.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import ( 4 | revocation "github.com/gr33nbl00d/caddy-revocation-validator/core" 5 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader" 6 | ) 7 | 8 | type CRLPersisterProcessor struct { 9 | CRLStore CRLStore 10 | } 11 | 12 | func (C CRLPersisterProcessor) StartUpdateCrl(crlMetaInfo *crlreader.CRLMetaInfo) error { 13 | return C.CRLStore.StartUpdateCrl(crlMetaInfo) 14 | } 15 | 16 | func (C CRLPersisterProcessor) InsertRevokedCertificate(entry *crlreader.CRLEntry) error { 17 | return C.CRLStore.InsertRevokedCert(entry) 18 | } 19 | 20 | func (C CRLPersisterProcessor) UpdateExtendedMetaInfo(info *crlreader.ExtendedCRLMetaInfo) error { 21 | return C.CRLStore.UpdateExtendedMetaInfo(info) 22 | } 23 | 24 | func (C CRLPersisterProcessor) UpdateSignatureCertificate(entry *revocation.CertificateChainEntry) error { 25 | return C.CRLStore.UpdateSignatureCertificate(entry) 26 | } 27 | 28 | func (C CRLPersisterProcessor) UpdateCRLLocations(crlLocations *revocation.CRLLocations) error { 29 | return C.CRLStore.UpdateCRLLocations(crlLocations) 30 | } 31 | -------------------------------------------------------------------------------- /testdata/crl1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN X509 CRL----- 2 | MIIC4TCCAckCAQEwDQYJKoZIhvcNAQEFBQAwgYExCzAJBgNVBAYTAlVTMQswCQYD 3 | VQQIEwJPUjESMBAGA1UEBxMJQmVhdmVydG9uMQ0wCwYDVQQLEwRTTUJVMQ8wDQYD 4 | VQQKEwZNY0FmZWUxMTAvBgNVBAMTKE1jQWZlZSBTSUEgU2lnbmluZyBDZXJ0aWZp 5 | Y2F0ZSBBdXRob3JpdHkXDTE4MDQwMjE1NTQyMFowaTATAgIQABcNMTUwNDA3MjIz 6 | ODU1WjATAgIQARcNMTUwNDA4MTYzMDQ1WjATAgIQAhcNMTUwNDA4MTYzMzE1WjAT 7 | AgIQBBcNMTUwNDIwMTQyNDI5WjATAgIQFxcNMTgwNDAyMTU1NDIwWqCBtjCBszCB 8 | owYDVR0jBIGbMIGYoYGDpIGAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJPUjES 9 | MBAGA1UEBxMJQmVhdmVydG9uMQ0wCwYDVQQLEwRTTUJVMQ8wDQYDVQQKEwZNY0Fm 10 | ZWUxLjAsBgNVBAMTJU1jQWZlZSBTSUEgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3Jp 11 | dHmCEIV4PTGihQe8Talyrr16KHEwCwYDVR0UBAQCAhAEMA0GCSqGSIb3DQEBBQUA 12 | A4IBAQBorZMdWgWC1NMrV8ZZvkWckBM2Az6jZP2yFXpsHWWGBAtYniqYR7WpLzll 13 | 5MfMcDnM21hi8uF+COL8zykeCnF1CbczVQkBF8BqO8KKn4Pjha8ttFvXZVH9BZMg 14 | BALSYema5VNY8Dk309jo6PH3kZ/Sxx87mxSCXNX+vCtM7c3sUNt0hZ8RtWa7G4Sv 15 | qRXvFT7H9d2kdETKQYxYFQgxsiilpTELd2bigHUJqFNFM2M5JqxJSllZ7957he8Z 16 | D2cfsicmuvz2NvtAHY0TuU5v43kDEGljvTdfOCLzZzHo+a7z4HHn/sxl97zGZDE1 17 | qnTvMU7qQ4Zk9pNZfIs4m9GWg1fP 18 | -----END X509 CRL----- 19 | -------------------------------------------------------------------------------- /testdata/invalidcrl1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN X509 CRL----- 2 | MIIC4TCCAckCAQEwDQYJKoZIhvcNAQEFBQAwgYExCzAJBgNVBAYTAlVTMQswCQYD89707 3 | VQQIEwJPUjESMBAGA1UEBxMJQmVhdmVydG9uMQ0wCwYDVQQLEwRTTUJVMQ8wDQYD 4 | VQQKEwZNY0FmZWUxMTAvBgNVBAMTKE1jQWZlZSBTSUEgU2lnbmluZyBDZXJ0aWZp 5 | Y2F0ZSBBdXRob3JpdHkXDTE4MDQwMjE1NTQyMFowaTATAgIQABcNMTUwNDA3MjIz 6 | ODU1WjATAgIQARcNMTUwNDA4MTYzMDQ1WjATAgIQAhcNMTUwNDA4MTYzMzE1WjAT 7 | AgIQBBcNMTUwNDIwMTQyNDI5WjATAgIQFxcNMTgwNDAyMTU1NDIwWqCBtjCBszCB 8 | owYDVR0jBIGbMIGYoYGDpIGAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJPUjES 9 | MBAGA1UEBxMJQmVhdmVydG9uMQ0wCwYDVQQLEwRTTUJVMQ8wDQYDVQQKEwZNY0Fm 10 | ZWUxLjAsBgNVBAMTJU1jQWZlZSBTSUEgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3Jp 11 | dHmCEIV4PTGihQe8Talyrr16KHEwCwYDVR0UBAQCAhAEMA0GCSqGSIb3DQEBBQUA 12 | A4IBAQBorZMdWgWC1NMrV8ZZvkWckBM2Az6jZP2yFXpsHWWGBAtYniqYR7WpLzll 13 | 5MfMcDnM21hi8uF+COL8zykeCnF1CbczVQkBF8BqO8KKn4Pjha8ttFvXZVH9BZMg 14 | BALSYema5VNY8Dk309jo6PH3kZ/Sxx87mxSCXNX+vCtM7c3sUNt0hZ8RtWa7G4Sv 15 | qRXvFT7H9d2kdETKQYxYFQgxsiilpTELd2bigHUJqFNFM2M5JqxJSllZ7957he8Z 16 | D2cfsicmuvz2NvtAHY0TuU5v43kDEGljvTdfOCLzZzHo+a7z4HHn/sxl97zGZDE1 17 | qnTvMU7qQ4Zk9pNZfIs4m9GWg1fP 18 | -----END X509 CRL----- 19 | -------------------------------------------------------------------------------- /core/signatureverify/ecdsasignatureverifystrategy.go: -------------------------------------------------------------------------------- 1 | package signatureverify 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/x509" 7 | "encoding/asn1" 8 | "errors" 9 | "log" 10 | "math/big" 11 | ) 12 | 13 | type ECDSASignatureVerifyStrategy struct { 14 | } 15 | 16 | type signature struct { 17 | R, S *big.Int 18 | } 19 | 20 | func (t ECDSASignatureVerifyStrategy) VerifySignature(_ crypto.Hash, key interface{}, calculatedSignature []byte, signatureBytes []byte) error { 21 | var ecdsaKey *ecdsa.PublicKey 22 | var ok bool 23 | 24 | if ecdsaKey, ok = key.(*ecdsa.PublicKey); !ok { 25 | return errors.New("not an ecdsa key") 26 | } 27 | 28 | var sig signature 29 | rest, err := asn1.Unmarshal(signatureBytes, &sig) 30 | if err != nil { 31 | return err 32 | } 33 | if len(rest) != 0 { 34 | log.Printf("[WARNING] more bytes found in signature than needed") 35 | } 36 | verify := ecdsa.Verify(ecdsaKey, calculatedSignature, sig.R, sig.S) 37 | if verify == false { 38 | return errors.New("signature verification failed") 39 | } 40 | return nil 41 | } 42 | 43 | func (t ECDSASignatureVerifyStrategy) GetAlgorithmID() x509.PublicKeyAlgorithm { 44 | return x509.ECDSA 45 | } 46 | -------------------------------------------------------------------------------- /crl/crlloader/crlloaderfactory.go: -------------------------------------------------------------------------------- 1 | package crlloader 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 6 | "go.uber.org/zap" 7 | "strings" 8 | ) 9 | 10 | type CRLLoaderFactory interface { 11 | CreatePreferredCrlLoader(crlLocations *core.CRLLocations, logger *zap.Logger) (CRLLoader, error) 12 | } 13 | type DefaultCRLLoaderFactory struct { 14 | } 15 | 16 | func (R DefaultCRLLoaderFactory) CreatePreferredCrlLoader(crlLocations *core.CRLLocations, logger *zap.Logger) (CRLLoader, error) { 17 | 18 | if len(crlLocations.CRLUrl) > 0 { 19 | return &URLLoader{crlLocations.CRLUrl, logger}, nil 20 | } 21 | if len(crlLocations.CRLFile) > 0 { 22 | return &FileLoader{crlLocations.CRLFile, logger}, nil 23 | } 24 | cdpLoaders := make([]CRLLoader, 0) 25 | for _, cdp := range crlLocations.CRLDistributionPoints { 26 | //todo might add support for LDAP 27 | if strings.HasPrefix(strings.ToLower(cdp), "http") { 28 | cdpLoaders = append(cdpLoaders, &URLLoader{cdp, logger}) 29 | } else { 30 | logger.Warn("unsupported CDP Location Scheme", zap.String("location", cdp)) 31 | } 32 | } 33 | if len(cdpLoaders) == 0 { 34 | return nil, fmt.Errorf("no suitable crl loader found") 35 | } 36 | return &MultiSchemesCRLLoader{ 37 | Loaders: cdpLoaders, 38 | Logger: logger, 39 | }, nil 40 | } 41 | -------------------------------------------------------------------------------- /testdata/crlpadding.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN X509 CRL----- 2 | MIIDeTCCAWECAQEwDQYJKoZIhvcNAQELBQAwgZkxCzAJBgNVBAYTAkRFMQwwCgYD 3 | VQQIDANOUlcxCzAJBgNVBAcMAk1HMQ4wDAYDVQQRDAU5OTk5OTESMBAGA1UECQwJ 4 | QnJlaXRlc3RyMQwwCgYDVQQKDANTdUIxDDAKBgNVBAsMA0ZDUzELMAkGA1UEAwwC 5 | Y2ExIjAgBgkqhkiG9w0BCQEWE3dlYm1hc3RlckBsb2NhbGhvc3QXDTIzMTIwNTA5 6 | NTM1NFoXDTI1MTIwNDA5NTM1NFowTjAlAhQW/bpzcc1GcRXQYwLdLAMOpf/0vhcN 7 | MjMxMjA0MTU1NzA1WjAlAhQW/bpzcc1GcRXQYwLdLAMOpf/0vxcNMjMxMjA0MTU1 8 | NzA1WqBDMEEwEgYDVR0SBAswCYIHYmxhLmNvbTAfBgNVHSMEGDAWgBRVO5VWOBxY 9 | x6Bo9p/Fx/71vVWqbDAKBgNVHRQEAwIBJTANBgkqhkiG9w0BAQsFAAOCAgEAtdri 10 | YVoROoY8QRdywf+3ipOLYQ4FDV6w7ickgMFy/KHr46f0L6LK3qeOaM8lw8Q0NIII 11 | y+nsPo1QWbolbmyLTq6UVL+roALjzFwQ7qftjH5r1FnDvbJURvV94BWC/3W0NqnE 12 | Ym8TRpO11cSA5Z447iC+isuzWNYjFAivE9D+6E+F5CXw2PwapDDDx4TBoaJPqd3U 13 | aKkmypezhompsFDH1S0jenpeOwVZvur4MuSwrW1a2mxqvHOwDwdyeKCJHy9I9Sim 14 | 1YbY2vEPeFjdpFPNYwYiwRxd4ZLt9/sn0cxww7hhgkEgIwgKgoI0AmWMai5mrFr9 15 | MkhJxHTnkJHMu+M1R9TUxOVKExRcjaw6oJJkirMvUzTcr42vIjOaEswVrJEjfKLL 16 | ZJnz0fVs3ixBOawtQhOAyqkuOM59YCVPkgPpt1BXexBHVS2sEiVa/Hb0AeqHIrxB 17 | d8wSJo0SEgOQnkzourDJc27S5gX0Y2qGp6zM/7x5bSn6s69eVG4XM4l+ANcC9EeC 18 | 53FO5DZvTq3Ph0r7gY2ie1Vozkib2+4T+0JR4v8Fej2Ea61QqLAZg3zq0HUvIy/k 19 | uTSOLjqjvf+3J+tbRvuwmF9pTfYKjB/LnwS8FLxmMmyAq5zmv5TJ3Xdse61v+OME 20 | nUf/Bg/n7OtvaGrGDz8Kg1FDhiXuPiRkuYG1kJU= 21 | -----END X509 CRL----- 22 | -------------------------------------------------------------------------------- /crl/crlreader/crlreader_test.go: -------------------------------------------------------------------------------- 1 | package crlreader 2 | 3 | import ( 4 | revocation "github.com/gr33nbl00d/caddy-revocation-validator/core" 5 | "github.com/gr33nbl00d/caddy-revocation-validator/testhelper" 6 | "github.com/smallstep/assert" 7 | "testing" 8 | ) 9 | 10 | type CRLPersisterProcessorMock struct { 11 | } 12 | 13 | func (C CRLPersisterProcessorMock) StartUpdateCrl(_ *CRLMetaInfo) error { 14 | return nil 15 | } 16 | 17 | func (C CRLPersisterProcessorMock) InsertRevokedCertificate(_ *CRLEntry) error { 18 | return nil 19 | } 20 | 21 | func (C CRLPersisterProcessorMock) UpdateExtendedMetaInfo(_ *ExtendedCRLMetaInfo) error { 22 | return nil 23 | } 24 | 25 | func (C CRLPersisterProcessorMock) UpdateSignatureCertificate(_ *revocation.CertificateChainEntry) error { 26 | return nil 27 | } 28 | 29 | func (C CRLPersisterProcessorMock) UpdateCRLLocations(crlLocations *revocation.CRLLocations) error { 30 | return nil 31 | } 32 | func TestReadCRL(t *testing.T) { 33 | reader := StreamingCRLFileReader{} 34 | result, err := reader.ReadCRL(CRLPersisterProcessorMock{}, testhelper.GetTestDataFilePath("crl1.crl")) 35 | assert.Nil(t, err) 36 | t.Logf("%v", result) 37 | } 38 | func TestReadCRLWithPadding(t *testing.T) { 39 | reader := StreamingCRLFileReader{} 40 | result, err := reader.ReadCRL(CRLPersisterProcessorMock{}, testhelper.GetTestDataFilePath("crlpadding.pem")) 41 | assert.Nil(t, err) 42 | t.Logf("%v", result) 43 | } 44 | -------------------------------------------------------------------------------- /crl/crlloader/filecrlloader.go: -------------------------------------------------------------------------------- 1 | package crlloader 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 6 | "go.uber.org/zap" 7 | "io" 8 | "os" 9 | ) 10 | 11 | type FileLoader struct { 12 | FileName string 13 | Logger *zap.Logger 14 | } 15 | 16 | func (f *FileLoader) LoadCRL(filePath string) error { 17 | err := utils.Retry(CRLLoaderRetryCount, CRLLoaderRetryDelay, f.Logger, func() error { 18 | return f.copyToTargetFile(filePath) 19 | }) 20 | return err 21 | } 22 | 23 | func (f *FileLoader) copyToTargetFile(filePath string) error { 24 | stat, err := os.Stat(f.FileName) 25 | if err != nil { 26 | return err 27 | } 28 | if stat.IsDir() { 29 | return fmt.Errorf("CRL File %s is a directory", f.FileName) 30 | } 31 | crlFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 32 | if err != nil { 33 | return err 34 | } 35 | defer utils.CloseWithErrorHandling(crlFile.Close) 36 | sourceFile, err := os.OpenFile(f.FileName, os.O_RDONLY|os.O_EXCL, 0600) 37 | if err != nil { 38 | return err 39 | } 40 | defer utils.CloseWithErrorHandling(sourceFile.Close) 41 | 42 | _, err = io.Copy(crlFile, sourceFile) 43 | if err != nil { 44 | return err 45 | } 46 | return nil 47 | } 48 | 49 | func (f *FileLoader) GetCRLLocationIdentifier() (string, error) { 50 | return calculateHashHexString(f.FileName), nil 51 | } 52 | 53 | func (f *FileLoader) GetDescription() string { 54 | return f.FileName 55 | } 56 | -------------------------------------------------------------------------------- /core/hashing/hashingreaderwrapper.go: -------------------------------------------------------------------------------- 1 | package hashing 2 | 3 | import ( 4 | "bufio" 5 | "crypto" 6 | "fmt" 7 | "hash" 8 | "io" 9 | ) 10 | 11 | type HashingReaderWrapper struct { 12 | Reader *bufio.Reader 13 | CalculateSignature bool 14 | hash hash.Hash 15 | } 16 | 17 | func (t *HashingReaderWrapper) Read(bytes []byte) (int, error) { 18 | byteCount, err := t.Reader.Read(bytes) 19 | if t.CalculateSignature == true && err == nil { 20 | if byteCount == len(bytes) { 21 | t.hash.Write(bytes) 22 | } else { 23 | copiedBytes := make([]byte, byteCount) 24 | copiedByteCount := copy(copiedBytes[:], bytes[0:byteCount]) 25 | if copiedByteCount != byteCount { 26 | return byteCount, fmt.Errorf("error while copy of signature bytes: %v", err) 27 | } 28 | t.hash.Write(copiedBytes) 29 | } 30 | } 31 | return byteCount, err 32 | } 33 | 34 | func (t *HashingReaderWrapper) Peek(count int) ([]byte, error) { 35 | return t.Reader.Peek(count) 36 | } 37 | 38 | func (t *HashingReaderWrapper) StartHashCalculation(hash crypto.Hash) { 39 | t.CalculateSignature = true 40 | t.hash = hash.New() 41 | } 42 | 43 | func (t *HashingReaderWrapper) FinishHashCalculation() []byte { 44 | t.CalculateSignature = false 45 | return t.hash.Sum(nil) 46 | } 47 | 48 | func (t HashingReaderWrapper) Reset(reader io.Reader) { 49 | t.Reader.Reset(reader) 50 | } 51 | 52 | func (t *HashingReaderWrapper) Discard(offset int64) error { 53 | _, err := t.Reader.Discard(int(offset)) 54 | if err != nil { 55 | return err 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | # Default is true, cancels jobs for other platforms in the matrix if one fails 9 | fail-fast: false 10 | matrix: 11 | os: [ ubuntu-latest, macos-latest, windows-latest ] 12 | go: [ '1.21'] 13 | 14 | include: 15 | # Set the minimum Go patch version for the given Go minor 16 | # Usable via ${{ matrix.GO_SEMVER }} 17 | - go: '1.21' 18 | GO_SEMVER: '1.21.7' 19 | 20 | # Set some variables per OS, usable via ${{ matrix.VAR }} 21 | # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing 22 | # SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True') 23 | - os: ubuntu-latest 24 | CADDY_BIN_PATH: ./cmd/caddy/caddy 25 | SUCCESS: 0 26 | 27 | - os: macos-latest 28 | CADDY_BIN_PATH: ./cmd/caddy/caddy 29 | SUCCESS: 0 30 | 31 | - os: windows-latest 32 | CADDY_BIN_PATH: ./cmd/caddy/caddy.exe 33 | SUCCESS: 'True' 34 | 35 | runs-on: ${{ matrix.os }} 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | 40 | - name: Set up Go 41 | uses: actions/setup-go@v3 42 | with: 43 | go-version: 1.21 44 | 45 | - name: Build 46 | run: go build -v ./... 47 | 48 | - name: Test 49 | run: go test -v ./... 50 | 51 | - name: Test Report 52 | run: go test -v ./... -json > testReport.json 53 | 54 | - name: Test Coverage 55 | run: go test -v ./... -covermode=atomic -coverprofile=coverage.out 56 | -------------------------------------------------------------------------------- /crl/crlloader/multischemescrlloader.go: -------------------------------------------------------------------------------- 1 | package crlloader 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/zap" 6 | "strings" 7 | ) 8 | 9 | type MultiSchemesCRLLoader struct { 10 | Loaders []CRLLoader 11 | Logger *zap.Logger 12 | lastSuccessfulLoader CRLLoader 13 | } 14 | 15 | func (f *MultiSchemesCRLLoader) LoadCRL(filePath string) error { 16 | if f.lastSuccessfulLoader != nil { 17 | err := f.lastSuccessfulLoader.LoadCRL(filePath) 18 | if err == nil { 19 | return nil 20 | } else { 21 | f.Logger.Warn("failed to load CRL from loader", zap.String("loader", f.lastSuccessfulLoader.GetDescription())) 22 | } 23 | } 24 | for _, loader := range f.Loaders { 25 | if loader == f.lastSuccessfulLoader { 26 | continue 27 | } 28 | err := loader.LoadCRL(filePath) 29 | if err != nil { 30 | f.Logger.Warn("failed to load CRL from loader", zap.String("loader", loader.GetDescription())) 31 | } else { 32 | f.lastSuccessfulLoader = loader 33 | return nil 34 | } 35 | } 36 | return fmt.Errorf("failed to load CRL from all loaders %+v", f.Loaders) 37 | } 38 | 39 | func (f *MultiSchemesCRLLoader) GetCRLLocationIdentifier() (string, error) { 40 | builder := strings.Builder{} 41 | for _, loader := range f.Loaders { 42 | identifier, err := loader.GetCRLLocationIdentifier() 43 | if err != nil { 44 | return "", err 45 | } 46 | builder.WriteString(identifier) 47 | } 48 | return calculateHashHexString(builder.String()), nil 49 | } 50 | 51 | func (f *MultiSchemesCRLLoader) GetDescription() string { 52 | var descriptions []string 53 | for _, loader := range f.Loaders { 54 | descriptions = append(descriptions, loader.GetDescription()) 55 | } 56 | return strings.Join(descriptions, ", ") 57 | } 58 | -------------------------------------------------------------------------------- /crl/crlstore/crlstore.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import ( 4 | "crypto/x509/pkix" 5 | "fmt" 6 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 7 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader" 8 | "go.uber.org/zap" 9 | "math/big" 10 | ) 11 | 12 | const ExtendedMetaInfoKey string = "#META#_EXT" 13 | const MetaInfoKey string = "#META#" 14 | const SignatureCertKey = "#CRL_SIG_CERT#" 15 | const CRLLocationKey = "#CRL_LOCATIONS#" 16 | 17 | type CRLStore interface { 18 | InsertRevokedCert(entry *crlreader.CRLEntry) error 19 | GetCertRevocationStatus(issuer *pkix.RDNSequence, certSerial *big.Int) (*core.RevocationStatus, error) 20 | StartUpdateCrl(info *crlreader.CRLMetaInfo) error 21 | GetCRLMetaInfo() (*crlreader.CRLMetaInfo, error) 22 | UpdateExtendedMetaInfo(extendedInfo *crlreader.ExtendedCRLMetaInfo) error 23 | GetCRLExtMetaInfo() (*crlreader.ExtendedCRLMetaInfo, error) 24 | UpdateSignatureCertificate(*core.CertificateChainEntry) error 25 | GetCRLSignatureCert() (*core.CertificateChainEntry, error) 26 | UpdateCRLLocations(points *core.CRLLocations) error 27 | GetCRLLocations() (*core.CRLLocations, error) 28 | Update(store CRLStore) error 29 | IsEmpty() bool 30 | Close() 31 | Delete() error 32 | } 33 | 34 | type Factory interface { 35 | CreateStore(identifier string, temporary bool) (CRLStore, error) 36 | } 37 | 38 | func CreateStoreFactory(storeType StoreType, repoBaseDir string, logger *zap.Logger) (Factory, error) { 39 | if storeType == Map { 40 | return MapStoreFactory{ 41 | Serializer: ASN1Serializer{}, 42 | Logger: logger, 43 | }, nil 44 | } else if storeType == LevelDB { 45 | return LevelDbStoreFactory{ 46 | Serializer: ASN1Serializer{}, 47 | BasePath: repoBaseDir, 48 | Logger: logger, 49 | }, nil 50 | } else { 51 | return nil, fmt.Errorf("unknown store type %d", storeType) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /core/pemreader/pemreader.go: -------------------------------------------------------------------------------- 1 | package pemreader 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "regexp" 9 | ) 10 | 11 | const pemMaxLineLength = 64 + 2 12 | 13 | var pemPaddingRegEx = regexp.MustCompile("^-{5}[A-Z0-9 ]*-{5}(\n|\r\n){0,1}$") 14 | 15 | type PemReader struct { 16 | Reader *bufio.Reader 17 | } 18 | 19 | func (p *PemReader) Read(byteData []byte) (int, error) { 20 | if len(byteData) < pemMaxLineLength { 21 | return 0, errors.New("buffer need to be at least 66 bytes long") 22 | } 23 | return p.readNextBase64Line(byteData) 24 | } 25 | 26 | func (p *PemReader) readNextBase64Line(byteData []byte) (int, error) { 27 | readString, err := p.Reader.ReadString('\n') 28 | if err != nil { 29 | return 0, err 30 | } 31 | matchString := pemPaddingRegEx.MatchString(readString) 32 | if matchString { 33 | return p.readNextBase64Line(byteData) 34 | } else { 35 | if len(readString) > pemMaxLineLength { 36 | return 0, fmt.Errorf("line was longer than 64 characters %s", readString) 37 | } else { 38 | i := copy(byteData, readString) 39 | return i, nil 40 | } 41 | } 42 | } 43 | 44 | func NewPemReader(reader *bufio.Reader) PemReader { 45 | return PemReader{ 46 | Reader: reader, 47 | } 48 | } 49 | 50 | func IsPemFile(file *os.File) (err error, isPemFile bool) { 51 | currentPosition, err := file.Seek(0, 1) 52 | if err != nil { 53 | return err, false 54 | } 55 | defer func() { 56 | _, errNew := file.Seek(currentPosition, 0) 57 | if err != nil { 58 | err = errNew 59 | } 60 | }() 61 | _, err = file.Seek(0, 0) 62 | if err != nil { 63 | return err, false 64 | } 65 | reader := bufio.NewReader(file) 66 | line, prefix, err := reader.ReadLine() 67 | if err != nil { 68 | return nil, false 69 | } 70 | if prefix { 71 | return nil, false 72 | } 73 | lineString := string(line) 74 | matchString := pemPaddingRegEx.MatchString(lineString) 75 | return nil, matchString 76 | } 77 | -------------------------------------------------------------------------------- /crl/crlloader/urlcrlloader.go: -------------------------------------------------------------------------------- 1 | package crlloader 2 | 3 | import ( 4 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 5 | "go.uber.org/zap" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | ) 11 | 12 | type URLLoader struct { 13 | UrlString string 14 | Logger *zap.Logger 15 | } 16 | 17 | func (L *URLLoader) LoadCRL(filePath string) error { 18 | normalizedUrl, err := L.normalizeUrl() 19 | if err != nil { 20 | return err 21 | } 22 | err = L.DownloadFromUrlWithRetries(filePath, normalizedUrl) 23 | if err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | func (L *URLLoader) DownloadFromUrlWithRetries(filePath string, normalizedUrl string) error { 30 | err := utils.Retry(CRLLoaderRetryCount, CRLLoaderRetryDelay, L.Logger, func() error { 31 | return L.downloadCRL(normalizedUrl, filePath) 32 | }) 33 | return err 34 | } 35 | 36 | func (L *URLLoader) GetCRLLocationIdentifier() (string, error) { 37 | normalizedUrl, err := L.normalizeUrl() 38 | if err != nil { 39 | return "", err 40 | } 41 | return calculateHashHexString(normalizedUrl), nil 42 | } 43 | 44 | func (L *URLLoader) GetDescription() string { 45 | return L.UrlString 46 | } 47 | 48 | func (L *URLLoader) downloadCRL(url string, filePath string) error { 49 | crlFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 50 | if err != nil { 51 | return err 52 | } 53 | defer utils.CloseWithErrorHandling(crlFile.Close) 54 | httpRequest, err := http.Get(url) 55 | if err != nil { 56 | return err 57 | } 58 | defer utils.CloseWithErrorHandling(httpRequest.Body.Close) 59 | _, err = io.Copy(crlFile, httpRequest.Body) 60 | if err != nil { 61 | return err 62 | } 63 | return nil 64 | } 65 | 66 | func (L *URLLoader) normalizeUrl() (string, error) { 67 | parse, err := url.Parse(L.UrlString) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | normalizedUrl := parse.String() 73 | return normalizedUrl, nil 74 | } 75 | -------------------------------------------------------------------------------- /crl/crlstore/crlstore_test.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/stretchr/testify/suite" 6 | "go.uber.org/zap" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | type CRLStoreSuite struct { 12 | suite.Suite 13 | logger *zap.Logger 14 | tmpDir string // Store the path to the temporary directory 15 | } 16 | 17 | func (suite *CRLStoreSuite) SetupTest() { 18 | // Initialize the logger before each test 19 | logger, _ := zap.NewDevelopment() 20 | suite.logger = logger 21 | 22 | // Create a temporary directory 23 | tmpDir, err := os.MkdirTemp("", "test-crl-dir-") 24 | assert.NoError(suite.T(), err, "Error creating temporary directory") 25 | suite.tmpDir = tmpDir 26 | } 27 | 28 | func (suite *CRLStoreSuite) TestCreateStoreFactoryMap() { 29 | factory, err := CreateStoreFactory(Map, suite.tmpDir, suite.logger) 30 | assert.NoError(suite.T(), err) 31 | assert.IsType(suite.T(), MapStoreFactory{}, factory) 32 | storeFactory := factory.(MapStoreFactory) 33 | assert.NotNil(suite.T(), storeFactory.Serializer) 34 | assert.Same(suite.T(), suite.logger, storeFactory.Logger) 35 | } 36 | 37 | func (suite *CRLStoreSuite) TestCreateStoreFactoryLevelDB() { 38 | factory, err := CreateStoreFactory(LevelDB, suite.tmpDir, suite.logger) 39 | assert.NoError(suite.T(), err) 40 | assert.IsType(suite.T(), LevelDbStoreFactory{}, factory) 41 | storeFactory := factory.(LevelDbStoreFactory) 42 | assert.NotNil(suite.T(), storeFactory.Serializer) 43 | assert.Same(suite.T(), suite.logger, storeFactory.Logger) 44 | assert.Equal(suite.T(), suite.tmpDir, storeFactory.BasePath) 45 | } 46 | 47 | func (suite *CRLStoreSuite) TestCreateStoreFactoryUnknownType() { 48 | factory, err := CreateStoreFactory(10, suite.tmpDir, suite.logger) 49 | assert.Error(suite.T(), err) 50 | assert.Nil(suite.T(), factory) 51 | } 52 | 53 | func TestCrlStoreSuite(t *testing.T) { 54 | // Run the test suite 55 | suite.Run(t, new(CRLStoreSuite)) 56 | } 57 | -------------------------------------------------------------------------------- /core/signatureverify/hashandverifystrategieslookup.go: -------------------------------------------------------------------------------- 1 | package signatureverify 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509/pkix" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | var oidToHashAlgorithmMap = map[string]crypto.Hash{ 12 | "1.2.840.113549.1.1.5": crypto.SHA1, //sha1WithRSA 13 | "1.2.840.113549.1.1.14": crypto.SHA224, //sha224WithRSA 14 | "1.2.840.113549.1.1.11": crypto.SHA256, //sha256WithRSA 15 | "1.2.840.113549.1.1.12": crypto.SHA384, //sha384WithRSA 16 | "1.2.840.113549.1.1.13": crypto.SHA512, //sha512WithRSA 17 | "1.2.840.10045.4.1": crypto.SHA1, //ECDSAWithSHA1, 18 | "1.2.840.10045.4.3.1": crypto.SHA224, //ECDSAWithSHA224, 19 | "1.2.840.10045.4.3.2": crypto.SHA256, //ECDSAWithSHA256, 20 | "1.2.840.10045.4.3.3": crypto.SHA384, //ECDSAWithSHA384, 21 | "1.2.840.10045.4.3.4": crypto.SHA512, //ECDSAWithSHA512, 22 | } 23 | 24 | var oidPrefixToVerifyStrategyMap = map[string]SignatureVerifyStrategy{ 25 | "1.2.840.113549": new(RSASignatureVerifyStrategy), //RSA 26 | "1.2.840.10045": new(ECDSASignatureVerifyStrategy), //ECDSA 27 | } 28 | 29 | func LookupHashAndVerifyStrategies(algoIdentifier pkix.AlgorithmIdentifier) (*HashAndVerifyStrategies, error) { 30 | h := new(HashAndVerifyStrategies) 31 | hashStrategy, err := getHashAlgorithmFromOID(algoIdentifier) 32 | if err != nil { 33 | return nil, err 34 | } 35 | h.HashStrategy = *hashStrategy 36 | h.VerifyStrategy = getVerifyStrategyFromOID(algoIdentifier) 37 | return h, nil 38 | } 39 | 40 | func getHashAlgorithmFromOID(target pkix.AlgorithmIdentifier) (*crypto.Hash, error) { 41 | for oidString, hash := range oidToHashAlgorithmMap { 42 | if strings.EqualFold(oidString, target.Algorithm.String()) { 43 | return &hash, nil 44 | } 45 | } 46 | return nil, errors.New(fmt.Sprintf("no valid hash algorithm is found for the oid: %#v", target)) 47 | } 48 | 49 | func getVerifyStrategyFromOID(target pkix.AlgorithmIdentifier) SignatureVerifyStrategy { 50 | for oidPrefix, verifyStrategy := range oidPrefixToVerifyStrategyMap { 51 | if strings.HasPrefix(target.Algorithm.String(), oidPrefix) { 52 | return verifyStrategy 53 | } 54 | } 55 | return new(RSASignatureVerifyStrategy) 56 | } 57 | -------------------------------------------------------------------------------- /core/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/smallstep/assert" 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | "go.uber.org/zap/zaptest" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | type MockRetry struct { 15 | countBeforeSuccess int8 16 | } 17 | 18 | // Mock function that returns an error. 19 | func (p *MockRetry) mockFunction() error { 20 | p.countBeforeSuccess-- 21 | if p.countBeforeSuccess == 0 { 22 | return nil 23 | } else { 24 | return errors.New("mock error") 25 | } 26 | } 27 | 28 | func TestRetryFailing(t *testing.T) { 29 | // Initialize test variables. 30 | retryMessageCount := 0 31 | attempts := 3 32 | expectedErr := errors.New("mock error") 33 | sleepTime := 20 * time.Millisecond 34 | logger := zaptest.NewLogger(t, zaptest.WrapOptions(zap.Hooks(func(e zapcore.Entry) error { 35 | if e.Message == "retrying after error" { 36 | retryMessageCount++ 37 | } 38 | if e.Level == zap.ErrorLevel { 39 | t.Fatal("Error should never happen!") 40 | } 41 | return nil 42 | }))) 43 | 44 | // Define the function under test. 45 | mockRetry := MockRetry{countBeforeSuccess: 5} 46 | err := Retry(attempts, sleepTime, logger, mockRetry.mockFunction) 47 | assert.Equals(t, 2, retryMessageCount) 48 | // Check if the error matches the expected error. 49 | if err.Error() != fmt.Sprintf("after %d attempts, last error: %s", attempts, expectedErr) { 50 | t.Errorf("Unexpected error message. Expected: %s, Got: %s", expectedErr, err) 51 | } 52 | } 53 | 54 | func TestRetryWith2Retries(t *testing.T) { 55 | // Initialize test variables. 56 | retryMessageCount := 0 57 | attempts := 4 58 | sleepTime := 20 * time.Millisecond 59 | logger := zaptest.NewLogger(t, zaptest.WrapOptions(zap.Hooks(func(e zapcore.Entry) error { 60 | if e.Message == "retrying after error" { 61 | retryMessageCount++ 62 | } 63 | if e.Level == zap.ErrorLevel { 64 | t.Fatal("Error should never happen!") 65 | } 66 | return nil 67 | }))) 68 | 69 | // Define the function under test. 70 | mockRetry := MockRetry{countBeforeSuccess: 4} 71 | err := Retry(attempts, sleepTime, logger, mockRetry.mockFunction) 72 | assert.Equals(t, 3, retryMessageCount) 73 | assert.Nil(t, err) 74 | } 75 | -------------------------------------------------------------------------------- /core/signatureverify/hashandverifystrategieslookup_test.go: -------------------------------------------------------------------------------- 1 | package signatureverify 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509/pkix" 6 | "encoding/asn1" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type SignatureVerifyTestSuite struct { 14 | suite.Suite 15 | } 16 | 17 | func TestSignatureVerifyTestSuite(t *testing.T) { 18 | suite.Run(t, new(SignatureVerifyTestSuite)) 19 | } 20 | 21 | func (suite *SignatureVerifyTestSuite) TestLookupHashAndVerifyStrategies() { 22 | // Define a test case with a valid OID for RSA and SHA256 23 | oid := pkix.AlgorithmIdentifier{ 24 | Algorithm: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}, // RSA with SHA256 25 | } 26 | 27 | // Call LookupHashAndVerifyStrategies with the test OID 28 | hvStrategies, err := LookupHashAndVerifyStrategies(oid) 29 | assert.NoError(suite.T(), err, "LookupHashAndVerifyStrategies should not return an error") 30 | 31 | // Ensure that the returned HashAndVerifyStrategies struct contains the expected values 32 | assert.Equal(suite.T(), crypto.SHA256, hvStrategies.HashStrategy, "HashStrategy should be SHA256") 33 | assert.IsType(suite.T(), &RSASignatureVerifyStrategy{}, hvStrategies.VerifyStrategy, "VerifyStrategy should be RSA") 34 | 35 | // Define a test case with a valid OID for ECDSA and SHA384 36 | oid = pkix.AlgorithmIdentifier{ 37 | Algorithm: asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3}, // ECDSA with SHA384 38 | } 39 | 40 | // Call LookupHashAndVerifyStrategies with the test OID 41 | hvStrategies, err = LookupHashAndVerifyStrategies(oid) 42 | assert.NoError(suite.T(), err, "LookupHashAndVerifyStrategies should not return an error") 43 | 44 | // Ensure that the returned HashAndVerifyStrategies struct contains the expected values 45 | assert.Equal(suite.T(), crypto.SHA384, hvStrategies.HashStrategy, "HashStrategy should be SHA384") 46 | assert.IsType(suite.T(), &ECDSASignatureVerifyStrategy{}, hvStrategies.VerifyStrategy, "VerifyStrategy should be ECDSA") 47 | 48 | // Define a test case with an invalid OID 49 | oid = pkix.AlgorithmIdentifier{ 50 | Algorithm: asn1.ObjectIdentifier{1, 2, 3, 4, 5}, // Invalid OID 51 | } 52 | 53 | // Call LookupHashAndVerifyStrategies with the invalid OID 54 | hvStrategies, err = LookupHashAndVerifyStrategies(oid) 55 | assert.Error(suite.T(), err, "LookupHashAndVerifyStrategies should return an error for an invalid OID") 56 | assert.Nil(suite.T(), hvStrategies, "HashAndVerifyStrategies should be nil for an invalid OID") 57 | } 58 | -------------------------------------------------------------------------------- /core/signatureverify/rsasignatureverifystrategy_test.go: -------------------------------------------------------------------------------- 1 | package signatureverify 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "crypto/sha256" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/suite" 14 | ) 15 | 16 | type RSASignatureVerifySuite struct { 17 | suite.Suite 18 | strategy RSASignatureVerifyStrategy 19 | privateKey *rsa.PrivateKey 20 | publicKey *rsa.PublicKey 21 | calculatedHash []byte 22 | validSignature []byte 23 | invalidSignature []byte 24 | } 25 | 26 | func (suite *RSASignatureVerifySuite) SetupSuite() { 27 | // Generate RSA key pair for testing 28 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 29 | assert.NoError(suite.T(), err) 30 | 31 | suite.privateKey = privateKey 32 | suite.publicKey = &privateKey.PublicKey 33 | 34 | // Generate a message and calculate its hash 35 | message := []byte("Test message") 36 | hash := sha256.Sum256(message) 37 | suite.calculatedHash = hash[:] 38 | } 39 | 40 | func (suite *RSASignatureVerifySuite) SetupTest() { 41 | // Sign the message to generate a valid signature 42 | signature, err := rsa.SignPKCS1v15(rand.Reader, suite.privateKey, crypto.SHA256, suite.calculatedHash) 43 | assert.NoError(suite.T(), err) 44 | suite.validSignature = signature 45 | 46 | // Generate an invalid signature (modify the valid signature) 47 | invalidSignature := make([]byte, len(signature)) 48 | copy(invalidSignature, signature) 49 | invalidSignature[10]++ // Modify the signature to make it invalid 50 | suite.invalidSignature = invalidSignature 51 | } 52 | 53 | func (suite *RSASignatureVerifySuite) TestValidSignature() { 54 | err := suite.strategy.VerifySignature(crypto.SHA256, suite.publicKey, suite.calculatedHash, suite.validSignature) 55 | assert.NoError(suite.T(), err, "Valid signature should be verified successfully") 56 | } 57 | 58 | func (suite *RSASignatureVerifySuite) TestInvalidSignature() { 59 | err := suite.strategy.VerifySignature(crypto.SHA256, suite.publicKey, suite.calculatedHash, suite.invalidSignature) 60 | assert.Error(suite.T(), err, "Invalid signature should result in an error") 61 | } 62 | 63 | func (suite *RSASignatureVerifySuite) TestWithWrongKeyType() { 64 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 65 | suite.Require().NoError(err) 66 | 67 | err = suite.strategy.VerifySignature(crypto.SHA256, privateKey.PublicKey, nil, nil) 68 | assert.Error(suite.T(), err, "not an rsa key") 69 | assert.Contains(suite.T(), err.Error(), "not an rsa key") 70 | } 71 | 72 | func TestRSASignatureVerifySuite(t *testing.T) { 73 | suite.Run(t, new(RSASignatureVerifySuite)) 74 | } 75 | -------------------------------------------------------------------------------- /crl/crlstore/asn1serializer.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import ( 4 | "crypto/x509" 5 | "crypto/x509/pkix" 6 | "encoding/asn1" 7 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 8 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader" 9 | ) 10 | 11 | type ASN1Serializer struct { 12 | } 13 | 14 | func (C ASN1Serializer) DeserializeMetaInfo(crlMetaBytes []byte) (*crlreader.CRLMetaInfo, error) { 15 | metaInfo := new(crlreader.CRLMetaInfo) 16 | _, err := asn1.Unmarshal(crlMetaBytes, metaInfo) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return metaInfo, nil 21 | } 22 | 23 | func (C ASN1Serializer) SerializeMetaInfo(metaInfo *crlreader.CRLMetaInfo) ([]byte, error) { 24 | crlMetaInfoBytes, err := asn1.Marshal(*metaInfo) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return crlMetaInfoBytes, nil 29 | } 30 | 31 | func (C ASN1Serializer) DeserializeRevokedCert(revokedCertBytes []byte) (*pkix.RevokedCertificate, error) { 32 | revokedCert := new(pkix.RevokedCertificate) 33 | _, err := asn1.Unmarshal(revokedCertBytes, revokedCert) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return revokedCert, nil 38 | } 39 | 40 | func (C ASN1Serializer) SerializeRevokedCert(revokedCert *pkix.RevokedCertificate) ([]byte, error) { 41 | revokedCertBytes, err := asn1.Marshal(*revokedCert) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return revokedCertBytes, nil 46 | } 47 | 48 | func (C ASN1Serializer) SerializeMetaInfoExt(metaInfo *crlreader.ExtendedCRLMetaInfo) ([]byte, error) { 49 | crlExtMetaInfoBytes, err := asn1.Marshal(*metaInfo) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return crlExtMetaInfoBytes, nil 54 | } 55 | 56 | func (C ASN1Serializer) DeserializeMetaInfoExt(crlExtMetaBytes []byte) (*crlreader.ExtendedCRLMetaInfo, error) { 57 | extMetaInfo := new(crlreader.ExtendedCRLMetaInfo) 58 | _, err := asn1.Unmarshal(crlExtMetaBytes, extMetaInfo) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return extMetaInfo, nil 63 | } 64 | 65 | func (C ASN1Serializer) SerializeSignatureCert(cert *x509.Certificate) ([]byte, error) { 66 | certBytes, err := asn1.Marshal(*cert) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return certBytes, nil 71 | } 72 | 73 | func (C ASN1Serializer) DeserializeSignatureCert(certBytes []byte) (*x509.Certificate, error) { 74 | cert, err := x509.ParseCertificate(certBytes) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return cert, nil 79 | } 80 | 81 | func (C ASN1Serializer) SerializeCRLLocations(crlLocations *core.CRLLocations) ([]byte, error) { 82 | bytes, err := asn1.Marshal(*crlLocations) 83 | if err != nil { 84 | return nil, err 85 | } 86 | return bytes, nil 87 | } 88 | func (C ASN1Serializer) DeserializeCRLLocations(certDistPointsBytes []byte) (*core.CRLLocations, error) { 89 | crlLocations := new(core.CRLLocations) 90 | _, err := asn1.Unmarshal(certDistPointsBytes, crlLocations) 91 | if err != nil { 92 | return nil, err 93 | } 94 | return crlLocations, nil 95 | } 96 | -------------------------------------------------------------------------------- /core/pemreader/pemreader_test.go: -------------------------------------------------------------------------------- 1 | package pemreader 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/sha1" 7 | "encoding/hex" 8 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 9 | "github.com/gr33nbl00d/caddy-revocation-validator/testhelper" 10 | "github.com/smallstep/assert" 11 | assert2 "github.com/stretchr/testify/assert" 12 | "io" 13 | "os" 14 | "strings" 15 | "testing" 16 | ) 17 | 18 | func TestIsPemFileWithPemFile(t *testing.T) { 19 | crlFile, err := os.Open(testhelper.GetTestDataFilePath("crl1.pem")) 20 | defer utils.CloseWithErrorHandling(crlFile.Close) 21 | if err != nil { 22 | t.Errorf("error occured %v", err) 23 | } 24 | err, isPemFile := IsPemFile(crlFile) 25 | assert.Nil(t, err) 26 | assert.True(t, isPemFile) 27 | } 28 | 29 | func TestIsPemFileWithNonePemFile(t *testing.T) { 30 | crlFile, err := os.Open(testhelper.GetTestDataFilePath("crl1.crl")) 31 | defer utils.CloseWithErrorHandling(crlFile.Close) 32 | if err != nil { 33 | t.Errorf("error occured %v", err) 34 | } 35 | err, isPemFile := IsPemFile(crlFile) 36 | assert.Nil(t, err) 37 | assert.False(t, isPemFile) 38 | } 39 | 40 | func TestNewPemReader(t *testing.T) { 41 | testReader := bufio.NewReader(strings.NewReader(" some funky shiny bytes")) 42 | result := NewPemReader(testReader) 43 | assert.NotNil(t, result) 44 | assert2.Same(t, testReader, result.Reader) 45 | } 46 | 47 | func TestPemReader_Read(t *testing.T) { 48 | testFile, err := os.Open(testhelper.GetTestDataFilePath("crl1.pem")) 49 | assert.Nil(t, err) 50 | reader := NewPemReader(bufio.NewReader(testFile)) 51 | allBytes, err := readAllBytes(t, reader) 52 | assert.Nil(t, err) 53 | hasher := sha1.New() 54 | 55 | hasher.Write(allBytes) 56 | resultHash := hex.EncodeToString(hasher.Sum(nil)) 57 | assert.Equals(t, "13e491524e70a5b1057b9010fe7b5aa3fc8b60b6", resultHash) 58 | } 59 | 60 | func TestPemReader_ReadPemWithPadding(t *testing.T) { 61 | testFile, err := os.Open(testhelper.GetTestDataFilePath("crlpadding.pem")) 62 | assert.Nil(t, err) 63 | reader := NewPemReader(bufio.NewReader(testFile)) 64 | allBytes, err := readAllBytes(t, reader) 65 | assert.Nil(t, err) 66 | hasher := sha1.New() 67 | 68 | hasher.Write(allBytes) 69 | resultHash := hex.EncodeToString(hasher.Sum(nil)) 70 | assert.Equals(t, "1a0526838635309c0c7660d04372f66d1d54a1dd", resultHash) 71 | } 72 | 73 | func TestPemReader_ReadWithInvalidFile(t *testing.T) { 74 | testFile, err := os.Open(testhelper.GetTestDataFilePath("invalidcrl1.pem")) 75 | assert.Nil(t, err) 76 | reader := NewPemReader(bufio.NewReader(testFile)) 77 | _, err = readAllBytes(nil, reader) 78 | assert.Error(t, err) 79 | } 80 | 81 | func TestPemReader_ReadWithInvalidBuffer(t *testing.T) { 82 | testFile, err := os.Open(testhelper.GetTestDataFilePath("crl1.pem")) 83 | assert.Nil(t, err) 84 | reader := NewPemReader(bufio.NewReader(testFile)) 85 | var readBuffer = make([]byte, 10) 86 | _, err = reader.Read(readBuffer) 87 | assert.Error(t, err) 88 | } 89 | 90 | func readAllBytes(t *testing.T, reader PemReader) (readData []byte, err error) { 91 | buf := &bytes.Buffer{} 92 | for { 93 | var readBuffer = make([]byte, 66) 94 | read, err := reader.Read(readBuffer) 95 | if err == io.EOF { 96 | err = nil 97 | allBytes := buf.Bytes() 98 | return allBytes, nil 99 | } 100 | if err != nil { 101 | return nil, err 102 | } 103 | t.Logf("Read bytes: %s", hex.EncodeToString(readBuffer[0:read])) 104 | buf.Write(readBuffer[0:read]) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /crl/crlloader/crlloaderfactory_test.go: -------------------------------------------------------------------------------- 1 | package crlloader 2 | 3 | import ( 4 | "github.com/gr33nbl00d/caddy-revocation-validator/core/testutils" 5 | "testing" 6 | 7 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | // Define a test suite 13 | type CRLLoaderSuite struct { 14 | suite.Suite 15 | logObserver testutils.LogObserver 16 | factory DefaultCRLLoaderFactory 17 | } 18 | 19 | func (suite *CRLLoaderSuite) SetupTest() { 20 | // Initialize the logger and factory before each test 21 | suite.logObserver = testutils.CreateAllLevelLogObserver() 22 | suite.factory = DefaultCRLLoaderFactory{} 23 | } 24 | 25 | func (suite *CRLLoaderSuite) TestURLLoader() { 26 | crlLocations := &core.CRLLocations{ 27 | CRLUrl: "http://example.com/crl", 28 | } 29 | 30 | loader, err := suite.factory.CreatePreferredCrlLoader(crlLocations, suite.logObserver.Logger) 31 | 32 | // Use assert to check for errors and loader types 33 | assert.NoError(suite.T(), err, "Expected no error, but got one") 34 | assert.IsType(suite.T(), &URLLoader{}, loader, "Expected URLLoader") 35 | suite.logObserver.AssertLogSize(suite.T(), 0) 36 | } 37 | 38 | func (suite *CRLLoaderSuite) TestFileLoader() { 39 | crlLocations := &core.CRLLocations{ 40 | CRLFile: "/path/to/crl", 41 | } 42 | 43 | loader, err := suite.factory.CreatePreferredCrlLoader(crlLocations, suite.logObserver.Logger) 44 | 45 | // Use assert to check for errors and loader types 46 | assert.NoError(suite.T(), err, "Expected no error, but got one") 47 | assert.IsType(suite.T(), &FileLoader{}, loader, "Expected FileLoader") 48 | suite.logObserver.AssertLogSize(suite.T(), 0) 49 | } 50 | 51 | func (suite *CRLLoaderSuite) TestMultiSchemesCRLLoader() { 52 | crlLocations := &core.CRLLocations{ 53 | CRLDistributionPoints: []string{"http://example.com/crl1", "http://example.com/crl2"}, 54 | } 55 | 56 | loader, err := suite.factory.CreatePreferredCrlLoader(crlLocations, suite.logObserver.Logger) 57 | 58 | // Use assert to check for errors and loader types 59 | assert.NoError(suite.T(), err, "Expected no error, but got one") 60 | assert.IsType(suite.T(), &MultiSchemesCRLLoader{}, loader, "Expected MultiSchemesCRLLoader") 61 | var multiSchemeLoader = loader.(*MultiSchemesCRLLoader) 62 | assert.Equal(suite.T(), 2, len(multiSchemeLoader.Loaders)) 63 | suite.logObserver.AssertLogSize(suite.T(), 0) 64 | } 65 | 66 | func (suite *CRLLoaderSuite) TestUnsupportedCDPLocationLogsWarning() { 67 | crlLocations := &core.CRLLocations{ 68 | CRLDistributionPoints: []string{"abcd://example.com/crl1", "http://example.com/crl2"}, 69 | } 70 | 71 | loader, err := suite.factory.CreatePreferredCrlLoader(crlLocations, suite.logObserver.Logger) 72 | 73 | // Use assert to check for errors and loader types 74 | assert.NoError(suite.T(), err, "Expected no error, but got one") 75 | assert.IsType(suite.T(), &MultiSchemesCRLLoader{}, loader, "Expected MultiSchemesCRLLoader") 76 | var multiSchemeLoader = loader.(*MultiSchemesCRLLoader) 77 | assert.Equal(suite.T(), 1, len(multiSchemeLoader.Loaders)) 78 | suite.logObserver.AssertLogSize(suite.T(), 1) 79 | suite.logObserver.AssertMessageEqual(suite.T(), 0, "unsupported CDP Location Scheme") 80 | } 81 | 82 | func (suite *CRLLoaderSuite) TestNoSuitableLoader() { 83 | crlLocations := &core.CRLLocations{} 84 | 85 | _, err := suite.factory.CreatePreferredCrlLoader(crlLocations, suite.logObserver.Logger) 86 | 87 | // Use assert to check for an error 88 | assert.Error(suite.T(), err, "Expected an error, but got nil") 89 | } 90 | 91 | func TestCRLLoaderSuite(t *testing.T) { 92 | // Run the test suite 93 | suite.Run(t, new(CRLLoaderSuite)) 94 | } 95 | -------------------------------------------------------------------------------- /crl/crlreader/extensionsupport/extensionsupport.go: -------------------------------------------------------------------------------- 1 | package extensionsupport 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/x509/pkix" 7 | "encoding/asn1" 8 | "errors" 9 | "fmt" 10 | "github.com/gr33nbl00d/caddy-revocation-validator/core/asn1parser" 11 | "math/big" 12 | ) 13 | 14 | type AuthorityKeyIdentifier struct { 15 | Raw asn1.RawContent 16 | KeyIdentifier []byte `asn1:"tag:0,optional"` 17 | AuthorityCertIssuer GeneralName `asn1:"tag:1,optional"` 18 | AuthorityCertSerialNumber *big.Int `asn1:"tag:2,optional"` 19 | } 20 | 21 | type GeneralName struct { 22 | Raw asn1.RawContent 23 | OtherName asn1.RawValue `asn1:"tag:0,optional"` 24 | Rfc822Name asn1.RawValue `asn1:"tag:1,ia5,optional"` 25 | DNSName asn1.RawValue `asn1:"tag:2,ia5,optional"` 26 | X400Address asn1.RawValue `asn1:"tag:3,optional"` 27 | DirectoryName asn1.RawValue `asn1:"tag:4,optional"` 28 | EdiPartyName asn1.RawValue `asn1:"tag:5,optional"` 29 | UniformResourceIdentifier asn1.RawValue `asn1:"tag:6,ia5,optional"` 30 | IPAddress asn1.RawValue `asn1:"tag:7,optional"` 31 | RegisteredID asn1.RawValue `asn1:"tag:8,optional"` 32 | } 33 | 34 | func (G GeneralName) GetGeneralNameType() (int, error) { 35 | reader := bufio.NewReader(bytes.NewReader(G.Raw)) 36 | tagLength, err := asn1parser.PeekTagLength(reader, 0) 37 | if err != nil { 38 | return 0, err 39 | } 40 | if asn1parser.IsContextSpecificTag(tagLength) == false { 41 | return -1, errors.New("generalname content is invalid") 42 | } 43 | lastContextSpecificTag, err := findLastRecursiveContextSpecificTagInOrder(nil, reader, 0) 44 | if err != nil { 45 | return 0, err 46 | } 47 | return asn1parser.GetContextSpecificTagId(lastContextSpecificTag), nil 48 | } 49 | 50 | func findLastRecursiveContextSpecificTagInOrder(previous *asn1parser.TagLength, reader *bufio.Reader, offset int) (*asn1parser.TagLength, error) { 51 | //this coding is needed because asn1 parser of go has wrong handling of multiple nested optional fields. 52 | //in this case rawcontent is not the content of the struct but content from the last parent sequence 53 | tagLength, err := asn1parser.PeekTagLength(reader, offset) 54 | if err != nil { 55 | return nil, err 56 | } 57 | if asn1parser.IsContextSpecificTag(tagLength) { 58 | return findLastRecursiveContextSpecificTagInOrder(tagLength, reader, offset+int(tagLength.CalculateTLLength().Int64())) 59 | } 60 | return previous, nil 61 | } 62 | 63 | const OidCertExtSubjectKeyId = "2.5.29.14" 64 | const OidCertExtAuthorityKeyId = "2.5.29.35" 65 | const OidCrlExtCrlNumber = "2.5.29.20" 66 | 67 | var handledCRLExtensions = map[string]bool{ 68 | OidCertExtAuthorityKeyId: true, 69 | OidCrlExtCrlNumber: true, 70 | } 71 | 72 | func FindExtension(oidString string, extensions *[]pkix.Extension) *pkix.Extension { 73 | for _, extension := range *extensions { 74 | if extension.Id.String() == oidString { 75 | return &extension 76 | } 77 | } 78 | return nil 79 | } 80 | 81 | func CheckForCriticalUnhandledCRLExtensions(extensions *[]pkix.Extension) error { 82 | //Unhandled CRL Extensions in general: 83 | //Delta CRL Indicator 2.5.29.27 - No delta list support (critical) 84 | //FreshestCRL 2.5.29.47 - No delta list support (non-critical) 85 | //Issuing Distribution Point 2.5.27.(Not needed as we get this information from cert to check) (non-critical) 86 | //Authority Information Access 1.3.6.1.5.5.7.1.1 - We expect the signing cert to be in the chain for now (non-critical) 87 | //Issuer Alternative Name 2.5.29.18 - Currently we only support normal issuer field as used in most cases (non-critical) 88 | for _, extension := range *extensions { 89 | if extension.Critical { 90 | extensionIdStr := extension.Id.String() 91 | if handledCRLExtensions[extensionIdStr] == false { 92 | return errors.New(fmt.Sprintf("unhandled critical crl extension %s", extensionIdStr)) 93 | } 94 | } 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /core/signatureverify/ecdsasignatureverifystrategy_test.go: -------------------------------------------------------------------------------- 1 | package signatureverify 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/ecdsa" 7 | "crypto/elliptic" 8 | "crypto/rand" 9 | "crypto/rsa" 10 | "crypto/x509" 11 | "encoding/asn1" 12 | "log" 13 | "math/big" 14 | "os" 15 | "strings" 16 | "testing" 17 | 18 | "github.com/stretchr/testify/assert" 19 | "github.com/stretchr/testify/suite" 20 | ) 21 | 22 | type ECDSASignatureVerifySuite struct { 23 | suite.Suite 24 | privateKey *ecdsa.PrivateKey 25 | publicKey *ecdsa.PublicKey 26 | } 27 | 28 | func (suite *ECDSASignatureVerifySuite) SetupTest() { 29 | // Create a sample ECDSA key pair for testing 30 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 31 | suite.Require().NoError(err) 32 | 33 | suite.privateKey = privateKey 34 | suite.publicKey = &privateKey.PublicKey 35 | } 36 | 37 | func (suite *ECDSASignatureVerifySuite) TestValidSignature() { 38 | strategy := ECDSASignatureVerifyStrategy{} 39 | 40 | // Test GetAlgorithmID 41 | algorithmID := strategy.GetAlgorithmID() 42 | suite.Equal(x509.ECDSA, algorithmID) 43 | 44 | // Test VerifySignature with a valid signature 45 | message := []byte("Test message") 46 | r, s, err := ecdsa.Sign(rand.Reader, suite.privateKey, message) 47 | suite.Require().NoError(err) 48 | 49 | validSignature, err := asn1.Marshal(struct{ R, S *big.Int }{r, s}) 50 | suite.Require().NoError(err) 51 | 52 | err = strategy.VerifySignature(crypto.SHA256, suite.publicKey, message, validSignature) 53 | suite.Nil(err) 54 | } 55 | 56 | func (suite *ECDSASignatureVerifySuite) TestInvalidSignature() { 57 | strategy := ECDSASignatureVerifyStrategy{} 58 | 59 | // Test VerifySignature with an invalid signature 60 | message := []byte("Test message") 61 | r, s, err := ecdsa.Sign(rand.Reader, suite.privateKey, message) 62 | suite.Require().NoError(err) 63 | 64 | invalidSignature, err := asn1.Marshal(struct{ R, S *big.Int }{r, s}) 65 | suite.Require().NoError(err) 66 | 67 | // Modify the signature to make it invalid 68 | invalidSignature[10]++ 69 | 70 | err = strategy.VerifySignature(crypto.SHA256, suite.publicKey, message, invalidSignature) 71 | assert.Error(suite.T(), err, "signature verification failed") 72 | assert.Contains(suite.T(), err.Error(), "signature verification failed") 73 | } 74 | 75 | type logCapture struct { 76 | buf *bytes.Buffer 77 | } 78 | 79 | func newLogCapture() *logCapture { 80 | return &logCapture{ 81 | buf: new(bytes.Buffer), 82 | } 83 | } 84 | 85 | func (lc *logCapture) Write(p []byte) (n int, err error) { 86 | return lc.buf.Write(p) 87 | } 88 | 89 | func (suite *ECDSASignatureVerifySuite) TestTooLongSignature() { 90 | // Create a logCapture instance to capture log output 91 | capture := newLogCapture() 92 | 93 | // Replace the default log output with our custom logCapture 94 | log.SetOutput(capture) 95 | 96 | strategy := ECDSASignatureVerifyStrategy{} 97 | 98 | // Test GetAlgorithmID 99 | algorithmID := strategy.GetAlgorithmID() 100 | suite.Equal(x509.ECDSA, algorithmID) 101 | 102 | // Test VerifySignature with a valid signature 103 | message := []byte("Test message") 104 | r, s, err := ecdsa.Sign(rand.Reader, suite.privateKey, message) 105 | suite.Require().NoError(err) 106 | 107 | validSignature, err := asn1.Marshal(struct{ R, S *big.Int }{r, s}) 108 | suite.Require().NoError(err) 109 | tooLongSignature := append(validSignature, 0xdd, 0xff, 0xdd, 0xee) 110 | 111 | err = strategy.VerifySignature(crypto.SHA256, suite.publicKey, message, tooLongSignature) 112 | suite.Nil(err) 113 | // Restore the default log output 114 | log.SetOutput(os.Stderr) 115 | 116 | // Check the captured log output for your expected message 117 | if !strings.Contains(capture.buf.String(), "[WARNING] more bytes found in signature than needed") { 118 | suite.Fail("Expected log message not found in log output") 119 | } 120 | } 121 | 122 | func (suite *ECDSASignatureVerifySuite) TestToFailWithWrongKeyImplementation() { 123 | strategy := ECDSASignatureVerifyStrategy{} 124 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 125 | assert.NoError(suite.T(), err) 126 | 127 | err = strategy.VerifySignature(crypto.SHA256, privateKey.PublicKey, nil, nil) 128 | assert.Error(suite.T(), err, "not an ecdsa key") 129 | } 130 | 131 | func TestECDSASignatureVerifySuite(t *testing.T) { 132 | suite.Run(t, new(ECDSASignatureVerifySuite)) 133 | } 134 | -------------------------------------------------------------------------------- /.github/workflows/sonarcloud.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow helps you trigger a SonarCloud analysis of your code and populates 7 | # GitHub Code Scanning alerts with the vulnerabilities found. 8 | # Free for open source project. 9 | 10 | # 1. Login to SonarCloud.io using your GitHub account 11 | 12 | # 2. Import your project on SonarCloud 13 | # * Add your GitHub organization first, then add your repository as a new project. 14 | # * Please note that many languages are eligible for automatic analysis, 15 | # which means that the analysis will start automatically without the need to set up GitHub Actions. 16 | # * This behavior can be changed in Administration > Analysis Method. 17 | # 18 | # 3. Follow the SonarCloud in-product tutorial 19 | # * a. Copy/paste the Project Key and the Organization Key into the args parameter below 20 | # (You'll find this information in SonarCloud. Click on "Information" at the bottom left) 21 | # 22 | # * b. Generate a new token and add it to your Github repository's secrets using the name SONAR_TOKEN 23 | # (On SonarCloud, click on your avatar on top-right > My account > Security 24 | # or go directly to https://sonarcloud.io/account/security/) 25 | 26 | # Feel free to take a look at our documentation (https://docs.sonarcloud.io/getting-started/github/) 27 | # or reach out to our community forum if you need some help (https://community.sonarsource.com/c/help/sc/9) 28 | 29 | name: SonarCloud analysis 30 | 31 | 32 | on: 33 | push: 34 | branches: [ "main" ] 35 | pull_request: 36 | branches: [ "main" ] 37 | workflow_dispatch: 38 | 39 | permissions: 40 | pull-requests: read # allows SonarCloud to decorate PRs with analysis results 41 | 42 | jobs: 43 | Analysis: 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | with: 49 | submodules: recursive 50 | fetch-depth: 0 51 | 52 | - name: Test 53 | run: go test -v ./... -json > testReport.json 54 | 55 | - name: Test Coverage 56 | run: go test -v ./... -covermode=atomic -coverprofile=coverage.out 57 | 58 | - name: Analyze with SonarCloud 59 | 60 | # You can pin the exact commit or the version. 61 | # uses: SonarSource/sonarcloud-github-action@de2e56b42aa84d0b1c5b622644ac17e505c9a049 62 | uses: SonarSource/sonarcloud-github-action@v2.1.1 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information 65 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret) 66 | with: 67 | # Additional arguments for the sonarcloud scanner 68 | args: 69 | # Unique keys of your project and organization. You can find them in SonarCloud > Information (bottom-left menu) 70 | # mandatory 71 | -Dsonar.projectKey=Gr33nbl00d_caddy-revocation-validator 72 | -Dsonar.organization=gr33nbl00d 73 | -Dsonar.sources=. 74 | -Dsonar.sources.inclusions=**/*.go 75 | -Dsonar.sources.exclusions=**/*_test.go 76 | -Dsonar.tests=. 77 | -Dsonar.test.inclusions=**/*_test.go 78 | -Dsonar.go.coverage.reportPaths=coverage.out 79 | -Dsonar.go.tests.reportPaths=testReport.json 80 | -Dsonar.verbose=true 81 | # Comma-separated paths to directories containing main source files. 82 | #-Dsonar.sources= # optional, default is project base directory 83 | # When you need the analysis to take place in a directory other than the one from which it was launched 84 | #-Dsonar.projectBaseDir= # optional, default is . 85 | # Comma-separated paths to directories containing test source files. 86 | #-Dsonar.tests= # optional. For more info about Code Coverage, please refer to https://docs.sonarcloud.io/enriching/test-coverage/overview/ 87 | # Adds more detail to both client and server-side analysis logs, activating DEBUG mode for the scanner, and adding client-side environment variables and system properties to the server-side log of analysis report processing. 88 | #-Dsonar.verbose= # optional, default is false 89 | -------------------------------------------------------------------------------- /crl/crlloader/urlcrlloader_test.go: -------------------------------------------------------------------------------- 1 | package crlloader 2 | 3 | import ( 4 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/suite" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | type URLLoaderSuite struct { 17 | suite.Suite 18 | logger *zap.Logger 19 | } 20 | 21 | func (suite *URLLoaderSuite) SetupTest() { 22 | // Initialize the logger before each test 23 | logger, _ := zap.NewDevelopment() 24 | suite.logger = logger 25 | } 26 | 27 | func (suite *URLLoaderSuite) TestLoadCRL() { 28 | // Create a temporary directory to store the downloaded CRL 29 | tmpDir, err := os.MkdirTemp("", "test-crl-dir-") 30 | assert.NoError(suite.T(), err, "Error creating temporary directory") 31 | defer utils.CloseWithErrorHandling(func() error { return os.RemoveAll(tmpDir) }) 32 | 33 | // Create a mock HTTP server that serves the CRL content 34 | mockCRLContent := "Mock CRL Data" 35 | mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 | w.Write([]byte(mockCRLContent)) 37 | })) 38 | defer mockServer.Close() 39 | 40 | // Create a URLLoader instance 41 | urlLoader := URLLoader{ 42 | UrlString: mockServer.URL, 43 | Logger: suite.logger, 44 | } 45 | 46 | // Define the path to save the downloaded CRL 47 | downloadFilePath := filepath.Join(tmpDir, "downloaded-crl.crl") 48 | 49 | // Call the LoadCRL method 50 | err = urlLoader.LoadCRL(downloadFilePath) 51 | assert.NoError(suite.T(), err, "Error loading CRL") 52 | 53 | // Check if the downloaded CRL matches the expected content 54 | downloadedCRLData, err := os.ReadFile(downloadFilePath) 55 | assert.NoError(suite.T(), err, "Error reading downloaded CRL") 56 | assert.Equal(suite.T(), mockCRLContent, string(downloadedCRLData), "Downloaded CRL content doesn't match") 57 | } 58 | 59 | func (suite *URLLoaderSuite) TestLoadCRLWithInvalidUrl() { 60 | // Create a temporary directory to store the downloaded CRL 61 | tmpDir, err := os.MkdirTemp("", "test-crl-dir-") 62 | assert.NoError(suite.T(), err, "Error creating temporary directory") 63 | defer utils.CloseWithErrorHandling(func() error { return os.RemoveAll(tmpDir) }) 64 | 65 | // Create a mock HTTP server that serves the CRL content 66 | mockCRLContent := "Mock CRL Data" 67 | mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 68 | w.Write([]byte(mockCRLContent)) 69 | })) 70 | defer mockServer.Close() 71 | 72 | // Create a URLLoader instance 73 | urlLoader := URLLoader{ 74 | UrlString: "httppppp:dsdsd", 75 | Logger: suite.logger, 76 | } 77 | 78 | // Define the path to save the downloaded CRL 79 | downloadFilePath := filepath.Join(tmpDir, "downloaded-crl.crl") 80 | 81 | // Call the LoadCRL method 82 | err = urlLoader.LoadCRL(downloadFilePath) 83 | assert.Error(suite.T(), err) 84 | assert.Contains(suite.T(), err.Error(), "unsupported protocol") 85 | } 86 | 87 | func (suite *URLLoaderSuite) TestLoadCRLWhereCRLTargetPathDoesNotExist() { 88 | // Create a temporary directory to store the downloaded CRL 89 | tmpDir, err := os.MkdirTemp("", "test-crl-dir-") 90 | assert.NoError(suite.T(), err, "Error creating temporary directory") 91 | defer utils.CloseWithErrorHandling(func() error { return os.RemoveAll(tmpDir) }) 92 | 93 | // Create a mock HTTP server that serves the CRL content 94 | mockCRLContent := "Mock CRL Data" 95 | mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 96 | w.Write([]byte(mockCRLContent)) 97 | })) 98 | defer mockServer.Close() 99 | 100 | // Create a URLLoader instance 101 | urlLoader := URLLoader{ 102 | UrlString: mockServer.URL, 103 | Logger: suite.logger, 104 | } 105 | 106 | // Define the path to save the downloaded CRL 107 | downloadFilePath := filepath.Join(tmpDir, "notexist/downloaded-crl.crl") 108 | 109 | // Call the LoadCRL method 110 | err = urlLoader.LoadCRL(downloadFilePath) 111 | assert.Error(suite.T(), err) 112 | } 113 | 114 | func (suite *URLLoaderSuite) TestGetCRLLocationIdentifier() { 115 | // Create a URLLoader instance 116 | urlLoader := URLLoader{ 117 | UrlString: "http://example.com/crl", 118 | Logger: suite.logger, 119 | } 120 | 121 | // Call the GetCRLLocationIdentifier method 122 | identifier, err := urlLoader.GetCRLLocationIdentifier() 123 | assert.NoError(suite.T(), err, "Error getting CRL location identifier") 124 | assert.NotEmpty(suite.T(), identifier, "CRL location identifier is empty") 125 | } 126 | 127 | func (suite *URLLoaderSuite) TestGetCRLLocationIdentifierWithInvalidUrl() { 128 | // Create a URLLoader instance 129 | urlLoader := URLLoader{ 130 | UrlString: "bongo://:dsdsd", 131 | Logger: suite.logger, 132 | } 133 | 134 | // Call the GetCRLLocationIdentifier method 135 | _, err := urlLoader.GetCRLLocationIdentifier() 136 | assert.Error(suite.T(), err) 137 | } 138 | 139 | func (suite *URLLoaderSuite) TestGetDescription() { 140 | // Create a URLLoader instance 141 | urlLoader := URLLoader{ 142 | UrlString: "http://example.com/crl", 143 | Logger: suite.logger, 144 | } 145 | 146 | // Call the GetDescription method 147 | description := urlLoader.GetDescription() 148 | assert.NotEmpty(suite.T(), description, "Description is empty") 149 | } 150 | 151 | func TestURLLoaderSuite(t *testing.T) { 152 | // Run the test suite 153 | suite.Run(t, new(URLLoaderSuite)) 154 | } 155 | -------------------------------------------------------------------------------- /caddyfile.go: -------------------------------------------------------------------------------- 1 | package revocation 2 | 3 | import ( 4 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 5 | "github.com/gr33nbl00d/caddy-revocation-validator/config" 6 | "strconv" 7 | ) 8 | 9 | type CertRevocationValidatorConfig struct { 10 | CRLConfig *config.CRLConfig 11 | OCSPConfig *config.OCSPConfig 12 | Mode string 13 | } 14 | 15 | func parseConfigFromCaddyfile(d *caddyfile.Dispenser) (*CertRevocationValidatorConfig, error) { 16 | // initialize structs 17 | crlConfig := config.CRLConfig{ 18 | CDPConfig: &config.CDPConfig{}, 19 | CRLUrls: []string{}, 20 | CRLFiles: []string{}, 21 | TrustedSignatureCertsFiles: []string{}, 22 | } 23 | ocspConfig := config.OCSPConfig{ 24 | TrustedResponderCertsFiles: []string{}, 25 | } 26 | certRevocationValidatorConfig := CertRevocationValidatorConfig{ 27 | OCSPConfig: &ocspConfig, 28 | CRLConfig: &crlConfig, 29 | } 30 | 31 | for d.Next() { 32 | for nesting := d.Nesting(); d.NextBlock(nesting); { 33 | key := d.Val() 34 | validatorConfig, err, done := parseConfigEntryFromCaddyfile(d, key, certRevocationValidatorConfig) 35 | if done { 36 | return validatorConfig, err 37 | } 38 | } 39 | } 40 | return &certRevocationValidatorConfig, nil 41 | } 42 | 43 | func parseConfigEntryFromCaddyfile(d *caddyfile.Dispenser, key string, certRevocationValidatorConfig CertRevocationValidatorConfig) (*CertRevocationValidatorConfig, error, bool) { 44 | switch key { 45 | case "mode": 46 | if !d.NextArg() { 47 | return nil, d.ArgErr(), true 48 | } 49 | certRevocationValidatorConfig.Mode = d.Val() 50 | case "crl_config": 51 | crlConfig, err := parseCaddyfileCRLConfig(d) 52 | if err != nil { 53 | return nil, err, true 54 | } 55 | certRevocationValidatorConfig.CRLConfig = crlConfig 56 | case "ocsp_config": 57 | ocspConfig, err := parseCaddyfileOCSPConfig(d) 58 | if err != nil { 59 | return nil, err, true 60 | } 61 | certRevocationValidatorConfig.OCSPConfig = ocspConfig 62 | default: 63 | return nil, d.Errf("unknown subdirective for the revocation verifier: %s", key), true 64 | } 65 | return nil, nil, false 66 | } 67 | 68 | func parseCaddyfileOCSPConfig(d *caddyfile.Dispenser) (*config.OCSPConfig, error) { 69 | ocspConfig := config.OCSPConfig{} 70 | for nesting := d.Nesting(); d.NextBlock(nesting); { 71 | switch d.Val() { 72 | case "default_cache_duration": 73 | if !d.NextArg() { 74 | return nil, d.ArgErr() 75 | } 76 | ocspConfig.DefaultCacheDuration = d.Val() 77 | case "trusted_responder_cert_file": 78 | if !d.NextArg() { 79 | return nil, d.ArgErr() 80 | } 81 | ocspConfig.TrustedResponderCertsFiles = append(ocspConfig.TrustedResponderCertsFiles, d.Val()) 82 | case "ocsp_aia_strict": 83 | if !d.NextArg() { 84 | return nil, d.ArgErr() 85 | } 86 | ocspConfig.OCSPAIAStrict = false 87 | default: 88 | return nil, d.Errf("unknown subdirective for the ocsp config in the revocation verifier: %s", d.Val()) 89 | } 90 | } 91 | return &ocspConfig, nil 92 | } 93 | 94 | func parseCaddyfileCRLConfig(d *caddyfile.Dispenser) (*config.CRLConfig, error) { 95 | crlConfig := config.CRLConfig{} 96 | for nesting := d.Nesting(); d.NextBlock(nesting); { 97 | c, err, done := parseCaddyFileCrlConfigEntry(d, crlConfig) 98 | if done { 99 | return c, err 100 | } 101 | } 102 | return &crlConfig, nil 103 | } 104 | 105 | func parseCaddyFileCrlConfigEntry(d *caddyfile.Dispenser, crlConfig config.CRLConfig) (*config.CRLConfig, error, bool) { 106 | switch d.Val() { 107 | case "work_dir": 108 | if !d.NextArg() { 109 | return nil, d.ArgErr(), true 110 | } 111 | crlConfig.WorkDir = d.Val() 112 | case "cdp_config": 113 | cdpConfig, err := parseCaddyfileCRLCDPConfig(d) 114 | if err != nil { 115 | return nil, err, true 116 | } 117 | crlConfig.CDPConfig = cdpConfig 118 | case "storage_type": 119 | if !d.NextArg() { 120 | return nil, d.ArgErr(), true 121 | } 122 | crlConfig.StorageType = d.Val() 123 | case "update_interval": 124 | if !d.NextArg() { 125 | return nil, d.ArgErr(), true 126 | } 127 | 128 | crlConfig.UpdateInterval = d.Val() 129 | case "signature_validation_mode": 130 | if !d.NextArg() { 131 | return nil, d.ArgErr(), true 132 | } 133 | 134 | crlConfig.SignatureValidationMode = d.Val() 135 | case "crl_url": 136 | if !d.NextArg() { 137 | return nil, d.ArgErr(), true 138 | } 139 | 140 | crlConfig.CRLUrls = append(crlConfig.CRLUrls, d.Val()) 141 | case "crl_file": 142 | if !d.NextArg() { 143 | return nil, d.ArgErr(), true 144 | } 145 | 146 | crlConfig.CRLFiles = append(crlConfig.CRLFiles, d.Val()) 147 | case "trusted_signature_cert_file": 148 | if !d.NextArg() { 149 | return nil, d.ArgErr(), true 150 | } 151 | 152 | crlConfig.TrustedSignatureCertsFiles = append(crlConfig.TrustedSignatureCertsFiles, d.Val()) 153 | default: 154 | return nil, d.Errf("unknown subdirective for the crl config in the revocation verifier: %s", d.Val()), true 155 | } 156 | return nil, nil, false 157 | } 158 | 159 | func parseCaddyfileCRLCDPConfig(d *caddyfile.Dispenser) (*config.CDPConfig, error) { 160 | cdpConfig := config.CDPConfig{} 161 | for nesting := d.Nesting(); d.NextBlock(nesting); { 162 | switch d.Val() { 163 | case "crl_fetch_mode": 164 | if !d.NextArg() { 165 | return nil, d.ArgErr() 166 | } 167 | cdpConfig.CRLFetchMode = d.Val() 168 | case "crl_cdp_strict": 169 | if !d.NextArg() { 170 | return nil, d.ArgErr() 171 | } 172 | 173 | b, err := strconv.ParseBool(d.Val()) 174 | if err != nil { 175 | return nil, d.ArgErr() 176 | } 177 | cdpConfig.CRLCDPStrict = b 178 | } 179 | } 180 | return &cdpConfig, nil 181 | } 182 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/x509" 5 | "time" 6 | ) 7 | 8 | type CRLFetchMode int 9 | 10 | const ( 11 | CRLFetchModeActively CRLFetchMode = iota 12 | CRLFetchModeBackground 13 | ) 14 | 15 | type SignatureValidationMode int 16 | 17 | const ( 18 | SignatureValidationModeNone SignatureValidationMode = iota 19 | SignatureValidationModeVerifyLog 20 | SignatureValidationModeVerify 21 | ) 22 | 23 | type StorageType int 24 | 25 | const ( 26 | Memory StorageType = iota 27 | Disk 28 | ) 29 | 30 | type RevocationCheckMode int 31 | 32 | const ( 33 | RevocationCheckModePreferOCSP RevocationCheckMode = iota 34 | RevocationCheckModePreferCRL 35 | RevocationCheckModeCRLOnly 36 | RevocationCheckModeOCSPOnly 37 | RevocationCheckModeDisabled 38 | ) 39 | 40 | type CDPConfig struct { 41 | // CRLFetchMode Configures how and when CRLs are downloaded for the first time 42 | // Supported Values: 'fetch_actively', 'fetch_background' 43 | // See: https://github.com/Gr33nbl00d/caddy-revocation-validator#crl_fetch_mode 44 | CRLFetchMode string `json:"crl_fetch_mode,omitempty"` 45 | // CRLCDPStrict Configures if CRL checking is mandatory to allow a connection if CDP is defined (Optional) (Default: false) 46 | // See: https://github.com/Gr33nbl00d/caddy-revocation-validator#crl_cdp_strict 47 | CRLCDPStrict bool `json:"crl_cdp_strict,omitempty"` 48 | // Configures how and when CRLs are downloaded for the first time 49 | // Supported Values: 'fetch_actively', 'fetch_background' 50 | // See: https://github.com/Gr33nbl00d/caddy-revocation-validator#crl_fetch_mode 51 | CRLFetchModeParsed CRLFetchMode `json:"-"` 52 | } 53 | 54 | type CRLConfig struct { 55 | // WorkDir Configures the working directory for temporary CRL downloads and for disk based persistent CRLs 56 | WorkDir string `json:"work_dir"` 57 | // CDPConfig Configures how CDP (Certificate Distribution Point Extension) entries in the client certificate are used 58 | CDPConfig *CDPConfig `json:"cdp_config,omitempty"` 59 | // StorageType Configures how to store CRLs locally (Optional) 60 | // Supported Values: 'memory', 'disk' Default: 'disk' 61 | // See: https://github.com/Gr33nbl00d/caddy-revocation-validator#storage_type 62 | StorageType string `json:"storage_type,omitempty"` 63 | // UpdateInterval The interval in which the already known CRLs will be updated. (Optional) (Default: 30 minutes) 64 | // Valid time units are “ns”, “us” (or “µs”), “ms”, “s”, “m”, “h” 65 | // See: https://pkg.go.dev/time#ParseDuration 66 | UpdateInterval string `json:"update_interval,omitempty"` 67 | // Configures the signature validation or the CRL (Optional) (Default: 'verify') 68 | // Supported Values: 'none', 'verify', 'verify_log' 69 | // See: https://github.com/Gr33nbl00d/caddy-revocation-validator#signature_validation_mode 70 | SignatureValidationMode string `json:"signature_validation_mode,omitempty"` 71 | // CRLUrls (Optional) A predefined list of http(s) urls pointing to CRLs. These lists will be checked for all client certificates. 72 | // The predefined CRLs will be loaded on startup and updated cyclic. 73 | // PEM and DER encoding are both supported 74 | CRLUrls []string `json:"crl_urls,omitempty"` 75 | // CRLFiles (Optional) A predefined list of files pointing to CRLs. These lists will be checked for all client certificates. 76 | // The predefined CRLs will be loaded on startup 77 | // PEM and DER encoding are both supported 78 | CRLFiles []string `json:"crl_files,omitempty"` 79 | // TrustedSignatureCertsFiles (Optional) A predefined list of files of CA certificates which are trusted for CRL signing. 80 | // These certificates will be used to verify CRL signature if the CRL signature cert is not part of the client cert chain. 81 | // If the signature cert is part of the client cert chain there is no need to configure a certificate here. 82 | // PEM and DER encoding are both supported 83 | TrustedSignatureCertsFiles []string `json:"trusted_signature_certs_files,omitempty"` 84 | 85 | SignatureValidationModeParsed SignatureValidationMode `json:"-"` 86 | StorageTypeParsed StorageType `json:"-"` 87 | TrustedSignatureCerts []*x509.Certificate `json:"-"` 88 | UpdateIntervalParsed time.Duration `json:"-"` 89 | } 90 | 91 | type OCSPConfig struct { 92 | // DefaultCacheDuration The default time to cache OCSP responses (Optional) (Default: 0) 93 | // Valid time units are “ns”, “us” (or “µs”), “ms”, “s”, “m”, “h” 94 | // If the default time is zero no caching will be performed. 95 | DefaultCacheDuration string `json:"default_cache_duration,omitempty"` 96 | // TrustedResponderCertsFiles (Optional) A predefined list of files of CA certificates which are trusted to verify the OCSP response signature. 97 | // These certificates will be used to verify OCSP response signature if the ocsp response signature cert is not part of the client cert chain. 98 | // If the signature cert is part of the client cert chain there is no need to configure a certificate here. 99 | // PEM and DER encoding are both supported 100 | TrustedResponderCertsFiles []string `json:"trusted_responder_certs_files,omitempty"` 101 | // OCSPAIAStrict Configures if OCSP checking is mandatory to allow a connection if AIA is defined (Optional) (Default: false) 102 | // In strict mode it is required that if an OCSP server is defined inside AIA extension at least 103 | // one OCSP server defined can be contacted to check for revocation. Or a valid response of one of the OCSP server is inside the cache 104 | // If no OCSP server can be contacted and no cached response is present or the validation of the OCSP response signature failed connection is denied. 105 | OCSPAIAStrict bool `json:"ocsp_aia_strict,omitempty"` 106 | 107 | TrustedResponderCerts []*x509.Certificate `json:"-"` 108 | DefaultCacheDurationParsed time.Duration `json:"-"` 109 | } 110 | -------------------------------------------------------------------------------- /crl/crlstore/map.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import ( 4 | "crypto/x509/pkix" 5 | "errors" 6 | "fmt" 7 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 8 | "github.com/gr33nbl00d/caddy-revocation-validator/core/hashing" 9 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader" 10 | "go.uber.org/zap" 11 | "math/big" 12 | ) 13 | 14 | type MapStore struct { 15 | Map map[string][]byte 16 | Serializer Serializer 17 | Logger *zap.Logger 18 | } 19 | 20 | func (S *MapStore) StartUpdateCrl(info *crlreader.CRLMetaInfo) error { 21 | metaInfoBytes, err := S.Serializer.SerializeMetaInfo(info) 22 | if err != nil { 23 | return fmt.Errorf("could not serialize CRLMetaInfo: %v", err) 24 | } 25 | err = S.set(MetaInfoKey, metaInfoBytes) 26 | if err != nil { 27 | return fmt.Errorf("could not update CRLMetaInfo: %v", err) 28 | } 29 | return nil 30 | } 31 | 32 | func (S *MapStore) InsertRevokedCert(entry *crlreader.CRLEntry) error { 33 | s := entry.Issuer.String() + "_" + entry.RevokedCertificate.SerialNumber.String() 34 | revokedCertBytes, err := S.Serializer.SerializeRevokedCert(entry.RevokedCertificate) 35 | if err != nil { 36 | return fmt.Errorf("could not serialize CRLEntry: %v", err) 37 | } 38 | err = S.set(s, revokedCertBytes) 39 | if err != nil { 40 | return fmt.Errorf("could not insert crl entry: %v", err) 41 | } 42 | return nil 43 | 44 | } 45 | func (S *MapStore) GetCertRevocationStatus(issuer *pkix.RDNSequence, certSerial *big.Int) (*core.RevocationStatus, error) { 46 | s := issuer.String() + "_" + certSerial.String() 47 | revokedCertBytes, err := S.get(s) 48 | 49 | revoked := false 50 | var revokedCert *pkix.RevokedCertificate 51 | if err == nil { 52 | revokedCert, err = S.Serializer.DeserializeRevokedCert(revokedCertBytes) 53 | revoked = true 54 | if err != nil { 55 | return nil, fmt.Errorf("could not deserialize revoked cert: %v", err) 56 | } 57 | } 58 | return &core.RevocationStatus{ 59 | Revoked: revoked, 60 | CRLRevokedCertEntry: revokedCert, 61 | }, nil 62 | } 63 | 64 | func (S *MapStore) GetCRLMetaInfo() (*crlreader.CRLMetaInfo, error) { 65 | crlMetaBytes, err := S.get(MetaInfoKey) 66 | if err == nil { 67 | return S.Serializer.DeserializeMetaInfo(crlMetaBytes) 68 | } 69 | return nil, err 70 | } 71 | 72 | func (S *MapStore) GetCRLExtMetaInfo() (*crlreader.ExtendedCRLMetaInfo, error) { 73 | crlMetaBytes, err := S.get(ExtendedMetaInfoKey) 74 | if err == nil { 75 | return S.Serializer.DeserializeMetaInfoExt(crlMetaBytes) 76 | } 77 | return nil, err 78 | } 79 | 80 | func (S *MapStore) UpdateExtendedMetaInfo(extMetaInfo *crlreader.ExtendedCRLMetaInfo) error { 81 | extMetaInfoBytes, err := S.Serializer.SerializeMetaInfoExt(extMetaInfo) 82 | if err != nil { 83 | return fmt.Errorf("could not serialize ExtendedCRLMetaInfo: %v", err) 84 | } 85 | err = S.set(ExtendedMetaInfoKey, extMetaInfoBytes) 86 | if err != nil { 87 | return fmt.Errorf("could not update ExtendedCRLMetaInfo: %v", err) 88 | } 89 | return nil 90 | } 91 | 92 | func (S *MapStore) UpdateSignatureCertificate(entry *core.CertificateChainEntry) error { 93 | err := S.set(SignatureCertKey, entry.RawCertificate) 94 | if err != nil { 95 | return fmt.Errorf("could not update signature certificate: %v", err) 96 | } 97 | return nil 98 | } 99 | 100 | func (S *MapStore) GetCRLSignatureCert() (*core.CertificateChainEntry, error) { 101 | certBytes, err := S.get(SignatureCertKey) 102 | if err != nil { 103 | return nil, fmt.Errorf("could not find signature key for crl: %v", err) 104 | } 105 | cert, err := S.Serializer.DeserializeSignatureCert(certBytes) 106 | if err != nil { 107 | return nil, fmt.Errorf("could not deserialize CRL signature certificate: %v", err) 108 | } 109 | return &core.CertificateChainEntry{RawCertificate: certBytes, Certificate: cert}, nil 110 | } 111 | 112 | func (S *MapStore) UpdateCRLLocations(crlLocations *core.CRLLocations) error { 113 | crlLocationBytes, err := S.Serializer.SerializeCRLLocations(crlLocations) 114 | if err != nil { 115 | return fmt.Errorf("could not serialize CRLLocations: %v", err) 116 | } 117 | err = S.set(CRLLocationKey, crlLocationBytes) 118 | if err != nil { 119 | return fmt.Errorf("could not update CRLLocations: %v", err) 120 | } 121 | return nil 122 | } 123 | 124 | func (S *MapStore) GetCRLLocations() (*core.CRLLocations, error) { 125 | crlLocationBytes, err := S.get(CRLLocationKey) 126 | if err == nil { 127 | return S.Serializer.DeserializeCRLLocations(crlLocationBytes) 128 | } 129 | return nil, err 130 | } 131 | 132 | func (S *MapStore) IsEmpty() bool { 133 | return len(S.Map) == 0 134 | } 135 | 136 | func (S *MapStore) Update(store CRLStore) error { 137 | var storeNew, ok = store.(*MapStore) 138 | if ok == false { 139 | return errors.New("invalid update store type") 140 | } 141 | // Copy the map from storeNew to S 142 | S.Map = make(map[string][]byte) 143 | for k, v := range storeNew.Map { 144 | S.Map[k] = v 145 | } 146 | storeNew.close() 147 | return nil 148 | } 149 | 150 | func (S *MapStore) Close() { 151 | //Nothing to do on close 152 | } 153 | 154 | func (S *MapStore) Delete() error { 155 | 156 | return nil 157 | } 158 | 159 | func (S *MapStore) set(key string, bytes []byte) error { 160 | S.Map[string(hashing.Sum64(key))] = bytes 161 | return nil 162 | } 163 | 164 | func (S *MapStore) get(key string) ([]byte, error) { 165 | bytes := S.Map[string(hashing.Sum64(key))] 166 | if bytes == nil { 167 | return nil, errors.New("entry not found") 168 | } 169 | return bytes, nil 170 | } 171 | 172 | func (S *MapStore) close() { 173 | S.Map = nil 174 | } 175 | 176 | type MapStoreFactory struct { 177 | Serializer Serializer 178 | Logger *zap.Logger 179 | } 180 | 181 | func (F MapStoreFactory) CreateStore(_ string, _ bool) (CRLStore, error) { 182 | return &MapStore{ 183 | Map: make(map[string][]byte, 0), 184 | Serializer: F.Serializer, 185 | Logger: F.Logger, 186 | }, nil 187 | } 188 | -------------------------------------------------------------------------------- /revocation.go: -------------------------------------------------------------------------------- 1 | package revocation 2 | 3 | import ( 4 | "crypto/x509" 5 | "errors" 6 | "fmt" 7 | "github.com/caddyserver/caddy/v2" 8 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 9 | "github.com/caddyserver/caddy/v2/modules/caddytls" 10 | "github.com/gr33nbl00d/caddy-revocation-validator/config" 11 | "github.com/gr33nbl00d/caddy-revocation-validator/crl" 12 | "github.com/gr33nbl00d/caddy-revocation-validator/ocsp" 13 | "go.uber.org/zap" 14 | "os" 15 | ) 16 | 17 | func init() { 18 | caddy.RegisterModule(&CertRevocationValidator{}) 19 | } 20 | 21 | // CertRevocationValidator Allows checking of client certificate revocation status based on CRL or OCSP 22 | type CertRevocationValidator struct { 23 | // Mode defines the "Revocation Check Mode" 24 | // Supported Values 'prefer_ocsp', 'prefer_crl', 'ocsp_only', 'crl_only', 'disabled' 25 | // See https://github.com/Gr33nbl00d/caddy-revocation-validator#mode 26 | Mode string `json:"mode"` 27 | // CRLConfig Contains the certificate revocation list configuration (Optional) 28 | CRLConfig *config.CRLConfig `json:"crl_config,omitempty"` 29 | // OCSPConfig Contains the Online Certificate Status Protocol configuration (Optional) 30 | OCSPConfig *config.OCSPConfig `json:"ocsp_config,omitempty"` 31 | logger *zap.Logger 32 | ctx caddy.Context 33 | crlRevocationChecker *crl.CRLRevocationChecker 34 | ocspRevocationChecker *ocsp.OCSPRevocationChecker 35 | ModeParsed config.RevocationCheckMode `json:"-"` 36 | } 37 | 38 | func (c *CertRevocationValidator) CaddyModule() caddy.ModuleInfo { 39 | return caddy.ModuleInfo{ 40 | ID: "tls.client_auth.verifier.revocation", 41 | New: func() caddy.Module { 42 | return new(CertRevocationValidator) 43 | }, 44 | } 45 | } 46 | 47 | // Provision sets up c 48 | func (c *CertRevocationValidator) Provision(ctx caddy.Context) error { 49 | c.ctx = ctx 50 | c.logger = ctx.Logger(c) 51 | c.logger.Info("start provisioning of caddy revocation validator") 52 | if isCRLCheckingEnabled(c) { 53 | c.crlRevocationChecker = &crl.CRLRevocationChecker{} 54 | } 55 | c.ocspRevocationChecker = &ocsp.OCSPRevocationChecker{} 56 | err := ParseConfig(c) 57 | if err != nil { 58 | return err 59 | } 60 | c.logger.Info("validating Config") 61 | err = validateConfig(c) 62 | if err != nil { 63 | return err 64 | } 65 | if isCRLCheckingEnabled(c) { 66 | c.logger.Info("crl checking was enabled start CRL provisioning") 67 | err = c.crlRevocationChecker.Provision(c.CRLConfig, c.logger) 68 | if err != nil { 69 | return err 70 | } 71 | } 72 | c.logger.Info("start ocsp provisioning") 73 | err = c.ocspRevocationChecker.Provision(c.OCSPConfig, c.logger) 74 | if err != nil { 75 | return err 76 | } 77 | c.logger.Info("finished provisioning of caddy revocation validator") 78 | return nil 79 | } 80 | 81 | func validateConfig(c *CertRevocationValidator) error { 82 | if c.ModeParsed == config.RevocationCheckModeDisabled { 83 | return nil 84 | } 85 | if isCRLCheckingEnabled(c) { 86 | if c.CRLConfig == nil { 87 | return errors.New("for CRL checking a working directory need to be defined in crl_config") 88 | } else { 89 | if c.CRLConfig.WorkDir == "" { 90 | return errors.New("for CRL checking a working directory need to be defined in crl_config") 91 | } 92 | stat, err := os.Stat(c.CRLConfig.WorkDir) 93 | if err != nil { 94 | return fmt.Errorf("error accessing working directory information %v", err) 95 | } 96 | if stat.IsDir() == false { 97 | return fmt.Errorf("working directory is not a directory %v", c.CRLConfig.WorkDir) 98 | } 99 | } 100 | } 101 | return nil 102 | } 103 | 104 | func (c *CertRevocationValidator) Cleanup() error { 105 | if isCRLCheckingEnabled(c) { 106 | err := c.crlRevocationChecker.Cleanup() 107 | if err != nil { 108 | return err 109 | } 110 | } 111 | err := c.ocspRevocationChecker.Cleanup() 112 | if err != nil { 113 | return err 114 | } 115 | return nil 116 | } 117 | 118 | func (c *CertRevocationValidator) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 119 | caddyConfig, err := parseConfigFromCaddyfile(d) 120 | if err != nil { 121 | return err 122 | } 123 | c.OCSPConfig = caddyConfig.OCSPConfig 124 | c.CRLConfig = caddyConfig.CRLConfig 125 | c.Mode = caddyConfig.Mode 126 | err = validateConfig(c) 127 | if err != nil { 128 | return err 129 | } 130 | return nil 131 | } 132 | 133 | func (c *CertRevocationValidator) VerifyClientCertificate(_ [][]byte, verifiedChains [][]*x509.Certificate) error { 134 | if len(verifiedChains) > 0 { 135 | clientCertificate := verifiedChains[0][0] 136 | if isOCSPCheckingEnabled(c) { 137 | revoked, err := c.ocspRevocationChecker.IsRevoked(clientCertificate, verifiedChains) 138 | if err != nil { 139 | return err 140 | } 141 | if revoked.Revoked { 142 | return errors.New("client certificate was revoked") 143 | } 144 | } 145 | if isCRLCheckingEnabled(c) { 146 | revoked, err := c.crlRevocationChecker.IsRevoked(clientCertificate, verifiedChains) 147 | if err != nil { 148 | return err 149 | } 150 | if revoked.Revoked { 151 | return errors.New("client certificate was revoked") 152 | } 153 | } 154 | 155 | } 156 | return nil 157 | } 158 | 159 | func isOCSPCheckingEnabled(c *CertRevocationValidator) bool { 160 | return c.ModeParsed == config.RevocationCheckModePreferCRL || c.ModeParsed == config.RevocationCheckModePreferOCSP || c.ModeParsed == config.RevocationCheckModeOCSPOnly 161 | } 162 | 163 | func isCRLCheckingEnabled(c *CertRevocationValidator) bool { 164 | return c.ModeParsed == config.RevocationCheckModePreferCRL || c.ModeParsed == config.RevocationCheckModePreferOCSP || c.ModeParsed == config.RevocationCheckModeCRLOnly 165 | } 166 | 167 | // Interface guards 168 | var ( 169 | _ caddy.Provisioner = (*CertRevocationValidator)(nil) 170 | _ caddy.CleanerUpper = (*CertRevocationValidator)(nil) 171 | _ caddytls.ClientCertificateVerifier = (*CertRevocationValidator)(nil) 172 | ) 173 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gr33nbl00d/caddy-revocation-validator 2 | 3 | go 1.22 4 | 5 | toolchain go1.22.3 6 | 7 | require ( 8 | github.com/caddyserver/caddy/v2 v2.8.4 9 | github.com/google/uuid v1.6.0 10 | github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021 11 | github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 12 | github.com/stretchr/testify v1.9.0 13 | github.com/syndtr/goleveldb v1.0.0 14 | go.uber.org/zap v1.27.0 15 | golang.org/x/crypto v0.23.0 16 | ) 17 | 18 | require ( 19 | filippo.io/edwards25519 v1.1.0 // indirect 20 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect 21 | github.com/Masterminds/goutils v1.1.1 // indirect 22 | github.com/Masterminds/semver/v3 v3.2.1 // indirect 23 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 24 | github.com/Microsoft/go-winio v0.6.1 // indirect 25 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/caddyserver/certmagic v0.21.3 // indirect 28 | github.com/caddyserver/zerossl v0.1.3 // indirect 29 | github.com/cespare/xxhash v1.1.0 // indirect 30 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 31 | github.com/chzyer/readline v1.5.1 // indirect 32 | github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/dgraph-io/badger v1.6.2 // indirect 35 | github.com/dgraph-io/badger/v2 v2.2007.4 // indirect 36 | github.com/dgraph-io/ristretto v0.1.1 // indirect 37 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect 38 | github.com/dustin/go-humanize v1.0.1 // indirect 39 | github.com/fsnotify/fsnotify v1.6.0 // indirect 40 | github.com/go-jose/go-jose/v3 v3.0.3 // indirect 41 | github.com/go-kit/kit v0.13.0 // indirect 42 | github.com/go-kit/log v0.2.1 // indirect 43 | github.com/go-logfmt/logfmt v0.6.0 // indirect 44 | github.com/go-sql-driver/mysql v1.7.1 // indirect 45 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 46 | github.com/golang/glog v1.2.4 // indirect 47 | github.com/golang/protobuf v1.5.4 // indirect 48 | github.com/golang/snappy v0.0.4 // indirect 49 | github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect 50 | github.com/huandu/xstrings v1.4.0 // indirect 51 | github.com/imdario/mergo v0.3.15 // indirect 52 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 53 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 54 | github.com/jackc/pgconn v1.14.3 // indirect 55 | github.com/jackc/pgio v1.0.0 // indirect 56 | github.com/jackc/pgpassfile v1.0.0 // indirect 57 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 58 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 59 | github.com/jackc/pgtype v1.14.0 // indirect 60 | github.com/jackc/pgx/v4 v4.18.3 // indirect 61 | github.com/klauspost/compress v1.17.8 // indirect 62 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 63 | github.com/libdns/libdns v0.2.2 // indirect 64 | github.com/manifoldco/promptui v0.9.0 // indirect 65 | github.com/mattn/go-colorable v0.1.13 // indirect 66 | github.com/mattn/go-isatty v0.0.20 // indirect 67 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 68 | github.com/mholt/acmez/v2 v2.0.1 // indirect 69 | github.com/miekg/dns v1.1.59 // indirect 70 | github.com/mitchellh/copystructure v1.2.0 // indirect 71 | github.com/mitchellh/go-ps v1.0.0 // indirect 72 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 73 | github.com/onsi/ginkgo v1.16.5 // indirect 74 | github.com/onsi/ginkgo/v2 v2.13.2 // indirect 75 | github.com/pkg/errors v0.9.1 // indirect 76 | github.com/pmezard/go-difflib v1.0.0 // indirect 77 | github.com/prometheus/client_golang v1.19.1 // indirect 78 | github.com/prometheus/client_model v0.5.0 // indirect 79 | github.com/prometheus/common v0.48.0 // indirect 80 | github.com/prometheus/procfs v0.12.0 // indirect 81 | github.com/quic-go/qpack v0.4.0 // indirect 82 | github.com/quic-go/quic-go v0.44.0 // indirect 83 | github.com/rs/xid v1.5.0 // indirect 84 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 85 | github.com/shopspring/decimal v1.3.1 // indirect 86 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 87 | github.com/slackhq/nebula v1.6.1 // indirect 88 | github.com/smallstep/certificates v0.26.1 // indirect 89 | github.com/smallstep/nosql v0.6.1 // indirect 90 | github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect 91 | github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect 92 | github.com/smallstep/truststore v0.13.0 // indirect 93 | github.com/spf13/cast v1.5.0 // indirect 94 | github.com/spf13/cobra v1.8.0 // indirect 95 | github.com/spf13/pflag v1.0.5 // indirect 96 | github.com/stretchr/objx v0.5.2 // indirect 97 | github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect 98 | github.com/urfave/cli v1.22.14 // indirect 99 | github.com/zeebo/blake3 v0.2.3 // indirect 100 | go.etcd.io/bbolt v1.3.9 // indirect 101 | go.step.sm/cli-utils v0.9.0 // indirect 102 | go.step.sm/crypto v0.45.0 // indirect 103 | go.step.sm/linkedca v0.20.1 // indirect 104 | go.uber.org/automaxprocs v1.5.3 // indirect 105 | go.uber.org/mock v0.4.0 // indirect 106 | go.uber.org/multierr v1.11.0 // indirect 107 | go.uber.org/zap/exp v0.2.0 // indirect 108 | golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect 109 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect 110 | golang.org/x/mod v0.17.0 // indirect 111 | golang.org/x/net v0.25.0 // indirect 112 | golang.org/x/sync v0.7.0 // indirect 113 | golang.org/x/sys v0.20.0 // indirect 114 | golang.org/x/term v0.20.0 // indirect 115 | golang.org/x/text v0.15.0 // indirect 116 | golang.org/x/time v0.5.0 // indirect 117 | golang.org/x/tools v0.21.0 // indirect 118 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect 119 | google.golang.org/grpc v1.63.2 // indirect 120 | google.golang.org/protobuf v1.34.1 // indirect 121 | gopkg.in/yaml.v3 v3.0.1 // indirect 122 | howett.net/plist v1.0.0 // indirect 123 | ) 124 | 125 | //replace github.com/caddyserver/caddy/v2 => d:\git\go\caddy 126 | -------------------------------------------------------------------------------- /crl/crlloader/filecrlloader_test.go: -------------------------------------------------------------------------------- 1 | package crlloader 2 | 3 | import ( 4 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/suite" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | type FileLoaderSuite struct { 16 | suite.Suite 17 | logger *zap.Logger 18 | tmpDir string // Store the path to the temporary directory 19 | } 20 | 21 | func (suite *FileLoaderSuite) SetupTest() { 22 | // Initialize the logger before each test 23 | logger, _ := zap.NewDevelopment() 24 | suite.logger = logger 25 | 26 | // Create a temporary directory 27 | tmpDir, err := os.MkdirTemp("", "test-crl-dir-") 28 | assert.NoError(suite.T(), err, "Error creating temporary directory") 29 | suite.tmpDir = tmpDir 30 | } 31 | 32 | func (suite *FileLoaderSuite) TearDownTest() { 33 | // Remove the temporary directory after each test 34 | 35 | err := utils.Retry(10, 3*time.Second, zap.NewExample(), func() error { 36 | err := os.RemoveAll(suite.tmpDir) 37 | assert.NoError(suite.T(), err, "Error removing temporary directory") 38 | return err 39 | }) 40 | assert.NoError(suite.T(), err) 41 | 42 | } 43 | 44 | func (suite *FileLoaderSuite) TestLoadCRL() { 45 | // Create a temporary CRL file within the temporary directory 46 | crlFile, err := os.CreateTemp(suite.tmpDir, "test-crl-*.crl") 47 | // Write some data to the temporary file 48 | _, err = crlFile.WriteString("Test CRL Data") 49 | assert.NoError(suite.T(), err) 50 | err = crlFile.Close() 51 | assert.NoError(suite.T(), err, "Error creating temporary CRL file") 52 | defer utils.CloseWithErrorHandling(func() error { return os.Remove(crlFile.Name()) }) 53 | 54 | // Create a FileLoader instance 55 | fileLoader := FileLoader{ 56 | FileName: crlFile.Name(), 57 | Logger: suite.logger, 58 | } 59 | 60 | // Define the path to copy the CRL to 61 | copyToPath := filepath.Join(suite.tmpDir, "copied-crl.crl") 62 | 63 | // Call the LoadCRL method 64 | err = fileLoader.LoadCRL(copyToPath) 65 | 66 | assert.NoError(suite.T(), err, "Error copying CRL") 67 | 68 | // Check if the CRL was copied correctly 69 | copiedCRLData, err := os.ReadFile(copyToPath) 70 | 71 | assert.NoError(suite.T(), err, "Error reading copied CRL") 72 | assert.Equal(suite.T(), "Test CRL Data", string(copiedCRLData), "Copied CRL data doesn't match") 73 | } 74 | 75 | func (suite *FileLoaderSuite) TestLoadCRLWhereCRLIsDirectory() { 76 | // Create a temporary CRL file within the temporary directory 77 | crlFile, err := os.CreateTemp(suite.tmpDir, "test-crl-*.crl") 78 | 79 | // Write some data to the temporary file 80 | _, err = crlFile.WriteString("Test CRL Data") 81 | assert.NoError(suite.T(), err) 82 | err = crlFile.Close() 83 | assert.NoError(suite.T(), err, "Error creating temporary CRL file") 84 | defer utils.CloseWithErrorHandling(func() error { return os.Remove(crlFile.Name()) }) 85 | 86 | // Create a FileLoader instance 87 | fileLoader := FileLoader{ 88 | FileName: suite.tmpDir, 89 | Logger: suite.logger, 90 | } 91 | 92 | // Define the path to copy the CRL to 93 | copyToPath := filepath.Join(suite.tmpDir, "copied-crl.crl") 94 | 95 | // Call the LoadCRL method 96 | err = fileLoader.LoadCRL(copyToPath) 97 | assert.Error(suite.T(), err, "should return an error") 98 | } 99 | 100 | func (suite *FileLoaderSuite) TestLoadCRLWhereCRLPathDoesNotExist() { 101 | 102 | // Create a temporary CRL file within the temporary directory 103 | crlFile, err := os.CreateTemp(suite.tmpDir, "test-crl-*.crl") 104 | 105 | // Write some data to the temporary file 106 | _, err = crlFile.WriteString("Test CRL Data") 107 | assert.NoError(suite.T(), err) 108 | err = crlFile.Close() 109 | assert.NoError(suite.T(), err, "Error creating temporary CRL file") 110 | defer utils.CloseWithErrorHandling(func() error { return os.Remove(crlFile.Name()) }) 111 | 112 | // Create a FileLoader instance 113 | fileLoader := FileLoader{ 114 | FileName: crlFile.Name(), 115 | Logger: suite.logger, 116 | } 117 | 118 | // Define the path to copy the CRL to 119 | copyToPath := filepath.Join(suite.tmpDir, "nonexistent/nonexsistent.crl") 120 | 121 | // Call the LoadCRL method 122 | err = fileLoader.LoadCRL(copyToPath) 123 | assert.Error(suite.T(), err, "should return an error") 124 | } 125 | 126 | func (suite *FileLoaderSuite) TestLoadCRLWhereCRLTargetPathDoesNotExist() { 127 | 128 | invalidTargetPath := filepath.Join(suite.tmpDir, "nonexistent/nonexsistent.crl") 129 | // Create a FileLoader instance 130 | fileLoader := FileLoader{ 131 | FileName: invalidTargetPath, 132 | Logger: suite.logger, 133 | } 134 | 135 | // Define the path to copy the CRL to 136 | copyToPath := filepath.Join(suite.tmpDir, "copied-crl.crl") 137 | 138 | // Call the LoadCRL method 139 | err := fileLoader.LoadCRL(copyToPath) 140 | assert.Error(suite.T(), err, "should return an error") 141 | } 142 | 143 | func (suite *FileLoaderSuite) TestGetCRLLocationIdentifier() { 144 | // Create a temporary CRL file within the temporary directory 145 | crlFile, err := os.CreateTemp(suite.tmpDir, "test-crl-*.crl") 146 | assert.NoError(suite.T(), err, "Error creating temporary CRL file") 147 | err = crlFile.Close() 148 | assert.NoError(suite.T(), err, "Error creating temporary CRL file") 149 | defer utils.CloseWithErrorHandling(func() error { return os.Remove(crlFile.Name()) }) 150 | 151 | // Create a FileLoader instance 152 | fileLoader := FileLoader{ 153 | FileName: crlFile.Name(), 154 | Logger: suite.logger, 155 | } 156 | 157 | // Call the GetCRLLocationIdentifier method 158 | identifier, err := fileLoader.GetCRLLocationIdentifier() 159 | assert.NoError(suite.T(), err, "Error getting CRL location identifier") 160 | assert.NotEmpty(suite.T(), identifier, "CRL location identifier is empty") 161 | } 162 | 163 | func (suite *FileLoaderSuite) TestGetDescription() { 164 | // Create a temporary CRL file within the temporary directory 165 | crlFile, err := os.CreateTemp(suite.tmpDir, "test-crl-*.crl") 166 | assert.NoError(suite.T(), err, "Error creating temporary CRL file") 167 | err = crlFile.Close() 168 | assert.NoError(suite.T(), err, "Error creating temporary CRL file") 169 | defer utils.CloseWithErrorHandling(func() error { return os.Remove(crlFile.Name()) }) 170 | 171 | // Create a FileLoader instance 172 | fileLoader := FileLoader{ 173 | FileName: crlFile.Name(), 174 | Logger: suite.logger, 175 | } 176 | 177 | // Call the GetDescription method 178 | description := fileLoader.GetDescription() 179 | assert.NotEmpty(suite.T(), description, "Description is empty") 180 | } 181 | 182 | func TestFileLoaderSuite(t *testing.T) { 183 | // Run the test suite 184 | suite.Run(t, new(FileLoaderSuite)) 185 | } 186 | -------------------------------------------------------------------------------- /core/hashing/hashingreaderwrapper_test.go: -------------------------------------------------------------------------------- 1 | package hashing 2 | 3 | import ( 4 | "bufio" 5 | "crypto" 6 | "encoding/hex" 7 | "github.com/smallstep/assert" 8 | "github.com/stretchr/testify/suite" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | type HashingReaderWrapperTestSuite struct { 14 | suite.Suite 15 | sut HashingReaderWrapper 16 | reader *bufio.Reader 17 | } 18 | 19 | // In order for 'go test' to run this suite, we need to create 20 | // a normal test function and pass our suite to suite.Run 21 | func TestHashingReaderWrapperTestSuite(t *testing.T) { 22 | suite.Run(t, new(HashingReaderWrapperTestSuite)) 23 | } 24 | 25 | // Executed before each test 26 | func (s *HashingReaderWrapperTestSuite) SetupTest() { 27 | s.sut = HashingReaderWrapper{ 28 | Reader: bufio.NewReader(strings.NewReader("somebytestoread")), 29 | } 30 | } 31 | 32 | func (s *HashingReaderWrapperTestSuite) TestStartFinishCalculation() { 33 | assert.False(s.T(), s.sut.CalculateSignature) 34 | assert.Nil(s.T(), s.sut.hash) 35 | 36 | s.sut.StartHashCalculation(crypto.SHA512) 37 | 38 | assert.NotNil(s.T(), s.sut.hash) 39 | assert.True(s.T(), s.sut.CalculateSignature) 40 | 41 | result := s.sut.FinishHashCalculation() 42 | assert.False(s.T(), s.sut.CalculateSignature) 43 | assert.NotNil(s.T(), result) 44 | } 45 | 46 | func (s *HashingReaderWrapperTestSuite) TestHashOf5Bytes() { 47 | 48 | var buffer = make([]byte, 5) 49 | 50 | assert.False(s.T(), s.sut.CalculateSignature) 51 | assert.Nil(s.T(), s.sut.hash) 52 | 53 | s.sut.StartHashCalculation(crypto.SHA512) 54 | 55 | assert.NotNil(s.T(), s.sut.hash) 56 | assert.True(s.T(), s.sut.CalculateSignature) 57 | 58 | read, err := s.sut.Read(buffer) 59 | assert.Equals(s.T(), 5, read) 60 | assert.Nil(s.T(), err) 61 | 62 | resultHash := s.sut.FinishHashCalculation() 63 | assert.False(s.T(), s.sut.CalculateSignature) 64 | 65 | resultHashHex := hex.EncodeToString(resultHash) 66 | assert.Equals(s.T(), "75c33fcac3113bf8aeeede1d4243ba4cab52fb249e98b5692ee03463fc418ce421bfdf8f1d9b74cbf22143f32716cac2bbc5077d98c6c7941af26faf734c5b44", resultHashHex) 67 | } 68 | func (s *HashingReaderWrapperTestSuite) TestHashOf15BytesWithBigBuffer() { 69 | 70 | var buffer = make([]byte, 500) 71 | 72 | assert.False(s.T(), s.sut.CalculateSignature) 73 | assert.Nil(s.T(), s.sut.hash) 74 | 75 | s.sut.StartHashCalculation(crypto.SHA512) 76 | 77 | assert.NotNil(s.T(), s.sut.hash) 78 | assert.True(s.T(), s.sut.CalculateSignature) 79 | 80 | read, err := s.sut.Read(buffer) 81 | assert.Equals(s.T(), 15, read) 82 | assert.Nil(s.T(), err) 83 | 84 | resultHash := s.sut.FinishHashCalculation() 85 | assert.False(s.T(), s.sut.CalculateSignature) 86 | 87 | resultHashHex := hex.EncodeToString(resultHash) 88 | assert.Equals(s.T(), "dbeaa9858872f3bc58f35cb958793d18f87e0c041d01653aac921771a5c15a6a464201a6b267f47ff86b9c8827a3b0c91a606b93e321759b9bcdfa73ab475903", resultHashHex) 89 | } 90 | 91 | func (s *HashingReaderWrapperTestSuite) TestHashOfLast4BytesIgnoringFirst11Bytes() { 92 | 93 | var buffer = make([]byte, 4) 94 | 95 | assert.False(s.T(), s.sut.CalculateSignature) 96 | assert.Nil(s.T(), s.sut.hash) 97 | 98 | s.sut.StartHashCalculation(crypto.SHA512) 99 | err := s.sut.Discard(11) 100 | assert.NoError(s.T(), err) 101 | 102 | assert.NotNil(s.T(), s.sut.hash) 103 | assert.True(s.T(), s.sut.CalculateSignature) 104 | 105 | read, err := s.sut.Read(buffer) 106 | assert.Equals(s.T(), 4, read) 107 | assert.Nil(s.T(), err) 108 | 109 | resultHash := s.sut.FinishHashCalculation() 110 | assert.False(s.T(), s.sut.CalculateSignature) 111 | 112 | resultHashHex := hex.EncodeToString(resultHash) 113 | assert.Equals(s.T(), "ee021c5aa94c55f1dbbe287200618d386799f21ce4e35af71c9e7474267ebaf5fde5436ea44d689c8abd9dbb24e76da9493f982453cad987d1ca003f9eb9ef34", resultHashHex) 114 | } 115 | 116 | func (s *HashingReaderWrapperTestSuite) TestHashOfFirst5BytesAfterReset() { 117 | 118 | var buffer = make([]byte, 5) 119 | 120 | assert.False(s.T(), s.sut.CalculateSignature) 121 | assert.Nil(s.T(), s.sut.hash) 122 | 123 | read, err := s.sut.Read(buffer) 124 | s.sut.Reset(strings.NewReader("somebytestoread")) 125 | s.sut.StartHashCalculation(crypto.SHA512) 126 | assert.NotNil(s.T(), s.sut.hash) 127 | assert.True(s.T(), s.sut.CalculateSignature) 128 | 129 | read, err = s.sut.Read(buffer) 130 | assert.Equals(s.T(), 5, read) 131 | assert.Nil(s.T(), err) 132 | 133 | resultHash := s.sut.FinishHashCalculation() 134 | assert.False(s.T(), s.sut.CalculateSignature) 135 | 136 | resultHashHex := hex.EncodeToString(resultHash) 137 | assert.Equals(s.T(), "75c33fcac3113bf8aeeede1d4243ba4cab52fb249e98b5692ee03463fc418ce421bfdf8f1d9b74cbf22143f32716cac2bbc5077d98c6c7941af26faf734c5b44", resultHashHex) 138 | } 139 | 140 | func (s *HashingReaderWrapperTestSuite) TestHashOf5BytesAfter2ByteRead() { 141 | s.sut = HashingReaderWrapper{ 142 | Reader: bufio.NewReader(strings.NewReader(" somebytestoread")), 143 | } 144 | 145 | var buffer = make([]byte, 5) 146 | 147 | assert.False(s.T(), s.sut.CalculateSignature) 148 | assert.Nil(s.T(), s.sut.hash) 149 | 150 | //read 2 empty bytes before starting signature calculation 151 | var smallbuffer = make([]byte, 2) 152 | read, err := s.sut.Read(smallbuffer) 153 | assert.Equals(s.T(), 2, read) 154 | assert.Nil(s.T(), err) 155 | 156 | s.sut.StartHashCalculation(crypto.SHA512) 157 | 158 | assert.NotNil(s.T(), s.sut.hash) 159 | assert.True(s.T(), s.sut.CalculateSignature) 160 | 161 | read, err = s.sut.Read(buffer) 162 | assert.Equals(s.T(), 5, read) 163 | assert.Nil(s.T(), err) 164 | 165 | resultHash := s.sut.FinishHashCalculation() 166 | assert.False(s.T(), s.sut.CalculateSignature) 167 | 168 | resultHashHex := hex.EncodeToString(resultHash) 169 | assert.Equals(s.T(), "75c33fcac3113bf8aeeede1d4243ba4cab52fb249e98b5692ee03463fc418ce421bfdf8f1d9b74cbf22143f32716cac2bbc5077d98c6c7941af26faf734c5b44", resultHashHex) 170 | } 171 | func (s *HashingReaderWrapperTestSuite) TestHashOf5BytesAWithPeek() { 172 | 173 | var buffer = make([]byte, 5) 174 | 175 | assert.False(s.T(), s.sut.CalculateSignature) 176 | assert.Nil(s.T(), s.sut.hash) 177 | 178 | s.sut.StartHashCalculation(crypto.SHA512) 179 | 180 | assert.NotNil(s.T(), s.sut.hash) 181 | assert.True(s.T(), s.sut.CalculateSignature) 182 | 183 | read, err := s.sut.Read(buffer) 184 | assert.Equals(s.T(), 5, read) 185 | assert.Nil(s.T(), err) 186 | 187 | peek, err := s.sut.Peek(2) 188 | assert.Nil(s.T(), err) 189 | assert.Equals(s.T(), []byte("yt"), peek) 190 | 191 | resultHash := s.sut.FinishHashCalculation() 192 | assert.False(s.T(), s.sut.CalculateSignature) 193 | 194 | resultHashHex := hex.EncodeToString(resultHash) 195 | assert.Equals(s.T(), "75c33fcac3113bf8aeeede1d4243ba4cab52fb249e98b5692ee03463fc418ce421bfdf8f1d9b74cbf22143f32716cac2bbc5077d98c6c7941af26faf734c5b44", resultHashHex) 196 | } 197 | -------------------------------------------------------------------------------- /crl/crlrevocationchecker.go: -------------------------------------------------------------------------------- 1 | package crl 2 | 3 | import ( 4 | "crypto/x509" 5 | "fmt" 6 | "github.com/gr33nbl00d/caddy-revocation-validator/config" 7 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 8 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlrepository" 9 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlstore" 10 | "go.uber.org/zap" 11 | "log" 12 | "runtime/debug" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | type CRLRevocationChecker struct { 18 | crlRepository *crlrepository.Repository 19 | crlConfig *config.CRLConfig 20 | logger *zap.Logger 21 | crlUpdateTicker *time.Ticker 22 | crlUpdateStop chan struct{} 23 | } 24 | 25 | func (c *CRLRevocationChecker) IsRevoked(clientCertificate *x509.Certificate, verifiedChains [][]*x509.Certificate) (*core.RevocationStatus, error) { 26 | //TODO verify if crl if defined in cdp is fresh otherwise might deny connnection with some grace time see 27 | //TODO See RFC 5019 Section 4 - Ensuring an OCSPResponse Is Fresh 28 | var locations *core.CRLLocations 29 | 30 | if len(clientCertificate.CRLDistributionPoints) > 0 { 31 | chains := core.NewCertificateChains(verifiedChains, c.crlConfig.TrustedSignatureCerts) 32 | locations = &core.CRLLocations{CRLDistributionPoints: clientCertificate.CRLDistributionPoints} 33 | added, err := c.crlRepository.AddCRL(locations, chains) 34 | if err != nil { 35 | c.logger.Warn("Failed to add CRL from CDP", zap.Strings("cdp", clientCertificate.CRLDistributionPoints), zap.Error(err)) 36 | } else { 37 | if added && c.crlConfig.CDPConfig.CRLFetchModeParsed == config.CRLFetchModeBackground { 38 | go c.updateCRLs(true) 39 | } 40 | } 41 | 42 | } 43 | 44 | revoked, err := c.crlRepository.IsRevoked(clientCertificate, locations) 45 | return revoked, err 46 | } 47 | 48 | func (c *CRLRevocationChecker) Provision(crlConfig *config.CRLConfig, logger *zap.Logger) error { 49 | err := RegisterCRLWorkDirUsage(crlConfig) 50 | if err != nil { 51 | return err 52 | } 53 | c.crlConfig = crlConfig 54 | c.logger = logger 55 | db := crlstore.Map 56 | if crlConfig.StorageTypeParsed == config.Disk { 57 | db = crlstore.LevelDB 58 | } 59 | logger.Info("creating crl repository of type " + crlstore.StoreTypeToString(db)) 60 | err, c.crlRepository = crlrepository.NewCRLRepository(c.logger.Named("revocation"), crlConfig, db) 61 | if err != nil { 62 | return err 63 | } 64 | c.crlRepository.DeleteTempFilesIfExist() 65 | 66 | logger.Info("creating crl certificate chains") 67 | chains := core.NewCertificateChains(nil, crlConfig.TrustedSignatureCerts) 68 | logger.Info("adding crl entries from crl_urls config") 69 | err = c.addCrlUrlsFromConfig(chains) 70 | if err != nil { 71 | return err 72 | } 73 | logger.Info("adding crl entries from crl_files config") 74 | err = c.addCrlFilesFromConfig(chains) 75 | if err != nil { 76 | return err 77 | } 78 | logger.Info("Initializing CRL update ticker") 79 | c.initCRLUpdateTicker() 80 | return nil 81 | } 82 | 83 | func (c *CRLRevocationChecker) Cleanup() error { 84 | if c.crlConfig != nil { 85 | DeregisterCRLWorkDirUsage(c.crlConfig) 86 | } 87 | if c.crlRepository != nil { 88 | c.crlRepository.Close() 89 | } 90 | 91 | if c.crlUpdateTicker != nil { 92 | c.crlUpdateTicker.Stop() 93 | } 94 | return nil 95 | } 96 | func (c *CRLRevocationChecker) addCrlUrlsFromConfig(chains *core.CertificateChains) error { 97 | for _, crlUrl := range c.crlConfig.CRLUrls { 98 | crlLocations := core.CRLLocations{ 99 | CRLUrl: crlUrl, 100 | } 101 | _, err := c.crlRepository.AddCRL(&crlLocations, chains) 102 | if err != nil { 103 | return err 104 | } 105 | //update in case chains have changed 106 | c.logger.Info("Updating crl from location " + crlUrl) 107 | err = c.crlRepository.UpdateCRL(&crlLocations, chains) 108 | if err != nil { 109 | return err 110 | } 111 | } 112 | return nil 113 | } 114 | 115 | func (c *CRLRevocationChecker) addCrlFilesFromConfig(chains *core.CertificateChains) error { 116 | for _, crlFile := range c.crlConfig.CRLFiles { 117 | crlLocations := core.CRLLocations{ 118 | CRLFile: crlFile, 119 | } 120 | _, err := c.crlRepository.AddCRL(&crlLocations, chains) 121 | if err != nil { 122 | return err 123 | } 124 | //update in case chains have changed 125 | c.logger.Info("Updating crl from location " + crlFile) 126 | err = c.crlRepository.UpdateCRL(&crlLocations, chains) 127 | if err != nil { 128 | return err 129 | } 130 | } 131 | return nil 132 | } 133 | 134 | func (c *CRLRevocationChecker) initCRLUpdateTicker() { 135 | parsed := c.crlConfig.UpdateIntervalParsed 136 | c.crlUpdateTicker = time.NewTicker(parsed) 137 | c.crlUpdateStop = make(chan struct{}) 138 | go func() { 139 | defer func() { 140 | if err := recover(); err != nil { 141 | log.Printf("[PANIC] crl updater: %v\n%s", err, debug.Stack()) 142 | } 143 | }() 144 | c.updateCRLs(false) 145 | for { 146 | select { 147 | case <-c.crlUpdateStop: 148 | return 149 | case <-c.crlUpdateTicker.C: 150 | go c.updateCRLs(false) 151 | } 152 | } 153 | }() 154 | 155 | } 156 | func (c *CRLRevocationChecker) updateCRLs(forceUpdate bool) { 157 | crlUpdateMutex.Lock() 158 | defer crlUpdateMutex.Unlock() 159 | 160 | // If crl update was recently done, don't do it again for now. Although the ticker 161 | // drops missed ticks for us, config reloads discard the old ticker and replace it 162 | // with a new one, possibly invoking a cleaning to happen again too soon. 163 | // (We divide the interval by 2 because the crl update takes non-zero time, 164 | // and we don't want to skip crl updates if we don't have to; whereas if a crl update 165 | // took the entire interval, we'd probably want to skip the next one, so we aren't 166 | // constantly updating. This allows crl updates to take up to half the interval's 167 | // duration before we decide to skip the next one.) 168 | if !forceUpdate && c.updateWasRecentlyFinished() { 169 | return 170 | } 171 | defer func() { 172 | // mark when crl update was last finished 173 | lastCrlUpdateFinishTime = time.Now() 174 | }() 175 | 176 | c.crlRepository.UpdateCRLs() 177 | } 178 | 179 | func (c *CRLRevocationChecker) updateWasRecentlyFinished() bool { 180 | return !lastCrlUpdateFinishTime.IsZero() && (time.Since(lastCrlUpdateFinishTime) < c.crlConfig.UpdateIntervalParsed/2) 181 | } 182 | 183 | func RegisterCRLWorkDirUsage(crlConfig *config.CRLConfig) error { 184 | workDirInUseMutex.Lock() 185 | defer workDirInUseMutex.Unlock() 186 | if workDirsInUse[crlConfig.WorkDir] == 1 { 187 | return fmt.Errorf("the same work dir %s was defined for multiple servers", crlConfig.WorkDir) 188 | } 189 | workDirsInUse[crlConfig.WorkDir] = 1 190 | return nil 191 | } 192 | 193 | func DeregisterCRLWorkDirUsage(crlConfig *config.CRLConfig) { 194 | workDirInUseMutex.Lock() 195 | defer workDirInUseMutex.Unlock() 196 | workDirsInUse[crlConfig.WorkDir] = 0 197 | } 198 | 199 | var ( 200 | workDirsInUse = make(map[string]int) 201 | workDirInUseMutex sync.Mutex 202 | crlUpdateMutex sync.Mutex 203 | lastCrlUpdateFinishTime time.Time 204 | ) 205 | -------------------------------------------------------------------------------- /ocsp/ocsprevocationchecker.go: -------------------------------------------------------------------------------- 1 | package ocsp 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/x509" 7 | "errors" 8 | "fmt" 9 | "github.com/gr33nbl00d/caddy-revocation-validator/config" 10 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 11 | "github.com/gr33nbl00d/caddy-revocation-validator/core/asn1parser" 12 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 13 | "github.com/muesli/cache2go" 14 | "go.uber.org/zap" 15 | "golang.org/x/crypto/ocsp" 16 | "io" 17 | "net/http" 18 | "net/url" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | const ( 24 | maxClockSkew = 900 * time.Second 25 | ) 26 | 27 | type OCSPRevocationChecker struct { 28 | ocspConfig *config.OCSPConfig 29 | logger *zap.Logger 30 | cache *cache2go.CacheTable 31 | } 32 | 33 | func (c *OCSPRevocationChecker) IsRevoked(clientCertificate *x509.Certificate, verifiedChains [][]*x509.Certificate) (*core.RevocationStatus, error) { 34 | subjectRDNSequence, err := asn1parser.ParseSubjectRDNSequence(clientCertificate) 35 | if err != nil { 36 | return nil, err 37 | } 38 | cacheKey := subjectRDNSequence.String() + "_" + clientCertificate.SerialNumber.String() 39 | cache, err := c.tryGetResponseFromCache(cacheKey) 40 | if err == nil { 41 | return cache, nil 42 | } else { 43 | c.logger.Debug("certificate not found in cache", zap.String("certificate", clientCertificate.Subject.String()), zap.Error(err)) 44 | } 45 | 46 | chains := core.NewCertificateChains(verifiedChains, c.ocspConfig.TrustedResponderCerts) 47 | //TODO Support AIA via clientCertificate.IssuingCertificateURL 48 | issuer, err := asn1parser.ParseIssuerRDNSequence(clientCertificate) 49 | if err != nil { 50 | return nil, err 51 | } 52 | certCandidates, err := core.FindCertificateIssuerCandidates(issuer, &clientCertificate.Extensions, clientCertificate.PublicKeyAlgorithm, chains) 53 | ocspServerList := c.filterHTTPOCSPServers(clientCertificate.OCSPServer) 54 | var output []byte = nil 55 | for _, ocspServer := range ocspServerList { 56 | for _, certCandidate := range certCandidates { 57 | output, err = c.executeHttpRequest(ocspServer, clientCertificate, certCandidate.Certificate) 58 | if err != nil { 59 | c.logger.Debug("ocsp server revocation query failed", zap.String("ocsp_server", ocspServer), zap.Error(err)) 60 | continue 61 | } 62 | 63 | if output == nil { 64 | continue 65 | } 66 | ocspResponse, err := c.parseOcspResponse(certCandidates, output, ocspServer) 67 | if err != nil { 68 | c.logger.Debug("failed to parse ocsp server response", zap.String("ocsp_server", ocspServer), zap.Error(err)) 69 | continue 70 | } 71 | revocationStatus := core.RevocationStatus{ 72 | Revoked: false, 73 | OcspResponse: ocspResponse, 74 | } 75 | 76 | if ocspResponse.Status == ocsp.Revoked { 77 | revocationStatus = core.RevocationStatus{ 78 | Revoked: true, 79 | OcspResponse: ocspResponse, 80 | } 81 | } 82 | evictionTime := c.calculateEvictionTime(ocspResponse) 83 | if evictionTime > 0 { 84 | c.cache.Add(cacheKey, evictionTime, revocationStatus) 85 | } 86 | return &revocationStatus, nil 87 | } 88 | } 89 | if len(ocspServerList) > 0 && c.ocspConfig.OCSPAIAStrict { 90 | return nil, fmt.Errorf("failed to check revocation status on all ocsp servers: %v", ocspServerList) 91 | } else { 92 | c.logger.Warn("failed to check revocation status on all ocsp servers", zap.Strings("ocsp_servers", ocspServerList)) 93 | return &core.RevocationStatus{ 94 | Revoked: false, 95 | }, nil 96 | } 97 | 98 | } 99 | 100 | func (c *OCSPRevocationChecker) calculateEvictionTime(response *ocsp.Response) time.Duration { 101 | timeTillNextUpdate := response.NextUpdate.Sub(time.Now()) 102 | if timeTillNextUpdate > 0 { 103 | return timeTillNextUpdate + maxClockSkew 104 | } else { 105 | return c.ocspConfig.DefaultCacheDurationParsed 106 | } 107 | } 108 | 109 | func (c *OCSPRevocationChecker) parseOcspResponse(certCandidates []*core.CertificateChainEntry, output []byte, ocspServer string) (*ocsp.Response, error) { 110 | ocspResponse, err := ocsp.ParseResponse(output, nil) 111 | if err == nil { 112 | return ocspResponse, nil 113 | } 114 | for _, certCandidate := range certCandidates { 115 | ocspResponse, err := ocsp.ParseResponse(output, certCandidate.Certificate) 116 | if err != nil { 117 | c.logger.Debug("failed to parse ocsp server response", zap.String("ocsp_server", ocspServer), zap.Error(err)) 118 | continue 119 | } 120 | return ocspResponse, nil 121 | } 122 | return nil, errors.New("unable to parse ocsp response with any certificate available") 123 | } 124 | 125 | func (c *OCSPRevocationChecker) Provision(ocspConfig *config.OCSPConfig, logger *zap.Logger) error { 126 | c.ocspConfig = ocspConfig 127 | c.logger = logger 128 | return nil 129 | } 130 | 131 | func (c *OCSPRevocationChecker) Cleanup() error { 132 | if c.cache != nil { 133 | c.cache.Flush() 134 | } 135 | 136 | return nil 137 | } 138 | 139 | func (c *OCSPRevocationChecker) executeHttpRequest(ocspServer string, clientCert *x509.Certificate, issuerCert *x509.Certificate) ([]byte, error) { 140 | opts := &ocsp.RequestOptions{Hash: crypto.SHA1} 141 | buffer, err := ocsp.CreateRequest(clientCert, issuerCert, opts) 142 | if err != nil { 143 | return nil, err 144 | } 145 | httpRequest, err := c.prepareHttpRequest(ocspServer, buffer) 146 | if err != nil { 147 | return nil, err 148 | } 149 | httpClient := &http.Client{} 150 | httpResponse, err := httpClient.Do(httpRequest) 151 | if err != nil { 152 | return nil, err 153 | } 154 | defer utils.CloseWithErrorHandling(httpResponse.Body.Close) 155 | output, err := io.ReadAll(httpResponse.Body) 156 | if err != nil { 157 | return nil, err 158 | } 159 | return output, nil 160 | } 161 | 162 | func (c *OCSPRevocationChecker) prepareHttpRequest(ocspServer string, httpBody []byte) (*http.Request, error) { 163 | httpRequest, err := http.NewRequest(http.MethodPost, ocspServer, bytes.NewBuffer(httpBody)) 164 | if err != nil { 165 | return nil, err 166 | } 167 | ocspUrl, err := url.Parse(ocspServer) 168 | if err != nil { 169 | return nil, err 170 | } 171 | httpRequest.Header.Add("Content-Type", "application/ocsp-request") 172 | httpRequest.Header.Add("Accept", "application/ocsp-response") 173 | httpRequest.Header.Add("host", ocspUrl.Host) 174 | return httpRequest, nil 175 | } 176 | 177 | func (c *OCSPRevocationChecker) filterHTTPOCSPServers(ocspServerList []string) []string { 178 | httpOcspUrls := make([]string, 0) 179 | for _, ocspServer := range ocspServerList { 180 | if strings.HasPrefix(strings.ToLower(ocspServer), "http") { 181 | httpOcspUrls = append(httpOcspUrls, ocspServer) 182 | } 183 | } 184 | return httpOcspUrls 185 | } 186 | 187 | func (c *OCSPRevocationChecker) tryGetResponseFromCache(cacheKey string) (*core.RevocationStatus, error) { 188 | c.cache = cache2go.Cache("ocsp_client") 189 | 190 | // Let's retrieve the item from the cache. 191 | res, err := c.cache.Value(cacheKey) 192 | if err == nil { 193 | response := res.Data().(core.RevocationStatus) 194 | return &response, nil 195 | } else { 196 | return nil, err 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /configparser.go: -------------------------------------------------------------------------------- 1 | package revocation 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "fmt" 7 | "github.com/gr33nbl00d/caddy-revocation-validator/config" 8 | "os" 9 | "time" 10 | ) 11 | 12 | const defaultCRLUpdateInterval = 30 * time.Minute 13 | 14 | func ParseConfig(certRevocationValidator *CertRevocationValidator) error { 15 | certRevocationValidator.logger.Info("parsing caddy revocation validator config") 16 | if certRevocationValidator.CRLConfig != nil { 17 | certRevocationValidator.logger.Info("parsing crl config") 18 | err := parseCRLConfig(certRevocationValidator.CRLConfig) 19 | if err != nil { 20 | return err 21 | } 22 | } 23 | 24 | if certRevocationValidator.OCSPConfig != nil { 25 | certRevocationValidator.logger.Info("parsing ocsp config") 26 | err := parseOCSPConfig(certRevocationValidator.OCSPConfig) 27 | if err != nil { 28 | return err 29 | } 30 | } else { 31 | certRevocationValidator.OCSPConfig = &config.OCSPConfig{ 32 | TrustedResponderCertsFiles: make([]string, 0), 33 | DefaultCacheDuration: "", 34 | TrustedResponderCerts: make([]*x509.Certificate, 0), 35 | DefaultCacheDurationParsed: 0, 36 | } 37 | } 38 | certRevocationValidator.logger.Info("parsing mode") 39 | err := parseMode(certRevocationValidator) 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | 46 | func parseMode(revocationValidator *CertRevocationValidator) error { 47 | if len(revocationValidator.Mode) > 0 { 48 | switch revocationValidator.Mode { 49 | case "prefer_crl": 50 | revocationValidator.ModeParsed = config.RevocationCheckModePreferCRL 51 | case "prefer_ocsp": 52 | revocationValidator.ModeParsed = config.RevocationCheckModePreferOCSP 53 | case "ocsp_only": 54 | revocationValidator.ModeParsed = config.RevocationCheckModeOCSPOnly 55 | case "crl_only": 56 | revocationValidator.ModeParsed = config.RevocationCheckModeCRLOnly 57 | case "disabled": 58 | revocationValidator.ModeParsed = config.RevocationCheckModeDisabled 59 | default: 60 | return fmt.Errorf("mode not recognized: %s", revocationValidator.Mode) 61 | } 62 | } else { 63 | revocationValidator.ModeParsed = config.RevocationCheckModePreferOCSP 64 | } 65 | return nil 66 | 67 | } 68 | 69 | func parseCDPConfig(cdpConfig *config.CDPConfig) error { 70 | if len(cdpConfig.CRLFetchMode) > 0 { 71 | switch cdpConfig.CRLFetchMode { 72 | case "fetch_actively": 73 | cdpConfig.CRLFetchModeParsed = config.CRLFetchModeActively 74 | case "fetch_background": 75 | cdpConfig.CRLFetchModeParsed = config.CRLFetchModeBackground 76 | default: 77 | return fmt.Errorf("crl_fetch_mode not recognized: %s", cdpConfig.CRLFetchMode) 78 | } 79 | } else { 80 | cdpConfig.CRLFetchModeParsed = config.CRLFetchModeActively 81 | } 82 | return nil 83 | } 84 | 85 | func parseCRLConfig(crlConfig *config.CRLConfig) error { 86 | err := parseSignatureValidationMode(crlConfig) 87 | if err != nil { 88 | return err 89 | } 90 | err = parseStorageType(crlConfig) 91 | if err != nil { 92 | return err 93 | } 94 | err = parseUpdateInterval(crlConfig) 95 | if err != nil { 96 | return err 97 | } 98 | err = parseTrustedCrlSignerCerts(crlConfig) 99 | if err != nil { 100 | return err 101 | } 102 | if crlConfig.CDPConfig != nil { 103 | err := parseCDPConfig(crlConfig.CDPConfig) 104 | if err != nil { 105 | return err 106 | } 107 | } else { 108 | crlConfig.CDPConfig = &config.CDPConfig{ 109 | CRLFetchMode: "", 110 | CRLFetchModeParsed: config.CRLFetchModeActively, 111 | CRLCDPStrict: false, 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | func parseOCSPConfig(ocspConfig *config.OCSPConfig) error { 118 | err := parseDefaultCacheDuration(ocspConfig) 119 | if err != nil { 120 | return err 121 | } 122 | err = parseTrustedOcspResponderCerts(ocspConfig) 123 | if err != nil { 124 | return err 125 | } 126 | return nil 127 | } 128 | 129 | func parseTrustedOcspResponderCerts(ocspConfig *config.OCSPConfig) error { 130 | ocspConfig.TrustedResponderCerts = make([]*x509.Certificate, 0) 131 | for _, certFile := range ocspConfig.TrustedResponderCertsFiles { 132 | certificate, err := parseCertFromFile(certFile) 133 | if err != nil { 134 | return err 135 | } 136 | ocspConfig.TrustedResponderCerts = append(ocspConfig.TrustedResponderCerts, certificate) 137 | } 138 | return nil 139 | } 140 | 141 | func parseDefaultCacheDuration(ocspConfig *config.OCSPConfig) error { 142 | if len(ocspConfig.DefaultCacheDuration) > 0 { 143 | duration, err := time.ParseDuration(ocspConfig.DefaultCacheDuration) 144 | if err != nil { 145 | return err 146 | } 147 | ocspConfig.DefaultCacheDurationParsed = duration 148 | } else { 149 | ocspConfig.DefaultCacheDurationParsed = time.Duration(0) 150 | } 151 | return nil 152 | } 153 | 154 | func parseTrustedCrlSignerCerts(crlConfig *config.CRLConfig) error { 155 | crlConfig.TrustedSignatureCerts = make([]*x509.Certificate, 0) 156 | for _, certFile := range crlConfig.TrustedSignatureCertsFiles { 157 | certificate, err := parseCertFromFile(certFile) 158 | if err != nil { 159 | return err 160 | } 161 | crlConfig.TrustedSignatureCerts = append(crlConfig.TrustedSignatureCerts, certificate) 162 | } 163 | return nil 164 | } 165 | 166 | func parseCertFromFile(certFile string) (*x509.Certificate, error) { 167 | certBytes, err := os.ReadFile(certFile) 168 | if err != nil { 169 | return nil, err 170 | } 171 | block, _ := pem.Decode(certBytes) 172 | if block == nil { 173 | return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", certFile) 174 | } 175 | certificate, err := x509.ParseCertificate(block.Bytes) 176 | if err != nil { 177 | return nil, fmt.Errorf("could not parse certificate from file #{certFile} %v", err) 178 | } 179 | return certificate, nil 180 | } 181 | 182 | func parseSignatureValidationMode(crlCfg *config.CRLConfig) error { 183 | if len(crlCfg.SignatureValidationMode) > 0 { 184 | switch crlCfg.SignatureValidationMode { 185 | case "none": 186 | crlCfg.SignatureValidationModeParsed = config.SignatureValidationModeNone 187 | case "verify_log": 188 | crlCfg.SignatureValidationModeParsed = config.SignatureValidationModeVerifyLog 189 | case "verify": 190 | crlCfg.SignatureValidationModeParsed = config.SignatureValidationModeVerify 191 | default: 192 | return fmt.Errorf("signature_validation_mode not recognized: %s", crlCfg.SignatureValidationMode) 193 | } 194 | } else { 195 | crlCfg.SignatureValidationModeParsed = config.SignatureValidationModeVerify 196 | } 197 | return nil 198 | } 199 | 200 | func parseStorageType(crlCfg *config.CRLConfig) error { 201 | if len(crlCfg.StorageType) > 0 { 202 | switch crlCfg.StorageType { 203 | case "memory": 204 | crlCfg.StorageTypeParsed = config.Memory 205 | case "disk": 206 | crlCfg.StorageTypeParsed = config.Disk 207 | default: 208 | return fmt.Errorf("storage_type not recognized: %s", crlCfg.StorageType) 209 | } 210 | } else { 211 | crlCfg.StorageTypeParsed = config.Disk 212 | } 213 | return nil 214 | } 215 | 216 | func parseUpdateInterval(config *config.CRLConfig) error { 217 | if len(config.UpdateInterval) > 0 { 218 | duration, err := time.ParseDuration(config.UpdateInterval) 219 | if err != nil { 220 | return err 221 | } 222 | config.UpdateIntervalParsed = duration 223 | } else { 224 | config.UpdateIntervalParsed = defaultCRLUpdateInterval 225 | } 226 | return nil 227 | } 228 | -------------------------------------------------------------------------------- /core/certificatechains.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/asn1" 9 | "errors" 10 | "fmt" 11 | "github.com/gr33nbl00d/caddy-revocation-validator/core/asn1parser" 12 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader/extensionsupport" 13 | ) 14 | 15 | type CertificateChains struct { 16 | CertificateChainList []CertificateChain 17 | } 18 | 19 | func (c *CertificateChains) AddCertificateChain(chain CertificateChain) { 20 | c.CertificateChainList = append(c.CertificateChainList, chain) 21 | } 22 | 23 | type CertificateChain struct { 24 | CertificateChainEntryList []CertificateChainEntry 25 | } 26 | 27 | func (c *CertificateChain) AddCertificateChainEntry(entry *CertificateChainEntry) { 28 | c.CertificateChainEntryList = append(c.CertificateChainEntryList, *entry) 29 | } 30 | 31 | type CertificateChainEntry struct { 32 | RawCertificate []byte 33 | Certificate *x509.Certificate 34 | } 35 | 36 | func NewCertificateChains(verifiedChains [][]*x509.Certificate, trustedSignerCerts []*x509.Certificate) *CertificateChains { 37 | chains := &CertificateChains{ 38 | CertificateChainList: make([]CertificateChain, 0), 39 | } 40 | for _, verifiedChain := range verifiedChains { 41 | chain := &CertificateChain{ 42 | CertificateChainEntryList: make([]CertificateChainEntry, 0), 43 | } 44 | for _, verifiedChainEntry := range verifiedChain { 45 | entry := CertificateChainEntry{ 46 | RawCertificate: verifiedChainEntry.Raw, 47 | Certificate: verifiedChainEntry, 48 | } 49 | chain.AddCertificateChainEntry(&entry) 50 | } 51 | chains.AddCertificateChain(*chain) 52 | } 53 | for _, trustedSignerCert := range trustedSignerCerts { 54 | chain := &CertificateChain{ 55 | CertificateChainEntryList: make([]CertificateChainEntry, 0), 56 | } 57 | entry := CertificateChainEntry{ 58 | RawCertificate: trustedSignerCert.Raw, 59 | Certificate: trustedSignerCert, 60 | } 61 | chain.AddCertificateChainEntry(&entry) 62 | chains.AddCertificateChain(*chain) 63 | } 64 | 65 | return chains 66 | } 67 | 68 | func NewCertificateChainsFromEntry(chainEntry *CertificateChainEntry) *CertificateChains { 69 | chains := &CertificateChains{ 70 | CertificateChainList: make([]CertificateChain, 0), 71 | } 72 | chain := &CertificateChain{ 73 | CertificateChainEntryList: make([]CertificateChainEntry, 0), 74 | } 75 | chain.AddCertificateChainEntry(chainEntry) 76 | chains.AddCertificateChain(*chain) 77 | return chains 78 | } 79 | 80 | // FindCertificateIssuerCandidates Implementation according to rfc5280 section 5.2.1 81 | func FindCertificateIssuerCandidates(issuer *pkix.RDNSequence, extensions *[]pkix.Extension, algorithmID x509.PublicKeyAlgorithm, chains *CertificateChains) ([]*CertificateChainEntry, error) { 82 | keyIdentifierExtension := extensionsupport.FindExtension(extensionsupport.OidCertExtAuthorityKeyId, extensions) 83 | if keyIdentifierExtension == nil { 84 | return findCertificateCandidatesByIssuerAndAlgorithm(issuer, algorithmID, chains) 85 | } else { 86 | authorityKeyIdentifier, err := parseKeyIdentifierFromExtension(keyIdentifierExtension) 87 | if err != nil { 88 | return nil, err 89 | } 90 | if authorityKeyIdentifier.AuthorityCertSerialNumber != nil && &authorityKeyIdentifier.AuthorityCertIssuer.Raw != nil { 91 | return findCertificateBySerialAndIssuer(authorityKeyIdentifier, chains) 92 | } else if authorityKeyIdentifier.KeyIdentifier != nil { 93 | return findCertificateCandidatesFromKeyIdentifier(chains, authorityKeyIdentifier) 94 | } else { 95 | return nil, errors.New("unsupported Authority Key Identifier combination") 96 | } 97 | } 98 | } 99 | 100 | func findCertificateCandidatesFromKeyIdentifier(verifiedChains *CertificateChains, authorityKeyIdentifier *extensionsupport.AuthorityKeyIdentifier) ([]*CertificateChainEntry, error) { 101 | var certificateCandidates = make([]*CertificateChainEntry, 0) 102 | for _, verifiedChain := range verifiedChains.CertificateChainList { 103 | for _, certCandidate := range verifiedChain.CertificateChainEntryList { 104 | subjectKeyIdentifierExtension := extensionsupport.FindExtension(extensionsupport.OidCertExtSubjectKeyId, &certCandidate.Certificate.Extensions) 105 | if subjectKeyIdentifierExtension != nil { 106 | subjectKeyIdReader := bufio.NewReader(bytes.NewReader(subjectKeyIdentifierExtension.Value)) 107 | subjectKeyId, err := asn1parser.ParseOctetString(subjectKeyIdReader) 108 | if err != nil { 109 | return nil, err 110 | } 111 | if bytes.Compare(authorityKeyIdentifier.KeyIdentifier, subjectKeyId) == 0 { 112 | certificateCandidates = append(certificateCandidates, &certCandidate) 113 | } 114 | } 115 | } 116 | } 117 | return certificateCandidates, nil 118 | } 119 | 120 | func findCertificateBySerialAndIssuer(identifier *extensionsupport.AuthorityKeyIdentifier, verifiedChains *CertificateChains) ([]*CertificateChainEntry, error) { 121 | var certificateCandidates = make([]*CertificateChainEntry, 0) 122 | for _, verifiedChain := range verifiedChains.CertificateChainList { 123 | for _, certCandidate := range verifiedChain.CertificateChainEntryList { 124 | if certCandidate.Certificate.SerialNumber.Cmp(identifier.AuthorityCertSerialNumber) == 0 { 125 | authorityCertIssuerDN := new(pkix.RDNSequence) 126 | if len(identifier.AuthorityCertIssuer.DirectoryName.Bytes) > 0 { 127 | reader := bufio.NewReader(bytes.NewReader(identifier.AuthorityCertIssuer.DirectoryName.Bytes)) 128 | err := asn1parser.ReadStruct(reader, authorityCertIssuerDN) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | certCandidateIssuer, err := asn1parser.ParseIssuerRDNSequence(certCandidate.Certificate) 134 | if err != nil { 135 | return nil, fmt.Errorf("failed to parse issuer rdn sequence from certificate %v", err) 136 | } 137 | 138 | if certCandidateIssuer.String() == authorityCertIssuerDN.String() { 139 | certificateCandidates = append(certificateCandidates, &certCandidate) 140 | } 141 | } 142 | } 143 | } 144 | } 145 | return certificateCandidates, nil 146 | } 147 | 148 | func findCertificateCandidatesByIssuerAndAlgorithm(issuer *pkix.RDNSequence, algorithmID x509.PublicKeyAlgorithm, verifiedChains *CertificateChains) ([]*CertificateChainEntry, error) { 149 | var certificateCandidates = make([]*CertificateChainEntry, 0) 150 | for _, verifiedChain := range verifiedChains.CertificateChainList { 151 | for _, certCandidate := range verifiedChain.CertificateChainEntryList { 152 | subjectRDNSequence, err := asn1parser.ParseSubjectRDNSequence(certCandidate.Certificate) 153 | if err != nil { 154 | return nil, err 155 | } 156 | if subjectRDNSequence.String() == issuer.String() { 157 | if certCandidate.Certificate.PublicKeyAlgorithm == algorithmID { 158 | certificateCandidates = append(certificateCandidates, &certCandidate) 159 | } 160 | } 161 | } 162 | } 163 | return certificateCandidates, nil 164 | } 165 | 166 | func parseKeyIdentifierFromExtension(keyIdentifierExtension *pkix.Extension) (*extensionsupport.AuthorityKeyIdentifier, error) { 167 | authorityKeyIdentifier := new(extensionsupport.AuthorityKeyIdentifier) 168 | _, err := asn1.Unmarshal(keyIdentifierExtension.Value, authorityKeyIdentifier) 169 | if err != nil { 170 | return nil, fmt.Errorf("could not parse AuthorityKeyIdentifier from extension") 171 | } 172 | return authorityKeyIdentifier, nil 173 | } 174 | -------------------------------------------------------------------------------- /crl/crlloader/multischemescrlloader_test.go: -------------------------------------------------------------------------------- 1 | package crlloader 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | "github.com/stretchr/testify/suite" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | // MockCRLLoader is a mock implementation of the CRLLoader interface 14 | type MockCRLLoader struct { 15 | mock.Mock 16 | } 17 | 18 | func (m *MockCRLLoader) LoadCRL(filePath string) error { 19 | args := m.Called(filePath) 20 | return args.Error(0) 21 | } 22 | 23 | func (m *MockCRLLoader) GetCRLLocationIdentifier() (string, error) { 24 | args := m.Called() 25 | return args.String(0), args.Error(1) 26 | } 27 | 28 | func (m *MockCRLLoader) GetDescription() string { 29 | args := m.Called() 30 | return args.String(0) 31 | } 32 | 33 | type MultiSchemesCRLLoaderSuite struct { 34 | suite.Suite 35 | logger *zap.Logger 36 | } 37 | 38 | func (suite *MultiSchemesCRLLoaderSuite) SetupTest() { 39 | // Initialize the logger before each test 40 | logger, _ := zap.NewDevelopment() 41 | suite.logger = logger 42 | } 43 | 44 | func (suite *MultiSchemesCRLLoaderSuite) TestLoadCRL_Success() { 45 | // Create two mock loaders 46 | mockLoader1 := new(MockCRLLoader) 47 | mockLoader2 := new(MockCRLLoader) 48 | 49 | // Configure the first loader to succeed 50 | mockLoader1.On("LoadCRL", mock.Anything).Return(nil) 51 | 52 | // Create a MultiSchemesCRLLoader with the two loaders 53 | loader := MultiSchemesCRLLoader{ 54 | Loaders: []CRLLoader{mockLoader1, mockLoader2}, 55 | Logger: suite.logger, 56 | } 57 | 58 | // Call the LoadCRL method 59 | err := loader.LoadCRL("test.crl") 60 | 61 | // Assert that the first loader was called and no error occurred 62 | assert.NoError(suite.T(), err) 63 | 64 | // Verify that the second loader was not called 65 | mockLoader2.AssertNotCalled(suite.T(), "LoadCRL", mock.Anything) 66 | } 67 | 68 | func (suite *MultiSchemesCRLLoaderSuite) TestLoadCRL_Failure() { 69 | // Create two mock loaders 70 | mockLoader1 := new(MockCRLLoader) 71 | mockLoader2 := new(MockCRLLoader) 72 | 73 | // Configure both loaders to fail 74 | mockLoader1.On("LoadCRL", mock.Anything).Return(fmt.Errorf("loader 1 failed")) 75 | mockLoader2.On("LoadCRL", mock.Anything).Return(fmt.Errorf("loader 2 failed")) 76 | // Configure both loaders to provide descriptions 77 | mockLoader1.On("GetDescription").Return("Loader 1") 78 | mockLoader2.On("GetDescription").Return("Loader 2") 79 | 80 | // Create a MultiSchemesCRLLoader with the two loaders 81 | loader := MultiSchemesCRLLoader{ 82 | Loaders: []CRLLoader{mockLoader1, mockLoader2}, 83 | Logger: suite.logger, 84 | } 85 | 86 | // Call the LoadCRL method 87 | err := loader.LoadCRL("test.crl") 88 | 89 | // Assert that an error occurred and no loader was successful 90 | assert.Error(suite.T(), err) 91 | assert.Contains(suite.T(), err.Error(), "failed to load CRL from all loaders") 92 | 93 | // Verify that both loaders were called 94 | mockLoader1.AssertCalled(suite.T(), "LoadCRL", "test.crl") 95 | mockLoader2.AssertCalled(suite.T(), "LoadCRL", "test.crl") 96 | } 97 | 98 | func (suite *MultiSchemesCRLLoaderSuite) TestLoadCRL_LastSuccessfulLoader() { 99 | // Create two mock loaders 100 | mockLoader1 := &MockCRLLoader{} 101 | mockLoader2 := &MockCRLLoader{} 102 | 103 | // Configure the first loader to fail initially and then succeed 104 | mockLoader1.On("LoadCRL", "test.crl"). 105 | Return(fmt.Errorf("loader 1 failed")). // Fails initially 106 | Once() // Only called once 107 | 108 | // Configure the second loader to succeed 109 | mockLoader2.On("LoadCRL", "test.crl").Return(nil) 110 | 111 | mockLoader1.On("GetDescription").Return("Loader 1") 112 | mockLoader2.On("GetDescription").Return("Loader 2") 113 | 114 | // Create a MultiSchemesCRLLoader with the two loaders 115 | loader := &MultiSchemesCRLLoader{ 116 | Loaders: []CRLLoader{mockLoader1, mockLoader2}, 117 | Logger: suite.logger, 118 | } 119 | 120 | // First load attempt (first loader fails, second loader succeeds) 121 | err := loader.LoadCRL("test.crl") 122 | assert.NoError(suite.T(), err) 123 | 124 | // Verify that both loades wre called 125 | mockLoader2.AssertCalled(suite.T(), "LoadCRL", "test.crl") 126 | mockLoader1.AssertCalled(suite.T(), "LoadCRL", "test.crl") 127 | 128 | mockLoader1.Calls = nil 129 | mockLoader2.Calls = nil 130 | mockLoader1.ExpectedCalls = nil 131 | 132 | // Now, configure the first loader to succeed 133 | mockLoader1.On("LoadCRL", "test.crl").Return(nil) 134 | 135 | // Second load attempt (second loader should be used as the last successful loader) 136 | err = loader.LoadCRL("test.crl") 137 | assert.NoError(suite.T(), err) 138 | 139 | // Verify that the second loader (last successful loader) was called 140 | mockLoader2.AssertCalled(suite.T(), "LoadCRL", "test.crl") 141 | mockLoader1.AssertNotCalled(suite.T(), "LoadCRL", "test.crl") 142 | } 143 | 144 | func (suite *MultiSchemesCRLLoaderSuite) TestLoadCRL_LastSuccessfulLoaderFailsOnSecondAttempt() { 145 | // Create two mock loaders 146 | mockLoader1 := &MockCRLLoader{} 147 | mockLoader2 := &MockCRLLoader{} 148 | 149 | // Configure the first loader to fail initially and then succeed 150 | mockLoader1.On("LoadCRL", "test.crl"). 151 | Return(fmt.Errorf("loader 1 failed")). // Fails initially 152 | Once() // Only called once 153 | 154 | // Configure the second loader to succeed 155 | mockLoader2.On("LoadCRL", "test.crl").Return(nil) 156 | 157 | mockLoader1.On("GetDescription").Return("Loader 1") 158 | mockLoader2.On("GetDescription").Return("Loader 2") 159 | 160 | // Create a MultiSchemesCRLLoader with the two loaders 161 | loader := &MultiSchemesCRLLoader{ 162 | Loaders: []CRLLoader{mockLoader1, mockLoader2}, 163 | Logger: suite.logger, 164 | } 165 | 166 | // First load attempt (first loader fails, second loader succeeds) 167 | err := loader.LoadCRL("test.crl") 168 | assert.NoError(suite.T(), err) 169 | 170 | // Verify that both loades wre called 171 | mockLoader2.AssertCalled(suite.T(), "LoadCRL", "test.crl") 172 | mockLoader1.AssertCalled(suite.T(), "LoadCRL", "test.crl") 173 | 174 | mockLoader1.Calls = nil 175 | mockLoader2.Calls = nil 176 | mockLoader1.ExpectedCalls = nil 177 | mockLoader2.ExpectedCalls = nil 178 | 179 | // Now, configure the first loader to succeed 180 | mockLoader1.On("LoadCRL", "test.crl").Return(nil) 181 | mockLoader1.On("GetDescription").Return("Loader 1") 182 | 183 | // and second loader to fail: 184 | mockLoader2.On("LoadCRL", "test.crl"). 185 | Return(fmt.Errorf("loader 2 failed")). // Fails initially 186 | Once() // Only called once 187 | mockLoader2.On("GetDescription").Return("Loader 2") 188 | // Second load attempt (second loader should be used as the last successful loader) 189 | err = loader.LoadCRL("test.crl") 190 | assert.NoError(suite.T(), err) 191 | 192 | // Verify that both are called 193 | mockLoader2.AssertCalled(suite.T(), "LoadCRL", "test.crl") 194 | mockLoader1.AssertCalled(suite.T(), "LoadCRL", "test.crl") 195 | } 196 | 197 | func (suite *MultiSchemesCRLLoaderSuite) TestGetCRLLocationIdentifier() { 198 | // Create two mock loaders 199 | mockLoader1 := new(MockCRLLoader) 200 | mockLoader2 := new(MockCRLLoader) 201 | 202 | // Configure the loaders to return identifiers 203 | mockLoader1.On("GetCRLLocationIdentifier").Return("Loader1Identifier", nil) 204 | mockLoader2.On("GetCRLLocationIdentifier").Return("Loader2Identifier", nil) 205 | 206 | // Create a MultiSchemesCRLLoader with the two loaders 207 | loader := MultiSchemesCRLLoader{ 208 | Loaders: []CRLLoader{mockLoader1, mockLoader2}, 209 | Logger: suite.logger, 210 | } 211 | 212 | // Call the GetCRLLocationIdentifier method 213 | identifier, err := loader.GetCRLLocationIdentifier() 214 | 215 | // Assert that no error occurred and the identifiers are concatenated 216 | assert.NoError(suite.T(), err) 217 | assert.Equal(suite.T(), "5c489d06cdb283469537650b99d9aebcbb96685df2ec3003c95704cf2d45924f", identifier) 218 | } 219 | 220 | func (suite *MultiSchemesCRLLoaderSuite) TestGetDescription() { 221 | // Create two mock loaders 222 | mockLoader1 := new(MockCRLLoader) 223 | mockLoader2 := new(MockCRLLoader) 224 | 225 | // Configure the loaders to return descriptions 226 | mockLoader1.On("GetDescription").Return("Loader 1") 227 | mockLoader2.On("GetDescription").Return("Loader 2") 228 | 229 | // Create a MultiSchemesCRLLoader with the two loaders 230 | loader := MultiSchemesCRLLoader{ 231 | Loaders: []CRLLoader{mockLoader1, mockLoader2}, 232 | Logger: suite.logger, 233 | } 234 | 235 | // Call the GetDescription method 236 | description := loader.GetDescription() 237 | 238 | // Assert that the descriptions are concatenated 239 | assert.Equal(suite.T(), "Loader 1, Loader 2", description) 240 | } 241 | 242 | func TestMultiSchemesCRLLoaderSuite(t *testing.T) { 243 | // Run the test suite 244 | suite.Run(t, new(MultiSchemesCRLLoaderSuite)) 245 | } 246 | -------------------------------------------------------------------------------- /crl/crlstore/leveldb.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import ( 4 | "crypto/x509/pkix" 5 | "errors" 6 | "fmt" 7 | "github.com/google/uuid" 8 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 9 | "github.com/gr33nbl00d/caddy-revocation-validator/core/hashing" 10 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 11 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader" 12 | "github.com/syndtr/goleveldb/leveldb" 13 | "go.uber.org/zap" 14 | "math/big" 15 | "os" 16 | "path/filepath" 17 | "time" 18 | ) 19 | 20 | type LevelDbStore struct { 21 | Db *leveldb.DB 22 | Serializer Serializer 23 | Identifier string 24 | BasePath string 25 | LevelDBPath string 26 | Logger *zap.Logger 27 | } 28 | 29 | const retryCount = 5 30 | const retryDelay = 1 * time.Second 31 | 32 | func (S *LevelDbStore) StartUpdateCrl(info *crlreader.CRLMetaInfo) error { 33 | metaInfoBytes, err := S.Serializer.SerializeMetaInfo(info) 34 | if err != nil { 35 | return fmt.Errorf("could not serialize CRLMetaInfo: %v", err) 36 | } 37 | hash := hashing.Sum64(MetaInfoKey) 38 | err = S.Db.Put(hash, metaInfoBytes, nil) 39 | if err != nil { 40 | return fmt.Errorf("could not update CRLMetaInfo: %v", err) 41 | } 42 | return nil 43 | } 44 | 45 | func (S *LevelDbStore) InsertRevokedCert(entry *crlreader.CRLEntry) error { 46 | s := entry.Issuer.String() + "_" + entry.RevokedCertificate.SerialNumber.String() 47 | revokedCertBytes, err := S.Serializer.SerializeRevokedCert(entry.RevokedCertificate) 48 | if err != nil { 49 | return fmt.Errorf("could not serialize CRLEntry: %v", err) 50 | } 51 | hash := hashing.Sum64(s) 52 | err = S.Db.Put(hash, revokedCertBytes, nil) 53 | if err != nil { 54 | return fmt.Errorf("could not insert crl entry: %v", err) 55 | } 56 | return nil 57 | } 58 | func (S *LevelDbStore) GetCertRevocationStatus(issuer *pkix.RDNSequence, certSerial *big.Int) (*core.RevocationStatus, error) { 59 | s := issuer.String() + "_" + certSerial.String() 60 | hash := hashing.Sum64(s) 61 | revokedCertBytes, err := S.Db.Get(hash, nil) 62 | revoked := false 63 | var revokedCert *pkix.RevokedCertificate 64 | if err == nil { 65 | revokedCert, err = S.Serializer.DeserializeRevokedCert(revokedCertBytes) 66 | revoked = true 67 | if err != nil { 68 | return nil, fmt.Errorf("could not deserialize revoked cert: %v", err) 69 | } 70 | } 71 | return &core.RevocationStatus{ 72 | Revoked: revoked, 73 | CRLRevokedCertEntry: revokedCert, 74 | }, nil 75 | } 76 | 77 | func (S *LevelDbStore) GetCRLMetaInfo() (*crlreader.CRLMetaInfo, error) { 78 | hash := hashing.Sum64(MetaInfoKey) 79 | crlMetaBytes, err := S.Db.Get(hash, nil) 80 | if err == nil { 81 | return S.Serializer.DeserializeMetaInfo(crlMetaBytes) 82 | } 83 | return nil, err 84 | } 85 | 86 | func (S *LevelDbStore) GetCRLExtMetaInfo() (*crlreader.ExtendedCRLMetaInfo, error) { 87 | hash := hashing.Sum64(ExtendedMetaInfoKey) 88 | crlMetaBytes, err := S.Db.Get(hash, nil) 89 | if err == nil { 90 | return S.Serializer.DeserializeMetaInfoExt(crlMetaBytes) 91 | } 92 | return nil, err 93 | } 94 | 95 | func (S *LevelDbStore) UpdateExtendedMetaInfo(extMetaInfo *crlreader.ExtendedCRLMetaInfo) error { 96 | extMetaInfoBytes, err := S.Serializer.SerializeMetaInfoExt(extMetaInfo) 97 | if err != nil { 98 | return fmt.Errorf("could not serialize ExtendedCRLMetaInfo: %v", err) 99 | } 100 | hash := hashing.Sum64(ExtendedMetaInfoKey) 101 | err = S.Db.Put(hash, extMetaInfoBytes, nil) 102 | if err != nil { 103 | return fmt.Errorf("could not update ExtendedCRLMetaInfo: %v", err) 104 | } 105 | return nil 106 | } 107 | 108 | func (S *LevelDbStore) UpdateSignatureCertificate(entry *core.CertificateChainEntry) error { 109 | hash := hashing.Sum64(SignatureCertKey) 110 | err := S.Db.Put(hash, entry.RawCertificate, nil) 111 | if err != nil { 112 | return fmt.Errorf("could not update signature certificate: %v", err) 113 | } 114 | return nil 115 | } 116 | 117 | func (S *LevelDbStore) GetCRLSignatureCert() (*core.CertificateChainEntry, error) { 118 | hash := hashing.Sum64(SignatureCertKey) 119 | certBytes, err := S.Db.Get(hash, nil) 120 | if err != nil { 121 | return nil, fmt.Errorf("could not find signature key for crl: %v", err) 122 | } 123 | cert, err := S.Serializer.DeserializeSignatureCert(certBytes) 124 | if err != nil { 125 | return nil, fmt.Errorf("could not deserialize CRL signature certificate: %v", err) 126 | } 127 | return &core.CertificateChainEntry{RawCertificate: certBytes, Certificate: cert}, nil 128 | } 129 | 130 | func (S *LevelDbStore) UpdateCRLLocations(crlLocations *core.CRLLocations) error { 131 | crlLocationBytes, err := S.Serializer.SerializeCRLLocations(crlLocations) 132 | if err != nil { 133 | return fmt.Errorf("could not serialize CRLLocations: %v", err) 134 | } 135 | hash := hashing.Sum64(CRLLocationKey) 136 | err = S.Db.Put(hash, crlLocationBytes, nil) 137 | if err != nil { 138 | return fmt.Errorf("could not update CRLLocations: %v", err) 139 | } 140 | return nil 141 | } 142 | 143 | func (S *LevelDbStore) GetCRLLocations() (*core.CRLLocations, error) { 144 | hash := hashing.Sum64(CRLLocationKey) 145 | crlLocationBytes, err := S.Db.Get(hash, nil) 146 | if err == nil { 147 | return S.Serializer.DeserializeCRLLocations(crlLocationBytes) 148 | } 149 | return nil, err 150 | } 151 | 152 | func (S *LevelDbStore) IsEmpty() bool { 153 | hash := hashing.Sum64(MetaInfoKey) 154 | has, err := S.Db.Has(hash, nil) 155 | if err != nil { 156 | return true 157 | } 158 | if has == false { 159 | return true 160 | } 161 | return false 162 | } 163 | 164 | func (S *LevelDbStore) Update(store CRLStore) error { 165 | var levelDbNew, ok = store.(*LevelDbStore) 166 | if ok == false { 167 | return errors.New("invalid update store type") 168 | } 169 | err := S.closeDbWithRetries(S.Db) 170 | if err != nil { 171 | return err 172 | } 173 | err = S.closeDbWithRetries(levelDbNew.Db) 174 | if err != nil { 175 | return err 176 | } 177 | levelDBPath := filepath.Join(S.BasePath, S.Identifier) 178 | levelDBPathTemp, err := S.renameWithRetriesToTempDir(S.LevelDBPath) 179 | if err != nil { 180 | return err 181 | } 182 | err = S.renameWithRetries(levelDbNew.LevelDBPath, levelDBPath) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | err = S.removeWithRetries(levelDBPathTemp) 188 | if err != nil { 189 | S.Logger.Warn("failed to delete temporary path, will be deleted on next restart", zap.String("path", levelDBPathTemp)) 190 | } 191 | db, err := openDbWithRetries(levelDBPath, S.Logger) 192 | if err != nil { 193 | return err 194 | } 195 | S.Db = db 196 | return nil 197 | } 198 | 199 | func (S *LevelDbStore) closeDbWithRetries(db *leveldb.DB) error { 200 | err := utils.Retry(retryCount, retryDelay, S.Logger, func() error { 201 | return db.Close() 202 | }) 203 | return err 204 | } 205 | 206 | func (S *LevelDbStore) removeWithRetries(dirToRemove string) error { 207 | err := utils.Retry(retryCount, retryDelay, S.Logger, func() error { 208 | return os.RemoveAll(dirToRemove) 209 | }) 210 | return err 211 | } 212 | 213 | func (S *LevelDbStore) renameWithRetries(oldPath string, newPath string) error { 214 | err := utils.Retry(retryCount, retryDelay, S.Logger, func() error { 215 | return os.Rename(oldPath, newPath) 216 | }) 217 | return err 218 | } 219 | 220 | func (S *LevelDbStore) renameWithRetriesToTempDir(oldPath string) (newPath string, err error) { 221 | err = utils.Retry(retryCount, retryDelay, S.Logger, func() error { 222 | newPath, err = createRandomFileName(S.BasePath) 223 | if err != nil { 224 | return err 225 | } 226 | return os.Rename(oldPath, newPath) 227 | }) 228 | if err != nil { 229 | return "", err 230 | } 231 | return newPath, nil 232 | } 233 | 234 | func createRandomFileName(basePath string) (string, error) { 235 | newUUID, err := uuid.NewUUID() 236 | if err != nil { 237 | return "", err 238 | } 239 | newPath := filepath.Join(basePath, "crl_"+newUUID.String()+"_tmp") 240 | return newPath, nil 241 | } 242 | 243 | func (S *LevelDbStore) Close() { 244 | err := S.closeDbWithRetries(S.Db) 245 | if err != nil { 246 | S.Logger.Warn("failed to close database", zap.Error(err)) 247 | } 248 | } 249 | 250 | func (S *LevelDbStore) Delete() error { 251 | err := S.removeWithRetries(S.LevelDBPath) 252 | if err != nil { 253 | return err 254 | } 255 | return nil 256 | } 257 | 258 | type LevelDbStoreFactory struct { 259 | Serializer Serializer 260 | BasePath string 261 | Logger *zap.Logger 262 | } 263 | 264 | func (F LevelDbStoreFactory) CreateStore(identifier string, temporary bool) (CRLStore, error) { 265 | levelDBPath := filepath.Join(F.BasePath, identifier) 266 | if temporary { 267 | dir, err := createTempDirWithRetries(F.BasePath, F.Logger) 268 | if err != nil { 269 | return nil, err 270 | } 271 | levelDBPath = dir 272 | } 273 | 274 | err := os.MkdirAll(levelDBPath, 0700) 275 | if err != nil { 276 | return nil, fmt.Errorf("could not create dirctory for crl storage in %s cause: %v", levelDBPath, err) 277 | } 278 | db, err := openDbWithRetries(levelDBPath, F.Logger) 279 | if err != nil { 280 | return nil, fmt.Errorf("could not create leveldb store: %v", err) 281 | } 282 | return &LevelDbStore{ 283 | Db: db, 284 | Serializer: F.Serializer, 285 | Identifier: identifier, 286 | BasePath: F.BasePath, 287 | LevelDBPath: levelDBPath, 288 | Logger: F.Logger, 289 | }, nil 290 | } 291 | 292 | func createTempDirWithRetries(basePath string, logger *zap.Logger) (string, error) { 293 | var newPath = "" 294 | err := utils.Retry(retryCount, retryDelay, logger, func() (err error) { 295 | newPath, err = createRandomFileName(basePath) 296 | if err != nil { 297 | return err 298 | } 299 | err = os.Mkdir(newPath, 0700) 300 | if err != nil { 301 | return err 302 | } 303 | return nil 304 | }) 305 | return newPath, err 306 | 307 | } 308 | 309 | func openDbWithRetries(levelDBPath string, logger *zap.Logger) (*leveldb.DB, error) { 310 | var db *leveldb.DB 311 | err := utils.Retry(retryCount, retryDelay, logger, func() (err error) { 312 | db, err = leveldb.OpenFile(levelDBPath, nil) 313 | return err 314 | }) 315 | return db, err 316 | } 317 | -------------------------------------------------------------------------------- /crl/crlreader/crlreader.go: -------------------------------------------------------------------------------- 1 | package crlreader 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/x509/pkix" 7 | "encoding/asn1" 8 | "encoding/base64" 9 | "errors" 10 | "fmt" 11 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 12 | "github.com/gr33nbl00d/caddy-revocation-validator/core/asn1parser" 13 | "github.com/gr33nbl00d/caddy-revocation-validator/core/hashing" 14 | "github.com/gr33nbl00d/caddy-revocation-validator/core/pemreader" 15 | "github.com/gr33nbl00d/caddy-revocation-validator/core/signatureverify" 16 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 17 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader/extensionsupport" 18 | asn1crypto "golang.org/x/crypto/cryptobyte/asn1" 19 | "math/big" 20 | "os" 21 | "time" 22 | ) 23 | 24 | type CRLMetaInfo struct { 25 | Issuer pkix.RDNSequence 26 | ThisUpdate time.Time 27 | NextUpdate time.Time `asn1:"tag:0,optional"` 28 | } 29 | 30 | type ExtendedCRLMetaInfo struct { 31 | CRLNumber *big.Int `asn1:"tag:0,optional"` 32 | } 33 | 34 | type CRLEntry struct { 35 | Issuer *pkix.RDNSequence 36 | RevokedCertificate *pkix.RevokedCertificate 37 | } 38 | 39 | type CRLReadResult struct { 40 | HashAndVerifyStrategy *signatureverify.HashAndVerifyStrategies 41 | Signature *asn1parser.BitString 42 | CalculatedSignature []byte 43 | Issuer *pkix.RDNSequence 44 | CRLExtensions *[]pkix.Extension 45 | } 46 | 47 | type CRLProcessor interface { 48 | StartUpdateCrl(crlMetaInfo *CRLMetaInfo) error 49 | InsertRevokedCertificate(entry *CRLEntry) error 50 | UpdateExtendedMetaInfo(info *ExtendedCRLMetaInfo) error 51 | UpdateSignatureCertificate(entry *core.CertificateChainEntry) error 52 | } 53 | type CRLReader interface { 54 | ReadCRL(crlProcessor CRLProcessor, crlFilePath string) (*CRLReadResult, error) 55 | } 56 | 57 | type StreamingCRLFileReader struct { 58 | } 59 | 60 | func (StreamingCRLFileReader) ReadCRL(crlProcessor CRLProcessor, crlFilePath string) (*CRLReadResult, error) { 61 | 62 | crlFile, err := os.Open(crlFilePath) 63 | if err != nil { 64 | return nil, err 65 | } 66 | defer utils.CloseWithErrorHandling(crlFile.Close) 67 | algorithmIdentifier, err := findAlgorithmIdentifierInCRL(crlFile) 68 | if err != nil { 69 | return nil, err 70 | } 71 | err = seekToCRLBegin(crlFile) 72 | reader := newHashingCRLReader(crlFile) 73 | 74 | certificateListTL, err := asn1parser.ReadTagLength(&reader) 75 | if err != nil { 76 | return nil, err 77 | } 78 | err = asn1parser.ExpectTag(asn1crypto.SEQUENCE, certificateListTL.Tag) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | strategies, err := signatureverify.LookupHashAndVerifyStrategies(*algorithmIdentifier) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | reader.StartHashCalculation(strategies.HashStrategy) 89 | tbsCertListTL, err := asn1parser.ReadTagLength(&reader) 90 | if err != nil { 91 | return nil, err 92 | } 93 | err = asn1parser.ExpectTag(asn1crypto.SEQUENCE, tbsCertListTL.Tag) 94 | if err != nil { 95 | return nil, err 96 | } 97 | version := 1 98 | if versionExists(reader) { 99 | version, err = parseVersion(reader, version) 100 | if err != nil { 101 | return nil, err 102 | } 103 | } 104 | if version > 2 { 105 | return nil, errors.New(fmt.Sprintf("CRL version %d is an unknown version", version)) 106 | } 107 | _, _ = readAlgorithmIdentifier(&reader) //skip algorithm identifier 108 | issuer := new(pkix.RDNSequence) 109 | err = asn1parser.ReadStruct(&reader, issuer) 110 | if err != nil { 111 | return nil, err 112 | } 113 | thisUpdate, err := asn1parser.ReadUtcTime(&reader) 114 | if err != nil { 115 | return nil, err 116 | } 117 | var nextUpdate time.Time 118 | if nextUpdateTimeExists(reader) { 119 | utcTime, err := asn1parser.ReadUtcTime(&reader) 120 | if err != nil { 121 | return nil, err 122 | } 123 | nextUpdate = *utcTime 124 | } 125 | 126 | metaInfo := CRLMetaInfo{ 127 | *issuer, 128 | *thisUpdate, 129 | nextUpdate, 130 | } 131 | err = crlProcessor.StartUpdateCrl(&metaInfo) 132 | if err != nil { 133 | return nil, err 134 | } 135 | if revokedCertificateListExists(reader) { 136 | err := parseRevokedCertificateList(issuer, reader, crlProcessor) 137 | if err != nil { 138 | return nil, err 139 | } 140 | } 141 | var crlExtensions *[]pkix.Extension = nil 142 | var crlNumber *big.Int = nil 143 | if extensionsExists(reader, version) { 144 | crlExtensions, err = parseExtensions(reader) 145 | if err != nil { 146 | return nil, err 147 | } 148 | crlNumber, err = parseCRlNumberIfExists(crlExtensions) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | } 154 | extendedMetaInfo := ExtendedCRLMetaInfo{ 155 | crlNumber, 156 | } 157 | err = crlProcessor.UpdateExtendedMetaInfo(&extendedMetaInfo) 158 | if err != nil { 159 | return nil, err 160 | } 161 | err = extensionsupport.CheckForCriticalUnhandledCRLExtensions(crlExtensions) 162 | if err != nil { 163 | return nil, err 164 | } 165 | calculatedSignature := reader.FinishHashCalculation() 166 | _, _ = readAlgorithmIdentifier(&reader) //skip algorithm identifier / we already parsed it 167 | signatureBitString, err := asn1parser.ParseBitString(&reader) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | return &CRLReadResult{ 173 | HashAndVerifyStrategy: strategies, 174 | Signature: signatureBitString, 175 | CalculatedSignature: calculatedSignature, 176 | Issuer: issuer, 177 | CRLExtensions: crlExtensions, 178 | }, nil 179 | } 180 | 181 | func newHashingCRLReader(crlFile *os.File) hashing.HashingReaderWrapper { 182 | var reader hashing.HashingReaderWrapper 183 | _, pemFile := pemreader.IsPemFile(crlFile) 184 | if pemFile { 185 | reader = newHashingPEMCRLReader(crlFile) 186 | } else { 187 | reader = newHashingDERCRLReader(crlFile) 188 | } 189 | return reader 190 | } 191 | 192 | func parseCRlNumberIfExists(crlExtensions *[]pkix.Extension) (*big.Int, error) { 193 | extension := extensionsupport.FindExtension(extensionsupport.OidCrlExtCrlNumber, crlExtensions) 194 | if extension != nil { 195 | return asn1parser.ReadBigInt(bufio.NewReader(bytes.NewReader(extension.Value))) 196 | } 197 | return nil, nil 198 | } 199 | 200 | func seekToCRLBegin(crlFile *os.File) error { 201 | _, err := crlFile.Seek(0, 0) 202 | if err != nil { 203 | return fmt.Errorf("could not seek to begin of crl file: %v", err) 204 | } 205 | return err 206 | } 207 | 208 | func nextUpdateTimeExists(reader hashing.HashingReaderWrapper) bool { 209 | possibleTimeTL, err := asn1parser.PeekTagLength(&reader, 0) 210 | if err != nil { 211 | return false 212 | } 213 | return possibleTimeTL.Tag == asn1.TagUTCTime 214 | } 215 | 216 | func parseVersion(reader hashing.HashingReaderWrapper, version int) (int, error) { 217 | //skip version tagLength 218 | _, _ = asn1parser.ReadTagLength(&reader) 219 | readUint8, err := asn1parser.ReadUint8(&reader) 220 | if err != nil { 221 | return 0, err 222 | } 223 | version = int(readUint8 + 1) 224 | return version, nil 225 | } 226 | 227 | func versionExists(reader hashing.HashingReaderWrapper) bool { 228 | tagLength, err := asn1parser.PeekTagLength(&reader, 0) 229 | if err != nil { 230 | return false 231 | } 232 | return tagLength.Tag == asn1crypto.INTEGER && tagLength.Length.Length.Cmp(big.NewInt(int64(1))) == 0 233 | } 234 | 235 | func parseExtensions(reader hashing.HashingReaderWrapper) (*[]pkix.Extension, error) { 236 | _, _ = asn1parser.ReadTagLength(&reader) //skip context specific tag 237 | extensions := new([]pkix.Extension) 238 | err := asn1parser.ReadStruct(&reader, extensions) 239 | if err != nil { 240 | return nil, err 241 | } 242 | return extensions, nil 243 | } 244 | 245 | func extensionsExists(reader hashing.HashingReaderWrapper, version int) bool { 246 | contextSpecificTagLength, err := asn1parser.PeekTagLength(&reader, 0) 247 | if err != nil { 248 | return false 249 | } 250 | return version > 1 && asn1parser.IsContextSpecificTagWithId(0, contextSpecificTagLength) 251 | } 252 | 253 | func parseRevokedCertificateList(issuer *pkix.RDNSequence, reader hashing.HashingReaderWrapper, processor CRLProcessor) error { 254 | revokedCertListTag, err := asn1parser.ReadTagLength(&reader) 255 | if err != nil { 256 | return err 257 | } 258 | err = asn1parser.ExpectTag(asn1crypto.SEQUENCE, revokedCertListTag.Tag) 259 | if err != nil { 260 | return err 261 | } 262 | for { 263 | revokedCertSeq, err := asn1parser.PeekTagLength(&reader, 0) 264 | if err != nil { 265 | return err 266 | } 267 | 268 | if revokedCertSeq.Tag != asn1crypto.SEQUENCE { 269 | break 270 | } 271 | revokedCert := new(pkix.RevokedCertificate) 272 | err = asn1parser.ReadStruct(&reader, revokedCert) 273 | if err != nil { 274 | return err 275 | } 276 | err = processor.InsertRevokedCertificate(&CRLEntry{ 277 | issuer, 278 | revokedCert, 279 | }) 280 | if err != nil { 281 | return err 282 | } 283 | } 284 | return nil 285 | } 286 | 287 | func revokedCertificateListExists(reader hashing.HashingReaderWrapper) bool { 288 | length, err := asn1parser.PeekTagLength(&reader, 0) 289 | if err != nil { 290 | return false 291 | } 292 | return length.Tag == asn1crypto.SEQUENCE 293 | } 294 | 295 | func findAlgorithmIdentifierInCRL(file *os.File) (*pkix.AlgorithmIdentifier, error) { 296 | var reader = newHashingCRLReader(file) 297 | algoIdOffset := big.NewInt(0) 298 | certificateListTL, err := asn1parser.ReadTagLength(&reader) 299 | if err != nil { 300 | return nil, err 301 | } 302 | err = asn1parser.ExpectTag(asn1crypto.SEQUENCE, certificateListTL.Tag) 303 | if err != nil { 304 | return nil, err 305 | } 306 | tbsCertListTL, err := asn1parser.PeekTagLength(&reader, 0) 307 | if err != nil { 308 | return nil, err 309 | } 310 | algoIdOffset = algoIdOffset.Add(algoIdOffset, tbsCertListTL.CalculateTLVLength()) 311 | err = reader.Discard(algoIdOffset.Int64()) 312 | if err != nil { 313 | return nil, err 314 | } 315 | value := new(pkix.AlgorithmIdentifier) 316 | err = asn1parser.ReadStruct(&reader, value) 317 | if err != nil { 318 | return nil, err 319 | } 320 | return value, nil 321 | 322 | } 323 | 324 | func readAlgorithmIdentifier(reader asn1parser.Asn1Reader) (*pkix.AlgorithmIdentifier, error) { 325 | value := new(pkix.AlgorithmIdentifier) 326 | err := asn1parser.ReadStruct(reader, value) 327 | if err != nil { 328 | return nil, err 329 | } 330 | return value, nil 331 | } 332 | 333 | func newHashingDERCRLReader(crlFile *os.File) hashing.HashingReaderWrapper { 334 | var reader = hashing.HashingReaderWrapper{ 335 | Reader: bufio.NewReader(crlFile), 336 | } 337 | return reader 338 | } 339 | 340 | func newHashingPEMCRLReader(crlFile *os.File) hashing.HashingReaderWrapper { 341 | pemReader := pemreader.NewPemReader(bufio.NewReader(crlFile)) 342 | decoder := base64.NewDecoder(base64.StdEncoding, &pemReader) 343 | 344 | var reader = hashing.HashingReaderWrapper{ 345 | Reader: bufio.NewReader(decoder), 346 | } 347 | return reader 348 | 349 | } 350 | -------------------------------------------------------------------------------- /crl/crlstore/map_test.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import ( 4 | pkix "crypto/x509/pkix" 5 | "encoding/asn1" 6 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 7 | "github.com/gr33nbl00d/caddy-revocation-validator/core/hashing" 8 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 9 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader" 10 | "github.com/gr33nbl00d/caddy-revocation-validator/testhelper" 11 | "math/big" 12 | "os" 13 | "testing" 14 | "time" 15 | 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/suite" 18 | "go.uber.org/zap/zaptest" 19 | ) 20 | 21 | // MapStoreSuite is a test suite for the MapStore type. 22 | type MapStoreSuite struct { 23 | suite.Suite 24 | store *MapStore 25 | testIssuer *pkix.RDNSequence 26 | revokedCert *pkix.RevokedCertificate 27 | revokedCertSerial *big.Int 28 | } 29 | 30 | // SetupTest is called before each test method in the suite. 31 | func (suite *MapStoreSuite) SetupTest() { 32 | logger := zaptest.NewLogger(suite.T()) 33 | serializer := ASN1Serializer{} 34 | suite.store = &MapStore{ 35 | Map: make(map[string][]byte), 36 | Serializer: serializer, 37 | Logger: logger, 38 | } 39 | 40 | suite.testIssuer = &pkix.RDNSequence{ 41 | pkix.RelativeDistinguishedNameSET{ 42 | pkix.AttributeTypeAndValue{ 43 | Type: asn1.ObjectIdentifier{2, 5, 4, 3}, // OID for CommonName 44 | Value: "Test Issuer", 45 | }, 46 | }, 47 | } 48 | suite.revokedCertSerial = new(big.Int) 49 | suite.revokedCertSerial.SetUint64(52314123) 50 | suite.revokedCert = &pkix.RevokedCertificate{ 51 | SerialNumber: suite.revokedCertSerial, 52 | RevocationTime: time.Time{}, 53 | } 54 | } 55 | 56 | // TearDownTest is called after each test method in the suite. 57 | func (suite *MapStoreSuite) TearDownTest() { 58 | suite.store.Close() 59 | } 60 | 61 | // TestStartUpdateCrl tests the StartUpdateCrl method of MapStore. 62 | func (suite *MapStoreSuite) TestStartUpdateCrl() { 63 | err := suite.store.StartUpdateCrl(&crlreader.CRLMetaInfo{}) 64 | assert.NoError(suite.T(), err) 65 | // Add more assertions if needed 66 | } 67 | 68 | // TestInsertRevokedCert tests the InsertRevokedCert method of MapStore. 69 | func (suite *MapStoreSuite) TestInsertRevokedCert() { 70 | status, err := suite.store.GetCertRevocationStatus(suite.testIssuer, suite.revokedCertSerial) 71 | assert.NoError(suite.T(), err) 72 | assert.False(suite.T(), status.Revoked) 73 | 74 | err = suite.store.InsertRevokedCert(&crlreader.CRLEntry{ 75 | Issuer: suite.testIssuer, 76 | RevokedCertificate: suite.revokedCert, 77 | }) 78 | assert.NoError(suite.T(), err) 79 | status, err = suite.store.GetCertRevocationStatus(suite.testIssuer, suite.revokedCertSerial) 80 | assert.NoError(suite.T(), err) 81 | assert.True(suite.T(), status.Revoked) 82 | } 83 | 84 | // TestGetCertRevocationStatus tests the GetCertRevocationStatus method of MapStore. 85 | func (suite *MapStoreSuite) TestGetCertRevocationStatus() { 86 | status, err := suite.store.GetCertRevocationStatus(suite.testIssuer, big.NewInt(123)) 87 | assert.NoError(suite.T(), err) 88 | assert.NotNil(suite.T(), status) 89 | // Add more assertions if needed 90 | } 91 | 92 | // TestGetCRLMetaInfo tests the GetCRLMetaInfo method of MapStore. 93 | func (suite *MapStoreSuite) TestGetCRLMetaInfo() { 94 | // No meta info set 95 | metaInfo, err := suite.store.GetCRLMetaInfo() 96 | assert.Error(suite.T(), err) 97 | assert.Nil(suite.T(), metaInfo) 98 | 99 | // Set meta info and check if retrieved properly 100 | err = suite.store.StartUpdateCrl(&crlreader.CRLMetaInfo{}) 101 | assert.NoError(suite.T(), err) 102 | metaInfo, err = suite.store.GetCRLMetaInfo() 103 | assert.NoError(suite.T(), err) 104 | assert.NotNil(suite.T(), metaInfo) 105 | } 106 | 107 | // TestGetCRLExtMetaInfo tests the GetCRLExtMetaInfo method of MapStore. 108 | func (suite *MapStoreSuite) TestGetCRLExtMetaInfo() { 109 | // No extended meta info set 110 | extMetaInfo, err := suite.store.GetCRLExtMetaInfo() 111 | assert.Error(suite.T(), err) 112 | assert.Nil(suite.T(), extMetaInfo) 113 | 114 | // Set extended meta info and check if retrieved properly 115 | err = suite.store.UpdateExtendedMetaInfo(&crlreader.ExtendedCRLMetaInfo{}) 116 | assert.NoError(suite.T(), err) 117 | extMetaInfo, err = suite.store.GetCRLExtMetaInfo() 118 | assert.NoError(suite.T(), err) 119 | assert.NotNil(suite.T(), extMetaInfo) 120 | } 121 | 122 | // TestUpdateExtendedMetaInfo tests the UpdateExtendedMetaInfo method of MapStore. 123 | func (suite *MapStoreSuite) TestUpdateExtendedMetaInfo() { 124 | // Update extended meta info and check if set properly 125 | extMetaInfo := &crlreader.ExtendedCRLMetaInfo{} 126 | err := suite.store.UpdateExtendedMetaInfo(extMetaInfo) 127 | assert.NoError(suite.T(), err) 128 | } 129 | 130 | // TestUpdateSignatureCertificate tests the UpdateSignatureCertificate method of MapStore. 131 | func (suite *MapStoreSuite) TestUpdateSignatureCertificate() { 132 | // Create a dummy certificate chain entry 133 | expectedCert := []byte{0x30, 0x82, 0x01, 0x0a} // replace with actual raw certificate bytes if needed 134 | certEntry := &core.CertificateChainEntry{ 135 | RawCertificate: expectedCert, 136 | } 137 | 138 | err := suite.store.UpdateSignatureCertificate(certEntry) 139 | assert.NoError(suite.T(), err) 140 | hash := hashing.Sum64(SignatureCertKey) 141 | returnedCert := suite.store.Map[string(hash)] 142 | assert.Equal(suite.T(), expectedCert, returnedCert) 143 | 144 | } 145 | 146 | // TestGetCRLSignatureCert tests the GetCRLSignatureCert method of MapStore. 147 | func (suite *MapStoreSuite) TestGetCRLSignatureCert() { 148 | // No signature certificate set initially, expect error 149 | certEntry, err := suite.store.GetCRLSignatureCert() 150 | assert.Error(suite.T(), err) 151 | assert.Nil(suite.T(), certEntry) 152 | crtFile, err := os.Open(testhelper.GetTestDataFilePath("testcert.der")) 153 | assert.NoError(suite.T(), err) 154 | crtBytes, err := os.ReadFile(crtFile.Name()) 155 | assert.NoError(suite.T(), err) 156 | defer utils.CloseWithErrorHandling(crtFile.Close) 157 | err = suite.store.UpdateSignatureCertificate(&core.CertificateChainEntry{RawCertificate: crtBytes}) 158 | assert.NoError(suite.T(), err) 159 | certEntry, err = suite.store.GetCRLSignatureCert() 160 | assert.NoError(suite.T(), err) 161 | assert.NotNil(suite.T(), certEntry) 162 | } 163 | 164 | func (suite *MapStoreSuite) TestUpdateCRLLocations() { 165 | // Create a dummy CRLLocations 166 | crlLocations := &core.CRLLocations{ 167 | CRLDistributionPoints: []string{"http://example.com/crl1", "http://example.com/crl2"}, 168 | } 169 | 170 | // Update the CRL locations in the LevelDbStore 171 | err := suite.store.UpdateCRLLocations(crlLocations) 172 | assert.NoError(suite.T(), err) 173 | 174 | // Retrieve the stored CRL locations 175 | hash := hashing.Sum64(CRLLocationKey) 176 | crlLocationBytes := suite.store.Map[string(hash)] 177 | 178 | // Deserialize the retrieved CRL locations 179 | retrievedCRLLocations, err := suite.store.Serializer.DeserializeCRLLocations(crlLocationBytes) 180 | assert.NoError(suite.T(), err) 181 | 182 | // Verify the retrieved CRL locations match the original 183 | assert.Equal(suite.T(), crlLocations, retrievedCRLLocations) 184 | } 185 | 186 | // TestGetCRLLocations tests the GetCRLLocations method of MapStore. 187 | func (suite *MapStoreSuite) TestGetCRLLocations() { 188 | // No CRL locations set 189 | returnedCrlLocations, err := suite.store.GetCRLLocations() 190 | assert.Error(suite.T(), err) 191 | assert.Nil(suite.T(), returnedCrlLocations) 192 | 193 | // Set CRL locations and check if retrieved properly 194 | expectedCrllocations := core.CRLLocations{CRLDistributionPoints: []string{"http://example.com/crl1", "http://example.com/crl2"}, CRLUrl: "http://test"} 195 | err = suite.store.UpdateCRLLocations(&expectedCrllocations) 196 | assert.NoError(suite.T(), err) 197 | 198 | // Retrieve CRL locations and check if retrieved properly 199 | returnedCrlLocations, err = suite.store.GetCRLLocations() 200 | assert.NoError(suite.T(), err) 201 | assert.NotNil(suite.T(), returnedCrlLocations) 202 | assert.Equal(suite.T(), expectedCrllocations, *returnedCrlLocations) 203 | } 204 | 205 | // TestUpdate tests the Update method of MapStore. 206 | func (suite *MapStoreSuite) TestUpdate() { 207 | // Create a new MapStore instance to update from 208 | newStore := &MapStore{ 209 | Map: make(map[string][]byte), 210 | Serializer: suite.store.Serializer, 211 | Logger: suite.store.Logger, 212 | } 213 | 214 | // Add some dummy data to the new store 215 | newStore.Map["dummy_key"] = []byte("dummy_value") 216 | expectedMapAfterUpdate := make(map[string][]byte) 217 | expectedMapAfterUpdate["dummy_key"] = []byte("dummy_value") 218 | 219 | // Call the Update method with the new store 220 | err := suite.store.Update(newStore) 221 | assert.NoError(suite.T(), err) 222 | 223 | // Check if the map of the original store has been updated to match the map of the new store 224 | assert.Equal(suite.T(), expectedMapAfterUpdate, suite.store.Map) 225 | 226 | // Ensure that the map in the new store object has been set to nil after the update 227 | assert.Nil(suite.T(), newStore.Map) 228 | } 229 | 230 | // TestIsEmpty tests the IsEmpty method of MapStore. 231 | func (suite *MapStoreSuite) TestIsEmpty() { 232 | // Create a new empty MapStore instance 233 | store := &MapStore{ 234 | Map: make(map[string][]byte), 235 | Serializer: suite.store.Serializer, 236 | Logger: suite.store.Logger, 237 | } 238 | 239 | // Check if the store is empty 240 | assert.True(suite.T(), store.IsEmpty()) 241 | 242 | // Add some dummy data to the store 243 | store.Map["dummy_key"] = []byte("dummy_value") 244 | 245 | // Check if the store is not empty after adding data 246 | assert.False(suite.T(), store.IsEmpty()) 247 | } 248 | 249 | // TestCreateStore tests the CreateStore method of MapStoreFactory. 250 | func (suite *MapStoreSuite) TestCreateStore() { 251 | // Create a new MapStoreFactory instance 252 | factory := &MapStoreFactory{ 253 | Serializer: ASN1Serializer{}, 254 | Logger: zaptest.NewLogger(suite.T()), 255 | } 256 | 257 | // Call the CreateStore method 258 | store, err := factory.CreateStore("", false) 259 | 260 | // Check if the method returns a non-nil store and no error 261 | assert.NotNil(suite.T(), store) 262 | assert.NoError(suite.T(), err) 263 | 264 | // Assert that the returned store is of type *MapStore 265 | _, ok := store.(*MapStore) 266 | assert.True(suite.T(), ok) 267 | } 268 | 269 | // TestDelete tests the Delete method of MapStore. 270 | func (suite *MapStoreSuite) TestDelete() { 271 | // Create a new MapStore instance 272 | store := &MapStore{ 273 | Map: make(map[string][]byte), 274 | Serializer: suite.store.Serializer, 275 | Logger: suite.store.Logger, 276 | } 277 | 278 | // Call the Delete method 279 | err := store.Delete() 280 | 281 | // Check if the method returns nil 282 | assert.Nil(suite.T(), err) 283 | } 284 | 285 | // TestSuite runs the test suite. 286 | func TestSuite(t *testing.T) { 287 | suite.Run(t, new(MapStoreSuite)) 288 | } 289 | -------------------------------------------------------------------------------- /crl/crlrepository/crlrepository_test.go: -------------------------------------------------------------------------------- 1 | package crlrepository 2 | 3 | import ( 4 | "crypto/x509" 5 | "crypto/x509/pkix" 6 | "encoding/asn1" 7 | "fmt" 8 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 9 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlloader" 10 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader" 11 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlstore" 12 | "github.com/gr33nbl00d/caddy-revocation-validator/testhelper" 13 | "io" 14 | "math/big" 15 | "os" 16 | "sync" 17 | "testing" 18 | "time" 19 | 20 | "github.com/gr33nbl00d/caddy-revocation-validator/config" 21 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 22 | "github.com/stretchr/testify/assert" 23 | "go.uber.org/zap" 24 | ) 25 | 26 | const testCRLIdentifier = "myidentifier" 27 | 28 | type TestingCRLLoaderFactory struct { 29 | } 30 | 31 | type TestingCrlLoader struct { 32 | crlLocations *core.CRLLocations 33 | logger *zap.Logger 34 | } 35 | 36 | type TestingCRLReader struct { 37 | } 38 | 39 | func (t *TestingCRLReader) ReadCRL(crlProcessor crlreader.CRLProcessor, _ string) (*crlreader.CRLReadResult, error) { 40 | 41 | issuer := &pkix.RDNSequence{ 42 | pkix.RelativeDistinguishedNameSET{ 43 | pkix.AttributeTypeAndValue{ 44 | Type: asn1.ObjectIdentifier{2, 5, 4, 3}, // OID for CommonName 45 | Value: "Test Issuer", 46 | }, 47 | }, 48 | } 49 | serialBigInt := new(big.Int) 50 | serialBigInt.SetUint64(52314123) 51 | 52 | err := crlProcessor.InsertRevokedCertificate(&crlreader.CRLEntry{ 53 | Issuer: issuer, 54 | RevokedCertificate: &pkix.RevokedCertificate{ 55 | SerialNumber: serialBigInt, 56 | RevocationTime: time.Time{}, 57 | }, 58 | }) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return nil, nil 63 | } 64 | 65 | func (t *TestingCrlLoader) LoadCRL(targetFilePath string) error { 66 | crlPath := testhelper.GetTestDataFilePath("crl1.crl") 67 | err := copyToTargetFile(crlPath, targetFilePath) 68 | return err 69 | } 70 | 71 | func copyToTargetFile(sourceFileName string, targetFileName string) error { 72 | stat, err := os.Stat(sourceFileName) 73 | if err != nil { 74 | return err 75 | } 76 | if stat.IsDir() { 77 | return fmt.Errorf("CRL File %s is a directory", sourceFileName) 78 | } 79 | crlFile, err := os.OpenFile(targetFileName, os.O_RDWR|os.O_EXCL, 0600) 80 | if err != nil { 81 | return err 82 | } 83 | defer utils.CloseWithErrorHandling(crlFile.Close) 84 | sourceFile, err := os.OpenFile(sourceFileName, os.O_RDONLY|os.O_EXCL, 0600) 85 | if err != nil { 86 | return err 87 | } 88 | defer utils.CloseWithErrorHandling(sourceFile.Close) 89 | 90 | _, err = io.Copy(crlFile, sourceFile) 91 | if err != nil { 92 | return err 93 | } 94 | return nil 95 | } 96 | 97 | func (t *TestingCrlLoader) GetCRLLocationIdentifier() (string, error) { 98 | return testCRLIdentifier, nil 99 | } 100 | 101 | func (t *TestingCrlLoader) GetDescription() string { 102 | return "testing crl loader" 103 | } 104 | 105 | func (t TestingCRLLoaderFactory) CreatePreferredCrlLoader(crlLocations *core.CRLLocations, logger *zap.Logger) (crlloader.CRLLoader, error) { 106 | return &TestingCrlLoader{crlLocations, logger}, nil 107 | } 108 | 109 | func TestNewCRLRepositoryOfTypeMap(t *testing.T) { 110 | logger := zap.NewNop() 111 | crlConfig := &config.CRLConfig{} 112 | storeType := crlstore.Map 113 | err, repo := NewCRLRepository(logger, crlConfig, storeType) 114 | assert.NoError(t, err) 115 | 116 | assert.NotNil(t, repo) 117 | assert.IsType(t, crlloader.DefaultCRLLoaderFactory{}, repo.crlLoaderFactory) 118 | assert.NotNil(t, repo.Factory) 119 | assert.IsType(t, crlstore.MapStoreFactory{}, repo.Factory) 120 | factory := repo.Factory.(crlstore.MapStoreFactory) 121 | assert.Equal(t, logger, factory.Logger) 122 | assert.IsType(t, crlstore.ASN1Serializer{}, factory.Serializer) 123 | assert.NotNil(t, repo.crlRepositoryLock) 124 | assert.NotNil(t, repo.crlRepository) 125 | assert.NotNil(t, repo.crlConfig) 126 | assert.NotNil(t, repo.logger) 127 | assert.NotNil(t, repo.crlReader) 128 | assert.IsType(t, crlreader.StreamingCRLFileReader{}, repo.crlReader) 129 | } 130 | 131 | func TestNewCRLRepositoryOfTypeLevelDB(t *testing.T) { 132 | tempDir, err := os.MkdirTemp("", "crlnew_test") 133 | if err != nil { 134 | t.Fatalf("Failed to create temporary directory: %v", err) 135 | } 136 | defer utils.CloseWithErrorHandling(func() error { return os.RemoveAll(tempDir) }) 137 | logger := zap.NewNop() 138 | crlConfig := &config.CRLConfig{ 139 | WorkDir: tempDir, 140 | } 141 | storeType := crlstore.LevelDB 142 | err, repo := NewCRLRepository(logger, crlConfig, storeType) 143 | assert.NoError(t, err) 144 | 145 | assert.NotNil(t, repo) 146 | assert.IsType(t, crlloader.DefaultCRLLoaderFactory{}, repo.crlLoaderFactory) 147 | assert.NotNil(t, repo.Factory) 148 | assert.IsType(t, crlstore.LevelDbStoreFactory{}, repo.Factory) 149 | factory := repo.Factory.(crlstore.LevelDbStoreFactory) 150 | assert.Equal(t, logger, factory.Logger) 151 | assert.Equal(t, tempDir, factory.BasePath) 152 | assert.IsType(t, crlstore.ASN1Serializer{}, factory.Serializer) 153 | assert.NotNil(t, repo.crlRepositoryLock) 154 | assert.NotNil(t, repo.crlRepository) 155 | assert.NotNil(t, repo.crlConfig) 156 | assert.NotNil(t, repo.logger) 157 | } 158 | 159 | func TestNewCRLRepositoryOfTypeUnknown(t *testing.T) { 160 | logger := zap.NewNop() 161 | crlConfig := &config.CRLConfig{} 162 | storeType := 199 163 | err, _ := NewCRLRepository(logger, crlConfig, crlstore.StoreType(storeType)) 164 | assert.Error(t, err) 165 | } 166 | 167 | func TestAddCRL(t *testing.T) { 168 | tempDir, err := os.MkdirTemp("", "crl_test") 169 | if err != nil { 170 | t.Fatalf("Failed to create temporary directory: %v", err) 171 | } 172 | defer utils.CloseWithErrorHandling(func() error { return os.RemoveAll(tempDir) }) 173 | 174 | logger := zap.NewNop() 175 | crlConfig := &config.CRLConfig{ 176 | WorkDir: tempDir, 177 | CDPConfig: &config.CDPConfig{CRLFetchModeParsed: config.CRLFetchModeBackground}, 178 | } 179 | storeType := crlstore.Map // or LevelDB 180 | err, repo := NewCRLRepository(logger, crlConfig, storeType) 181 | assert.NoError(t, err) 182 | chains := &core.CertificateChains{} 183 | crlLocations := &core.CRLLocations{CRLFile: "./test.crl"} 184 | 185 | // Call the AddCRL function 186 | crlAdded, err := repo.AddCRL(crlLocations, chains) 187 | 188 | assert.NoError(t, err) 189 | assert.True(t, crlAdded) 190 | } 191 | 192 | func TestIsRevokedNonStrict(t *testing.T) { 193 | logger := zap.NewNop() 194 | crlConfig := &config.CRLConfig{ 195 | CDPConfig: &config.CDPConfig{ 196 | CRLCDPStrict: false, 197 | }, 198 | // Set your CRLConfig properties here 199 | } 200 | storeType := crlstore.Map // or LevelDB 201 | factory, err := crlstore.CreateStoreFactory(storeType, crlConfig.WorkDir, logger) 202 | if err != nil { 203 | panic(err) 204 | } 205 | repo := Repository{factory, 206 | &sync.RWMutex{}, 207 | make(map[string]*Entry), 208 | crlConfig, 209 | logger, 210 | TestingCRLLoaderFactory{}, 211 | &TestingCRLReader{}, 212 | } 213 | 214 | certificate := &x509.Certificate{} 215 | crlLocations := &core.CRLLocations{} 216 | 217 | // Call the IsRevoked function 218 | revocationStatus, err := repo.IsRevoked(certificate, crlLocations) 219 | 220 | assert.NoError(t, err) 221 | assert.NotNil(t, revocationStatus) 222 | assert.False(t, revocationStatus.Revoked) 223 | } 224 | 225 | func TestIsRevokedStrictButNotLoaded(t *testing.T) { 226 | logger := zap.NewNop() 227 | crlConfig := &config.CRLConfig{ 228 | CDPConfig: &config.CDPConfig{ 229 | CRLCDPStrict: true, 230 | }, 231 | // Set your CRLConfig properties here 232 | } 233 | storeType := crlstore.Map // or LevelDB 234 | factory, err := crlstore.CreateStoreFactory(storeType, crlConfig.WorkDir, logger) 235 | if err != nil { 236 | panic(err) 237 | } 238 | repo := Repository{factory, 239 | &sync.RWMutex{}, 240 | make(map[string]*Entry), 241 | crlConfig, 242 | logger, 243 | TestingCRLLoaderFactory{}, 244 | &TestingCRLReader{}, 245 | } 246 | 247 | // Create a test Certificate and CRLLocations 248 | certificate := &x509.Certificate{} 249 | crlLocations := &core.CRLLocations{} 250 | 251 | // Call the IsRevoked function 252 | _, err = repo.IsRevoked(certificate, crlLocations) 253 | 254 | assert.Error(t, err) 255 | assert.Contains(t, err.Error(), "CRL defined in CDP was not loaded") 256 | } 257 | 258 | func TestIsRevokedStrictLoadedWithRevokedCert(t *testing.T) { 259 | logger := zap.NewNop() 260 | crlConfig := &config.CRLConfig{ 261 | CDPConfig: &config.CDPConfig{ 262 | CRLCDPStrict: true, 263 | }, 264 | // Set your CRLConfig properties here 265 | } 266 | storeType := crlstore.Map // or LevelDB 267 | factory, err := crlstore.CreateStoreFactory(storeType, crlConfig.WorkDir, logger) 268 | if err != nil { 269 | panic(err) 270 | } 271 | loaderFactory := TestingCRLLoaderFactory{} 272 | repo := Repository{factory, 273 | &sync.RWMutex{}, 274 | make(map[string]*Entry), 275 | crlConfig, 276 | logger, 277 | loaderFactory, 278 | &TestingCRLReader{}, 279 | } 280 | crlLocations := &core.CRLLocations{} 281 | 282 | loader, err := loaderFactory.CreatePreferredCrlLoader(nil, nil) 283 | assert.NoError(t, err) 284 | entry, b, err := repo.getOrAddEntry(testCRLIdentifier, loader, nil) 285 | assert.True(t, b) 286 | assert.NoError(t, err) 287 | err = repo.loadActively(entry, nil, crlLocations) 288 | assert.NoError(t, err) 289 | // Create a test Certificate and CRLLocations 290 | issuer := &pkix.RDNSequence{ 291 | pkix.RelativeDistinguishedNameSET{ 292 | pkix.AttributeTypeAndValue{ 293 | Type: asn1.ObjectIdentifier{2, 5, 4, 3}, // OID for CommonName 294 | Value: "Test Issuer", 295 | }, 296 | }, 297 | } 298 | 299 | issuerBytes, err := asn1.Marshal(*issuer) 300 | serialBigInt := new(big.Int) 301 | serialBigInt.SetUint64(52314123) 302 | 303 | certificate := &x509.Certificate{ 304 | RawIssuer: issuerBytes, 305 | SerialNumber: serialBigInt, 306 | } 307 | 308 | // Call the IsRevoked function 309 | revoked, err := repo.IsRevoked(certificate, crlLocations) 310 | 311 | assert.NoError(t, err) 312 | assert.True(t, revoked.Revoked) 313 | assert.NotNil(t, revoked.CRLRevokedCertEntry) 314 | } 315 | 316 | func TestIsRevokedStrictLoadedWithNonRevokedCert(t *testing.T) { 317 | logger := zap.NewNop() 318 | crlConfig := &config.CRLConfig{ 319 | CDPConfig: &config.CDPConfig{ 320 | CRLCDPStrict: true, 321 | }, 322 | // Set your CRLConfig properties here 323 | } 324 | storeType := crlstore.Map // or LevelDB 325 | factory, err := crlstore.CreateStoreFactory(storeType, crlConfig.WorkDir, logger) 326 | if err != nil { 327 | panic(err) 328 | } 329 | loaderFactory := TestingCRLLoaderFactory{} 330 | repo := Repository{factory, 331 | &sync.RWMutex{}, 332 | make(map[string]*Entry), 333 | crlConfig, 334 | logger, 335 | loaderFactory, 336 | &TestingCRLReader{}, 337 | } 338 | crlLocations := &core.CRLLocations{} 339 | 340 | loader, err := loaderFactory.CreatePreferredCrlLoader(nil, nil) 341 | assert.NoError(t, err) 342 | entry, b, err := repo.getOrAddEntry(testCRLIdentifier, loader, nil) 343 | assert.True(t, b) 344 | assert.NoError(t, err) 345 | err = repo.loadActively(entry, nil, crlLocations) 346 | assert.NoError(t, err) 347 | // Create a test Certificate and CRLLocations 348 | issuer := &pkix.RDNSequence{ 349 | pkix.RelativeDistinguishedNameSET{ 350 | pkix.AttributeTypeAndValue{ 351 | Type: asn1.ObjectIdentifier{2, 5, 4, 3}, // OID for CommonName 352 | Value: "Test Issuer", 353 | }, 354 | }, 355 | } 356 | 357 | issuerBytes, err := asn1.Marshal(*issuer) 358 | serialBigInt := new(big.Int) 359 | serialBigInt.SetUint64(11314123) 360 | 361 | certificate := &x509.Certificate{ 362 | RawIssuer: issuerBytes, 363 | SerialNumber: serialBigInt, 364 | } 365 | 366 | // Call the IsRevoked function 367 | revoked, err := repo.IsRevoked(certificate, crlLocations) 368 | 369 | assert.NoError(t, err) 370 | assert.False(t, revoked.Revoked) 371 | assert.Nil(t, revoked.CRLRevokedCertEntry) 372 | } 373 | 374 | func TestUpdateCRLs(t *testing.T) { 375 | // Implement tests for UpdateCRLs function 376 | } 377 | 378 | func TestClose(t *testing.T) { 379 | // Implement tests for Close function 380 | } 381 | 382 | // You can add tests for other functions in a similar manner 383 | -------------------------------------------------------------------------------- /crl/crlstore/asn1serializer_test.go: -------------------------------------------------------------------------------- 1 | package crlstore 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/x509" 6 | "crypto/x509/pkix" 7 | "encoding/hex" 8 | "github.com/gr33nbl00d/caddy-revocation-validator/core" 9 | "github.com/gr33nbl00d/caddy-revocation-validator/core/utils" 10 | "github.com/gr33nbl00d/caddy-revocation-validator/crl/crlreader" 11 | "github.com/gr33nbl00d/caddy-revocation-validator/testhelper" 12 | "math/big" 13 | "net" 14 | "os" 15 | "testing" 16 | "time" 17 | 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | func TestASN1Serializer_SerializeMetaInfo(t *testing.T) { 22 | // Define the expected values 23 | expectedThisUpdate := time.Date(2022, time.December, 1, 0, 0, 0, 0, time.UTC) 24 | expectedNextUpdate := expectedThisUpdate.Add(time.Hour) 25 | 26 | issuer := pkix.Name{ 27 | Country: []string{"US"}, 28 | Organization: []string{"Example Organization"}, 29 | CommonName: "Example Issuer", 30 | } 31 | // Create an example CRLMetaInfo with the expected values 32 | metaInfo := &crlreader.CRLMetaInfo{ 33 | Issuer: issuer.ToRDNSequence(), 34 | ThisUpdate: expectedThisUpdate, 35 | NextUpdate: expectedNextUpdate, 36 | } 37 | 38 | // Create an instance of ASN1Serializer 39 | serializer := ASN1Serializer{} 40 | 41 | // Serialize the example CRLMetaInfo 42 | serializedMetaInfoBytes, err := serializer.SerializeMetaInfo(metaInfo) 43 | assert.NoError(t, err) 44 | 45 | expectedHex := "30653045310b3009060355040613025553311d301b060355040a13144578616d706c65204f7267616e697a6174696f6e311730150603550403130e4578616d706c6520497373756572170d3232313230313030303030305a800d3232313230313031303030305a" 46 | 47 | resultHex := hex.EncodeToString(serializedMetaInfoBytes) 48 | 49 | assert.Equal(t, expectedHex, resultHex) 50 | } 51 | 52 | func TestASN1Serializer_DeserializeMetaInfo(t *testing.T) { 53 | exampleMetaInfoHex := "30653045310b3009060355040613025553311d301b060355040a13144578616d706c65204f7267616e697a6174696f6e311730150603550403130e4578616d706c6520497373756572170d3232313230313030303030305a800d3232313230313031303030305a" 54 | 55 | // Convert the hex-encoded string to bytes 56 | exampleMetaInfoBytes, err := hex.DecodeString(exampleMetaInfoHex) 57 | assert.NoError(t, err) 58 | 59 | // Create an instance of ASN1Serializer 60 | serializer := ASN1Serializer{} 61 | 62 | // Deserialize the hex-encoded example CRLMetaInfo into a CRLMetaInfo structure 63 | metaInfo, err := serializer.DeserializeMetaInfo(exampleMetaInfoBytes) 64 | assert.NoError(t, err) 65 | 66 | // Define the expected values 67 | expectedThisUpdate := time.Date(2022, time.December, 1, 0, 0, 0, 0, time.UTC) 68 | expectedNextUpdate := expectedThisUpdate.Add(time.Hour) 69 | 70 | // Compare the deserialized values to the expected values 71 | assert.Equal(t, expectedThisUpdate, metaInfo.ThisUpdate) 72 | assert.Equal(t, expectedNextUpdate, metaInfo.NextUpdate) 73 | assert.Equal(t, "CN=Example Issuer,O=Example Organization,C=US", metaInfo.Issuer.String()) 74 | } 75 | 76 | func TestASN1Serializer_SerializeRevokedCert(t *testing.T) { 77 | // Create an example RevokedCertificate with a fixed date 78 | fixedTime := time.Date(2022, time.December, 1, 0, 0, 0, 0, time.UTC) 79 | exampleRevokedCert := &pkix.RevokedCertificate{ 80 | SerialNumber: big.NewInt(12345), 81 | RevocationTime: fixedTime, 82 | } 83 | 84 | // Create an instance of ASN1Serializer 85 | serializer := ASN1Serializer{} 86 | 87 | // Serialize the example RevokedCertificate 88 | serializedRevokedCertBytes, err := serializer.SerializeRevokedCert(exampleRevokedCert) 89 | assert.NoError(t, err) 90 | 91 | // Define the expected hex-encoded serialized string 92 | expectedHex := "301302023039170d3232313230313030303030305a" 93 | 94 | // Convert the serialized bytes to a hex-encoded string 95 | serializedHex := hex.EncodeToString(serializedRevokedCertBytes) 96 | 97 | // Compare the serialized hex string to the expected hex string 98 | assert.Equal(t, expectedHex, serializedHex) 99 | } 100 | 101 | func TestASN1Serializer_DeserializeRevokedCert(t *testing.T) { 102 | // Define the hex-encoded example RevokedCertificate data with a fixed date 103 | fixedTime := time.Date(2022, time.December, 1, 0, 0, 0, 0, time.UTC) 104 | exampleRevokedCertHex := "301302023039170d3232313230313030303030305a" 105 | exampleRevokedCertBytes, err := hex.DecodeString(exampleRevokedCertHex) 106 | assert.NoError(t, err) 107 | 108 | // Create an instance of ASN1Serializer 109 | serializer := ASN1Serializer{} 110 | 111 | // Deserialize the hex-encoded example RevokedCertificate into a RevokedCertificate structure 112 | revokedCert, err := serializer.DeserializeRevokedCert(exampleRevokedCertBytes) 113 | assert.NoError(t, err) 114 | 115 | expectedSerialNumber := big.NewInt(12345) 116 | expectedRevocationTime := fixedTime 117 | 118 | // Compare the deserialized values to the expected values 119 | assert.Equal(t, expectedSerialNumber, revokedCert.SerialNumber) 120 | assert.Equal(t, expectedRevocationTime.Unix(), revokedCert.RevocationTime.Unix()) 121 | } 122 | 123 | func TestASN1Serializer_SerializeMetaInfoExt(t *testing.T) { 124 | // Create an example ExtendedCRLMetaInfo with fixed values 125 | exampleMetaInfoExt := &crlreader.ExtendedCRLMetaInfo{ 126 | CRLNumber: big.NewInt(67890), 127 | } 128 | 129 | // Create an instance of ASN1Serializer 130 | serializer := ASN1Serializer{} 131 | 132 | // Serialize the example ExtendedCRLMetaInfo 133 | serializedMetaInfoExtBytes, err := serializer.SerializeMetaInfoExt(exampleMetaInfoExt) 134 | assert.NoError(t, err) 135 | 136 | expectedHex := "30058003010932" 137 | 138 | // Convert the serialized bytes to a hex-encoded string 139 | serializedHex := hex.EncodeToString(serializedMetaInfoExtBytes) 140 | 141 | // Compare the serialized hex string to the expected hex string 142 | assert.Equal(t, expectedHex, serializedHex) 143 | } 144 | 145 | func TestASN1Serializer_DeserializeMetaInfoExt(t *testing.T) { 146 | exampleMetaInfoExtHex := "30058003010932" 147 | exampleMetaInfoExtBytes, err := hex.DecodeString(exampleMetaInfoExtHex) 148 | assert.NoError(t, err) 149 | 150 | // Create an instance of ASN1Serializer 151 | serializer := ASN1Serializer{} 152 | 153 | // Deserialize the hex-encoded example ExtendedCRLMetaInfo into an ExtendedCRLMetaInfo structure 154 | metaInfoExt, err := serializer.DeserializeMetaInfoExt(exampleMetaInfoExtBytes) 155 | 156 | assert.NoError(t, err) 157 | 158 | expectedBaseCRLNumber := big.NewInt(67890) 159 | 160 | // Compare the deserialized values to the expected values 161 | assert.Equal(t, expectedBaseCRLNumber, metaInfoExt.CRLNumber) 162 | } 163 | 164 | func TestASN1Serializer_SerializeSignatureCert(t *testing.T) { 165 | examplePublicKey := &rsa.PublicKey{ 166 | N: big.NewInt(12345), 167 | E: 65537, // Common RSA public exponent 168 | } 169 | exampleCert := &x509.Certificate{ 170 | SerialNumber: big.NewInt(12345), 171 | Subject: pkix.Name{ 172 | Organization: []string{"Example Organization"}, 173 | OrganizationalUnit: []string{"Example Organizational Unit"}, 174 | CommonName: "example.com", 175 | }, 176 | Issuer: pkix.Name{ 177 | Organization: []string{"Example Organization"}, 178 | OrganizationalUnit: []string{"Example Organizational Unit"}, 179 | CommonName: "example.com", 180 | }, 181 | NotBefore: time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), 182 | NotAfter: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC), 183 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 184 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, 185 | EmailAddresses: []string{"test@example.com"}, 186 | IPAddresses: []net.IP{net.IPv4(192, 168, 1, 1), net.IPv6loopback}, 187 | IsCA: false, 188 | BasicConstraintsValid: true, 189 | Signature: []byte{0x30, 0x45, 0x02, 0x20, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15}, 190 | SignatureAlgorithm: x509.SHA256WithRSA, 191 | PublicKey: *examplePublicKey, 192 | } 193 | 194 | // Create an instance of ASN1Serializer 195 | serializer := ASN1Serializer{} 196 | 197 | // Serialize the example x509.Certificate 198 | serializedCertBytes, err := serializer.SerializeSignatureCert(exampleCert) 199 | assert.NoError(t, err) 200 | 201 | expectedHex := "3082019c04000400040004000400043030450220123456789abcdef0112233445566778899aabbccddeeff0102030405060708090a0b0c0d0e0f10111213141502010402010030090202303902030100010201000202303930543000301613144578616d706c65204f7267616e697a6174696f6e301d131b4578616d706c65204f7267616e697a6174696f6e616c20556e697430003000300030001300130b6578616d706c652e636f6d3000300030543000301613144578616d706c65204f7267616e697a6174696f6e301d131b4578616d706c65204f7267616e697a6174696f6e616c20556e697430003000300030001300130b6578616d706c652e636f6d30003000170d3232303130313030303030305a170d3233303130313030303030305a020105300030003000300602010102010230000101ff0101000201000101000400040030003000300030120c1074657374406578616d706c652e636f6d3024041000000000000000000000ffffc0a80101041000000000000000000000000000000001300001010030003000300030003000300030003000300030003000" 202 | 203 | // Convert the serialized bytes to a hex-encoded string 204 | serializedHex := hex.EncodeToString(serializedCertBytes) 205 | 206 | // Compare the serialized hex string to the expected hex string 207 | assert.Equal(t, expectedHex, serializedHex) 208 | } 209 | 210 | func TestASN1Serializer_DeserializeSignatureCert(t *testing.T) { 211 | crtFile, err := os.Open(testhelper.GetTestDataFilePath("testcert.der")) 212 | assert.NoError(t, err) 213 | defer utils.CloseWithErrorHandling(crtFile.Close) 214 | if err != nil { 215 | t.Errorf("error occured %v", err) 216 | } 217 | crtBytes, err := os.ReadFile(crtFile.Name()) 218 | assert.NoError(t, err) 219 | 220 | // Create an instance of ASN1Serializer 221 | serializer := ASN1Serializer{} 222 | 223 | cert, err := serializer.DeserializeSignatureCert(crtBytes) 224 | assert.NoError(t, err) 225 | 226 | expectedSerialNumber := big.NewInt(0) 227 | expectedSerialNumber.SetUint64(15994519719171511020) 228 | // Add other expected certificate fields here as needed 229 | 230 | // Compare the deserialized values to the expected values 231 | assert.Equal(t, expectedSerialNumber, cert.SerialNumber) 232 | // Add other comparisons for expected fields here as needed 233 | } 234 | 235 | func TestASN1Serializer_SerializeCRLLocations(t *testing.T) { 236 | // Create an example CRLLocations with fixed data 237 | exampleCRLLocations := &core.CRLLocations{ 238 | CRLUrl: "https://example.com/crl.crl", 239 | CRLFile: "crl.crl", 240 | CRLDistributionPoints: []string{"http://crl1.example.com", "http://crl2.example.com"}, 241 | } 242 | 243 | // Create an instance of ASN1Serializer 244 | serializer := ASN1Serializer{} 245 | 246 | // Serialize the example CRLLocations 247 | serializedCRLLocationsBytes, err := serializer.SerializeCRLLocations(exampleCRLLocations) 248 | assert.NoError(t, err) 249 | 250 | expectedHex := "305a30321317687474703a2f2f63726c312e6578616d706c652e636f6d1317687474703a2f2f63726c322e6578616d706c652e636f6d131b68747470733a2f2f6578616d706c652e636f6d2f63726c2e63726c130763726c2e63726c" 251 | 252 | // Convert the serialized bytes to a hex-encoded string 253 | serializedHex := hex.EncodeToString(serializedCRLLocationsBytes) 254 | 255 | // Compare the serialized hex string to the expected hex string 256 | assert.Equal(t, expectedHex, serializedHex) 257 | } 258 | 259 | func TestASN1Serializer_DeserializeCRLLocations(t *testing.T) { 260 | expectedHex := "305a30321317687474703a2f2f63726c312e6578616d706c652e636f6d1317687474703a2f2f63726c322e6578616d706c652e636f6d131b68747470733a2f2f6578616d706c652e636f6d2f63726c2e63726c130763726c2e63726c" 261 | 262 | // Convert the expected hex string to bytes 263 | expectedBytes, err := hex.DecodeString(expectedHex) 264 | assert.NoError(t, err) 265 | 266 | // Create an instance of ASN1Serializer 267 | serializer := ASN1Serializer{} 268 | 269 | // Deserialize the expected bytes into CRLLocations 270 | deserializedCRLLocations, err := serializer.DeserializeCRLLocations(expectedBytes) 271 | assert.NoError(t, err) 272 | 273 | // Create an example CRLLocations with fixed data to compare 274 | exampleCRLLocations := &core.CRLLocations{ 275 | CRLUrl: "https://example.com/crl.crl", 276 | CRLFile: "crl.crl", 277 | CRLDistributionPoints: []string{"http://crl1.example.com", "http://crl2.example.com"}, 278 | } 279 | 280 | // Compare the deserialized CRLLocations to the example 281 | assert.Equal(t, exampleCRLLocations, deserializedCRLLocations) 282 | } 283 | -------------------------------------------------------------------------------- /core/asn1parser/asn1parser.go: -------------------------------------------------------------------------------- 1 | package asn1parser 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/asn1" 9 | "errors" 10 | "fmt" 11 | asn1crypto "golang.org/x/crypto/cryptobyte/asn1" 12 | "io" 13 | "math/big" 14 | "time" 15 | ) 16 | 17 | type Asn1Reader interface { 18 | Read(p []byte) (int, error) 19 | Peek(n int) ([]byte, error) 20 | } 21 | 22 | type Length struct { 23 | Length big.Int //number of bytes the value has 24 | LengthSize int //number of bytes the length has 25 | } 26 | 27 | type TagLength struct { 28 | Tag asn1crypto.Tag 29 | Length Length 30 | } 31 | 32 | // CalculateTLVLength Complete length in byte of the tlv record 33 | func (l TagLength) CalculateTLVLength() *big.Int { 34 | sum := big.NewInt(0) 35 | lengthSizeBigInt := big.NewInt(int64(l.Length.LengthSize)) 36 | sum = sum.Add(sum, big.NewInt(1)) 37 | sum = sum.Add(sum, lengthSizeBigInt) 38 | sum = sum.Add(sum, &l.Length.Length) 39 | return sum 40 | } 41 | 42 | // CalculateValueLength Complete length in byte of value 43 | func (l TagLength) CalculateValueLength() *big.Int { 44 | sum := big.NewInt(0) 45 | sum = sum.Add(sum, &l.Length.Length) 46 | return sum 47 | } 48 | 49 | // CalculateTLLength Complete length in byte of length of the value 50 | func (l TagLength) CalculateTLLength() *big.Int { 51 | sum := big.NewInt(0) 52 | lengthSizeBigInt := big.NewInt(int64(l.Length.LengthSize)) 53 | sum = sum.Add(sum, lengthSizeBigInt) 54 | sum = sum.Add(sum, big.NewInt(1)) 55 | return sum 56 | } 57 | 58 | type BitString struct { 59 | Bytes []byte // bits packed into bytes. 60 | BitLength int // length in bits. 61 | } 62 | 63 | func IsContextSpecificTagWithId(tagId int, tagLength *TagLength) bool { 64 | if IsContextSpecificTag(tagLength) { 65 | tag := GetContextSpecificTagId(tagLength) 66 | if tag == tagId { 67 | return true 68 | } 69 | } 70 | return false 71 | } 72 | 73 | func GetContextSpecificTagId(tagLength *TagLength) int { 74 | return (int)(tagLength.Tag & 0x0F) 75 | } 76 | 77 | func IsContextSpecificTag(tagLength *TagLength) bool { 78 | if (tagLength.Tag & 0xF0) == 0xa0 { 79 | return true 80 | } 81 | return false 82 | } 83 | 84 | func ReadUtcTime(reader Asn1Reader) (*time.Time, error) { 85 | lastUpdateUtcTag, err := ReadTagLength(reader) 86 | if err != nil { 87 | return nil, err 88 | } 89 | err = ExpectTag(lastUpdateUtcTag.Tag, asn1.TagUTCTime) 90 | if err != nil { 91 | return nil, err 92 | } 93 | lastUpdateUtcBytes, err := ReadExpectedBytes(reader, int(lastUpdateUtcTag.Length.Length.Int64())) 94 | if err != nil { 95 | return nil, err 96 | } 97 | utcTime, err := ParseUTCTime(lastUpdateUtcBytes) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return utcTime, nil 102 | } 103 | 104 | func ParseBitString(reader Asn1Reader) (*BitString, error) { 105 | tagLength, err := ReadTagLength(reader) 106 | if err != nil { 107 | return nil, err 108 | } 109 | err = ExpectTag(asn1crypto.BIT_STRING, tagLength.Tag) 110 | if err != nil { 111 | return nil, err 112 | } 113 | readBytes, err := ReadExpectedBytes(reader, int(tagLength.Length.Length.Int64())) 114 | if err != nil { 115 | return nil, err 116 | } 117 | if len(readBytes) == 0 { 118 | return nil, errors.New("BitString is empty") 119 | } 120 | var ret BitString 121 | paddingBits := int(readBytes[0]) 122 | if paddingBits > 7 || 123 | len(readBytes) == 1 && paddingBits > 0 || 124 | readBytes[len(readBytes)-1]&((1<= 2050 { 161 | utcTime = utcTime.AddDate(-100, 0, 0) 162 | } 163 | return &utcTime, nil 164 | } 165 | 166 | func ReadStruct(reader Asn1Reader, value interface{}) error { 167 | tagLength, err := PeekTagLength(reader, 0) 168 | if err != nil { 169 | return err 170 | } 171 | err = ExpectTag(asn1crypto.SEQUENCE, tagLength.Tag) 172 | if err != nil { 173 | return err 174 | } 175 | tlvBytes, err := ReadTVLBytesWithLimit(reader, *tagLength, 81920) 176 | if err != nil { 177 | return err 178 | } 179 | rest, err := asn1.Unmarshal(tlvBytes, value) 180 | if err != nil { 181 | return err 182 | } 183 | if len(rest) != 0 { 184 | return errors.New("trailing data after asn1 object") 185 | } 186 | return nil 187 | } 188 | 189 | func ReadTVLBytesWithLimit(reader Asn1Reader, tagLength TagLength, maxLength int64) ([]byte, error) { 190 | err := ExpectLengthNotGreater(big.NewInt(maxLength), &tagLength.Length.Length) 191 | if err != nil { 192 | return nil, err 193 | } 194 | size := CalculateWholeTLVLength(tagLength) 195 | return ReadExpectedBytes(reader, size) 196 | } 197 | 198 | func CalculateWholeTLVLength(tagLength TagLength) int { 199 | tlvHeaderLength := tagLength.Length.LengthSize + 1 200 | realLength := int(tagLength.Length.Length.Int64()) + tlvHeaderLength 201 | return realLength 202 | } 203 | 204 | func ExpectLengthNotGreater(expectedLength *big.Int, length *big.Int) error { 205 | if length.Cmp(expectedLength) == 1 { 206 | return fmt.Errorf("length of tag is greater than expected. Expected max length %s but length was %s", expectedLength, length) 207 | } 208 | return nil 209 | } 210 | 211 | func ReadTagLength(reader Asn1Reader) (*TagLength, error) { 212 | tag, err := ReadTag(reader) 213 | if err != nil { 214 | return nil, err 215 | } 216 | length, err := ReadLength(reader) 217 | if err != nil { 218 | return nil, err 219 | } 220 | return &TagLength{Tag: *tag, Length: *length}, nil 221 | } 222 | 223 | func PeekTagLength(reader Asn1Reader, offset int) (*TagLength, error) { 224 | tag, err := PeekTag(reader, offset) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | length, err := PeekLength(reader, offset+1) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | return &TagLength{Tag: *tag, Length: *length}, nil 235 | } 236 | 237 | func ExpectTag(expectedTag asn1crypto.Tag, tag asn1crypto.Tag) error { 238 | if expectedTag != tag { 239 | return fmt.Errorf("unexpected tag. Expected: %d but found %d", expectedTag, tag) 240 | } 241 | return nil 242 | } 243 | 244 | func ReadTag(reader Asn1Reader) (*asn1crypto.Tag, error) { 245 | readBytes, err := ReadExpectedBytes(reader, 1) 246 | if err != nil { 247 | return nil, err 248 | } 249 | tag := asn1crypto.Tag(readBytes[0]) 250 | return &tag, err 251 | } 252 | 253 | func PeekTag(reader Asn1Reader, offset int) (*asn1crypto.Tag, error) { 254 | readBytes, err := PeekExpectedBytes(reader, 1, offset) 255 | if err != nil { 256 | return nil, err 257 | } 258 | tag := asn1crypto.Tag(readBytes[0]) 259 | return &tag, nil 260 | } 261 | 262 | func ReadExpectedBytes(reader Asn1Reader, byteSize int) ([]byte, error) { 263 | readBytes := make([]byte, byteSize) 264 | err := ReadExpectedBytesRecursive(reader, byteSize, &readBytes, 0) 265 | if err != nil { 266 | return nil, err 267 | } 268 | return readBytes, nil 269 | } 270 | 271 | func ReadExpectedBytesRecursive(reader Asn1Reader, byteSize int, byteArray *[]byte, currentPosition int) error { 272 | bytesLeftToRead := byteSize - currentPosition 273 | readBytes := make([]byte, bytesLeftToRead) 274 | read, err := reader.Read(readBytes) 275 | if err != nil { 276 | if err == io.EOF { 277 | return fmt.Errorf("end of file reached while still expecting bytes %v", err) 278 | } 279 | return err 280 | } 281 | copyBytes(byteArray, readBytes, currentPosition, read) 282 | if read != bytesLeftToRead { 283 | err := ReadExpectedBytesRecursive(reader, byteSize, byteArray, currentPosition+read) 284 | if err != nil { 285 | return err 286 | } 287 | } 288 | return nil 289 | } 290 | 291 | func copyBytes(targetBytes *[]byte, bytesToAdd []byte, targetBytePosition int, countOfBytesToAdd int) { 292 | for i := targetBytePosition; i < targetBytePosition+countOfBytesToAdd; i++ { 293 | (*targetBytes)[i] = bytesToAdd[i-targetBytePosition] 294 | } 295 | } 296 | 297 | func PeekExpectedBytes(reader Asn1Reader, byteSize int, offset int) ([]byte, error) { 298 | read, err := reader.Peek(byteSize + offset) 299 | if err != nil { 300 | if err == io.EOF { 301 | return nil, fmt.Errorf("end of file reached while still expecting bytes %v", err) 302 | } else { 303 | return nil, err 304 | } 305 | } 306 | readBytes := make([]byte, byteSize) 307 | copiedBytes := copy(readBytes[:], read[offset:(offset+byteSize)]) 308 | if copiedBytes != byteSize { 309 | return nil, errors.New("error while copy of expected bytes") 310 | } 311 | 312 | return readBytes, nil 313 | } 314 | 315 | func ReadLength(reader Asn1Reader) (*Length, error) { 316 | sizeOfLength := 1 317 | length := new(big.Int) 318 | lengthOrSizeOfLength, err := ReadUint8(reader) 319 | if err != nil { 320 | return nil, err 321 | } 322 | if (lengthOrSizeOfLength & 0x80) == 0 { 323 | length.SetUint64(uint64(lengthOrSizeOfLength)) 324 | } else { 325 | sizeOfLength = int(lengthOrSizeOfLength & 0x0F) 326 | length, err = ReadExpectedBigInt(reader, sizeOfLength) 327 | if err != nil { 328 | return nil, err 329 | } 330 | sizeOfLength = sizeOfLength + 1 331 | } 332 | return &Length{ 333 | Length: *length, LengthSize: sizeOfLength, 334 | }, nil 335 | } 336 | 337 | func PeekLength(reader Asn1Reader, offset int) (*Length, error) { 338 | sizeOfLength := 1 339 | length := new(big.Int) 340 | lengthOrSizeOfLength, err := PeekUint8(reader, offset) 341 | if err != nil { 342 | return nil, err 343 | } 344 | if (lengthOrSizeOfLength & 0x80) == 0 { 345 | length.SetUint64(uint64(lengthOrSizeOfLength)) 346 | } else { 347 | offset += 1 348 | sizeOfLength = int(lengthOrSizeOfLength & 0x0F) 349 | length, err = PeekExpectedBigInt(reader, sizeOfLength, offset) 350 | if err != nil { 351 | return nil, err 352 | } 353 | sizeOfLength = sizeOfLength + 1 354 | } 355 | return &Length{ 356 | Length: *length, LengthSize: sizeOfLength, 357 | }, nil 358 | } 359 | 360 | func ReadExpectedBigInt(reader Asn1Reader, sizeOfLength int) (*big.Int, error) { 361 | length := new(big.Int) 362 | lengthBytes, err := ReadExpectedBytes(reader, sizeOfLength) 363 | if err != nil { 364 | return nil, err 365 | } 366 | 367 | length = length.SetBytes(lengthBytes) 368 | return length, nil 369 | } 370 | 371 | func ReadBigInt(reader Asn1Reader) (*big.Int, error) { 372 | length := new(big.Int) 373 | tagLength, err := ReadTagLength(reader) 374 | if err != nil { 375 | return nil, err 376 | } 377 | err = ExpectTag(asn1.TagInteger, tagLength.Tag) 378 | if err != nil { 379 | return nil, err 380 | } 381 | readBytes, err := ReadExpectedBytes(reader, int(tagLength.CalculateValueLength().Int64())) 382 | if err != nil { 383 | return nil, err 384 | } 385 | 386 | length = length.SetBytes(readBytes) 387 | return length, nil 388 | } 389 | 390 | func PeekExpectedBigInt(reader Asn1Reader, sizeOfLength int, offset int) (*big.Int, error) { 391 | length := new(big.Int) 392 | lengthBytes, err := PeekExpectedBytes(reader, sizeOfLength, offset) 393 | if err != nil { 394 | return nil, err 395 | } 396 | length = length.SetBytes(lengthBytes) 397 | return length, nil 398 | } 399 | 400 | func ReadUint8(reader Asn1Reader) (uint8, error) { 401 | readBytes, err := ReadExpectedBytes(reader, 1) 402 | if err != nil { 403 | return 0, err 404 | } 405 | 406 | return readBytes[0], nil 407 | } 408 | 409 | func PeekUint8(reader Asn1Reader, offset int) (uint8, error) { 410 | readBytes, err := PeekExpectedBytes(reader, 1, offset) 411 | if err != nil { 412 | return 0, err 413 | } 414 | return readBytes[0], nil 415 | } 416 | 417 | func ParseIssuerRDNSequence(cert *x509.Certificate) (*pkix.RDNSequence, error) { 418 | return ParseRDNSequence(cert.RawIssuer) 419 | } 420 | 421 | func ParseSubjectRDNSequence(cert *x509.Certificate) (*pkix.RDNSequence, error) { 422 | return ParseRDNSequence(cert.RawSubject) 423 | } 424 | 425 | func ParseRDNSequence(rdnData []byte) (*pkix.RDNSequence, error) { 426 | rdnObject := new(pkix.RDNSequence) 427 | reader := bufio.NewReader(bytes.NewReader(rdnData)) 428 | err := ReadStruct(reader, rdnObject) 429 | if err != nil { 430 | return nil, fmt.Errorf("could not parse the RDNSequence: %v", err) 431 | } 432 | return rdnObject, nil 433 | } 434 | --------------------------------------------------------------------------------