Merge branch 'master' of https://github.com/hashicorp/vault into vishalvault

This commit is contained in:
Vishal Nayak 2015-06-24 18:13:26 -04:00
commit f39df58eef
31 changed files with 5900 additions and 2 deletions

4
Godeps/Godeps.json generated
View file

@ -63,6 +63,10 @@
"Comment": "v2.0.0-7-g73a8ef7",
"Rev": "73a8ef737e8ea002281a28b4cb92a1de121ad4c6"
},
{
"ImportPath": "github.com/fatih/structs",
"Rev": "a9f7daa9c2729e97450c2da2feda19130a367d8f"
},
{
"ImportPath": "github.com/go-sql-driver/mysql",
"Comment": "v1.2-88-ga197e5d",

View file

@ -0,0 +1,23 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test

View file

@ -0,0 +1,11 @@
language: go
go: 1.3
before_install:
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- go get code.google.com/p/go.tools/cmd/cover
script:
- $HOME/gopath/bin/goveralls -repotoken $COVERALLS_TOKEN
env:
global:
- secure: hkc+92KPmMFqIH9n4yWdnH1JpZjahmOyDJwpTh8Yl0JieJNG0XEXpOqNao27eA0cLF+UHdyjFeGcPUJKNmgE46AoQjtovt+ICjCXKR2yF6S2kKJcUOz/Vd6boZF7qHV06jjxyxOebpID5iSoW6UfFr001bFxpd3jaSLFTzSHWRQ=

21
Godeps/_workspace/src/github.com/fatih/structs/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Fatih Arslan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,164 @@
# Structs [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/structs) [![Build Status](http://img.shields.io/travis/fatih/structs.svg?style=flat-square)](https://travis-ci.org/fatih/structs) [![Coverage Status](http://img.shields.io/coveralls/fatih/structs.svg?style=flat-square)](https://coveralls.io/r/fatih/structs)
Structs contains various utilities to work with Go (Golang) structs. It was
initially used by me to convert a struct into a `map[string]interface{}`. With
time I've added other utilities for structs. It's basically a high level
package based on primitives from the reflect package. Feel free to add new
functions or improve the existing code.
## Install
```bash
go get github.com/fatih/structs
```
## Usage and Examples
Just like the standard lib `strings`, `bytes` and co packages, `structs` has
many global functions to manipulate or organize your struct data. Lets define
and declare a struct:
```go
type Server struct {
Name string `json:"name,omitempty"`
ID int
Enabled bool
users []string // not exported
http.Server // embedded
}
server := &Server{
Name: "gopher",
ID: 123456,
Enabled: true,
}
```
```go
// Convert a struct to a map[string]interface{}
// => {"Name":"gopher", "ID":123456, "Enabled":true}
m := structs.Map(server)
// Convert the values of a struct to a []interface{}
// => ["gopher", 123456, true]
v := structs.Values(server)
// Convert the names of a struct to a []string
// (see "Names methods" for more info about fields)
n := structs.Names(server)
// Convert the values of a struct to a []*Field
// (see "Field methods" for more info about fields)
f := structs.Fields(server)
// Return the struct name => "Server"
n := structs.Name(server)
// Check if any field of a struct is initialized or not.
h := structs.HasZero(server)
// Check if all fields of a struct is initialized or not.
z := structs.IsZero(server)
// Check if server is a struct or a pointer to struct
i := structs.IsStruct(server)
```
### Struct methods
The structs functions can be also used as independent methods by creating a new
`*structs.Struct`. This is handy if you want to have more control over the
structs (such as retrieving a single Field).
```go
// Create a new struct type:
s := structs.New(server)
m := s.Map() // Get a map[string]interface{}
v := s.Values() // Get a []interface{}
f := s.Fields() // Get a []*Field
n := s.Names() // Get a []string
f := s.Field(name) // Get a *Field based on the given field name
f, ok := s.FieldOk(name) // Get a *Field based on the given field name
n := s.Name() // Get the struct name
h := s.HasZero() // Check if any field is initialized
z := s.IsZero() // Check if all fields are initialized
```
### Field methods
We can easily examine a single Field for more detail. Below you can see how we
get and interact with various field methods:
```go
s := structs.New(server)
// Get the Field struct for the "Name" field
name := s.Field("Name")
// Get the underlying value, value => "gopher"
value := name.Value().(string)
// Set the field's value
name.Set("another gopher")
// Get the field's kind, kind => "string"
name.Kind()
// Check if the field is exported or not
if name.IsExported() {
fmt.Println("Name field is exported")
}
// Check if the value is a zero value, such as "" for string, 0 for int
if !name.IsZero() {
fmt.Println("Name is initialized")
}
// Check if the field is an anonymous (embedded) field
if !name.IsEmbedded() {
fmt.Println("Name is not an embedded field")
}
// Get the Field's tag value for tag name "json", tag value => "name,omitempty"
tagValue := name.Tag("json")
```
Nested structs are supported too:
```go
addrField := s.Field("Server").Field("Addr")
// Get the value for addr
a := addrField.Value().(string)
// Or get all fields
httpServer := s.Field("Server").Fields()
```
We can also get a slice of Fields from the Struct type to iterate over all
fields. This is handy if you wish to examine all fields:
```go
// Convert the fields of a struct to a []*Field
fields := s.Fields()
for _, f := range fields {
fmt.Printf("field name: %+v\n", f.Name())
if f.IsExported() {
fmt.Printf("value : %+v\n", f.Value())
fmt.Printf("is zero : %+v\n", f.IsZero())
}
}
```
## Credits
* [Fatih Arslan](https://github.com/fatih)
* [Cihangir Savas](https://github.com/cihangir)
## License
The MIT License (MIT) - see LICENSE.md for more details

126
Godeps/_workspace/src/github.com/fatih/structs/field.go generated vendored Normal file
View file

@ -0,0 +1,126 @@
package structs
import (
"errors"
"fmt"
"reflect"
)
var (
errNotExported = errors.New("field is not exported")
errNotSettable = errors.New("field is not settable")
)
// Field represents a single struct field that encapsulates high level
// functions around the field.
type Field struct {
value reflect.Value
field reflect.StructField
defaultTag string
}
// Tag returns the value associated with key in the tag string. If there is no
// such key in the tag, Tag returns the empty string.
func (f *Field) Tag(key string) string {
return f.field.Tag.Get(key)
}
// Value returns the underlying value of of the field. It panics if the field
// is not exported.
func (f *Field) Value() interface{} {
return f.value.Interface()
}
// IsEmbedded returns true if the given field is an anonymous field (embedded)
func (f *Field) IsEmbedded() bool {
return f.field.Anonymous
}
// IsExported returns true if the given field is exported.
func (f *Field) IsExported() bool {
return f.field.PkgPath == ""
}
// IsZero returns true if the given field is not initalized (has a zero value).
// It panics if the field is not exported.
func (f *Field) IsZero() bool {
zero := reflect.Zero(f.value.Type()).Interface()
current := f.Value()
return reflect.DeepEqual(current, zero)
}
// Name returns the name of the given field
func (f *Field) Name() string {
return f.field.Name
}
// Kind returns the fields kind, such as "string", "map", "bool", etc ..
func (f *Field) Kind() reflect.Kind {
return f.value.Kind()
}
// Set sets the field to given value v. It retuns an error if the field is not
// settable (not addresable or not exported) or if the given value's type
// doesn't match the fields type.
func (f *Field) Set(val interface{}) error {
// we can't set unexported fields, so be sure this field is exported
if !f.IsExported() {
return errNotExported
}
// do we get here? not sure...
if !f.value.CanSet() {
return errNotSettable
}
given := reflect.ValueOf(val)
if f.value.Kind() != given.Kind() {
return fmt.Errorf("wrong kind. got: %s want: %s", given.Kind(), f.value.Kind())
}
f.value.Set(given)
return nil
}
// Fields returns a slice of Fields. This is particular handy to get the fields
// of a nested struct . A struct tag with the content of "-" ignores the
// checking of that particular field. Example:
//
// // Field is ignored by this package.
// Field *http.Request `structs:"-"`
//
// It panics if field is not exported or if field's kind is not struct
func (f *Field) Fields() []*Field {
return getFields(f.value, f.defaultTag)
}
// Field returns the field from a nested struct. It panics if the nested struct
// is not exported or if the field was not found.
func (f *Field) Field(name string) *Field {
field, ok := f.FieldOk(name)
if !ok {
panic("field not found")
}
return field
}
// Field returns the field from a nested struct. The boolean returns true if
// the field was found. It panics if the nested struct is not exported or if
// the field was not found.
func (f *Field) FieldOk(name string) (*Field, bool) {
v := strctVal(f.value.Interface())
t := v.Type()
field, ok := t.FieldByName(name)
if !ok {
return nil, false
}
return &Field{
field: field,
value: v.FieldByName(name),
}, true
}

View file

@ -0,0 +1,324 @@
package structs
import (
"reflect"
"testing"
)
// A test struct that defines all cases
type Foo struct {
A string
B int `structs:"y"`
C bool `json:"c"`
d string // not exported
E *Baz
x string `xml:"x"` // not exported, with tag
Y []string
Z map[string]interface{}
*Bar // embedded
}
type Baz struct {
A string
B int
}
type Bar struct {
E string
F int
g []string
}
func newStruct() *Struct {
b := &Bar{
E: "example",
F: 2,
g: []string{"zeynep", "fatih"},
}
// B and x is not initialized for testing
f := &Foo{
A: "gopher",
C: true,
d: "small",
E: nil,
Y: []string{"example"},
Z: nil,
}
f.Bar = b
return New(f)
}
func TestField_Set(t *testing.T) {
s := newStruct()
f := s.Field("A")
err := f.Set("fatih")
if err != nil {
t.Error(err)
}
if f.Value().(string) != "fatih" {
t.Errorf("Setted value is wrong: %s want: %s", f.Value().(string), "fatih")
}
f = s.Field("Y")
err = f.Set([]string{"override", "with", "this"})
if err != nil {
t.Error(err)
}
sliceLen := len(f.Value().([]string))
if sliceLen != 3 {
t.Errorf("Setted values slice length is wrong: %d, want: %d", sliceLen, 3)
}
f = s.Field("C")
err = f.Set(false)
if err != nil {
t.Error(err)
}
if f.Value().(bool) {
t.Errorf("Setted value is wrong: %s want: %s", f.Value().(bool), false)
}
// let's pass a different type
f = s.Field("A")
err = f.Set(123) // Field A is of type string, but we are going to pass an integer
if err == nil {
t.Error("Setting a field's value with a different type than the field's type should return an error")
}
// old value should be still there :)
if f.Value().(string) != "fatih" {
t.Errorf("Setted value is wrong: %s want: %s", f.Value().(string), "fatih")
}
// let's access an unexported field, which should give an error
f = s.Field("d")
err = f.Set("large")
if err != errNotExported {
t.Error(err)
}
// let's set a pointer to struct
b := &Bar{
E: "gopher",
F: 2,
}
f = s.Field("Bar")
err = f.Set(b)
if err != nil {
t.Error(err)
}
baz := &Baz{
A: "helloWorld",
B: 42,
}
f = s.Field("E")
err = f.Set(baz)
if err != nil {
t.Error(err)
}
ba := s.Field("E").Value().(*Baz)
if ba.A != "helloWorld" {
t.Errorf("could not set baz. Got: %s Want: helloWorld", ba.A)
}
}
func TestField(t *testing.T) {
s := newStruct()
defer func() {
err := recover()
if err == nil {
t.Error("Retrieveing a non existing field from the struct should panic")
}
}()
_ = s.Field("no-field")
}
func TestField_Kind(t *testing.T) {
s := newStruct()
f := s.Field("A")
if f.Kind() != reflect.String {
t.Errorf("Field A has wrong kind: %s want: %s", f.Kind(), reflect.String)
}
f = s.Field("B")
if f.Kind() != reflect.Int {
t.Errorf("Field B has wrong kind: %s want: %s", f.Kind(), reflect.Int)
}
// unexported
f = s.Field("d")
if f.Kind() != reflect.String {
t.Errorf("Field d has wrong kind: %s want: %s", f.Kind(), reflect.String)
}
}
func TestField_Tag(t *testing.T) {
s := newStruct()
v := s.Field("B").Tag("json")
if v != "" {
t.Errorf("Field's tag value of a non existing tag should return empty, got: %s", v)
}
v = s.Field("C").Tag("json")
if v != "c" {
t.Errorf("Field's tag value of the existing field C should return 'c', got: %s", v)
}
v = s.Field("d").Tag("json")
if v != "" {
t.Errorf("Field's tag value of a non exported field should return empty, got: %s", v)
}
v = s.Field("x").Tag("xml")
if v != "x" {
t.Errorf("Field's tag value of a non exported field with a tag should return 'x', got: %s", v)
}
v = s.Field("A").Tag("json")
if v != "" {
t.Errorf("Field's tag value of a existing field without a tag should return empty, got: %s", v)
}
}
func TestField_Value(t *testing.T) {
s := newStruct()
v := s.Field("A").Value()
val, ok := v.(string)
if !ok {
t.Errorf("Field's value of a A should be string")
}
if val != "gopher" {
t.Errorf("Field's value of a existing tag should return 'gopher', got: %s", val)
}
defer func() {
err := recover()
if err == nil {
t.Error("Value of a non exported field from the field should panic")
}
}()
// should panic
_ = s.Field("d").Value()
}
func TestField_IsEmbedded(t *testing.T) {
s := newStruct()
if !s.Field("Bar").IsEmbedded() {
t.Errorf("Fields 'Bar' field is an embedded field")
}
if s.Field("d").IsEmbedded() {
t.Errorf("Fields 'd' field is not an embedded field")
}
}
func TestField_IsExported(t *testing.T) {
s := newStruct()
if !s.Field("Bar").IsExported() {
t.Errorf("Fields 'Bar' field is an exported field")
}
if !s.Field("A").IsExported() {
t.Errorf("Fields 'A' field is an exported field")
}
if s.Field("d").IsExported() {
t.Errorf("Fields 'd' field is not an exported field")
}
}
func TestField_IsZero(t *testing.T) {
s := newStruct()
if s.Field("A").IsZero() {
t.Errorf("Fields 'A' field is an initialized field")
}
if !s.Field("B").IsZero() {
t.Errorf("Fields 'B' field is not an initialized field")
}
}
func TestField_Name(t *testing.T) {
s := newStruct()
if s.Field("A").Name() != "A" {
t.Errorf("Fields 'A' field should have the name 'A'")
}
}
func TestField_Field(t *testing.T) {
s := newStruct()
e := s.Field("Bar").Field("E")
val, ok := e.Value().(string)
if !ok {
t.Error("The value of the field 'e' inside 'Bar' struct should be string")
}
if val != "example" {
t.Errorf("The value of 'e' should be 'example, got: %s", val)
}
defer func() {
err := recover()
if err == nil {
t.Error("Field of a non existing nested struct should panic")
}
}()
_ = s.Field("Bar").Field("e")
}
func TestField_Fields(t *testing.T) {
s := newStruct()
fields := s.Field("Bar").Fields()
if len(fields) != 3 {
t.Errorf("We expect 3 fields in embedded struct, was: %d", len(fields))
}
}
func TestField_FieldOk(t *testing.T) {
s := newStruct()
b, ok := s.FieldOk("Bar")
if !ok {
t.Error("The field 'Bar' should exists.")
}
e, ok := b.FieldOk("E")
if !ok {
t.Error("The field 'E' should exists.")
}
val, ok := e.Value().(string)
if !ok {
t.Error("The value of the field 'e' inside 'Bar' struct should be string")
}
if val != "example" {
t.Errorf("The value of 'e' should be 'example, got: %s", val)
}
}

View file

@ -0,0 +1,449 @@
// Package structs contains various utilities functions to work with structs.
package structs
import "reflect"
var (
// DefaultTagName is the default tag name for struct fields which provides
// a more granular to tweak certain structs. Lookup the necessary functions
// for more info.
DefaultTagName = "structs" // struct's field default tag name
)
// Struct encapsulates a struct type to provide several high level functions
// around the struct.
type Struct struct {
raw interface{}
value reflect.Value
TagName string
}
// New returns a new *Struct with the struct s. It panics if the s's kind is
// not struct.
func New(s interface{}) *Struct {
return &Struct{
raw: s,
value: strctVal(s),
TagName: DefaultTagName,
}
}
// Map converts the given struct to a map[string]interface{}, where the keys
// of the map are the field names and the values of the map the associated
// values of the fields. The default key string is the struct field name but
// can be changed in the struct field's tag value. The "structs" key in the
// struct's field tag value is the key name. Example:
//
// // Field appears in map as key "myName".
// Name string `structs:"myName"`
//
// A tag value with the content of "-" ignores that particular field. Example:
//
// // Field is ignored by this package.
// Field bool `structs:"-"`
//
// A tag value with the option of "omitnested" stops iterating further if the type
// is a struct. Example:
//
// // Field is not processed further by this package.
// Field time.Time `structs:"myName,omitnested"`
// Field *http.Request `structs:",omitnested"`
//
// A tag value with the option of "omitempty" ignores that particular field if
// the field value is empty. Example:
//
// // Field appears in map as key "myName", but the field is
// // skipped if empty.
// Field string `structs:"myName,omitempty"`
//
// // Field appears in map as key "Field" (the default), but
// // the field is skipped if empty.
// Field string `structs:",omitempty"`
//
// Note that only exported fields of a struct can be accessed, non exported
// fields will be neglected.
func (s *Struct) Map() map[string]interface{} {
out := make(map[string]interface{})
fields := s.structFields()
for _, field := range fields {
name := field.Name
val := s.value.FieldByName(name)
var finalVal interface{}
tagName, tagOpts := parseTag(field.Tag.Get(s.TagName))
if tagName != "" {
name = tagName
}
// if the value is a zero value and the field is marked as omitempty do
// not include
if tagOpts.Has("omitempty") {
zero := reflect.Zero(val.Type()).Interface()
current := val.Interface()
if reflect.DeepEqual(current, zero) {
continue
}
}
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
// look out for embedded structs, and convert them to a
// map[string]interface{} too
n := New(val.Interface())
n.TagName = s.TagName
finalVal = n.Map()
} else {
finalVal = val.Interface()
}
out[name] = finalVal
}
return out
}
// Values converts the given s struct's field values to a []interface{}. A
// struct tag with the content of "-" ignores the that particular field.
// Example:
//
// // Field is ignored by this package.
// Field int `structs:"-"`
//
// A value with the option of "omitnested" stops iterating further if the type
// is a struct. Example:
//
// // Fields is not processed further by this package.
// Field time.Time `structs:",omitnested"`
// Field *http.Request `structs:",omitnested"`
//
// A tag value with the option of "omitempty" ignores that particular field and
// is not added to the values if the field value is empty. Example:
//
// // Field is skipped if empty
// Field string `structs:",omitempty"`
//
// Note that only exported fields of a struct can be accessed, non exported
// fields will be neglected.
func (s *Struct) Values() []interface{} {
fields := s.structFields()
var t []interface{}
for _, field := range fields {
val := s.value.FieldByName(field.Name)
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
// if the value is a zero value and the field is marked as omitempty do
// not include
if tagOpts.Has("omitempty") {
zero := reflect.Zero(val.Type()).Interface()
current := val.Interface()
if reflect.DeepEqual(current, zero) {
continue
}
}
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
// look out for embedded structs, and convert them to a
// []interface{} to be added to the final values slice
for _, embeddedVal := range Values(val.Interface()) {
t = append(t, embeddedVal)
}
} else {
t = append(t, val.Interface())
}
}
return t
}
// Fields returns a slice of Fields. A struct tag with the content of "-"
// ignores the checking of that particular field. Example:
//
// // Field is ignored by this package.
// Field bool `structs:"-"`
//
// It panics if s's kind is not struct.
func (s *Struct) Fields() []*Field {
return getFields(s.value, s.TagName)
}
// Names returns a slice of field names. A struct tag with the content of "-"
// ignores the checking of that particular field. Example:
//
// // Field is ignored by this package.
// Field bool `structs:"-"`
//
// It panics if s's kind is not struct.
func (s *Struct) Names() []string {
fields := getFields(s.value, s.TagName)
names := make([]string, len(fields))
for i, field := range fields {
names[i] = field.Name()
}
return names
}
func getFields(v reflect.Value, tagName string) []*Field {
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
var fields []*Field
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if tag := field.Tag.Get(tagName); tag == "-" {
continue
}
f := &Field{
field: field,
value: v.FieldByName(field.Name),
}
fields = append(fields, f)
}
return fields
}
// Field returns a new Field struct that provides several high level functions
// around a single struct field entity. It panics if the field is not found.
func (s *Struct) Field(name string) *Field {
f, ok := s.FieldOk(name)
if !ok {
panic("field not found")
}
return f
}
// Field returns a new Field struct that provides several high level functions
// around a single struct field entity. The boolean returns true if the field
// was found.
func (s *Struct) FieldOk(name string) (*Field, bool) {
t := s.value.Type()
field, ok := t.FieldByName(name)
if !ok {
return nil, false
}
return &Field{
field: field,
value: s.value.FieldByName(name),
defaultTag: s.TagName,
}, true
}
// IsZero returns true if all fields in a struct is a zero value (not
// initialized) A struct tag with the content of "-" ignores the checking of
// that particular field. Example:
//
// // Field is ignored by this package.
// Field bool `structs:"-"`
//
// A value with the option of "omitnested" stops iterating further if the type
// is a struct. Example:
//
// // Field is not processed further by this package.
// Field time.Time `structs:"myName,omitnested"`
// Field *http.Request `structs:",omitnested"`
//
// Note that only exported fields of a struct can be accessed, non exported
// fields will be neglected. It panics if s's kind is not struct.
func (s *Struct) IsZero() bool {
fields := s.structFields()
for _, field := range fields {
val := s.value.FieldByName(field.Name)
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
ok := IsZero(val.Interface())
if !ok {
return false
}
continue
}
// zero value of the given field, such as "" for string, 0 for int
zero := reflect.Zero(val.Type()).Interface()
// current value of the given field
current := val.Interface()
if !reflect.DeepEqual(current, zero) {
return false
}
}
return true
}
// HasZero returns true if a field in a struct is not initialized (zero value).
// A struct tag with the content of "-" ignores the checking of that particular
// field. Example:
//
// // Field is ignored by this package.
// Field bool `structs:"-"`
//
// A value with the option of "omitnested" stops iterating further if the type
// is a struct. Example:
//
// // Field is not processed further by this package.
// Field time.Time `structs:"myName,omitnested"`
// Field *http.Request `structs:",omitnested"`
//
// Note that only exported fields of a struct can be accessed, non exported
// fields will be neglected. It panics if s's kind is not struct.
func (s *Struct) HasZero() bool {
fields := s.structFields()
for _, field := range fields {
val := s.value.FieldByName(field.Name)
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
ok := HasZero(val.Interface())
if ok {
return true
}
continue
}
// zero value of the given field, such as "" for string, 0 for int
zero := reflect.Zero(val.Type()).Interface()
// current value of the given field
current := val.Interface()
if reflect.DeepEqual(current, zero) {
return true
}
}
return false
}
// Name returns the structs's type name within its package. For more info refer
// to Name() function.
func (s *Struct) Name() string {
return s.value.Type().Name()
}
// structFields returns the exported struct fields for a given s struct. This
// is a convenient helper method to avoid duplicate code in some of the
// functions.
func (s *Struct) structFields() []reflect.StructField {
t := s.value.Type()
var f []reflect.StructField
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// we can't access the value of unexported fields
if field.PkgPath != "" {
continue
}
// don't check if it's omitted
if tag := field.Tag.Get(s.TagName); tag == "-" {
continue
}
f = append(f, field)
}
return f
}
func strctVal(s interface{}) reflect.Value {
v := reflect.ValueOf(s)
// if pointer get the underlying element≤
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
panic("not struct")
}
return v
}
// Map converts the given struct to a map[string]interface{}. For more info
// refer to Struct types Map() method. It panics if s's kind is not struct.
func Map(s interface{}) map[string]interface{} {
return New(s).Map()
}
// Values converts the given struct to a []interface{}. For more info refer to
// Struct types Values() method. It panics if s's kind is not struct.
func Values(s interface{}) []interface{} {
return New(s).Values()
}
// Fields returns a slice of *Field. For more info refer to Struct types
// Fields() method. It panics if s's kind is not struct.
func Fields(s interface{}) []*Field {
return New(s).Fields()
}
// Names returns a slice of field names. For more info refer to Struct types
// Names() method. It panics if s's kind is not struct.
func Names(s interface{}) []string {
return New(s).Names()
}
// IsZero returns true if all fields is equal to a zero value. For more info
// refer to Struct types IsZero() method. It panics if s's kind is not struct.
func IsZero(s interface{}) bool {
return New(s).IsZero()
}
// HasZero returns true if any field is equal to a zero value. For more info
// refer to Struct types HasZero() method. It panics if s's kind is not struct.
func HasZero(s interface{}) bool {
return New(s).HasZero()
}
// IsStruct returns true if the given variable is a struct or a pointer to
// struct.
func IsStruct(s interface{}) bool {
v := reflect.ValueOf(s)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
// uninitialized zero value of a struct
if v.Kind() == reflect.Invalid {
return false
}
return v.Kind() == reflect.Struct
}
// Name returns the structs's type name within its package. It returns an
// empty string for unnamed types. It panics if s's kind is not struct.
func Name(s interface{}) string {
return New(s).Name()
}

