Skip to content

Commit c4330cd

Browse files
committed
feat: info subcommand
1 parent 3fff766 commit c4330cd

File tree

2 files changed

+356
-0
lines changed

2 files changed

+356
-0
lines changed

cmd/info.go

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
/*
2+
Copyright © 2025 xiexianbin.cn
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package main
15+
16+
import (
17+
"crypto/ecdsa"
18+
"crypto/rsa"
19+
"crypto/x509"
20+
"crypto/x509/pkix"
21+
"encoding/asn1"
22+
"encoding/pem"
23+
"fmt"
24+
"math/big"
25+
"os"
26+
"strings"
27+
28+
"github.com/spf13/cobra"
29+
)
30+
31+
var (
32+
certfilePath string
33+
)
34+
35+
// OIDs for various X.509 extensions.
36+
var (
37+
oidExtensionSubjectKeyId = asn1.ObjectIdentifier{2, 5, 29, 14}
38+
oidExtensionKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15}
39+
oidExtensionExtendedKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}
40+
oidExtensionAuthorityKeyId = asn1.ObjectIdentifier{2, 5, 29, 35}
41+
oidExtensionSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
42+
oidExtensionBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19}
43+
oidExtensionCRLDistributionPoints = asn1.ObjectIdentifier{2, 5, 29, 31}
44+
oidExtensionCertificatePolicies = asn1.ObjectIdentifier{2, 5, 29, 32}
45+
oidAuthorityInfoAccess = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 1}
46+
)
47+
48+
// Maps OIDs to their string representations for better readability.
49+
var oidToStringMap = map[string]string{
50+
"2.5.29.14": "X509v3 Subject Key Identifier",
51+
"2.5.29.15": "X509v3 Key Usage",
52+
"2.5.29.19": "X509v3 Basic Constraints",
53+
"2.5.29.31": "X509v3 CRL Distribution Points",
54+
"2.5.29.32": "X509v3 Certificate Policies",
55+
"2.5.29.35": "X509v3 Authority Key Identifier",
56+
"2.5.29.37": "X509v3 Extended Key Usage",
57+
"2.5.29.17": "X509v3 Subject Alternative Name",
58+
"1.3.6.1.5.5.7.1.1": "Authority Information Access",
59+
}
60+
61+
func printCertificateInfo(cert *x509.Certificate) {
62+
fmt.Println("Certificate:")
63+
fmt.Println(" Data:")
64+
// Version
65+
fmt.Printf(" Version: %d (0x%x)\n", cert.Version, cert.Version-1)
66+
67+
// Serial Number
68+
fmt.Printf(" Serial Number:\n %s\n", formatSerialNumber(cert.SerialNumber))
69+
70+
// Signature Algorithm
71+
fmt.Printf(" Signature Algorithm: %s\n", cert.SignatureAlgorithm.String())
72+
73+
// Issuer
74+
fmt.Printf(" Issuer: %s\n", formatName(cert.Issuer))
75+
76+
// Validity
77+
fmt.Println(" Validity")
78+
fmt.Printf(" Not Before: %s\n", cert.NotBefore.UTC().Format("Jan 2 15:04:05 2006 GMT"))
79+
fmt.Printf(" Not After : %s\n", cert.NotAfter.UTC().Format("Jan 2 15:04:05 2006 GMT"))
80+
81+
// Subject
82+
fmt.Printf(" Subject: %s\n", formatName(cert.Subject))
83+
84+
// Public Key
85+
fmt.Println(" Subject Public Key Info:")
86+
printPublicKeyInfo(cert.PublicKey)
87+
88+
// Extensions
89+
if len(cert.Extensions) > 0 {
90+
fmt.Println(" X509v3 extensions:")
91+
printExtensions(cert)
92+
}
93+
94+
// Signature
95+
fmt.Printf(" Signature Algorithm: %s\n", cert.SignatureAlgorithm.String())
96+
printHexBlock(" ", cert.Signature, 18)
97+
}
98+
99+
func formatName(name pkix.Name) string {
100+
var parts []string
101+
if len(name.Country) > 0 {
102+
parts = append(parts, "C="+strings.Join(name.Country, ","))
103+
}
104+
if len(name.Province) > 0 {
105+
parts = append(parts, "ST="+strings.Join(name.Province, ","))
106+
}
107+
if len(name.Locality) > 0 {
108+
parts = append(parts, "L="+strings.Join(name.Locality, ","))
109+
}
110+
if len(name.Organization) > 0 {
111+
parts = append(parts, "O="+strings.Join(name.Organization, ","))
112+
}
113+
if len(name.OrganizationalUnit) > 0 {
114+
parts = append(parts, "OU="+strings.Join(name.OrganizationalUnit, ","))
115+
}
116+
if name.CommonName != "" {
117+
parts = append(parts, "CN="+name.CommonName)
118+
}
119+
return strings.Join(parts, ", ")
120+
}
121+
122+
func formatSerialNumber(serial *big.Int) string {
123+
hex := fmt.Sprintf("%x", serial)
124+
if len(hex)%2 != 0 {
125+
hex = "0" + hex
126+
}
127+
var parts []string
128+
for i := 0; i < len(hex); i += 2 {
129+
parts = append(parts, hex[i:i+2])
130+
}
131+
return strings.Join(parts, ":")
132+
}
133+
134+
func printPublicKeyInfo(pub interface{}) {
135+
switch pub := pub.(type) {
136+
case *rsa.PublicKey:
137+
fmt.Printf(" Public Key Algorithm: %s\n", x509.RSA.String())
138+
fmt.Printf(" RSA Public-Key: (%d bit)\n", pub.N.BitLen())
139+
fmt.Println(" Modulus:")
140+
printHexBlock(" ", pub.N.Bytes(), 15)
141+
fmt.Printf(" Exponent: %d (0x%x)\n", pub.E, pub.E)
142+
143+
case *ecdsa.PublicKey:
144+
fmt.Printf(" Public Key Algorithm: %s\n", x509.ECDSA.String())
145+
fmt.Printf(" Public-Key: (%d bit)\n", pub.Curve.Params().BitSize)
146+
printHexBlock(" ", pub.X.Bytes(), 15) // Just showing the X coordinate for brevity, similar to some tools
147+
fmt.Printf(" Curve: %s\n", pub.Curve.Params().Name)
148+
default:
149+
fmt.Println(" Public Key Algorithm: Unknown")
150+
}
151+
}
152+
153+
func printExtensions(cert *x509.Certificate) {
154+
for _, ext := range cert.Extensions {
155+
oidStr := ext.Id.String()
156+
extName, ok := oidToStringMap[oidStr]
157+
if !ok {
158+
extName = oidStr // Fallback to OID if not in map
159+
}
160+
161+
criticalStr := ""
162+
if ext.Critical {
163+
criticalStr = "critical"
164+
}
165+
fmt.Printf(" %s: %s\n", extName, criticalStr)
166+
167+
// Parse and print specific extension details
168+
printExtensionValue(ext, cert)
169+
}
170+
}
171+
172+
func printExtensionValue(ext pkix.Extension, cert *x509.Certificate) {
173+
indent := " "
174+
switch {
175+
case ext.Id.Equal(oidExtensionKeyUsage):
176+
printKeyUsage(cert.KeyUsage, indent)
177+
case ext.Id.Equal(oidExtensionExtendedKeyUsage):
178+
printExtendedKeyUsage(cert.ExtKeyUsage, indent)
179+
case ext.Id.Equal(oidExtensionBasicConstraints):
180+
fmt.Printf("%sCA:%t\n", indent, cert.IsCA)
181+
case ext.Id.Equal(oidExtensionSubjectKeyId):
182+
fmt.Printf("%s%s\n", indent, formatHex(cert.SubjectKeyId))
183+
case ext.Id.Equal(oidExtensionAuthorityKeyId):
184+
fmt.Printf("%skeyid:%s\n", indent, formatHex(cert.AuthorityKeyId))
185+
case ext.Id.Equal(oidExtensionSubjectAltName):
186+
printSAN(cert, indent)
187+
case ext.Id.Equal(oidAuthorityInfoAccess):
188+
printAIA(cert, indent)
189+
case ext.Id.Equal(oidExtensionCRLDistributionPoints):
190+
for _, point := range cert.CRLDistributionPoints {
191+
fmt.Printf("%sFull Name:\n%s URI:%s\n", indent, indent, point)
192+
}
193+
case ext.Id.Equal(oidExtensionCertificatePolicies):
194+
for _, policy := range cert.PolicyIdentifiers {
195+
fmt.Printf("%sPolicy: %s\n", indent, policy.String())
196+
}
197+
}
198+
}
199+
200+
func printKeyUsage(ku x509.KeyUsage, indent string) {
201+
var usages []string
202+
if ku&x509.KeyUsageDigitalSignature != 0 {
203+
usages = append(usages, "Digital Signature")
204+
}
205+
if ku&x509.KeyUsageContentCommitment != 0 {
206+
usages = append(usages, "Content Commitment")
207+
}
208+
if ku&x509.KeyUsageKeyEncipherment != 0 {
209+
usages = append(usages, "Key Encipherment")
210+
}
211+
if ku&x509.KeyUsageDataEncipherment != 0 {
212+
usages = append(usages, "Data Encipherment")
213+
}
214+
if ku&x509.KeyUsageKeyAgreement != 0 {
215+
usages = append(usages, "Key Agreement")
216+
}
217+
if ku&x509.KeyUsageCertSign != 0 {
218+
usages = append(usages, "Certificate Sign")
219+
}
220+
if ku&x509.KeyUsageCRLSign != 0 {
221+
usages = append(usages, "CRL Sign")
222+
}
223+
if ku&x509.KeyUsageEncipherOnly != 0 {
224+
usages = append(usages, "Encipher Only")
225+
}
226+
if ku&x509.KeyUsageDecipherOnly != 0 {
227+
usages = append(usages, "Decipher Only")
228+
}
229+
fmt.Printf("%s%s\n", indent, strings.Join(usages, ", "))
230+
}
231+
232+
func printExtendedKeyUsage(ekus []x509.ExtKeyUsage, indent string) {
233+
var usages []string
234+
for _, eku := range ekus {
235+
switch eku {
236+
case x509.ExtKeyUsageAny:
237+
usages = append(usages, "Any")
238+
case x509.ExtKeyUsageServerAuth:
239+
usages = append(usages, "TLS Web Server Authentication")
240+
case x509.ExtKeyUsageClientAuth:
241+
usages = append(usages, "TLS Web Client Authentication")
242+
case x509.ExtKeyUsageCodeSigning:
243+
usages = append(usages, "Code Signing")
244+
case x509.ExtKeyUsageEmailProtection:
245+
usages = append(usages, "E-mail Protection")
246+
case x509.ExtKeyUsageIPSECEndSystem:
247+
usages = append(usages, "IPSEC End System")
248+
case x509.ExtKeyUsageIPSECTunnel:
249+
usages = append(usages, "IPSEC Tunnel")
250+
case x509.ExtKeyUsageIPSECUser:
251+
usages = append(usages, "IPSEC User")
252+
case x509.ExtKeyUsageTimeStamping:
253+
usages = append(usages, "Time Stamping")
254+
case x509.ExtKeyUsageOCSPSigning:
255+
usages = append(usages, "OCSP Signing")
256+
case x509.ExtKeyUsageMicrosoftServerGatedCrypto:
257+
usages = append(usages, "Microsoft Server Gated Crypto")
258+
case x509.ExtKeyUsageNetscapeServerGatedCrypto:
259+
usages = append(usages, "Netscape Server Gated Crypto")
260+
default:
261+
usages = append(usages, "Unknown")
262+
}
263+
}
264+
fmt.Printf("%s%s\n", indent, strings.Join(usages, ", "))
265+
}
266+
267+
func printSAN(cert *x509.Certificate, indent string) {
268+
var san []string
269+
for _, name := range cert.DNSNames {
270+
san = append(san, "DNS:"+name)
271+
}
272+
for _, email := range cert.EmailAddresses {
273+
san = append(san, "email:"+email)
274+
}
275+
for _, ip := range cert.IPAddresses {
276+
san = append(san, "IP Address:"+ip.String())
277+
}
278+
for _, uri := range cert.URIs {
279+
san = append(san, "URI:"+uri.String())
280+
}
281+
fmt.Printf("%s%s\n", indent, strings.Join(san, ", "))
282+
}
283+
284+
func printAIA(cert *x509.Certificate, indent string) {
285+
if len(cert.OCSPServer) > 0 {
286+
fmt.Printf("%sOCSP - URI:%s\n", indent, strings.Join(cert.OCSPServer, ", "))
287+
}
288+
if len(cert.IssuingCertificateURL) > 0 {
289+
fmt.Printf("%sCA Issuers - URI:%s\n", indent, strings.Join(cert.IssuingCertificateURL, ", "))
290+
}
291+
}
292+
293+
func formatHex(data []byte) string {
294+
var parts []string
295+
for i, b := range data {
296+
if i > 0 {
297+
parts = append(parts, ":")
298+
}
299+
parts = append(parts, fmt.Sprintf("%02X", b))
300+
}
301+
return strings.Join(parts, "")
302+
}
303+
304+
func printHexBlock(prefix string, data []byte, wrap int) {
305+
var parts []string
306+
for i, b := range data {
307+
if i > 0 && i%wrap == 0 {
308+
parts = append(parts, "\n"+prefix)
309+
}
310+
parts = append(parts, fmt.Sprintf("%02x:", b))
311+
}
312+
// remove last ":"
313+
if len(parts) > 0 {
314+
str := strings.Join(parts, "")
315+
fmt.Printf("%s%s\n", prefix, str[:len(str)-1])
316+
}
317+
}
318+
319+
// infoCmd represents the info command
320+
var infoCmd = &cobra.Command{
321+
Use: "info",
322+
Short: "Display information about the XCA tool",
323+
Long: `Display information about Certificate, like 'openssl x509 -noout -text -in xxx.crt' including version, and basic information.
324+
325+
Examples:
326+
xca info <path-of>/root-ca.crt`,
327+
Args: cobra.ExactArgs(1),
328+
Run: func(cmd *cobra.Command, args []string) {
329+
certfilePath = args[0]
330+
331+
// Read the certificate file
332+
certPEM, err := os.ReadFile(certfilePath)
333+
if err != nil {
334+
fmt.Fprintf(os.Stderr, "Error reading certificate file: %v\n", err)
335+
os.Exit(1)
336+
}
337+
338+
// Decode the PEM-encoded certificate
339+
block, _ := pem.Decode(certPEM)
340+
if block == nil || block.Type != "CERTIFICATE" {
341+
fmt.Fprintf(os.Stderr, "Failed to decode PEM block containing certificate\n")
342+
os.Exit(1)
343+
}
344+
345+
// Parse the certificate
346+
cert, err := x509.ParseCertificate(block.Bytes)
347+
if err != nil {
348+
fmt.Fprintf(os.Stderr, "Error parsing certificate: %v\n", err)
349+
os.Exit(1)
350+
}
351+
352+
// Print certificate details
353+
printCertificateInfo(cert)
354+
},
355+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func init() {
5555

5656
// Add subcommands
5757
rootCmd.AddCommand(createCaCmd)
58+
rootCmd.AddCommand(infoCmd)
5859
rootCmd.AddCommand(signCmd)
5960
}
6061

0 commit comments

Comments
 (0)