Skip to content

Commit 576d8ad

Browse files
authored
Fix certificate inspect (#1153)
* Pull in updated go.step.sm/crypto with improved parsing for PEM formatted CSR and certificate files - Allows for ignoring extraneous data in PEM files * go mod tidy * Removed unused derToPemBlock helper function * Update changelog | allow certificate inspect to output CSR in PEM format * Add unit tests for inspectCertificateRequest
1 parent 6236b6e commit 576d8ad

File tree

5 files changed

+114
-94
lines changed

5 files changed

+114
-94
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2626

2727
---
2828

29+
## [unreleased] - aaaa-bb-cc
30+
31+
### Added
32+
33+
- Ability to output inspected CSR in PEM format (smallstep/cli#1153)
34+
35+
### Fixed
36+
37+
- Allow 'certificate inspect' to parse PEM files containig extraneous data (smallstep/cli#1153)
38+
39+
2940
## [v0.26.0] - 2024-03-27
3041

3142
### Added

command/certificate/inspect.go

Lines changed: 38 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package certificate
22

33
import (
4-
"bytes"
54
"crypto/x509"
65
"encoding/json"
76
"encoding/pem"
@@ -12,10 +11,10 @@ import (
1211
"github.com/pkg/errors"
1312
"github.com/smallstep/certinfo"
1413
"github.com/smallstep/cli/flags"
15-
"github.com/smallstep/cli/utils"
1614
zx509 "github.com/smallstep/zcrypto/x509"
1715
"github.com/urfave/cli"
1816
"go.step.sm/cli-utils/errs"
17+
"go.step.sm/crypto/pemutil"
1918
)
2019

2120
func inspectCommand() cli.Command {
@@ -26,7 +25,7 @@ func inspectCommand() cli.Command {
2625
UsageText: `**step certificate inspect** <crt-file>
2726
[**--bundle**] [**--short**] [**--format**=<format>] [**--roots**=<root-bundle>]
2827
[**--servername**=<servername>]`,
29-
Description: `**step certificate inspect** prints the details of the
28+
Description: `**step certificate inspect** prints the details of the
3029
certificate or CSR in a human- or machine-readable format. Beware: Local certificates
3130
are never verified. Always verify a certificate (using **step certificate verify**)
3231
before relying on the output of this command.
@@ -206,9 +205,6 @@ func inspectAction(ctx *cli.Context) error {
206205
return errs.IncompatibleFlagWithFlag(ctx, "short", "format json")
207206
}
208207

209-
var block *pem.Block
210-
var blocks []*pem.Block
211-
212208
switch addr, isURL, err := trimURL(crtFile); {
213209
case err != nil:
214210
return err
@@ -217,67 +213,35 @@ func inspectAction(ctx *cli.Context) error {
217213
if err != nil {
218214
return err
219215
}
220-
for _, crt := range peerCertificates {
221-
blocks = append(blocks, &pem.Block{
222-
Type: "CERTIFICATE",
223-
Bytes: crt.Raw,
224-
})
225-
}
216+
return inspectCertificates(ctx, peerCertificates, os.Stdout)
226217
default: // is not URL
227-
crtBytes, err := utils.ReadFile(crtFile)
228-
if err != nil {
229-
return errs.FileError(err, crtFile)
230-
}
231-
if bytes.Contains(crtBytes, []byte("-----BEGIN ")) {
232-
for len(crtBytes) > 0 {
233-
block, crtBytes = pem.Decode(crtBytes)
234-
if block == nil {
235-
break
236-
}
237-
if bundle && block.Type != "CERTIFICATE" {
238-
return errors.Errorf("certificate bundle %q contains an unexpected PEM block of type %q\n\n expected type: CERTIFICATE",
239-
crtFile, block.Type)
240-
}
241-
blocks = append(blocks, block)
218+
var pemError *pemutil.InvalidPEMError
219+
crts, err := pemutil.ReadCertificateBundle(crtFile)
220+
switch {
221+
case errors.As(err, &pemError) && pemError.Type == pemutil.PEMTypeCertificate:
222+
csr, err := pemutil.ReadCertificateRequest(crtFile)
223+
if err != nil {
224+
return errors.Errorf("file %s does not contain any valid CERTIFICATE or CERTIFICATE REQUEST blocks", crtFile)
242225
}
243-
} else {
244-
if block = derToPemBlock(crtBytes); block == nil {
245-
return errors.Errorf("%q contains an invalid PEM block", crtFile)
226+
return inspectCertificateRequest(ctx, csr, os.Stdout)
227+
case err != nil:
228+
return err
229+
default:
230+
if bundle {
231+
return inspectCertificates(ctx, crts, os.Stdout)
246232
}
247-
blocks = append(blocks, block)
248-
}
249-
250-
// prevent index out of range errors
251-
if len(blocks) == 0 {
252-
return fmt.Errorf("%q does not contain valid PEM blocks", crtFile)
233+
return inspectCertificates(ctx, crts[:1], os.Stdout)
253234
}
254235
}
255-
256-
// Keep the first one if !bundle
257-
if !bundle {
258-
blocks = []*pem.Block{blocks[0]}
259-
}
260-
261-
switch blocks[0].Type {
262-
case "CERTIFICATE":
263-
return inspectCertificates(ctx, blocks, os.Stdout)
264-
case "CERTIFICATE REQUEST", "NEW CERTIFICATE REQUEST": // only one is supported
265-
return inspectCertificateRequest(ctx, blocks[0])
266-
default:
267-
return errors.Errorf("Invalid PEM type in %q. Expected [CERTIFICATE|CERTIFICATE REQUEST] but got %q)", crtFile, block.Type)
268-
}
269236
}
270237

271-
func inspectCertificates(ctx *cli.Context, blocks []*pem.Block, w io.Writer) error {
238+
func inspectCertificates(ctx *cli.Context, crts []*x509.Certificate, w io.Writer) error {
239+
var err error
272240
format, short := ctx.String("format"), ctx.Bool("short")
273241
switch format {
274242
case "text":
275243
var text string
276-
for _, block := range blocks {
277-
crt, err := x509.ParseCertificate(block.Bytes)
278-
if err != nil {
279-
return errors.WithStack(err)
280-
}
244+
for _, crt := range crts {
281245
if short {
282246
if text, err = certinfo.CertificateShortText(crt); err != nil {
283247
return err
@@ -292,16 +256,16 @@ func inspectCertificates(ctx *cli.Context, blocks []*pem.Block, w io.Writer) err
292256
return nil
293257
case "json":
294258
var v interface{}
295-
if len(blocks) == 1 {
296-
zcrt, err := zx509.ParseCertificate(blocks[0].Bytes)
259+
if len(crts) == 1 {
260+
zcrt, err := zx509.ParseCertificate(crts[0].Raw)
297261
if err != nil {
298262
return errors.WithStack(err)
299263
}
300264
v = struct{ *zx509.Certificate }{zcrt}
301265
} else {
302266
var zcrts []*zx509.Certificate
303-
for _, block := range blocks {
304-
zcrt, err := zx509.ParseCertificate(block.Bytes)
267+
for _, crt := range crts {
268+
zcrt, err := zx509.ParseCertificate(crt.Raw)
305269
if err != nil {
306270
return errors.WithStack(err)
307271
}
@@ -317,8 +281,8 @@ func inspectCertificates(ctx *cli.Context, blocks []*pem.Block, w io.Writer) err
317281
}
318282
return nil
319283
case "pem":
320-
for _, block := range blocks {
321-
err := pem.Encode(w, block)
284+
for _, crt := range crts {
285+
err := pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: crt.Raw})
322286
if err != nil {
323287
return errors.WithStack(err)
324288
}
@@ -329,15 +293,12 @@ func inspectCertificates(ctx *cli.Context, blocks []*pem.Block, w io.Writer) err
329293
}
330294
}
331295

332-
func inspectCertificateRequest(ctx *cli.Context, block *pem.Block) error {
296+
func inspectCertificateRequest(ctx *cli.Context, csr *x509.CertificateRequest, w io.Writer) error {
297+
var err error
333298
format, short := ctx.String("format"), ctx.Bool("short")
334299
switch format {
335300
case "text":
336301
var text string
337-
csr, err := x509.ParseCertificateRequest(block.Bytes)
338-
if err != nil {
339-
return errors.WithStack(err)
340-
}
341302
if short {
342303
text, err = certinfo.CertificateRequestShortText(csr)
343304
if err != nil {
@@ -349,35 +310,26 @@ func inspectCertificateRequest(ctx *cli.Context, block *pem.Block) error {
349310
return err
350311
}
351312
}
352-
fmt.Print(text)
313+
fmt.Fprint(w, text)
353314
return nil
354315
case "json":
355-
zcsr, err := zx509.ParseCertificateRequest(block.Bytes)
316+
zcsr, err := zx509.ParseCertificateRequest(csr.Raw)
356317
if err != nil {
357318
return errors.WithStack(err)
358319
}
359-
b, err := json.MarshalIndent(struct {
360-
*zx509.CertificateRequest
361-
}{zcsr}, "", " ")
320+
enc := json.NewEncoder(w)
321+
enc.SetIndent("", " ")
322+
if err := enc.Encode(zcsr); err != nil {
323+
return errors.WithStack(err)
324+
}
325+
return nil
326+
case "pem":
327+
err := pem.Encode(w, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw})
362328
if err != nil {
363329
return errors.WithStack(err)
364330
}
365-
os.Stdout.Write(b)
366331
return nil
367332
default:
368333
return errs.InvalidFlagValue(ctx, "format", format, "text, json")
369334
}
370335
}
371-
372-
// derToPemBlock attempts to parse the ASN.1 data as a certificate or a
373-
// certificate request, returning a pem.Block of the one that succeeds. Returns
374-
// nil if it cannot parse the data.
375-
func derToPemBlock(b []byte) *pem.Block {
376-
if _, err := x509.ParseCertificate(b); err == nil {
377-
return &pem.Block{Type: "CERTIFICATE", Bytes: b}
378-
}
379-
if _, err := x509.ParseCertificateRequest(b); err == nil {
380-
return &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: b}
381-
}
382-
return nil
383-
}

command/certificate/inspect_test.go

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package certificate
33
import (
44
"bytes"
55
"encoding/json"
6-
"encoding/pem"
76
"flag"
87
"testing"
98

109
"github.com/smallstep/assert"
1110
"github.com/urfave/cli"
11+
"go.step.sm/crypto/pemutil"
1212
)
1313

1414
var pemData = []byte(`-----BEGIN CERTIFICATE-----
@@ -39,9 +39,8 @@ func TestInspectCertificates(t *testing.T) {
3939
_ = set.String("format", "", "")
4040
ctx := cli.NewContext(app, set, nil)
4141

42-
var blocks []*pem.Block
43-
block, _ := pem.Decode(pemData)
44-
blocks = append(blocks, block)
42+
certs, err := pemutil.ParseCertificateBundle(pemData)
43+
assert.FatalError(t, err)
4544

4645
type testCase struct {
4746
format string
@@ -72,7 +71,65 @@ func TestInspectCertificates(t *testing.T) {
7271
t.Run(name, func(t *testing.T) {
7372
var buf bytes.Buffer
7473
ctx.Set("format", tc.format)
75-
err := inspectCertificates(ctx, blocks, &buf)
74+
err := inspectCertificates(ctx, certs, &buf)
75+
assert.NoError(t, err)
76+
if err == nil {
77+
tc.verify(&buf)
78+
}
79+
})
80+
}
81+
82+
}
83+
84+
var csrPEMData = []byte(`-----BEGIN CERTIFICATE REQUEST-----
85+
MIHmMIGNAgEAMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASGlyI2t5ibpcG+
86+
hGm0JMW0or/QphyTlc4GGAccapsz4BeXkNucKpeX3nupFbbABHLcN/bjxL87Ims8
87+
jz5sdl6xoCswKQYJKoZIhvcNAQkOMRwwGjAYBgNVHREEETAPggNmb2+CA2JhcoID
88+
YmF6MAoGCCqGSM49BAMCA0gAMEUCIEuWM0UdEeDfvWqssxyoY4cUuv++FrmA97j+
89+
Fbp7Kk6gAiEAuoyrBIvX28Spmeog9Jl4iBJYzceSNz8a7crRNGLTyjs=
90+
-----END CERTIFICATE REQUEST-----
91+
`)
92+
93+
func TestInspectCertificateRequest(t *testing.T) {
94+
// This is just to get a simple CLI context
95+
app := &cli.App{}
96+
set := flag.NewFlagSet("contrive", 0)
97+
_ = set.String("format", "", "")
98+
ctx := cli.NewContext(app, set, nil)
99+
100+
csr, err := pemutil.ParseCertificateRequest(csrPEMData)
101+
assert.FatalError(t, err)
102+
103+
type testCase struct {
104+
format string
105+
verify func(buf *bytes.Buffer)
106+
}
107+
108+
tests := map[string]testCase{
109+
"format text": {"text",
110+
func(buf *bytes.Buffer) {
111+
assert.HasPrefix(t, buf.String(), "Certificate Request:")
112+
},
113+
},
114+
"format json": {"json",
115+
func(buf *bytes.Buffer) {
116+
var v interface{}
117+
err := json.Unmarshal(buf.Bytes(), &v)
118+
assert.NoError(t, err)
119+
},
120+
},
121+
"format pem": {"pem",
122+
func(buf *bytes.Buffer) {
123+
assert.Equals(t, string(csrPEMData), buf.String())
124+
},
125+
},
126+
}
127+
128+
for name, tc := range tests {
129+
t.Run(name, func(t *testing.T) {
130+
var buf bytes.Buffer
131+
ctx.Set("format", tc.format)
132+
err := inspectCertificateRequest(ctx, csr, &buf)
76133
assert.NoError(t, err)
77134
if err == nil {
78135
tc.verify(&buf)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ require (
2626
github.com/urfave/cli v1.22.14
2727
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
2828
go.step.sm/cli-utils v0.9.0
29-
go.step.sm/crypto v0.44.6
29+
go.step.sm/crypto v0.44.7
3030
go.step.sm/linkedca v0.20.1
3131
golang.org/x/crypto v0.22.0
3232
golang.org/x/sys v0.19.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,8 +476,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
476476
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
477477
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
478478
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
479-
go.step.sm/crypto v0.44.6 h1:vQg8ujce7fNXDO8EWdriSz+ZSJpYnNh22QrFtRjdyoY=
480-
go.step.sm/crypto v0.44.6/go.mod h1:oKRO4jaf2MaCohJDN+/8ShImkvIgUKfJxxy87gqsnXs=
479+
go.step.sm/crypto v0.44.7 h1:aJ7dVbkm5TxEtHbicgN6JEVzPxZlp9JW9RQQH5bpi/o=
480+
go.step.sm/crypto v0.44.7/go.mod h1:oKRO4jaf2MaCohJDN+/8ShImkvIgUKfJxxy87gqsnXs=
481481
go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU=
482482
go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw=
483483
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=

0 commit comments

Comments
 (0)