View file

@ -0,0 +1,351 @@
package structs
import (
"fmt"
"time"
)
func ExampleNew() {
type Server struct {
Name string
ID int32
Enabled bool
}
server := &Server{
Name: "Arslan",
ID: 123456,
Enabled: true,
}
s := New(server)
fmt.Printf("Name : %v\n", s.Name())
fmt.Printf("Values : %v\n", s.Values())
fmt.Printf("Value of ID : %v\n", s.Field("ID").Value())
// Output:
// Name : Server
// Values : [Arslan 123456 true]
// Value of ID : 123456
}
func ExampleMap() {
type Server struct {
Name string
ID int32
Enabled bool
}
s := &Server{
Name: "Arslan",
ID: 123456,
Enabled: true,
}
m := Map(s)
fmt.Printf("%#v\n", m["Name"])
fmt.Printf("%#v\n", m["ID"])
fmt.Printf("%#v\n", m["Enabled"])
// Output:
// "Arslan"
// 123456
// true
}
func ExampleMap_tags() {
// Custom tags can change the map keys instead of using the fields name
type Server struct {
Name string `structs:"server_name"`
ID int32 `structs:"server_id"`
Enabled bool `structs:"enabled"`
}
s := &Server{
Name: "Zeynep",
ID: 789012,
}
m := Map(s)
// access them by the custom tags defined above
fmt.Printf("%#v\n", m["server_name"])
fmt.Printf("%#v\n", m["server_id"])
fmt.Printf("%#v\n", m["enabled"])
// Output:
// "Zeynep"
// 789012
// false
}
func ExampleMap_nested() {
// By default field with struct types are processed too. We can stop
// processing them via "omitnested" tag option.
type Server struct {
Name string `structs:"server_name"`
ID int32 `structs:"server_id"`
Time time.Time `structs:"time,omitnested"` // do not convert to map[string]interface{}
}
const shortForm = "2006-Jan-02"
t, _ := time.Parse("2006-Jan-02", "2013-Feb-03")
s := &Server{
Name: "Zeynep",
ID: 789012,
Time: t,
}
m := Map(s)
// access them by the custom tags defined above
fmt.Printf("%v\n", m["server_name"])
fmt.Printf("%v\n", m["server_id"])
fmt.Printf("%v\n", m["time"].(time.Time))
// Output:
// Zeynep
// 789012
// 2013-02-03 00:00:00 +0000 UTC
}
func ExampleMap_omitEmpty() {
// By default field with struct types of zero values are processed too. We
// can stop processing them via "omitempty" tag option.
type Server struct {
Name string `structs:",omitempty"`
ID int32 `structs:"server_id,omitempty"`
Location string
}
// Only add location
s := &Server{
Location: "Tokyo",
}
m := Map(s)
// map contains only the Location field
fmt.Printf("%v\n", m)
// Output:
// map[Location:Tokyo]
}
func ExampleValues() {
type Server struct {
Name string
ID int32
Enabled bool
}
s := &Server{
Name: "Fatih",
ID: 135790,
Enabled: false,
}
m := Values(s)
fmt.Printf("Values: %+v\n", m)
// Output:
// Values: [Fatih 135790 false]
}
func ExampleValues_omitEmpty() {
// By default field with struct types of zero values are processed too. We
// can stop processing them via "omitempty" tag option.
type Server struct {
Name string `structs:",omitempty"`
ID int32 `structs:"server_id,omitempty"`
Location string
}
// Only add location
s := &Server{
Location: "Ankara",
}
m := Values(s)
// values contains only the Location field
fmt.Printf("Values: %+v\n", m)
// Output:
// Values: [Ankara]
}
func ExampleValues_tags() {
type Location struct {
City string
Country string
}
type Server struct {
Name string
ID int32
Enabled bool
Location Location `structs:"-"` // values from location are not included anymore
}
s := &Server{
Name: "Fatih",
ID: 135790,
Enabled: false,
Location: Location{City: "Ankara", Country: "Turkey"},
}
// Let get all values from the struct s. Note that we don't include values
// from the Location field
m := Values(s)
fmt.Printf("Values: %+v\n", m)
// Output:
// Values: [Fatih 135790 false]
}
func ExampleFields() {
type Access struct {
Name string
LastAccessed time.Time
Number int
}
s := &Access{
Name: "Fatih",
LastAccessed: time.Now(),
Number: 1234567,
}
fields := Fields(s)
for i, field := range fields {
fmt.Printf("[%d] %+v\n", i, field.Name())
}
// Output:
// [0] Name
// [1] LastAccessed
// [2] Number
}
func ExampleFields_nested() {
type Person struct {
Name string
Number int
}
type Access struct {
Person Person
HasPermission bool
LastAccessed time.Time
}
s := &Access{
Person: Person{Name: "fatih", Number: 1234567},
LastAccessed: time.Now(),
HasPermission: true,
}
// Let's get all fields from the struct s.
fields := Fields(s)
for _, field := range fields {
if field.Name() == "Person" {
fmt.Printf("Access.Person.Name: %+v\n", field.Field("Name").Value())
}
}
// Output:
// Access.Person.Name: fatih
}
func ExampleField() {
type Person struct {
Name string
Number int
}
type Access struct {
Person Person
HasPermission bool
LastAccessed time.Time
}
access := &Access{
Person: Person{Name: "fatih", Number: 1234567},
LastAccessed: time.Now(),
HasPermission: true,
}
// Create a new Struct type
s := New(access)
// Get the Field type for "Person" field
p := s.Field("Person")
// Get the underlying "Name field" and print the value of it
name := p.Field("Name")
fmt.Printf("Value of Person.Access.Name: %+v\n", name.Value())
// Output:
// Value of Person.Access.Name: fatih
}
func ExampleIsZero() {
type Server struct {
Name string
ID int32
Enabled bool
}
// Nothing is initalized
a := &Server{}
isZeroA := IsZero(a)
// Name and Enabled is initialized, but not ID
b := &Server{
Name: "Golang",
Enabled: true,
}
isZeroB := IsZero(b)
fmt.Printf("%#v\n", isZeroA)
fmt.Printf("%#v\n", isZeroB)
// Output:
// true
// false
}
func ExampleHasZero() {
// Let's define an Access struct. Note that the "Enabled" field is not
// going to be checked because we added the "structs" tag to the field.
type Access struct {
Name string
LastAccessed time.Time
Number int
Enabled bool `structs:"-"`
}
// Name and Number is not initialized.
a := &Access{
LastAccessed: time.Now(),
}
hasZeroA := HasZero(a)
// Name and Number is initialized.
b := &Access{
Name: "Fatih",
LastAccessed: time.Now(),
Number: 12345,
}
hasZeroB := HasZero(b)
fmt.Printf("%#v\n", hasZeroA)
fmt.Printf("%#v\n", hasZeroB)
// Output:
// true
// false
}

View file

