├── testdata ├── README ├── server.crt ├── server1.crt ├── server2.crt ├── server.key ├── server1.key └── server2.key ├── .codeclimate.yml ├── go.mod ├── go.sum ├── .travis.yml ├── LICENSE ├── README.md ├── certman.go └── certman_test.go /testdata/README: -------------------------------------------------------------------------------- 1 | Test certificates generated with the following command: 2 | 3 | openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -sha256 -keyout server.key -out server.crt -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | gofmt: 3 | enabled: true 4 | govet: 5 | enabled: true 6 | golint: 7 | enabled: true 8 | fixme: 9 | enabled: true 10 | ratings: 11 | paths: 12 | - "**.go" -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dyson/certman 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.6.0 7 | github.com/pkg/errors v0.9.1 8 | ) 9 | 10 | require golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 2 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 3 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 4 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 5 | golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= 6 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | notifications: 4 | email: false 5 | go: 6 | - 1.17.x 7 | - 1.18.x 8 | - 1.19.x 9 | - master 10 | 11 | before_install: 12 | - go get github.com/mattn/goveralls 13 | before_script: 14 | - npm install -g codeclimate-test-reporter 15 | script: 16 | - $HOME/gopath/bin/goveralls -service=travis-ci 17 | - for pkg in $PKGS; do go test -race -coverprofile=profile.out -covermode=atomic $pkg; if [[ -f profile.out ]]; then cat profile.out >> coverage.txt; rm profile.out; fi; done 18 | after_success: 19 | - codeclimate-test-reporter < coverage.txt 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Dyson Simmons 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /testdata/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDWjCCAkKgAwIBAgIJAJiO53P0WQTzMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV 3 | BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg 4 | Q29tcGFueSBMdGQwHhcNMTcwODA0MTAzMzIyWhcNMTgwODA0MTAzMzIyWjBCMQsw 5 | CQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZh 6 | dWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 7 | sYk+rsOElxPB5gPH8Vg/RFzdBzDJesI4VCXYcSCl0Ek/lO4AAKjgRssjmZDWWXcZ 8 | 3/Z1UlO4cRZy6zaSllFM07WXX3SQ54iBy3lS6NgBcHiWVfKrtXz/dQrswSPBnGEH 9 | PVBmd+xHcEv4wqYnYYVtJuIcGY1P/i14h1ogpEW22cnCVYJwqmjxrD4UQ8vSeTf9 10 | YwVCbKXZ5T+eiNhfQOkCG0rClqvfvZiMgBBlLYvJrc9bELsqIbNGVD4CrkYjZGEc 11 | ToMrb/wZhqLxlcs/iXKI0/579+9vvc44/pi2MFU0nvhoSnEgopKKPy83SwBogpiH 12 | +mDcVgMiEwbXHUyNAFbz/wIDAQABo1MwUTAdBgNVHQ4EFgQULtTUM8bHdg4jhRT3 13 | 9piSLGyTSsUwHwYDVR0jBBgwFoAULtTUM8bHdg4jhRT39piSLGyTSsUwDwYDVR0T 14 | AQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAchsCFqaRslfgR0r0CoOJsX/H 15 | gEfqYHaL8/yJtUUqCRgyN5VtP1Cxzet8GVsAJaIKimeUCXshhaq94JQRZqxMFxJD 16 | sD9nfWVByorkSO9tmq+1FCWRzfau1AFsJxR6J1hpIbRfcfoi9HG4QnoJwULK/2yh 17 | DEXwnmgsCHTcNnj2U9F2vMLydHEKtmtMNe/S0Z7fHw1qlmqXgXmN5a/KTEPVBQm8 18 | eM6AhNlzrMqUPc8IiiZ32eAdgR2eAuhLnTCdnHwnHafpep72hSjwoUXsDjWNA/xW 19 | /lHx4jVL8muLP5G9YHZWzJwMijLqIlnryyyi+IfKAn4ANW/k44Lxm5hmRXh4UA== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /testdata/server1.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDWjCCAkKgAwIBAgIJALkZ2SqlmTT6MA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV 3 | BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg 4 | Q29tcGFueSBMdGQwHhcNMTcwODA1MTU1MjIyWhcNMTgwODA1MTU1MjIyWjBCMQsw 5 | CQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZh 6 | dWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 7 | p3vZAGYjBC4kPf6jqh6NYCWMVe8Db5IOHYECtGxBVOhS5DnuXPm0LUT9UCoUfPeB 8 | mU/1LZk41J70tpwl2IHhSIFqbG+fhNlRlUAA9CIHeRa2RVkFxH7I7xt2FhwU2WsX 9 | vi+GEiOKoZ2mC2cuZ5t8zWKinW+Hd8pL270OboQ980gThfF2ugzoXlyYVa+MrE5z 10 | wjLa3lQoKkqzo54/gKwK+KycXYoljmf2Q0++sSsDAqZlFIYknfct4+ST4TvGRqSw 11 | nPxQcJ33MXDpbnGcTBPXqWxXhvocd+QrOF4Rn7fVRLWZqYoFchSnt4R3qsoPuB8q 12 | WdZn+McT6qqgLUUtRy72PQIDAQABo1MwUTAdBgNVHQ4EFgQU83D1oNnMvjHU5q3E 13 | KIJUtrHMh94wHwYDVR0jBBgwFoAU83D1oNnMvjHU5q3EKIJUtrHMh94wDwYDVR0T 14 | AQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADByFIBVqK7b2ZJVqWzQlY7pC 15 | DbR4bh+z8hwP/VU/JjlR0IdFj1tWLLPq5hmCWScntsmI8AvThCTbZgwogptvbeuA 16 | n2QRuSb3LMZBtq22XdDTPCdM4CQblinbXR3ePmz2ZpF7xxQMz8/IafeoZaaF2w4l 17 | PfHrf4ID89dMOq13MAhiSFmKeyElx2iGGnhsGQzhcoTbhDCW/HK3Zr/BN+uFyBgr 18 | PcV0H+VLOS7/XVnz5wbIiqTfnx0NhzQzw91zY2dXVOvNadD6QpNN5Abwxo9x16TP 19 | SfqKsjPvxk06wvchNAkopBLsqjLMovgKYMMbolVmFeeZVvNEnY19RoBuneJncA== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /testdata/server2.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDWjCCAkKgAwIBAgIJAJiO53P0WQTzMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV 3 | BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg 4 | Q29tcGFueSBMdGQwHhcNMTcwODA0MTAzMzIyWhcNMTgwODA0MTAzMzIyWjBCMQsw 5 | CQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZh 6 | dWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA 7 | sYk+rsOElxPB5gPH8Vg/RFzdBzDJesI4VCXYcSCl0Ek/lO4AAKjgRssjmZDWWXcZ 8 | 3/Z1UlO4cRZy6zaSllFM07WXX3SQ54iBy3lS6NgBcHiWVfKrtXz/dQrswSPBnGEH 9 | PVBmd+xHcEv4wqYnYYVtJuIcGY1P/i14h1ogpEW22cnCVYJwqmjxrD4UQ8vSeTf9 10 | YwVCbKXZ5T+eiNhfQOkCG0rClqvfvZiMgBBlLYvJrc9bELsqIbNGVD4CrkYjZGEc 11 | ToMrb/wZhqLxlcs/iXKI0/579+9vvc44/pi2MFU0nvhoSnEgopKKPy83SwBogpiH 12 | +mDcVgMiEwbXHUyNAFbz/wIDAQABo1MwUTAdBgNVHQ4EFgQULtTUM8bHdg4jhRT3 13 | 9piSLGyTSsUwHwYDVR0jBBgwFoAULtTUM8bHdg4jhRT39piSLGyTSsUwDwYDVR0T 14 | AQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAchsCFqaRslfgR0r0CoOJsX/H 15 | gEfqYHaL8/yJtUUqCRgyN5VtP1Cxzet8GVsAJaIKimeUCXshhaq94JQRZqxMFxJD 16 | sD9nfWVByorkSO9tmq+1FCWRzfau1AFsJxR6J1hpIbRfcfoi9HG4QnoJwULK/2yh 17 | DEXwnmgsCHTcNnj2U9F2vMLydHEKtmtMNe/S0Z7fHw1qlmqXgXmN5a/KTEPVBQm8 18 | eM6AhNlzrMqUPc8IiiZ32eAdgR2eAuhLnTCdnHwnHafpep72hSjwoUXsDjWNA/xW 19 | /lHx4jVL8muLP5G9YHZWzJwMijLqIlnryyyi+IfKAn4ANW/k44Lxm5hmRXh4UA== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /testdata/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCxiT6uw4SXE8Hm 3 | A8fxWD9EXN0HMMl6wjhUJdhxIKXQST+U7gAAqOBGyyOZkNZZdxnf9nVSU7hxFnLr 4 | NpKWUUzTtZdfdJDniIHLeVLo2AFweJZV8qu1fP91CuzBI8GcYQc9UGZ37EdwS/jC 5 | pidhhW0m4hwZjU/+LXiHWiCkRbbZycJVgnCqaPGsPhRDy9J5N/1jBUJspdnlP56I 6 | 2F9A6QIbSsKWq9+9mIyAEGUti8mtz1sQuyohs0ZUPgKuRiNkYRxOgytv/BmGovGV 7 | yz+JcojT/nv372+9zjj+mLYwVTSe+GhKcSCikoo/LzdLAGiCmIf6YNxWAyITBtcd 8 | TI0AVvP/AgMBAAECggEATZpnau8N+xfozsliUa24YgKRnv4FZAKXqrisRq71q/kI 9 | sOnj2GX5Oxi6o/q6p3q3Nb2+hNERs2UTsJs3MjuxcG1VEKWcXYi+65lJ03vwDSC4 10 | 3jLoObm81IWE/dvKWrfS+Us2rz757y1WPIdyeV9gWfnGPKkXiUyI/ek4kXXjuoiL 11 | WqYeWlhe2mGVxQs5DfxBigvN0th2yfmeQJTLXpoJkCDmSTS3vnvKJfiH/mflLm/B 12 | 51lgKEziqvu53JmgYmhswsgRnLtyZ4yjksMroJYPKCTA3RbDud+J6SO01ok+hiNy 13 | QcO/j+1zlFQlWqAE4BSB1IXbW/6ZQAdszPpX9l318QKBgQDaW+nq0FH7djne5838 14 | sqBO7pHGde2j6To8PuGIoskH86p8X4dgutNiingGnYoqK2qTKVTudkdu2ahwGI3P 15 | S3dflwcyJwAvTbjc3ATSoez+P0oUC62DQciMfqaCR+WYAoAR8cdHtSoMyMvLVhSs 16 | 1h05AoalTybuT+spscV8hFBdlwKBgQDQI9VxKPt9VwhKf0W6K2IKbNxvHcu0UFvZ 17 | pCZKdxQLr7tP3+oo9fWn6R0sSX/aV+km9YQsn6keX0e4ayJHNfU6UwzNSMKWQZvs 18 | JF7OBuZnpZYX/1lSIUDOw8I29kcVtqCHjpgozbFhJSQzJDDcPvRFmkPBI74sgA1z 19 | xuH7xcc52QKBgDPX7snZfB2ADG1oC/gbUQRskB/Wj/2CuljjdRjDzYcdyzSMWdAV 20 | i2qyBZ1MeilY9YzLG2cingMrmlpC+ihleooviX3W1Kxmf6Wwd1SrLWGQFT59J00q 21 | qTryNwZnm5NjxJR+GxpjYQB4DCrS3UXL8FRAzUcia9PZFbRoiMLvh0UxAoGBAIjZ 22 | prL6cTBeEvN4bw4TDCkynlTo0FDELUASL6LyXFm6t3uzC7DW1ygJm8bMpKWY+5FE 23 | CB2W9IkluHBG8IjFr3Ejvd0To+1LQgundjYcT02CkAdDOyVG++d2yrF8iAx8wVuf 24 | o+fgJmprEzwU5ZNKSS2iWj4ZFCcKIs4my9rQlUcxAoGBAMiqpes+Ka3ofb8sxpzj 25 | xAB2oOub0X93a3xQ4xv5VLDysA7+rv1CDwuYKlOKTjGFwg3CwvbnE2EKkKI9BNgh 26 | UHAdpVXWOck5ubD5EB6vp7xWvzu6AA4KyhyRJaRL1GdSLFl9JDg5YvHskVxumt+L 27 | /GgTbnT/P3+JOhCdc0H0XhGq 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /testdata/server1.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCne9kAZiMELiQ9 3 | /qOqHo1gJYxV7wNvkg4dgQK0bEFU6FLkOe5c+bQtRP1QKhR894GZT/UtmTjUnvS2 4 | nCXYgeFIgWpsb5+E2VGVQAD0Igd5FrZFWQXEfsjvG3YWHBTZaxe+L4YSI4qhnaYL 5 | Zy5nm3zNYqKdb4d3ykvbvQ5uhD3zSBOF8Xa6DOheXJhVr4ysTnPCMtreVCgqSrOj 6 | nj+ArAr4rJxdiiWOZ/ZDT76xKwMCpmUUhiSd9y3j5JPhO8ZGpLCc/FBwnfcxcOlu 7 | cZxME9epbFeG+hx35Cs4XhGft9VEtZmpigVyFKe3hHeqyg+4HypZ1mf4xxPqqqAt 8 | RS1HLvY9AgMBAAECggEAGE5R7Mvl0wp7OgAFcn/ilox8dFAumHeC0udRJCv9wzvA 9 | I90Aab/XVSaI+KRSutwUk9JVy5tL8xdqfkHlACnBLwuRDVGZvebn/xf9y3BQ01Ln 10 | euLzglPAB2td1NGYeQEgvfoZo/JCgTfmzAraYjDfiNMCtIRmDY1vOuGSAZnxf6e/ 11 | C7LB7XdKdGijUgjvHOv82DUbdt5RD/RL9chvk+i9esdjJIxS3YaSOHclV7FSjRqe 12 | f5e9MrPKuSdijYHqHOcxX3hztOrLjL7J4gVPRvjGaWbRmdU79m2oFlwfGlLTU+w6 13 | zlzIu5FqKD375rchkOKw3W8uSyPCRsdnmZxT+CZKIQKBgQDPHiXmGWlTBdcBcgaK 14 | 4PIZltAZbA+5MdDDJPDZp9b5gS8/0mXoqOvvC2PeOkrv96l3nrVoTbfOxxutftjO 15 | Xh2OfplpNkxABR8WqY7mKaHR4aphlq1QtxcCcOlmAbmo9Eo03Vhir7q913upgEnY 16 | jzEoX9b41JEJBTR6Mfbfx3b6+QKBgQDPAw2ZuBqiyPmozCDqKnP2yiOJz6wkunqI 17 | rUkZLizN+U9kByiq/NzoDGo6BNchs2XpVhoIt3ixC/xzzhDUkn7V7ZCn8BkEdj/T 18 | jgZyupMJ1wNQK9Du3GaYPRnQ1I3OrfA0oOx1ZVoOOKOEDAEAfhzSW8GWicO3305o 19 | 6/4Tq4gCZQKBgQC18vEuS+KX+chgz6/pryVfz3ou6xyA/786v6gKPYUAGTnN4mJ+ 20 | Wm8xx5rLLgCJANPSbw1EfQndUFMDPizuVgW3GYZhxD6F+znNadVMYwRyYcGRC5Jk 21 | FwPStCiF4TwdrcXG3TB5OZFelv9e74FwCpMPueobHHnxJ65rLpuHCS5/2QKBgBDF 22 | AYwLUvUO7NKUvrHZgI1kcJ6QWTSceqKpzvsgN3b0FE9ZGR1I4KhXoR9UFw1e2Amf 23 | 9PnxyvAktW24KrrdpzKzTP2dwJkQ7zi3D6SpopGwfk83TXScHB+HC5lULqyogIXy 24 | 51TXQgVW50AiLM6aaMFNt4/3VwiFKXfsbievxJPVAoGADlJV011pkxlF2wZxpvDT 25 | RGIWCtT3G3GksNPxTkLDsuBnJ38YE0p95MivBP3Q7kdVhfgOzG1AnWMeOUzcdzNM 26 | hXKW9EcyovPYrHpAcdwMXOj+WuU74l2tHLPJQ5+4zTKAc/4ZJ7TKMjZ21qRjqGHQ 27 | dxddYOr2kCOIFNreAaKD9Po= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /testdata/server2.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCxiT6uw4SXE8Hm 3 | A8fxWD9EXN0HMMl6wjhUJdhxIKXQST+U7gAAqOBGyyOZkNZZdxnf9nVSU7hxFnLr 4 | NpKWUUzTtZdfdJDniIHLeVLo2AFweJZV8qu1fP91CuzBI8GcYQc9UGZ37EdwS/jC 5 | pidhhW0m4hwZjU/+LXiHWiCkRbbZycJVgnCqaPGsPhRDy9J5N/1jBUJspdnlP56I 6 | 2F9A6QIbSsKWq9+9mIyAEGUti8mtz1sQuyohs0ZUPgKuRiNkYRxOgytv/BmGovGV 7 | yz+JcojT/nv372+9zjj+mLYwVTSe+GhKcSCikoo/LzdLAGiCmIf6YNxWAyITBtcd 8 | TI0AVvP/AgMBAAECggEATZpnau8N+xfozsliUa24YgKRnv4FZAKXqrisRq71q/kI 9 | sOnj2GX5Oxi6o/q6p3q3Nb2+hNERs2UTsJs3MjuxcG1VEKWcXYi+65lJ03vwDSC4 10 | 3jLoObm81IWE/dvKWrfS+Us2rz757y1WPIdyeV9gWfnGPKkXiUyI/ek4kXXjuoiL 11 | WqYeWlhe2mGVxQs5DfxBigvN0th2yfmeQJTLXpoJkCDmSTS3vnvKJfiH/mflLm/B 12 | 51lgKEziqvu53JmgYmhswsgRnLtyZ4yjksMroJYPKCTA3RbDud+J6SO01ok+hiNy 13 | QcO/j+1zlFQlWqAE4BSB1IXbW/6ZQAdszPpX9l318QKBgQDaW+nq0FH7djne5838 14 | sqBO7pHGde2j6To8PuGIoskH86p8X4dgutNiingGnYoqK2qTKVTudkdu2ahwGI3P 15 | S3dflwcyJwAvTbjc3ATSoez+P0oUC62DQciMfqaCR+WYAoAR8cdHtSoMyMvLVhSs 16 | 1h05AoalTybuT+spscV8hFBdlwKBgQDQI9VxKPt9VwhKf0W6K2IKbNxvHcu0UFvZ 17 | pCZKdxQLr7tP3+oo9fWn6R0sSX/aV+km9YQsn6keX0e4ayJHNfU6UwzNSMKWQZvs 18 | JF7OBuZnpZYX/1lSIUDOw8I29kcVtqCHjpgozbFhJSQzJDDcPvRFmkPBI74sgA1z 19 | xuH7xcc52QKBgDPX7snZfB2ADG1oC/gbUQRskB/Wj/2CuljjdRjDzYcdyzSMWdAV 20 | i2qyBZ1MeilY9YzLG2cingMrmlpC+ihleooviX3W1Kxmf6Wwd1SrLWGQFT59J00q 21 | qTryNwZnm5NjxJR+GxpjYQB4DCrS3UXL8FRAzUcia9PZFbRoiMLvh0UxAoGBAIjZ 22 | prL6cTBeEvN4bw4TDCkynlTo0FDELUASL6LyXFm6t3uzC7DW1ygJm8bMpKWY+5FE 23 | CB2W9IkluHBG8IjFr3Ejvd0To+1LQgundjYcT02CkAdDOyVG++d2yrF8iAx8wVuf 24 | o+fgJmprEzwU5ZNKSS2iWj4ZFCcKIs4my9rQlUcxAoGBAMiqpes+Ka3ofb8sxpzj 25 | xAB2oOub0X93a3xQ4xv5VLDysA7+rv1CDwuYKlOKTjGFwg3CwvbnE2EKkKI9BNgh 26 | UHAdpVXWOck5ubD5EB6vp7xWvzu6AA4KyhyRJaRL1GdSLFl9JDg5YvHskVxumt+L 27 | /GgTbnT/P3+JOhCdc0H0XhGq 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Certman 2 | 3 | ![version](https://img.shields.io/github/v/tag/dyson/certman?label=version) 4 | [![Build Status](https://travis-ci.org/dyson/certman.svg?branch=master)](https://travis-ci.org/dyson/certman) 5 | [![Coverage Status](https://coveralls.io/repos/github/dyson/certman/badge.svg?branch=master)](https://coveralls.io/github/dyson/certman?branch=master) 6 | [![Code Climate](https://codeclimate.com/github/dyson/certman/badges/gpa.svg)](https://codeclimate.com/github/dyson/certman) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/dyson/certman)](https://goreportcard.com/report/github.com/dyson/certman) 8 | 9 | [![GoDoc](https://godoc.org/github.com/dyson/certman?status.svg)](http://godoc.org/github.com/dyson/certman) 10 | [![license](https://img.shields.io/github/license/dyson/certman.svg)](https://github.com/dyson/certman/blob/master/LICENSE) 11 | 12 | Go TLS certificate reloading for the standard library http server. 13 | 14 | Certman watches for changes to your certificate and key files and reloads them on change allowing the server to stay online during certificate changes. Useful for Let's Encrypt but also just in general as there's no reason to bring your servers down just to update certificates and keys. 15 | 16 | ## Limitation 17 | 18 | Certman handles only a single certificate and key pair and responds to all requests with this pair. It ignores if the client is sending a server name using SNI. I'm not sure if there's a generic enough way to implement this that handles all use cases and as it's a niche feature I haven't needed so I've left it out (pull requests welcome!). Certman's codebase is small so either fork the repo or copy and paste it into your project and modify it to your needs. 19 | 20 | ## Installation 21 | Using dep for dependency management (https://github.com/golang/dep): 22 | ``` 23 | dep ensure github.com/dyson/certman 24 | ``` 25 | 26 | Using go get: 27 | ``` 28 | $ go get github.com/dyson/certman 29 | ``` 30 | ## Usage 31 | 32 | Generate a cert and key: 33 | 34 | ``` 35 | $ openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -sha256 -keyout /tmp/server.key -out /tmp/server.crt 36 | ``` 37 | Basic server passing in a logger to log certman events: 38 | ```go 39 | package main 40 | 41 | import ( 42 | "crypto/tls" 43 | "fmt" 44 | "log" 45 | "net/http" 46 | "os" 47 | 48 | "github.com/dyson/certman" 49 | ) 50 | 51 | func main() { 52 | logger := log.New(os.Stdout, "", log.LstdFlags) 53 | 54 | cm, err := certman.New("/tmp/server.crt", "/tmp/server.key") 55 | if err != nil { 56 | logger.Println(err) 57 | } 58 | cm.Logger(logger) 59 | if err := cm.Watch(); err != nil { 60 | logger.Println(err) 61 | } 62 | 63 | http.HandleFunc("/", handler) 64 | s := &http.Server{ 65 | Addr: ":8080", 66 | TLSConfig: &tls.Config{ 67 | GetCertificate: cm.GetCertificate, 68 | }, 69 | } 70 | if err := s.ListenAndServeTLS("", ""); err != nil { 71 | logger.Println(err) 72 | } 73 | } 74 | 75 | func handler(w http.ResponseWriter, r *http.Request) { 76 | fmt.Fprintf(w, "Hello") 77 | } 78 | ``` 79 | Visit https://localhost:8080. 80 | 81 | Overwrite exising certificate and key using the openssl gen command above. 82 | 83 | Visit https://localhost:8080 again. Notice how existing requests are continued to be served by the old certificate. 84 | 85 | Visit https://localhost:8080 in another browser and see the new certificate is being served for new requests. 86 | 87 | Running example: 88 | ``` 89 | $ go run main.go 90 | 2017/08/01 16:05:23 certman: certificate and key loaded 91 | 2017/08/01 16:05:23 certman: watching for cert and key change 92 | # Regenerated certificate and key here 93 | 2017/08/01 16:06:30 certman: watch event: "/tmp/server.key": WRITE 94 | 2017/08/01 16:06:30 certman: can't load cert or key file: tls: private key does not match public key 95 | 2017/08/01 16:06:30 certman: watch event: "/tmp/server.key": WRITE 96 | 2017/08/01 16:06:30 certman: can't load cert or key file: tls: private key does not match public key 97 | 2017/08/01 16:06:32 certman: watch event: "/tmp/server.crt": WRITE 98 | 2017/08/01 16:06:32 certman: certificate and key loaded 99 | # Certificate loaded once the certificate and key can both be read correctly and they match 100 | ``` 101 | 102 | ## License 103 | See LICENSE file. 104 | -------------------------------------------------------------------------------- /certman.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Dyson Simmons. All rights reserved. 2 | // Use of this source code is governed by a MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package certman provides live reloading of the certificate and key 6 | // files used by the standard library http.Server. It defines a type, 7 | // certMan, with methods watching and getting the files. 8 | // Only valid certificate and key pairs are loaded and an optional 9 | // logger can be passed to certman for logging providing it implements 10 | // the logger interface. 11 | package certman 12 | 13 | import ( 14 | "crypto/tls" 15 | "path/filepath" 16 | "sync" 17 | 18 | "github.com/fsnotify/fsnotify" 19 | "github.com/pkg/errors" 20 | ) 21 | 22 | // A CertMan represents a certificate manager able to watch certificate 23 | // and key pairs for changes. 24 | type CertMan struct { 25 | mu sync.RWMutex 26 | certFile string 27 | keyFile string 28 | keyPair *tls.Certificate 29 | watcher *fsnotify.Watcher 30 | watching chan bool 31 | log logger 32 | } 33 | 34 | // logger is an interface that wraps the basic Printf method. 35 | type logger interface { 36 | Printf(string, ...interface{}) 37 | } 38 | 39 | type nopLogger struct{} 40 | 41 | func (l *nopLogger) Printf(format string, v ...interface{}) {} 42 | 43 | // New creates a new certMan. The certFile and the keyFile 44 | // are both paths to the location of the files. Relative and 45 | // absolute paths are accepted. 46 | func New(certFile, keyFile string) (*CertMan, error) { 47 | var err error 48 | 49 | certFile, err = filepath.Abs(certFile) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | keyFile, err = filepath.Abs(keyFile) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | cm := &CertMan{ 60 | mu: sync.RWMutex{}, 61 | certFile: certFile, 62 | keyFile: keyFile, 63 | log: &nopLogger{}, 64 | } 65 | 66 | return cm, nil 67 | } 68 | 69 | // Logger sets the logger for certMan to use. It accepts 70 | // a logger interface. 71 | func (cm *CertMan) Logger(logger logger) { 72 | cm.log = logger 73 | } 74 | 75 | // Watch starts watching for changes to the certificate 76 | // and key files. On any change the certificate and key 77 | // are reloaded. If there is an issue the load will fail 78 | // and the old (if any) certificates and keys will continue 79 | // to be used. 80 | func (cm *CertMan) Watch() error { 81 | var err error 82 | 83 | if cm.watcher, err = fsnotify.NewWatcher(); err != nil { 84 | return errors.Wrap(err, "can't create watcher") 85 | } 86 | 87 | if err = cm.watcher.Add(cm.certFile); err != nil { 88 | return errors.Wrap(err, "can't watch cert file") 89 | } 90 | 91 | if err = cm.watcher.Add(cm.keyFile); err != nil { 92 | return errors.Wrap(err, "can't watch key file") 93 | } 94 | 95 | if err := cm.load(); err != nil { 96 | cm.log.Printf("can't load cert or key file: %v", err) 97 | } 98 | 99 | cm.log.Printf("watching for cert and key change") 100 | 101 | cm.watching = make(chan bool) 102 | 103 | go cm.run() 104 | 105 | return nil 106 | } 107 | 108 | func (cm *CertMan) load() error { 109 | keyPair, err := tls.LoadX509KeyPair(cm.certFile, cm.keyFile) 110 | if err == nil { 111 | cm.mu.Lock() 112 | cm.keyPair = &keyPair 113 | cm.mu.Unlock() 114 | cm.log.Printf("certificate and key loaded") 115 | } 116 | 117 | return err 118 | } 119 | 120 | func (cm *CertMan) run() { 121 | loop: 122 | for { 123 | select { 124 | case <-cm.watching: 125 | break loop 126 | case event := <-cm.watcher.Events: 127 | cm.log.Printf("watch event: %v", event) 128 | if err := cm.load(); err != nil { 129 | cm.log.Printf("can't load cert or key file: %v", err) 130 | } 131 | case err := <-cm.watcher.Errors: 132 | cm.log.Printf("error watching files: %v", err) 133 | } 134 | } 135 | 136 | cm.log.Printf("stopped watching") 137 | 138 | cm.watcher.Close() 139 | } 140 | 141 | // GetCertificate returns the loaded certificate for use by 142 | // the TLSConfig fields GetCertificate field in a http.Server. 143 | func (cm *CertMan) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { 144 | cm.mu.RLock() 145 | defer cm.mu.RUnlock() 146 | 147 | return cm.keyPair, nil 148 | } 149 | 150 | // Stop tells certMan to stop watching for changes to the 151 | // certificate and key files. 152 | func (cm *CertMan) Stop() { 153 | cm.watching <- false 154 | } 155 | -------------------------------------------------------------------------------- /certman_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Dyson Simmons. All rights reserved. 2 | // Use of this source code is governed by a MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package certman_test 6 | 7 | import ( 8 | "bytes" 9 | "crypto/tls" 10 | "io" 11 | "log" 12 | "os" 13 | "reflect" 14 | "strings" 15 | "testing" 16 | "time" 17 | 18 | "github.com/dyson/certman" 19 | ) 20 | 21 | func TestValidPair(t *testing.T) { 22 | buf := new(bytes.Buffer) 23 | l := log.New(buf, "", 0) 24 | 25 | cm, err := certman.New("./testdata/server1.crt", "./testdata/server1.key") 26 | if err != nil { 27 | t.Fatalf("could not create certman: %v", err) 28 | } 29 | 30 | cm.Logger(l) 31 | if err := cm.Watch(); err != nil { 32 | t.Fatalf("could not watch files: %v", err) 33 | } 34 | 35 | logWant := "certificate and key loaded\n" + 36 | "watching for cert and key change\n" 37 | logGot := buf.String() 38 | 39 | if logGot != logWant { 40 | t.Log("log output expected:", logWant) 41 | t.Log("log output received:", logGot) 42 | t.Fatal("log from certman not as expected") 43 | } 44 | } 45 | 46 | func TestInvalidPair(t *testing.T) { 47 | buf := new(bytes.Buffer) 48 | l := log.New(buf, "", 0) 49 | 50 | cm, err := certman.New("./testdata/server1.crt", "./testdata/server2.key") 51 | if err != nil { 52 | t.Fatalf("could not create certman: %v", err) 53 | } 54 | 55 | cm.Logger(l) 56 | if err := cm.Watch(); err != nil { 57 | t.Fatalf("could not watch files: %v", err) 58 | } 59 | 60 | logWant := "can't load cert or key file: tls: private key does not match public key\n" + 61 | "watching for cert and key change\n" 62 | logGot := buf.String() 63 | 64 | if logGot != logWant { 65 | t.Log("log output expected:", logWant) 66 | t.Log("log output received:", logGot) 67 | t.Fatalf("test didn't in the way expected") 68 | } 69 | } 70 | 71 | func TestCertificateNotFound(t *testing.T) { 72 | buf := new(bytes.Buffer) 73 | l := log.New(buf, "", 0) 74 | 75 | cm, err := certman.New("./testdata/nothere.crt", "./testdata/server2.key") 76 | if err != nil { 77 | t.Fatalf("could not create certman: %v", err) 78 | } 79 | 80 | cm.Logger(l) 81 | if err := cm.Watch(); err != nil { 82 | if !strings.HasPrefix(err.Error(), "can't watch cert file:") { 83 | t.Fatalf("unexpected watch error: %v", err) 84 | } 85 | } 86 | } 87 | 88 | func TestKeyNotFound(t *testing.T) { 89 | buf := new(bytes.Buffer) 90 | l := log.New(buf, "", 0) 91 | cm, err := certman.New("./testdata/server1.crt", "./testdata/nothere.key") 92 | 93 | if err != nil { 94 | t.Fatalf("could not create certman: %v", err) 95 | } 96 | 97 | cm.Logger(l) 98 | if err := cm.Watch(); err != nil { 99 | if !strings.HasPrefix(err.Error(), "can't watch key file:") { 100 | t.Fatalf("unexpected watch error: %v", err) 101 | } 102 | } 103 | } 104 | 105 | func TestValidPairValidPair(t *testing.T) { 106 | buf := new(bytes.Buffer) 107 | l := log.New(buf, "", 0) 108 | 109 | copyPair("./testdata/server1.crt", "./testdata/server1.key") 110 | 111 | cm, err := certman.New("./testdata/server.crt", "./testdata/server.key") 112 | if err != nil { 113 | t.Fatalf("could not create certman: %v", err) 114 | } 115 | 116 | cm.Logger(l) 117 | if err := cm.Watch(); err != nil { 118 | t.Fatalf("could not watch files: %v", err) 119 | } 120 | 121 | logWant := "certificate and key loaded\n" + 122 | "watching for cert and key change\n" 123 | logGot := buf.String() 124 | 125 | if logGot != logWant { 126 | t.Log("log output expected:", logWant) 127 | t.Log("log output received:", logGot) 128 | t.Fatalf("log from certman not as expected") 129 | } 130 | 131 | buf.Reset() 132 | copyPair("./testdata/server2.crt", "./testdata/server2.key") 133 | 134 | time.Sleep(200 * time.Millisecond) 135 | 136 | logWant = "certificate and key loaded" 137 | logGot = strings.Split(buf.String(), "\n")[3] 138 | 139 | if logGot != logWant { 140 | t.Log("log output expected:", logWant) 141 | t.Log("log output received:", logGot) 142 | t.Fatalf("log from certman not as expected") 143 | } 144 | } 145 | 146 | func TestValidPairInvalidPair(t *testing.T) { 147 | buf := new(bytes.Buffer) 148 | l := log.New(buf, "", 0) 149 | 150 | copyPair("./testdata/server1.crt", "./testdata/server1.key") 151 | 152 | cm, err := certman.New("./testdata/server.crt", "./testdata/server.key") 153 | if err != nil { 154 | t.Fatalf("could not create certman: %v", err) 155 | } 156 | 157 | cm.Logger(l) 158 | if err := cm.Watch(); err != nil { 159 | t.Fatalf("could not watch files: %v", err) 160 | } 161 | 162 | logWant := "certificate and key loaded\n" + 163 | "watching for cert and key change\n" 164 | logGot := buf.String() 165 | 166 | if logGot != logWant { 167 | t.Log("log output expected:", logWant) 168 | t.Log("log output received:", logGot) 169 | t.Fatalf("log from certman not as expected") 170 | } 171 | 172 | buf.Reset() 173 | 174 | copyPair("./testdata/server1.crt", "./testdata/server2.key") 175 | 176 | time.Sleep(200 * time.Millisecond) 177 | 178 | logWant = "can't load cert or key file: tls: private key does not match public key" 179 | logGot = strings.Split(buf.String(), "\n")[3] 180 | 181 | if logGot != logWant { 182 | t.Log("log output expected:", logWant) 183 | t.Log("log output received:", logGot) 184 | t.Fatalf("log from certman not as expected") 185 | } 186 | } 187 | 188 | func TestStop(t *testing.T) { 189 | buf := new(bytes.Buffer) 190 | l := log.New(buf, "", 0) 191 | 192 | copyPair("./testdata/server1.crt", "./testdata/server1.key") 193 | 194 | cm, err := certman.New("./testdata/server.crt", "./testdata/server.key") 195 | if err != nil { 196 | t.Fatalf("could not create certman: %v", err) 197 | } 198 | 199 | cm.Logger(l) 200 | if err := cm.Watch(); err != nil { 201 | t.Fatalf("could not watch files: %v", err) 202 | } 203 | 204 | logWant := "certificate and key loaded\n" + 205 | "watching for cert and key change\n" 206 | logGot := buf.String() 207 | 208 | if logGot != logWant { 209 | t.Log("log output expected:", logWant) 210 | t.Log("log output received:", logGot) 211 | t.Fatalf("log from certman not as expected") 212 | } 213 | 214 | buf.Reset() 215 | cm.Stop() 216 | 217 | copyPair("./testdata/server2.crt", "./testdata/server2.key") 218 | time.Sleep(200 * time.Millisecond) 219 | 220 | logWant = "stopped watching\n" 221 | logGot = buf.String() 222 | 223 | if logGot != logWant { 224 | t.Log("log output expected:", logWant) 225 | t.Log("log output received:", logGot) 226 | t.Fatalf("log from certman not as expected") 227 | } 228 | } 229 | 230 | func TestGetCertificate(t *testing.T) { 231 | cm, err := certman.New("./testdata/server1.crt", "./testdata/server1.key") 232 | if err != nil { 233 | t.Fatalf("could not create certman: %v", err) 234 | } 235 | 236 | if err := cm.Watch(); err != nil { 237 | t.Fatalf("could not watch files: %v", err) 238 | } 239 | 240 | hello := &tls.ClientHelloInfo{} 241 | 242 | cmCert, err := cm.GetCertificate(hello) 243 | if err != nil { 244 | t.Fatalf("could not get certman certificate") 245 | } 246 | 247 | expectedCert, _ := tls.LoadX509KeyPair("./testdata/server1.crt", "./testdata/server1.key") 248 | if err != nil { 249 | t.Fatalf("could not load certificate and key files to test: %v", err) 250 | } 251 | 252 | if !reflect.DeepEqual(cmCert.Certificate, expectedCert.Certificate) { 253 | t.Fatalf("certman certificate doesn't match expected certificate") 254 | } 255 | 256 | } 257 | 258 | func copyPair(crt, key string) { 259 | // ignore error handling 260 | crtSource, _ := os.Open(crt) 261 | defer crtSource.Close() 262 | 263 | crtDest, _ := os.Create("./testdata/server.crt") 264 | defer crtDest.Close() 265 | 266 | io.Copy(crtDest, crtSource) 267 | 268 | keySource, _ := os.Open(key) 269 | defer keySource.Close() 270 | 271 | keyDest, _ := os.Create("./testdata/server.key") 272 | defer keyDest.Close() 273 | 274 | io.Copy(keyDest, keySource) 275 | } 276 | --------------------------------------------------------------------------------