diff --git a/README.md b/README.md index ab7bfcb..674fdd0 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ certstrap allows you to build your own certificate system: 1. Initialize certificate authorities 2. Create identities and certificate signature requests for hosts 3. Sign and generate certificates +4. Revoke certificates ## Certificate architecture @@ -80,6 +81,13 @@ $ ./bin/certstrap sign Alice --CA CertAuth Created out/Alice.crt from out/Alice.csr signed by out/CertAuth.key ``` +### Revoke certificate: + +``` +$ ./bin/certstrap revoke --CN Alice --CA CertAuth +'CertAuth' authority has revoked certificate for 'Alice'. +``` + #### PKCS Format: If you'd like to convert your certificate and key to PKCS12 format, simply run: ``` diff --git a/certstrap.go b/certstrap.go index da19ee1..04422f9 100644 --- a/certstrap.go +++ b/certstrap.go @@ -44,6 +44,7 @@ func main() { cmd.NewInitCommand(), cmd.NewCertRequestCommand(), cmd.NewSignCommand(), + mcmd.NewRevokeCommand(), } app.Before = func(c *cli.Context) error { cmd.InitDepot(c.String("depot-path")) diff --git a/cmd/revoke.go b/cmd/revoke.go new file mode 100644 index 0000000..2763a80 --- /dev/null +++ b/cmd/revoke.go @@ -0,0 +1,127 @@ +package cmd + +import ( + "crypto/rand" + "crypto/x509" + x509pkix "crypto/x509/pkix" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/square/certstrap/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/square/certstrap/depot" + "github.com/square/certstrap/pkix" +) + +type revokeCommand struct { + ca, cn string +} + +// NewRevokeCommand revokes the given CN's certificate by adding it to the CA's CRL. +func NewRevokeCommand() cli.Command { + return cli.Command{ + Name: "revoke", + Usage: "Revoke certificate", + Description: "Add certificate to the CA's CRL.", + Flags: []cli.Flag{ + cli.StringFlag{"CN", "", "Certificate's CN to revoke", ""}, + cli.StringFlag{"CA", "", "CA's name to revoke cert", ""}, + }, + Action: new(revokeCommand).run, + } +} + +func (c *revokeCommand) checkErr(err error) { + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } +} + +func (c *revokeCommand) parseArgs(ctx *cli.Context) error { + if ctx.String("CA") == "" { + return errors.New("CA name must be provided") + } + c.ca = strings.Replace(ctx.String("CA"), " ", "_", -1) + + if ctx.String("CN") == "" { + return errors.New("CN name must be provided") + } + c.cn = strings.Replace(ctx.String("CN"), " ", "_", -1) + + return nil +} + +func (c *revokeCommand) run(ctx *cli.Context) { + c.checkErr(c.parseArgs(ctx)) + + caCert, err := c.CAx509Certificate() + c.checkErr(err) + + cnCert, err := c.CNx509Certificate() + c.checkErr(err) + + revoked, err := c.revokedCertificates() + c.checkErr(err) + + revoked = append(revoked, x509pkix.RevokedCertificate{ + SerialNumber: cnCert.SerialNumber, + RevocationTime: time.Now(), + }) + + err = c.saveRevokedCertificates(caCert, revoked) + c.checkErr(err) + + fmt.Fprintln(os.Stdout, "%q authority has revoked the certificate for %q.", c.cn, c.ca) +} + +func (c *revokeCommand) CAx509Certificate() (*x509.Certificate, error) { + cert, err := depot.GetCertificate(d, c.ca) + if err != nil { + return nil, err + } + return cert.GetRawCertificate() +} + +func (c *revokeCommand) CNx509Certificate() (*x509.Certificate, error) { + cert, err := depot.GetCertificate(d, c.cn) + if err != nil { + return nil, err + } + return cert.GetRawCertificate() +} + +func (c *revokeCommand) revokedCertificates() ([]x509pkix.RevokedCertificate, error) { + list, err := depot.GetCertificateRevocationList(d, c.ca) + if err != nil { + return nil, err + } + + certList, err := x509.ParseDERCRL(list.DERBytes()) + if err != nil { + return nil, err + } + + return certList.TBSCertList.RevokedCertificates, nil +} + +func (c *revokeCommand) saveRevokedCertificates(cert *x509.Certificate, list []x509pkix.RevokedCertificate) error { + priv, err := depot.GetPrivateKey(d, c.ca) + if err != nil { + return fmt.Errorf("could not get %q private key: %v", c.ca, err) + } + + crlBytes, err := cert.CreateCRL(rand.Reader, priv.Private, list, time.Now(), time.Now()) + if err != nil { + return fmt.Errorf("could not create CRL: %v", err) + } + if err := d.Delete(depot.CrlTag(c.ca)); err != nil { + return fmt.Errorf("could not delete CRL: %v", err) + } + if err = depot.PutCertificateRevocationList(d, c.ca, pkix.NewCertificateRevocationListFromDER(crlBytes)); err != nil { + return fmt.Errorf("could not put revokation list: %v", err) + } + return nil +} diff --git a/depot/pkix.go b/depot/pkix.go index aa92691..49fd564 100644 --- a/depot/pkix.go +++ b/depot/pkix.go @@ -169,3 +169,12 @@ func PutCertificateRevocationList(d Depot, name string, crl *pkix.CertificateRev } return d.Put(CrlTag(name), b) } + +//GetCertificateRevocationList gets a CRL file for a given name and ca in the depot. +func GetCertificateRevocationList(d Depot, name string) (*pkix.CertificateRevocationList, error) { + b, err := d.Get(CrlTag(name)) + if err != nil { + return nil, err + } + return pkix.NewCertificateRevocationListFromPEM(b) +} diff --git a/pkix/crl.go b/pkix/crl.go index 0136b63..b3d8df1 100644 --- a/pkix/crl.go +++ b/pkix/crl.go @@ -48,6 +48,11 @@ type CertificateRevocationList struct { derBytes []byte } +//DERBytes returns DER-formatted bytes of the CRL. +func (c *CertificateRevocationList) DERBytes() []byte { + return c.derBytes +} + // NewCertificateRevocationListFromDER inits CertificateRevocationList from DER-format bytes func NewCertificateRevocationListFromDER(derBytes []byte) *CertificateRevocationList { return &CertificateRevocationList{derBytes: derBytes}