@ -0,0 +1,898 @@
package structs
import (
"fmt"
"reflect"
"testing"
"time"
)
func TestMapNonStruct(t *testing.T) {
foo := []string{"foo"}
defer func() {
err := recover()
if err == nil {
t.Error("Passing a non struct into Map should panic")
}
}()
// this should panic. We are going to recover and and test it
_ = Map(foo)
}
func TestStructIndexes(t *testing.T) {
type C struct {
something int
Props map[string]interface{}
}
defer func() {
err := recover()
if err != nil {
fmt.Printf("err %+v\n", err)
t.Error("Using mixed indexes should not panic")
}
}()
// They should not panic
_ = Map(&C{})
_ = Fields(&C{})
_ = Values(&C{})
_ = IsZero(&C{})
_ = HasZero(&C{})
}
func TestMap(t *testing.T) {
var T = struct {
A string
B int
C bool
}{
A: "a-value",
B: 2,
C: true,
}
a := Map(T)
if typ := reflect.TypeOf(a).Kind(); typ != reflect.Map {
t.Errorf("Map should return a map type, got: %v", typ)
}
// we have three fields
if len(a) != 3 {
t.Errorf("Map should return a map of len 3, got: %d", len(a))
}
inMap := func(val interface{}) bool {
for _, v := range a {
if reflect.DeepEqual(v, val) {
return true
}
}
return false
}
for _, val := range []interface{}{"a-value", 2, true} {
if !inMap(val) {
t.Errorf("Map should have the value %v", val)
}
}
}
func TestMap_Tag(t *testing.T) {
var T = struct {
A string `structs:"x"`
B int `structs:"y"`
C bool `structs:"z"`
}{
A: "a-value",
B: 2,
C: true,
}
a := Map(T)
inMap := func(key interface{}) bool {
for k := range a {
if reflect.DeepEqual(k, key) {
return true
}
}
return false
}
for _, key := range []string{"x", "y", "z"} {
if !inMap(key) {
t.Errorf("Map should have the key %v", key)
}
}
}
func TestMap_CustomTag(t *testing.T) {
var T = struct {
A string `json:"x"`
B int `json:"y"`
C bool `json:"z"`
D struct {
E string `json:"jkl"`
} `json:"nested"`
}{
A: "a-value",
B: 2,
C: true,
}
T.D.E = "e-value"
s := New(T)
s.TagName = "json"
a := s.Map()
inMap := func(key interface{}) bool {
for k := range a {
if reflect.DeepEqual(k, key) {
return true
}
}
return false
}
for _, key := range []string{"x", "y", "z"} {
if !inMap(key) {
t.Errorf("Map should have the key %v", key)
}
}
nested, ok := a["nested"].(map[string]interface{})
if !ok {
t.Fatalf("Map should contain the D field that is tagged as 'nested'")
}
e, ok := nested["jkl"].(string)
if !ok {
t.Fatalf("Map should contain the D.E field that is tagged as 'jkl'")
}
if e != "e-value" {
t.Errorf("D.E field should be equal to 'e-value', got: '%v'", e)
}
}
func TestMap_MultipleCustomTag(t *testing.T) {
var A = struct {
X string `aa:"ax"`
}{"a_value"}
aStruct := New(A)
aStruct.TagName = "aa"
var B = struct {
X string `bb:"bx"`
}{"b_value"}
bStruct := New(B)
bStruct.TagName = "bb"
a, b := aStruct.Map(), bStruct.Map()
if !reflect.DeepEqual(a, map[string]interface{}{"ax": "a_value"}) {
t.Error("Map should have field ax with value a_value")
}
if !reflect.DeepEqual(b, map[string]interface{}{"bx": "b_value"}) {
t.Error("Map should have field bx with value b_value")
}
}
func TestMap_OmitEmpty(t *testing.T) {
type A struct {
Name string
Value string `structs:",omitempty"`
Time time.Time `structs:",omitempty"`
}
a := A{}
m := Map(a)
_, ok := m["Value"].(map[string]interface{})
if ok {
t.Error("Map should not contain the Value field that is tagged as omitempty")
}
_, ok = m["Time"].(map[string]interface{})
if ok {
t.Error("Map should not contain the Time field that is tagged as omitempty")
}
}
func TestMap_OmitNested(t *testing.T) {
type A struct {
Name string
Value string
Time time.Time `structs:",omitnested"`
}
a := A{Time: time.Now()}
type B struct {
Desc string
A A
}
b := &B{A: a}
m := Map(b)
in, ok := m["A"].(map[string]interface{})
if !ok {
t.Error("Map nested structs is not available in the map")
}
// should not happen
if _, ok := in["Time"].(map[string]interface{}); ok {
t.Error("Map nested struct should omit recursiving parsing of Time")
}
if _, ok := in["Time"].(time.Time); !ok {
t.Error("Map nested struct should stop parsing of Time at is current value")
}
}
func TestMap_Nested(t *testing.T) {
type A struct {
Name string
}
a := &A{Name: "example"}
type B struct {
A *A
}
b := &B{A: a}
m := Map(b)
if typ := reflect.TypeOf(m).Kind(); typ != reflect.Map {
t.Errorf("Map should return a map type, got: %v", typ)
}
in, ok := m["A"].(map[string]interface{})
if !ok {
t.Error("Map nested structs is not available in the map")
}
if name := in["Name"].(string); name != "example" {
t.Errorf("Map nested struct's name field should give example, got: %s", name)
}
}
func TestMap_Anonymous(t *testing.T) {
type A struct {
Name string
}
a := &A{Name: "example"}
type B struct {
*A
}
b := &B{}
b.A = a
m := Map(b)
if typ := reflect.TypeOf(m).Kind(); typ != reflect.Map {
t.Errorf("Map should return a map type, got: %v", typ)
}
in, ok := m["A"].(map[string]interface{})
if !ok {
t.Error("Embedded structs is not available in the map")
}
if name := in["Name"].(string); name != "example" {
t.Errorf("Embedded A struct's Name field should give example, got: %s", name)
}
}
func TestStruct(t *testing.T) {
var T = struct{}{}
if !IsStruct(T) {
t.Errorf("T should be a struct, got: %T", T)
}
if !IsStruct(&T) {
t.Errorf("T should be a struct, got: %T", T)
}
}
func TestValues(t *testing.T) {
var T = struct {
A string
B int
C bool
}{
A: "a-value",
B: 2,
C: true,
}
s := Values(T)
if typ := reflect.TypeOf(s).Kind(); typ != reflect.Slice {
t.Errorf("Values should return a slice type, got: %v", typ)
}
inSlice := func(val interface{}) bool {
for _, v := range s {
if reflect.DeepEqual(v, val) {
return true
}
}
return false
}
for _, val := range []interface{}{"a-value", 2, true} {
if !inSlice(val) {
t.Errorf("Values should have the value %v", val)
}
}
}
func TestValues_OmitEmpty(t *testing.T) {
type A struct {
Name string
Value int `structs:",omitempty"`
}
a := A{Name: "example"}
s := Values(a)
if len(s) != 1 {
t.Errorf("Values of omitted empty fields should be not counted")
}
if s[0].(string) != "example" {
t.Errorf("Values of omitted empty fields should left the value example")
}
}
func TestValues_OmitNested(t *testing.T) {
type A struct {
Name string
Value int
}
a := A{
Name: "example",
Value: 123,
}
type B struct {
A A `structs:",omitnested"`
C int
}
b := &B{A: a, C: 123}
s := Values(b)
if len(s) != 2 {
t.Errorf("Values of omitted nested struct should be not counted")
}
inSlice := func(val interface{}) bool {
for _, v := range s {
if reflect.DeepEqual(v, val) {
return true
}
}
return false
}
for _, val := range []interface{}{123, a} {
if !inSlice(val) {
t.Errorf("Values should have the value %v", val)
}
}
}
func TestValues_Nested(t *testing.T) {
type A struct {
Name string
}
a := A{Name: "example"}
type B struct {
A A
C int
}
b := &B{A: a, C: 123}
s := Values(b)
inSlice := func(val interface{}) bool {
for _, v := range s {
if reflect.DeepEqual(v, val) {
return true
}
}
return false
}
for _, val := range []interface{}{"example", 123} {
if !inSlice(val) {
t.Errorf("Values should have the value %v", val)
}
}
}
func TestValues_Anonymous(t *testing.T) {
type A struct {
Name string
}
a := A{Name: "example"}
type B struct {
A
C int
}
b := &B{C: 123}
b.A = a
s := Values(b)
inSlice := func(val interface{}) bool {
for _, v := range s {
if reflect.DeepEqual(v, val) {
return true
}
}
return false
}
for _, val := range []interface{}{"example", 123} {
if !inSlice(val) {
t.Errorf("Values should have the value %v", val)
}
}
}
func TestNames(t *testing.T) {
var T = struct {
A string
B int
C bool
}{
A: "a-value",
B: 2,
C: true,
}
s := Names(T)
if len(s) != 3 {
t.Errorf("Names should return a slice of len 3, got: %d", len(s))
}
inSlice := func(val string) bool {
for _, v := range s {
if reflect.DeepEqual(v, val) {
return true
}
}
return false
}
for _, val := range []string{"A", "B", "C"} {
if !inSlice(val) {
t.Errorf("Names should have the value %v", val)
}
}
}
func TestFields(t *testing.T) {
var T = struct {
A string
B int
C bool
}{
A: "a-value",
B: 2,
C: true,
}
s := Fields(T)
if len(s) != 3 {
t.Errorf("Fields should return a slice of len 3, got: %d", len(s))
}
inSlice := func(val string) bool {
for _, v := range s {
if reflect.DeepEqual(v.Name(), val) {
return true
}
}
return false
}
for _, val := range []string{"A", "B", "C"} {
if !inSlice(val) {
t.Errorf("Fields should have the value %v", val)
}
}
}
func TestFields_OmitNested(t *testing.T) {
type A struct {
Name string
Enabled bool
}
a := A{Name: "example"}
type B struct {
A A
C int
Value string `structs:"-"`
Number int
}
b := &B{A: a, C: 123}
s := Fields(b)
if len(s) != 3 {
t.Errorf("Fields should omit nested struct. Expecting 2 got: %d", len(s))
}
inSlice := func(val interface{}) bool {
for _, v := range s {
if reflect.DeepEqual(v.Name(), val) {
return true
}
}
return false
}
for _, val := range []interface{}{"A", "C"} {
if !inSlice(val) {
t.Errorf("Fields should have the value %v", val)
}
}
}
func TestFields_Anonymous(t *testing.T) {
type A struct {
Name string
}
a := A{Name: "example"}
type B struct {
A
C int
}
b := &B{C: 123}
b.A = a
s := Fields(b)
inSlice := func(val interface{}) bool {
for _, v := range s {
if reflect.DeepEqual(v.Name(), val) {
return true
}
}
return false
}
for _, val := range []interface{}{"A", "C"} {
if !inSlice(val) {
t.Errorf("Fields should have the value %v", val)
}
}
}
func TestIsZero(t *testing.T) {
var T = struct {
A string
B int
C bool `structs:"-"`
D []string
}{}
ok := IsZero(T)
if !ok {
t.Error("IsZero should return true because none of the fields are initialized.")
}
var X = struct {
A string
F *bool
}{
A: "a-value",
}
ok = IsZero(X)
if ok {
t.Error("IsZero should return false because A is initialized")
}
var Y = struct {
A string
B int
}{
A: "a-value",
B: 123,
}
ok = IsZero(Y)
if ok {
t.Error("IsZero should return false because A and B is initialized")
}
}
func TestIsZero_OmitNested(t *testing.T) {
type A struct {
Name string
D string
}
a := A{Name: "example"}
type B struct {
A A `structs:",omitnested"`
C int
}
b := &B{A: a, C: 123}
ok := IsZero(b)
if ok {
t.Error("IsZero should return false because A, B and C are initialized")
}
aZero := A{}
bZero := &B{A: aZero}
ok = IsZero(bZero)
if !ok {
t.Error("IsZero should return true because neither A nor B is initialized")
}
}
func TestIsZero_Nested(t *testing.T) {
type A struct {
Name string
D string
}
a := A{Name: "example"}
type B struct {
A A
C int
}
b := &B{A: a, C: 123}
ok := IsZero(b)
if ok {
t.Error("IsZero should return false because A, B and C are initialized")
}
aZero := A{}
bZero := &B{A: aZero}
ok = IsZero(bZero)
if !ok {
t.Error("IsZero should return true because neither A nor B is initialized")
}
}
func TestIsZero_Anonymous(t *testing.T) {
type A struct {
Name string
D string
}
a := A{Name: "example"}
type B struct {
A
C int
}
b := &B{C: 123}
b.A = a
ok := IsZero(b)
if ok {
t.Error("IsZero should return false because A, B and C are initialized")
}
aZero := A{}
bZero := &B{}
bZero.A = aZero
ok = IsZero(bZero)
if !ok {
t.Error("IsZero should return true because neither A nor B is initialized")
}
}
func TestHasZero(t *testing.T) {
var T = struct {
A string
B int
C bool `structs:"-"`
D []string
}{
A: "a-value",
B: 2,
}
ok := HasZero(T)
if !ok {
t.Error("HasZero should return true because A and B are initialized.")
}
var X = struct {
A string
F *bool
}{
A: "a-value",
}
ok = HasZero(X)
if !ok {
t.Error("HasZero should return true because A is initialized")
}
var Y = struct {
A string
B int
}{
A: "a-value",
B: 123,
}
ok = HasZero(Y)
if ok {
t.Error("HasZero should return false because A and B is initialized")
}
}
func TestHasZero_OmitNested(t *testing.T) {
type A struct {
Name string
D string
}
a := A{Name: "example"}
type B struct {
A A `structs:",omitnested"`
C int
}
b := &B{A: a, C: 123}
// Because the Field A inside B is omitted HasZero should return false
// because it will stop iterating deeper andnot going to lookup for D
ok := HasZero(b)
if ok {
t.Error("HasZero should return false because A and C are initialized")
}
}
func TestHasZero_Nested(t *testing.T) {
type A struct {
Name string
D string
}
a := A{Name: "example"}
type B struct {
A A
C int
}
b := &B{A: a, C: 123}
ok := HasZero(b)
if !ok {
t.Error("HasZero should return true because D is not initialized")
}
}
func TestHasZero_Anonymous(t *testing.T) {
type A struct {
Name string
D string
}
a := A{Name: "example"}
type B struct {
A
C int
}
b := &B{C: 123}
b.A = a
ok := HasZero(b)
if !ok {
t.Error("HasZero should return false because D is not initialized")
}
}
func TestName(t *testing.T) {
type Foo struct {
A string
B bool
}
f := &Foo{}
n := Name(f)
if n != "Foo" {
t.Errorf("Name should return Foo, got: %s", n)
}
unnamed := struct{ Name string }{Name: "Cihangir"}
m := Name(unnamed)
if m != "" {
t.Errorf("Name should return empty string for unnamed struct, got: %s", n)
}
defer func() {
err := recover()
if err == nil {
t.Error("Name should panic if a non struct is passed")
}
}()
Name([]string{})
}
func TestNestedNilPointer(t *testing.T) {
type Collar struct {
Engraving string
}
type Dog struct {
Name string
Collar *Collar
}
type Person struct {
Name string
Dog *Dog
}
person := &Person{
Name: "John",
}
personWithDog := &Person{
Name: "Ron",
Dog: &Dog{
Name: "Rover",
},
}
personWithDogWithCollar := &Person{
Name: "Kon",
Dog: &Dog{
Name: "Ruffles",
Collar: &Collar{
Engraving: "If lost, call Kon",
},
},
}
defer func() {
err := recover()
if err != nil {
fmt.Printf("err %+v\n", err)
t.Error("Internal nil pointer should not panic")
}
}()
_ = Map(person) // Panics
_ = Map(personWithDog) // Panics
_ = Map(personWithDogWithCollar) // Doesn't panic
}

32
Godeps/_workspace/src/github.com/fatih/structs/tags.go generated vendored Normal file
View file

@ -0,0 +1,32 @@
package structs
import "strings"
// tagOptions contains a slice of tag options
type tagOptions []string
// Has returns true if the given optiton is available in tagOptions
func (t tagOptions) Has(opt string) bool {
for _, tagOpt := range t {
if tagOpt == opt {
return true
}
}
return false
}
// parseTag splits a struct field's tag into its name and a list of options
// which comes after a name. A tag is in the form of: "name,option1,option2".
// The name can be neglectected.
func parseTag(tag string) (string, tagOptions) {
// tag is one of followings:
// ""
// "name"
// "name,opt"
// "name,opt,opt2"
// ",opt"
res := strings.Split(tag, ",")
return res[0], res[1:]
}

View file

@ -0,0 +1,46 @@
package structs
import "testing"
func TestParseTag_Name(t *testing.T) {
tags := []struct {
tag string
has bool
}{
{"", false},
{"name", true},
{"name,opt", true},
{"name , opt, opt2", false}, // has a single whitespace
{", opt, opt2", false},
}
for _, tag := range tags {
name, _ := parseTag(tag.tag)
if (name != "name") && tag.has {
t.Errorf("Parse tag should return name: %#v", tag)
}
}
}
func TestParseTag_Opts(t *testing.T) {
tags := []struct {
opts string
has bool
}{
{"name", false},
{"name,opt", true},
{"name , opt, opt2", false}, // has a single whitespace
{",opt, opt2", true},
{", opt3, opt4", false},
}
// search for "opt"
for _, tag := range tags {
_, opts := parseTag(tag.opts)
if opts.Has("opt") != tag.has {
t.Errorf("Tag opts should have opt: %#v", tag)
}
}
}

View file

