From b27992b308413487901a29767df8fe254964e7f1 Mon Sep 17 00:00:00 2001 From: Mikhail Knyazhev Date: Sun, 21 Sep 2025 23:17:44 +0300 Subject: [PATCH 1/2] go1.24 + refactoring x509 --- .github/dependabot.yml | 0 .github/workflows/ci.yml | 2 +- .github/workflows/codeql.yml | 38 ---- .gitignore | 21 +- .golangci.yml | 394 ++++------------------------------- LICENSE | 2 +- Makefile | 23 +- aesgcm/aesgcm.go | 7 +- aesgcm/aesgcm_test.go | 5 +- go.mod | 8 +- go.sum | 10 +- hash/hash.go | 101 +++++++-- hash/hash_test.go | 59 ++++++ pgp/pgp.go | 2 +- pgp/pgp_test.go | 2 +- x509/x509.go | 155 -------------- x509/x509_test.go | 31 --- x509cert/cert.go | 133 ++++++++++++ x509cert/cert_test.go | 109 ++++++++++ x509cert/common.go | 19 ++ x509cert/config.go | 54 +++++ x509cert/crl.go | 47 +++++ x509cert/models.go | 281 +++++++++++++++++++++++++ x509cert/ocsp.go | 70 +++++++ x509cert/utils.go | 49 +++++ 25 files changed, 987 insertions(+), 635 deletions(-) mode change 100644 => 100755 .github/dependabot.yml mode change 100644 => 100755 .github/workflows/ci.yml delete mode 100644 .github/workflows/codeql.yml mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .golangci.yml mode change 100644 => 100755 Makefile create mode 100644 hash/hash_test.go delete mode 100644 x509/x509.go delete mode 100644 x509/x509_test.go create mode 100644 x509cert/cert.go create mode 100644 x509cert/cert_test.go create mode 100644 x509cert/common.go create mode 100644 x509cert/config.go create mode 100644 x509cert/crl.go create mode 100644 x509cert/models.go create mode 100644 x509cert/ocsp.go create mode 100644 x509cert/utils.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml old mode 100644 new mode 100755 index 8ddfece..a76bd9e --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.21' ] + go: [ '1.24' ] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index ddee4f0..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,38 +0,0 @@ - -name: "CodeQL" - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - schedule: - - cron: '16 8 * * 1' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'go' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 37e78f6..2d09f4d --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ + +.tools/ +bin/ +vendor/ +build/ +.idea/ +.vscode/ +coverage.txt +coverage.out *.exe *.exe~ *.dll @@ -8,14 +17,4 @@ *.mmdb *.test *.out - -.idea/ -.vscode/ -.tools/ - -coverage.txt -coverage.out - -bin/ -vendor/ -build/ +.env \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml old mode 100644 new mode 100755 index f94fdbb..d680b02 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,375 +1,71 @@ +version: "2" -# options for analysis running run: - # timeout for analysis, e.g. 30s, 5m, default is 1m - deadline: 5m - - # exit code when at least one issue was found, default is 1 + go: "1.24" + timeout: 5m + tests: false issues-exit-code: 1 - - # include test files or not, default is true - tests: true - - # which files to skip: they will be analyzed, but issues from them - # won't be reported. Default value is empty list, but there is - # no need to include all autogenerated files, we confidently recognize - # autogenerated files. If it's not please let us know. - skip-files: - - easyjson + modules-download-mode: readonly + allow-parallel-runners: true issues: - # Independently from option 'exclude' we use default exclude patterns, - # it can be disabled by this option. To list all - # excluded by default patterns execute 'golangci-lint run --help'. - # Default value for this option is true. - exclude-use-default: false - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - prealloc - - errcheck + max-issues-per-linter: 0 + max-same-issues: 0 + new: false + fix: false -# output configuration options output: - # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" - format: colored-line-number - - # print lines of code with issue, default is true - print-issued-lines: true - - # print linter name in the end of issue text, default is true - print-linter-name: true + formats: + text: + print-linter-name: true + print-issued-lines: true -# all available settings of specific linters -linters-settings: - govet: - # report about shadowed variables - check-shadowing: true +formatters: + exclusions: + paths: + - vendors/ enable: - # report mismatches between assembly files and Go declarations - - asmdecl - # check for useless assignments - - assign - # check for common mistakes using the sync/atomic package - - atomic - # check for non-64-bits-aligned arguments to sync/atomic functions - - atomicalign - # check for common mistakes involving boolean operators - - bools - # check that +build tags are well-formed and correctly located - - buildtag - # detect some violations of the cgo pointer passing rules - - cgocall - # check for unkeyed composite literals - - composites - # check for locks erroneously passed by value - - copylocks - # check for calls of reflect.DeepEqual on error values - - deepequalerrors - # report passing non-pointer or non-error values to errors.As - - errorsas - # find calls to a particular function - - findcall - # report assembly that clobbers the frame pointer before saving it - - framepointer - # check for mistakes using HTTP responses - - httpresponse - # detect impossible interface-to-interface type assertions - - ifaceassert - # check references to loop variables from within nested functions - - loopclosure - # check cancel func returned by context.WithCancel is called - - lostcancel - # check for useless comparisons between functions and nil - - nilfunc - # check for redundant or impossible nil comparisons - - nilness - # check consistency of Printf format strings and arguments - - printf - # check for comparing reflect.Value values with == or reflect.DeepEqual - - reflectvaluecompare - # check for possible unintended shadowing of variables - - shadow - # check for shifts that equal or exceed the width of the integer - - shift - # check for unbuffered channel of os.Signal - - sigchanyzer - # check the argument type of sort.Slice - - sortslice - # check signature of methods of well-known interfaces - - stdmethods - # check for string(int) conversions - - stringintconv - # check that struct field tags conform to reflect.StructTag.Get - - structtag - # report calls to (*testing.T).Fatal from goroutines started by a test. - - testinggoroutine - # check for common mistaken usages of tests and examples - - tests - # report passing non-pointer or non-interface values to unmarshal - - unmarshal - # check for unreachable code - - unreachable - # check for invalid conversions of uintptr to unsafe.Pointer - - unsafeptr - # check for unused results of calls to some functions - - unusedresult - # checks for unused writes - - unusedwrite - disable: - # find structs that would use less memory if their fields were sorted - - fieldalignment - gofmt: - # simplify code: gofmt with '-s' option, true by default - simplify: true - errcheck: - # report about not checking of errors in type assetions: 'a := b.(MyStruct)'; - # default is false: such cases aren't reported by default. - check-type-assertions: true - # report about assignment of errors to blank identifier: 'num, _ := strconv.Atoi(numStr)'; - # default is false: such cases aren't reported by default. - check-blank: true - gocyclo: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 15 - misspell: - # Correct spellings using locale preferences for US or UK. - # Default is to use a neutral variety of English. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. - locale: US - prealloc: - # XXX: we don't recommend using this linter before doing performance profiling. - # For most programs usage of prealloc will be a premature optimization. - # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. - # True by default. - simple: true - range-loops: true # Report preallocation suggestions on range loops, true by default - for-loops: true # Report preallocation suggestions on for loops, false by default - unparam: - # Inspect exported functions, default is false. Set to true if no external program/library imports your code. - # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find external interfaces. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false - gci: - # Section configuration to compare against. - # Section names are case-insensitive and may contain parameters in (). - # The default order of sections is 'standard > default > custom > blank > dot', - # If 'custom-order' is 'true', it follows the order of 'sections' option. - # Default: ["standard", "default"] - #sections: - #- standard # Standard section: captures all standard packages. - #- default # Default section: contains all imports that could not be matched to another section type. - #- blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled. - #- dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled. - # Skip generated files. - # Default: true - skip-generated: true - # Enable custom order of sections. - # If 'true', make the section order the same as the order of 'sections'. - # Default: false - custom-order: false - gosec: - # To select a subset of rules to run. - # Available rules: https://github.com/securego/gosec#available-rules - # Default: [] - means include all rules - includes: - - G101 # Look for hard coded credentials - - G102 # Bind to all interfaces - - G103 # Audit the use of unsafe block - - G104 # Audit errors not checked - - G106 # Audit the use of ssh.InsecureIgnoreHostKey - - G107 # Url provided to HTTP request as taint input - - G108 # Profiling endpoint automatically exposed on /debug/pprof - - G109 # Potential Integer overflow made by strconv.Atoi result conversion to int16/32 - - G110 # Potential DoS vulnerability via decompression bomb - - G111 # Potential directory traversal - - G112 # Potential slowloris attack - - G113 # Usage of Rat.SetString in math/big with an overflow (CVE-2022-23772) - - G114 # Use of net/http serve function that has no support for setting timeouts - - G201 # SQL query construction using format string - - G202 # SQL query construction using string concatenation - - G203 # Use of unescaped data in HTML templates - - G204 # Audit use of command execution - - G301 # Poor file permissions used when creating a directory - - G302 # Poor file permissions used with chmod - - G303 # Creating tempfile using a predictable path - - G304 # File path provided as taint input - - G305 # File traversal when extracting zip/tar archive - - G306 # Poor file permissions used when writing to a new file - - G307 # Deferring a method which returns an error - - G401 # Detect the usage of DES, RC4, MD5 or SHA1 - - G402 # Look for bad TLS connection settings - - G403 # Ensure minimum RSA key length of 2048 bits - - G404 # Insecure random number source (rand) - - G501 # Import blocklist: crypto/md5 - - G502 # Import blocklist: crypto/des - - G503 # Import blocklist: crypto/rc4 - - G504 # Import blocklist: net/http/cgi - - G505 # Import blocklist: crypto/sha1 - - G601 # Implicit memory aliasing of items from a range statement - # To specify a set of rules to explicitly exclude. - # Available rules: https://github.com/securego/gosec#available-rules - # Default: [] - excludes: - - G101 # Look for hard coded credentials - - G102 # Bind to all interfaces - - G103 # Audit the use of unsafe block - - G104 # Audit errors not checked - - G106 # Audit the use of ssh.InsecureIgnoreHostKey - - G107 # Url provided to HTTP request as taint input - - G108 # Profiling endpoint automatically exposed on /debug/pprof - - G109 # Potential Integer overflow made by strconv.Atoi result conversion to int16/32 - - G110 # Potential DoS vulnerability via decompression bomb - - G111 # Potential directory traversal - - G112 # Potential slowloris attack - - G113 # Usage of Rat.SetString in math/big with an overflow (CVE-2022-23772) - - G114 # Use of net/http serve function that has no support for setting timeouts - - G201 # SQL query construction using format string - - G202 # SQL query construction using string concatenation - - G203 # Use of unescaped data in HTML templates - - G204 # Audit use of command execution - - G301 # Poor file permissions used when creating a directory - - G302 # Poor file permissions used with chmod - - G303 # Creating tempfile using a predictable path - - G304 # File path provided as taint input - - G305 # File traversal when extracting zip/tar archive - - G306 # Poor file permissions used when writing to a new file - - G307 # Deferring a method which returns an error - - G401 # Detect the usage of DES, RC4, MD5 or SHA1 - - G402 # Look for bad TLS connection settings - - G403 # Ensure minimum RSA key length of 2048 bits - - G404 # Insecure random number source (rand) - - G501 # Import blocklist: crypto/md5 - - G502 # Import blocklist: crypto/des - - G503 # Import blocklist: crypto/rc4 - - G504 # Import blocklist: net/http/cgi - - G505 # Import blocklist: crypto/sha1 - - G601 # Implicit memory aliasing of items from a range statement - # Exclude generated files - # Default: false - exclude-generated: true - # Filter out the issues with a lower severity than the given value. - # Valid options are: low, medium, high. - # Default: low - severity: medium - # Filter out the issues with a lower confidence than the given value. - # Valid options are: low, medium, high. - # Default: low - confidence: medium - # Concurrency value. - # Default: the number of logical CPUs usable by the current process. - concurrency: 12 - # To specify the configuration of rules. - config: - # Globals are applicable to all rules. - global: - # If true, ignore #nosec in comments (and an alternative as well). - # Default: false - nosec: true - # Add an alternative comment prefix to #nosec (both will work at the same time). - # Default: "" - "#nosec": "#my-custom-nosec" - # Define whether nosec issues are counted as finding or not. - # Default: false - show-ignored: true - # Audit mode enables addition checks that for normal code analysis might be too nosy. - # Default: false - audit: true - G101: - # Regexp pattern for variables and constants to find. - # Default: "(?i)passwd|pass|password|pwd|secret|token|pw|apiKey|bearer|cred" - pattern: "(?i)example" - # If true, complain about all cases (even with low entropy). - # Default: false - ignore_entropy: false - # Maximum allowed entropy of the string. - # Default: "80.0" - entropy_threshold: "80.0" - # Maximum allowed value of entropy/string length. - # Is taken into account if entropy >= entropy_threshold/2. - # Default: "3.0" - per_char_threshold: "3.0" - # Calculate entropy for first N chars of the string. - # Default: "16" - truncate: "32" - # Additional functions to ignore while checking unhandled errors. - # Following functions always ignored: - # bytes.Buffer: - # - Write - # - WriteByte - # - WriteRune - # - WriteString - # fmt: - # - Print - # - Printf - # - Println - # - Fprint - # - Fprintf - # - Fprintln - # strings.Builder: - # - Write - # - WriteByte - # - WriteRune - # - WriteString - # io.PipeWriter: - # - CloseWithError - # hash.Hash: - # - Write - # os: - # - Unsetenv - # Default: {} - G104: - fmt: - - Fscanf - G111: - # Regexp pattern to find potential directory traversal. - # Default: "http\\.Dir\\(\"\\/\"\\)|http\\.Dir\\('\\/'\\)" - pattern: "custom\\.Dir\\(\\)" - # Maximum allowed permissions mode for os.Mkdir and os.MkdirAll - # Default: "0750" - G301: "0750" - # Maximum allowed permissions mode for os.OpenFile and os.Chmod - # Default: "0600" - G302: "0600" - # Maximum allowed permissions mode for os.WriteFile and ioutil.WriteFile - # Default: "0600" - G306: "0600" - - lll: - # Max line length, lines longer will be reported. - # '\t' is counted as 1 character by default, and can be changed with the tab-width option. - # Default: 120. - line-length: 120 - # Tab width in spaces. - # Default: 1 - tab-width: 1 + - gofmt + - goimports linters: - disable-all: true + settings: + staticcheck: + checks: + - all + - -S1023 + - -ST1000 + - -ST1003 + - -SA1019 + - -ST1020 + gosec: + excludes: + - G104 + - G115 + - G301 + - G304 + - G306 + - G501 + - G505 + - G506 + - G507 + exclusions: + paths: + - vendors/ + default: none enable: - govet - - gofmt - errcheck - misspell - gocyclo - ineffassign - - goimports - - nakedret - unparam - unused - prealloc - durationcheck -# - nolintlint -# - staticcheck + - staticcheck - makezero - nilerr - errorlint - bodyclose - - exportloopref - - gci - gosec - - lll - fast: false diff --git a/LICENSE b/LICENSE index da2fb54..93caddc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2024, Mikhail Knyazhev +Copyright (c) 2024-2025, Mikhail Knyazhev Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 index 918d40d..7120e0b --- a/Makefile +++ b/Makefile @@ -1,31 +1,30 @@ +SHELL=/bin/bash + .PHONY: install install: - go install github.com/osspkg/devtool@latest - -.PHONY: setup -setup: - devtool setup-lib + go install go.osspkg.com/goppy/v2/cmd/goppy@latest + goppy setup-lib .PHONY: lint lint: - devtool lint + goppy lint .PHONY: license license: - devtool license + goppy license .PHONY: build build: - devtool build --arch=amd64 + goppy build --arch=amd64 .PHONY: tests tests: - devtool test + goppy test -.PHONY: pre-commite -pre-commite: setup lint build tests +.PHONY: pre-commit +pre-commit: install license lint tests build .PHONY: ci -ci: install setup lint build tests +ci: pre-commit diff --git a/aesgcm/aesgcm.go b/aesgcm/aesgcm.go index add762c..a2ebc1b 100644 --- a/aesgcm/aesgcm.go +++ b/aesgcm/aesgcm.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -41,10 +41,7 @@ func (v *Codec) Encrypt(plaintext []byte) ([]byte, error) { if err != nil { return nil, err } - nonce, err := random.CryptoBytes(gcm.NonceSize()) - if err != nil { - return nil, err - } + nonce := random.CryptoBytes(gcm.NonceSize()) ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) return ciphertext, nil } diff --git a/aesgcm/aesgcm_test.go b/aesgcm/aesgcm_test.go index b05b7c5..0282036 100644 --- a/aesgcm/aesgcm_test.go +++ b/aesgcm/aesgcm_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -10,8 +10,9 @@ import ( "testing" "go.osspkg.com/casecheck" - "go.osspkg.com/encrypt/aesgcm" "go.osspkg.com/random" + + "go.osspkg.com/encrypt/aesgcm" ) func TestUnit_Codec(t *testing.T) { diff --git a/go.mod b/go.mod index 4186f2c..f304650 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ module go.osspkg.com/encrypt -go 1.21 +go 1.24.4 require ( go.osspkg.com/casecheck v0.3.0 go.osspkg.com/errors v0.3.1 - go.osspkg.com/random v0.3.1 - golang.org/x/crypto v0.25.0 + go.osspkg.com/random v0.5.0 + golang.org/x/crypto v0.42.0 ) + +require golang.org/x/sys v0.36.0 // indirect diff --git a/go.sum b/go.sum index 567ba88..a0a5d64 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,9 @@ go.osspkg.com/casecheck v0.3.0 h1:x15blEszElbrHrEH5H02JIIhGIg/lGZzIt1kQlD3pwM= go.osspkg.com/casecheck v0.3.0/go.mod h1:TRFXDMFJEOtnlp3ET2Hix3osbxwPWhvaiT/HfD3+gBA= go.osspkg.com/errors v0.3.1 h1:F9m/EEd/Ot2jba/TV7tvVRIpWXzIpNLc7vRJKcBD86A= go.osspkg.com/errors v0.3.1/go.mod h1:dKXe6Rt07nzY7OyKQNZ8HGBicZ2uQ5TKEoVFnVFOK44= -go.osspkg.com/random v0.3.1 h1:bD3lJUKnLk7DsmF86PsmgDy3K56BFoo5KbxiF4kx20o= -go.osspkg.com/random v0.3.1/go.mod h1:6gWEblgbMt6EMIJG/KPz+YiLzvuxGtHj54TK4BeBzIE= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +go.osspkg.com/random v0.5.0 h1:6x2CQ5Vb6PVyuGi6Ao3K6Pr2fzVviBPCEEJC5HQNSmg= +go.osspkg.com/random v0.5.0/go.mod h1:lsg3FI87PQdjhVWIVo2GXyPBclipljUxjMlWqRl2cck= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/hash/hash.go b/hash/hash.go index 195bd13..ade5453 100644 --- a/hash/hash.go +++ b/hash/hash.go @@ -1,39 +1,98 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ package hash import ( - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" + "encoding/base64" "fmt" + "hash" "io" + "reflect" ) -func SHA1(v string) string { - h := sha1.New() - io.WriteString(h, v) // nolint: errcheck - return fmt.Sprintf("%x", h.Sum(nil)) +type Adapter struct { + H hash.Hash } -func SHA256(v string) string { - h := sha256.New() - io.WriteString(h, v) // nolint: errcheck - return fmt.Sprintf("%x", h.Sum(nil)) +func (a *Adapter) Read(r io.Reader) error { + if a.H == nil { + return fmt.Errorf("hash is nil") + } + if r == nil { + return fmt.Errorf("reader is nil") + } + + _, err := io.Copy(a.H, r) + return err +} + +func (a *Adapter) Write(b []byte) error { + if a.H == nil { + return fmt.Errorf("hash is nil") + } + + _, err := a.H.Write(b) + return err +} + +func (a *Adapter) WriteString(s string) error { + if a.H == nil { + return fmt.Errorf("hash is nil") + } + + _, err := io.WriteString(a.H, s) + return err } -func SHA512(v string) string { - h := sha512.New() - io.WriteString(h, v) // nolint: errcheck - return fmt.Sprintf("%x", h.Sum(nil)) +func (a *Adapter) WriteAny(args ...any) error { + if a.H == nil { + return fmt.Errorf("hash is nil") + } + + for _, arg := range args { + ref := reflect.ValueOf(arg) + if ref.Kind() == reflect.Ptr { + ref = ref.Elem() + } + if _, err := fmt.Fprintf(a.H, "%#v", ref.Interface()); err != nil { + return err + } + } + + return nil } -func MD5(v string) string { - h := md5.New() - io.WriteString(h, v) // nolint: errcheck - return fmt.Sprintf("%x", h.Sum(nil)) +func (a *Adapter) Result() []byte { + if a.H == nil { + return nil + } + + return a.H.Sum(nil) +} + +func (a *Adapter) ResultHex() string { + if a.H == nil { + return "" + } + + return fmt.Sprintf("%x", a.H.Sum(nil)) +} + +func (a *Adapter) ResultBase64() string { + if a.H == nil { + return "" + } + + return base64.StdEncoding.EncodeToString(a.H.Sum(nil)) +} + +func (a *Adapter) Reset() { + if a.H == nil { + return + } + + a.H.Reset() } diff --git a/hash/hash_test.go b/hash/hash_test.go new file mode 100644 index 0000000..2dddbeb --- /dev/null +++ b/hash/hash_test.go @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package hash_test + +import ( + "crypto/md5" + "strings" + "testing" + + "go.osspkg.com/casecheck" + + "go.osspkg.com/encrypt/hash" +) + +type testData struct { + A string +} + +func TestUnit_Adapter(t *testing.T) { + ha := &hash.Adapter{H: md5.New()} + + casecheck.NoError(t, ha.Read(strings.NewReader("123"))) + casecheck.Equal(t, "202cb962ac59075b964b07152d234b70", ha.ResultHex()) + casecheck.Equal(t, "ICy5YqxZB1uWSwcVLSNLcA==", ha.ResultBase64()) + ha.Reset() + + casecheck.NoError(t, ha.Write([]byte("123"))) + casecheck.Equal(t, "202cb962ac59075b964b07152d234b70", ha.ResultHex()) + casecheck.Equal(t, "ICy5YqxZB1uWSwcVLSNLcA==", ha.ResultBase64()) + ha.Reset() + + casecheck.NoError(t, ha.WriteString("123")) + casecheck.Equal(t, "202cb962ac59075b964b07152d234b70", ha.ResultHex()) + casecheck.Equal(t, "ICy5YqxZB1uWSwcVLSNLcA==", ha.ResultBase64()) + ha.Reset() + + casecheck.NoError(t, ha.WriteAny(1, 2, 3)) + casecheck.Equal(t, "202cb962ac59075b964b07152d234b70", ha.ResultHex()) + casecheck.Equal(t, "ICy5YqxZB1uWSwcVLSNLcA==", ha.ResultBase64()) + ha.Reset() + + casecheck.NoError(t, ha.WriteAny(1, 2, 3)) + casecheck.Equal(t, "202cb962ac59075b964b07152d234b70", ha.ResultHex()) + casecheck.Equal(t, "ICy5YqxZB1uWSwcVLSNLcA==", ha.ResultBase64()) + ha.Reset() + + casecheck.NoError(t, ha.WriteAny(&testData{A: "123"})) + casecheck.Equal(t, "5c2f64f6fa624f78f67911662b727e7a", ha.ResultHex()) + casecheck.Equal(t, "XC9k9vpiT3j2eRFmK3J+eg==", ha.ResultBase64()) + ha.Reset() + + casecheck.NoError(t, ha.WriteAny(&testData{A: "123"})) + casecheck.Equal(t, "5c2f64f6fa624f78f67911662b727e7a", ha.ResultHex()) + casecheck.Equal(t, "XC9k9vpiT3j2eRFmK3J+eg==", ha.ResultBase64()) + ha.Reset() +} diff --git a/pgp/pgp.go b/pgp/pgp.go index 44e7706..13d8356 100644 --- a/pgp/pgp.go +++ b/pgp/pgp.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/pgp/pgp_test.go b/pgp/pgp_test.go index 228852f..a9b8d1a 100644 --- a/pgp/pgp_test.go +++ b/pgp/pgp_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/x509/x509.go b/x509/x509.go deleted file mode 100644 index 595cc2a..0000000 --- a/x509/x509.go +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package x509 - -import ( - "bytes" - "crypto/rand" - "crypto/rsa" - cx509 "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "math/big" - "time" - - "go.osspkg.com/errors" -) - -type ( - Cert struct { - Public []byte - Private []byte - } - - Config struct { - Organization string - OrganizationalUnit string - Country string - Province string - Locality string - StreetAddress string - PostalCode string - } -) - -func (v *Config) ToSubject() pkix.Name { - result := pkix.Name{} - - if len(v.Country) > 0 { - result.Country = []string{v.Country} - } - if len(v.Organization) > 0 { - result.Organization = []string{v.Organization} - } - if len(v.OrganizationalUnit) > 0 { - result.OrganizationalUnit = []string{v.OrganizationalUnit} - } - if len(v.Locality) > 0 { - result.Locality = []string{v.Locality} - } - if len(v.Province) > 0 { - result.Province = []string{v.Province} - } - if len(v.StreetAddress) > 0 { - result.StreetAddress = []string{v.StreetAddress} - } - if len(v.PostalCode) > 0 { - result.PostalCode = []string{v.PostalCode} - } - - return result -} - -func generate(c *Config, ttl time.Duration, sn int64, ca *Cert, cn ...string) (*Cert, error) { - crt := &cx509.Certificate{ - SerialNumber: big.NewInt(sn), - Subject: c.ToSubject(), - NotBefore: time.Now(), - NotAfter: time.Now().Add(ttl), - } - - var b []byte - - if ca == nil { - crt.IsCA = true - crt.BasicConstraintsValid = true - crt.MaxPathLenZero = true - crt.MaxPathLen = 0 - crt.KeyUsage = cx509.KeyUsageKeyEncipherment | cx509.KeyUsageDigitalSignature | - cx509.KeyUsageCertSign | cx509.KeyUsageCRLSign - if len(cn) > 0 { - crt.Subject.CommonName = cn[0] - } - } else { - crt.KeyUsage = cx509.KeyUsageKeyEncipherment | cx509.KeyUsageDigitalSignature | cx509.KeyUsageKeyAgreement - crt.ExtKeyUsage = []cx509.ExtKeyUsage{cx509.ExtKeyUsageClientAuth, cx509.ExtKeyUsageServerAuth} - crt.PermittedDNSDomainsCritical = true - for i, s := range cn { - if i == 0 { - crt.Subject.CommonName = cn[0] - } - crt.DNSNames = append(crt.DNSNames, s) - } - } - - pk, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, errors.Wrapf(err, "generate private key") - } - - if ca == nil { - b, err = cx509.CreateCertificate(rand.Reader, crt, crt, &pk.PublicKey, pk) - } else { - block, _ := pem.Decode(ca.Public) - if block == nil { - return nil, errors.New("invalid decode public CA pem ") - } - var caCrt *cx509.Certificate - caCrt, err = cx509.ParseCertificate(block.Bytes) - if err != nil { - return nil, errors.Wrapf(err, "parse CA certificate") - } - - block, _ = pem.Decode(ca.Private) - if block == nil { - return nil, errors.New("invalid decode private CA pem ") - } - var caPK *rsa.PrivateKey - caPK, err = cx509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, errors.Wrapf(err, "decode CA private key") - } - - b, err = cx509.CreateCertificate(rand.Reader, crt, caCrt, &pk.PublicKey, caPK) - } - if err != nil { - return nil, errors.Wrapf(err, "generate certificate") - } - - var pubPEM bytes.Buffer - if err = pem.Encode(&pubPEM, &pem.Block{Type: "CERTIFICATE", Bytes: b}); err != nil { - return nil, errors.Wrapf(err, "encode public pem") - } - - var privPEM bytes.Buffer - if err = pem.Encode(&privPEM, - &pem.Block{Type: "RSA PRIVATE KEY", Bytes: cx509.MarshalPKCS1PrivateKey(pk)}); err != nil { - return nil, errors.Wrapf(err, "encode private pem") - } - - return &Cert{ - Public: pubPEM.Bytes(), - Private: privPEM.Bytes(), - }, nil -} - -func NewCA(c *Config, ttl time.Duration, cn string) (*Cert, error) { - return generate(c, ttl, 1, nil, cn) -} - -func NewCert(c *Config, ttl time.Duration, sn int64, ca *Cert, cn ...string) (*Cert, error) { - return generate(c, ttl, sn, ca, cn...) -} diff --git a/x509/x509_test.go b/x509/x509_test.go deleted file mode 100644 index aa8f65e..0000000 --- a/x509/x509_test.go +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package x509_test - -import ( - "testing" - "time" - - "go.osspkg.com/encrypt/x509" -) - -func TestUnit_X509(t *testing.T) { - conf := &x509.Config{ - Organization: "Demo Inc.", - } - - crt, err := x509.NewCA(conf, time.Hour*24*365*10, "Demo Root R1") - if err != nil { - t.Fatalf(err.Error()) - } - t.Log(string(crt.Private), string(crt.Public)) - - crt, err = x509.NewCert(conf, time.Hour*24*90, 2, crt, "example.com", "*.example.com") - if err != nil { - t.Fatalf(err.Error()) - } - t.Log(string(crt.Private), string(crt.Public)) -} diff --git a/x509cert/cert.go b/x509cert/cert.go new file mode 100644 index 0000000..6799df8 --- /dev/null +++ b/x509cert/cert.go @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package x509cert + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "math/big" + "time" + + "go.osspkg.com/errors" +) + +func NewCA(c Config, ca *Cert, bits int, deadline time.Duration, serialNumber int64, commonName string) (Cert, + error) { + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return Cert{}, errors.Wrapf(err, "generate private key") + } + + template := &x509.Certificate{ + IsCA: true, + BasicConstraintsValid: true, + SignatureAlgorithm: c.SignatureAlgorithm, + SerialNumber: big.NewInt(serialNumber), + Subject: c.ToSubject(), + NotBefore: time.Now(), + NotAfter: time.Now().Add(deadline), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + OCSPServer: c.OCSPServer, + IssuingCertificateURL: c.IssuingCertificateURL, + CRLDistributionPoints: c.CRLDistributionPoints, + } + template.Subject.CommonName = commonName + + var crt []byte + if ca != nil && !ca.IsEmpty() { + template.MaxPathLenZero = false + template.MaxPathLen = ca.Cert.Certificate.MaxPathLen + 1 + + if template.NotAfter.After(ca.Cert.Certificate.NotAfter) { + return Cert{}, errors.New("deadline expires after root certificate expires") + } + + crt, err = x509.CreateCertificate(rand.Reader, template, ca.Cert.Certificate, &key.PublicKey, ca.Key.Key) + } else { + template.MaxPathLenZero = true + template.MaxPathLen = 0 + + crt, err = x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + } + if err != nil { + return Cert{}, errors.Wrapf(err, "create certificate") + } + + rc := &RawCert{} + if err = rc.DecodeDER(crt); err != nil { + return Cert{}, errors.Wrapf(err, "decode certificate") + } + + return Cert{ + Cert: rc, + Key: &RawKey{Key: key}, + }, nil +} + +func NewCert(c Config, ca Cert, bits int, deadline time.Duration, serialNumber int64, commonNames ...string) (Cert, error) { + if ca.IsEmpty() { + return Cert{}, errors.New("CA cert is empty") + } + ok, err := ca.Cert.IsCa() + if err != nil { + return Cert{}, err + } + if !ok { + return Cert{}, errors.New("CA cert is not valid") + } + + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return Cert{}, errors.Wrapf(err, "generate private key") + } + + template := &x509.Certificate{ + IsCA: false, + BasicConstraintsValid: false, + SignatureAlgorithm: c.SignatureAlgorithm, + SerialNumber: big.NewInt(serialNumber), + Subject: c.ToSubject(), + NotBefore: time.Now(), + NotAfter: time.Now().Add(deadline), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + OCSPServer: c.OCSPServer, + IssuingCertificateURL: c.IssuingCertificateURL, + CRLDistributionPoints: c.CRLDistributionPoints, + } + + if template.NotAfter.After(ca.Cert.Certificate.NotAfter) { + return Cert{}, errors.New("deadline expires after root certificate expires") + } + + commonName := "*" + ips, dns, err := splitCommonNames(commonNames) + if err != nil { + return Cert{}, errors.Wrapf(err, "apply common names") + } + if len(dns) > 0 { + commonName = dns[0] + } + template.Subject.CommonName = commonName + template.IPAddresses = ips + template.DNSNames = dns + + crt, err := x509.CreateCertificate(rand.Reader, template, ca.Cert.Certificate, &key.PublicKey, ca.Key.Key) + if err != nil { + return Cert{}, errors.Wrapf(err, "create certificate") + } + + rc := &RawCert{} + if err = rc.DecodeDER(crt); err != nil { + return Cert{}, errors.Wrapf(err, "decode certificate") + } + + return Cert{ + Cert: rc, + Key: &RawKey{Key: key}, + }, nil +} diff --git a/x509cert/cert_test.go b/x509cert/cert_test.go new file mode 100644 index 0000000..deebec6 --- /dev/null +++ b/x509cert/cert_test.go @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package x509cert_test + +import ( + "crypto" + x510 "crypto/x509" + "encoding/hex" + "fmt" + "testing" + "time" + + "go.osspkg.com/casecheck" + "golang.org/x/crypto/ocsp" + + "go.osspkg.com/encrypt/x509cert" +) + +func TestUnit_X509(t *testing.T) { + t.SkipNow() + + fmt.Println(time.Now().Unix(), time.Now().UnixNano()) + + conf := x509cert.Config{ + Organization: "Demo Inc.", + CRLDistributionPoints: []string{"https://crl.demo.com/"}, + OCSPServer: []string{"https://ocsp.demo.com"}, + SignatureAlgorithm: x510.SHA384WithRSA, + } + + ca, err := x509cert.NewCA(conf, nil, 2048, time.Hour*24*365*10, 1, "Root CA") + casecheck.NoError(t, err) + cacpem, err := ca.Cert.EncodePEM() + casecheck.NoError(t, err) + fmt.Println(string(cacpem)) + //cakpem, err := ca.Key.EncodePEM() + //casecheck.NoError(t, err) + //fmt.Println(string(cakpem)) + + ica, err := x509cert.NewCA(conf, &ca, 2048, time.Hour*24*365*5, 1, "Intermediate CA") + casecheck.NoError(t, err) + icacpem, err := ica.Cert.EncodePEM() + casecheck.NoError(t, err) + fmt.Println(string(icacpem)) + + crt, err := x509cert.NewCert(conf, ica, 2048, time.Hour*24*90, time.Now().UnixNano(), "example.com", "*.example.com") + casecheck.NoError(t, err) + crtcpem, err := crt.Cert.EncodePEM() + casecheck.NoError(t, err) + fmt.Println(string(crtcpem)) + //crtkpem, err := crt.Key.EncodePEM() + //casecheck.NoError(t, err) + //fmt.Println(string(crtkpem)) + + algs := []crypto.Hash{ + crypto.MD4, + crypto.MD5, + crypto.SHA1, + crypto.SHA224, + crypto.SHA256, + crypto.SHA384, + crypto.SHA512, + crypto.SHA512_224, + crypto.SHA512_256, + crypto.SHA3_224, + crypto.SHA3_256, + crypto.SHA3_384, + crypto.SHA3_512, + crypto.RIPEMD160, + crypto.BLAKE2s_256, + crypto.BLAKE2b_256, + crypto.BLAKE2b_384, + crypto.BLAKE2b_512, + } + for _, alg := range algs { + fmt.Println(alg.String(), alg.Available()) + + fp, err := crt.Cert.FingerPrint(alg) + casecheck.NoError(t, err) + fmt.Println("FingerPrint", hex.EncodeToString(fp)) + + inh, err := crt.Cert.IssuerNameHash(alg) + casecheck.NoError(t, err) + fmt.Println("IssuerNameHash", hex.EncodeToString(inh)) + + ikh, err := crt.Cert.IssuerKeyHash(alg) + casecheck.NoError(t, err) + fmt.Println("IssuerKeyHash", hex.EncodeToString(ikh)) + } + + req, err := ocsp.CreateRequest(crt.Cert.Certificate, ca.Cert.Certificate, nil) + casecheck.NoError(t, err) + req1, err := ocsp.ParseRequest(req) + casecheck.NoError(t, err) + + fmt.Println(req1.SerialNumber.Int64(), "IssuerNameHash", hex.EncodeToString(req1.IssuerNameHash), + "IssuerKeyHash", hex.EncodeToString(req1.IssuerKeyHash)) + + b, err := ca.Cert.IssuerNameHash(crypto.SHA1) + casecheck.NoError(t, err) + fmt.Println("IssuerNameHash", hex.EncodeToString(b)) + + b, err = ca.Cert.IssuerKeyHash(crypto.SHA1) + casecheck.NoError(t, err) + fmt.Println("IssuerKeyHash", hex.EncodeToString(b)) +} diff --git a/x509cert/common.go b/x509cert/common.go new file mode 100644 index 0000000..ddaf919 --- /dev/null +++ b/x509cert/common.go @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package x509cert + +import ( + _ "crypto/md5" + _ "crypto/sha1" + _ "crypto/sha256" + _ "crypto/sha512" + + _ "golang.org/x/crypto/blake2b" + _ "golang.org/x/crypto/blake2s" + _ "golang.org/x/crypto/md4" + _ "golang.org/x/crypto/ripemd160" + _ "golang.org/x/crypto/sha3" +) diff --git a/x509cert/config.go b/x509cert/config.go new file mode 100644 index 0000000..453ff75 --- /dev/null +++ b/x509cert/config.go @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package x509cert + +import ( + "crypto/x509" + "crypto/x509/pkix" +) + +type Config struct { + Organization string + OrganizationalUnit string + Country string + Province string + Locality string + StreetAddress string + PostalCode string + + OCSPServer []string + IssuingCertificateURL []string + CRLDistributionPoints []string + SignatureAlgorithm x509.SignatureAlgorithm +} + +func (v Config) ToSubject() pkix.Name { + result := pkix.Name{} + + if len(v.Country) > 0 { + result.Country = []string{v.Country} + } + if len(v.Organization) > 0 { + result.Organization = []string{v.Organization} + } + if len(v.OrganizationalUnit) > 0 { + result.OrganizationalUnit = []string{v.OrganizationalUnit} + } + if len(v.Locality) > 0 { + result.Locality = []string{v.Locality} + } + if len(v.Province) > 0 { + result.Province = []string{v.Province} + } + if len(v.StreetAddress) > 0 { + result.StreetAddress = []string{v.StreetAddress} + } + if len(v.PostalCode) > 0 { + result.PostalCode = []string{v.PostalCode} + } + + return result +} diff --git a/x509cert/crl.go b/x509cert/crl.go new file mode 100644 index 0000000..c4f9843 --- /dev/null +++ b/x509cert/crl.go @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package x509cert + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math/big" + "time" +) + +type RevocationEntity struct { + SerialNumber int64 `yaml:"serial_number" json:"serial_number"` + RevocationTime time.Time `yaml:"revocation_time" json:"revocation_time"` +} + +func NewCRL(ca Cert, serialNumber int64, updateInterval time.Duration, revs []RevocationEntity) ([]byte, error) { + list := make([]x509.RevocationListEntry, 0, len(revs)) + for _, rev := range revs { + list = append(list, x509.RevocationListEntry{ + SerialNumber: big.NewInt(rev.SerialNumber), + RevocationTime: rev.RevocationTime, + }) + } + + template := &x509.RevocationList{ + Number: big.NewInt(serialNumber), + Issuer: ca.Cert.Certificate.Subject, + SignatureAlgorithm: ca.Cert.Certificate.SignatureAlgorithm, + ThisUpdate: time.Now(), + NextUpdate: time.Now().Add(updateInterval), + RevokedCertificateEntries: list, + ExtraExtensions: []pkix.Extension{}, + } + + b, err := x509.CreateRevocationList(rand.Reader, template, ca.Cert.Certificate, ca.Key.Key) + if err != nil { + return nil, fmt.Errorf("failed create revocation list: %w", err) + } + + return encodePEM(b, pemTypeRevocationList), nil +} diff --git a/x509cert/models.go b/x509cert/models.go new file mode 100644 index 0000000..23abfe8 --- /dev/null +++ b/x509cert/models.go @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package x509cert + +import ( + "crypto" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "errors" + "os" +) + +var ( + ErrDecodePEMBlock = errors.New("crypto/x509: failed decoding PEM block") + ErrEmptyCertificate = errors.New("certificate is nil") + ErrNotInitedCertificate = errors.New("certificate is not initialized") + ErrEmptyKey = errors.New("key is nil") + ErrNotInitedKey = errors.New("key is not initialized") + ErrHashAlgNotDefined = errors.New("hash algorithm not defined") +) + +type RawCert struct { + Certificate *x509.Certificate +} + +func (v *RawCert) IsCa() (bool, error) { + if v == nil || v.Certificate == nil { + return false, ErrEmptyCertificate + } + + return v.Certificate.IsCA, nil +} + +func (v *RawCert) FingerPrint(h crypto.Hash) ([]byte, error) { + if v == nil || v.Certificate == nil { + return nil, ErrEmptyCertificate + } + + if !h.Available() { + return nil, ErrHashAlgNotDefined + } + + w := h.New() + w.Write(v.Certificate.Raw) + + return w.Sum(nil), nil +} + +func (v *RawCert) IssuerKeyHash(h crypto.Hash) ([]byte, error) { + if v == nil || v.Certificate == nil { + return nil, ErrEmptyCertificate + } + + if !h.Available() { + return nil, ErrHashAlgNotDefined + } + + var publicKeyInfo struct { + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString + } + + if _, err := asn1.Unmarshal(v.Certificate.RawSubjectPublicKeyInfo, &publicKeyInfo); err != nil { + return nil, err + } + + w := h.New() + w.Write(publicKeyInfo.PublicKey.RightAlign()) + + return w.Sum(nil), nil +} + +func (v *RawCert) IssuerNameHash(h crypto.Hash) ([]byte, error) { + if v == nil || v.Certificate == nil { + return nil, ErrEmptyCertificate + } + + if !h.Available() { + return nil, ErrHashAlgNotDefined + } + + w := h.New() + w.Write(v.Certificate.RawSubject) + + return w.Sum(nil), nil +} + +func (v *RawCert) EncodeDER() ([]byte, error) { + if v == nil || v.Certificate == nil { + return nil, ErrEmptyCertificate + } + return v.Certificate.Raw, nil +} + +func (v *RawCert) EncodePEM() ([]byte, error) { + b, err := v.EncodeDER() + if err != nil { + return nil, err + } + return encodePEM(b, pemTypeCertificate), nil +} + +func (v *RawCert) EncodeDERFile(filename string) error { + b, err := v.EncodeDER() + if err != nil { + return err + } + + return os.WriteFile(filename, b, 0644) +} + +func (v *RawCert) EncodePEMFile(filename string) error { + b, err := v.EncodePEM() + if err != nil { + return err + } + + return os.WriteFile(filename, b, 0644) +} + +func (v *RawCert) DecodeDER(b []byte) (err error) { + if v == nil { + return ErrNotInitedCertificate + } + v.Certificate, err = x509.ParseCertificate(b) + return +} + +func (v *RawCert) DecodePEM(b []byte) error { + if v == nil { + return ErrNotInitedCertificate + } + block, _ := pem.Decode(b) + if block == nil || block.Type != string(pemTypeCertificate) { + return ErrDecodePEMBlock + } + return v.DecodeDER(block.Bytes) +} + +func (v *RawCert) DecodeDERFile(filename string) error { + b, err := os.ReadFile(filename) + if err != nil { + return err + } + + return v.DecodeDER(b) +} + +func (v *RawCert) DecodePEMFile(filename string) error { + b, err := os.ReadFile(filename) + if err != nil { + return err + } + + return v.DecodePEM(b) +} + +// ------------------------------------------------------------------------------------------------------------------- + +type RawKey struct { + Key *rsa.PrivateKey +} + +func (v *RawKey) EncodeDER() ([]byte, error) { + if v == nil || v.Key == nil { + return nil, ErrEmptyKey + } + return x509.MarshalPKCS1PrivateKey(v.Key), nil +} + +func (v *RawKey) EncodePEM() ([]byte, error) { + b, err := v.EncodeDER() + if err != nil { + return nil, err + } + return encodePEM(b, pemTypePrivateKey), nil +} + +func (v *RawKey) EncodeDERFile(filename string) error { + b, err := v.EncodeDER() + if err != nil { + return err + } + + return os.WriteFile(filename, b, 0600) +} + +func (v *RawKey) EncodePEMFile(filename string) error { + b, err := v.EncodePEM() + if err != nil { + return err + } + + return os.WriteFile(filename, b, 0600) +} + +func (v *RawKey) DecodeDER(b []byte) (err error) { + if v == nil { + return ErrNotInitedKey + } + v.Key, err = x509.ParsePKCS1PrivateKey(b) + return +} + +func (v *RawKey) DecodePEM(b []byte) error { + block, _ := pem.Decode(b) + if block == nil || block.Type != string(pemTypePrivateKey) { + return ErrDecodePEMBlock + } + return v.DecodeDER(block.Bytes) +} + +func (v *RawKey) DecodeDERFile(filename string) error { + b, err := os.ReadFile(filename) + if err != nil { + return err + } + + return v.DecodeDER(b) +} + +func (v *RawKey) DecodePEMFile(filename string) error { + b, err := os.ReadFile(filename) + if err != nil { + return err + } + + return v.DecodePEM(b) +} + +// ------------------------------------------------------------------------------------------------------------------- + +type RawCRL struct { + b []byte +} + +func (v *RawCRL) EncodeDER() []byte { + if v == nil { + return nil + } + return v.b +} + +func (v *RawCRL) EncodePEM() []byte { + if v == nil { + return nil + } + return encodePEM(v.b, pemTypeRevocationList) +} + +func (v *RawCRL) EncodeDERFile(filename string) error { + if v == nil { + return nil + } + return os.WriteFile(filename, v.EncodeDER(), 0744) +} + +func (v *RawCRL) EncodePEMFile(filename string) error { + if v == nil { + return nil + } + return os.WriteFile(filename, v.EncodePEM(), 0744) +} + +// ------------------------------------------------------------------------------------------------------------------- + +type Cert struct { + Cert *RawCert + Key *RawKey +} + +func (c Cert) IsEmpty() bool { + return c.Cert == nil || c.Cert.Certificate == nil || + c.Key == nil || c.Key.Key == nil +} diff --git a/x509cert/ocsp.go b/x509cert/ocsp.go new file mode 100644 index 0000000..43b7fa1 --- /dev/null +++ b/x509cert/ocsp.go @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package x509cert + +import ( + "io" + "net/http" + "time" + + "golang.org/x/crypto/ocsp" +) + +type OCSPStatusResolver interface { + OCSPStatusResolve(*ocsp.Request) (OCSPStatus, error) +} + +type OCSPStatus int + +const ( + OCSPStatusUnknown OCSPStatus = ocsp.Unknown + OCSPStatusGood OCSPStatus = ocsp.Good + OCSPStatusRevoked OCSPStatus = ocsp.Revoked +) + +type OCSPServer struct { + CA Cert + Resolver OCSPStatusResolver + UpdateInterval time.Duration +} + +func (v *OCSPServer) HTTPHandler(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() //nolint:errcheck + raw, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "invalid request", http.StatusBadRequest) + return + } + + req, err := ocsp.ParseRequest(raw) + if err != nil { + http.Error(w, "invalid ocsp data", http.StatusBadRequest) + return + } + + status, err := v.Resolver.OCSPStatusResolve(req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + response := ocsp.Response{ + Status: int(status), + SerialNumber: req.SerialNumber, + ThisUpdate: time.Now(), + NextUpdate: time.Now().Add(v.UpdateInterval), + ProducedAt: time.Now(), + } + + resp, err := ocsp.CreateResponse(v.CA.Cert.Certificate, v.CA.Cert.Certificate, response, v.CA.Key.Key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/ocsp-response") + w.Write(resp) //nolint:errcheck +} diff --git a/x509cert/utils.go b/x509cert/utils.go new file mode 100644 index 0000000..77e0e2b --- /dev/null +++ b/x509cert/utils.go @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package x509cert + +import ( + "encoding/pem" + "fmt" + "net" +) + +func splitCommonNames(commonNames []string) ([]net.IP, []string, error) { + if len(commonNames) == 0 { + return nil, nil, fmt.Errorf("no common names specified") + } + + ips := make([]net.IP, 0, len(commonNames)) + domains := make([]string, 0, len(commonNames)) + + for _, commonName := range commonNames { + if ip, _, err := net.SplitHostPort(commonName); err == nil { + ips = append(ips, net.ParseIP(ip)) + continue + } + + domains = append(domains, commonName) + } + + return ips, domains, nil +} + +type pemType string + +const ( + pemTypeCertificate pemType = "CERTIFICATE" + pemTypePrivateKey pemType = "RSA PRIVATE KEY" + pemTypeRevocationList pemType = "X509 CRL" +) + +func encodePEM(b []byte, t pemType) []byte { + block := &pem.Block{ + Type: string(t), + Bytes: b, + } + + return pem.EncodeToMemory(block) +} From ce101cb449e0346a7600ac2928b44066738d14eb Mon Sep 17 00:00:00 2001 From: Mikhail Knyazhev Date: Sun, 21 Sep 2025 23:29:40 +0300 Subject: [PATCH 2/2] fix --- pgp/pgp_test.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pgp/pgp_test.go b/pgp/pgp_test.go index a9b8d1a..3c0a73e 100644 --- a/pgp/pgp_test.go +++ b/pgp/pgp_test.go @@ -10,6 +10,7 @@ import ( "crypto" "testing" + "go.osspkg.com/casecheck" "go.osspkg.com/encrypt/pgp" ) @@ -20,21 +21,19 @@ func TestUnit_PGP(t *testing.T) { Comment: "Test Comment", } crt, err := pgp.NewCert(conf, crypto.MD5, 1024, "tool", "dewep utils") - if err != nil { - t.Fatalf(err.Error()) - } + casecheck.NoError(t, err) t.Log(string(crt.Private), string(crt.Public)) in := bytes.NewBufferString("Hello world") out := &bytes.Buffer{} sig := pgp.New() - if err = sig.SetKey(crt.Private, ""); err != nil { - t.Fatalf(err.Error()) - } + err = sig.SetKey(crt.Private, "") + casecheck.NoError(t, err) + sig.SetHash(crypto.MD5, 1024) - if err = sig.Sign(in, out); err != nil { - t.Fatalf(err.Error()) - } + err = sig.Sign(in, out) + casecheck.NoError(t, err) + t.Log(out.String()) }