diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 35fd563..ddcd68f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -22,7 +22,7 @@ jobs: uses: golangci/golangci-lint-action@v2 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.43.0 + version: v1.49.0 # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/.golangci.yaml b/.golangci.yaml index 2d16611..f9bceda 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -183,6 +183,18 @@ linters-settings: - '*.Test' - '*.Test2' + exhaustruct: + # List of regular expressions to match struct packages and names. + # If this list is empty, all structs are tested. + # Default: [] + include: + - '.*\.Test' + - '.*\.Test2' + # List of regular expressions to exclude struct packages and names from check. + # Default: [] + # exclude: + # - 'cobra\.Command$' + forbidigo: # Forbid the following identifiers (identifiers are written using regexp): forbid: @@ -805,7 +817,7 @@ linters: - asciicheck - bodyclose - cyclop - - deadcode + #- deadcode # deprecated, replaced by unused - depguard - dogsled - dupl @@ -813,7 +825,8 @@ linters: - errcheck - errorlint - exhaustive - - exhaustivestruct + - exhaustruct + #- exhaustivestruct # deprecated, replaced by exhaustruct. - exportloopref - forbidigo - funlen @@ -833,7 +846,7 @@ linters: - gosec - gosimple - govet - - ifshort + #- ifshort # 'ifshort' is deprecated - importas - ineffassign - lll @@ -852,14 +865,14 @@ linters: - rowserrcheck - sqlclosecheck - staticcheck - - structcheck + #- structcheck # deprecated, replaced by unused - stylecheck - thelper - tparallel - unconvert - unparam - unused - - varcheck + #- varcheck # deprecated, replaced by unused - wastedassign - whitespace - bidichk diff --git a/cliutil/cli.go b/cliutil/cli.go index c0a001c..0c233d1 100644 --- a/cliutil/cli.go +++ b/cliutil/cli.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "errors" "io" "os" "os/exec" @@ -108,7 +109,7 @@ func readBuf(r *bufio.Reader, fn LoggingFunc) error { if err != nil { return err } - } else if err == io.EOF { + } else if errors.Is(err, io.EOF) { break } else { return err diff --git a/crtutil/crt.go b/crtutil/crt.go index 95315e6..ec0bd66 100644 --- a/crtutil/crt.go +++ b/crtutil/crt.go @@ -9,87 +9,25 @@ import ( ) var ( - ErrNoPEMData = errors.New("no pem data is found") ErrUnknownKeyType = errors.New("unknown private key type in PKCS#8 wrapping") ) -// ParseCertFile parses x509.Certificate from the given file. -// The data is expected to be PEM Encoded and contain one certificate +// ReadAsX509FromFile read x509.Certificate from the given file. +// The data is expected to be PEM Encoded and contain one or more certificates // with PEM type "CERTIFICATE". -// Deprecated: use ReadFileAsX509 instead. -func ParseCertFile(fpath string) (*x509.Certificate, error) { - return ReadFileAsX509(fpath) -} - -// ReadFileAsX509 read x509.Certificate from the given file. -// The data is expected to be PEM Encoded and contain one certificate -// with PEM type "CERTIFICATE". -func ReadFileAsX509(fpath string) (*x509.Certificate, error) { - bs, err := ioutil.ReadFile(fpath) - if err != nil { - return nil, err - } - - return ReadBytesAsX509(bs) -} - -// ParseCertBytes parses a single x509.Certificate from the given data. -// The data is expected to be PEM Encoded and contain one certificate -// with PEM type "CERTIFICATE". -// Deprecated: use ReadBytesAsX509 instead. -func ParseCertBytes(data []byte) (*x509.Certificate, error) { - return ReadBytesAsX509(data) -} - -// ReadBytesAsX509 read x509.Certificate from the given data. -// The data is expected to be PEM Encoded and contain one certificate -// with PEM type "CERTIFICATE". -func ReadBytesAsX509(data []byte) (*x509.Certificate, error) { - if len(data) == 0 { - return nil, nil - } - bl, _ := pem.Decode(data) - if bl == nil { - return nil, ErrNoPEMData - } - cert, err := x509.ParseCertificate(bl.Bytes) - if err != nil { - return nil, err - } - return cert, nil -} - -// ParseCertChainFile parses the x509.Certificate chain from the given file. -// The data is expected to be PEM Encoded and contain one of more certificates -// with PEM type "CERTIFICATE". -// Deprecated: use ReadChainFileAsX509 instead. -func ParseCertChainFile(fpath string) ([]*x509.Certificate, error) { - return ReadChainFileAsX509(fpath) -} - -// ReadChainFileAsX509 read the x509.Certificate chain from the given file. -// The data is expected to be PEM Encoded and contain one of more certificates -// with PEM type "CERTIFICATE". -func ReadChainFileAsX509(fpath string) ([]*x509.Certificate, error) { +func ReadAsX509FromFile(fpath string) ([]*x509.Certificate, error) { bs, err := ioutil.ReadFile(fpath) if err != nil { return nil, err } - return ReadChainBytesAsX509(bs) -} -// ParseCertChainBytes parses x509.Certificate chain from the given data. -// The data is expected to be PEM Encoded and contain one of more certificates -// with PEM type "CERTIFICATE". -// Deprecated: use ReadChainBytesAsX509 instead. -func ParseCertChainBytes(data []byte) ([]*x509.Certificate, error) { - return ReadChainBytesAsX509(data) + return ReadAsX509(bs) } -// ReadChainBytesAsX509 read x509.Certificate chain from the given data. +// ReadAsX509 read x509.Certificate chain from the given data. // The data is expected to be PEM Encoded and contain one of more certificates // with PEM type "CERTIFICATE". -func ReadChainBytesAsX509(data []byte) ([]*x509.Certificate, error) { +func ReadAsX509(data []byte) ([]*x509.Certificate, error) { var ( certs []*x509.Certificate cert *x509.Certificate @@ -111,19 +49,9 @@ func ReadChainBytesAsX509(data []byte) ([]*x509.Certificate, error) { certs = append(certs, cert) } - if len(certs) == 0 { - return nil, ErrNoPEMData - } - return certs, nil } -// CertToPEM converts a x509.Certificate into a PEM block. -// Deprecated: use EncodeX509ToPEM instead. -func CertToPEM(cert *x509.Certificate) []byte { - return EncodeX509ToPEM(cert, nil) -} - // EncodeX509ToPEM converts a x509.Certificate into a PEM block. func EncodeX509ToPEM(cert *x509.Certificate, headers map[string]string) []byte { return pem.EncodeToMemory(&pem.Block{ @@ -133,12 +61,6 @@ func EncodeX509ToPEM(cert *x509.Certificate, headers map[string]string) []byte { }) } -// CertChainToPEM converts a slice of x509.Certificate into PEM block, in the order they are passed. -// Deprecated: use EncodeX509ChainToPEM instead. -func CertChainToPEM(chain []*x509.Certificate) ([]byte, error) { - return EncodeX509ChainToPEM(chain, nil) -} - // EncodeX509ChainToPEM converts a slice of x509.Certificate into PEM block, in the order they are passed. func EncodeX509ChainToPEM(chain []*x509.Certificate, headers map[string]string) ([]byte, error) { var buf bytes.Buffer @@ -156,3 +78,8 @@ func EncodeX509ChainToPEM(chain []*x509.Certificate, headers map[string]string) } return buf.Bytes(), nil } + +// IsSelfSigned whether the given x509.Certificate is self-signed. +func IsSelfSigned(cert *x509.Certificate) bool { + return cert.CheckSignatureFrom(cert) == nil +} diff --git a/crtutil/crt_test.go b/crtutil/crt_test.go index 076b5b8..4b34ccd 100644 --- a/crtutil/crt_test.go +++ b/crtutil/crt_test.go @@ -1,65 +1,91 @@ package crtutil import ( - "bytes" - "crypto/x509" - "fmt" - "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestParseCrtFile(t *testing.T) { - _, err := ReadFileAsX509("testdata/server.crt") - assert.NoError(t, err) - // printCrt(t, crt, "server") +func TestReadAsX509FromFile(t *testing.T) { + tests := []struct { + title string + input string + expected int + shoulderr bool + }{ + { + "read server certificate", + "testdata/server.crt", + 1, + false, + }, + { + "returns error while reading a non-existent file", + "testdata/server-non-existent.crt", + 0, + true, + }, + { + "returns 2 CA certificates", + "testdata/server-ca.crt", + 2, + false, + }, + { + "returns 3 certificates", + "testdata/server-3layers.crt", + 3, + false, + }, + { + "returns 3 certificates and ignore redundant characters", + "testdata/server-3layers-withcharacters.crt", + 3, + false, + }, + } + for _, v := range tests { + t.Run(v.title, func(t *testing.T) { + certs, err := ReadAsX509FromFile(v.input) + if v.shoulderr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, v.expected, len(certs)) + } + }) + } - t.Run("parse error with empty data", func(t *testing.T) { - _, err = ReadFileAsX509("testdata/server-fail.crt") - assert.Error(t, err) - }) } func TestParseCertBytes(t *testing.T) { - t.Run("empty data", func(t *testing.T) { - _, err := ReadBytesAsX509([]byte{}) - assert.NoError(t, err) - }) - t.Run("ErrNoPEMData", func(t *testing.T) { - _, err := ReadBytesAsX509([]byte("sdfklhjasdfkjhasdfkjlhas")) - assert.ErrorIs(t, err, ErrNoPEMData) - }) -} - -func TestParseCrtSetFile(t *testing.T) { - crts, err := ReadChainFileAsX509("testdata/server-ca.crt") - assert.NoError(t, err) - assert.Equal(t, 2, len(crts)) - - crts, err = ReadChainFileAsX509("testdata/server-3layers.crt") - assert.NoError(t, err) - assert.Equal(t, 3, len(crts)) - - crts, err = ReadChainFileAsX509("testdata/server-3layers-withcharacters.crt") - assert.NoError(t, err) - assert.Equal(t, 3, len(crts)) - - t.Run("parse error with empty data", func(t *testing.T) { - _, err = ReadChainFileAsX509("testdata/server-fail.crt") - assert.Error(t, err) - }) -} - -func TestParseCertChainBytes(t *testing.T) { - t.Run("ErrNoPEMData", func(t *testing.T) { - _, err := ReadChainBytesAsX509([]byte("sdfklhjasdfkjhasdfkjlhas")) - assert.ErrorIs(t, err, ErrNoPEMData) - }) + tests := []struct { + title string + input []byte + expected int + }{ + { + "empty data, should got 0 certificates", + []byte{}, + 0, + }, + { + "string data, should got 0 certificates", + []byte("sdfklhjasdfkjhasdfkjlhas"), + 0, + }, + } + for _, v := range tests { + t.Run(v.title, func(t *testing.T) { + certs, err := ReadAsX509(v.input) + assert.NoError(t, err) + assert.Equal(t, v.expected, len(certs)) + }) + } } -func TestCertChainToPEM(t *testing.T) { - crts, err := ReadChainFileAsX509("testdata/server-3layers-withcharacters.crt") +func TestEncodeX509ChainToPEM(t *testing.T) { + crts, err := ReadAsX509FromFile("testdata/server-3layers-withcharacters.crt") assert.NoError(t, err) assert.Equal(t, 3, len(crts)) got, err := EncodeX509ChainToPEM(crts, nil) @@ -67,34 +93,39 @@ func TestCertChainToPEM(t *testing.T) { assert.NotEmpty(t, got) } -func TestCertToPEM(t *testing.T) { - crt, err := ReadFileAsX509("testdata/server.crt") +func TestEncodeX509ToPEM(t *testing.T) { + crt, err := ReadAsX509FromFile("testdata/server.crt") assert.NoError(t, err) - got := EncodeX509ToPEM(crt, nil) + got := EncodeX509ToPEM(crt[0], nil) assert.NotEmpty(t, got) } -func printCrt(t *testing.T, cert *x509.Certificate, name string) { - t.Log("") - t.Logf("%s Certificate Information:", name) - t.Logf(" Issuer: %s", cert.Issuer) - t.Logf(" NotBefore: %s", cert.NotBefore.String()) - t.Logf(" NotAfter: %s", cert.NotAfter.String()) - t.Logf(" Subject: %s", cert.Subject) +func TestIsSelfSigned(t *testing.T) { + tests := []struct { + title string + input string + expected bool + }{ + { + "should not be self-signed", + "testdata/server.crt", + false, + }, + { + "should be self-signed", + "testdata/self-signed.crt", + true, + }, + } - dnsStr := strings.Join(cert.DNSNames, ",") - ipBuf := new(bytes.Buffer) + for _, v := range tests { + t.Run(v.title, func(t *testing.T) { + parsed, err := ReadAsX509FromFile(v.input) + assert.NoError(t, err) + assert.NotEmpty(t, parsed) + got := IsSelfSigned(parsed[0]) - for k := range cert.IPAddresses { - if k == 0 { - _, _ = fmt.Fprintf(ipBuf, "%s", cert.IPAddresses[k].String()) - } else { - _, _ = fmt.Fprintf(ipBuf, ", %s", cert.IPAddresses[k].String()) - } + assert.Equal(t, v.expected, got) + }) } - t.Logf(" DNSNames: %s", dnsStr) - t.Logf(" IPAddresses: %s", ipBuf.String()) - t.Logf(" KeyUsage: %v", cert.KeyUsage) - t.Logf(" ExtKeyUsage: %v", cert.ExtKeyUsage) - t.Logf(" IsCA: %v", cert.IsCA) } diff --git a/crtutil/key.go b/crtutil/key.go index 9425d52..d671151 100644 --- a/crtutil/key.go +++ b/crtutil/key.go @@ -10,48 +10,30 @@ import ( "io/ioutil" ) -// ParseKeyFile parses an unencrypted crypto.PrivateKey from the given file. -// Deprecated: use ReadFileAsSigner instead. -func ParseKeyFile(fpath string) (crypto.PrivateKey, error) { - return ReadFileAsSigner(fpath) -} - -// ReadFileAsSigner read a crypto.PrivateKey from the given file. -func ReadFileAsSigner(fpath string) (crypto.PrivateKey, error) { +// ReadAsSignerFromFile read a crypto.PrivateKey from the given file. +func ReadAsSignerFromFile(fpath string) (crypto.PrivateKey, error) { f, err := ioutil.ReadFile(fpath) if err != nil { return nil, err } - return ReadBytesAsSigner(f, false) -} - -// ParseKeyFileWithPass read a crypto.PrivateKey from the given file. -// Deprecated: use ReadFileAsSignerWithPass instead. -func ParseKeyFileWithPass(keyPath, keyPass string) (crypto.PrivateKey, error) { - return ReadFileAsSignerWithPass(keyPath, keyPass) + return ReadAsSigner(f, false) } -// ReadFileAsSignerWithPass read a crypto.PrivateKey from the given file. -func ReadFileAsSignerWithPass(keyPath, keyPass string) (crypto.PrivateKey, error) { +// ReadAsSignerWithPassFromFile read a crypto.PrivateKey from the given file. +func ReadAsSignerWithPassFromFile(keyPath, keyPass string) (crypto.PrivateKey, error) { f, err := ioutil.ReadFile(keyPath) if err != nil { return nil, err } - return readBytesAsSigner(f, []byte(keyPass), false) -} - -// ParseKeyBytes read a crypto.PrivateKey from the given data. -// Deprecated: use ReadBytesAsSigner instead. -func ParseKeyBytes(data []byte, isBase64 bool) (crypto.PrivateKey, error) { - return readBytesAsSigner(data, nil, isBase64) + return readAsSigner(f, []byte(keyPass), false) } -// ReadBytesAsSigner read a crypto.PrivateKey from the given data. -func ReadBytesAsSigner(data []byte, isBase64 bool) (crypto.PrivateKey, error) { - return readBytesAsSigner(data, nil, isBase64) +// ReadAsSigner read a crypto.PrivateKey from the given data. +func ReadAsSigner(data []byte, isBase64 bool) (crypto.PrivateKey, error) { + return readAsSigner(data, nil, isBase64) } -func readBytesAsSigner(key, keypass []byte, isBase64 bool) (crypto.PrivateKey, error) { +func readAsSigner(key, keypass []byte, isBase64 bool) (crypto.PrivateKey, error) { var err error dkeystr := key diff --git a/crtutil/key_test.go b/crtutil/key_test.go index 24788f6..f98dcf8 100644 --- a/crtutil/key_test.go +++ b/crtutil/key_test.go @@ -9,7 +9,7 @@ import ( ) func TestParseKeyFile(t *testing.T) { - prik, err := ReadFileAsSigner("testdata/server-rsa.key") + prik, err := ReadAsSignerFromFile("testdata/server-rsa.key") assert.NoError(t, err) _, ok := prik.(*rsa.PrivateKey) @@ -17,7 +17,7 @@ func TestParseKeyFile(t *testing.T) { } func TestParseKeyFileWithPass(t *testing.T) { - prik, err := ReadFileAsSignerWithPass("testdata/server-rsa.key", "") + prik, err := ReadAsSignerWithPassFromFile("testdata/server-rsa.key", "") assert.NoError(t, err) _, ok := prik.(*rsa.PrivateKey) @@ -28,7 +28,7 @@ func TestParseKeyBytes(t *testing.T) { f, err := ioutil.ReadFile("testdata/server-rsa-base64.key") assert.NoError(t, err) - prik, err := ReadBytesAsSigner(f, true) + prik, err := ReadAsSigner(f, true) assert.NoError(t, err) _, ok := prik.(*rsa.PrivateKey) diff --git a/crtutil/testdata/self-signed.crt b/crtutil/testdata/self-signed.crt new file mode 100644 index 0000000..d519cba --- /dev/null +++ b/crtutil/testdata/self-signed.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE6jCCAtKgAwIBAgIITWWCIQf8/VIwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UE +AxMQQ1JUQ1RMIENPTU1PTiBDQTAeFw0yMjA5MjMwNjA5MTVaFw0zMjA5MzAwNjA5 +MTVaMBsxGTAXBgNVBAMTEENSVENUTCBDT01NT04gQ0EwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQC6rMnrnn5qJ2V7HssLdxW2B9t+0V40X3d0R7/rHm4/ +Hl8r1HjonYvW915mNQPJTlhXD/hOnnS4MeFl1Xh9cSn5k9DfA3jDodGWcFti7lDy +3chB0TSfVVyDb4dspmdj1s4Vns1P3uc5I5Bo9eW8Dd1eHiSCryzHCFv2lE2spnF9 +N/vlERZoUyLmb7JEbu/DERFJK2kaxaVK2ksvtEXzuqxWTjZhzbUtFpLuehwFSFYk +LeDNztLWFbCCjOIe4dn5JSxqlQcKawYB6wTOJPgEr2TZZaE3Hxbrh6Q0WNvYk9U4 +z1ufR6M8bFAEQX631rpTMsSjXJlrEZ3H3zr1JL1DuGRzH8nxH/3V33TUaS1/UnAr +fYNCB7wHabe6mG8Qe3cUH1FwFxCV0I8zVBsC0Tmq0pjvUseELcZ+4yoRpL7MTSmo +0s5FynZyC+DWRT68fIX2UYyaBXEB2hFHU5K1J227UikYEUBGHybSNUW2EJejhoHU +0V23EDBcSDunM8Po1VQQZbw0mk0+ICgJ7jujRuVaxugkTyzNrNxuuBsWl79juEA1 +efoaqfv2Jg5aKvcE6XXY3nfc6wchMJVit/pLXx9mZ0YbAJskZb8dX3wcJ0pLXm/s +YiIrYAcSZapYroG6/1lTs46LQigXRk69xrD5QbbfRlXIuGamYZYO0L2C7kxMtm9a +4QIDAQABozIwMDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRt6SsrHVmrtUaM +e5PDSX6VsCDlTDANBgkqhkiG9w0BAQsFAAOCAgEAczKncwdRjdhnNuB1h1Otzs0l +A/s1WFifszEwTa49IkT6IcyOkF3soU2e44gFgx+A6Po8yYXHfdZBHPrxwe4Mrr8H +AYN7V48XpX+QzGrUQZzNeEy4pKDr9QlqnZ8tpkC8kY2m3Bcea6A7U454cyuynrs2 +MWXin1Mr+GRs4RHjeG454DhszQZaMVgPNclnMpAvAUdDXNEcidM0sCbqF2oUzeV0 +Iv/O9zSQOGkFJqG5nMSLiaJDZsyI1yK2myfoMO2tmhiZLuOjsKQX4DrvfkcSADR9 +2InBqvKYyBh82g+Guu9Zx0pNpoUF7x9txbhmfG3EIuXgKJvKDp6+h5OQvu5hdahU +Hph7yqRsacq+QCvn+7QAhQD7maVaFhzAmydkd81l3JGY75d8iEyMK5uuTElNt7uP +DhNeIEl3zrrGvnz86QJeDZaBUFRtFcgjgIRMPx61mJk+FKx1SNsMb/pmrfVcxPmo +AwVtzzt8NfnGqEylh6ejIcnnYAI2q4DJDsBQIjnG0pp+om1OVbQB7kOR+ZGeKQvP +kSRnAYzAmb5MBKoOCe0ueyNcX01aT/D594m1nXyQ+wm692rSgkkjjuQysLMS3aVU +xX5PybKg0pxxnllCpumxzCDPguMAc1zaaw+d6kK4K70fLS4IrJjxxThd6uubCBC2 +v6R24xLVduhwWADQggs= +-----END CERTIFICATE----- diff --git a/crtutil/tmpl/funcs.go b/crtutil/tmpl/funcs.go new file mode 100644 index 0000000..d7f8a63 --- /dev/null +++ b/crtutil/tmpl/funcs.go @@ -0,0 +1,281 @@ +package tmpl + +import ( + "bytes" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/hex" + "fmt" + "strings" + "time" + + "github.com/fatih/color" + "github.com/shipengqi/golib/crtutil" +) + +const ( + ValidityWarnThreshold = "720h" + TimeFormat = "2006-01-02 15:04 MST" +) + +var ( + Green = color.New(color.Bold, color.FgGreen) + Yellow = color.New(color.Bold, color.FgYellow) + Red = color.New(color.Bold, color.FgRed) + Blue = color.New(color.Bold, color.FgBlue) + Cyan = color.New(color.Bold, color.FgCyan) +) + +var AlgorithmMapping = map[x509.SignatureAlgorithm]AlgorithmDesc{ + x509.MD2WithRSA: {Red, "MD2-RSA"}, + x509.MD5WithRSA: {Red, "MD5-RSA"}, + x509.SHA1WithRSA: {Red, "SHA1-RSA"}, + x509.SHA256WithRSA: {Green, "SHA256-RSA"}, + x509.SHA384WithRSA: {Green, "SHA384-RSA"}, + x509.SHA512WithRSA: {Green, "SHA512-RSA"}, + x509.DSAWithSHA1: {Red, "DSA-SHA1"}, + x509.DSAWithSHA256: {Red, "DSA-SHA256"}, + x509.ECDSAWithSHA1: {Red, "ECDSA-SHA1"}, + x509.ECDSAWithSHA256: {Green, "ECDSA-SHA256"}, + x509.ECDSAWithSHA384: {Green, "ECDSA-SHA384"}, + x509.ECDSAWithSHA512: {Green, "ECDSA-SHA512"}, + x509.PureEd25519: {Green, "ED25519"}, +} + +var KeyUsageStringMapping = map[x509.KeyUsage]string{ + x509.KeyUsageDigitalSignature: "Digital Signature", + x509.KeyUsageContentCommitment: "Content Commitment", + x509.KeyUsageKeyEncipherment: "Key Encipherment", + x509.KeyUsageDataEncipherment: "Data Encipherment", + x509.KeyUsageKeyAgreement: "Key Agreement", + x509.KeyUsageCertSign: "Cert Sign", + x509.KeyUsageCRLSign: "CRL Sign", + x509.KeyUsageEncipherOnly: "Encipher Only", + x509.KeyUsageDecipherOnly: "Decipher Only", +} + +var ExtKeyUsageStringMapping = map[x509.ExtKeyUsage]string{ + x509.ExtKeyUsageAny: "Any", + x509.ExtKeyUsageServerAuth: "Server Auth", + x509.ExtKeyUsageClientAuth: "Client Auth", + x509.ExtKeyUsageCodeSigning: "Code Signing", + x509.ExtKeyUsageEmailProtection: "Email Protection", + x509.ExtKeyUsageIPSECEndSystem: "IPSEC End System", + x509.ExtKeyUsageIPSECTunnel: "IPSEC Tunnel", + x509.ExtKeyUsageIPSECUser: "IPSEC User", + x509.ExtKeyUsageTimeStamping: "Time Stamping", + x509.ExtKeyUsageOCSPSigning: "OCSP Signing", + x509.ExtKeyUsageMicrosoftServerGatedCrypto: "Microsoft ServerGatedCrypto", + x509.ExtKeyUsageNetscapeServerGatedCrypto: "Netscape ServerGatedCrypto", +} + +var KeyUsages = []x509.KeyUsage{ + x509.KeyUsageDigitalSignature, + x509.KeyUsageContentCommitment, + x509.KeyUsageKeyEncipherment, + x509.KeyUsageDataEncipherment, + x509.KeyUsageKeyAgreement, + x509.KeyUsageCertSign, + x509.KeyUsageCRLSign, + x509.KeyUsageEncipherOnly, + x509.KeyUsageDecipherOnly, +} + +// OidDesc returns a human-readable name, a short acronym from RFC1485, a snake_case slug suitable as a json key, +// and a boolean describing whether multiple copies can appear on an X509 cert. +type OidDesc struct { + Name string + Short string + Slug string + Multiple bool +} + +type AlgorithmDesc struct { + Color *color.Color + Name string +} + +func (a *AlgorithmDesc) String() string { + return a.Color.SprintFunc()(a.Name) +} + +func CommonName(name pkix.Name) string { + if name.CommonName != "" { + return fmt.Sprintf("CN=%s", name.CommonName) + } + return ShortName(name) +} + +func ShortName(name pkix.Name) (out string) { + show := false + for _, n := range name.Names { + short := OidShort(n.Type) + if short != "" { + if show { + out += ", " + } + out += fmt.Sprintf("%s=%v", short, n.Value) + show = true + } + } + + return +} + +// HighlightAlgorithm changes the color of the signing algorithm. +func HighlightAlgorithm(alg x509.SignatureAlgorithm) string { + desc, ok := AlgorithmMapping[alg] + if !ok { + return alg.String() + } + return desc.String() +} + +// NotBefore takes a given NotBefore time of a certificate +// and returns that colorized time properly based on how +func NotBefore(start time.Time) string { + return start.Format(TimeFormat) +} + +// NotAfter takes a given NotAfter time of a certificate +// and returns that colorized time properly based on how +// close it is to expiry. +// If the certificate is valid for more than one month +// returns a green string. +// If the certificate is valid is less than one month +// the string will be yellow. +// If the certificate has already expired, the string +// will be red. +func NotAfter(end time.Time) string { + now := time.Now() + threshold := thresholdToTime(ValidityWarnThreshold, now) + + if end.Before(now) { + return ColorizeTimeString(end, "red") + } else if end.Before(threshold) { + return ColorizeTimeString(end, "yellow") + } else { + return ColorizeTimeString(end, "green") + } +} + +// KeyUsage returns key usage string from a certificate. +func KeyUsage(ku x509.KeyUsage) []string { + var out []string + for _, key := range KeyUsages { + if ku&key > 0 { + out = append(out, KeyUsageStringMapping[key]) + } + } + return out +} + +// ExtKeyUsage returns extended key usage string from a certificate. +func ExtKeyUsage(eku x509.ExtKeyUsage) string { + val, ok := ExtKeyUsageStringMapping[eku] + if ok { + return val + } + return fmt.Sprintf("Unknown:%d", eku) +} + +// Colorize colorizes the given string. +func Colorize(text, c string) string { + switch strings.ToLower(c) { + case "red": + return Red.SprintfFunc()(text) + case "yellow": + return Yellow.SprintfFunc()(text) + case "green": + return Green.SprintfFunc()(text) + case "blue": + return Blue.SprintfFunc()(text) + case "cyan": + return Cyan.SprintfFunc()(text) + default: + return Blue.SprintfFunc()(text) + } +} + +// Hexadecimalize returns a colon separated, hexadecimal representation +// of a given byte array. +func Hexadecimalize(data []byte) string { + var hexed bytes.Buffer + for i := 0; i < len(data); i++ { + hexed.WriteString(strings.ToUpper(hex.EncodeToString(data[i : i+1]))) + if i < len(data)-1 { + hexed.WriteString(":") + } + } + return hexed.String() +} + +func OidName(oid asn1.ObjectIdentifier) string { + return oiddesc(oid).Name +} + +func OidShort(oid asn1.ObjectIdentifier) string { + return oiddesc(oid).Short +} + +func ColorizeTimeString(t time.Time, c string) string { + return Colorize(t.Format(TimeFormat), c) +} + +func ShowNameConstraints(cert *x509.Certificate) bool { + if cert.PermittedDNSDomains != nil || cert.PermittedEmailAddresses != nil || + cert.PermittedIPRanges != nil || cert.PermittedURIDomains != nil || + cert.ExcludedDNSDomains != nil || cert.ExcludedEmailAddresses != nil || + cert.ExcludedIPRanges != nil || cert.ExcludedURIDomains != nil { + return true + } + + return false +} + +func ShowSelfSigned(cert *x509.Certificate) string { + if crtutil.IsSelfSigned(cert) { + return " (self-signed)" + } + + return "" +} + +func thresholdToTime(threshold string, nowT ...time.Time) time.Time { + var now time.Time + if len(nowT) == 0 { + now = time.Now() + } else { + now = nowT[0] + } + month, _ := time.ParseDuration(threshold) + return now.Add(month) +} + +func oiddesc(oid asn1.ObjectIdentifier) OidDesc { + raw := oid.String() + // Multiple should be true for any types that are []string in x509.pkix.Name. When in doubt, set it to true. + names := map[string]OidDesc{ + "2.5.4.3": {"CommonName", "CN", "common_name", false}, + "2.5.4.5": {"EV Incorporation Registration Number", "", "ev_registration_number", false}, + "2.5.4.6": {"Country", "C", "country", true}, + "2.5.4.7": {"Locality", "L", "locality", true}, + "2.5.4.8": {"Province", "ST", "province", true}, + "2.5.4.9": {"Street", "", "street", true}, + "2.5.4.10": {"Organization", "O", "organization", true}, + "2.5.4.11": {"Organizational Unit", "OU", "organizational_unit", true}, + "2.5.4.15": {"Business Category", "", "business_category", true}, + "2.5.4.17": {"Postal Code", "", "postalcode", true}, + "1.2.840.113549.1.9.1": {"Email Address", "", "email_address", true}, + "1.3.6.1.4.1.311.60.2.1.1": {"EV Incorporation Locality", "", "ev_locality", true}, + "1.3.6.1.4.1.311.60.2.1.2": {"EV Incorporation Province", "", "ev_province", true}, + "1.3.6.1.4.1.311.60.2.1.3": {"EV Incorporation Country", "", "ev_country", true}, + "0.9.2342.19200300.100.1.1": {"User ID", "UID", "user_id", true}, + "0.9.2342.19200300.100.1.25": {"Domain Component", "DC", "domain_component", true}, + } + if desc, ok := names[raw]; ok { + return desc + } + return OidDesc{raw, "", raw, true} +} diff --git a/crtutil/tmpl/template.go b/crtutil/tmpl/template.go new file mode 100644 index 0000000..4378fd4 --- /dev/null +++ b/crtutil/tmpl/template.go @@ -0,0 +1,179 @@ +package tmpl + +import ( + "bytes" + "crypto/x509" + "text/template" + + "github.com/Masterminds/sprig/v3" +) + +const ( + VerboseTmpl = ` +{{- define "PkixName" -}} +{{- range .Names}} + {{ .Type | oidName }}: {{ .Value }} +{{- end -}} +{{end -}} + +{{- define "IsSelfSigned" -}} +{{ . | isSelfSigned }} +{{end -}} + +Serial: {{.SerialNumber}} +Valid: {{.NotBefore | notBefore}} to {{.NotAfter | notAfter}} +Signature: {{.SignatureAlgorithm | highlightAlgorithm}}{{- template "IsSelfSigned" . -}} +Subject Info: + {{- template "PkixName" .Subject}} +Issuer Info: + {{- template "PkixName" .Issuer}} +{{- if .SubjectKeyId}} +Subject Key ID: {{.SubjectKeyId | tohex}} +{{- end}} +{{- if .AuthorityKeyId}} +Authority Key ID: {{.AuthorityKeyId | tohex}} +{{- end}} +{{- if .BasicConstraintsValid}} +Basic Constraints: CA:{{.IsCA}}{{if .MaxPathLen}}, pathlen:{{.MaxPathLen}}{{end}}{{end}} +{{- if (nameConstraints .) }} +Name Constraints{{if .PermittedDNSDomainsCritical}} (critical){{end}}: +{{- if .PermittedDNSDomains}} +Permitted DNS domains: + {{join ", " .PermittedDNSDomains}} +{{- end -}} +{{- if .PermittedEmailAddresses}} +Permitted email addresses: + {{join ", " .PermittedEmailAddresses}} +{{- end -}} +{{- if .PermittedIPRanges}} +Permitted IP ranges: + {{join ", " .PermittedIPRanges}} +{{- end -}} +{{- if .PermittedURIDomains}} +Permitted URI domains: + {{join ", " .PermittedURIDomains}} +{{- end}} +{{- if .ExcludedDNSDomains}} +Excluded DNS domains: + {{join ", " .ExcludedDNSDomains}} +{{- end}} +{{- if .ExcludedEmailAddresses}} +Excluded email addresses: + {{join ", " .ExcludedEmailAddresses}} +{{- end}} +{{- if .ExcludedIPRanges}} +Excluded IP ranges: + {{join ", " .ExcludedIPRanges}} +{{- end}} +{{- if .ExcludedURIDomains}} +Excluded URI domains: + {{join ", " .NameConstraints.ExcludedURIDomains}} +{{- end}} +{{- end}} +{{- if .OCSPServer}} +OCSP Server(s): + {{join ", " .OCSPServer}} +{{- end}} +{{- if .IssuingCertificateURL}} +Issuing Certificate URL(s): + {{join ", " .IssuingCertificateURL}} +{{- end}} +{{- if .KeyUsage}} +Key Usage: +{{- range .KeyUsage | keyUsage}} + {{.}} +{{- end}} +{{- end}} +{{- if .ExtKeyUsage}} +Extended Key Usage: +{{- range .ExtKeyUsage}} + {{. | extKeyUsage}}{{end}} +{{- end}} +{{- if .DNSNames}} +DNS Names: + {{join ", " .DNSNames}} +{{- end}} +{{- if .IPAddresses}} +IP Addresses: + {{join ", " .IPAddresses}} +{{- end}} +{{- if .URIs}} +URI Names: + {{join ", " .URIs}} +{{- end}} +{{- if .EmailAddresses}} +Email Addresses: + {{join ", " .EmailAddresses}} +{{- end}}` + + SimpleTmpl = `Valid: {{.NotBefore | notBefore}} to {{.NotAfter | notAfter}} +Subject: + {{.Subject | shortName}} +Issuer: + {{.Issuer | shortName}} +{{- if .DNSNames}} +DNS Names: + {{join ", " .DNSNames}}{{end}} +{{- if .IPAddresses}} +IP Addresses: + {{join ", " .IPAddresses}}{{end}} +{{- if .URIs}} +URI Names: + {{join ", " .URIs}}{{end}} +{{- if .EmailAddresses}} +Email Addresses: + {{join ", " .EmailAddresses}}{{end}}` + + WarningTmpl = ` +{{- if .Warnings}} +Warnings: +{{- range .Warnings}} + {{colorize . "red"}} +{{- end}} +{{- end}}` +) + +// BuildCertFuncMap build a template.FuncMap with some extras. +func BuildCertFuncMap() template.FuncMap { + funcmap := sprig.TxtFuncMap() + extras := template.FuncMap{ + "notBefore": NotBefore, + "notAfter": NotAfter, + "colorize": Colorize, + "highlightAlgorithm": HighlightAlgorithm, + "tohex": Hexadecimalize, + "keyUsage": KeyUsage, + "extKeyUsage": ExtKeyUsage, + "oidName": OidName, + "oidShort": OidShort, + "shortName": ShortName, + "commonName": CommonName, + "isSelfSigned": ShowSelfSigned, + "nameConstraints": ShowNameConstraints, + } + for k, v := range extras { + funcmap[k] = v + } + return funcmap +} + +func BuildDefaultCertTemplate(cert *x509.Certificate, verbose bool) ([]byte, error) { + t := template.New("crtutil template").Funcs(BuildCertFuncMap()) + + var err error + if verbose { + t, err = t.Parse(VerboseTmpl) + } else { + t, err = t.Parse(SimpleTmpl) + } + if err != nil { + return nil, err + } + + var buf bytes.Buffer + err = t.Execute(&buf, cert) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/crtutil/tmpl/template_test.go b/crtutil/tmpl/template_test.go new file mode 100644 index 0000000..8cf2726 --- /dev/null +++ b/crtutil/tmpl/template_test.go @@ -0,0 +1,54 @@ +package tmpl + +import ( + "testing" + + "github.com/shipengqi/golib/crtutil" + "github.com/stretchr/testify/assert" +) + +func TestBuildDefaultCertTemplate(t *testing.T) { + tests := []struct { + title string + input string + expected []string + }{ + { + "successfully output the certificate according to the given template", + "../testdata/server.crt", + []string{ + "Serial: 4751997750760398084", + "Valid: 2021-11-29 08:39 UTC to 2022-11-29 08:39 UTC", + "Signature: SHA256-RSA", + "Authority Key ID: CA:A5:79:D4:EB:5D:1F:F0:8F:40:52:A9:AF:3B:E7:6B:84:74:F9:B9", + "Basic Constraints: CA:false, pathlen:-1", + }, + }, + { + "successfully output the self-signed certificate according to the given template", + "../testdata/self-signed.crt", + []string{ + "Serial: 5577006791947779410", + "Valid: 2022-09-23 06:09 UTC to 2032-09-30 06:09 UTC", + "Signature: SHA256-RSA (self-signed)", + "Subject Key ID: 6D:E9:2B:2B:1D:59:AB:B5:46:8C:7B:93:C3:49:7E:95:B0:20:E5:4C", + "Basic Constraints: CA:true, pathlen:-1", + }, + }, + } + + for _, v := range tests { + t.Run(v.title, func(t *testing.T) { + parsed, err := crtutil.ReadAsX509FromFile(v.input) + assert.NoError(t, err) + assert.NotEmpty(t, parsed) + + output, err := BuildDefaultCertTemplate(parsed[0], true) + assert.NoError(t, err) + + for _, contain := range v.expected { + assert.Contains(t, string(output), contain) + } + }) + } +} diff --git a/go.mod b/go.mod index 1a34f88..3a051f9 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,8 @@ require ( ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.2 + github.com/fatih/color v1.13.0 github.com/kr/pretty v0.1.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/go.sum b/go.sum index 819b6f8..58e0b9d 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,66 @@ +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shipengqi/errors v0.1.0 h1:7ijUYXaUapqBcMXC1uhhUtITuGgSRolNeCoIJanw2Ag= github.com/shipengqi/errors v0.1.0/go.mod h1:xymtEu8M3i0SIEhhMyShYHZfn/e7tBQwjuLHyQ/UGCw= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/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-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=