@ -0,0 +1,74 @@
package pki
import (
"strings"
"sync"
"time"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// Factory creates a new backend implementing the logical.Backend interface
func Factory(map[string]string) (logical.Backend, error) {
return Backend(), nil
}
// Backend returns a new Backend framework struct
func Backend() *framework.Backend {
var b backend
b.Backend = &framework.Backend{
Help: strings.TrimSpace(backendHelp),
PathsSpecial: &logical.Paths{
Root: []string{
"config/*",
"revoke/*",
"crl/rotate",
},
Unauthenticated: []string{
"cert/*",
"ca/pem",
"ca",
"crl/pem",
"crl",
},
},
Paths: []*framework.Path{
pathRoles(&b),
pathConfigCA(&b),
pathConfigCRL(&b),
pathIssue(&b),
pathRotateCRL(&b),
pathFetchCA(&b),
pathFetchCRL(&b),
pathFetchCRLViaCertPath(&b),
pathFetchValid(&b),
pathRevoke(&b),
},
Secrets: []*framework.Secret{
secretCerts(&b),
},
}
b.crlLifetime = time.Hour * 72
b.revokeStorageLock = &sync.Mutex{}
return b.Backend
}
type backend struct {
*framework.Backend
crlLifetime time.Duration
revokeStorageLock *sync.Mutex
}
const backendHelp = `
The PKI backend dynamically generates X509 server and client certificates.
After mounting this backend, configure the CA using the "pem_bundle" endpoint within
the "config/" path.
`

View file

@ -0,0 +1,448 @@
package pki
import (
"crypto/x509"
"encoding/pem"
"fmt"
"math"
"math/rand"
"os"
"testing"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
logicaltest "github.com/hashicorp/vault/logical/testing"
"github.com/mitchellh/mapstructure"
)
var (
stepCount = 0
)
// Performs basic tests on CA functionality
func TestBackend_basic(t *testing.T) {
b := Backend()
testCase := logicaltest.TestCase{
Backend: b,
Steps: []logicaltest.TestStep{},
}
stepCount += len(testCase.Steps)
testCase.Steps = append(testCase.Steps, generateCASteps(t)...)
logicaltest.Test(t, testCase)
}
// Generates and tests steps that walk through the various possibilities
// of role flags to ensure that they are properly restricted
func TestBackend_roles(t *testing.T) {
b := Backend()
testCase := logicaltest.TestCase{
Backend: b,
Steps: []logicaltest.TestStep{},
}
testCase.Steps = append(testCase.Steps, generateCASteps(t)...)
testCase.Steps = append(testCase.Steps, generateRoleSteps(t)...)
if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 {
for i, v := range testCase.Steps {
fmt.Printf("Step %d:\n%+v\n\n", i+stepCount, v)
}
}
stepCount += len(testCase.Steps)
logicaltest.Test(t, testCase)
}
// Performs some validity checking on the returned bundles
func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Duration, certBundle *certutil.CertBundle) (*certutil.ParsedCertBundle, error) {
parsedCertBundle, err := certBundle.ToParsedCertBundle()
if err != nil {
return nil, fmt.Errorf("Error parsing cert bundle: %s", err)
}
switch {
case parsedCertBundle.Certificate == nil:
return nil, fmt.Errorf("Did not find a certificate in the cert bundle")
case parsedCertBundle.IssuingCA == nil:
return nil, fmt.Errorf("Did not find a CA in the cert bundle")
case parsedCertBundle.PrivateKey == nil:
return nil, fmt.Errorf("Did not find a private key in the cert bundle")
case parsedCertBundle.PrivateKeyType == certutil.UnknownPrivateKey:
return nil, fmt.Errorf("Could not figure out type of private key")
}
switch {
case parsedCertBundle.PrivateKeyType == certutil.RSAPrivateKey && keyType != "rsa":
fallthrough
case parsedCertBundle.PrivateKeyType == certutil.ECPrivateKey && keyType != "ec":
return nil, fmt.Errorf("Given key type does not match type found in bundle")
}
cert := parsedCertBundle.Certificate
// There should only be one usage type, because only one is requested
// in the tests
if len(cert.ExtKeyUsage) != 1 {
return nil, fmt.Errorf("Got wrong size key usage in generated cert")
}
switch usage {
case serverUsage:
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth {
return nil, fmt.Errorf("Bad key usage")
}
case clientUsage:
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth {
return nil, fmt.Errorf("Bad key usage")
}
case codeSigningUsage:
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning {
return nil, fmt.Errorf("Bad key usage")
}
}
if math.Abs(float64(time.Now().Unix()-cert.NotBefore.Unix())) > 10 {
return nil, fmt.Errorf("Validity period starts out of range")
}
if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 10 {
return nil, fmt.Errorf("Validity period too large")
}
return parsedCertBundle, nil
}
// Generates steps to test out CA configuration -- certificates + CRL expiry,
// and ensure that the certificates are readable after storing them
func generateCASteps(t *testing.T) []logicaltest.TestStep {
ret := []logicaltest.TestStep{
logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "config/ca",
Data: map[string]interface{}{
"pem_bundle": caKey + caCert,
},
},
logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "config/crl",
Data: map[string]interface{}{
"expiry": "16h",
},
},
// Ensure we can fetch it back via unauthenticated means, in various formats
logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "cert/ca",
Unauthenticated: true,
Check: func(resp *logical.Response) error {
if resp.Data["certificate"].(string) != caCert {
return fmt.Errorf("CA certificate:\n%s\ndoes not match original:\n%s\n", resp.Data["certificate"].(string), caCert)
}
return nil
},
},
logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "ca/pem",
Unauthenticated: true,
Check: func(resp *logical.Response) error {
rawBytes := resp.Data["http_raw_body"].([]byte)
if string(rawBytes) != caCert {
return fmt.Errorf("CA certificate:\n%s\ndoes not match original:\n%s\n", string(rawBytes), caCert)
}
if resp.Data["http_content_type"].(string) != "application/pkix-cert" {
return fmt.Errorf("Expected application/pkix-cert as content-type, but got %s", resp.Data["http_content_type"].(string))
}
return nil
},
},
logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "ca",
Unauthenticated: true,
Check: func(resp *logical.Response) error {
rawBytes := resp.Data["http_raw_body"].([]byte)
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: rawBytes,
})
if string(pemBytes) != caCert {
return fmt.Errorf("CA certificate:\n%s\ndoes not match original:\n%s\n", string(pemBytes), caCert)
}
if resp.Data["http_content_type"].(string) != "application/pkix-cert" {
return fmt.Errorf("Expected application/pkix-cert as content-type, but got %s", resp.Data["http_content_type"].(string))
}
return nil
},
},
logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "config/crl",
Check: func(resp *logical.Response) error {
if resp.Data["expiry"].(string) != "16h" {
return fmt.Errorf("CRL lifetimes do not match (got %s)", resp.Data["expiry"].(string))
}
return nil
},
},
}
return ret
}
// Generates steps to test out various role permutations
func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
roleVals := roleEntry{
LeaseMax: "12h",
}
issueVals := certutil.IssueData{}
ret := []logicaltest.TestStep{}
roleTestStep := logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "roles/test",
}
issueTestStep := logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "issue/test",
}
genericErrorOkCheck := func(resp *logical.Response) error {
if resp.IsError() {
return nil
}
return fmt.Errorf("Expected an error, but did not seem to get one")
}
// Adds tests with the currently configured issue/role information
addTests := func(testCheck logicaltest.TestCheckFunc) {
//fmt.Printf("role vals: %#v\n", roleVals)
//fmt.Printf("issue vals: %#v\n", issueTestStep)
roleTestStep.Data = structs.New(roleVals).Map()
ret = append(ret, roleTestStep)
issueTestStep.Data = structs.New(issueVals).Map()
switch {
case issueTestStep.ErrorOk:
issueTestStep.Check = genericErrorOkCheck
case testCheck != nil:
issueTestStep.Check = testCheck
default:
issueTestStep.Check = nil
}
ret = append(ret, issueTestStep)
}
// Returns a TestCheckFunc that performs various validity checks on the
// returned certificate information, mostly within checkCertsAndPrivateKey
getCnCheck := func(name, keyType string, usage certUsage, validity time.Duration) logicaltest.TestCheckFunc {
var certBundle certutil.CertBundle
return func(resp *logical.Response) error {
err := mapstructure.Decode(resp.Data, &certBundle)
if err != nil {
return err
}
parsedCertBundle, err := checkCertsAndPrivateKey(keyType, usage, validity, &certBundle)
if err != nil {
return fmt.Errorf("Error checking generated certificate: %s", err)
}
cert := parsedCertBundle.Certificate
if cert.Subject.CommonName != name {
return fmt.Errorf("Error: returned certificate has CN of %s but %s was requested", cert.Subject.CommonName, name)
}
if len(cert.DNSNames) != 1 {
return fmt.Errorf("Error: found more than one DNS SAN but only one was requested")
}
if cert.DNSNames[0] != name {
return fmt.Errorf("Error: returned certificate has a DNS SAN of %s but %s was requested", cert.DNSNames[0], name)
}
return nil
}
}
// Common names to test with the various role flags toggled
var commonNames struct {
Localhost bool `structs:"localhost"`
BaseDomain bool `structs:"foo.example.com"`
Wildcard bool `structs:"*.example.com"`
Subdomain bool `structs:"foo.bar.example.com"`
SubdomainWildcard bool `structs:"*.bar.example.com"`
AnyHost bool `structs:"porkslap.beer"`
}
// Adds a series of tests based on the current selection of
// allowed common names; contains some (seeded) randomness
//
// This allows for a variety of common names to be tested in various
// combinations with allowed toggles of the role
addCnTests := func() {
cnMap := structs.New(commonNames).Map()
// For the number of tests being run, this is known to hit all
// of the various values below
mathRand := rand.New(rand.NewSource(1))
for name, allowedInt := range cnMap {
roleVals.KeyType = "rsa"
roleVals.KeyBits = 2048
if mathRand.Int()%2 == 1 {
roleVals.KeyType = "ec"
roleVals.KeyBits = 224
}
roleVals.ServerFlag = false
roleVals.ClientFlag = false
roleVals.CodeSigningFlag = false
var usage certUsage
i := mathRand.Int()
switch {
case i%3 == 0:
usage = serverUsage
roleVals.ServerFlag = true
case i%2 == 0:
usage = clientUsage
roleVals.ClientFlag = true
default:
usage = codeSigningUsage
roleVals.CodeSigningFlag = true
}
allowed := allowedInt.(bool)
issueVals.CommonName = name
if allowed {
issueTestStep.ErrorOk = false
} else {
issueTestStep.ErrorOk = true
}
validity, _ := time.ParseDuration(roleVals.LeaseMax)
addTests(getCnCheck(name, roleVals.KeyType, usage, validity))
}
}
// Common Name tests
{
// common_name not provided
issueVals.CommonName = ""
issueTestStep.ErrorOk = true
addTests(nil)
// Nothing is allowed
addCnTests()
roleVals.AllowLocalhost = true
commonNames.Localhost = true
addCnTests()
roleVals.AllowedBaseDomain = "foobar.com"
addCnTests()
roleVals.AllowedBaseDomain = "example.com"
commonNames.BaseDomain = true
commonNames.Wildcard = true
addCnTests()
roleVals.AllowSubdomains = true
commonNames.Subdomain = true
commonNames.SubdomainWildcard = true
addCnTests()
roleVals.AllowAnyName = true
commonNames.AnyHost = true
addCnTests()
}
// IP SAN tests
{
issueVals.IPSANs = "127.0.0.1,::1"
issueTestStep.ErrorOk = true
addTests(nil)
roleVals.AllowIPSANs = true
issueTestStep.ErrorOk = false
addTests(nil)
issueVals.IPSANs = "foobar"
issueTestStep.ErrorOk = true
addTests(nil)
issueTestStep.ErrorOk = false
issueVals.IPSANs = ""
}
// Lease tests
{
roleTestStep.ErrorOk = true
roleVals.Lease = ""
roleVals.LeaseMax = ""
addTests(nil)
roleVals.Lease = "12h"
roleVals.LeaseMax = "6h"
addTests(nil)
roleTestStep.ErrorOk = false
}
return ret
}
const (
caKey string = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA1eKB2nFbRqTFs7KyZjbzB5VRCBbnLZfEXVP1c3bHe+YGjlfl
34cy52dmancUzOf1/Jfo+VglocjTLVy5wHSGJwQYs8b6pEuuvAVo/6wUL5Z7ZlQD
R4kDe5Q+xgoRT6Bi/Bs57E+fNYgyUq/YAUY5WLuC+ZliCbJkLnb15ItuP1yVUTDX
TYORRE3qJS5RRol8D3QvteG9LyPEc7C+jsm5iBCagyxluzU0dnEOib5q7xwZncoM
bQz+rZH3QnwOij41FOGRPazrD5Mv6xLBkFnE5VAJ+GIgvd4bpOwvYMuofvF4PS7S
FzxkGssMLlICap6PFpKz86DpAoDxPuoZeOhU4QIDAQABAoIBAQCp6VIdFdZcDYPd
WIVuvBJfINiJo6AtURa2yX8BJggdPkRRCjTcWUwwFq1+wHDuwwtgidGTW9oxZxeU
Psh1wlvcXN2+28C7ikAar/WUvsAeed44EV+1kXwJzV/89XyBFDnuazadqzcgUL0h
gP4JLR9bhULsRFRkvanmW6zFzZpcjBzi/UoFuWkFRRqZ0euM2Lpz8L75PFfW9s9M
kNglZpcV6ZmvR9c1JkEMUs/mrB8ZgCd1uvmcVosQ+u7sE8Yk/xAurHXuNJQlGXx4
azrLW0XY1CLO2Tm4l4MwPjmhH0WytXNjOSKycBCXVnBIfZsI128DsP5YyA/fW9qA
BAqFSzABAoGBAPcBNk9sf3cnZ5w6qwlE2ysDwGIGR+I1fb09YjRI6vjwwdWZgGR0
EE4UB1Pp+KIehXaTJHcEgvBBErM2NLS4qKzh25O30C2EwK6o//3jEAribuYutBhJ
ihu1qKzqcPbKClG+34kjX6nmtux2wlYM05f5v3ALki5Is7W/RrfceBuBAoGBAN2s
hdt4TcgIcZymPG2931qCBGF3E8AaA8bUl9TKaZHuFikOMFKA/KM5O5mznPGnQP2d
kXYKXuqdYhVLwp32FTbIbozGZZ8XliO5oS7J3vIID+sLWQhrvyFO7d0lpSjv41HH
yJ2DrykHRg8hxsbh2D4By7olBx6Q2m+B8lPzHmlhAoGACHUeKvIIG0haH9tSZ+rX
pk1mlPSqGXDDcWtcpXWptgRoXqv23Xmr5UCCT7k/Li3lW/4FzZ117kwMG97LRzTb
ca/6GMC+fBCDmHdo7ISN1BGUwoTu3bYG6JP7xo/wdkLMv6fNd6CicerYcJhQZynh
RN7kUy3SP4t1u89k2H7QDgECgYBpU0bKr8+tQq3Qs3+02OmeFHbGZJDCztmKiIqX
tZERoGFxIme9W8IuP8xczGW+wCx2FH7/6g+NRDhNTBDtgvYzcGpugvnX7JoO4W1/
ULWYpFID6QFlqeRHjDwivndKCykkO1vL07zPLsCQAglzh+16ENpe2KcYU9Ul9EVS
tAp4IQKBgQDrb/NpiVx7NI6PyTCm6ctuUAYm3ihAiQNV4Bmr0liPDp9PozbqkhcF
udNtivO4LlRb/PJ+DK6afDyH8aJQdDqe3NpDvyrmKiMSYOY3iVFvan4tbIiofxdQ
flwiZUzox814fzXbxheO9Cs6pXz7PUBVU4fN0Y/hXJCfRO4Ns9152A==
-----END RSA PRIVATE KEY-----
`
caCert string = `-----BEGIN CERTIFICATE-----
MIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjAxMjA1MTUzWhcNMjUwNTI5MjA1
MTUzWjAbMRkwFwYDVQQDDBBWYXVsdCBUZXN0aW5nIENBMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEA1eKB2nFbRqTFs7KyZjbzB5VRCBbnLZfEXVP1c3bH
e+YGjlfl34cy52dmancUzOf1/Jfo+VglocjTLVy5wHSGJwQYs8b6pEuuvAVo/6wU
L5Z7ZlQDR4kDe5Q+xgoRT6Bi/Bs57E+fNYgyUq/YAUY5WLuC+ZliCbJkLnb15Itu
P1yVUTDXTYORRE3qJS5RRol8D3QvteG9LyPEc7C+jsm5iBCagyxluzU0dnEOib5q
7xwZncoMbQz+rZH3QnwOij41FOGRPazrD5Mv6xLBkFnE5VAJ+GIgvd4bpOwvYMuo
fvF4PS7SFzxkGssMLlICap6PFpKz86DpAoDxPuoZeOhU4QIDAQABo4GXMIGUMB0G
A1UdDgQWBBTknN5eFxxo5aTlfq+G4ZXs3AsxWTAfBgNVHSMEGDAWgBTknN5eFxxo
5aTlfq+G4ZXs3AsxWTAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vbG9jYWxob3N0
OjgyMDAvdjEvcGtpL2NybDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
BjANBgkqhkiG9w0BAQsFAAOCAQEAsINcA4PZm+OyldgNrwRVgxoSrhV1I9zszhc9
VV340ZWlpTTxFKVb/K5Hg+jMF9tv70X1HwlYdlutE6KdrsA3gks5zanh4/3zlrYk
ABNBmSD6SSU2HKX1bFCBAAS3YHONE5o1K5tzwLsMl5uilNf+Wid3NjFnQ4KfuYI5
loN/opnM6+a/O3Zua8RAuMMAv9wyqwn88aVuLvVzDNSMe5qC5kkuLGmRkNgY06rI
S/fXIHIOldeQxgYCqhdVmcDWJ1PtVaDfBsKVpRg1GRU8LUGw2E4AY+twd+J2FBfa
G/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==
-----END CERTIFICATE-----
`
)

View file

@ -0,0 +1,270 @@
package pki
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net"
"regexp"
"strings"
"time"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
)
type certUsage int
const (
serverUsage certUsage = 1 << iota
clientUsage
codeSigningUsage
)
type certCreationBundle struct {
SigningBundle *certutil.ParsedCertBundle
CACert *x509.Certificate
CommonNames []string
IPSANs []net.IP
KeyType string
KeyBits int
Lease time.Duration
Usage certUsage
}
// Fetches the CA info. Unlike other certificates, the CA info is stored
// in the backend as a CertBundle, because we are storing its private key
func fetchCAInfo(req *logical.Request) (*certutil.ParsedCertBundle, error) {
bundleEntry, err := req.Storage.Get("config/ca_bundle")
if err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to fetch local CA certificate/key: %s", err)}
}
if bundleEntry == nil {
return nil, certutil.UserError{Err: fmt.Sprintf("Backend must be configured with a CA certificate/key")}
}
var bundle certutil.CertBundle
if err := bundleEntry.DecodeJSON(&bundle); err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to decode local CA certificate/key: %s", err)}
}
parsedBundle, err := bundle.ToParsedCertBundle()
if err != nil {
return nil, certutil.InternalError{Err: err.Error()}
}
if parsedBundle.Certificate == nil {
return nil, certutil.InternalError{Err: "Stored CA information not able to be parsed"}
}
return parsedBundle, nil
}
// Allows fetching certificates from the backend; it handles the slightly
// separate pathing for CA, CRL, and revoked certificates.
func fetchCertBySerial(req *logical.Request, prefix, serial string) (*logical.StorageEntry, error) {
var path string
switch {
case serial == "ca":
path = "ca"
case serial == "crl":
path = "crl"
case strings.HasPrefix(prefix, "revoked/"):
path = "revoked/" + strings.Replace(strings.ToLower(serial), "-", ":", -1)
default:
path = "certs/" + strings.Replace(strings.ToLower(serial), "-", ":", -1)
}
certEntry, err := req.Storage.Get(path)
if err != nil || certEntry == nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Certificate with serial number %s not found", serial)}
}
if certEntry.Value == nil || len(certEntry.Value) == 0 {
return nil, certutil.InternalError{Err: fmt.Sprintf("Returned certificate bytes for serial %s were empty", serial)}
}
return certEntry, nil
}
// Given a set of requested names for a certificate, verifies that all of them
// match the various toggles set in the role for controlling issuance.
// If one does not pass, it is returned in the string argument.
func validateCommonNames(req *logical.Request, commonNames []string, role *roleEntry) (string, error) {
hostnameRegex, err := regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
if err != nil {
return "", fmt.Errorf("Error compiling hostname regex: %s", err)
}
subdomainRegex, err := regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))*$`)
if err != nil {
return "", fmt.Errorf("Error compiling subdomain regex: %s", err)
}
for _, name := range commonNames {
if role.AllowLocalhost && name == "localhost" {
continue
}
sanitizedName := name
isWildcard := false
if strings.HasPrefix(name, "*.") {
sanitizedName = name[2:]
isWildcard = true
}
if !hostnameRegex.MatchString(sanitizedName) {
return name, nil
}
if role.AllowAnyName {
continue
}
if role.AllowTokenDisplayName {
if name == req.DisplayName {
continue
}
if role.AllowSubdomains {
if strings.HasSuffix(name, "."+req.DisplayName) {
continue
}
}
}
if len(role.AllowedBaseDomain) != 0 {
if strings.HasSuffix(name, "."+role.AllowedBaseDomain) {
if role.AllowSubdomains {
continue
}
if subdomainRegex.MatchString(strings.TrimSuffix(name, "."+role.AllowedBaseDomain)) {
continue
}
if isWildcard && role.AllowedBaseDomain == sanitizedName {
continue
}
}
}
return name, nil
}
return "", nil
}
// Performs the heavy lifting of creating a certificate. Returns
// a fully-filled-in ParsedCertBundle.
func createCertificate(creationInfo *certCreationBundle) (*certutil.ParsedCertBundle, error) {
var clientPrivKey crypto.Signer
var err error
result := &certutil.ParsedCertBundle{}
var serialNumber *big.Int
serialNumber, err = rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil))
if err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Error getting random serial number")}
}
switch creationInfo.KeyType {
case "rsa":
result.PrivateKeyType = certutil.RSAPrivateKey
clientPrivKey, err = rsa.GenerateKey(rand.Reader, creationInfo.KeyBits)
if err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Error generating RSA private key")}
}
result.PrivateKey = clientPrivKey
result.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey))
case "ec":
result.PrivateKeyType = certutil.ECPrivateKey
var curve elliptic.Curve
switch creationInfo.KeyBits {
case 224:
curve = elliptic.P224()
case 256:
curve = elliptic.P256()
case 384:
curve = elliptic.P384()
case 521:
curve = elliptic.P521()
default:
return nil, certutil.UserError{Err: fmt.Sprintf("Unsupported bit length for EC key: %d", creationInfo.KeyBits)}
}
clientPrivKey, err = ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Error generating EC private key")}
}
result.PrivateKey = clientPrivKey
result.PrivateKeyBytes, err = x509.MarshalECPrivateKey(clientPrivKey.(*ecdsa.PrivateKey))
if err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Error marshalling EC private key")}
}
default:
return nil, certutil.UserError{Err: fmt.Sprintf("Unknown key type: %s", creationInfo.KeyType)}
}
subjKeyID, err := certutil.GetSubjKeyID(result.PrivateKey)
if err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Error getting subject key ID: %s", err)}
}
subject := pkix.Name{
Country: creationInfo.CACert.Subject.Country,
Organization: creationInfo.CACert.Subject.Organization,
OrganizationalUnit: creationInfo.CACert.Subject.OrganizationalUnit,
Locality: creationInfo.CACert.Subject.Locality,
Province: creationInfo.CACert.Subject.Province,
StreetAddress: creationInfo.CACert.Subject.StreetAddress,
PostalCode: creationInfo.CACert.Subject.PostalCode,
SerialNumber: serialNumber.String(),
CommonName: creationInfo.CommonNames[0],
}
certTemplate := &x509.Certificate{
SignatureAlgorithm: x509.SHA256WithRSA,
SerialNumber: serialNumber,
Subject: subject,
NotBefore: time.Now(),
NotAfter: time.Now().Add(creationInfo.Lease),
KeyUsage: x509.KeyUsage(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement),
BasicConstraintsValid: true,
IsCA: false,
SubjectKeyId: subjKeyID,
DNSNames: creationInfo.CommonNames,
IPAddresses: creationInfo.IPSANs,
PermittedDNSDomainsCritical: false,
PermittedDNSDomains: nil,
CRLDistributionPoints: creationInfo.CACert.CRLDistributionPoints,
}
if creationInfo.Usage&serverUsage != 0 {
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
}
if creationInfo.Usage&clientUsage != 0 {
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
}
if creationInfo.Usage&codeSigningUsage != 0 {
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageCodeSigning)
}
cert, err := x509.CreateCertificate(rand.Reader, certTemplate, creationInfo.CACert, clientPrivKey.Public(), creationInfo.SigningBundle.PrivateKey)
if err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to create certificate: %s", err)}
}
result.CertificateBytes = cert
result.Certificate, err = x509.ParseCertificate(cert)
if err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to parse created certificate: %s", err)}
}
result.IssuingCABytes = creationInfo.SigningBundle.CertificateBytes
result.IssuingCA = creationInfo.SigningBundle.Certificate
return result, nil
}

View file

