diff --git a/CHANGELOG b/CHANGELOG index 6fcb788..4fec9d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,21 @@ +Unreleased - 2025-11-15 + +"Error handling modernization" (in progress) + +- Introduced typed, wrapped errors via certlib/certerr.Error (Source, Kind, Op, Err) with Unwrap. +- Standardized helper constructors: DecodeError, ParsingError, VerifyError, LoadingError. +- Preserved sentinel errors (e.g., ErrEncryptedPrivateKey, ErrInvalidPEMType, ErrEmptyCertificate) for errors.Is. +- Refactored certlib to use certerr in key paths (CSR parsing/verification, PEM cert pool, certificate read/load). +- Migrated logging/file.go and cmd/kgz away from github.com/pkg/errors to stdlib wrapping. +- Removed dependency on github.com/pkg/errors; ran go mod tidy. +- Added package docs for certerr and a README section on error handling and matching. +- Added unit tests for certerr (Is/As and message formatting). + +Planned next steps: +- Continue refactoring remaining error paths for consistent wrapping. +- Add focused tests for key flows (encrypted private key, CSR invalid PEM types, etc.). +- Run golangci-lint (errorlint, errcheck) and address findings. + Release 1.2.1 - 2018-09-15 + Add missing format argument to Errorf call in kgz. diff --git a/LICENSE b/LICENSE index b824a4c..4d14584 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,194 @@ -Copyright (c) 2015-2023 Kyle Isom + Copyright 2025 K. Isom -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. + 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 -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + 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. + + 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. ======================================================================= + The backoff package (written during my time at Cloudflare) is released under the following license: diff --git a/README.md b/README.md index 73eec0a..fe830e6 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,43 @@ Each program should have a small README in the directory with more information. All code here is licensed under the ISC license. + + +Error handling +-------------- + +This repo standardizes on Go 1.13+ error wrapping and matching. Libraries and +CLIs should: + +- Wrap causes with context using `fmt.Errorf("context: %w", err)`. +- Use typed, structured errors from `certlib/certerr` for certificate-related + operations. These include a typed `*certerr.Error` with `Source` and `Kind`. +- Match errors programmatically: + - `errors.Is(err, certerr.ErrEncryptedPrivateKey)` to detect sentinel states. + - `errors.As(err, &e)` (where `var e *certerr.Error`) to inspect + `e.Source`/`e.Kind`. + +Examples: + +``` +cert, err := certlib.LoadCertificate(path) +if err != nil { + // sentinel match + if errors.Is(err, certerr.ErrEmptyCertificate) { + // handle empty input + } + + // typed error match + var ce *certerr.Error + if errors.As(err, &ce) { + switch ce.Kind { + case certerr.KindParse: + // parse error handling + case certerr.KindLoad: + // file loading error handling + } + } +} +``` + +Avoid including sensitive data (keys, passwords, tokens) in error messages. diff --git a/certlib/certerr/doc.go b/certlib/certerr/doc.go new file mode 100644 index 0000000..724889b --- /dev/null +++ b/certlib/certerr/doc.go @@ -0,0 +1,33 @@ +// Package certerr provides typed errors and helpers for certificate-related +// operations across the repository. It standardizes error construction and +// matching so callers can reliably branch on error source/kind using the +// Go 1.13+ `errors.Is` and `errors.As` helpers. +// +// Guidelines +// - Always wrap underlying causes using the helper constructors or with +// fmt.Errorf("context: %w", err). +// - Do not include sensitive data (keys, passwords, tokens) in error +// messages; add only non-sensitive, actionable context. +// - Prefer programmatic checks via errors.Is (for sentinel errors) and +// errors.As (to retrieve *certerr.Error) rather than relying on error +// string contents. +// +// Typical usage +// +// if err := doParse(); err != nil { +// return certerr.ParsingError(certerr.ErrorSourceCertificate, err) +// } +// +// Callers may branch on error kinds and sources: +// +// var e *certerr.Error +// if errors.As(err, &e) { +// switch e.Kind { +// case certerr.KindParse: +// // handle parse error +// } +// } +// +// Sentinel errors are provided for common conditions like +// `certerr.ErrEncryptedPrivateKey` and can be matched with `errors.Is`. +package certerr diff --git a/certlib/certerr/errors.go b/certlib/certerr/errors.go index 865f98d..68db92b 100644 --- a/certlib/certerr/errors.go +++ b/certlib/certerr/errors.go @@ -37,6 +37,48 @@ const ( ErrorSourceKeypair ErrorSourceType = 5 ) +// ErrorKind is a broad classification describing what went wrong. +type ErrorKind uint8 + +const ( + KindParse ErrorKind = iota + 1 + KindDecode + KindVerify + KindLoad +) + +func (k ErrorKind) String() string { + switch k { + case KindParse: + return "parse" + case KindDecode: + return "decode" + case KindVerify: + return "verify" + case KindLoad: + return "load" + default: + return "unknown" + } +} + +// Error is a typed, wrapped error with structured context for programmatic checks. +// It implements error and supports errors.Is/As via Unwrap. +type Error struct { + Source ErrorSourceType // which domain produced the error (certificate, private key, etc.) + Kind ErrorKind // operation category (parse, decode, verify, load) + Op string // optional operation or function name + Err error // wrapped cause +} + +func (e *Error) Error() string { + // Keep message format consistent with existing helpers: "failed to : " + // Do not include Op by default to preserve existing output expectations. + return fmt.Sprintf("failed to %s %s: %v", e.Kind.String(), e.Source.String(), e.Err) +} + +func (e *Error) Unwrap() error { return e.Err } + // InvalidPEMType is used to indicate that we were expecting one type of PEM // file, but saw another. type InvalidPEMType struct { @@ -61,19 +103,19 @@ func ErrInvalidPEMType(have string, want ...string) error { } func LoadingError(t ErrorSourceType, err error) error { - return fmt.Errorf("failed to load %s from disk: %w", t, err) + return &Error{Source: t, Kind: KindLoad, Err: err} } func ParsingError(t ErrorSourceType, err error) error { - return fmt.Errorf("failed to parse %s: %w", t, err) + return &Error{Source: t, Kind: KindParse, Err: err} } func DecodeError(t ErrorSourceType, err error) error { - return fmt.Errorf("failed to decode %s: %w", t, err) + return &Error{Source: t, Kind: KindDecode, Err: err} } func VerifyError(t ErrorSourceType, err error) error { - return fmt.Errorf("failed to verify %s: %w", t, err) + return &Error{Source: t, Kind: KindVerify, Err: err} } var ErrEncryptedPrivateKey = errors.New("private key is encrypted") diff --git a/certlib/certerr/errors_test.go b/certlib/certerr/errors_test.go new file mode 100644 index 0000000..dcc52dd --- /dev/null +++ b/certlib/certerr/errors_test.go @@ -0,0 +1,55 @@ +package certerr + +import ( + "errors" + "strings" + "testing" +) + +func TestTypedErrorWrappingAndFormatting(t *testing.T) { + cause := errors.New("bad data") + err := DecodeError(ErrorSourceCertificate, cause) + + // Ensure we can retrieve the typed error + var e *Error + if !errors.As(err, &e) { + t.Fatalf("expected errors.As to retrieve *certerr.Error, got %T", err) + } + if e.Kind != KindDecode { + t.Fatalf("unexpected kind: %v", e.Kind) + } + if e.Source != ErrorSourceCertificate { + t.Fatalf("unexpected source: %v", e.Source) + } + + // Check message format (no trailing punctuation enforced by content) + msg := e.Error() + if !strings.Contains(msg, "failed to decode certificate") || !strings.Contains(msg, "bad data") { + t.Fatalf("unexpected error message: %q", msg) + } +} + +func TestErrorsIsOnWrappedSentinel(t *testing.T) { + err := DecodeError(ErrorSourcePrivateKey, ErrEncryptedPrivateKey) + if !errors.Is(err, ErrEncryptedPrivateKey) { + t.Fatalf("expected errors.Is to match ErrEncryptedPrivateKey") + } +} + +func TestInvalidPEMTypeMessageSingle(t *testing.T) { + err := ErrInvalidPEMType("FOO", "CERTIFICATE") + want := "invalid PEM type: have FOO, expected CERTIFICATE" + if err.Error() != want { + t.Fatalf("unexpected error message: got %q, want %q", err.Error(), want) + } +} + +func TestInvalidPEMTypeMessageMultiple(t *testing.T) { + err := ErrInvalidPEMType("FOO", "CERTIFICATE", "NEW CERTIFICATE REQUEST") + if !strings.Contains( + err.Error(), + "invalid PEM type: have FOO, expected one of CERTIFICATE, NEW CERTIFICATE REQUEST", + ) { + t.Fatalf("unexpected error message: %q", err.Error()) + } +} diff --git a/certlib/certlib.go b/certlib/certlib.go index 27cbe00..cd13a2d 100644 --- a/certlib/certlib.go +++ b/certlib/certlib.go @@ -4,7 +4,7 @@ import ( "crypto/x509" "encoding/pem" "errors" - "io/ioutil" + "os" "git.wntrmute.dev/kyle/goutils/certlib/certerr" ) @@ -13,28 +13,31 @@ import ( // byte slice. func ReadCertificate(in []byte) (cert *x509.Certificate, rest []byte, err error) { if len(in) == 0 { - err = certerr.ErrEmptyCertificate - return cert, rest, err + return nil, nil, certerr.ParsingError(certerr.ErrorSourceCertificate, certerr.ErrEmptyCertificate) } if in[0] == '-' { p, remaining := pem.Decode(in) if p == nil { - err = errors.New("certlib: invalid PEM file") - return cert, rest, err + return nil, nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("invalid PEM file")) } rest = remaining if p.Type != "CERTIFICATE" { - err = certerr.ErrInvalidPEMType(p.Type, "CERTIFICATE") - return cert, rest, err + return nil, rest, certerr.ParsingError( + certerr.ErrorSourceCertificate, + certerr.ErrInvalidPEMType(p.Type, "CERTIFICATE"), + ) } in = p.Bytes } cert, err = x509.ParseCertificate(in) - return cert, rest, err + if err != nil { + return nil, rest, certerr.ParsingError(certerr.ErrorSourceCertificate, err) + } + return cert, rest, nil } // ReadCertificates tries to read all the certificates in a @@ -64,9 +67,9 @@ func ReadCertificates(in []byte) (certs []*x509.Certificate, err error) { // the file contains multiple certificates (e.g. a chain), only the // first certificate is returned. func LoadCertificate(path string) (*x509.Certificate, error) { - in, err := ioutil.ReadFile(path) + in, err := os.ReadFile(path) if err != nil { - return nil, err + return nil, certerr.LoadingError(certerr.ErrorSourceCertificate, err) } cert, _, err := ReadCertificate(in) @@ -76,9 +79,9 @@ func LoadCertificate(path string) (*x509.Certificate, error) { // LoadCertificates tries to read all the certificates in a file, // returning them in the order that it found them in the file. func LoadCertificates(path string) ([]*x509.Certificate, error) { - in, err := ioutil.ReadFile(path) + in, err := os.ReadFile(path) if err != nil { - return nil, err + return nil, certerr.LoadingError(certerr.ErrorSourceCertificate, err) } return ReadCertificates(in) diff --git a/certlib/helpers.go b/certlib/helpers.go index d38e1df..e7324b6 100644 --- a/certlib/helpers.go +++ b/certlib/helpers.go @@ -91,16 +91,20 @@ var Apr2015 = InclusiveDate(2015, time.April, 01) // KeyLength returns the bit size of ECDSA or RSA PublicKey. func KeyLength(key any) int { - if key == nil { + switch k := key.(type) { + case *ecdsa.PublicKey: + if k == nil { + return 0 + } + return k.Curve.Params().BitSize + case *rsa.PublicKey: + if k == nil { + return 0 + } + return k.N.BitLen() + default: return 0 } - if ecdsaKey, ok := key.(*ecdsa.PublicKey); ok { - return ecdsaKey.Curve.Params().BitSize - } else if rsaKey, ok := key.(*rsa.PublicKey); ok { - return rsaKey.N.BitLen() - } - - return 0 } // ExpiryTime returns the time when the certificate chain is expired. @@ -144,91 +148,79 @@ func ValidExpiry(c *x509.Certificate) bool { maxMonths = 39 case issued.After(Jul2012): maxMonths = 60 - case issued.Before(Jul2012): + default: maxMonths = 120 } - if MonthsValid(c) > maxMonths { - return false - } - return true + return MonthsValid(c) <= maxMonths +} + +// SignatureString returns the TLS signature string corresponding to +// an X509 signature algorithm. +var signatureString = map[x509.SignatureAlgorithm]string{ + x509.MD2WithRSA: "MD2WithRSA", + x509.MD5WithRSA: "MD5WithRSA", + x509.SHA1WithRSA: "SHA1WithRSA", + x509.SHA256WithRSA: "SHA256WithRSA", + x509.SHA384WithRSA: "SHA384WithRSA", + x509.SHA512WithRSA: "SHA512WithRSA", + x509.DSAWithSHA1: "DSAWithSHA1", + x509.DSAWithSHA256: "DSAWithSHA256", + x509.ECDSAWithSHA1: "ECDSAWithSHA1", + x509.ECDSAWithSHA256: "ECDSAWithSHA256", + x509.ECDSAWithSHA384: "ECDSAWithSHA384", + x509.ECDSAWithSHA512: "ECDSAWithSHA512", } // SignatureString returns the TLS signature string corresponding to // an X509 signature algorithm. func SignatureString(alg x509.SignatureAlgorithm) string { - switch alg { - case x509.MD2WithRSA: - return "MD2WithRSA" - case x509.MD5WithRSA: - return "MD5WithRSA" - case x509.SHA1WithRSA: - return "SHA1WithRSA" - case x509.SHA256WithRSA: - return "SHA256WithRSA" - case x509.SHA384WithRSA: - return "SHA384WithRSA" - case x509.SHA512WithRSA: - return "SHA512WithRSA" - case x509.DSAWithSHA1: - return "DSAWithSHA1" - case x509.DSAWithSHA256: - return "DSAWithSHA256" - case x509.ECDSAWithSHA1: - return "ECDSAWithSHA1" - case x509.ECDSAWithSHA256: - return "ECDSAWithSHA256" - case x509.ECDSAWithSHA384: - return "ECDSAWithSHA384" - case x509.ECDSAWithSHA512: - return "ECDSAWithSHA512" - default: - return "Unknown Signature" + if s, ok := signatureString[alg]; ok { + return s } + return "Unknown Signature" +} + +// HashAlgoString returns the hash algorithm name contains in the signature +// method. +var hashAlgoString = map[x509.SignatureAlgorithm]string{ + x509.MD2WithRSA: "MD2", + x509.MD5WithRSA: "MD5", + x509.SHA1WithRSA: "SHA1", + x509.SHA256WithRSA: "SHA256", + x509.SHA384WithRSA: "SHA384", + x509.SHA512WithRSA: "SHA512", + x509.DSAWithSHA1: "SHA1", + x509.DSAWithSHA256: "SHA256", + x509.ECDSAWithSHA1: "SHA1", + x509.ECDSAWithSHA256: "SHA256", + x509.ECDSAWithSHA384: "SHA384", + x509.ECDSAWithSHA512: "SHA512", } // HashAlgoString returns the hash algorithm name contains in the signature // method. func HashAlgoString(alg x509.SignatureAlgorithm) string { - switch alg { - case x509.MD2WithRSA: - return "MD2" - case x509.MD5WithRSA: - return "MD5" - case x509.SHA1WithRSA: - return "SHA1" - case x509.SHA256WithRSA: - return "SHA256" - case x509.SHA384WithRSA: - return "SHA384" - case x509.SHA512WithRSA: - return "SHA512" - case x509.DSAWithSHA1: - return "SHA1" - case x509.DSAWithSHA256: - return "SHA256" - case x509.ECDSAWithSHA1: - return "SHA1" - case x509.ECDSAWithSHA256: - return "SHA256" - case x509.ECDSAWithSHA384: - return "SHA384" - case x509.ECDSAWithSHA512: - return "SHA512" - default: - return "Unknown Hash Algorithm" + if s, ok := hashAlgoString[alg]; ok { + return s } + return "Unknown Hash Algorithm" } // StringTLSVersion returns underlying enum values from human names for TLS // versions, defaults to current golang default of TLS 1.0. func StringTLSVersion(version string) uint16 { switch version { + case "1.3": + return tls.VersionTLS13 case "1.2": return tls.VersionTLS12 case "1.1": return tls.VersionTLS11 + case "1.0": + return tls.VersionTLS10 default: + // Default to Go's historical default of TLS 1.0 for unknown values return tls.VersionTLS10 } } @@ -283,33 +275,40 @@ func ParseCertificatesPEM(certsPEM []byte) ([]*x509.Certificate, error) { // either PKCS #7, PKCS #12, or raw x509. func ParseCertificatesDER(certsDER []byte, password string) (certs []*x509.Certificate, key crypto.Signer, err error) { certsDER = bytes.TrimSpace(certsDER) - pkcs7data, err := pkcs7.ParsePKCS7(certsDER) - if err != nil { - var pkcs12data any - var ok bool - certs = make([]*x509.Certificate, 1) - pkcs12data, certs[0], err = pkcs12.Decode(certsDER, password) - if err != nil { - certs, err = x509.ParseCertificates(certsDER) - if err != nil { - return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, err) - } - } else { - key, ok = pkcs12data.(crypto.Signer) - if !ok { - return nil, nil, certerr.DecodeError(certerr.ErrorSourcePrivateKey, errors.New("PKCS12 data does not contain a private key")) - } - } - } else { + + // First, try PKCS #7 + if pkcs7data, err7 := pkcs7.ParsePKCS7(certsDER); err7 == nil { if pkcs7data.ContentInfo != "SignedData" { - return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("can only extract certificates from signed data content info")) + return nil, nil, certerr.DecodeError( + certerr.ErrorSourceCertificate, + errors.New("can only extract certificates from signed data content info"), + ) } certs = pkcs7data.Content.SignedData.Certificates + if certs == nil { + return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificates decoded")) + } + return certs, nil, nil } - if certs == nil { - return nil, key, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificates decoded")) + + // Next, try PKCS #12 + if pkcs12data, cert, err12 := pkcs12.Decode(certsDER, password); err12 == nil { + signer, ok := pkcs12data.(crypto.Signer) + if !ok { + return nil, nil, certerr.DecodeError( + certerr.ErrorSourcePrivateKey, + errors.New("PKCS12 data does not contain a private key"), + ) + } + return []*x509.Certificate{cert}, signer, nil } - return certs, key, nil + + // Finally, attempt to parse raw X.509 certificates + certs, err = x509.ParseCertificates(certsDER) + if err != nil { + return nil, nil, certerr.DecodeError(certerr.ErrorSourceCertificate, err) + } + return certs, nil, nil } // ParseSelfSignedCertificatePEM parses a PEM-encoded certificate and check if it is self-signed. @@ -329,17 +328,26 @@ func ParseSelfSignedCertificatePEM(certPEM []byte) (*x509.Certificate, error) { // can handle PEM encoded PKCS #7 structures. func ParseCertificatePEM(certPEM []byte) (*x509.Certificate, error) { certPEM = bytes.TrimSpace(certPEM) - cert, rest, err := ParseOneCertificateFromPEM(certPEM) + certs, rest, err := ParseOneCertificateFromPEM(certPEM) if err != nil { return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, err) - } else if cert == nil { - return nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificate decoded")) - } else if len(rest) > 0 { - return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("the PEM file should contain only one object")) - } else if len(cert) > 1 { - return nil, certerr.ParsingError(certerr.ErrorSourceCertificate, errors.New("the PKCS7 object in the PEM file should contain only one certificate")) } - return cert[0], nil + if certs == nil { + return nil, certerr.DecodeError(certerr.ErrorSourceCertificate, errors.New("no certificate decoded")) + } + if len(rest) > 0 { + return nil, certerr.ParsingError( + certerr.ErrorSourceCertificate, + errors.New("the PEM file should contain only one object"), + ) + } + if len(certs) > 1 { + return nil, certerr.ParsingError( + certerr.ErrorSourceCertificate, + errors.New("the PKCS7 object in the PEM file should contain only one certificate"), + ) + } + return certs[0], nil } // ParseOneCertificateFromPEM attempts to parse one PEM encoded certificate object, @@ -392,7 +400,7 @@ func PEMToCertPool(pemCerts []byte) (*x509.CertPool, error) { certPool := x509.NewCertPool() if !certPool.AppendCertsFromPEM(pemCerts) { - return nil, errors.New("failed to load cert pool") + return nil, certerr.LoadingError(certerr.ErrorSourceCertificate, errors.New("failed to load cert pool")) } return certPool, nil @@ -461,12 +469,12 @@ func ParseCSR(in []byte) (csr *x509.CertificateRequest, rest []byte, err error) } if err != nil { - return nil, rest, err + return nil, rest, certerr.ParsingError(certerr.ErrorSourceCSR, err) } err = csr.CheckSignature() if err != nil { - return nil, rest, err + return nil, rest, certerr.VerifyError(certerr.ErrorSourceCSR, err) } return csr, rest, nil @@ -483,7 +491,7 @@ func ParseCSRPEM(csrPEM []byte) (*x509.CertificateRequest, error) { csrObject, err := x509.ParseCertificateRequest(block.Bytes) if err != nil { - return nil, err + return nil, certerr.ParsingError(certerr.ErrorSourceCSR, err) } return csrObject, nil @@ -628,20 +636,16 @@ func SCTListFromOCSPResponse(response *ocsp.Response) ([]ct.SignedCertificateTim // the subsequent file. If no prefix is provided, valFile is assumed to be a // file path. func ReadBytes(valFile string) ([]byte, error) { - switch splitVal := strings.SplitN(valFile, ":", 2); len(splitVal) { - case 1: + prefix, rest, found := strings.Cut(valFile, ":") + if !found { return os.ReadFile(valFile) - case 2: - switch splitVal[0] { - case "env": - return []byte(os.Getenv(splitVal[1])), nil - case "file": - return os.ReadFile(splitVal[1]) - default: - return nil, fmt.Errorf("unknown prefix: %s", splitVal[0]) - } + } + switch prefix { + case "env": + return []byte(os.Getenv(rest)), nil + case "file": + return os.ReadFile(rest) default: - return nil, fmt.Errorf("multiple prefixes: %s", - strings.Join(splitVal[:len(splitVal)-1], ", ")) + return nil, fmt.Errorf("unknown prefix: %s", prefix) } } diff --git a/cmd/kgz/main.go b/cmd/kgz/main.go index 06b7eea..c7e915b 100644 --- a/cmd/kgz/main.go +++ b/cmd/kgz/main.go @@ -1,70 +1,68 @@ package main import ( - "compress/flate" - "compress/gzip" - "flag" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" + "compress/flate" + "compress/gzip" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "strings" ) const gzipExt = ".gz" func compress(path, target string, level int) error { - sourceFile, err := os.Open(path) - if err != nil { - return errors.Wrap(err, "opening file for read") - } + sourceFile, err := os.Open(path) + if err != nil { + return fmt.Errorf("opening file for read: %w", err) + } defer sourceFile.Close() - destFile, err := os.Create(target) - if err != nil { - return errors.Wrap(err, "opening file for write") - } + destFile, err := os.Create(target) + if err != nil { + return fmt.Errorf("opening file for write: %w", err) + } defer destFile.Close() - gzipCompressor, err := gzip.NewWriterLevel(destFile, level) - if err != nil { - return errors.Wrap(err, "invalid compression level") - } + gzipCompressor, err := gzip.NewWriterLevel(destFile, level) + if err != nil { + return fmt.Errorf("invalid compression level: %w", err) + } defer gzipCompressor.Close() - _, err = io.Copy(gzipCompressor, sourceFile) - if err != nil { - return errors.Wrap(err, "compressing file") - } + _, err = io.Copy(gzipCompressor, sourceFile) + if err != nil { + return fmt.Errorf("compressing file: %w", err) + } return nil } func uncompress(path, target string) error { - sourceFile, err := os.Open(path) - if err != nil { - return errors.Wrap(err, "opening file for read") - } + sourceFile, err := os.Open(path) + if err != nil { + return fmt.Errorf("opening file for read: %w", err) + } defer sourceFile.Close() - gzipUncompressor, err := gzip.NewReader(sourceFile) - if err != nil { - return errors.Wrap(err, "reading gzip headers") - } + gzipUncompressor, err := gzip.NewReader(sourceFile) + if err != nil { + return fmt.Errorf("reading gzip headers: %w", err) + } defer gzipUncompressor.Close() - destFile, err := os.Create(target) - if err != nil { - return errors.Wrap(err, "opening file for write") - } + destFile, err := os.Create(target) + if err != nil { + return fmt.Errorf("opening file for write: %w", err) + } defer destFile.Close() - _, err = io.Copy(destFile, gzipUncompressor) - if err != nil { - return errors.Wrap(err, "uncompressing file") - } + _, err = io.Copy(destFile, gzipUncompressor) + if err != nil { + return fmt.Errorf("uncompressing file: %w", err) + } return nil } @@ -108,9 +106,9 @@ func pathForUncompressing(source, dest string) (string, error) { } source = filepath.Base(source) - if !strings.HasSuffix(source, gzipExt) { - return "", errors.Errorf("%s is a not gzip-compressed file", source) - } + if !strings.HasSuffix(source, gzipExt) { + return "", fmt.Errorf("%s is a not gzip-compressed file", source) + } outFile := source[:len(source)-len(gzipExt)] outFile = filepath.Join(dest, outFile) return outFile, nil @@ -122,9 +120,9 @@ func pathForCompressing(source, dest string) (string, error) { } source = filepath.Base(source) - if strings.HasSuffix(source, gzipExt) { - return "", errors.Errorf("%s is a gzip-compressed file", source) - } + if strings.HasSuffix(source, gzipExt) { + return "", fmt.Errorf("%s is a gzip-compressed file", source) + } dest = filepath.Join(dest, source+gzipExt) return dest, nil diff --git a/go.mod b/go.mod index fa9890a..8c39d09 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.24.0 require ( github.com/hashicorp/go-syslog v1.0.0 github.com/kr/text v0.2.0 - github.com/pkg/errors v0.9.1 github.com/pkg/sftp v1.12.0 golang.org/x/crypto v0.44.0 golang.org/x/sys v0.38.0 @@ -20,5 +19,6 @@ require ( require ( github.com/kr/fs v0.1.0 // indirect github.com/kr/pretty v0.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index 422c06f..49f16ce 100644 --- a/go.sum +++ b/go.sum @@ -25,19 +25,15 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b h1:Qwe1rC8PSniVfAFPFJeyUkB+zcysC3RgJBAGk7eqBEU= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/logging/file.go b/logging/file.go index 56023f5..a65f18b 100644 --- a/logging/file.go +++ b/logging/file.go @@ -1,9 +1,8 @@ package logging import ( - "os" - - "github.com/pkg/errors" + "fmt" + "os" ) // File writes its logs to file. @@ -60,12 +59,12 @@ func NewSplitFile(outpath, errpath string, overwrite bool) (*File, error) { fl.fe, err = os.OpenFile(errpath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) } - if err != nil { - if closeErr := fl.Close(); closeErr != nil { - return nil, errors.Wrap(err, closeErr.Error()) - } - return nil, err - } + if err != nil { + if closeErr := fl.Close(); closeErr != nil { + return nil, fmt.Errorf("failed to open error log: cleanup close failed: %v: %w", closeErr, err) + } + return nil, err + } fl.LogWriter = NewLogWriter(fl.fo, fl.fe) return fl, nil @@ -95,13 +94,13 @@ func (fl *File) Flush() error { } func (fl *File) Chmod(mode os.FileMode) error { - if err := fl.fo.Chmod(mode); err != nil { - return errors.WithMessage(err, "failed to chmod output log") - } + if err := fl.fo.Chmod(mode); err != nil { + return fmt.Errorf("failed to chmod output log: %w", err) + } - if err := fl.fe.Chmod(mode); err != nil { - return errors.WithMessage(err, "failed to chmod error log") - } + if err := fl.fe.Chmod(mode); err != nil { + return fmt.Errorf("failed to chmod error log: %w", err) + } - return nil + return nil }