From e7d6b97b09534c3f40c20d0ff66ad3a726c9a4fc Mon Sep 17 00:00:00 2001 From: Dwi Siswanto Date: Sat, 2 Mar 2024 20:39:06 +0700 Subject: [PATCH] init 1 Signed-off-by: Dwi Siswanto --- .github/FUNDING.yaml | 1 + .github/workflows/tests.yaml | 30 ++++++ CODEOWNERS | 1 + LICENSE | 201 +++++++++++++++++++++++++++++++++++ README.md | 81 ++++++++++++++ data.go | 7 ++ doc.go | 4 + errors.go | 17 +++ errors_test.go | 19 ++++ go.mod | 10 ++ go.sum | 6 ++ options.go | 12 +++ pkg/cert/cert.go | 60 +++++++++++ pkg/sslbl/const.go | 3 + pkg/sslbl/doc.go | 3 + pkg/sslbl/record.go | 15 +++ pkg/sslbl/sslbl.go | 62 +++++++++++ pkg/sslbl/utils.go | 44 ++++++++ roundtripper.go | 28 +++++ roundtripper_test.go | 24 +++++ sebel.go | 92 ++++++++++++++++ sebel_example_test.go | 44 ++++++++ sebel_test.go | 86 +++++++++++++++ 23 files changed, 850 insertions(+) create mode 100644 .github/FUNDING.yaml create mode 100644 .github/workflows/tests.yaml create mode 100644 CODEOWNERS create mode 100644 LICENSE create mode 100644 README.md create mode 100644 data.go create mode 100644 doc.go create mode 100644 errors.go create mode 100644 errors_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 options.go create mode 100644 pkg/cert/cert.go create mode 100644 pkg/sslbl/const.go create mode 100644 pkg/sslbl/doc.go create mode 100644 pkg/sslbl/record.go create mode 100644 pkg/sslbl/sslbl.go create mode 100644 pkg/sslbl/utils.go create mode 100644 roundtripper.go create mode 100644 roundtripper_test.go create mode 100644 sebel.go create mode 100644 sebel_example_test.go create mode 100644 sebel_test.go diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml new file mode 100644 index 0000000..c20c1fc --- /dev/null +++ b/.github/FUNDING.yaml @@ -0,0 +1 @@ +github: ["dwisiswant0"] \ No newline at end of file diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..bd6c94f --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,30 @@ +on: + push: + branches: + - master + paths: + - "**.go" + - "go.mod" + pull_request: + branches: + - "**" + paths: + - "**.go" + - "go.mod" + workflow_dispatch: + +name: tests +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache-dependency-path: '**/go.sum' + - run: go test -v -race ./... + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + args: --timeout=5m diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..bddc66a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @dwisiswant0 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d516fb0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Dwi Siswanto + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ddad69 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# sebel + +[![GoDoc](https://pkg.go.dev/static/frontend/badge/badge.svg)](http://pkg.go.dev/github.com/teler-sh/sebel) +[![tests](https://github.com/teler-sh/sebel/actions/workflows/tests.yaml/badge.svg)](https://github.com/teler-sh/sebel/actions/workflows/tests.yaml) + +sebel is a Go package that provides functionality for checking SSL/TLS certificates against a malicious connections, by identifying and blacklisting certificates used by botnet command and control (C&C) servers. + +## Usage + +Setting up Sebel instance: + +```go +import "github.com/teler-sh/sebel" + +// ... + +s := sebel.New(Options{/* ... */}) +``` + +> [!NOTE] +> The `Options` parameter is optional. Currently, the only supported option is disabling the SSL blacklist. See [TODO](#TODO). + +### Examples + +Next, set the transport for the HTTP client you are using: + +```go +// initialize Sebel (fetch SSLBL data) +s := sebel.New() + +client := &http.Client{ + Transport: s.RoundTripper(http.DefaultTransport), +} + +// now, you can use [client.Do], [client.Get], etc. to create requests. + +resp, err := client.Get("https://c2.host") +if err != nil && sebel.IsBlacklist(err) { + // certificate blacklisted + panic(err) +} +defer resp.Body.Close() +``` + +Alternatively, for seamless integration without configuring a new client, replace your current default HTTP client with Sebel's `RoundTripper`: + +```go +http.DefaultClient.Transport = sebel.New().RoundTripper(http.DefaultTransport) +``` + +You can also check the certificate later using Sebel's `CheckTLS`. + +```go +r, err := http.Get("https://c2.host") +if err != nil { + panic(err) +} +defer r.Body.Close() + +s := sebel.New() + +_, err = s.CheckTLS(r.TLS) +if err != nil && sebel.IsBlacklist(err) { + // certificate blacklisted + panic(err) +} +``` + +These examples demonstrate various ways to set up Sebel and integrate it with HTTP clients for SSL/TLS certificate checks. + +## TODO + +* [ ] Caching SSLBL data under user-specific cache directory. +* [ ] Add `io.Writer` option. +* [ ] Add `CheckIP` method. + +# License + +`sebel` is released by [**@dwisiswant0**](https://github.com/dwisiswant0) under the Apache 2.0 license. See [LICENSE](/LICENSE). + +The data used in this project are © by [abuse.ch](https://abuse.ch/) under [CC0](https://creativecommons.org/public-domain/cc0/). \ No newline at end of file diff --git a/data.go b/data.go new file mode 100644 index 0000000..b5c41c6 --- /dev/null +++ b/data.go @@ -0,0 +1,7 @@ +package sebel + +import "github.com/teler-sh/sebel/pkg/sslbl" + +type data struct { + sslbl []sslbl.Record +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..4c1d036 --- /dev/null +++ b/doc.go @@ -0,0 +1,4 @@ +// Package sebel provides functionality for checking SSL/TLS certificates +// against a malicious connections, by identifying and blacklisting certificates +// used by botnet command and control (C&C) servers. +package sebel diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..702df65 --- /dev/null +++ b/errors.go @@ -0,0 +1,17 @@ +package sebel + +import "errors" + +var ( + ErrSSLBlacklist = errors.New("certificate blacklisted") + ErrNoSSLBLData = errors.New("no SSLBL data") +) + +// IsBlacklist checks if the given error is an [ErrSSLBlacklist]. +func IsBlacklist(err error) bool { + if err == nil { + return false + } + + return errors.Is(err, ErrSSLBlacklist) +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..3dd70fe --- /dev/null +++ b/errors_test.go @@ -0,0 +1,19 @@ +package sebel + +import "testing" + +func TestIsBlacklist(t *testing.T) { + t.Parallel() + + t.Run("true", func(t *testing.T) { + if IsBlacklist(ErrSSLBlacklist) != true { + t.Fail() + } + }) + + t.Run("false", func(t *testing.T) { + if IsBlacklist(ErrNoSSLBLData) == true { + t.Fail() + } + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8f81472 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/teler-sh/sebel + +go 1.22.0 + +require ( + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/samber/lo v1.39.0 +) + +require golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d2a0d38 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= diff --git a/options.go b/options.go new file mode 100644 index 0000000..270a9bd --- /dev/null +++ b/options.go @@ -0,0 +1,12 @@ +package sebel + +// Options holds configuration settings for the [Sebel] package. +type Options struct { + // DisableSSLBlacklist, when set to true, disables SSL/TLS certificate + // blacklist checks. + DisableSSLBlacklist bool + + // TODO(dwisiswant0): Add these fields + // DisableIPBlacklist bool + // Output io.Writer +} diff --git a/pkg/cert/cert.go b/pkg/cert/cert.go new file mode 100644 index 0000000..5d06b00 --- /dev/null +++ b/pkg/cert/cert.go @@ -0,0 +1,60 @@ +// Package cert provides utilities for working with SSL/TLS certificates, +// including fingerprint generation. +package cert + +import ( + "bytes" + "fmt" + + "crypto/sha1" + "crypto/sha256" + "crypto/x509" +) + +// Fingerprint defines methods to calculate SHA1 and SHA256 fingerprints. +// +// If the certificate is nil, an empty buffer is returned from SHA1 & SHA256. +type Fingerprint interface { + SHA1() *bytes.Buffer + SHA256() *bytes.Buffer +} + +// fingerprint implementation of Fingerprint interface. +type fingerprint struct { + *x509.Certificate +} + +// New creates a new Fingerprint instance based on the [x509.Certificate]. +func New(cert *x509.Certificate) Fingerprint { + return &fingerprint{Certificate: cert} +} + +func (f *fingerprint) SHA1() *bytes.Buffer { + var b bytes.Buffer + + if f.Certificate == nil { + return &b + } + + sha1sum := sha1.Sum(f.Certificate.Raw) + for _, v := range sha1sum { + fmt.Fprintf(&b, "%02x", v) + } + + return &b +} + +func (f *fingerprint) SHA256() *bytes.Buffer { + var b bytes.Buffer + + if f.Certificate == nil { + return &b + } + + sha256sum := sha256.Sum256(f.Certificate.Raw) + for _, v := range sha256sum { + fmt.Fprintf(&b, "%02x", v) + } + + return &b +} diff --git a/pkg/sslbl/const.go b/pkg/sslbl/const.go new file mode 100644 index 0000000..a6d96fe --- /dev/null +++ b/pkg/sslbl/const.go @@ -0,0 +1,3 @@ +package sslbl + +const dataURL = "https://sslbl.abuse.ch/blacklist/sslblacklist.csv" diff --git a/pkg/sslbl/doc.go b/pkg/sslbl/doc.go new file mode 100644 index 0000000..12d1abd --- /dev/null +++ b/pkg/sslbl/doc.go @@ -0,0 +1,3 @@ +// Package sslbl provides a simple SSL Blacklist (SSLBL) implementation for +// checking certificates. +package sslbl diff --git a/pkg/sslbl/record.go b/pkg/sslbl/record.go new file mode 100644 index 0000000..08f7f5c --- /dev/null +++ b/pkg/sslbl/record.go @@ -0,0 +1,15 @@ +package sslbl + +// Record represents an entry in the SSL Blacklist (SSLBL). +type Record struct { + Listing struct { + // Date is the date when the SSL certificate was listed in the SSLBL. + Date string + // Reason provides information about why the SSL certificate was + // blacklisted. + Reason string + } + // SHA1Sum is the SHA-1 fingerprint of the SSL certificate associated with + // the record. + SHA1Sum string +} diff --git a/pkg/sslbl/sslbl.go b/pkg/sslbl/sslbl.go new file mode 100644 index 0000000..494643e --- /dev/null +++ b/pkg/sslbl/sslbl.go @@ -0,0 +1,62 @@ +package sslbl + +import ( + "io" + + "encoding/csv" + "net/http" + + "github.com/samber/lo" +) + +// Find searches for a record with a given SHA-1 fingerprint in a slice of SSLBL +// records. +// +// It returns the found record and a boolean indicating whether the record was +// found. +func Find(sha1sum string, records []Record) (*Record, bool) { + record, ok := lo.Find(records, func(r Record) bool { + return r.SHA1Sum == sha1sum + }) + + return &record, ok +} + +// Get retrieves SSLBL records from a `sslbl.abuse.ch`, parses the CSV data, +// and returns them. +func Get() ([]Record, error) { + var records []Record + + resp, err := http.Get(dataURL) + if err != nil { + return records, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return records, err + } + + csvData := sanitizeBody(body) + csvReader := csv.NewReader(csvData) + + data, err := csvReader.ReadAll() + if err != nil { + return records, err + } + records = parseCSV(data) + + return records, nil +} + +// MustGet is like [Get] but panics if there is an error during the retrieval +// process. +func MustGet() []Record { + records, err := Get() + if err != nil { + panic(err) + } + + return records +} diff --git a/pkg/sslbl/utils.go b/pkg/sslbl/utils.go new file mode 100644 index 0000000..17c6a0f --- /dev/null +++ b/pkg/sslbl/utils.go @@ -0,0 +1,44 @@ +package sslbl + +import "bytes" + +func sanitizeBody(body []byte) *bytes.Reader { + lines := bytes.Split(body, []byte("\n")) + var split [][]byte + + for _, line := range lines { + if !bytes.HasPrefix(line, []byte("#")) { + split = append(split, line) + } + } + + cleaned := bytes.Join(split, []byte("\n")) + + // pattern := regexp.MustCompile(`^\s*#.*$`) + // cleaned := pattern.ReplaceAll(body, []byte("")) + + return bytes.NewReader(cleaned) +} + +func parseCSV(records [][]string) []Record { + var sslBlRecords []Record + + for _, record := range records { + var data Record + + for i, value := range record { + switch i { + case 0: + data.Listing.Date = value + case 1: + data.SHA1Sum = value + case 2: + data.Listing.Reason = value + } + } + + sslBlRecords = append(sslBlRecords, data) + } + + return sslBlRecords +} diff --git a/roundtripper.go b/roundtripper.go new file mode 100644 index 0000000..645aec5 --- /dev/null +++ b/roundtripper.go @@ -0,0 +1,28 @@ +package sebel + +import ( + "net/http" +) + +// roundTripper is a wrapper for the [http.RoundTripper] interface with a +// reference to Sebel. +type roundTripper struct { + http.RoundTripper + sebel *Sebel +} + +// RoundTrip implements the [http.RoundTripper] interface and checks the TLS +// connection after the round trip. +func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := rt.RoundTripper.RoundTrip(req) + if resp.TLS != nil && err == nil { + rt.sebel.tls = resp.TLS + + _, err = rt.sebel.checkTLS() + if err != nil { + resp = nil + } + } + + return resp, err +} diff --git a/roundtripper_test.go b/roundtripper_test.go new file mode 100644 index 0000000..49c5280 --- /dev/null +++ b/roundtripper_test.go @@ -0,0 +1,24 @@ +package sebel + +import ( + "testing" + + "net/http" +) + +func TestRoundTrip(t *testing.T) { + t.Parallel() + + tr := http.DefaultTransport + rt := &roundTripper{RoundTripper: tr, sebel: sebel} + + req, err := http.NewRequest("GET", "https://httpbin.org/get", nil) + if err != nil { + t.SkipNow() + } + + _, err = rt.RoundTrip(req) + if err != nil { + t.Fail() + } +} diff --git a/sebel.go b/sebel.go new file mode 100644 index 0000000..cb944e5 --- /dev/null +++ b/sebel.go @@ -0,0 +1,92 @@ +package sebel + +import ( + "crypto/tls" + "crypto/x509" + "net/http" + + "github.com/teler-sh/sebel/pkg/cert" + "github.com/teler-sh/sebel/pkg/sslbl" +) + +// Sebel holds information and [Options]. +type Sebel struct { + data data + options *Options + tls *tls.ConnectionState +} + +// New creates a new instance of [Sebel] with the provided options. +func New(opt ...Options) *Sebel { + sebel := new(Sebel) + + if len(opt) > 0 { + sebel.options = &opt[0] + } else { + sebel.options = new(Options) + } + + sebel.data.sslbl = sslbl.MustGet() + + return sebel +} + +// RoundTripper creates a new RoundTripper using the provided [http.RoundTripper] +// and [Sebel] instance. +func (s *Sebel) RoundTripper(rt http.RoundTripper) http.RoundTripper { + return &roundTripper{RoundTripper: rt, sebel: s} +} + +// CheckTLS checks the TLS connection against the SSLBL (SSL Blacklist) and +// returns the SSLBL record. +// +// It returns [ErrSSLBlacklist] error if the certificate is blacklisted. +func (s *Sebel) CheckTLS(connState *tls.ConnectionState) (*sslbl.Record, error) { + s.tls = connState + + return s.checkTLS() +} + +// getCert retrieves the peer certificate from the TLS connection state. +func (s *Sebel) getCert() *x509.Certificate { + if s.tls == nil { + return nil + } + + if cert := s.tls.PeerCertificates; len(cert) > 0 { + return cert[0] + } + + return nil +} + +// checkTLS runs actual checks on the TLS connection and returns the SSLBL +// record and [ErrSSLBlacklist] error if blacklisted. +func (s *Sebel) checkTLS() (*sslbl.Record, error) { + record, ok := new(sslbl.Record), false + + // return early if disabled + if s.options.DisableSSLBlacklist { + return record, nil + } + + data := s.data.sslbl + if len(data) == 0 { + return record, ErrNoSSLBLData + } + + certificate := s.getCert() + if certificate == nil { + return record, nil + } + + fingerprint := cert.New(certificate) + sha1sum := fingerprint.SHA1().String() + + record, ok = sslbl.Find(sha1sum, data) + if ok { + return record, ErrSSLBlacklist + } + + return record, nil +} diff --git a/sebel_example_test.go b/sebel_example_test.go new file mode 100644 index 0000000..3088fbc --- /dev/null +++ b/sebel_example_test.go @@ -0,0 +1,44 @@ +package sebel_test + +import ( + "net/http" + + "github.com/teler-sh/sebel" +) + +func ExampleNew() { + client := &http.Client{ + Transport: sebel.New().RoundTripper(http.DefaultTransport), + } + + resp, err := client.Get("https://c2.host") + if err != nil && sebel.IsBlacklist(err) { + // certificate blacklisted + panic(err) + } + defer resp.Body.Close() + + println("OK") +} + +func ExampleSebel_CheckTLS() { + r, err := http.Get("https://c2.host") + if err != nil { + panic(err) + } + defer r.Body.Close() + + s := sebel.New() + + _, err = s.CheckTLS(r.TLS) + if err != nil && sebel.IsBlacklist(err) { + // certificate blacklisted + panic(err) + } +} + +// To seamlessly integrate it without need to configure a new client, you can +// simply replace your current http.DefaultClient with sebel's RoundTripper. +func ExampleSebel_RoundTripper() { + http.DefaultClient.Transport = sebel.New().RoundTripper(http.DefaultTransport) +} diff --git a/sebel_test.go b/sebel_test.go new file mode 100644 index 0000000..b367f11 --- /dev/null +++ b/sebel_test.go @@ -0,0 +1,86 @@ +package sebel + +import ( + "testing" + + "net/http" +) + +var sebel *Sebel + +func init() { + sebel = New() +} + +func TestNew(t *testing.T) { + t.Parallel() + + t.Run("empty", func(t *testing.T) { + if sebel == nil { + t.Fail() + } + }) + + t.Run("set", func(t *testing.T) { + sebelWithOptions := New(Options{DisableSSLBlacklist: true}) + if !sebelWithOptions.options.DisableSSLBlacklist { + t.Fail() + } + }) +} + +func TestRoundTripper(t *testing.T) { + t.Parallel() + + tr := http.DefaultTransport + if tr == sebel.RoundTripper(tr) { + t.Fail() + } +} + +func TestCheckTLS(t *testing.T) { + resp, err := http.Get("https://httpbin.org/get") + if err != nil { + t.SkipNow() + } + + if resp.TLS == nil { + t.SkipNow() + } + + t.Parallel() + + t.Run("default", func(t *testing.T) { + _, err := sebel.CheckTLS(resp.TLS) + if err != nil { + t.Fail() + } + }) + + t.Run("WithDisableSSLBlacklist", func(t *testing.T) { + sebelWithOptions := New(Options{DisableSSLBlacklist: true}) + + _, err := sebelWithOptions.CheckTLS(resp.TLS) + if err != nil { + t.Fail() + } + }) +} + +func BenchmarkCheckTLS(b *testing.B) { + resp, err := http.Get("https://httpbin.org/get") + if err != nil { + b.SkipNow() + } + + if resp.TLS == nil { + b.SkipNow() + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = sebel.CheckTLS(resp.TLS) + } +}