@ -0,0 +1,195 @@
package pki
import (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"time"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
)
type revocationInfo struct {
CertificateBytes []byte `json:"certificate_bytes"`
RevocationTime int64 `json:"revocation_time"`
}
// Revokes a cert, and tries to be smart about error recovery
func revokeCert(b *backend, req *logical.Request, serial string) (*logical.Response, error) {
alreadyRevoked := false
var revInfo revocationInfo
certEntry, err := fetchCertBySerial(req, "revoked/", serial)
// Don't check error because it's expected that it may fail here;
// just check for existence
if certEntry != nil {
// Verify that it is also deleted from certs/
// in case of partial failure from an earlier run.
certEntry, _ = fetchCertBySerial(req, "certs/", serial)
if certEntry == nil {
// Everything seems sane, so don't rebuild the CRL
return nil, nil
}
// Still exists in certs/; set the revocation info, below it will
// be removed from certs/ and the CRL rotated
alreadyRevoked = true
revEntry, err := req.Storage.Get("revoked/" + serial)
if revEntry == nil || err != nil {
return nil, fmt.Errorf("Error getting existing revocation info")
}
err = revEntry.DecodeJSON(&revInfo)
if err != nil {
return nil, fmt.Errorf("Error decoding existing revocation info")
}
}
if !alreadyRevoked {
certEntry, err = fetchCertBySerial(req, "certs/", serial)
switch err.(type) {
case certutil.UserError:
return logical.ErrorResponse(err.Error()), nil
case certutil.InternalError:
return nil, err
}
cert, err := x509.ParseCertificate(certEntry.Value)
if err != nil {
return nil, fmt.Errorf("Error parsing certificate")
}
if cert == nil {
return nil, fmt.Errorf("Got a nil certificate")
}
if cert.NotAfter.Before(time.Now()) {
return nil, nil
}
revInfo.CertificateBytes = certEntry.Value
revInfo.RevocationTime = time.Now().Unix()
certEntry, err = logical.StorageEntryJSON("revoked/"+serial, revInfo)
if err != nil {
return nil, fmt.Errorf("Error creating revocation entry")
}
err = req.Storage.Put(certEntry)
if err != nil {
return nil, fmt.Errorf("Error saving revoked certificate to new location")
}
}
crlErr := buildCRL(b, req)
switch crlErr.(type) {
case certutil.UserError:
return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil
case certutil.InternalError:
return nil, fmt.Errorf("Error encountered during CRL building: %s", crlErr)
}
err = req.Storage.Delete("certs/" + serial)
if err != nil {
return nil, fmt.Errorf("Error deleting cert from valid-certs location")
}
return &logical.Response{
Data: map[string]interface{}{
"revocation_time": revInfo.RevocationTime,
},
}, nil
}
// Builds a CRL by going through the list of revoked certificates and building
// a new CRL with the stored revocation times and serial numbers.
//
// If a certificate has already expired, it will be removed entirely rather than
// become part of the new CRL.
func buildCRL(b *backend, req *logical.Request) error {
revokedSerials, err := req.Storage.List("revoked/")
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Error fetching list of revoked certs: %s", err)}
}
revokedCerts := []pkix.RevokedCertificate{}
var revInfo revocationInfo
for _, serial := range revokedSerials {
revokedEntry, err := req.Storage.Get("revoked/" + serial)
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Unable to fetch revoked cert with serial %s: %s", serial, err)}
}
if revokedEntry == nil {
return certutil.InternalError{Err: fmt.Sprintf("Revoked certificate entry for serial %s is nil", serial)}
}
if revokedEntry.Value == nil || len(revokedEntry.Value) == 0 {
// TODO: In this case, remove it and continue? How likely is this to
// happen? Alternately, could skip it entirely, or could implement a
// delete function so that there is a way to remove these
return certutil.InternalError{Err: fmt.Sprintf("Found revoked serial but actual certificate is empty")}
}
err = revokedEntry.DecodeJSON(&revInfo)
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Error decoding revocation entry for serial %s: %s", serial, err)}
}
revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes)
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Unable to parse stored revoked certificate with serial %s: %s", serial, err)}
}
if revokedCert.NotAfter.Before(time.Now()) {
err = req.Storage.Delete(serial)
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Unable to delete revoked, expired certificate with serial %s: %s", serial, err)}
}
continue
}
revokedCerts = append(revokedCerts, pkix.RevokedCertificate{
SerialNumber: revokedCert.SerialNumber,
RevocationTime: time.Unix(revInfo.RevocationTime, 0),
})
}
signingBundle, caErr := fetchCAInfo(req)
switch caErr.(type) {
case certutil.UserError:
return certutil.UserError{Err: fmt.Sprintf("Could not fetch the CA certificate: %s", caErr)}
case certutil.InternalError:
return certutil.InternalError{Err: fmt.Sprintf("Error fetching CA certificate: %s", caErr)}
}
crlLifetime := b.crlLifetime
crlInfo, err := b.CRL(req.Storage)
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Error fetching CRL config information: %s", err)}
}
if crlInfo != nil {
crlDur, err := time.ParseDuration(crlInfo.Expiry)
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Error parsing CRL duration of %s", crlInfo.Expiry)}
}
crlLifetime = crlDur
}
crlBytes, err := signingBundle.Certificate.CreateCRL(rand.Reader, signingBundle.PrivateKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime))
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Error creating new CRL: %s", err)}
}
err = req.Storage.Put(&logical.StorageEntry{
Key: "crl",
Value: crlBytes,
})
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Error storing CRL: %s", err)}
}
return nil
}

View file

@ -0,0 +1,106 @@
package pki
import (
"fmt"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathConfigCA(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/ca",
Fields: map[string]*framework.FieldSchema{
"pem_bundle": &framework.FieldSchema{
Type: framework.TypeString,
Description: `PEM-format, concatenated unencrypted secret key
and certificate`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: b.pathCAWrite,
},
HelpSynopsis: pathConfigCAHelpSyn,
HelpDescription: pathConfigCAHelpDesc,
}
}
func (b *backend) pathCAWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
pemBundle := d.Get("pem_bundle").(string)
parsedBundle, err := certutil.ParsePEMBundle(pemBundle)
if err != nil {
switch err.(type) {
case certutil.InternalError:
return nil, err
default:
return logical.ErrorResponse(err.Error()), nil
}
}
// Handle the case of a self-signed certificate
if parsedBundle.Certificate == nil && parsedBundle.IssuingCA != nil {
parsedBundle.Certificate = parsedBundle.IssuingCA
parsedBundle.CertificateBytes = parsedBundle.IssuingCABytes
}
// TODO?: CRLs can only be generated with RSA keys right now, in the
// Go standard library. The plubming is here to support non-RSA keys
// if the library gets support
if parsedBundle.PrivateKeyType != certutil.RSAPrivateKey {
return logical.ErrorResponse("Currently, only RSA keys are supported for the CA certificate"), nil
}
if !parsedBundle.Certificate.IsCA {
return logical.ErrorResponse("The given certificate is not marked for CA use and cannot be used with this backend"), nil
}
cb, err := parsedBundle.ToCertBundle()
if err != nil {
return nil, fmt.Errorf("Error converting raw values into cert bundle: %s", err)
}
entry, err := logical.StorageEntryJSON("config/ca_bundle", cb)
if err != nil {
return nil, err
}
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
// For ease of later use, also store just the certificate at a known
// location, plus a blank CRL
entry.Key = "ca"
entry.Value = parsedBundle.CertificateBytes
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
entry.Key = "crl"
entry.Value = []byte{}
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
return nil, nil
}
const pathConfigCAHelpSyn = `
Configure the CA certificate and private key used for generated credentials.
`
const pathConfigCAHelpDesc = `
This configures the CA information used for credentials
generated by this backend. This must be a PEM-format, concatenated
unencrypted secret key and certificate.
For security reasons, you can only view the certificate when reading this endpoint.
`

View file

@ -0,0 +1,102 @@
package pki
import (
"fmt"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// CRLConfig holds basic CRL configuration information
type crlConfig struct {
Expiry string `json:"expiry" mapstructure:"expiry" structs:"expiry"`
}
func pathConfigCRL(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/crl",
Fields: map[string]*framework.FieldSchema{
"expiry": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The amount of time the generated CRL should be
valid; defaults to 72 hours`,
Default: "72h",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathCRLRead,
logical.WriteOperation: b.pathCRLWrite,
},
HelpSynopsis: pathConfigCRLHelpSyn,
HelpDescription: pathConfigCRLHelpDesc,
}
}
func (b *backend) CRL(s logical.Storage) (*crlConfig, error) {
entry, err := s.Get("config/crl")
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result crlConfig
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathCRLRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := b.CRL(req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return nil, nil
}
return &logical.Response{
Data: structs.New(config).Map(),
}, nil
}
func (b *backend) pathCRLWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
expiry := d.Get("expiry").(string)
_, err := time.ParseDuration(expiry)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Given expiry could not be decoded: %s", err)), nil
}
config := &crlConfig{
Expiry: expiry,
}
entry, err := logical.StorageEntryJSON("config/crl", config)
if err != nil {
return nil, err
}
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
return nil, nil
}
const pathConfigCRLHelpSyn = `
Configure the CRL expiration.
`
const pathConfigCRLHelpDesc = `
This endpoint allows configuration of the CRL lifetime.
`

View file

@ -0,0 +1,177 @@
package pki
import (
"encoding/pem"
"fmt"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// Returns the CA in raw format
func pathFetchCA(b *backend) *framework.Path {
return &framework.Path{
Pattern: `ca(/pem)?`,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathFetchRead,
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
// Returns the CRL in raw format
func pathFetchCRL(b *backend) *framework.Path {
return &framework.Path{
Pattern: `crl(/pem)?`,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathFetchRead,
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
// Returns any valid (non-revoked) cert. Since "ca" fits the pattern, this path
// also handles returning the CA cert in a non-raw format.
func pathFetchValid(b *backend) *framework.Path {
return &framework.Path{
Pattern: `cert/(?P<serial>[0-9A-Fa-f-:]+)`,
Fields: map[string]*framework.FieldSchema{
"serial": &framework.FieldSchema{
Type: framework.TypeString,
Description: `Certificate serial number, in colon- or
hyphen-separated octal`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathFetchRead,
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
// This returns the CRL in a non-raw format
func pathFetchCRLViaCertPath(b *backend) *framework.Path {
return &framework.Path{
Pattern: `cert/crl`,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathFetchRead,
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) {
var serial string
var pemType string
var contentType string
var certEntry *logical.StorageEntry
var funcErr error
var certificate []byte
response = &logical.Response{
Data: map[string]interface{}{},
}
// Some of these need to return raw and some non-raw;
// this is basically handled by setting contentType or not.
// Errors don't cause an immediate exit, because the raw
// paths still need to return raw output.
switch {
case req.Path == "ca" || req.Path == "ca/pem":
serial = "ca"
contentType = "application/pkix-cert"
if req.Path == "ca/pem" {
pemType = "CERTIFICATE"
}
case req.Path == "crl" || req.Path == "crl/pem":
serial = "crl"
contentType = "application/pkix-crl"
if req.Path == "crl/pem" {
pemType = "X509 CRL"
}
case req.Path == "cert/crl":
serial = "crl"
pemType = "X509 CRL"
default:
serial = data.Get("serial").(string)
pemType = "CERTIFICATE"
}
if len(serial) == 0 {
response = logical.ErrorResponse("The serial number must be provided")
goto reply
}
_, funcErr = fetchCAInfo(req)
switch funcErr.(type) {
case certutil.UserError:
response = logical.ErrorResponse(fmt.Sprintf("%s", funcErr))
goto reply
case certutil.InternalError:
retErr = funcErr
goto reply
}
certEntry, funcErr = fetchCertBySerial(req, req.Path, serial)
switch funcErr.(type) {
case certutil.UserError:
response = logical.ErrorResponse(funcErr.Error())
goto reply
case certutil.InternalError:
retErr = funcErr
goto reply
}
certificate = certEntry.Value
if len(pemType) != 0 {
block := pem.Block{
Type: pemType,
Bytes: certEntry.Value,
}
certificate = pem.EncodeToMemory(&block)
}
reply:
switch {
case len(contentType) != 0:
response = &logical.Response{
Data: map[string]interface{}{
logical.HTTPContentType: contentType,
logical.HTTPRawBody: certificate,
}}
if retErr != nil {
b.Logger().Printf("Possible error, but cannot return in raw response: %s. Note that an empty CA probably means none was configured, and an empty CRL is quite possibly correct", retErr)
}
retErr = nil
response.Data[logical.HTTPStatusCode] = 200
case retErr != nil:
response = nil
default:
response.Data["certificate"] = string(certificate)
}
return
}
const pathFetchHelpSyn = `
Fetch a CA, CRL, or non-revoked certificate.
`
const pathFetchHelpDesc = `
This allows certificates to be fetched. If using the fetch/ prefix any non-revoked certificate can be fetched.
Using "ca" or "crl" as the value fetches the appropriate information in DER encoding. Add "/pem" to either to get PEM encoding.
`

View file

@ -0,0 +1,201 @@
package pki
import (
"fmt"
"net"
"strings"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathIssue(b *backend) *framework.Path {
return &framework.Path{
Pattern: `issue/(?P<role>\w+)`,
Fields: map[string]*framework.FieldSchema{
"role": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The desired role with configuration for this
request`,
},
"common_name": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The requested common name; if you want more than
one, specify the alternative names in the
alt_names map`,
},
"alt_names": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The requested Subject Alternative Names, if any,
in a comma-delimited list`,
},
"ip_sans": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The requested IP SANs, if any, in a
common-delimited list`,
},
"lease": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The requested lease",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: b.pathIssueCert,
},
HelpSynopsis: pathIssueCertHelpSyn,
HelpDescription: pathIssueCertHelpDesc,
}
}
func (b *backend) pathIssueCert(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := data.Get("role").(string)
// Get the common name(s)
var commonNames []string
cn := data.Get("common_name").(string)
if len(cn) == 0 {
return logical.ErrorResponse("The common_name field is required"), nil
}
commonNames = []string{cn}
cnAlt := data.Get("alt_names").(string)
if len(cnAlt) != 0 {
for _, v := range strings.Split(cnAlt, ",") {
commonNames = append(commonNames, v)
}
}
// Get the role
role, err := b.getRole(req.Storage, roleName)
if err != nil {
return nil, err
}
if role == nil {
return logical.ErrorResponse(fmt.Sprintf("Unknown role: %s", roleName)), nil
}
// Get any IP SANs
ipSANs := []net.IP{}
ipAlt := data.Get("ip_sans").(string)
if len(ipAlt) != 0 {
if !role.AllowIPSANs {
return logical.ErrorResponse(fmt.Sprintf("IP Subject Alternative Names are not allowed in this role, but was provided %s", ipAlt)), nil
}
for _, v := range strings.Split(ipAlt, ",") {
parsedIP := net.ParseIP(v)
if parsedIP == nil {
return logical.ErrorResponse(fmt.Sprintf("The value '%s' is not a valid IP address", v)), nil
}
ipSANs = append(ipSANs, parsedIP)
}
}
leaseField := data.Get("lease").(string)
if len(leaseField) == 0 {
leaseField = role.Lease
}
lease, err := time.ParseDuration(leaseField)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid requested lease: %s", err)), nil
}
leaseMax, err := time.ParseDuration(role.LeaseMax)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid lease: %s", err)), nil
}
if lease > leaseMax {
return logical.ErrorResponse("Lease expires after maximum allowed by this role"), nil
}
badName, err := validateCommonNames(req, commonNames, role)
if len(badName) != 0 {
return logical.ErrorResponse(fmt.Sprintf("Name %s not allowed by this role", badName)), nil
} else if err != nil {
return nil, fmt.Errorf("Error validating name %s: %s", badName, err)
}
signingBundle, caErr := fetchCAInfo(req)
switch caErr.(type) {
case certutil.UserError:
return logical.ErrorResponse(fmt.Sprintf("Could not fetch the CA certificate: %s", caErr)), nil
case certutil.InternalError:
return nil, fmt.Errorf("Error fetching CA certificate: %s", caErr)
}
if time.Now().Add(lease).After(signingBundle.Certificate.NotAfter) {
return logical.ErrorResponse(fmt.Sprintf("Cannot satisfy request, as maximum lease is beyond the expiration of the CA certificate")), nil
}
var usage certUsage
if role.ServerFlag {
usage = usage | serverUsage
}
if role.ClientFlag {
usage = usage | clientUsage
}
if role.CodeSigningFlag {
usage = usage | codeSigningUsage
}
creationBundle := &certCreationBundle{
SigningBundle: signingBundle,
CACert: signingBundle.Certificate,
CommonNames: commonNames,
IPSANs: ipSANs,
KeyType: role.KeyType,
KeyBits: role.KeyBits,
Lease: lease,
Usage: usage,
}
parsedBundle, err := createCertificate(creationBundle)
switch err.(type) {
case certutil.UserError:
return logical.ErrorResponse(err.Error()), nil
case certutil.InternalError:
return nil, err
}
cb, err := parsedBundle.ToCertBundle()
if err != nil {
return nil, fmt.Errorf("Error converting raw cert bundle to cert bundle: %s", err)
}
resp := b.Secret(SecretCertsType).Response(
structs.New(cb).Map(),
map[string]interface{}{
"serial_number": cb.SerialNumber,
})
resp.Secret.Lease = lease
err = req.Storage.Put(&logical.StorageEntry{
Key: "certs/" + cb.SerialNumber,
Value: parsedBundle.CertificateBytes,
})
if err != nil {
return nil, fmt.Errorf("Unable to store certificate locally")
}
return resp, nil
}
const pathIssueCertHelpSyn = `
Request certificates using a certain role with the provided common name.
`
const pathIssueCertHelpDesc = `
This path allows requesting certificates to be issued according to the
policy of the given role. The certificate will only be issued if the
requested common name is allowed by the role policy.
`

View file

@ -0,0 +1,89 @@
package pki
import (
"fmt"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathRevoke(b *backend) *framework.Path {
return &framework.Path{
Pattern: `revoke`,
Fields: map[string]*framework.FieldSchema{
"serial_number": &framework.FieldSchema{
Type: framework.TypeString,
Description: `Certificate serial number, in colon- or
hyphen-separated octal`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: b.pathRevokeWrite,
},
HelpSynopsis: pathRevokeHelpSyn,
HelpDescription: pathRevokeHelpDesc,
}
}
func pathRotateCRL(b *backend) *framework.Path {
return &framework.Path{
Pattern: `crl/rotate`,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathRotateCRLRead,
},
HelpSynopsis: pathRotateCRLHelpSyn,
HelpDescription: pathRotateCRLHelpDesc,
}
}
func (b *backend) pathRevokeWrite(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
serial := data.Get("serial_number").(string)
if len(serial) == 0 {
return logical.ErrorResponse("The serial number must be provided"), nil
}
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
return revokeCert(b, req, serial)
}
func (b *backend) pathRotateCRLRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
crlErr := buildCRL(b, req)
switch crlErr.(type) {
case certutil.UserError:
return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil
case certutil.InternalError:
return nil, fmt.Errorf("Error encountered during CRL building: %s", crlErr)
default:
return &logical.Response{
Data: map[string]interface{}{
"success": true,
},
}, nil
}
}
const pathRevokeHelpSyn = `
Revoke a certificate by serial number.
`
const pathRevokeHelpDesc = `
This allows certificates to be revoked using its serial number. A root token is required.
`
const pathRotateCRLHelpSyn = `
Force a rebuild of the CRL.
`
const pathRotateCRLHelpDesc = `
Force a rebuild of the CRL. This can be used to remove expired certificates from it if no certificates have been revoked. A root token is required.
`

View file

@ -0,0 +1,276 @@
package pki
import (
"fmt"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathRoles(b *backend) *framework.Path {
return &framework.Path{
Pattern: "roles/(?P<name>\\w+)",
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the role",
},
"lease": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: `The lease length if no specific lease length is
requested. The lease length controls the expiration
of certificates issued by this backend. Defaults to
the value of lease_max.`,
},
"lease_max": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "The maximum allowed lease length",
},
"allow_localhost": &framework.FieldSchema{
Type: framework.TypeBool,
Default: true,
Description: `Whether to allow "localhost" as a valid common
name in a request`,
},
"allowed_base_domain": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: `If set, clients can request certificates for
subdomains directly beneath this base domain, including
the wildcard subdomain. See the documentation for more
information.`,
},
"allow_token_displayname": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: `If set, clients can request certificates for
matching the value of the Display Name on the requesting
token. See the documentation for more information.`,
},
"allow_subdomains": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: `If set, clients can request certificates for
subdomains of the CNs allowed by the other role options,
including wildcard subdomains. See the documentation for
more information.`,
},
"allow_any_name": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: `If set, clients can request certificates for
any CN they like. See the documentation for more
information.`,
},
"allow_ip_sans": &framework.FieldSchema{
Type: framework.TypeBool,
Default: true,
Description: `If set, IP Subject Alternative Names are allowed.
Any valid IP is accepted.`,
},
"server_flag": &framework.FieldSchema{
Type: framework.TypeBool,
Default: true,
Description: `If set, certificates are flagged for server use.
Defaults to true.`,
},
"client_flag": &framework.FieldSchema{
Type: framework.TypeBool,
Default: true,
Description: `If set, certificates are flagged for client use.
Defaults to true.`,
},
"code_signing_flag": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: `If set, certificates are flagged for code signing
use. Defaults to false.`,
},
"key_type": &framework.FieldSchema{
Type: framework.TypeString,
Default: "rsa",
Description: `The type of key to use; defaults to RSA. "rsa"
and "ec" are the only valid values.`,
},
"key_bits": &framework.FieldSchema{
Type: framework.TypeInt,
Default: 2048,
Description: `The number of bits to use. You will almost
certainly want to change this if you adjust
the key_type.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathRoleRead,
logical.WriteOperation: b.pathRoleCreate,
logical.DeleteOperation: b.pathRoleDelete,
},
HelpSynopsis: pathRoleHelpSyn,
HelpDescription: pathRoleHelpDesc,
}
}
func (b *backend) getRole(s logical.Storage, n string) (*roleEntry, error) {
entry, err := s.Get("role/" + n)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result roleEntry
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathRoleDelete(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
err := req.Storage.Delete("role/" + data.Get("name").(string))
if err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathRoleRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
role, err := b.getRole(req.Storage, data.Get("name").(string))
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
resp := &logical.Response{
Data: structs.New(role).Map(),
}
return resp, nil
}
func (b *backend) pathRoleCreate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string)
entry := &roleEntry{
LeaseMax: data.Get("lease_max").(string),
Lease: data.Get("lease").(string),
AllowLocalhost: data.Get("allow_localhost").(bool),
AllowedBaseDomain: data.Get("allowed_base_domain").(string),
AllowTokenDisplayName: data.Get("allow_token_displayname").(bool),
AllowSubdomains: data.Get("allow_subdomains").(bool),
AllowAnyName: data.Get("allow_any_name").(bool),
AllowIPSANs: data.Get("allow_ip_sans").(bool),
ServerFlag: data.Get("server_flag").(bool),
ClientFlag: data.Get("client_flag").(bool),
CodeSigningFlag: data.Get("code_signing_flag").(bool),
KeyType: data.Get("key_type").(string),
KeyBits: data.Get("key_bits").(int),
}
if len(entry.LeaseMax) == 0 {
return logical.ErrorResponse("\"lease_max\" value must be supplied"), nil
}
leaseMax, err := time.ParseDuration(entry.LeaseMax)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid lease: %s", err)), nil
}
switch len(entry.Lease) {
case 0:
entry.Lease = entry.LeaseMax
default:
lease, err := time.ParseDuration(entry.Lease)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Invalid lease: %s", err)), nil
}
if lease > leaseMax {
return logical.ErrorResponse("\"lease\" value must be less than \"lease_max\" value"), nil
}
}
if len(entry.KeyType) == 0 {
entry.KeyType = "rsa"
}
if entry.KeyBits == 0 {
entry.KeyBits = 2048
}
switch entry.KeyType {
case "rsa":
case "ec":
switch entry.KeyBits {
case 224:
case 256:
case 384:
case 521:
default:
return logical.ErrorResponse(fmt.Sprintf("Unsupported bit length for EC key: %d", entry.KeyBits)), nil
}
default:
return logical.ErrorResponse(fmt.Sprintf("Unknown key type %s", entry.KeyType)), nil
}
// Store it
jsonEntry, err := logical.StorageEntryJSON("role/"+name, entry)
if err != nil {
return nil, err
}
if err := req.Storage.Put(jsonEntry); err != nil {
return nil, err
}
return nil, nil
}
type roleEntry struct {
LeaseMax string `json:"lease_max" structs:"lease_max" mapstructure:"lease_max"`
Lease string `json:"lease" structs:"lease" mapstructure:"lease"`
AllowLocalhost bool `json:"allow_localhost" structs:"allow_localhost" mapstructure:"allow_localhost"`
AllowedBaseDomain string `json:"allowed_base_domain" structs:"allowed_base_domain" mapstructure:"allowed_base_domain"`
AllowTokenDisplayName bool `json:"allow_token_displayname" structs:"allow_token_displayname" mapstructure:"allow_token_displayname"`
AllowSubdomains bool `json:"allow_subdomains" structs:"allow_subdomains" mapstructure:"allow_subdomains"`
AllowAnyName bool `json:"allow_any_name" structs:"allow_any_name" mapstructure:"allow_any_name"`
AllowIPSANs bool `json:"allow_ip_sans" structs:"allow_ip_sans" mapstructure:"allow_ip_sans"`
ServerFlag bool `json:"server_flag" structs:"server_flag" mapstructure:"server_flag"`
ClientFlag bool `json:"client_flag" structs:"client_flag" mapstructure:"client_flag"`
CodeSigningFlag bool `json:"code_signing_flag" structs:"code_signing_flag" mapstructure:"code_signing_flag"`
KeyType string `json:"key_type" structs:"key_type" mapstructure:"key_type"`
KeyBits int `json:"key_bits" structs:"key_bits" mapstructure:"key_bits"`
}
const pathRoleHelpSyn = `
Manage the roles that can be created with this backend.
`
const pathRoleHelpDesc = `
This path lets you manage the roles that can be created with this backend.
`

View file

@ -0,0 +1,59 @@
package pki
import (
"fmt"
"strings"
"time"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// SecretCertsType is the name used to identify this type
const SecretCertsType = "pki"
func secretCerts(b *backend) *framework.Secret {
return &framework.Secret{
Type: SecretCertsType,
Fields: map[string]*framework.FieldSchema{
"certificate": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The PEM-encoded concatenated certificate and
issuing certificate authority`,
},
"private_key": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The PEM-encoded private key for the certificate",
},
"serial": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The serial number of the certificate, for handy
reference`,
},
},
DefaultDuration: 168 * time.Hour,
DefaultGracePeriod: 10 * time.Minute,
Revoke: b.secretCredsRevoke,
}
}
func (b *backend) secretCredsRevoke(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
if req.Secret == nil {
return nil, fmt.Errorf("Secret is nil in request")
}
serialInt, ok := req.Secret.InternalData["serial_number"]
if !ok {
return nil, fmt.Errorf("Could not find serial in internal secret data")
}
serial := strings.Replace(strings.ToLower(serialInt.(string)), "-", ":", -1)
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
return revokeCert(b, req, serial)
}

View file

@ -17,6 +17,7 @@ import (
"github.com/hashicorp/vault/builtin/logical/aws"
"github.com/hashicorp/vault/builtin/logical/consul"
"github.com/hashicorp/vault/builtin/logical/mysql"
"github.com/hashicorp/vault/builtin/logical/pki"
"github.com/hashicorp/vault/builtin/logical/postgresql"
"github.com/hashicorp/vault/builtin/logical/ssh"
"github.com/hashicorp/vault/builtin/logical/transit"
@ -68,6 +69,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
"aws": aws.Factory,
"consul": consul.Factory,
"postgresql": postgresql.Factory,
"pki": pki.Factory,
"transit": transit.Factory,
"mysql": mysql.Factory,
"ssh": ssh.Factory,

View file

@ -0,0 +1,344 @@
package certutil
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/fatih/structs"
"github.com/hashicorp/vault/api"
)
// Tests converting back and forth between a CertBundle and a ParsedCertBundle.
//
// Also tests the GetSubjKeyID, GetOctalFormatted, and
// ParsedCertBundle.getSigner functions.
func TestCertBundleConversion(t *testing.T) {
cbuts := []*CertBundle{
refreshRSACertBundle(),
refreshECCertBundle(),
}
for _, cbut := range cbuts {
pcbut, err := cbut.ToParsedCertBundle()
if err != nil {
t.Fatalf("Error converting to parsed cert bundle: %s", err)
}
err = compareCertBundleToParsedCertBundle(cbut, pcbut)
if err != nil {
t.Fatalf(err.Error())
}
}
}
func TestTLSConfig(t *testing.T) {
cbut := refreshRSACertBundle()
pcbut, err := cbut.ToParsedCertBundle()
if err != nil {
t.Fatalf("Error getting parsed cert bundle: %s", err)
}
usages := []TLSUsage{
TLSUnknown,
TLSClient,
TLSServer,
TLSClient | TLSServer,
}
for _, usage := range usages {
tlsConfig, err := pcbut.GetTLSConfig(usage)
if err != nil {
t.Fatalf("Error getting tls config: %s", err)
}
if tlsConfig == nil {
t.Fatalf("Got nil tls.Config")
}
if len(tlsConfig.Certificates) != 1 {
t.Fatalf("Unexpected length in config.Certificates")
}
// Length should be 2, since we passed in a CA
if len(tlsConfig.Certificates[0].Certificate) != 2 {
t.Fatalf("Did not find both certificates in config.Certificates.Certificate")
}
if tlsConfig.Certificates[0].Leaf != pcbut.Certificate {
t.Fatalf("Leaf certificate does not match parsed bundle's certificate")
}
if tlsConfig.Certificates[0].PrivateKey != pcbut.PrivateKey {
t.Fatalf("Config's private key does not match parsed bundle's private key")
}
switch usage {
case TLSServer | TLSClient:
if len(tlsConfig.ClientCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.ClientCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 {
t.Fatalf("CA certificate not in client cert pool as expected")
}
if len(tlsConfig.RootCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.RootCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 {
t.Fatalf("CA certificate not in root cert pool as expected")
}
case TLSServer:
if len(tlsConfig.ClientCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.ClientCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 {
t.Fatalf("CA certificate not in client cert pool as expected")
}
if tlsConfig.RootCAs != nil {
t.Fatalf("Found root pools in config object when not expected")
}
case TLSClient:
if len(tlsConfig.RootCAs.Subjects()) != 1 || bytes.Compare(tlsConfig.RootCAs.Subjects()[0], pcbut.IssuingCA.RawSubject) != 0 {
t.Fatalf("CA certificate not in root cert pool as expected")
}
if tlsConfig.ClientCAs != nil {
t.Fatalf("Found root pools in config object when not expected")
}
default:
if tlsConfig.RootCAs != nil || tlsConfig.ClientCAs != nil {
t.Fatalf("Found root pools in config object when not expected")
}
}
}
}
func TestCertBundleParsing(t *testing.T) {
jsonBundle := refreshRSACertBundle()
jsonString, err := json.Marshal(jsonBundle)
if err != nil {
t.Fatalf("Error marshaling testing certbundle to JSON: %s", err)
}
pcbut, err := ParsePKIJSON(jsonString)
if err != nil {
t.Fatalf("Error during JSON bundle handling: %s", err)
}
err = compareCertBundleToParsedCertBundle(jsonBundle, pcbut)
if err != nil {
t.Fatalf(err.Error())
}
secret := &api.Secret{
Data: structs.New(jsonBundle).Map(),
}
pcbut, err = ParsePKIMap(secret.Data)
if err != nil {
t.Fatalf("Error during JSON bundle handling: %s", err)
}
err = compareCertBundleToParsedCertBundle(jsonBundle, pcbut)
if err != nil {
t.Fatalf(err.Error())
}
pemBundle := strings.Join([]string{
jsonBundle.Certificate,
jsonBundle.IssuingCA,
jsonBundle.PrivateKey,
}, "\n")
pcbut, err = ParsePEMBundle(pemBundle)
if err != nil {
t.Fatalf("Error during JSON bundle handling: %s", err)
}
err = compareCertBundleToParsedCertBundle(jsonBundle, pcbut)
if err != nil {
t.Fatalf(err.Error())
}
}
func compareCertBundleToParsedCertBundle(cbut *CertBundle, pcbut *ParsedCertBundle) error {
if cbut == nil {
return fmt.Errorf("Got nil bundle")
}
if pcbut == nil {
return fmt.Errorf("Got nil parsed bundle")
}
switch {
case pcbut.Certificate == nil:
return fmt.Errorf("Parsed bundle has nil certificate")
case pcbut.PrivateKey == nil:
return fmt.Errorf("Parsed bundle has nil private key")
case pcbut.IssuingCA == nil:
return fmt.Errorf("Parsed bundle has nil issuing CA")
}
switch cbut.PrivateKey {
case privRSAKeyPem:
if pcbut.PrivateKeyType != RSAPrivateKey {
return fmt.Errorf("Parsed bundle has wrong private key type")
}
case privECKeyPem:
if pcbut.PrivateKeyType != ECPrivateKey {
return fmt.Errorf("Parsed bundle has wrong private key type")
}
default:
return fmt.Errorf("Parsed bundle has unknown private key type")
}
subjKeyID, err := GetSubjKeyID(pcbut.PrivateKey)
if err != nil {
return fmt.Errorf("Error when getting subject key id: %s", err)
}
if bytes.Compare(subjKeyID, pcbut.Certificate.SubjectKeyId) != 0 {
return fmt.Errorf("Parsed bundle private key does not match subject key id")
}
cb, err := pcbut.ToCertBundle()
if err != nil {
return fmt.Errorf("Thrown error during parsed bundle conversion: %s\n\nInput was: %#v", err, *pcbut)
}
switch {
case len(cb.Certificate) == 0:
return fmt.Errorf("Bundle has nil certificate")
case len(cb.PrivateKey) == 0:
return fmt.Errorf("Bundle has nil private key")
case len(cb.IssuingCA) == 0:
return fmt.Errorf("Bundle has nil issuing CA")
}
switch cb.PrivateKeyType {
case "rsa":
if pcbut.PrivateKeyType != RSAPrivateKey {
return fmt.Errorf("Bundle has wrong private key type")
}
if cb.PrivateKey != privRSAKeyPem {
return fmt.Errorf("Bundle private key does not match")
}
case "ec":
if pcbut.PrivateKeyType != ECPrivateKey {
return fmt.Errorf("Bundle has wrong private key type")
}
if cb.PrivateKey != privECKeyPem {
return fmt.Errorf("Bundle private key does not match")
}
default:
return fmt.Errorf("Bundle has unknown private key type")
}
if cb.SerialNumber != GetOctalFormatted(pcbut.Certificate.SerialNumber.Bytes(), ":") {
return fmt.Errorf("Bundle serial number does not match")
}
return nil
}
func refreshRSACertBundle() *CertBundle {
return &CertBundle{
Certificate: certRSAPem,
PrivateKey: privRSAKeyPem,
IssuingCA: issuingCaPem,
}
}
func refreshECCertBundle() *CertBundle {
return &CertBundle{
Certificate: certECPem,
PrivateKey: privECKeyPem,
IssuingCA: issuingCaPem,
}
}
const (
privRSAKeyPem = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAt3ZJUaztCRiVg87P0y8T7QMNFQi61BCSIKepxXXWc7zi5JJS
MfQAstXJEqBYiShsSpYm6soiT6hX074t7wQAHGS3+u7qNogWpmTAUTUnNIM+QCxH
2Nc/kzYxaWajupVzgGvLeiqU3d4tIUk/ZkftvWJryr2hZc8zEN3C4pGS/2F+RQ+z
Ov+BpAI1BdbQGhF7m92vn6KS/iWsqmwHG9oChgvWeBHjWUI8qGauBc+it4S5RxfN
8JJIBXUIZtbaqFZzgjv8kUDyqoQGvkY/4Ce1K0bFJsM7wmMPv+5QscIBF4KWgN0k
TSMPPXfnn/QIAfhaYQkT9MjwGr6+B3SODNGCTQIDAQABAoIBAHtSwhprGbNhmS/P
F5ioLsbFpEedZKkksnXM/qxDd/K45/Qp/6KgmM+eMdmZe6pHR/QjVunBEqtlSBSH
5KykjcaIVbwSWdJqTH9xfm2YQ1BjYLcWjP1QQ+YbKb/mRO0phUiwLUlj0koKDWAw
srN4anFB9Z+FNTcQvwz5ZQWUQbH0neQtWO1nDvLsScgu1kchoEzJEJaFOQ1+HfGe
WxD766fZyqZQi5+cLrhOqHOGSlO+IFVe0hguiEHFr9LEPTXXkZtOR4wTf7j1Us8s
1KQ/jv01sx9S7HEbZJurzIjS23OywEUdJd1EsIE2lJV2QUwSiAsPYZOSQZlgOGzP
VRKVkGkCgYEA1u+pVP2r+xSxYy8KcdcRCdGGBh00VLx1yJRHWZ5YjF56hp0R0cG+
xGLar5KCdBpr4jJnQGIrx8lw3SDCt4EXlxgJxitXlBtiKByM7/mYRRfURr9WMRr4
88GQlWDbo2Xalnuac0qlkFqVIg0BaW+Z15A/E1L69aUxaR0ozlA9Jl8CgYEA2oNA
5F2otqzo9eNYucNAjihVhATd11DECQvbIQp/0bEJe0Znnzq/QIGIOVapC0VKGBwB
P5DuLL1P/nTPjjE/ZhjFuhMNM5PzC6obAjBh+gCpc+c+21Qerv7RKUTi2sGTzRHu
lpccRDfuF8bhzD6lAo50FpSmPE/ovZzb9+IsXtMCgYBVnUdM9HKh47846870Q5+k
0pHZM57ZtewQxoeZOgq5dxTFNCGZ9NvBLENBtlFCYBfjFQKt0azwutu7KUaGg+Ra
qheSmUccVsAFjEHTgQ9XTkOfHq39h2ns5ohqCBfVAUhNstR14iEK3BoVYyrRzcNw
6yNE1kPivzdsUFIlxC5nbwKBgDUUjT7sQX+eoTiZ8YOumo/t3Fglln4ncHeCGcj8
8+/MQbFgeOuFKdBRpvXGx2mle0pAA02dtz3G/xeg6IpyDCSQ//cjiaFt3yyGNeli
N2qznnY5RluhI5L+83BC+5iITY8TPBH4wzUPIRdFiLREw3DLigeyNG+SOcdVw1mD
56NhAoGBALFh3sGkhvPiI/G/i/5tGZVA/dS/4DVXOoHW43+ZDHWEwqiN6vTf/VVi
cm+8kcfLY1E5fSf/4e7mIQq7o5qVn9Y3HWsajS1FFeznJjPj4Jaa1HvegNcycAzs
XOQ7xy23/8wUupgNeD1mFdSFCXQ3UedsJuVBHsElPc5W74q4F4+F
-----END RSA PRIVATE KEY-----`
certRSAPem = `-----BEGIN CERTIFICATE-----
MIID+jCCAuSgAwIBAgIUcFCL9ESWTKLE6RqSYV7iZ78f1KcwCwYJKoZIhvcNAQEL
MBsxGTAXBgNVBAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjE5MTcyMzA0WhcN
MTUwNzAzMTcyMzA0WjBPMRIwEAYDVQQDEwlsb2NhbGhvc3QxOTA3BgNVBAUTMDY0
MTIwMzIxNzY3NTk2MjQyMjU0OTg5MTUxMzAyMjg1NzQ0NTc0OTkzMjY3NjI2MzCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALd2SVGs7QkYlYPOz9MvE+0D
DRUIutQQkiCnqcV11nO84uSSUjH0ALLVyRKgWIkobEqWJurKIk+oV9O+Le8EABxk
t/ru6jaIFqZkwFE1JzSDPkAsR9jXP5M2MWlmo7qVc4Bry3oqlN3eLSFJP2ZH7b1i
a8q9oWXPMxDdwuKRkv9hfkUPszr/gaQCNQXW0BoRe5vdr5+ikv4lrKpsBxvaAoYL
1ngR41lCPKhmrgXPoreEuUcXzfCSSAV1CGbW2qhWc4I7/JFA8qqEBr5GP+AntStG
xSbDO8JjD7/uULHCAReCloDdJE0jDz1355/0CAH4WmEJE/TI8Bq+vgd0jgzRgk0C
AwEAAaOCAQQwggEAMA4GA1UdDwEB/wQEAwIAqDAdBgNVHSUEFjAUBggrBgEFBQcD
AQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUZHtkxSX5GVAYo3h8
B8TGJ36vTH4wHwYDVR0jBBgwFoAU5JzeXhccaOWk5X6vhuGV7NwLMVkwTgYDVR0R
BEcwRYIJbG9jYWxob3N0gg9mb28uZXhhbXBsZS5jb22CD2Jhci5leGFtcGxlLmNv
bYcEgAMFBocQ/gEAAAAAAAAAAAAAAAAAATAxBgNVHR8EKjAoMCagJKAihiBodHRw
Oi8vbG9jYWxob3N0OjgyMDAvdjEvcGtpL2NybDALBgkqhkiG9w0BAQsDggEBAAps
W2ZDOAfwWufclmGPHt+YRXXSTWvPfF/cBeg5Oq/F8qUCVMHqdE/+EDWzh+Kz8jp0
ggklnh76frROvHxygbVD2Hs9ACzgpnHPy8FYOdN+OblvAMtGlMyTq/5XheasmWdY
FFH/ft6tReG7BjGgfdyH8yL/R6b/RtU/qPlowfrZgAzOv7/ou6yRlfjIhsWbne/S
SQuGASRxRp3Txp7Cf3RcdCwVuiQhFLVeVHH+atTc8v2DO/CLfi9enQo96qUku8Bd
b5QPKIV0sQdtwGV5fo2JGd25rWpCo6TkAM9EeNkcVze8wgArSRk8zLkvM/5z+5sn
Qaka08px4wljGQ2Wc88=
-----END CERTIFICATE-----`
privECKeyPem = `-----BEGIN EC PRIVATE KEY-----
MGgCAQEEHM3nuYLlrvawBN9hGVcu9mpaCEr7LMe44a7oQOygBwYFK4EEACGhPAM6
AATBZ3VXwBE9oeSREpM5b25PW6WiuLb4EXWpKZyjj552QYKYe7QBuGe9wvvgOeCB
ovN3tSuGKzTiUA==
-----END EC PRIVATE KEY-----`
certECPem = `-----BEGIN CERTIFICATE-----
MIIDJDCCAg6gAwIBAgIUM3J02tw0ZvpHUVHv6t8kcoft2/MwCwYJKoZIhvcNAQEL
MBsxGTAXBgNVBAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjE5MTcyODQyWhcN
MTUwNzAzMTcyODQyWjBPMRIwEAYDVQQDEwlsb2NhbGhvc3QxOTA3BgNVBAUTMDI5
MzcxMDk5Mzc2NDA3NDYyNjg3MTQzODcwMjc3Njg1OTkzMTkyMzkxNjM4MTE3MTBO
MBAGByqGSM49AgEGBSuBBAAhAzoABMFndVfAET2h5JESkzlvbk9bpaK4tvgRdakp
nKOPnnZBgph7tAG4Z73C++A54IGi83e1K4YrNOJQo4IBBDCCAQAwDgYDVR0PAQH/
BAQDAgCoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8E
AjAAMB0GA1UdDgQWBBQiFoWDvInznUGjdJPjBAyoxIkQITAfBgNVHSMEGDAWgBTk
nN5eFxxo5aTlfq+G4ZXs3AsxWTBOBgNVHREERzBFgglsb2NhbGhvc3SCD2Zvby5l
eGFtcGxlLmNvbYIPYmFyLmV4YW1wbGUuY29thwSAAwUGhxD+AQAAAAAAAAAAAAAA
AAABMDEGA1UdHwQqMCgwJqAkoCKGIGh0dHA6Ly9sb2NhbGhvc3Q6ODIwMC92MS9w
a2kvY3JsMAsGCSqGSIb3DQEBCwOCAQEA0RU18OdSdt2k4FKWyUS7EhVFOybiUHof
1n9EeBoxd7fEP/IuQnJGr3CPV5LRFdHRxkihf4N5bRjsst7cqczaIZZLWkAj+P/2
JxBqv2Hm57dwaw2gtwt3GcYN/5j76fYaoZOgPMqas72vYgnBgdKQs8GYSoy7BVpC
x3nTYHwlOF+sM4wuVSi78lwkcgADF5GIWXrM3tYilmcT9fNbUgSvcVWdNTRJ0W+m
S2AF+4eby5PC9U8eIoCnZPRNmH0jZbNWzZyD0hDhBrDlaEbS2QXKRURPHzht/SqN
nWWcpQG3B8EI7p749dP5L+idi3ajHIH8vm/PK+o5TRrcHB585MlErQ==
-----END CERTIFICATE-----`
issuingCaPem = `-----BEGIN CERTIFICATE-----
MIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAMMEFZhdWx0IFRlc3RpbmcgQ0EwHhcNMTUwNjAxMjA1MTUzWhcNMjUwNTI5MjA1
MTUzWjAbMRkwFwYDVQQDDBBWYXVsdCBUZXN0aW5nIENBMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEA1eKB2nFbRqTFs7KyZjbzB5VRCBbnLZfEXVP1c3bH
e+YGjlfl34cy52dmancUzOf1/Jfo+VglocjTLVy5wHSGJwQYs8b6pEuuvAVo/6wU
L5Z7ZlQDR4kDe5Q+xgoRT6Bi/Bs57E+fNYgyUq/YAUY5WLuC+ZliCbJkLnb15Itu
P1yVUTDXTYORRE3qJS5RRol8D3QvteG9LyPEc7C+jsm5iBCagyxluzU0dnEOib5q
7xwZncoMbQz+rZH3QnwOij41FOGRPazrD5Mv6xLBkFnE5VAJ+GIgvd4bpOwvYMuo
fvF4PS7SFzxkGssMLlICap6PFpKz86DpAoDxPuoZeOhU4QIDAQABo4GXMIGUMB0G
A1UdDgQWBBTknN5eFxxo5aTlfq+G4ZXs3AsxWTAfBgNVHSMEGDAWgBTknN5eFxxo
5aTlfq+G4ZXs3AsxWTAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vbG9jYWxob3N0
OjgyMDAvdjEvcGtpL2NybDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
BjANBgkqhkiG9w0BAQsFAAOCAQEAsINcA4PZm+OyldgNrwRVgxoSrhV1I9zszhc9
VV340ZWlpTTxFKVb/K5Hg+jMF9tv70X1HwlYdlutE6KdrsA3gks5zanh4/3zlrYk
ABNBmSD6SSU2HKX1bFCBAAS3YHONE5o1K5tzwLsMl5uilNf+Wid3NjFnQ4KfuYI5
loN/opnM6+a/O3Zua8RAuMMAv9wyqwn88aVuLvVzDNSMe5qC5kkuLGmRkNgY06rI
S/fXIHIOldeQxgYCqhdVmcDWJ1PtVaDfBsKVpRg1GRU8LUGw2E4AY+twd+J2FBfa
G/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==
-----END CERTIFICATE-----`
)

162
helper/certutil/helpers.go Normal file
View file

@ -0,0 +1,162 @@
package certutil
import (
"bytes"
"crypto"
"crypto/sha1"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"github.com/mitchellh/mapstructure"
)
// GetOctalFormatted returns the byte buffer formatted in octal with
// the specified separator between bytes.
func GetOctalFormatted(buf []byte, sep string) string {
var ret bytes.Buffer
for _, cur := range buf {
if ret.Len() > 0 {
fmt.Fprintf(&ret, sep)
}
fmt.Fprintf(&ret, "%02x", cur)
}
return ret.String()
}
// GetSubjKeyID returns the subject key ID, e.g. the SHA1 sum
// of the marshaled public key
func GetSubjKeyID(privateKey crypto.Signer) ([]byte, error) {
if privateKey == nil {
return nil, InternalError{"Passed-in private key is nil"}
}
marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public())
if err != nil {
return nil, InternalError{fmt.Sprintf("Error marshalling public key: %s", err)}
}
subjKeyID := sha1.Sum(marshaledKey)
return subjKeyID[:], nil
}
// ParsePKIMap takes a map (for instance, the Secret.Data
// returned from the PKI backend) and returns a ParsedCertBundle.
func ParsePKIMap(data map[string]interface{}) (*ParsedCertBundle, error) {
result := &CertBundle{}
err := mapstructure.Decode(data, result)
if err != nil {
return nil, UserError{err.Error()}
}
return result.ToParsedCertBundle()
}
// ParsePKIJSON takes a JSON-encoded string and returns a CertBundle
// ParsedCertBundle.
//
// This can be either the output of an
// issue call from the PKI backend or just its data member; or,
// JSON not coming from the PKI backend.
func ParsePKIJSON(input []byte) (*ParsedCertBundle, error) {
result := &CertBundle{}
err := json.Unmarshal(input, &result)
if err == nil {
return result.ToParsedCertBundle()
}
var secret Secret
err = json.Unmarshal(input, &secret)
if err == nil {
return ParsePKIMap(secret.Data)
}
return nil, UserError{"Unable to parse out of either secret data or a secret object"}
}
// ParsePEMBundle takes a string of concatenated PEM-format certificate
// and private key values and decodes/parses them, checking validity along
// the way. There must be at max two certificates (a certificate and its
// issuing certificate) and one private key.
func ParsePEMBundle(pemBundle string) (*ParsedCertBundle, error) {
if len(pemBundle) == 0 {
return nil, UserError{"Empty PEM bundle"}
}
pemBytes := []byte(pemBundle)
var pemBlock *pem.Block
parsedBundle := &ParsedCertBundle{}
for {
pemBlock, pemBytes = pem.Decode(pemBytes)
if pemBlock == nil {
return nil, UserError{"No data found"}
}
if signer, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil {
if parsedBundle.PrivateKeyType != UnknownPrivateKey {
return nil, UserError{"More than one private key given; provide only one private key in the bundle"}
}
parsedBundle.PrivateKeyType = ECPrivateKey
parsedBundle.PrivateKeyBytes = pemBlock.Bytes
parsedBundle.PrivateKey = signer
} else if signer, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil {
if parsedBundle.PrivateKeyType != UnknownPrivateKey {
return nil, UserError{"More than one private key given; provide only one private key in the bundle"}
}
parsedBundle.PrivateKeyType = RSAPrivateKey
parsedBundle.PrivateKeyBytes = pemBlock.Bytes
parsedBundle.PrivateKey = signer
} else if certificates, err := x509.ParseCertificates(pemBlock.Bytes); err == nil {
switch len(certificates) {
case 0:
return nil, UserError{"PEM block cannot be decoded to a private key or certificate"}
case 1:
if parsedBundle.Certificate != nil {
switch {
// We just found the issuing CA
case bytes.Equal(parsedBundle.Certificate.AuthorityKeyId, certificates[0].SubjectKeyId) && certificates[0].IsCA:
parsedBundle.IssuingCABytes = pemBlock.Bytes
parsedBundle.IssuingCA = certificates[0]
// Our saved certificate is actually the issuing CA
case bytes.Equal(parsedBundle.Certificate.SubjectKeyId, certificates[0].AuthorityKeyId) && parsedBundle.Certificate.IsCA:
parsedBundle.IssuingCA = parsedBundle.Certificate
parsedBundle.IssuingCABytes = parsedBundle.CertificateBytes
parsedBundle.CertificateBytes = pemBlock.Bytes
parsedBundle.Certificate = certificates[0]
}
} else {
switch {
// If this case isn't correct, the caller needs to assign
// the values to Certificate/CertificateBytes; assumptions
// made here will not be valid for all cases.
case certificates[0].IsCA:
parsedBundle.IssuingCABytes = pemBlock.Bytes
parsedBundle.IssuingCA = certificates[0]
default:
parsedBundle.CertificateBytes = pemBlock.Bytes
parsedBundle.Certificate = certificates[0]
}
}
default:
return nil, UserError{"Too many certificates given; provide a maximum of two certificates in the bundle"}
}
}
if len(pemBytes) == 0 {
break
}
}
return parsedBundle, nil
}

291
helper/certutil/types.go Normal file
View file

@ -0,0 +1,291 @@
// Package certutil contains helper functions that are mostly used
// with the PKI backend but can be generally useful. Functionality
// includes helpers for converting a certificate/private key bundle
// between DER and PEM, printing certificate serial numbers, and more.
//
// Functionality specific to the PKI backend includes some types
// and helper methods to make requesting certificates from the
// backend easy.
package certutil
import (
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
)
// Secret is used to attempt to unmarshal a Vault secret
// JSON response, as a convenience
type Secret struct {
Data map[string]interface{} `json:"data"`
}
// TLSUsage controls whether the intended usage of a *tls.Config
// returned from ParsedCertBundle.GetTLSConfig is for server use,
// client use, or both, which affects which values are set
type TLSUsage int
// The type of of the Private Key referenced in CertBundle
// and ParsedCertBundle. This uses colloquial names rather than
// official names, to eliminate confusion
const (
UnknownPrivateKey = iota
RSAPrivateKey
ECPrivateKey
TLSUnknown TLSUsage = 0
TLSServer TLSUsage = 1 << iota
TLSClient
)
// UserError represents an error generated due to invalid user input
type UserError struct {
Err string
}
func (e UserError) Error() string {
return e.Err
}
// InternalError represents an error generated internally,
// presumably not due to invalid user input
type InternalError struct {
Err string
}
func (e InternalError) Error() string {
return e.Err
}
// CertBundle contains a key type, a PEM-encoded private key,
// a PEM-encoded certificate, and a string-encoded serial number,
// returned from a successful Issue request
type CertBundle struct {
PrivateKeyType string `json:"private_key_type" structs:"private_key_type" mapstructure:"private_key_type"`
Certificate string `json:"certificate" structs:"certificate" mapstructure:"certificate"`
IssuingCA string `json:"issuing_ca" structs:"issuing_ca" mapstructure:"issuing_ca"`
PrivateKey string `json:"private_key" structs:"private_key" mapstructure:"private_key"`
SerialNumber string `json:"serial_number" structs:"serial_number" mapstructure:"serial_number"`
}
// ParsedCertBundle contains a key type, a DER-encoded private key,
// a DER-encoded certificate, and a big.Int serial number
type ParsedCertBundle struct {
PrivateKeyType int
PrivateKeyBytes []byte
PrivateKey crypto.Signer
IssuingCABytes []byte
IssuingCA *x509.Certificate
CertificateBytes []byte
Certificate *x509.Certificate
}
// ToParsedCertBundle converts a string-based certificate bundle
// to a byte-based raw certificate bundle
func (c *CertBundle) ToParsedCertBundle() (*ParsedCertBundle, error) {
result := &ParsedCertBundle{}
var err error
var pemBlock *pem.Block
if len(c.PrivateKey) > 0 {
pemBlock, _ = pem.Decode([]byte(c.PrivateKey))
if pemBlock == nil {
return nil, UserError{"Error decoding private key from cert bundle"}
}
result.PrivateKeyBytes = pemBlock.Bytes
switch c.PrivateKeyType {
case "ec":
result.PrivateKeyType = ECPrivateKey
case "rsa":
result.PrivateKeyType = RSAPrivateKey
default:
// Try to figure it out and correct
if _, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil {
result.PrivateKeyType = ECPrivateKey
c.PrivateKeyType = "ec"
} else if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil {
result.PrivateKeyType = RSAPrivateKey
c.PrivateKeyType = "rsa"
} else {
return nil, UserError{fmt.Sprintf("Unknown private key type in bundle: %s", c.PrivateKeyType)}
}
}
result.PrivateKey, err = result.getSigner()
if err != nil {
return nil, UserError{fmt.Sprintf("Error getting signer: %s", err)}
}
}
if len(c.Certificate) > 0 {
pemBlock, _ = pem.Decode([]byte(c.Certificate))
if pemBlock == nil {
return nil, UserError{"Error decoding certificate from cert bundle"}
}
result.CertificateBytes = pemBlock.Bytes
result.Certificate, err = x509.ParseCertificate(result.CertificateBytes)
if err != nil {
return nil, UserError{"Error encountered parsing certificate bytes from raw bundle"}
}
}
if len(c.IssuingCA) > 0 {
pemBlock, _ = pem.Decode([]byte(c.IssuingCA))
if pemBlock == nil {
return nil, UserError{"Error decoding issuing CA from cert bundle"}
}
result.IssuingCABytes = pemBlock.Bytes
result.IssuingCA, err = x509.ParseCertificate(result.IssuingCABytes)
if err != nil {
return nil, UserError{fmt.Sprintf("Error parsing CA certificate: %s", err)}
}
}
if len(c.SerialNumber) == 0 && len(c.Certificate) > 0 {
c.SerialNumber = GetOctalFormatted(result.Certificate.SerialNumber.Bytes(), ":")
}
return result, nil
}
// ToCertBundle converts a byte-based raw DER certificate bundle
// to a PEM-based string certificate bundle
func (p *ParsedCertBundle) ToCertBundle() (*CertBundle, error) {
result := &CertBundle{}
block := pem.Block{
Type: "CERTIFICATE",
}
if p.Certificate != nil {
result.SerialNumber = strings.TrimSpace(GetOctalFormatted(p.Certificate.SerialNumber.Bytes(), ":"))
}
if p.CertificateBytes != nil && len(p.CertificateBytes) > 0 {
block.Bytes = p.CertificateBytes
result.Certificate = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
}
if p.IssuingCABytes != nil && len(p.IssuingCABytes) > 0 {
block.Bytes = p.IssuingCABytes
result.IssuingCA = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
}
if p.PrivateKeyBytes != nil && len(p.PrivateKeyBytes) > 0 {
block.Bytes = p.PrivateKeyBytes
switch p.PrivateKeyType {
case RSAPrivateKey:
result.PrivateKeyType = "rsa"
block.Type = "RSA PRIVATE KEY"
case ECPrivateKey:
result.PrivateKeyType = "ec"
block.Type = "EC PRIVATE KEY"
default:
return nil, InternalError{"Could not determine private key type when creating block"}
}
result.PrivateKey = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
}
return result, nil
}
// GetSigner returns a crypto.Signer corresponding to the private key
// contained in this ParsedCertBundle. The Signer contains a Public() function
// for getting the corresponding public. The Signer can also be
// type-converted to private keys
func (p *ParsedCertBundle) getSigner() (crypto.Signer, error) {
var signer crypto.Signer
var err error
if p.PrivateKeyBytes == nil || len(p.PrivateKeyBytes) == 0 {
return nil, UserError{"Given parsed cert bundle does not have private key information"}
}
switch p.PrivateKeyType {
case ECPrivateKey:
signer, err = x509.ParseECPrivateKey(p.PrivateKeyBytes)
if err != nil {
return nil, UserError{fmt.Sprintf("Unable to parse CA's private EC key: %s", err)}
}
case RSAPrivateKey:
signer, err = x509.ParsePKCS1PrivateKey(p.PrivateKeyBytes)
if err != nil {
return nil, UserError{fmt.Sprintf("Unable to parse CA's private RSA key: %s", err)}
}
default:
return nil, UserError{"Unable to determine type of private key; only RSA and EC are supported"}
}
return signer, nil
}
// GetTLSConfig returns a TLS config generally suitable for client
// authentiation. The returned TLS config can be modified slightly
// to be made suitable for a server requiring client authentication;
// specifically, you should set the value of ClientAuth in the returned
// config to match your needs.
func (p *ParsedCertBundle) GetTLSConfig(usage TLSUsage) (*tls.Config, error) {
tlsCert := tls.Certificate{
Certificate: [][]byte{},
}
tlsConfig := &tls.Config{
NextProtos: []string{"http/1.1"},
}
if p.Certificate != nil {
tlsCert.Leaf = p.Certificate
}
if p.PrivateKey != nil {
tlsCert.PrivateKey = p.PrivateKey
}
if p.CertificateBytes != nil && len(p.CertificateBytes) > 0 {
tlsCert.Certificate = append(tlsCert.Certificate, p.CertificateBytes)
}
if p.IssuingCABytes != nil && len(p.IssuingCABytes) > 0 {
tlsCert.Certificate = append(tlsCert.Certificate, p.IssuingCABytes)
// Technically we only need one cert, but this doesn't duplicate code
certBundle, err := p.ToCertBundle()
if err != nil {
return nil, fmt.Errorf("Error converting parsed bundle to string bundle when getting TLS config: %s", err)
}
caPool := x509.NewCertPool()
ok := caPool.AppendCertsFromPEM([]byte(certBundle.IssuingCA))
if !ok {
return nil, fmt.Errorf("Could not append CA certificate")
}
if usage&TLSServer > 0 {
tlsConfig.ClientCAs = caPool
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
}
if usage&TLSClient > 0 {
tlsConfig.RootCAs = caPool
}
}
if tlsCert.Certificate != nil && len(tlsCert.Certificate) > 0 {
tlsConfig.Certificates = []tls.Certificate{tlsCert}
tlsConfig.BuildNameToCertificate()
}
return tlsConfig, nil
}
// IssueData is a structure that is suitable for marshaling into a request;
// either via JSON, or into a map[string]interface{} via the structs package
type IssueData struct {
Lease string `json:"lease" structs:"lease" mapstructure:"lease"`
CommonName string `json:"common_name" structs:"common_name" mapstructure:"common_name"`
AltNames string `json:"alt_names" structs:"alt_names" mapstructure:"alt_names"`
IPSANs string `json:"ip_sans" structs:"ip_sans" mapstructure:"ip_sans"`
}

View file

@ -1,4 +1,4 @@
// +build linux darwin
// +build linux darwin freebsd
package password

View file

@ -74,7 +74,7 @@ employees actively contribute to Vault.
Jack Pearkes is the creator of the online interactive demo of Vault.
He maintains this demo as well as the design and interaction of the
Vault website. Jack is an employee of HashiCorp and a primary engineer
behind <a href="https//atlas.hashicorp.com">Atlas</a>.
behind <a href="https://atlas.hashicorp.com">Atlas</a>.
He is also a core committer to
<a href="https://www.packer.io">Packer</a>,
<a href="https://www.consul.io">Consul</a>, and

View file

@ -0,0 +1,649 @@
---
layout: "docs"
page_title: "Secret Backend: PKI"
sidebar_current: "docs-secrets-pki"
description: |-
The PKI secret backend for Vault generates TLS certificates.
---
# PKI Secret Backend
Name: `pki`
The PKI secret backend for Vault generates X.509 certificates dynamically based on configured roles. This means services can get certificates needed for both client and server authentication without going through the usual manual process of generating a private key and CSR, submitting to a CA, and waiting for a verification and signing process to complete. Vault's built-in authentication and authorization mechanisms provide the verification functionality.
By keeping leases relatively short, revocations are less likely to be needed, keeping CRLs short and helping the backend scale to large workloads. This in turn allows each instance of a running application to have a unique certificate, eliminating sharing and the accompanying pain of revocation and rollover.
In addition, by allowing revocation to mostly be forgone, this backend allows for ephemeral certificates; certificates can be fetched and stored in memory upon application startup and discarded upon shutdown, without ever being written to disk.
This page will show a quick start for this backend. For detailed documentation on every path, use `vault help` after mounting the backend.
## Considerations
To successfully deploy this backend, there are a number of important considerations to be aware of, as well as some preparatory steps that should be undertaken. You should read all of these *before* using this backend or generating the CA to use with this backend.
### Never use root CAs
Vault storage is secure, but not as secure as a piece of paper in a bank vault. It is, after all, networked software. Your long-lived self-signed root CA's private key should instead be used to issue a shorter-lived intermediate CA certificate, and this is what you should put into Vault. This aligns with industry best practices.
### One CA Certificate, One Backend
In order to vastly simplify both the configuration and codebase of the PKI backend, only one CA certificate is allowed per backend. If you want to issue certificates from multiple CAs, mount the PKI backend at multiple mount points with separate CA certificates in each.
This also provides a convenient method of switching to a new CA certificate while keeping CRLs valid from the old CA certificate; simply mount a new backend and issue from there.
### Keep certificate lifetimes short, for CRL's sake
This backend aligns with Vault's philosophy of short-lived secrets. As such it is not expected that CRLs will grow large; the only place a private key is ever returned is to the requesting client (this backend does *not* store generated private keys). In most cases, if the key is lost, the certificate can simply be ignored, as it will expire shortly.
If a certificate must truly be revoked, the normal Vault revocation function can be used; alternately a root token can be used to revoke the certificate using the certificate's serial number. Any revocation action will cause the CRL to be regenerated. When the CRL is regenerated, any expired certificates are removed from the CRL (and any revoked, expired certificate are removed from backend storage).
This backend does not support multiple CRL endpoints with sliding date windows; often such mechanisms will have the transition point a few days apart, but this gets into the expected realm of the actual certificate validity periods issued from this backend. A good rule of thumb for this backend would be to simply not issue certificates with a validity period greater than your maximum comfortable CRL lifetime. Alternately, you can control CRL caching behavior on the client to ensure that checks happen more often.
Often multiple endpoints are used in case a single CRL endpoint is down so that clients don't have to figure out what to do with a lack of response. Run Vault in HA mode, and the CRL endpoint should be available even if a particular node is down.
### You must configure CRL information *in advance*
This backend serves CRLs from a predictable location. That location must be encoded into your CA certificate if you want to allow applications to use the CRL endpoint encoded in certificates to find the CRL. Instructions for doing so are below. If you need to adjust this later, you will have to generate a new CA certificate using the same private key if you want to keep validity for already-issued certificates.
### No OCSP support, yet
Vault's architecture does not currently allow for a binary protocol such as OCSP to be supported by a backend. As such, you should configure your software to use CRLs for revocation information, with a caching lifetime that feels good to you. Since you are following the advice above about keeping lifetimes short (right?), CRLs should not grow too large.
## Quick Start
### CA certificate
In order for this backend to serve CRL information at the expected location, you will need to generate your CA certificate with this information. For OpenSSL, this means putting a value in the CA section with the appropriate URL; in this example the PKI backend is mounted at `pki`:
```text
crlDistributionPoints = URI:https://vault.example.com:8200/v1/pki/crl
```
Adjust the URI as appropriate.
### Vault
The first step to using the PKI backend is to mount it. Unlike the `generic` backend, the `pki` backend is not mounted by default.
```text
$ vault mount pki
Successfully mounted 'pki' at 'pki'!
```
Next, Vault must be configured with a root certificate and associated private key. This is done by writing the contents of a file or *stdin*:
```text
$ vault write pki/config/ca pem_bundle="@ca_bundle.pem"
Success! Data written to: pki/config/ca
```
or
```
$ cat bundle.pem | vault write pki/config/ca pem_bundle="-"
Success! Data written to: pki/config/ca
```
Although in this example the value being piped into *stdin* could be passed directly into the Vault CLI command, a more complex usage might be to use [Ansible](http://www.ansible.com) to securely store the certificate and private key in an `ansible-vault` file, then have an `ansible-playbook` command decrypt this value and pass it in to Vault.
The next step is to configure a role. A role is a logical name that maps to a policy used to generated those credentials. For example, let's create an "example-dot-com" role:
```text
$ vault write pki/roles/example-dot-com \
allowed_base_domain="example.com" \
allow_subdomains="true" lease_max="72h"
Success! Data written to: pki/roles/example-dot-com
```
By writing to the `roles/example-dot-com` path we are defining the `example-dot-com` role. To generate a new set of credentials, we simply write to the `issue` endpoint with that role name: Vault is now configured to create and manage certificates!
```text
$ vault write pki/issue/example-dot-com common_name=blah.example.com
Key Value
lease_id pki/issue/example-dot-com/819393b5-e1a1-9efd-b72f-4dc3a1972e31
lease_duration 259200
lease_renewable false
certificate -----BEGIN CERTIFICATE-----
MIIECDCCAvKgAwIBAgIUXmLrLkTdBIOOIYg2/BXO7docKfUwCwYJKoZIhvcNAQEL
...
az3gfwlOqVTdgi/ZVAtIzhSEJ0OY136bq4NOaw==
-----END CERTIFICATE-----
issuing_ca -----BEGIN CERTIFICATE-----
MIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
...
-----END CERTIFICATE-----
private_key -----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA0cczc7Y2yIu7aD/IaDi23Io+tvvDS9XaXXDUFW1kqd58P83r
...
3xhCNnZ3CMQaM2I48sloVK/XoikMLb5MZwOUQn/V+TrhWP4Lu7qD
-----END RSA PRIVATE KEY-----
serial 5e:62:eb:2e:44:dd:04:83:8e:21:88:36:fc:15:ce:ed:da:1c:29:f5
```
Note that this is a write, not a read, to allow values to be passed in at request time.
Vault has now generated a new set of credentials using the `example-dot-com` role configuration. Here we see the dynamically generated private key and certificate. The issuing CA certificate is returned as well.
Using ACLs, it is possible to restrict using the pki backend such that trusted operators can manage the role definitions, and both users and applications are restricted in the credentials they are allowed to read.
If you get stuck at any time, simply run `vault help pki` or with a subpath for interactive help output.
## API
### /pki/ca(/pem)
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Retrieves the CA certificate *in raw DER-encoded form*.
This is a bare endpoint that does not return a
standard Vault data structure. If `/pem` is added to the
endpoint, the CA certificate is returned in PEM format.
<br /><br />This is an unauthenticated endpoint.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/pki/ca(/pem)`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```
<binary DER-encoded certficiate>
```
</dd>
</dl>
### /pki/cert/
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Retrieves one of a selection of certificates. Valid values: `ca`
for the CA certificate, `crl` for the current CRL, or a serial
number in either hyphen-separated or colon-separated octal format.
This endpoint returns the certificate in PEM formatting in the
`certificate` key of the JSON object.
<br /><br />This is an unauthenticated endpoint.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/pki/cert/<serial>`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"certificate": "-----BEGIN CERTIFICATE-----\nMIIGmDCCBYCgAwIBAgIHBzEB3fTzhTANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE\n..."
}
}
...
```
</dd>
</dl>
### /pki/config/ca
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
A PEM file containing the issuing CA certificate
and its private key, concatenated.
<br /><br />This is a root-protected endpoint.
<br /><br />The information can be provided from a file via a `curl`
command similar to the following:<br/>
```text
curl -X POST --data "@cabundle.json" http://127.0.0.1:8200/v1/pki/config/ca -H X-Vault-Token:06b9d...
```
Note that if you provide the data through the HTTP API it must be
JSON-formatted, with newlines replaced with `\n`, like so:
```text
{ "pem_bundle": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END CERTIFICATE-----" }
```
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/pki/config/ca`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">pem_bundle</span>
<span class="param-flags">required</span>
The key and certificate concatenated in PEM format.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
</dl>
### /pki/crl(/pem)
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Retrieves the current CRL *in raw DER-encoded form*. This endpoint
is suitable for usage in the CRL Distribution Points extension in a
CA certificate. This is a bare endpoint that does not return a
standard Vault data structure. If `/pem` is added to the endpoint,
the CRL is returned in PEM format.
<br /><br />This is an unauthenticated endpoint.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/pki/crl(/pem)`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```
<binary DER-encoded CRL>
```
</dd>
</dl>
### /pki/crl/rotate
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
This endpoint forces a rotation of the CRL. This can be used
by administrators to cut the size of the CRL if it contains
a number of certificates that have now expired, but has
not been rotated due to no further certificates being revoked.
<br /><br />This is a root-protected endpoint.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/pki/crl/rotate`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"success": true
}
}
```
</dd>
</dl>
### /pki/issue/
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Generates a new set of credentials (private key and
certificate) based on the named role. The issuing CA
certificate is returned as well, so that only the root CA
need be in a client's trust store.
<br /><br />*The private key is _not_ stored.
If you do not save the private key, you will need to
request a new certificate.*
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/pki/issue/<name>`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">common_name</span>
<span class="param-flags">required</span>
The requested CN for the certificate. If the CN is allowed
by role policy, it will be issued.
</li>
<li>
<span class="param">alt_names</span>
<span class="param-flags">optional</span>
Requested Subject Alternative Names, in a comma-delimited
list. If any requested names do not match role policy,
the entire request will be denied.
</li>
<li>
<span class="param">ip_sans</span>
<span class="param-flags">optional</span>
Requested IP Subject Alternative Names, in a comma-delimited
list. Only valid if the role allows IP SANs (which is the
default).
</li>
<li>
<span class="param">lease</span>
<span class="param-flags">optional</span>
Requested lease time. Cannot be greater than the role's
`lease_max` parameter. If not provided, the role's `lease`
value will be used.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"lease_id": "pki/issue/test/7ad6cfa5-f04f-c62a-d477-f33210475d05",
"renewable": false,
"lease_duration": 21600,
"data": {
"certificate": "-----BEGIN CERTIFICATE-----\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\n...\numkqeYeO30g1uYvDuWLXVA==\n-----END CERTIFICATE-----\n",
"issuing_ca": "-----BEGIN CERTIFICATE-----\nMIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV\n...\nG/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==\n-----END CERTIFICATE-----\n",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAnVHfwoKsUG1GDVyWB1AFroaKl2ImMBO8EnvGLRrmobIkQvh+\n...\nQN351pgTphi6nlCkGPzkDuwvtxSxiCWXQcaxrHAL7MiJpPzkIBq1\n-----END RSA PRIVATE KEY-----\n",
"serial": "39:dd:2e:90:b7:23:1f:8d:d3:7d:31:c5:1b:da:84:d0:5b:65:31:58"
},
"auth": null
}
```
</dd>
</dl>
### /pki/revoke
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Revokes a certificate using its serial number. This is an
alternative option to the standard method of revoking
using Vault lease IDs. A successful revocation will
rotate the CRL.
<br /><br />This is a root-protected endpoint.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/pki/revoke`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">serial</span>
<span class="param-flags">required</span>
The serial number of the certificate to revoke, in
hyphen-separated or colon-separated octal.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"revocation_time": 1433269787
}
}
```
</dd>
</dl>
### /pki/roles/
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Creates or updates the role definition. Note that
the `allowed_base_domain`, `allow_token_displayname`,
`allow_subdomains`, and `allow_any_name` attributes
are additive; between them nearly and across multiple
roles nearly any issuing policy can be accommodated.
`server_flag`, `client_flag`, and `code_signing_flag`
are additive as well. If a client requests a
certificate that is not allowed by the CN policy in
the role, the request is denied.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/pki/roles/<name>`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">lease</span>
<span class="param-flags">optional</span>
The lease value provided as a string duration
with time suffix. Hour is the largest suffix.
If not set, uses the value of `lease_max`.
</li>
<li>
<span class="param">lease_max</span>
<span class="param-flags">required</span>
The maximum lease value provided as a string duration
with time suffix. Hour is the largest suffix.
</li>
<li>
<span class="param">allow_localhost</span>
<span class="param-flags">optional</span>
If set, clients can request certificates for `localhost`
as one of the requested common names. This is useful
for testing and to allow clients on a single host to
talk securely.
Defaults to true.
</li>
<li>
<span class="param">allowed_base_domain</span>
<span class="param-flags">optional</span>
If set, clients can request certificates for subdomains
directly off of this base domain. _This includes the
wildcard subdomain._ For instance, a base_domain of
`example.com` allows clients to request certificates for
`foo.example.com` and `*.example.com`. To allow further
levels of subdomains, enable the `allow_subdomains` option.
There is no default.
</li>
<li>
<span class="param">allow_token_displayname</span>
<span class="param-flags">optional</span>
If set, clients can request certificates matching
the value of Display Name from the requesting token.
Remember, this stacks with the other CN options,
including `allowed_base_domain`. Defaults to `false`.
</li>
<li>
<span class="param">allow_subdomains</span>
<span class="param-flags">optional</span>
If set, clients can request certificates with CNs that
are subdomains of the CNs allowed by the other role
options. _This includes wildcard subdomains._ This is
redundant when using the `allow_any_name` option.
Defaults to `false`.
</li>
<li>
<span class="param">allow_any_name</span>
<span class="param-flags">optional</span>
If set, clients can request any CN. Useful in some
circumstances, but make sure you understand whether it
is appropriate for your installation before enabling it.
Defaults to `false`.
</li>
<li>
<span class="param">allow_ip_sans</span>
<span class="param-flags">optional</span>
If set, clients can request IP Subject Alternative
Names. Unlike CNs, no authorization checking is
performed except to verify that the given values
are valid IP addresses. Defaults to `true`.
<li>
<span class="param">server_flag</span>
<span class="param-flags">optional</span>
If set, certificates are flagged for server use.
Defaults to `true`.
</li>
<li>
<span class="param">client_flag</span>
<span class="param-flags">optional</span>
If set, certificates are flagged for client use.
Defaults to `true`.
</li>
<li>
<span class="param">code_signing_flag</span>
<span class="param-flags">optional</span>
If set, certificates are flagged for code signing
use. Defaults to `false`.
</li>
<li>
<span class="param">key_type</span>
<span class="param-flags">optional</span>
The type of key to generate for generated private
keys. Currently, `rsa` and `ec` are supported.
Defaults to `rsa`.
</li>
<li>
<span class="param">key_bits</span>
<span class="param-flags">optional</span>
The number of bits to use for the generated keys.
Defaults to `2048`; this will need to be changed for
`ec` keys. See https://golang.org/pkg/crypto/elliptic/#Curve
for an overview of allowed bit lengths for `ec`.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
</dl>
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Queries the role definition.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/pki/roles/<name>`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"allow_any_name": false,
"allow_ip_sans": true,
"allow_localhost": true,
"allow_subdomains": false,
"allow_token_displayname": false,
"allowed_base_domain": "example.com",
"client_flag": true,
"code_signing_flag": false,
"key_bits": 2048,
"key_type": "rsa",
"lease": "6h",
"lease_max": "12h",
"server_flag": true
}
}
```
</dd>
</dl>
#### DELETE
<dl class="api">
<dt>Description</dt>
<dd>
Deletes the role definition. Deleting a role does <b>not</b> revoke
certificates previously issued under this role.
</dd>
<dt>Method</dt>
<dd>DELETE</dd>
<dt>URL</dt>
<dd>`/pki/roles/<name>`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
</dl>

View file

@ -106,6 +106,10 @@
<a href="/docs/secrets/consul/index.html">Consul</a>
</li>
<li<%= sidebar_current("docs-secrets-pki") %>>
<a href="/docs/secrets/pki/index.html">PKI (Certificates)</a>
</li>
<li<%= sidebar_current("docs-secrets-postgresql") %>>
<a href="/docs/secrets/postgresql/index.html">PostgreSQL</a>
</li>