├── .gitignore ├── .travis.yml ├── Gomfile ├── LICENSE ├── Makefile ├── README.md ├── Vendors ├── api └── api.go ├── config ├── admin.go ├── admin_test.go ├── node.go ├── node_test.go ├── org.go └── org_test.go ├── crypto ├── crypto.go ├── crypto_test.go ├── doc.go ├── helpers.go └── helpers_test.go ├── document ├── ca.go.ig ├── container.go ├── container_test.go ├── document.go └── document_test.go ├── entity ├── entity.go └── entity_test.go ├── fs ├── api.go ├── api_test.go ├── helpers.go ├── home.go ├── home_test.go ├── local.go └── local_test.go ├── index ├── helpers.go ├── node.go ├── node_test.go ├── org.go └── org_test.go ├── node ├── node.go └── node_test.go ├── ssh └── ssh.go └── x509 ├── ca.go ├── ca_test.go ├── certificate.go ├── certificate_test.go ├── csr.go ├── csr_test.go ├── helpers.go └── x509_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | _vendor/ 4 | .vendor/ 5 | 6 | # intellij 7 | .idea 8 | *.iml 9 | 10 | fs/fs-test 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.5.1 4 | - 1.4 5 | sudo: false 6 | before_install: 7 | - mkdir /tmp/fdm 8 | - wget https://raw.githubusercontent.com/pki-io/fdm/master/fdm -O /tmp/fdm/fdm 9 | - chmod +x /tmp/fdm/fdm 10 | - export PATH=$PATH:/tmp/fdm 11 | - go get github.com/stretchr/testify 12 | - go get github.com/modocache/gover 13 | - go get github.com/axw/gocov/gocov 14 | - go get github.com/mattn/goveralls 15 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 16 | script: 17 | - make get-deps 18 | - fdm test -coverprofile=config.coverprofile ./config 19 | - fdm test -coverprofile=crypto.coverprofile ./crypto 20 | - fdm test -coverprofile=document.coverprofile ./document 21 | - fdm test -coverprofile=entity.coverprofile ./entity 22 | - fdm test -coverprofile=fs.coverprofile ./fs 23 | - fdm test -coverprofile=index.coverprofile ./index 24 | - fdm test -coverprofile=node.coverprofile ./node 25 | - fdm test -coverprofile=x509.coverprofile ./x509 26 | - gover 27 | - goveralls -coverprofile=gover.coverprofile -service travis-ci 28 | -------------------------------------------------------------------------------- /Gomfile: -------------------------------------------------------------------------------- 1 | gom "github.com/BurntSushi/toml" 2 | gom "github.com/mitchellh/packer" 3 | gom "github.com/stretchr/testify" 4 | gom "github.com/xeipuuv/gojsonschema" 5 | gom "golang.org/x/crypto/pbkdf2" 6 | gom "github.com/mitchellh/go-homedir" 7 | gom "github.com/pki-io/ecies" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 The pki.io Authors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DIRS = api config crypto document entity fs index node x509 2 | 3 | default: get-deps test 4 | 5 | get-deps: 6 | fdm 7 | 8 | test: 9 | fdm test ./... 10 | 11 | dev: clean get-deps 12 | mkdir -p _vendor/src/github.com/pki-io/core && \ 13 | for d in $(DIRS); do (cd _vendor/src/github.com/pki-io/core && ln -s ../../../../../$$d .); done && \ 14 | test ! -d _vendor/pkg || rm -rf _vendor/pkg 15 | fdm 16 | fdm get github.com/stretchr/testify 17 | 18 | lint: 19 | fdm --exec gometalinter ./... 20 | 21 | clean: 22 | test ! -d _vendor || rm -rf _vendor/* 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pki.io 2 | Main pki.io repo 3 | 4 | [![Build Status](https://travis-ci.org/pki-io/core.svg?branch=master)](https://travis-ci.org/pki-io/core) 5 | [![Coverage Status](https://coveralls.io/repos/pki-io/core/badge.svg)](https://coveralls.io/r/pki-io/core) 6 | [![Stories in Ready](https://badge.waffle.io/pki-io/core.png?label=ready&title=Ready)](https://waffle.io/pki-io/core) 7 | [![GoDoc](https://godoc.org/github.com/pki-io/core?status.svg)](https://godoc.org/github.com/pki-io/core) 8 | 9 | Hello there. 10 | I'm sad to say that I have decided to completely stop working on the pki.io project. I haven't actively worked on it for about a year and don't see this changing any time soon.... 11 | 12 | The project was started because as a sysadmin I had the need to easily manage and deploy TLS certificates in a secure way. I think the usability and security models of pki.io are still unique and powerful, but this project was started before Let's Encrypt and Netflix's Lemur were announced, so there are probably better alternatives out there now. 13 | 14 | On a positive note, writing an open source security tool meant needing to threat model it in a modern, code-driven way. Thus pki.io gave birth to ThreatSpec.org which aims to make continuous threat modelling through code a reality. I'll definitely continue to work on ThreatSpec because it addresses a more general problem and I think that code-driven continuous threat modelling is a natural evolution of security shifting left. 15 | 16 | I'd absolutely love it if you could fill out a survey on threat modelling as it would help me define the future of ThreatSpec: https://www.surveymonkey.com/r/N7SR5J6 17 | 18 | If you'd like to help out with ThreatSpec, have thoughts or suggestions, check out the site at http://threatspec.org or talk to us on Twitter @ThreatSpec. 19 | 20 | So, what's the future for pki.io? Well, I'll leave the source code on GitHub [1]. If you'd like to adopt the project in some way, drop me an email to fraser@pki.io. Also, feel free to email me if you have any questions about pki.io. 21 | 22 | Thanks to everyone for your thoughts, feedback, code contributions and support. 23 | 24 | Farewell and all the best, 25 | Fraser 26 | -------------------------------------------------------------------------------- /Vendors: -------------------------------------------------------------------------------- 1 | vendor -f "github.com/pki-io/docopt.go" -r "github.com/docopt/docopt-go" 2 | vendor -f "github.com/pki-io/seelog" -r "github.com/cihub/seelog" 3 | vendor -f "github.com/pki-io/toml" -r "github.com/BurntSushi/toml" 4 | vendor -f "github.com/pki-io/go-homedir" -r "github.com/mitchellh/go-homedir" 5 | vendor -f "github.com/pki-io/ecies" -r "github.com/obscuren/ecies" 6 | 7 | vendor -f "github.com/pki-io/gojsonpointer" -r "github.com/xeipuuv/gojsonpointer" 8 | vendor -f "github.com/pki-io/gojsonreference" -r "github.com/xeipuuv/gojsonreference" 9 | vendor -f "github.com/pki-io/gojsonschema" -r "github.com/xeipuuv/gojsonschema" 10 | 11 | vendor --clone -f "github.com/pki-io/crypto" -r "golang.org/x/crypto" 12 | vendor --build -f "github.com/pki-io/crypto" -r "golang.org/x/crypto" -p "pbkdf2" 13 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type Api struct { 4 | Id string 5 | Path string 6 | } 7 | 8 | type Apier interface { 9 | Connect(string) error 10 | Authenticate(string, string) error 11 | SendPublic(string, string, string) error 12 | GetPublic(string, string) (string, error) 13 | SendPrivate(string, string, string) error 14 | GetPrivate(string, string) (string, error) 15 | DeletePrivate(string, string) error 16 | Push(string, string, string, string) error 17 | PushIncoming(string, string, string) error 18 | PushOutgoing(string, string, string) error 19 | Pop(string, string, string) (string, error) 20 | PopIncoming(string, string) (string, error) 21 | PopOutgoing(string, string) (string, error) 22 | Size(string, string, string) (int, error) 23 | OutgoingSize(string, string) (int, error) 24 | IncomingSize(string, string) (int, error) 25 | } 26 | -------------------------------------------------------------------------------- /config/admin.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/BurntSushi/toml" 7 | ) 8 | 9 | type AdminOrgData struct { 10 | Name string `toml:"name"` 11 | Id string `toml:"id"` 12 | AdminId string `toml:"admin_id"` 13 | } 14 | 15 | type AdminConfigData struct { 16 | Org []AdminOrgData `toml:"org"` 17 | } 18 | 19 | type AdminConfig struct { 20 | Data AdminConfigData 21 | } 22 | 23 | func NewAdmin() (*AdminConfig, error) { 24 | conf := new(AdminConfig) 25 | conf.Data = AdminConfigData{} 26 | 27 | return conf, nil 28 | } 29 | 30 | func (conf *AdminConfig) Dump() (string, error) { 31 | buf := new(bytes.Buffer) 32 | if err := toml.NewEncoder(buf).Encode(conf.Data); err != nil { 33 | return "", fmt.Errorf("Could not encode config: %s", err) 34 | } 35 | return string(buf.Bytes()), nil 36 | } 37 | 38 | func (conf *AdminConfig) Load(tomlString string) error { 39 | data := new(AdminConfigData) 40 | if _, err := toml.Decode(tomlString, data); err != nil { 41 | return fmt.Errorf("Could not decode config: %s", err) 42 | } 43 | conf.Data = *data 44 | return nil 45 | } 46 | 47 | func (conf *AdminConfig) AddOrg(name, id, adminId string) error { 48 | for _, org := range conf.Data.Org { 49 | if org.Name == name { 50 | return fmt.Errorf("Org '%s' already exists", name) 51 | } 52 | } 53 | conf.Data.Org = append(conf.Data.Org, AdminOrgData{name, id, adminId}) 54 | return nil 55 | } 56 | 57 | func (conf *AdminConfig) GetOrg(name string) (*AdminOrgData, error) { 58 | for _, org := range conf.Data.Org { 59 | if org.Name == name { 60 | return &org, nil 61 | } 62 | } 63 | return nil, fmt.Errorf("Could not find org %s", name) 64 | } 65 | 66 | func (conf *AdminConfig) GetOrgs() []AdminOrgData { 67 | return conf.Data.Org 68 | } 69 | 70 | func (conf *AdminConfig) OrgExists(name string) bool { 71 | for _, org := range conf.Data.Org { 72 | if org.Name == name { 73 | return true 74 | } 75 | } 76 | return false 77 | } 78 | -------------------------------------------------------------------------------- /config/admin_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestAdminConfigNew(t *testing.T) { 9 | conf, err := NewAdmin() 10 | assert.Nil(t, err) 11 | assert.NotNil(t, conf) 12 | } 13 | 14 | func TestAdminConfigDumpLoad(t *testing.T) { 15 | conf, _ := NewAdmin() 16 | conf.AddOrg("org1", "000", "123") 17 | configString, err := conf.Dump() 18 | assert.Nil(t, err) 19 | 20 | newConf, _ := NewAdmin() 21 | err = newConf.Load(configString) 22 | assert.Nil(t, err) 23 | assert.Equal(t, conf.Data, newConf.Data) 24 | } 25 | 26 | func TestAdminDuplicateOrg(t *testing.T) { 27 | conf, _ := NewAdmin() 28 | err := conf.AddOrg("org1", "000", "123") 29 | assert.NoError(t, err) 30 | err = conf.AddOrg("org1", "666", "777") 31 | assert.Error(t, err) 32 | } 33 | 34 | func TestAdminConfigGetOrg(t *testing.T) { 35 | conf, _ := NewAdmin() 36 | conf.AddOrg("org1", "000", "111") 37 | org, err := conf.GetOrg("org1") 38 | assert.Nil(t, err) 39 | assert.Equal(t, org.Name, "org1") 40 | } 41 | -------------------------------------------------------------------------------- /config/node.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/BurntSushi/toml" 7 | ) 8 | 9 | const nodeFile string = "node.conf" 10 | 11 | type NodeData struct { 12 | Name string `toml:"name"` 13 | Id string `toml:"id"` 14 | Index string `toml:"index"` 15 | OrgId string `toml:"org_id"` 16 | } 17 | 18 | type NodeConfigData struct { 19 | Node []NodeData `toml:"node"` 20 | } 21 | 22 | type NodeConfig struct { 23 | Data NodeConfigData 24 | } 25 | 26 | func NewNode() (*NodeConfig, error) { 27 | conf := new(NodeConfig) 28 | conf.Data = NodeConfigData{} 29 | return conf, nil 30 | } 31 | 32 | func (conf *NodeConfig) Dump() (string, error) { 33 | buf := new(bytes.Buffer) 34 | if err := toml.NewEncoder(buf).Encode(conf.Data); err != nil { 35 | return "", fmt.Errorf("Could not encode config: %s", err) 36 | } 37 | return string(buf.Bytes()), nil 38 | } 39 | 40 | func (conf *NodeConfig) Load(tomlString string) error { 41 | data := new(NodeConfigData) 42 | 43 | if _, err := toml.Decode(tomlString, data); err != nil { 44 | return fmt.Errorf("Could not decode config: %s", err) 45 | } 46 | conf.Data = *data 47 | return nil 48 | } 49 | 50 | func (conf *NodeConfig) AddNode(name, id, indexId, orgId string) error { 51 | for _, node := range conf.Data.Node { 52 | if node.Name == name { 53 | return fmt.Errorf("Could not add node '%s' as one with that name already exists", name) 54 | } 55 | } 56 | conf.Data.Node = append(conf.Data.Node, NodeData{name, id, indexId, orgId}) 57 | return nil 58 | } 59 | 60 | func (conf *NodeConfig) GetNode(name string) (*NodeData, error) { 61 | for _, node := range conf.Data.Node { 62 | if node.Name == name { 63 | return &node, nil 64 | } 65 | } 66 | return nil, fmt.Errorf("Couldn't find node: %s", name) 67 | } 68 | 69 | func (conf *NodeConfig) RemoveNode(name string) error { 70 | for i, node := range conf.Data.Node { 71 | if node.Name == name { 72 | conf.Data.Node = append(conf.Data.Node[:i], conf.Data.Node[i+1:]...) 73 | return nil 74 | } 75 | } 76 | return fmt.Errorf("key %s does not exist: %s", name) 77 | } 78 | 79 | func (conf *NodeConfig) OrgExists(orgId string) bool { 80 | for _, node := range conf.Data.Node { 81 | if node.OrgId == orgId { 82 | return true 83 | } 84 | } 85 | return false 86 | } 87 | -------------------------------------------------------------------------------- /config/node_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestNodeConfigNew(t *testing.T) { 9 | conf, err := NewNode() 10 | assert.Nil(t, err) 11 | assert.NotNil(t, conf) 12 | } 13 | 14 | func TestNodeConfigDumpLoad(t *testing.T) { 15 | conf, _ := NewNode() 16 | conf.AddNode("node1", "000", "123", "666") 17 | configString, err := conf.Dump() 18 | assert.Nil(t, err) 19 | 20 | newConf, _ := NewNode() 21 | err = newConf.Load(configString) 22 | assert.Nil(t, err) 23 | assert.Equal(t, conf.Data, newConf.Data) 24 | } 25 | 26 | func TestDuplicateAddNode(t *testing.T) { 27 | conf, _ := NewNode() 28 | err := conf.AddNode("node1", "000", "123", "666") 29 | assert.NoError(t, err) 30 | err = conf.AddNode("node1", "666", "777", "888") 31 | assert.Error(t, err) 32 | } 33 | 34 | func TestNodeConfigGetNode(t *testing.T) { 35 | conf, _ := NewNode() 36 | conf.AddNode("node1", "000", "111", "666") 37 | node, err := conf.GetNode("node1") 38 | assert.Nil(t, err) 39 | assert.Equal(t, node.Name, "node1") 40 | } 41 | -------------------------------------------------------------------------------- /config/org.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/BurntSushi/toml" 7 | ) 8 | 9 | const orgFile string = "org.conf" 10 | 11 | type OrgConfigData struct { 12 | Name string `toml:"name"` 13 | Id string `toml:"id"` 14 | Index string `toml:"index"` 15 | } 16 | 17 | type OrgConfig struct { 18 | Data OrgConfigData 19 | } 20 | 21 | func NewOrg() (*OrgConfig, error) { 22 | conf := new(OrgConfig) 23 | conf.Data = OrgConfigData{} 24 | return conf, nil 25 | } 26 | 27 | func (conf *OrgConfig) Dump() (string, error) { 28 | buf := new(bytes.Buffer) 29 | if err := toml.NewEncoder(buf).Encode(conf.Data); err != nil { 30 | return "", fmt.Errorf("Could not encode config: %s", err) 31 | } 32 | return string(buf.Bytes()), nil 33 | } 34 | 35 | func (conf *OrgConfig) Load(tomlString string) error { 36 | data := new(OrgConfigData) 37 | 38 | if _, err := toml.Decode(tomlString, data); err != nil { 39 | return fmt.Errorf("Could not decode config: %s", err) 40 | } 41 | conf.Data = *data 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /config/org_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestOrgConfigNew(t *testing.T) { 9 | conf, err := NewOrg() 10 | assert.Nil(t, err) 11 | assert.NotNil(t, conf) 12 | } 13 | 14 | func TestOrgConfigDumpLoad(t *testing.T) { 15 | conf, _ := NewOrg() 16 | conf.Data.Name = "org1" 17 | conf.Data.Id = "123" 18 | configString, err := conf.Dump() 19 | assert.Nil(t, err) 20 | 21 | newConf, _ := NewOrg() 22 | err = newConf.Load(configString) 23 | assert.Nil(t, err) 24 | assert.Equal(t, conf.Data, newConf.Data) 25 | } 26 | -------------------------------------------------------------------------------- /crypto/crypto.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/crypto as crypto 2 | package crypto 3 | 4 | import ( 5 | "crypto/ecdsa" 6 | "crypto/rsa" 7 | "encoding/hex" 8 | "fmt" 9 | ) 10 | 11 | // Mode of signature or encryption 12 | type Mode string 13 | 14 | // Signature modes 15 | const ( 16 | SignatureModeSha256Rsa Mode = "sha256+rsa" 17 | SignatureModeSha256Ecdsa Mode = "sha256+ecdsa" 18 | SignatureModeSha256Hmac Mode = "sha256+hmac" 19 | ) 20 | 21 | // TODO - encryption mode consts 22 | 23 | // Encrypted represents a ciphertext with related inputs 24 | type Encrypted struct { 25 | Ciphertext string 26 | Mode string 27 | Inputs map[string]string 28 | Keys map[string]string 29 | } 30 | 31 | // Signed represents a signature and related inputs 32 | type Signed struct { 33 | Message string 34 | Mode Mode 35 | Signature string 36 | } 37 | 38 | // ThreatSpec TMv0.1 for NewSignature 39 | // Does new signature creation for App:Crypto 40 | 41 | // NewSignature returns a new Signed 42 | func NewSignature(mode Mode) *Signed { 43 | return &Signed{Mode: mode} 44 | } 45 | 46 | // ThreatSpec TMv0.1 for GroupEncrypt 47 | // Does hybrid encryption with one or more public keys fore App:Crypto 48 | 49 | // GroupEncrypt takes a plaintext and encrypts with one or more public keys. 50 | func GroupEncrypt(plaintext string, publicKeys map[string]string) (*Encrypted, error) { 51 | 52 | keySize := 32 53 | key, err := RandomBytes(keySize) 54 | if err != nil { 55 | return nil, err 56 | } 57 | ciphertext, iv, err := AESEncrypt([]byte(plaintext), key) 58 | if err != nil { 59 | return nil, err 60 | } 61 | inputs := make(map[string]string) 62 | inputs["iv"] = string(Base64Encode(iv)) 63 | 64 | encryptedKeys := make(map[string]string) 65 | for id, publicKeyString := range publicKeys { 66 | publicKey, err := PemDecodePublic([]byte(publicKeyString)) 67 | encryptedKey, err := Encrypt(key, publicKey) 68 | if err != nil { 69 | return nil, err 70 | } 71 | encryptedKeys[id] = string(Base64Encode(encryptedKey)) 72 | } 73 | 74 | return &Encrypted{Ciphertext: string(Base64Encode(ciphertext)), Mode: "aes-cbc-256+rsa", Inputs: inputs, Keys: encryptedKeys}, nil 75 | } 76 | 77 | // ThreatSpec TMv0.1 for SymmetricEncrypt 78 | // Does symmetric encryption with a shared key for App:Crypto 79 | 80 | // SymmetricEncrypt takes a plaintext and symmetrically encrypts using the given key. 81 | func SymmetricEncrypt(plaintext, id, key string) (*Encrypted, error) { 82 | 83 | rawKey, err := hex.DecodeString(key) 84 | if err != nil { 85 | return nil, fmt.Errorf("Could not decode key: %s", err) 86 | } 87 | 88 | newKey, salt, err := ExpandKey(rawKey, nil) 89 | if err != nil { 90 | return nil, fmt.Errorf("Cold not expand key: %s", err) 91 | } 92 | 93 | ciphertext, iv, err := AESEncrypt([]byte(plaintext), newKey) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | inputs := make(map[string]string) 99 | inputs["key-id"] = id 100 | inputs["iv"] = string(Base64Encode(iv)) 101 | inputs["salt"] = string(Base64Encode(salt)) 102 | 103 | return &Encrypted{Ciphertext: string(Base64Encode(ciphertext)), Mode: "aes-cbc-256", Inputs: inputs}, nil 104 | } 105 | 106 | // ThreatSpec TMv0.1 for GroupDecrypt 107 | // Does hybrid decryption with a private key for App:Crypto 108 | 109 | // GroupDecrypt takes an Encrypted struct and decrypts for the given private key, returning a plaintext string. 110 | func GroupDecrypt(encrypted *Encrypted, keyID string, privateKeyPem string) (string, error) { 111 | var privateKey interface{} 112 | var err error 113 | 114 | if encrypted.Mode != "aes-cbc-256+rsa" { 115 | return "", fmt.Errorf("Invalid mode '%s'", encrypted.Mode) 116 | } 117 | 118 | if len(privateKeyPem) == 0 { 119 | return "", fmt.Errorf("Private key pem is 0 bytes") 120 | } 121 | 122 | // TODO - check errors 123 | ciphertext, _ := Base64Decode([]byte(encrypted.Ciphertext)) 124 | iv, _ := Base64Decode([]byte(encrypted.Inputs["iv"])) 125 | encryptedKey, _ := Base64Decode([]byte(encrypted.Keys[keyID])) 126 | privateKey, err = PemDecodePrivate([]byte(privateKeyPem)) 127 | key, err := Decrypt(encryptedKey, privateKey) 128 | plaintext, err := AESDecrypt(ciphertext, iv, key) 129 | return string(plaintext), err 130 | } 131 | 132 | // ThreatSpec TMv0.1 for SymmetricDecrypt 133 | // Does symmetric decryption with a shared key for App:Crypto 134 | 135 | // SymmetricDecrypt takes an Encrypted struct and decrypts with the given symmetric key, returning a plaintext string. 136 | func SymmetricDecrypt(encrypted *Encrypted, key string) (string, error) { 137 | if encrypted.Mode != "aes-cbc-256" { 138 | return "", fmt.Errorf("Invalid mode: %s", encrypted.Mode) 139 | } 140 | 141 | // TODO - check errors 142 | ciphertext, _ := Base64Decode([]byte(encrypted.Ciphertext)) 143 | iv, _ := Base64Decode([]byte(encrypted.Inputs["iv"])) 144 | salt, _ := Base64Decode([]byte(encrypted.Inputs["salt"])) 145 | 146 | rawKey, err := hex.DecodeString(key) 147 | if err != nil { 148 | return "", fmt.Errorf("Could not decode key: %s", err) 149 | } 150 | 151 | newKey, _, err := ExpandKey(rawKey, salt) 152 | if err != nil { 153 | return "", fmt.Errorf("Cold not expand key: %s", err) 154 | } 155 | 156 | plaintext, err := AESDecrypt(ciphertext, iv, newKey) 157 | if err != nil { 158 | return "", err 159 | } 160 | 161 | return string(plaintext), nil 162 | } 163 | 164 | // ThreatSpec TMv0.1 for Sign 165 | // Does message signing for App:Crypto 166 | 167 | // Sign takes a message string and signs using the given private key. The signature and inputs are added to the provided Signed input. 168 | func Sign(message string, privateKeyString string, signature *Signed) error { 169 | privateKey, err := PemDecodePrivate([]byte(privateKeyString)) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | switch privateKey.(type) { 175 | case *rsa.PrivateKey: 176 | signature.Mode = SignatureModeSha256Rsa 177 | case *ecdsa.PrivateKey: 178 | signature.Mode = SignatureModeSha256Ecdsa 179 | } 180 | sig, err := SignMessage([]byte(message), privateKey) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | signature.Message = message 186 | signature.Signature = string(Base64Encode(sig)) 187 | return nil 188 | } 189 | 190 | // ThreatSpec TMv0.1 for Authenticate 191 | // Does message authentication for App:Crypto 192 | 193 | // Authenticate takes a message and MACs using the given key. The signature and inputs are added to the provided Signed input. 194 | func Authenticate(message string, key []byte, signature *Signed) error { 195 | 196 | if err := HMAC([]byte(message), key, signature); err != nil { 197 | return fmt.Errorf("Could not HMAC container: %s", err) 198 | } 199 | 200 | signature.Mode = SignatureModeSha256Hmac 201 | return nil 202 | } 203 | 204 | // ThreatSpec TMv0.1 for Verify 205 | // Does signature verification for App:Crypto 206 | 207 | // Verify takes a Signed struct and verifies the signature using the given key. It supports both symmetric (MAC) and public key signatures. 208 | func Verify(signed *Signed, key []byte) error { 209 | message := []byte(signed.Message) 210 | signature, _ := Base64Decode([]byte(signed.Signature)) 211 | 212 | if signed.Mode == SignatureModeSha256Hmac { 213 | return HMACVerify(message, key, signature) 214 | } 215 | 216 | publicKey, err := PemDecodePublic(key) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | return VerifySignature(message, signature, publicKey) 222 | } 223 | -------------------------------------------------------------------------------- /crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "encoding/hex" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestSymmetricEncryptDecrypt(t *testing.T) { 10 | rawID, _ := RandomBytes(16) 11 | rawKey, _ := RandomBytes(16) 12 | 13 | id := hex.EncodeToString(rawID) 14 | key := hex.EncodeToString(rawKey) 15 | 16 | message := "this is a secret" 17 | encrypted, err := SymmetricEncrypt(message, id, key) 18 | assert.Nil(t, err) 19 | assert.NotNil(t, encrypted.Ciphertext) 20 | assert.NotEqual(t, len(encrypted.Ciphertext), 0) 21 | assert.NotEqual(t, encrypted.Ciphertext, message) 22 | 23 | newMessage, err := SymmetricDecrypt(encrypted, key) 24 | assert.Nil(t, err) 25 | assert.Equal(t, message, newMessage) 26 | } 27 | 28 | func TestAuthenticateVerify(t *testing.T) { 29 | key, _ := RandomBytes(16) 30 | 31 | message := "auth this" 32 | sig := new(Signed) 33 | 34 | err := Authenticate(message, key, sig) 35 | assert.Nil(t, err) 36 | 37 | err = Verify(sig, key) 38 | assert.Nil(t, err) 39 | } 40 | 41 | func TestGroupEncrypt(t *testing.T) { 42 | key1, _ := GenerateRSAKey() 43 | key2, _ := GenerateECKey() 44 | keys := make(map[string]string) 45 | k1, _ := PemEncodePublic(&key1.PublicKey) 46 | k2, _ := PemEncodePublic(&key2.PublicKey) 47 | keys["1"] = string(k1) 48 | keys["2"] = string(k2) 49 | 50 | plaintext := "this is a secret message" 51 | e, err := GroupEncrypt(plaintext, keys) 52 | assert.NoError(t, err) 53 | assert.NotNil(t, e) 54 | } 55 | 56 | func TestGroupDecrypt(t *testing.T) { 57 | key1, _ := GenerateRSAKey() 58 | key2, _ := GenerateECKey() 59 | keys := make(map[string]string) 60 | k1, _ := PemEncodePublic(&key1.PublicKey) 61 | k2, _ := PemEncodePublic(&key2.PublicKey) 62 | keys["1"] = string(k1) 63 | keys["2"] = string(k2) 64 | 65 | plaintext := "this is a secret message" 66 | e, _ := GroupEncrypt(plaintext, keys) 67 | pk1, _ := PemEncodePrivate(key1) 68 | newPlaintext, err := GroupDecrypt(e, "1", string(pk1)) 69 | assert.NoError(t, err) 70 | assert.Equal(t, plaintext, newPlaintext) 71 | } 72 | 73 | func TestSign(t *testing.T) { 74 | message := "this is a message" 75 | rsakey, _ := GenerateRSAKey() 76 | eckey, _ := GenerateECKey() 77 | 78 | privateKey, _ := PemEncodePrivate(rsakey) 79 | sig := new(Signed) 80 | err := Sign(message, string(privateKey), sig) 81 | assert.NoError(t, err) 82 | assert.NotNil(t, sig.Signature) 83 | 84 | privateKey, _ = PemEncodePrivate(eckey) 85 | sig = new(Signed) 86 | err = Sign(message, string(privateKey), sig) 87 | assert.NoError(t, err) 88 | assert.NotNil(t, sig.Signature) 89 | } 90 | 91 | func TestVerify(t *testing.T) { 92 | message := "this is a message" 93 | rsakey, _ := GenerateRSAKey() 94 | eckey, _ := GenerateECKey() 95 | 96 | privateKey, _ := PemEncodePrivate(rsakey) 97 | sig := new(Signed) 98 | Sign(message, string(privateKey), sig) 99 | 100 | publicKey, _ := PemEncodePublic(&rsakey.PublicKey) 101 | err := Verify(sig, publicKey) 102 | assert.NoError(t, err) 103 | 104 | privateKey, _ = PemEncodePrivate(eckey) 105 | sig = new(Signed) 106 | Sign(message, string(privateKey), sig) 107 | 108 | publicKey, _ = PemEncodePublic(&eckey.PublicKey) 109 | err = Verify(sig, publicKey) 110 | assert.NoError(t, err) 111 | } 112 | 113 | func TestNewHMAC(t *testing.T) { 114 | mac := NewSignature(SignatureModeSha256Hmac) 115 | message := "message to be authenticated" 116 | key, _ := RandomBytes(32) 117 | 118 | HMAC([]byte(message), key, mac) 119 | 120 | signature, _ := Base64Decode([]byte(mac.Signature)) 121 | err := HMACVerify([]byte(message), key, signature) 122 | assert.Nil(t, err) 123 | } 124 | -------------------------------------------------------------------------------- /crypto/doc.go: -------------------------------------------------------------------------------- 1 | // The crypto package contains a number of cryptographic helper functions. It is intended that these are called from other packages, not directly from application code. 2 | package crypto 3 | -------------------------------------------------------------------------------- /crypto/helpers.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/crypto as crypto 2 | package crypto 3 | 4 | import ( 5 | "bytes" 6 | "crypto" 7 | "crypto/aes" 8 | "crypto/cipher" 9 | "crypto/ecdsa" 10 | "crypto/elliptic" 11 | "crypto/hmac" 12 | "crypto/rand" 13 | "crypto/rsa" 14 | "crypto/sha256" 15 | "crypto/x509" 16 | "encoding/base64" 17 | "encoding/pem" 18 | "errors" 19 | "fmt" 20 | "github.com/pki-io/ecies" 21 | "golang.org/x/crypto/pbkdf2" 22 | "io" 23 | "math/big" 24 | "time" 25 | ) 26 | 27 | // https://www.socketloop.com/tutorials/golang-example-for-rsa-package-functions-example 28 | 29 | // KeyType represents a supported public key pair type 30 | type KeyType string 31 | 32 | // Key types 33 | const ( 34 | KeyTypeRSA KeyType = "rsa" 35 | KeyTypeEC KeyType = "ec" 36 | ) 37 | 38 | // ThreatSpec TMv0.1 for TimeOrderedUUID 39 | // Does time-ordered UUID generation for App:Crypto 40 | 41 | // TimeOrderedUUID taken directly from https://github.com/mitchellh/packer/blob/master/common/uuid/uuid.go 42 | func TimeOrderedUUID() string { 43 | unix := uint32(time.Now().UTC().Unix()) 44 | 45 | b := make([]byte, 12) 46 | n, err := rand.Read(b) 47 | if n != len(b) { 48 | err = fmt.Errorf("Not enough entropy available") 49 | } 50 | if err != nil { 51 | panic(err) 52 | } 53 | return fmt.Sprintf("%08x-%04x-%04x-%04x-%04x%08x", 54 | unix, b[0:2], b[2:4], b[4:6], b[6:8], b[8:]) 55 | } 56 | 57 | // ThreatSpec TMv0.1 for UUID 58 | // Does UUID generation for App:Crypto 59 | 60 | // UUID is an opinionated helper function that generate a 128 bit time-ordered UUID string. 61 | // 62 | // Documentation for the TimeOrderedUUID function is available here: 63 | // TODO 64 | // 65 | // From the source docs: Top 32 bits are a timestamp, bottom 96 bytes are random. 66 | func UUID() string { 67 | return TimeOrderedUUID() 68 | } 69 | 70 | // ThreatSpec TMv0.1 for RandomBytes 71 | // Mitigates App:Crypto against Use of Insufficiently Random Values (CWE-330) with standard package which uses secure implementation 72 | 73 | // RandomBytes generates and returns size number of random bytes. 74 | func RandomBytes(size int) ([]byte, error) { 75 | randomBytes := make([]byte, size) 76 | numBytesRead, err := rand.Read(randomBytes) 77 | if err != nil { 78 | return nil, fmt.Errorf("Could not generate random bytes: %s", err) 79 | } 80 | 81 | if numBytesRead != size { 82 | return nil, fmt.Errorf("Wrong number of random bytes read: %i vs %i", size, numBytesRead) 83 | } 84 | 85 | return randomBytes, nil 86 | } 87 | 88 | // ThreatSpec TMv0.1 for Pad 89 | // Does PKCS5 padding for App:Crypto 90 | 91 | // Pad takes the src byte array and PKCS5 pads it to blockSize, returning the padded byte array. 92 | // 93 | // Taken from the tutorial available here: 94 | // https://www.socketloop.com/tutorials/golang-padding-un-padding-data 95 | func Pad(src []byte, blockSize int) []byte { 96 | padding := blockSize - len(src)%blockSize 97 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 98 | return append(src, padtext...) 99 | } 100 | 101 | // ThreatSpec TMv0.1 for Unpad 102 | // Does PKCS5 unpadding for App:Crypto 103 | 104 | // UnPad takes the src byte array and PKCS5 unpads it. 105 | // 106 | // Taken from the tutorial available here: 107 | // https://www.socketloop.com/tutorials/golang-padding-un-padding-data 108 | func UnPad(src []byte) []byte { 109 | length := len(src) 110 | unpadding := int(src[length-1]) 111 | return src[:(length - unpadding)] 112 | } 113 | 114 | // ThreatSpec TMv0.1 for ExpandKey 115 | // Mitigates App:Crypto against Use of Password Hash With Insufficient Computational Effort (CWE-916) with PBKDF2 provided by standard package 116 | // Mitigates App:Crypto against Use of a One-Way Hash without a Salt (CWE-759) with salt create by function 117 | // Mitigates App:Crypto against Use of a One-Way Hash with a Predictable Salt (CWE-760) with salt created with good PRNG 118 | 119 | // ExpandKey is an opinionated helper function to cryptographically expand a key using a 128 bit salt and PBKDF2. 120 | // If the salt is of 0 length, it generates a new salt, and returns the expanded key and salt as byte arrays. 121 | // 122 | // A salt should only be provided as part of a decryption or verification process. When using ExpandKey to create a new key, let ExpandKey generate the salt. This is to lessen the risk of a weak or non-unique salt being used. 123 | func ExpandKey(key, salt []byte) ([]byte, []byte, error) { 124 | if len(salt) == 0 { 125 | var err error 126 | salt, err = RandomBytes(16) // TODO Shouldn't be hardcoded i guess 127 | if err != nil { 128 | return nil, nil, err 129 | } 130 | } 131 | newKey := pbkdf2.Key(key, salt, 100000, 32, sha256.New) 132 | return newKey, salt, nil 133 | } 134 | 135 | // ThreatSpec TMv0.1 for Base64Encode 136 | // Does base64 encoding for App:Crypto 137 | 138 | // Base64Encode returns the base64 encoding of the input. 139 | func Base64Encode(input []byte) []byte { 140 | return []byte(base64.StdEncoding.EncodeToString(input)) 141 | } 142 | 143 | // ThreatSpec TMv0.1 for Base64Decode 144 | // Does base64 decoding for App:Crypto 145 | 146 | // Base64Decode returns the base64 decoded input. 147 | func Base64Decode(input []byte) (decoded []byte, err error) { 148 | b, err := base64.StdEncoding.DecodeString(string(input)) 149 | if err != nil { 150 | return nil, fmt.Errorf("Can't Base64 decode: %s", err) 151 | } 152 | return []byte(b), nil 153 | } 154 | 155 | // ThreatSpec TMv0.1 for AESEncrypt 156 | // Does symmetric encryption for App:Crypto 157 | // Mitigates App:Crypto against weak cipher with strong encryption cipher in CBC mode 158 | // Mitigates App:Crypto against weak cipher with sufficient key size 159 | // Mitigates App:Crypto against failure to use a random IV in CBC mode with generated random IV 160 | 161 | // AESEncrypt is an opinionated helper function that implements 256 bit AES in CBC mode. 162 | // It creates a random 128 bit IV which is returned along with the ciphertext. 163 | func AESEncrypt(plaintext, key []byte) (ciphertext []byte, iv []byte, err error) { 164 | if len(plaintext) == 0 { 165 | return nil, nil, fmt.Errorf("Plaintext can't be empty") 166 | } 167 | 168 | block, err := aes.NewCipher(key) 169 | if err != nil { 170 | return nil, nil, fmt.Errorf("Can't initialise cipher: %s", err) 171 | } 172 | 173 | paddedPlaintext := Pad(plaintext, aes.BlockSize) 174 | ciphertext = make([]byte, len(paddedPlaintext)) 175 | iv, err = RandomBytes(aes.BlockSize) 176 | if err != nil { 177 | return nil, nil, err 178 | } 179 | 180 | mode := cipher.NewCBCEncrypter(block, iv) 181 | mode.CryptBlocks(ciphertext, paddedPlaintext) 182 | 183 | return ciphertext, iv, nil 184 | } 185 | 186 | // ThreatSpec TMv0.1 for AESDecrypt 187 | // Does symmetric decryption for App:Crypto 188 | 189 | // AESDecrypt is an opinionated helper function that decryptes a ciphertext encrypted 190 | // with 256 bit AES in CBC mode and returns the plaintext. 191 | func AESDecrypt(ciphertext, iv, key []byte) ([]byte, error) { 192 | block, err := aes.NewCipher(key) 193 | if err != nil { 194 | return nil, fmt.Errorf("Can't initialise cipher: %s", err) 195 | } 196 | 197 | if len(ciphertext)%aes.BlockSize != 0 { 198 | return nil, fmt.Errorf("ciphertext is not a multiple of the block size") 199 | } 200 | 201 | if len(iv) != aes.BlockSize { 202 | return nil, fmt.Errorf("iv is not equal to block size") 203 | } 204 | 205 | paddedPlaintext := make([]byte, len(ciphertext)) 206 | mode := cipher.NewCBCDecrypter(block, iv) 207 | mode.CryptBlocks(paddedPlaintext, ciphertext) 208 | 209 | return UnPad(paddedPlaintext), nil 210 | } 211 | 212 | // TheatSpec TMv0.1 for GetKeyType 213 | // Does key type identification for App:Crypto 214 | 215 | // GetKeyType returns the key type for a given key 216 | func GetKeyType(key interface{}) (KeyType, error) { 217 | switch t := key.(type) { 218 | case *rsa.PrivateKey: 219 | return KeyTypeRSA, nil 220 | case *ecdsa.PrivateKey: 221 | return KeyTypeEC, nil 222 | default: 223 | return "", fmt.Errorf("Unknown key type: %T", t) 224 | } 225 | } 226 | 227 | // ThreatSpec TMv0.1 for GenerateRSAKey 228 | // Does RSA key generation for App:Crypto 229 | // Mitigates App:Crypto against weak private key with RSA key generated using standard package 230 | // Mitigates App:Crypto against weak private key with sufficient RSa key size of 2048 bits 231 | 232 | // GenerateRSAKey is an opinionated helper function to generate a 2048 bit RSA key pair 233 | func GenerateRSAKey() (*rsa.PrivateKey, error) { 234 | key, err := rsa.GenerateKey(rand.Reader, 2048) 235 | if err != nil { 236 | return nil, fmt.Errorf("Can't create RSA keys: %s", err) 237 | } 238 | return key, nil 239 | } 240 | 241 | // ThreatSpec TMv0.1 for GenerateECKey 242 | // Does Elliptic Curve key generation for App:Crypto 243 | // Exposes App:Crypto to weak private key with EC key generated by third-party package 244 | 245 | // GenerateECKey is an opinionated helper function to generate a P256 ECDSA key pair. 246 | func GenerateECKey() (*ecdsa.PrivateKey, error) { 247 | key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 248 | if err != nil { 249 | return nil, fmt.Errorf("Can't create ECDSA keys: %s", err) 250 | } 251 | return key, nil 252 | } 253 | 254 | // ThreatSpec TMv0.1 for PemEncodePrivate 255 | // Does PEM encoding of private keys for App:Crypto 256 | 257 | // PemEncodePrivate PEM encodes a private key. It supports RSA and ECDSA key types. 258 | func PemEncodePrivate(key crypto.PrivateKey) ([]byte, error) { 259 | 260 | switch k := key.(type) { 261 | case *rsa.PrivateKey: 262 | der := x509.MarshalPKCS1PrivateKey(k) 263 | b := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: der} 264 | return pem.EncodeToMemory(b), nil 265 | case *ecdsa.PrivateKey: 266 | der, err := x509.MarshalECPrivateKey(k) 267 | if err != nil { 268 | return nil, fmt.Errorf("Can't marshal ECDSA key: %s", err) 269 | } 270 | b := &pem.Block{Type: "EC PRIVATE KEY", Bytes: der} 271 | return pem.EncodeToMemory(b), nil 272 | default: 273 | return nil, errors.New("Unsupported private key type") 274 | } 275 | 276 | } 277 | 278 | // ThreatSpec TMv0.1 for PemEncodePublic 279 | // Does PEM encoding of public keys for App:Crypto 280 | 281 | // PemEncodePublic PEM encodes a public key. It supports RSA and ECDSA. 282 | func PemEncodePublic(key crypto.PublicKey) ([]byte, error) { 283 | der, err := x509.MarshalPKIXPublicKey(key) 284 | if err != nil { 285 | return nil, err 286 | } 287 | 288 | var t string 289 | switch key.(type) { 290 | case *rsa.PublicKey: 291 | t = "RSA PUBLIC KEY" 292 | case *ecdsa.PublicKey: 293 | t = "EC PUBLIC KEY" 294 | default: 295 | return nil, errors.New("Unsupported public key type") 296 | } 297 | 298 | b := &pem.Block{Type: t, Bytes: der} 299 | return pem.EncodeToMemory(b), nil 300 | } 301 | 302 | // ThreatSpec TMv0.1 for PemDecodePrivate 303 | // Does PEM decoding of private keys for App:Crypto 304 | 305 | // PemDecodePrivate decodes a PEM encoded private key. It supports PKCS1 and EC private keys. 306 | func PemDecodePrivate(in []byte) (crypto.PrivateKey, error) { 307 | b, _ := pem.Decode(in) 308 | key, err := x509.ParsePKCS1PrivateKey(b.Bytes) 309 | if err != nil { 310 | eckey, err := x509.ParseECPrivateKey(b.Bytes) 311 | if err != nil { 312 | return nil, fmt.Errorf("Could not parse private key: %s", err) 313 | } 314 | return eckey, nil 315 | } 316 | return key, nil 317 | } 318 | 319 | // ThreatSpec TMv0.1 for PemDecodePublic 320 | // Does PEM decodimg of public keys for App:Crypto 321 | 322 | // PemDecodePublic decodes a PEM encoded public key. It supports any PKIX public key. 323 | func PemDecodePublic(in []byte) (crypto.PublicKey, error) { 324 | b, _ := pem.Decode(in) 325 | pubKey, err := x509.ParsePKIXPublicKey(b.Bytes) 326 | if err != nil { 327 | return nil, fmt.Errorf("Could not parse public key: %s", err) 328 | } 329 | return pubKey, nil 330 | } 331 | 332 | // ThreatSpec TMv0.1 for Encrypt 333 | // Does asymmetric encryption for App:Crypto 334 | 335 | // Encrypt is a wrapper function that will encrypt a plaintext using the provided public key, 336 | // and returns the ciphertext. It supports RSA and ECDSA public keys. 337 | func Encrypt(plaintext []byte, publicKey crypto.PublicKey) ([]byte, error) { 338 | switch k := publicKey.(type) { 339 | case *rsa.PublicKey: 340 | return rsaEncrypt(plaintext, k) 341 | case *ecdsa.PublicKey: 342 | return eciesEncrypt(plaintext, k) 343 | default: 344 | return nil, errors.New("Unsupporte public key type") 345 | } 346 | } 347 | 348 | // ThreatSpec TMv0.1 for rsaEncrypt 349 | // Does RSA encryption for App:Crypto 350 | // Mitigates App:Crypto against Use of RSA Algorithm without OAEP (CWE-780) with RSA encryption using OAEP with SHA-256 351 | 352 | // rsaEncrypt is an opinionated helper function that encryptes a plaintext using an RSA public key, 353 | // and returns the ciphertext. It uses OAEP with SHA-256. 354 | func rsaEncrypt(plaintext []byte, publicKey *rsa.PublicKey) ([]byte, error) { 355 | label := []byte("") 356 | hash := sha256.New() 357 | ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, publicKey, plaintext, label) 358 | if err != nil { 359 | return nil, fmt.Errorf("Could not RSA encrypt: %s", err) 360 | } 361 | return ciphertext, nil 362 | } 363 | 364 | // ThreatSpec TMv0.1 for eciesEncrypt 365 | // Does EC IES encryption for App:Crypto 366 | // Mitigates App:Crypto against something else with Elliptic Curve Integrated Encryption Scheme 367 | // Exposes App:Crypto to something bleh with encryption performed by third-party package 368 | 369 | // eciesEncrypt is an opinionated helper function that encryptes a plaintext using an EC DSA public key, 370 | // and returns the ciphertext. 371 | // 372 | // It uses ecies (integrated encryption scheme) provided by an external library, documentation of which is available here: 373 | // https://github.com/obscuren/ecies 374 | func eciesEncrypt(plaintext []byte, publicKey *ecdsa.PublicKey) ([]byte, error) { 375 | pub := ecies.ImportECDSAPublic(publicKey) 376 | return ecies.Encrypt(rand.Reader, pub, plaintext, nil, nil) 377 | } 378 | 379 | // ThreatSpec TMv0.1 for Decrypt 380 | // Does asymmetric decryption for App:Crypto 381 | 382 | // Decrypt is a wrapper function that will decrypt a ciphertext using the provided private key, 383 | // and returns the plaintext. It supports RSA and ECDSA private keys. 384 | func Decrypt(cipherText []byte, privateKey crypto.PrivateKey) ([]byte, error) { 385 | switch k := privateKey.(type) { 386 | case *rsa.PrivateKey: 387 | return rsaDecrypt(cipherText, k) 388 | case *ecdsa.PrivateKey: 389 | return eciesDecrypt(cipherText, k) 390 | default: 391 | return nil, errors.New("Unsupported private key type") 392 | } 393 | } 394 | 395 | // ThreatSpec TMv0.1 for rsaDecrypt 396 | // Does RSA decryption for App:Crypto 397 | 398 | // rsaDecrypt is an opinionated helper function that decryptes a ciphertext using an RSA private key. 399 | // It uses OAEP with SHA-256. 400 | func rsaDecrypt(ciphertext []byte, privateKey *rsa.PrivateKey) ([]byte, error) { 401 | label := []byte("") 402 | hash := sha256.New() 403 | plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, privateKey, ciphertext, label) 404 | if err != nil { 405 | return nil, fmt.Errorf("Could not RSA decrypt: %s", err) 406 | } 407 | return plaintext, nil 408 | } 409 | 410 | // ThreatSpec TMv0.1 for eciesDecrypt 411 | // Does EC IES decryption for App:Crypto 412 | 413 | // eciesDecrypt is an opinionated helper function that decryptes a ciphertext using an ECDSA private key. 414 | // 415 | // it uses ecies (integrated encryption scheme) provided by an external library, documentation of which is available here: 416 | // https://github.com/obscuren/ecies 417 | func eciesDecrypt(cipherText []byte, privateKey *ecdsa.PrivateKey) ([]byte, error) { 418 | pri := ecies.ImportECDSA(privateKey) 419 | return pri.Decrypt(rand.Reader, cipherText, nil, nil) 420 | } 421 | 422 | // ThreatSpec TMv0.1 for SignMessage 423 | // Does asymmetric message signing for App:Crypto 424 | 425 | // SignMessage signs a message using the provided private key. It supports RSA and ECDSA and returns the message signature. 426 | func SignMessage(message []byte, privateKey crypto.PrivateKey) ([]byte, error) { 427 | switch k := privateKey.(type) { 428 | case *rsa.PrivateKey: 429 | return rsaSign(message, k) 430 | case *ecdsa.PrivateKey: 431 | return ecdsaSign(message, k) 432 | default: 433 | return nil, errors.New("Unsupported private key type.") 434 | } 435 | } 436 | 437 | // ThreatSpec TMv0.1 for rsaSign 438 | // Does RSA message signing for App:Crypto 439 | 440 | // rsaSign is an opinionated helper function that signs a message using an RSA private key. It uses PKCS1v15 with SHA-256, and returns the message signature. 441 | func rsaSign(message []byte, privateKey *rsa.PrivateKey) ([]byte, error) { 442 | var h crypto.Hash 443 | hash := sha256.New() 444 | _, err := io.WriteString(hash, string(message)) 445 | if err != nil { 446 | return nil, fmt.Errorf("Could not write to hash: %s", err) 447 | } 448 | 449 | hashed := hash.Sum(nil) 450 | signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, h, hashed) 451 | if err != nil { 452 | return nil, fmt.Errorf("Could not RSA sign: %s", err) 453 | } 454 | return signature, nil 455 | } 456 | 457 | // ThreatSpec TMv0.1 for ecdsaSign 458 | // Does EC DSA message signing for App:Crypto 459 | 460 | // ecdsaSign is an opinionated helper function that signs a message using an ECDSA private key, and returns the message signature. It uses SHA-256 for hashing. 461 | func ecdsaSign(message []byte, privateKey *ecdsa.PrivateKey) ([]byte, error) { 462 | hash := sha256.New() 463 | _, err := io.WriteString(hash, string(message)) 464 | if err != nil { 465 | return nil, fmt.Errorf("Could not write to hash: %s", err) 466 | } 467 | 468 | hashed := hash.Sum(nil) 469 | r, s, err := ecdsa.Sign(rand.Reader, privateKey, hashed) 470 | if err != nil { 471 | return nil, fmt.Errorf("Could not ECDSA sign: %s", err) 472 | } 473 | 474 | // TODO - this bit is ugly 475 | buf := new(bytes.Buffer) 476 | _, err = buf.Write([]byte{byte(len(r.Bytes()))}) 477 | if err != nil { 478 | return nil, fmt.Errorf("Could not write to buffer: %s", err) 479 | } 480 | _, err = buf.Write(r.Bytes()) 481 | if err != nil { 482 | return nil, fmt.Errorf("Could not write to buffer: %s", err) 483 | } 484 | _, err = buf.Write(s.Bytes()) 485 | if err != nil { 486 | return nil, fmt.Errorf("Could not write to buffer: %s", err) 487 | } 488 | 489 | return buf.Bytes(), nil 490 | } 491 | 492 | // ThreatSpec TMv0.1 for VerifySignature 493 | // Does asymmetric signature verification for App:Crypto 494 | 495 | // VerifySignature verifies a message for a given signature and public key. If verified, the function returns nil, otherwise it returns an error. It supports RSA and ECDSA public keys. 496 | func VerifySignature(message []byte, signature []byte, publicKey crypto.PublicKey) error { 497 | switch k := publicKey.(type) { 498 | case *rsa.PublicKey: 499 | return rsaVerify(message, signature, k) 500 | case *ecdsa.PublicKey: 501 | return ecdsaVerify(message, signature, k) 502 | default: 503 | return errors.New("Unsupported public key type.") 504 | } 505 | } 506 | 507 | // ThreatSpec TMv0.1 for rsaVerify 508 | // Does RSA signature verification for App:Crypto 509 | 510 | // rsaVerify is an opinionated helper function that verifies a message for a given signature and RSA public key. If verified, the function returns nil, otherwise it returns an error. It uses PKCS1v15 with SHA-256. 511 | func rsaVerify(message []byte, signature []byte, publicKey *rsa.PublicKey) error { 512 | var h crypto.Hash 513 | hash := sha256.New() 514 | _, err := io.WriteString(hash, string(message)) 515 | if err != nil { 516 | return fmt.Errorf("Could not write to hash: %s", err) 517 | } 518 | 519 | hashed := hash.Sum(nil) 520 | err = rsa.VerifyPKCS1v15(publicKey, h, hashed, signature) 521 | if err != nil { 522 | return fmt.Errorf("Could not RSA verify: %s", err) 523 | } 524 | return nil 525 | } 526 | 527 | // ThreatSpec TMv0.1 for ecdsaVerify 528 | // Does EC DSA signature verification for App:Crypto 529 | 530 | // ecdsaVerify is an opinionated helper function that verifies a message for a given signature and ECDSA public key. If verified, the function returns nil, otherwise it returns an error. It uses SHA-256 for hashing. 531 | func ecdsaVerify(message []byte, signature []byte, publicKey *ecdsa.PublicKey) error { 532 | hash := sha256.New() 533 | _, err := io.WriteString(hash, string(message)) 534 | if err != nil { 535 | return fmt.Errorf("Could not write to hash: %s", err) 536 | } 537 | 538 | hashed := hash.Sum(nil) 539 | l := int(signature[0]) 540 | r := new(big.Int).SetBytes(signature[1 : l+1]) 541 | s := new(big.Int).SetBytes(signature[l+1:]) 542 | ok := ecdsa.Verify(publicKey, hashed, r, s) 543 | if !ok { 544 | return errors.New("Could not ECDSA verify.") 545 | } 546 | return nil 547 | } 548 | 549 | // ThreatSpec TMv0.1 for hmac256 550 | // Does SHA-256 message authentication for App:Crypto 551 | 552 | // hmac256 is an opinionated helper function that generates a HMAC for the given message using SHA-256. 553 | func hmac256(message, key []byte) ([]byte, error) { 554 | mac := hmac.New(sha256.New, key) 555 | _, err := mac.Write(message) 556 | if err != nil { 557 | return nil, fmt.Errorf("Could not write to mac: %s", err) 558 | } 559 | 560 | return mac.Sum(nil), nil 561 | } 562 | 563 | // ThreatSpec TMv0.1 for HMAC 564 | // Does symmetric message authentication for App:Crypto 565 | 566 | // HMAC is a wrapper function that calculates a HMAC for a given message and symmetric key. 567 | func HMAC(message []byte, key []byte, signature *Signed) error { 568 | mac, err := hmac256(message, key) 569 | if err != nil { 570 | return fmt.Errorf("Could not get mac: %s", err) 571 | } 572 | 573 | signature.Message = string(message) 574 | signature.Signature = string(Base64Encode(mac)) 575 | return nil 576 | } 577 | 578 | // ThreatSpec TMv0.1 for HMACVerify 579 | // Does symmetric message authentication verification for App:Crypto 580 | // Mitigates App:Crypto against side-channel attack with use of time-constant comparison provided by standard package 581 | 582 | // HMACVerify verifies the HMAC of the given message. If verified, the function returns nil, otherwise it returns an error. 583 | func HMACVerify(message, key, signature []byte) error { 584 | newMac := hmac.New(sha256.New, key) 585 | _, err := newMac.Write(message) 586 | if err != nil { 587 | return fmt.Errorf("Could not write to mac: %s", err) 588 | } 589 | 590 | newFinalMac := newMac.Sum(nil) 591 | 592 | if hmac.Equal(newFinalMac, signature) { 593 | return nil 594 | } 595 | return fmt.Errorf("MACs not equal") 596 | } 597 | -------------------------------------------------------------------------------- /crypto/helpers_test.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/crypto as crypto 2 | package crypto 3 | 4 | import ( 5 | "crypto/ecdsa" 6 | "crypto/rsa" 7 | "github.com/stretchr/testify/assert" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | // ThreatSpec TMv0.1 for TestUUID 13 | // Tests UUID for correct output size 14 | 15 | func TestUUID(t *testing.T) { 16 | uuid := UUID() 17 | assert.Equal(t, 36, len(uuid), "incorrect size") 18 | } 19 | 20 | // ThreatSpec TMv0.1 for TestUUIDNotEqual 21 | // Tests UUID for two outputs not being the same 22 | 23 | func TestUUIDNotEqual(t *testing.T) { 24 | uuid1 := UUID() 25 | uuid2 := UUID() 26 | assert.NotEqual(t, uuid1, uuid2, "can't be the same") 27 | } 28 | 29 | // ThreatSpec TMv0.1 for TestRandomBytesSize 30 | // Tests RandomBytes for correct size 31 | 32 | func TestRandomBytesSize(t *testing.T) { 33 | size := 10 34 | random, err := RandomBytes(size) 35 | assert.NoError(t, err) 36 | assert.Equal(t, size, len(random), "they should be equal") 37 | } 38 | 39 | // ThreatSpec TMv0.1 for TestRandomBytesNotEqual 40 | // Tests RandomBytes for Use of Insufficiently Random Values (CWE-330) 41 | // Note that this isn't a very good test 42 | 43 | // TestRandomBytesNotEqual tests that two random byte arrays aren't the same. 44 | // This should protect against basic problems like all 0s etc but is very basic. 45 | func TestRandomBytesNotEqual(t *testing.T) { 46 | size := 10 47 | rand1, err := RandomBytes(size) 48 | assert.NoError(t, err) 49 | rand2, err := RandomBytes(size) 50 | assert.NoError(t, err) 51 | assert.NotEqual(t, rand1, rand2, "can't be the same") 52 | } 53 | 54 | // ThreatSpec TMv0.1 for TestPad 55 | // Tests Pad for correct output size 56 | 57 | func TestPad(t *testing.T) { 58 | size := 10 59 | msg := []byte("012345") 60 | padded := Pad(msg, size) 61 | assert.Equal(t, len(padded), size, "short input should be padded") 62 | 63 | msg = []byte("0123456789") 64 | padded = Pad(msg, size) 65 | expectedSize := size * 2 66 | assert.Equal(t, len(padded), expectedSize, "not a full block of padding") 67 | } 68 | 69 | // ThreatSpec TMv0.1 for TestUnpad 70 | // Tests Unpad for correct output size 71 | 72 | func TestUnpad(t *testing.T) { 73 | size := 10 74 | expectedSize := 5 75 | padded := []byte{1, 2, 3, 4, 5, 5, 5, 5, 5, 5} 76 | assert.Equal(t, len(padded), size, "padding size is incorrect") 77 | msg := UnPad(padded) 78 | assert.Equal(t, len(msg), expectedSize) 79 | } 80 | 81 | // ThreatSpec TMv0.1 for TestBase64Encode 82 | // Tests Base64Encode for matching output 83 | 84 | func TestBase64Encode(t *testing.T) { 85 | in := []byte("an input") 86 | expectedOut := []byte("YW4gaW5wdXQ=") // echo -n "an input" | base64 87 | out := Base64Encode(in) 88 | assert.Equal(t, out, expectedOut, "output should match") 89 | } 90 | 91 | // ThreatSpec TMv0.1 for TestBase64Decode 92 | // Tests Base64Decode for matching output 93 | 94 | func TestBase64Decode(t *testing.T) { 95 | in := []byte("YW4gaW5wdXQ=") // echo -n "an input" | base64 96 | expectedOut := []byte("an input") 97 | out, err := Base64Decode(in) 98 | assert.Nil(t, err) 99 | assert.Equal(t, out, expectedOut, "output should match") 100 | } 101 | 102 | // ThreatSpec TMv0.1 for TestAESEncrypt 103 | // Tests AESEncrypt for difference between plaintext and ciphertext 104 | 105 | func TestAESEncrypt(t *testing.T) { 106 | plaintext := []byte("secret message") 107 | key, _ := RandomBytes(32) 108 | ciphertext, iv, err := AESEncrypt(plaintext, key) 109 | assert.Nil(t, err) 110 | assert.NotNil(t, ciphertext) 111 | assert.NotEqual(t, ciphertext, plaintext, "no encryption took place") 112 | assert.NotNil(t, iv) 113 | } 114 | 115 | // TestAESEncryptRepeat ensures that repeated AES encryption of a plaintext with the same key 116 | // doesn't produce the same ciphertext. Ie. the IV used is different. 117 | func TestAESEncryptRepeat(t *testing.T) { 118 | plaintext := []byte("secret message") 119 | key, _ := RandomBytes(32) 120 | ciphertext1, iv1, _ := AESEncrypt(plaintext, key) 121 | ciphertext2, iv2, _ := AESEncrypt(plaintext, key) 122 | assert.NotEqual(t, ciphertext1, ciphertext2, "should be different") 123 | assert.NotEqual(t, iv1, iv2, "should be different") 124 | } 125 | 126 | func TestAESDecrypt(t *testing.T) { 127 | plaintext := []byte("secret message") 128 | key, _ := RandomBytes(32) 129 | ciphertext, iv, err := AESEncrypt(plaintext, key) 130 | newPlaintext, err := AESDecrypt(ciphertext, iv, key) 131 | assert.Nil(t, err) 132 | assert.Equal(t, string(plaintext), string(newPlaintext), "new plaintext must equal old plaintext") 133 | } 134 | 135 | func TestGetKeyType(t *testing.T) { 136 | rsakey, _ := GenerateRSAKey() 137 | eckey, _ := GenerateECKey() 138 | 139 | rsaKeyType, err := GetKeyType(rsakey) 140 | assert.NoError(t, err) 141 | assert.Equal(t, rsaKeyType, KeyTypeRSA) 142 | 143 | ecKeyType, err := GetKeyType(eckey) 144 | assert.NoError(t, err) 145 | assert.Equal(t, ecKeyType, KeyTypeEC) 146 | } 147 | 148 | func TestGenerateRSAKey(t *testing.T) { 149 | key, err := GenerateRSAKey() 150 | assert.NoError(t, err) 151 | assert.NotNil(t, key.D) 152 | } 153 | 154 | // TestGenerateRSAKeyRepeat tests that two generated RSA keys are different. 155 | func TestGenerateRSAKeyRepeat(t *testing.T) { 156 | key1, _ := GenerateRSAKey() 157 | key2, _ := GenerateRSAKey() 158 | assert.NotEqual(t, key1.D, key2.D) 159 | } 160 | 161 | func TestGenerateECKey(t *testing.T) { 162 | key, err := GenerateECKey() 163 | assert.NoError(t, err) 164 | assert.NotNil(t, key.D) 165 | } 166 | 167 | // TestGenerateECKeyRepeat tests that two generated EC keys are different. 168 | func TestGenerateECKeyRepeat(t *testing.T) { 169 | key1, _ := GenerateECKey() 170 | key2, _ := GenerateECKey() 171 | assert.NotEqual(t, key1.D, key2.D) 172 | } 173 | 174 | func TestPemEncodePrivate(t *testing.T) { 175 | rsakey, _ := GenerateRSAKey() 176 | eckey, _ := GenerateECKey() 177 | pemKey, err := PemEncodePrivate(rsakey) 178 | assert.NoError(t, err) 179 | assert.NotNil(t, pemKey) 180 | assert.Equal(t, strings.Contains(string(pemKey), "RSA PRIVATE KEY"), true) 181 | 182 | pemKey, err = PemEncodePrivate(eckey) 183 | assert.NoError(t, err) 184 | assert.NotNil(t, pemKey) 185 | assert.Equal(t, strings.Contains(string(pemKey), "EC PRIVATE KEY"), true) 186 | } 187 | 188 | // TestPemEncodePrivateRepeat tests that two different keys don't encode to the same thing 189 | func TestPemEncodePrivateRepeat(t *testing.T) { 190 | rsakey1, _ := GenerateRSAKey() 191 | rsakey2, _ := GenerateRSAKey() 192 | eckey1, _ := GenerateECKey() 193 | eckey2, _ := GenerateECKey() 194 | 195 | pemKey1, _ := PemEncodePrivate(rsakey1) 196 | pemKey2, _ := PemEncodePrivate(rsakey2) 197 | assert.NotEqual(t, pemKey1, pemKey2) 198 | 199 | pemKey1, _ = PemEncodePrivate(eckey1) 200 | pemKey2, _ = PemEncodePrivate(eckey2) 201 | assert.NotEqual(t, pemKey1, pemKey2) 202 | } 203 | 204 | func TestPemDecodePrivate(t *testing.T) { 205 | rsakey, _ := GenerateRSAKey() 206 | eckey, _ := GenerateECKey() 207 | pemKey, _ := PemEncodePrivate(rsakey) 208 | newKey, err := PemDecodePrivate(pemKey) 209 | assert.NoError(t, err) 210 | assert.Equal(t, rsakey, newKey) 211 | 212 | pemKey, _ = PemEncodePrivate(eckey) 213 | newKey, err = PemDecodePrivate(pemKey) 214 | assert.NoError(t, err) 215 | assert.Equal(t, eckey, newKey) 216 | } 217 | 218 | func TestPemEncodePublic(t *testing.T) { 219 | rsakey, _ := GenerateRSAKey() 220 | eckey, _ := GenerateECKey() 221 | 222 | pemKey, err := PemEncodePublic(&rsakey.PublicKey) 223 | assert.NoError(t, err) 224 | assert.NotNil(t, pemKey) 225 | assert.Equal(t, strings.Contains(string(pemKey), "RSA PUBLIC KEY"), true) 226 | 227 | pemKey, err = PemEncodePublic(&eckey.PublicKey) 228 | assert.NoError(t, err) 229 | assert.NotNil(t, pemKey) 230 | assert.Equal(t, strings.Contains(string(pemKey), "EC PUBLIC KEY"), true) 231 | } 232 | 233 | func TestPemEncodePublicRepeat(t *testing.T) { 234 | rsakey1, _ := GenerateRSAKey() 235 | rsakey2, _ := GenerateRSAKey() 236 | eckey1, _ := GenerateECKey() 237 | eckey2, _ := GenerateECKey() 238 | 239 | pemKey1, _ := PemEncodePublic(&rsakey1.PublicKey) 240 | pemKey2, _ := PemEncodePublic(&rsakey2.PublicKey) 241 | assert.NotEqual(t, pemKey1, pemKey2) 242 | 243 | pemKey1, _ = PemEncodePublic(&eckey1.PublicKey) 244 | pemKey2, _ = PemEncodePublic(&eckey2.PublicKey) 245 | assert.NotEqual(t, pemKey1, pemKey2) 246 | } 247 | 248 | func TestPemDecodePublic(t *testing.T) { 249 | rsakey, _ := GenerateRSAKey() 250 | eckey, _ := GenerateECKey() 251 | 252 | pemKey, _ := PemEncodePublic(&rsakey.PublicKey) 253 | newKey, err := PemDecodePublic(pemKey) 254 | assert.NoError(t, err) 255 | assert.Equal(t, rsakey.N, newKey.(*rsa.PublicKey).N) 256 | 257 | pemKey, _ = PemEncodePublic(&eckey.PublicKey) 258 | newKey, err = PemDecodePublic(pemKey) 259 | assert.NoError(t, err) 260 | assert.Equal(t, eckey.Curve, newKey.(*ecdsa.PublicKey).Curve) 261 | } 262 | 263 | func TestEncrypt(t *testing.T) { 264 | plaintext, _ := RandomBytes(32) 265 | rsakey, _ := GenerateRSAKey() 266 | eckey, _ := GenerateECKey() 267 | 268 | ciphertext, err := Encrypt(plaintext, &rsakey.PublicKey) 269 | assert.NoError(t, err) 270 | assert.NotNil(t, ciphertext) 271 | 272 | ciphertext, err = Encrypt(plaintext, &eckey.PublicKey) 273 | assert.NoError(t, err) 274 | assert.NotNil(t, ciphertext) 275 | } 276 | 277 | func TestEncryptRepeat(t *testing.T) { 278 | plaintext, _ := RandomBytes(32) 279 | 280 | rsakey1, _ := GenerateRSAKey() 281 | rsakey2, _ := GenerateRSAKey() 282 | eckey1, _ := GenerateECKey() 283 | eckey2, _ := GenerateECKey() 284 | 285 | ciphertext1, _ := Encrypt(plaintext, &rsakey1.PublicKey) 286 | ciphertext2, _ := Encrypt(plaintext, &rsakey2.PublicKey) 287 | assert.NotEqual(t, ciphertext1, ciphertext2) 288 | 289 | ciphertext1, _ = Encrypt(plaintext, &eckey1.PublicKey) 290 | ciphertext2, _ = Encrypt(plaintext, &eckey2.PublicKey) 291 | assert.NotEqual(t, ciphertext1, ciphertext2) 292 | } 293 | 294 | func TestDecrypt(t *testing.T) { 295 | plaintext, _ := RandomBytes(32) 296 | rsakey, _ := GenerateRSAKey() 297 | eckey, _ := GenerateECKey() 298 | 299 | ciphertext, _ := Encrypt(plaintext, &rsakey.PublicKey) 300 | newPlaintext, err := Decrypt(ciphertext, rsakey) 301 | assert.NoError(t, err) 302 | assert.Equal(t, plaintext, newPlaintext) 303 | 304 | ciphertext, _ = Encrypt(plaintext, &eckey.PublicKey) 305 | newPlaintext, err = Decrypt(ciphertext, eckey) 306 | assert.NoError(t, err) 307 | assert.Equal(t, plaintext, newPlaintext) 308 | } 309 | 310 | func TestSignMessage(t *testing.T) { 311 | message := []byte("this is a message") 312 | rsakey, _ := GenerateRSAKey() 313 | eckey, _ := GenerateECKey() 314 | 315 | sig, err := SignMessage(message, rsakey) 316 | assert.NoError(t, err) 317 | assert.NotNil(t, sig) 318 | 319 | sig, err = SignMessage(message, eckey) 320 | assert.NoError(t, err) 321 | assert.NotNil(t, sig) 322 | } 323 | 324 | // TestSignMessageRepeat tests that repeated signatures with the same private key produces different signatures 325 | func TestSignMessageRepeat(t *testing.T) { 326 | message := []byte("this is a message") 327 | rsakey1, _ := GenerateRSAKey() 328 | rsakey2, _ := GenerateRSAKey() 329 | eckey1, _ := GenerateECKey() 330 | eckey2, _ := GenerateECKey() 331 | 332 | sig1, _ := SignMessage(message, rsakey1) 333 | sig2, _ := SignMessage(message, rsakey2) 334 | assert.NotEqual(t, sig1, sig2) 335 | 336 | sig1, _ = SignMessage(message, eckey1) 337 | sig2, _ = SignMessage(message, eckey2) 338 | assert.NotEqual(t, sig1, sig2) 339 | } 340 | 341 | func TestVerifySignature(t *testing.T) { 342 | message := []byte("this is a message") 343 | rsakey, _ := GenerateRSAKey() 344 | eckey, _ := GenerateECKey() 345 | 346 | sig, _ := SignMessage(message, rsakey) 347 | err := VerifySignature(message, sig, &rsakey.PublicKey) 348 | assert.NoError(t, err) 349 | 350 | sig, _ = SignMessage(message, eckey) 351 | err = VerifySignature(message, sig, &eckey.PublicKey) 352 | assert.NoError(t, err) 353 | } 354 | 355 | func TestHMAC(t *testing.T) { 356 | mac := NewSignature(SignatureModeSha256Hmac) 357 | message := "message to be authenticated" 358 | key, _ := RandomBytes(32) 359 | HMAC([]byte(message), key, mac) 360 | assert.Equal(t, mac.Message, message) 361 | assert.NotEqual(t, mac.Signature, "") 362 | } 363 | 364 | // TestHMACRepeat tests that a message HMAC'd with a single key always produces the same result 365 | func TestHMACRepeat(t *testing.T) { 366 | mac1 := NewSignature(SignatureModeSha256Hmac) 367 | mac2 := NewSignature(SignatureModeSha256Hmac) 368 | message := "message to be authenticated" 369 | key, _ := RandomBytes(32) 370 | 371 | HMAC([]byte(message), key, mac1) 372 | HMAC([]byte(message), key, mac2) 373 | 374 | assert.Equal(t, mac1.Signature, mac2.Signature) 375 | } 376 | 377 | func TestHMACVerify(t *testing.T) { 378 | mac := NewSignature(SignatureModeSha256Hmac) 379 | message := "message to be authenticated" 380 | key, _ := RandomBytes(32) 381 | 382 | HMAC([]byte(message), key, mac) 383 | 384 | signature, _ := Base64Decode([]byte(mac.Signature)) 385 | err := HMACVerify([]byte(message), key, signature) 386 | assert.Nil(t, err) 387 | } 388 | 389 | func TestExpandKey(t *testing.T) { 390 | key, _ := RandomBytes(16) 391 | newKey, salt, err := ExpandKey(key, nil) 392 | assert.NoError(t, err) 393 | assert.Equal(t, len(salt), 16) 394 | assert.Equal(t, len(newKey), 32) 395 | } 396 | 397 | // TestExpandKeyRepeat tests that repeated key expansion for a single key produces different results 398 | func TestExpandKeyRepeat(t *testing.T) { 399 | key, _ := RandomBytes(16) 400 | newKey1, salt1, _ := ExpandKey(key, nil) 401 | newKey2, salt2, _ := ExpandKey(key, nil) 402 | assert.NotEqual(t, newKey1, newKey2) 403 | assert.NotEqual(t, salt1, salt2) 404 | } 405 | 406 | func TestExpandKeyWithSalt(t *testing.T) { 407 | key, _ := RandomBytes(16) 408 | salt, _ := RandomBytes(16) 409 | newKey, newSalt, err := ExpandKey(key, salt) 410 | assert.NoError(t, err) 411 | assert.Equal(t, salt, newSalt) 412 | assert.Equal(t, len(newKey), 32) 413 | } 414 | 415 | // TestExpandKeyWithSaltRepeat tests that repeated key expansion for a given key and salt gives the same result 416 | func TestExpandKeyWithSaltRepeat(t *testing.T) { 417 | key, _ := RandomBytes(16) 418 | salt, _ := RandomBytes(16) 419 | 420 | newKey1, newSalt1, _ := ExpandKey(key, salt) 421 | newKey2, newSalt2, _ := ExpandKey(key, salt) 422 | assert.Equal(t, newKey1, newKey2) 423 | assert.Equal(t, newSalt1, newSalt2) 424 | } 425 | -------------------------------------------------------------------------------- /document/ca.go.ig: -------------------------------------------------------------------------------- 1 | package document 2 | 3 | const CADefault string = `{ 4 | "scope": "pki.io", 5 | "version": 1, 6 | "type": "ca-document", 7 | "options": { 8 | "source": "00112233445566778899aabbccddeeff", 9 | "signature-mode": "sha256+rsa", 10 | "signature": "abc" 11 | }, 12 | "body": { 13 | "tags": [], 14 | "certificate": "xxx", 15 | "private-key": "yyy" 16 | } 17 | }` 18 | 19 | const CASchema string = `{ 20 | "$schema": "http://json-schema.org/draft-04/schema#", 21 | "title": "CADocument", 22 | "description": "CA Document", 23 | "type": "object", 24 | "required": ["scope","version","type","options","body"], 25 | "additionalProperties": false, 26 | "properties": { 27 | "scope": { 28 | "description": "Scope of the document", 29 | "type": "string" 30 | }, 31 | "version": { 32 | "description": "Document schema version", 33 | "type": "integer" 34 | }, 35 | "type": { 36 | "description": "Type of document", 37 | "type": "string" 38 | }, 39 | "options": { 40 | "description": "Options data", 41 | "type": "object", 42 | "required": ["source", "signature-mode", "signature"], 43 | "additionalProperties": false, 44 | "properties": { 45 | "source" : { 46 | "description": "Source ID", 47 | "type": "string", 48 | "pattern": "^[a-fA-F0-9]{32}$" 49 | }, 50 | "signature-mode" : { 51 | "description": "Signature mode", 52 | "type": "string" 53 | }, 54 | "signature" : { 55 | "description": "Base64 encoded signature", 56 | "type": "string" 57 | } 58 | } 59 | }, 60 | "body": { 61 | "description": "Body data", 62 | "type": "object", 63 | "required": ["tags","certificate","private-key"], 64 | "additionalProperties": false, 65 | "properties": { 66 | "tags" : { 67 | "description": "Tags", 68 | "type": "array", 69 | "minItems": 0, 70 | "uniqueItems": true, 71 | "items": { 72 | "type": "string" 73 | } 74 | }, 75 | "certificate" : { 76 | "description": "Base64 encoded X.509 certificate", 77 | "type": "string" 78 | }, 79 | "private-key" : { 80 | "description": "Base64 encoded private key", 81 | "type": "string" 82 | } 83 | } 84 | } 85 | } 86 | }` 87 | 88 | type CADocumentData struct { 89 | Scope string `json:"scope"` 90 | Version int `json:"version"` 91 | Type string `json:"type"` 92 | Options struct { 93 | Source string `json:"source"` 94 | SignatureMode string `json:"signature-mode"` 95 | Signature string `json:"signature"` 96 | } `json:"options"` 97 | Body struct { 98 | Tags []string `json:"tags"` 99 | Certificate string `json:"certificate"` 100 | PrivateKey string `json:"private-key"` 101 | } `json:"body"` 102 | } 103 | 104 | type CADocument struct { 105 | document 106 | Data CADocumentData 107 | } 108 | 109 | func NewCA(jsonData interface{}) (*CADocument, error) { 110 | doc := new(CADocument) 111 | data := new(CADocumentData) 112 | doc.schema = CASchema 113 | doc.defaultValue = CADefault 114 | if data, err := doc.fromJson(jsonData, data); err != nil { 115 | return nil, err 116 | } else { 117 | doc.Data = *data.(*CADocumentData) 118 | return doc, nil 119 | } 120 | } 121 | 122 | func (doc *CADocument) Json() (string, error) { 123 | if jsonString, err := doc.toJson(doc.Data); err != nil { 124 | return "", err 125 | } else { 126 | return jsonString, nil 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /document/container.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/document as document 2 | package document 3 | 4 | import ( 5 | "fmt" 6 | "github.com/pki-io/core/crypto" 7 | ) 8 | 9 | // ContainerDefault sets default values for a Container. 10 | const ContainerDefault string = `{ 11 | "scope": "pki.io", 12 | "version": 1, 13 | "type": "container", 14 | "options": { 15 | "source": "", 16 | "signature-mode": "", 17 | "signature-inputs": {}, 18 | "signature": "", 19 | "encryption-keys": {}, 20 | "encryption-mode": "", 21 | "encryption-inputs": {} 22 | }, 23 | "body": "" 24 | }` 25 | 26 | // ContainerSchema defines the JSON schema for a Container. 27 | const ContainerSchema string = `{ 28 | "$schema": "http://json-schema.org/draft-04/schema#", 29 | "title": "Container", 30 | "description": "Container", 31 | "type": "object", 32 | "required": ["scope","version","type","options","body"], 33 | "additionalProperties": false, 34 | "properties": { 35 | "scope": { 36 | "description": "Scope of the document", 37 | "type": "string" 38 | }, 39 | "version": { 40 | "description": "Document schema version", 41 | "type": "integer" 42 | }, 43 | "type": { 44 | "description": "Type of document", 45 | "type": "string" 46 | }, 47 | "options": { 48 | "description": "Options data", 49 | "type": "object", 50 | "required": ["source","signature-mode","signature","encryption-keys","encryption-mode","encryption-inputs"], 51 | "additionalProperties": false, 52 | "properties": { 53 | "source" : { 54 | "description": "Source ID", 55 | "type": "string" 56 | }, 57 | "signature-mode": { 58 | "description": "Signature mode", 59 | "type": "string" 60 | }, 61 | "signature-inputs": { 62 | "description": "Signature inputs", 63 | "type": "object" 64 | }, 65 | "signature": { 66 | "description": "Base64 encoded signature", 67 | "type": "string" 68 | }, 69 | "encryption-keys": { 70 | "description": "Encryption keys", 71 | "type": "object" 72 | }, 73 | "encryption-mode": { 74 | "description": "Encryption mode", 75 | "type": "string" 76 | }, 77 | "encryption-inputs": { 78 | "description": "Encryption inputs", 79 | "type": "object" 80 | } 81 | } 82 | }, 83 | "body": { 84 | "description": "Encrypted data", 85 | "type": "string" 86 | } 87 | } 88 | }` 89 | 90 | // ContainerData stores the parsed JSON data. 91 | type ContainerData struct { 92 | Scope string `json:"scope"` 93 | Version int `json:"version"` 94 | Type string `json:"type"` 95 | Options struct { 96 | Source string `json:"source"` 97 | SignatureMode string `json:"signature-mode"` 98 | SignatureInputs map[string]string `json:"signature-inputs"` 99 | Signature string `json:"signature"` 100 | EncryptionKeys map[string]string `json:"encryption-keys"` 101 | EncryptionMode string `json:"encryption-mode"` 102 | EncryptionInputs map[string]string `json:"encryption-inputs"` 103 | } `json:"options"` 104 | Body string `json:"body"` 105 | } 106 | 107 | // Container is a cryptographic document that can be signed and/or encrypted. 108 | type Container struct { 109 | Document 110 | Data ContainerData 111 | } 112 | 113 | // ThreatSpec TMv0.1 for NewContainer 114 | // Creates new container for App:Document 115 | 116 | // NewContainer creates a new Container. 117 | func NewContainer(jsonData interface{}) (*Container, error) { 118 | doc := new(Container) 119 | data := new(ContainerData) 120 | doc.Schema = ContainerSchema 121 | doc.Default = ContainerDefault 122 | if data, err := doc.FromJson(jsonData, data); err != nil { 123 | return nil, fmt.Errorf("Could not load container json: %s", err) 124 | } else { 125 | doc.Data = *data.(*ContainerData) 126 | return doc, nil 127 | } 128 | } 129 | 130 | // ThreatSpec TMv0.1 for Container.Dump 131 | // Does container dumping for App:Document 132 | 133 | // Dump serializes the Container to JSON. 134 | func (doc *Container) Dump() string { 135 | if jsonString, err := doc.ToJson(doc.Data); err != nil { 136 | return "" 137 | } else { 138 | return jsonString 139 | } 140 | } 141 | 142 | // ThreatSpec TMv0.1 for Container.Encrypt 143 | // Does container hybdrid encryption for App:Document 144 | 145 | // Encrypt takes a plaintext string and group encrypts for the given public keys and updates its data to the ciphertext and inputs. 146 | func (doc *Container) Encrypt(jsonString string, keys map[string]string) error { 147 | encrypted, err := crypto.GroupEncrypt(jsonString, keys) 148 | if err != nil { 149 | return fmt.Errorf("Could not group encrypt: %s", err) 150 | } 151 | 152 | doc.Data.Options.EncryptionKeys = encrypted.Keys 153 | doc.Data.Options.EncryptionMode = encrypted.Mode 154 | doc.Data.Options.EncryptionInputs = encrypted.Inputs 155 | doc.Data.Body = encrypted.Ciphertext 156 | 157 | return nil 158 | } 159 | 160 | // ThreatSpec TMv0.1 for Container.SymmetricEncrypt 161 | // Does symmetric encryption of container for App:Document 162 | 163 | // SymmetricEncrypt takes a plaintext string and encrypts with the given key. It updates its data to the ciphertext and inputs. 164 | func (doc *Container) SymmetricEncrypt(jsonString, id, key string) error { 165 | encrypted, err := crypto.SymmetricEncrypt(jsonString, id, key) 166 | if err != nil { 167 | return fmt.Errorf("Couldn't symmetric encrypt content: %s", err) 168 | } 169 | 170 | doc.Data.Options.EncryptionMode = encrypted.Mode 171 | doc.Data.Options.EncryptionInputs = encrypted.Inputs 172 | doc.Data.Body = encrypted.Ciphertext 173 | 174 | return nil 175 | } 176 | 177 | // ThreatSpec TMv0.1 for Container.Decrypt 178 | // Does hybdrid decryption of container for App:Document 179 | 180 | // Decrypt takes a private key and decrypts the Container body, return a plaintext string. 181 | func (doc *Container) Decrypt(id string, privateKey string) (string, error) { 182 | encrypted := new(crypto.Encrypted) 183 | encrypted.Keys = doc.Data.Options.EncryptionKeys 184 | encrypted.Mode = doc.Data.Options.EncryptionMode 185 | encrypted.Inputs = doc.Data.Options.EncryptionInputs 186 | encrypted.Ciphertext = doc.Data.Body 187 | 188 | if decryptedJson, err := crypto.GroupDecrypt(encrypted, id, privateKey); err != nil { 189 | return "", fmt.Errorf("Could not decrypt container: %s", err) 190 | } else { 191 | return decryptedJson, nil 192 | } 193 | } 194 | 195 | // ThreatSpec TMv0.1 for Container.SymmetricDecrypt 196 | // Does symmetric decryption of container for App:Document 197 | 198 | // SymmetricDecrypt takes a key and decrypts the Container body, returning a plaintext string. 199 | func (doc *Container) SymmetricDecrypt(key string) (string, error) { 200 | encrypted := new(crypto.Encrypted) 201 | encrypted.Keys = doc.Data.Options.EncryptionKeys 202 | encrypted.Mode = doc.Data.Options.EncryptionMode 203 | encrypted.Inputs = doc.Data.Options.EncryptionInputs 204 | encrypted.Ciphertext = doc.Data.Body 205 | 206 | if decryptedJson, err := crypto.SymmetricDecrypt(encrypted, key); err != nil { 207 | return "", fmt.Errorf("Couldn't decrypt container: %s", err) 208 | } else { 209 | return decryptedJson, nil 210 | } 211 | } 212 | 213 | // ThreatSpec TMv0.1 for Container.IsEncrypted 214 | // Returns whether container is encrypted for App:Document 215 | 216 | // IsEncrypted checks whether the Container is encrypted. 217 | func (doc *Container) IsEncrypted() bool { 218 | if len(doc.Data.Options.EncryptionKeys) == 0 || 219 | len(doc.Data.Options.EncryptionMode) == 0 || 220 | len(doc.Data.Options.EncryptionInputs) == 0 { 221 | return false 222 | } else { 223 | return true 224 | } 225 | } 226 | 227 | // ThreatSpec TMv0.1 for Container.IsSigned 228 | // Returns whether container is signed for App:Document 229 | 230 | // IsSigned checks whether the Container is signed. 231 | func (doc *Container) IsSigned() bool { 232 | if len(doc.Data.Options.SignatureMode) == 0 || 233 | len(doc.Data.Options.Signature) == 0 { 234 | return false 235 | } else { 236 | return true 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /document/container_test.go: -------------------------------------------------------------------------------- 1 | package document 2 | 3 | import ( 4 | "encoding/hex" 5 | "github.com/pki-io/core/crypto" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestNewContainer(t *testing.T) { 11 | container, err := NewContainer(nil) 12 | assert.Nil(t, err) 13 | assert.NotNil(t, container) 14 | assert.Equal(t, container.Data.Type, "container") 15 | } 16 | 17 | func TestSymmetricEncryptDecrypt(t *testing.T) { 18 | rawId, _ := crypto.RandomBytes(16) 19 | rawKey, _ := crypto.RandomBytes(16) 20 | 21 | id := hex.EncodeToString(rawId) 22 | key := hex.EncodeToString(rawKey) 23 | 24 | container, _ := NewContainer(nil) 25 | message := "this is a secret" 26 | err := container.SymmetricEncrypt(message, id, key) 27 | assert.Nil(t, err) 28 | 29 | newMessage, err := container.SymmetricDecrypt(key) 30 | assert.Nil(t, err) 31 | assert.NotNil(t, newMessage) 32 | assert.Equal(t, newMessage, message) 33 | } 34 | -------------------------------------------------------------------------------- /document/document.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/document as document 2 | package document 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "github.com/xeipuuv/gojsonschema" 9 | "strings" 10 | ) 11 | 12 | type Documenter interface { 13 | Dump() 14 | Load() 15 | } 16 | 17 | // Documents represents a generic JSON schema based document 18 | type Document struct { 19 | Schema string 20 | Default string 21 | } 22 | 23 | // ThreatSpec TMv0.1 for Document.FromJson 24 | // Creates document from JSON for App:Document 25 | 26 | // FromJson parses the provided data after verifying the schema. If the data is nil, it uses the default values set for the document. 27 | func (doc *Document) FromJson(data interface{}, target interface{}) (interface{}, error) { 28 | var jsonData string 29 | doValidation := true 30 | 31 | switch t := data.(type) { 32 | case []byte: 33 | jsonData = string(t) 34 | case string: 35 | jsonData = t 36 | case nil: 37 | jsonData = doc.Default 38 | doValidation = false 39 | default: 40 | return nil, fmt.Errorf("Invalid input type: %T", t) 41 | } 42 | 43 | if doValidation { 44 | documentLoader := gojsonschema.NewStringLoader(jsonData) 45 | schemaLoader := gojsonschema.NewStringLoader(doc.Schema) 46 | 47 | if result, err := gojsonschema.Validate(schemaLoader, documentLoader); err != nil { 48 | return nil, errors.New("Something went wrong when trying to validate json.") 49 | } else if result.Valid() { 50 | if err := json.Unmarshal([]byte(jsonData), target); err != nil { 51 | return nil, err 52 | } else { 53 | return target, nil 54 | } 55 | } else { 56 | // Loop through errors 57 | var errs []string 58 | for _, desc := range result.Errors() { 59 | errs = append(errs, fmt.Sprint(desc)) 60 | } 61 | return nil, errors.New(strings.Join(errs, "\n")) 62 | } 63 | } else { 64 | if err := json.Unmarshal([]byte(jsonData), target); err != nil { 65 | return nil, err 66 | } else { 67 | return target, nil 68 | } 69 | } 70 | } 71 | 72 | // ThreatSpec TMv0.1 for Document.ToJson 73 | // Returns document as JSON for App:Document 74 | 75 | // ToJson serializes the document to JSON. 76 | func (doc *Document) ToJson(data interface{}) (string, error) { 77 | jsonData, err := json.Marshal(data) 78 | if err != nil { 79 | return "", err 80 | } 81 | 82 | documentLoader := gojsonschema.NewStringLoader(string(jsonData)) 83 | schemaLoader := gojsonschema.NewStringLoader(doc.Schema) 84 | 85 | if result, err := gojsonschema.Validate(schemaLoader, documentLoader); err != nil { 86 | return "", errors.New("something went wrong when trying to validate json.") 87 | } else if result.Valid() { 88 | return string(jsonData), nil 89 | } else { 90 | // Loop through errors 91 | for _, desc := range result.Errors() { 92 | fmt.Printf("- %s\n", desc) 93 | } 94 | return "", errors.New("ffs") 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /document/document_test.go: -------------------------------------------------------------------------------- 1 | package document 2 | 3 | import ( 4 | "testing" 5 | "github.com/stretchr/testify/assert" 6 | ) 7 | 8 | func TestAbstractDocument(t *testing.T) { 9 | d := new(Document) 10 | assert.NotNil(t, d) 11 | } 12 | 13 | func TestDocumentDefault(t *testing.T) { 14 | schema := `{ 15 | "$schema": "http://json-schema.org/draft-04/schema#", 16 | "title": "CADocument", 17 | "description": "CA Document", 18 | "type": "object" 19 | }` 20 | defaultValue := `{"test":"testing"}` 21 | 22 | type TestData struct { 23 | Test string `json:"test"` 24 | } 25 | 26 | type TestDocument struct { 27 | Document 28 | Data TestData 29 | } 30 | 31 | doc := new(TestDocument) 32 | data := new(TestData) 33 | assert.NotNil(t, doc) 34 | assert.NotNil(t, data) 35 | doc.Schema = schema 36 | doc.Default = defaultValue 37 | d, err := doc.FromJson(nil, data) 38 | assert.Nil(t, err) 39 | assert.NotNil(t, data) 40 | doc.Data = *d.(*TestData) 41 | assert.Equal(t, doc.Data.Test, "testing") 42 | } 43 | 44 | func TestDocumentJson(t *testing.T) { 45 | schema := `{ 46 | "$schema": "http://json-schema.org/draft-04/schema#", 47 | "title": "CADocument", 48 | "description": "CA Document", 49 | "type": "object" 50 | }` 51 | defaultValue := `{"test":"testing"}` 52 | 53 | type TestData struct { 54 | Test string `json:"test"` 55 | } 56 | 57 | type TestDocument struct { 58 | Document 59 | Data TestData 60 | } 61 | 62 | inputJson := `{"test":"badgers"}` 63 | 64 | doc := new(TestDocument) 65 | data := new(TestData) 66 | assert.NotNil(t, doc) 67 | assert.NotNil(t, data) 68 | doc.Schema = schema 69 | doc.Default = defaultValue 70 | d, err := doc.FromJson(inputJson, data) 71 | assert.Nil(t, err) 72 | assert.NotNil(t, data) 73 | doc.Data = *d.(*TestData) 74 | assert.Equal(t, doc.Data.Test, "badgers") 75 | } 76 | -------------------------------------------------------------------------------- /entity/entity.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/entity as entity 2 | package entity 3 | 4 | import ( 5 | "crypto/ecdsa" 6 | "crypto/rsa" 7 | "encoding/hex" 8 | "fmt" 9 | "github.com/pki-io/core/crypto" 10 | "github.com/pki-io/core/document" 11 | ) 12 | 13 | // EntityDefault provides default values for Entity. 14 | const EntityDefault string = `{ 15 | "scope": "pki.io", 16 | "version": 1, 17 | "type": "entity-document", 18 | "options": "", 19 | "body": { 20 | "id": "", 21 | "name": "", 22 | "key-type": "ec", 23 | "public-signing-key": "", 24 | "private-signing-key": "", 25 | "public-encryption-key": "", 26 | "private-encryption-key": "" 27 | } 28 | }` 29 | 30 | // EntitySchema defines the JSON Schema for Entity. 31 | const EntitySchema string = `{ 32 | "$schema": "http://json-schema.org/draft-04/schema#", 33 | "title": "EntityDocument", 34 | "description": "Entity Document", 35 | "type": "object", 36 | "required": ["scope","version","type","options","body"], 37 | "additionalProperties": false, 38 | "properties": { 39 | "scope": { 40 | "description": "Scope of the document", 41 | "type": "string" 42 | }, 43 | "version": { 44 | "description": "Document schema version", 45 | "type": "integer" 46 | }, 47 | "type": { 48 | "description": "Type of document", 49 | "type": "string" 50 | }, 51 | "options": { 52 | "description": "Options data", 53 | "type": "string" 54 | }, 55 | "body": { 56 | "description": "Body data", 57 | "type": "object", 58 | "required": ["id", "name", "key-type", "public-signing-key", "private-signing-key", "public-encryption-key", "private-encryption-key"], 59 | "additionalProperties": false, 60 | "properties": { 61 | "id" : { 62 | "description": "Entity ID", 63 | "type": "string" 64 | }, 65 | "name" : { 66 | "description": "Entity name", 67 | "type": "string" 68 | }, 69 | "key-type": { 70 | "description": "Key type. Either rsa or ec", 71 | "type": "string" 72 | }, 73 | "public-signing-key" : { 74 | "description": "Public signing key", 75 | "type": "string" 76 | }, 77 | "private-signing-key" : { 78 | "description": "Private signing key", 79 | "type": "string" 80 | }, 81 | "public-encryption-key" : { 82 | "description": "Public encryption key", 83 | "type": "string" 84 | }, 85 | "private-encryption-key" : { 86 | "description": "Private encryption key", 87 | "type": "string" 88 | } 89 | } 90 | } 91 | } 92 | }` 93 | 94 | type Encrypter interface { 95 | Id() string 96 | Body() EntityBody 97 | EncryptThenAuthenticateString(string, string, string) (*document.Container, error) 98 | EncryptThenSignString(string, []Encrypter) (*document.Container, error) 99 | } 100 | 101 | type Decrypter interface { 102 | Id() string 103 | Body() EntityBody 104 | VerifyAuthenticationThenDecrypt(*document.Container, string) (string, error) 105 | VerifyThenDecrypt(*document.Container) (string, error) 106 | } 107 | 108 | type EntityBody struct { 109 | Id string `json:"id"` 110 | Name string `json:"name"` 111 | KeyType string `json:"key-type"` 112 | PublicSigningKey string `json:"public-signing-key"` 113 | PrivateSigningKey string `json:"private-signing-key"` 114 | PublicEncryptionKey string `json:"public-encryption-key"` 115 | PrivateEncryptionKey string `json:"private-encryption-key"` 116 | } 117 | 118 | // EntityData represents parsed Entity JSON data. 119 | type EntityData struct { 120 | Scope string `json:"scope"` 121 | Version int `json:"version"` 122 | Type string `json:"type"` 123 | Options string `json:"options"` 124 | Body EntityBody `json:"body"` 125 | } 126 | 127 | // Entity participates in cryptographic operations, sending and receiving secured data. 128 | type Entity struct { 129 | document.Document 130 | Data EntityData 131 | } 132 | 133 | // ThreatSpec TMv0.1 for New 134 | // Creates new entity for App:Entity 135 | 136 | // New returns a new Entity. 137 | func New(jsonString interface{}) (*Entity, error) { 138 | entity := new(Entity) 139 | if err := entity.New(jsonString); err != nil { 140 | return nil, fmt.Errorf("Couldn't create new entity: %s", err) 141 | } else { 142 | return entity, nil 143 | } 144 | } 145 | 146 | // ThreatSpec TMv0.1 for Entity.New 147 | // Does entity initialisation for App:Entity 148 | 149 | // New initializes the entity. 150 | func (entity *Entity) New(jsonString interface{}) error { 151 | entity.Schema = EntitySchema 152 | entity.Default = EntityDefault 153 | if err := entity.Load(jsonString); err != nil { 154 | return fmt.Errorf("Could not create new Entity: %s", err) 155 | } else { 156 | return nil 157 | } 158 | } 159 | 160 | // ThreatSpec TMv0.1 for Entity.Load 161 | // Does entity JSON loading for App:Entity 162 | 163 | // Load takes a JSON string and sets the entity data. 164 | func (entity *Entity) Load(jsonString interface{}) error { 165 | data := new(EntityData) 166 | if data, err := entity.FromJson(jsonString, data); err != nil { 167 | return fmt.Errorf("Could not load entity JSON: %s", err) 168 | } else { 169 | entity.Data = *data.(*EntityData) 170 | return nil 171 | } 172 | } 173 | 174 | func (entity *Entity) Id() string { 175 | return entity.Data.Body.Id 176 | } 177 | 178 | func (entity *Entity) Name() string { 179 | return entity.Data.Body.Name 180 | } 181 | 182 | func (entity *Entity) Body() EntityBody { 183 | return entity.Data.Body 184 | } 185 | 186 | // ThreatSpec TMv0.1 for Entity.Dump 187 | // Does entity JSON dumping for App:Entity 188 | 189 | // Dump serializes the entity, returning a JSON string. 190 | func (entity *Entity) Dump() string { 191 | if jsonString, err := entity.ToJson(entity.Data); err != nil { 192 | return "" 193 | } else { 194 | return jsonString 195 | } 196 | } 197 | 198 | // ThreatSpec TMv0.1 for Entity.DumpPublic 199 | // Does entity dumping of public JSON for App:Entity 200 | 201 | // DumpPublic serializes the public entity data, returning a JSON string. 202 | func (entity *Entity) DumpPublic() string { 203 | public, err := entity.Public() 204 | if err != nil { 205 | return "" 206 | } else { 207 | return public.Dump() 208 | } 209 | } 210 | 211 | // ThreatSpec TMv0.1 for Entity.generateRSAKeys 212 | // Does RSA key generation for App:Entity 213 | 214 | // generateRSAKeys generates RSA keys. 215 | func (entity *Entity) generateRSAKeys() (*rsa.PrivateKey, *rsa.PrivateKey, error) { 216 | signingKey, err := crypto.GenerateRSAKey() 217 | if err != nil { 218 | return nil, nil, err 219 | } 220 | 221 | encryptionKey, err := crypto.GenerateRSAKey() 222 | if err != nil { 223 | return nil, nil, err 224 | } 225 | 226 | signingKey.Precompute() 227 | encryptionKey.Precompute() 228 | 229 | if err := signingKey.Validate(); err != nil { 230 | return nil, nil, fmt.Errorf("Could not validate signing key: %s", err) 231 | } 232 | 233 | if err := encryptionKey.Validate(); err != nil { 234 | return nil, nil, fmt.Errorf("Could not validate encryption key: %s", err) 235 | } 236 | 237 | if pub, err := crypto.PemEncodePublic(&signingKey.PublicKey); err != nil { 238 | return nil, nil, err 239 | } else { 240 | entity.Data.Body.PublicSigningKey = string(pub) 241 | } 242 | 243 | return signingKey, encryptionKey, nil 244 | } 245 | 246 | // ThreatSpec TMv0.1 for Entity.generateECKeys 247 | // Does EC key generation for App:Entity 248 | 249 | // generateECKeys generates EC keys. 250 | func (entity *Entity) generateECKeys() (*ecdsa.PrivateKey, *ecdsa.PrivateKey, error) { 251 | signingKey, err := crypto.GenerateECKey() 252 | if err != nil { 253 | return nil, nil, err 254 | } 255 | 256 | encryptionKey, err := crypto.GenerateECKey() 257 | if err != nil { 258 | return nil, nil, err 259 | } 260 | 261 | // TODO: Do we need to do any validation here? 262 | 263 | return signingKey, encryptionKey, nil 264 | } 265 | 266 | // ThreatSpec TMv0.1 for Entity.GenerateKeys 267 | // Does key generation for App:Entity 268 | 269 | // GenerateKeys generates RSA or EC keys for the entity, depending on the KeyType set. 270 | func (entity *Entity) GenerateKeys() error { 271 | var signingKey interface{} 272 | var encryptionKey interface{} 273 | var publicSigningKey interface{} 274 | var publicEncryptionKey interface{} 275 | var err error 276 | switch crypto.KeyType(entity.Data.Body.KeyType) { 277 | case crypto.KeyTypeRSA: 278 | signingKey, encryptionKey, err = entity.generateRSAKeys() 279 | if err != nil { 280 | return err 281 | } 282 | publicSigningKey = &signingKey.(*rsa.PrivateKey).PublicKey 283 | publicEncryptionKey = &encryptionKey.(*rsa.PrivateKey).PublicKey 284 | case crypto.KeyTypeEC: 285 | signingKey, encryptionKey, err = entity.generateECKeys() 286 | if err != nil { 287 | return err 288 | } 289 | publicSigningKey = &signingKey.(*ecdsa.PrivateKey).PublicKey 290 | publicEncryptionKey = &encryptionKey.(*ecdsa.PrivateKey).PublicKey 291 | default: 292 | return fmt.Errorf("Invalid key type: %s", entity.Data.Body.KeyType) 293 | } 294 | 295 | if pub, err := crypto.PemEncodePublic(publicSigningKey); err != nil { 296 | return err 297 | } else { 298 | entity.Data.Body.PublicSigningKey = string(pub) 299 | } 300 | 301 | if key, err := crypto.PemEncodePrivate(signingKey); err != nil { 302 | return err 303 | } else { 304 | entity.Data.Body.PrivateSigningKey = string(key) 305 | } 306 | 307 | if pub, err := crypto.PemEncodePublic(publicEncryptionKey); err != nil { 308 | return err 309 | } else { 310 | entity.Data.Body.PublicEncryptionKey = string(pub) 311 | } 312 | 313 | if key, err := crypto.PemEncodePrivate(encryptionKey); err != nil { 314 | return err 315 | } else { 316 | entity.Data.Body.PrivateEncryptionKey = string(key) 317 | } 318 | 319 | return nil 320 | } 321 | 322 | // ThreatSpec TMv0.1 for Entity.Sign 323 | // Does container using for App:Entity 324 | 325 | // Sign takes a Container and signs it using its private signing key. 326 | func (entity *Entity) Sign(container *document.Container) error { 327 | var signatureMode crypto.Mode 328 | switch crypto.KeyType(entity.Data.Body.KeyType) { 329 | case crypto.KeyTypeRSA: 330 | signatureMode = crypto.SignatureModeSha256Rsa 331 | case crypto.KeyTypeEC: 332 | signatureMode = crypto.SignatureModeSha256Ecdsa 333 | default: 334 | return fmt.Errorf("Invalid key type: %s", entity.Data.Body.KeyType) 335 | } 336 | 337 | signature := crypto.NewSignature(signatureMode) 338 | container.Data.Options.SignatureMode = string(signature.Mode) 339 | // Force a clear of any existing signature values as that doesn't make sense 340 | container.Data.Options.Signature = "" 341 | 342 | containerJson := container.Dump() 343 | 344 | if err := crypto.Sign(containerJson, entity.Data.Body.PrivateSigningKey, signature); err != nil { 345 | return fmt.Errorf("Could not sign container json: %s", err) 346 | } 347 | if signature.Message != containerJson { 348 | return fmt.Errorf("Signed message doesn't match input") 349 | } 350 | 351 | container.Data.Options.SignatureMode = string(signature.Mode) 352 | container.Data.Options.Signature = signature.Signature 353 | return nil 354 | } 355 | 356 | // ThreatSpec TMv0.1 for Entity.Authenticate 357 | // Does container authentication with shared keys for App:Entity 358 | 359 | // Authenticate takes a Container and MACs it using the provided key. 360 | func (entity *Entity) Authenticate(container *document.Container, id, key string) error { 361 | 362 | // Have to expand key here as we need to add the salt to the container before we turn it into json 363 | rawKey, err := hex.DecodeString(key) 364 | if err != nil { 365 | return fmt.Errorf("Could not decode key: %s", err) 366 | } 367 | 368 | newKey, salt, err := crypto.ExpandKey(rawKey, nil) 369 | if err != nil { 370 | return fmt.Errorf("Cold not expand key: %s", err) 371 | } 372 | 373 | signature := crypto.NewSignature(crypto.SignatureModeSha256Hmac) 374 | container.Data.Options.SignatureMode = string(signature.Mode) 375 | signatureInputs := make(map[string]string) 376 | signatureInputs["key-id"] = id 377 | signatureInputs["signature-salt"] = string(crypto.Base64Encode(salt)) 378 | container.Data.Options.SignatureInputs = signatureInputs 379 | 380 | // Force a clear of any existing signature values as that doesn't make sense 381 | container.Data.Options.Signature = "" 382 | 383 | containerJson := container.Dump() 384 | 385 | if err := crypto.Authenticate(containerJson, newKey, signature); err != nil { 386 | return fmt.Errorf("Couldn't authenticate container: %s", err) 387 | } 388 | 389 | if signature.Message != containerJson { 390 | return fmt.Errorf("Authenticated message doesn't match") 391 | } 392 | 393 | container.Data.Options.Signature = signature.Signature 394 | return nil 395 | } 396 | 397 | // ThreatSpec TMv0.1 for Entity.VerifyAuthentication 398 | // Does authenticated container verification for App:Entity 399 | 400 | // VerifyAuthentication takes a Container and verifies the MAC for the given key. 401 | func (entity *Entity) VerifyAuthentication(container *document.Container, key string) error { 402 | rawKey, err := hex.DecodeString(key) 403 | if err != nil { 404 | return fmt.Errorf("Could not decode key: %s", err) 405 | } 406 | 407 | salt, err := crypto.Base64Decode([]byte(container.Data.Options.SignatureInputs["signature-salt"])) 408 | if err != nil { 409 | fmt.Errorf("Could not base64 decode signature salt: %s", err) 410 | } 411 | 412 | newKey, _, err := crypto.ExpandKey(rawKey, salt) 413 | if err != nil { 414 | return fmt.Errorf("Could not expand key: %s", err) 415 | } 416 | mac := crypto.NewSignature(crypto.SignatureModeSha256Hmac) 417 | 418 | mac.Signature = container.Data.Options.Signature 419 | container.Data.Options.Signature = "" 420 | 421 | mac.Message = container.Dump() 422 | 423 | if err := crypto.Verify(mac, newKey); err != nil { 424 | return fmt.Errorf("Couldn't verify container: %s", err) 425 | } else { 426 | return nil 427 | } 428 | } 429 | 430 | // ThreatSpec TMv0.1 for Entity.Verify 431 | // Does container signature verification for App:Entity 432 | 433 | // Verify takes a Container and verifies the signature using the entities public key. 434 | func (entity *Entity) Verify(container *document.Container) error { 435 | 436 | if container.IsSigned() == false { 437 | return fmt.Errorf("Container isn't signed") 438 | } 439 | 440 | signature := new(crypto.Signed) 441 | signature.Signature = container.Data.Options.Signature 442 | 443 | container.Data.Options.Signature = "" 444 | containerJson := container.Dump() 445 | signature.Message = containerJson 446 | 447 | if err := crypto.Verify(signature, []byte(entity.Data.Body.PublicSigningKey)); err != nil { 448 | return fmt.Errorf("Could not verify org container signature: %s", err) 449 | } else { 450 | return nil 451 | } 452 | } 453 | 454 | // ThreatSpec TMv0.1 for Entity.Decrypt 455 | // Does container decryption using private keys for App:Entity 456 | 457 | // Decrypt takes a Container and decrypts the content using the entities private decryption key. 458 | // It returns a plaintext string. 459 | func (entity *Entity) Decrypt(container *document.Container) (string, error) { 460 | 461 | if container.IsEncrypted() == false { 462 | return "", fmt.Errorf("Container isn't encrypted") 463 | } 464 | 465 | id := entity.Data.Body.Id 466 | key := entity.Data.Body.PrivateEncryptionKey 467 | if decryptedJson, err := container.Decrypt(id, key); err != nil { 468 | return "", fmt.Errorf("Could not decrypt: %s", err) 469 | } else { 470 | return decryptedJson, nil 471 | } 472 | } 473 | 474 | // ThreatSpec TMv0.1 for Entity.SymmetricDecrypt 475 | // Does container symmetric decryption using shared keys for App:Entity 476 | 477 | // SymmetricDecrypt takes a Container and decrypts the content using the provided key. 478 | // It returns a plaintext string. 479 | func (entity *Entity) SymmetricDecrypt(container *document.Container, key string) (string, error) { 480 | 481 | // TODO - check container is encrypted 482 | if decryptedJson, err := container.SymmetricDecrypt(key); err != nil { 483 | return "", fmt.Errorf("Could not decrypt: %s", err) 484 | } else { 485 | return decryptedJson, nil 486 | } 487 | } 488 | 489 | // ThreatSpec TMv0.1 for Entity.Public 490 | // Returns public version of entity for App:Entity 491 | 492 | // Public returns the public entity data. 493 | func (entity *Entity) Public() (*Entity, error) { 494 | selfJson := entity.Dump() 495 | publicEntity, err := New(selfJson) 496 | if err != nil { 497 | return nil, fmt.Errorf("Could not create public entity: %s", err) 498 | } 499 | publicEntity.Data.Body.PrivateSigningKey = "" 500 | publicEntity.Data.Body.PrivateEncryptionKey = "" 501 | return publicEntity, nil 502 | } 503 | 504 | // ThreatSpec TMv0.1 for Entity.SignString 505 | // Does string signing for App:Entity 506 | 507 | // SignString takes a message string and signs it. 508 | func (entity *Entity) SignString(content string) (*document.Container, error) { 509 | container, err := document.NewContainer(nil) 510 | if err != nil { 511 | return nil, fmt.Errorf("Could not create container: %s", err) 512 | } 513 | container.Data.Options.Source = entity.Data.Body.Id 514 | container.Data.Body = content 515 | if err := entity.Sign(container); err != nil { 516 | return nil, fmt.Errorf("Could not sign container: %s", err) 517 | } else { 518 | return container, nil 519 | } 520 | } 521 | 522 | // ThreatSpec TMv0.1 for Entity.AuthenticateString 523 | // Does string authentication using shared keys for App:Entity 524 | 525 | // AuthenticateString takes a message string and key and MACs the message using the provided key. 526 | func (entity *Entity) AuthenticateString(content, id, key string) (*document.Container, error) { 527 | container, err := document.NewContainer(nil) 528 | if err != nil { 529 | return nil, fmt.Errorf("Could not create container: %s", err) 530 | } 531 | container.Data.Options.Source = entity.Data.Body.Id 532 | container.Data.Body = content 533 | if err := entity.Authenticate(container, id, key); err != nil { 534 | return nil, fmt.Errorf("Could not sign container: %s", err) 535 | } else { 536 | return container, nil 537 | } 538 | } 539 | 540 | // ThreatSpec TMv0.1 for Entity.Encrypt 541 | // Does public key encryption for App:Entity 542 | 543 | // Encrypt takes a plaintext string and encrypts it for each provided entity. 544 | func (entity *Entity) Encrypt(content string, entities []Encrypter) (*document.Container, error) { 545 | encryptionKeys := make(map[string]string) 546 | 547 | if entities == nil { 548 | body := entity.Body() 549 | encryptionKeys[entity.Id()] = body.PublicEncryptionKey 550 | } else { 551 | for _, e := range entities { 552 | body := e.Body() 553 | encryptionKeys[e.Id()] = body.PublicEncryptionKey 554 | } 555 | 556 | } 557 | 558 | container, err := document.NewContainer(nil) 559 | if err != nil { 560 | return nil, fmt.Errorf("Could not create container: %s", err) 561 | } 562 | 563 | container.Data.Options.Source = entity.Data.Body.Id 564 | if err := container.Encrypt(content, encryptionKeys); err != nil { 565 | return nil, fmt.Errorf("Could not encrypt container: %s", err) 566 | } 567 | return container, nil 568 | } 569 | 570 | // ThreatSpec TMv0.1 for Entity.SymmetricEncrypt 571 | // Does symmetric encryption using shared keys for App:Entity 572 | 573 | // SymmetricEncrypt takes a plaintext string and encrypts it with the given key. 574 | func (entity *Entity) SymmetricEncrypt(content, id, key string) (*document.Container, error) { 575 | 576 | container, err := document.NewContainer(nil) 577 | if err != nil { 578 | return nil, fmt.Errorf("Could not create container: %s", err) 579 | } 580 | 581 | container.Data.Options.Source = entity.Data.Body.Id 582 | if err := container.SymmetricEncrypt(content, id, key); err != nil { 583 | return nil, fmt.Errorf("Could not symmetric encrypt container: %s", err) 584 | } 585 | 586 | return container, nil 587 | } 588 | 589 | // ThreatSpec TMv0.1 for Entity.EncryptThenSignString 590 | // Does public key encrypt-then-sign of strings for App:Entity 591 | 592 | // EncryptThenSignString takes a plaintext string, encrypts it then signs the ciphertext. 593 | func (entity *Entity) EncryptThenSignString(content string, entities []Encrypter) (*document.Container, error) { 594 | 595 | container, err := entity.Encrypt(content, entities) 596 | if err != nil { 597 | return nil, fmt.Errorf("Couldn't encrypt content: %s", err) 598 | } 599 | 600 | if err := entity.Sign(container); err != nil { 601 | return nil, fmt.Errorf("Could not sign container: %s", err) 602 | } 603 | 604 | return container, nil 605 | } 606 | 607 | // ThreatSpec TMv0.1 for Entity.EncryptThenAuthenticateString 608 | // Does symmetric encrypt-then-mac of strings for App:Entity 609 | 610 | // EncryptThenAuthenticateString takes a plaintext string, encrypts it using the key and the MACs the ciphertext using they key. 611 | // 612 | // Note: under the hood, the key is expanded into two separate keys, one for encryption and one for signing. 613 | func (entity *Entity) EncryptThenAuthenticateString(content, id, key string) (*document.Container, error) { 614 | 615 | container, err := entity.SymmetricEncrypt(content, id, key) 616 | if err != nil { 617 | return nil, fmt.Errorf("Couldn't encrypt content: %s", err) 618 | } 619 | if err := entity.Authenticate(container, id, key); err != nil { 620 | return nil, fmt.Errorf("Could not authenticate container: %s", err) 621 | } 622 | return container, nil 623 | } 624 | 625 | // ThreatSpec TMv0.1 for Entity.VerifyThenDecrypt 626 | // Does public key verify-then-decrypt for App:Entity 627 | 628 | // VerifyThenDecrypt takes a container, verifies the signature then decrypts, returning a plaintext string. 629 | func (entity *Entity) VerifyThenDecrypt(container *document.Container) (string, error) { 630 | if err := entity.Verify(container); err != nil { 631 | return "", fmt.Errorf("Could not verify container: %s", err) 632 | } 633 | 634 | content, err := entity.Decrypt(container) 635 | if err != nil { 636 | return "", fmt.Errorf("Could not decrypt container: %s", err) 637 | } 638 | return content, nil 639 | 640 | } 641 | 642 | // ThreatSpec TMv0.1 for Entity.VerifyAuthenticationThenDecrypt 643 | // Does symmetric verify-then-decrypt for App:Entity 644 | 645 | // VerifyAuthenticationThenDecrypt takes a container and verifies the MAC using the given key, then decrypts using the key, returning a plaintext string. 646 | // 647 | // Note: under the hood, the key is expanded into two separate keys, one for encryption and one for signing. 648 | func (entity *Entity) VerifyAuthenticationThenDecrypt(container *document.Container, key string) (string, error) { 649 | if err := entity.VerifyAuthentication(container, key); err != nil { 650 | return "", fmt.Errorf("Could not verify container: %s", err) 651 | } 652 | 653 | content, err := entity.SymmetricDecrypt(container, key) 654 | if err != nil { 655 | return "", fmt.Errorf("Could not decrypt container: %s", err) 656 | } 657 | return content, nil 658 | } 659 | -------------------------------------------------------------------------------- /entity/entity_test.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "encoding/hex" 5 | "github.com/pki-io/core/crypto" 6 | "github.com/stretchr/testify/assert" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestEntityNewDefault(t *testing.T) { 12 | entity, err := New(nil) 13 | assert.NoError(t, err) 14 | assert.NotNil(t, entity) 15 | assert.Equal(t, entity.Data.Scope, "pki.io") 16 | } 17 | 18 | func TestGenerateKeys(t *testing.T) { 19 | entity, _ := New(nil) 20 | entity.Data.Body.KeyType = string(crypto.KeyTypeRSA) 21 | err := entity.GenerateKeys() 22 | assert.NoError(t, err) 23 | assert.Equal(t, strings.Contains(entity.Data.Body.PublicSigningKey, "RSA PUBLIC KEY"), true) 24 | assert.Equal(t, strings.Contains(entity.Data.Body.PublicEncryptionKey, "RSA PUBLIC KEY"), true) 25 | 26 | entity.Data.Body.KeyType = string(crypto.KeyTypeEC) 27 | err = entity.GenerateKeys() 28 | assert.NoError(t, err) 29 | assert.Equal(t, strings.Contains(entity.Data.Body.PublicSigningKey, "EC PUBLIC KEY"), true) 30 | assert.Equal(t, strings.Contains(entity.Data.Body.PublicEncryptionKey, "EC PUBLIC KEY"), true) 31 | } 32 | 33 | func TestRSASignString(t *testing.T) { 34 | entity, _ := New(nil) 35 | entity.Data.Body.KeyType = string(crypto.KeyTypeRSA) 36 | entity.GenerateKeys() 37 | message := "this is a message" 38 | container, err := entity.SignString(message) 39 | assert.NoError(t, err) 40 | assert.Equal(t, container.Data.Body, message) 41 | assert.NotEqual(t, len(container.Data.Options.Signature), 0) 42 | assert.True(t, container.IsSigned()) 43 | } 44 | 45 | func TestRSAVerify(t *testing.T) { 46 | entity, _ := New(nil) 47 | entity.Data.Body.KeyType = string(crypto.KeyTypeRSA) 48 | entity.GenerateKeys() 49 | container, _ := entity.SignString("this is a message") 50 | err := entity.Verify(container) 51 | assert.NoError(t, err) 52 | } 53 | 54 | func TestECSignString(t *testing.T) { 55 | entity, _ := New(nil) 56 | entity.Data.Body.KeyType = string(crypto.KeyTypeEC) 57 | entity.GenerateKeys() 58 | message := "this is a message" 59 | container, err := entity.SignString(message) 60 | assert.NoError(t, err) 61 | assert.Equal(t, container.Data.Body, message) 62 | assert.NotEqual(t, len(container.Data.Options.Signature), 0) 63 | assert.True(t, container.IsSigned()) 64 | } 65 | 66 | func TestECVerify(t *testing.T) { 67 | entity, _ := New(nil) 68 | entity.Data.Body.KeyType = string(crypto.KeyTypeEC) 69 | entity.GenerateKeys() 70 | container, _ := entity.SignString("this is a message") 71 | err := entity.Verify(container) 72 | assert.NoError(t, err) 73 | } 74 | 75 | func TestAuthentication(t *testing.T) { 76 | entity, _ := New(nil) 77 | id := crypto.UUID() 78 | keyBytes, _ := crypto.RandomBytes(16) 79 | key := hex.EncodeToString(keyBytes) 80 | 81 | message := "this is a message" 82 | container, err := entity.AuthenticateString(message, id, key) 83 | assert.NoError(t, err) 84 | assert.Equal(t, container.Data.Body, message) 85 | assert.NotEqual(t, len(container.Data.Options.Signature), 0) 86 | assert.True(t, container.IsSigned()) 87 | } 88 | 89 | func TestVerifyAuthentication(t *testing.T) { 90 | entity, _ := New(nil) 91 | id := crypto.UUID() 92 | keyBytes, _ := crypto.RandomBytes(16) 93 | key := hex.EncodeToString(keyBytes) 94 | message := "this is a message" 95 | container, err := entity.AuthenticateString(message, id, key) 96 | 97 | err = entity.VerifyAuthentication(container, key) 98 | assert.NoError(t, err) 99 | } 100 | -------------------------------------------------------------------------------- /fs/api.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pki-io/core/api" 6 | "github.com/pki-io/core/crypto" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | const apiVersion string = "v0" 13 | const pathRoot string = "api" 14 | const publicPath string = "public" 15 | const privatePath string = "private" 16 | 17 | type Api struct { 18 | api.Api 19 | } 20 | 21 | func NewAPI(path string) (*Api, error) { 22 | fullPath := filepath.Join(path, pathRoot, apiVersion) 23 | if err := os.MkdirAll(fullPath, publicDirMode); err != nil { 24 | return nil, fmt.Errorf("Could not create path '%s': %s", fullPath, err) 25 | } 26 | api := new(Api) 27 | api.Path = fullPath 28 | return api, nil 29 | } 30 | 31 | func (api *Api) Connect(path string) error { 32 | fullPath := filepath.Join(path, pathRoot, apiVersion) 33 | if err := os.MkdirAll(fullPath, publicDirMode); err != nil { 34 | return fmt.Errorf("Could not create path '%s': %s", fullPath, err) 35 | } 36 | api.Path = fullPath 37 | return nil 38 | } 39 | 40 | func (api *Api) SendPublic(dstId string, name string, content string) error { 41 | path := filepath.Join(api.Path, dstId, publicPath) 42 | if err := os.MkdirAll(path, publicDirMode); err != nil { 43 | return fmt.Errorf("Could not create path '%s': %s", path, err) 44 | } 45 | filename := filepath.Join(path, name) 46 | if err := ioutil.WriteFile(filename, []byte(content), publicFileMode); err != nil { 47 | return fmt.Errorf("Could not write file '%s': %s", filename, err) 48 | } 49 | return nil 50 | } 51 | 52 | func (api *Api) GetPublic(dstId string, name string) (string, error) { 53 | filename := filepath.Join(api.Path, dstId, publicPath, name) 54 | if content, err := ReadFile(filename); err != nil { 55 | return "", fmt.Errorf("Could not read file '%s': %s", filename, err) 56 | } else { 57 | return string(content), nil 58 | } 59 | } 60 | 61 | func (api *Api) SendPrivate(dstId string, name string, content string) error { 62 | path := filepath.Join(api.Path, dstId, privatePath) 63 | if err := os.MkdirAll(path, privateDirMode); err != nil { 64 | return fmt.Errorf("Could not create path '%s': %s", path, err) 65 | } 66 | filename := filepath.Join(path, name) 67 | if err := ioutil.WriteFile(filename, []byte(content), privateFileMode); err != nil { 68 | return fmt.Errorf("Could not write file '%s': %s", filename, err) 69 | } 70 | return nil 71 | } 72 | 73 | func (api *Api) GetPrivate(dstId string, name string) (string, error) { 74 | filename := filepath.Join(api.Path, dstId, privatePath, name) 75 | if content, err := ReadFile(filename); err != nil { 76 | return "", fmt.Errorf("Could not read file '%s': %s", filename, err) 77 | } else { 78 | return string(content), nil 79 | } 80 | } 81 | 82 | func (api *Api) DeletePrivate(id, name string) error { 83 | filename := filepath.Join(api.Path, id, privatePath, name) 84 | 85 | if err := os.Remove(filename); err != nil { 86 | return fmt.Errorf("Couldn't remove file: %s", err) 87 | } 88 | return nil 89 | } 90 | 91 | func (api *Api) Push(dstId, name, queue, content string) error { 92 | path := filepath.Join(api.Path, dstId, name, queue) 93 | 94 | if err := os.MkdirAll(path, privateDirMode); err != nil { 95 | return fmt.Errorf("Could not create path '%s': %s", path, err) 96 | } 97 | filename := filepath.Join(path, crypto.TimeOrderedUUID()) 98 | if err := ioutil.WriteFile(filename, []byte(content), privateFileMode); err != nil { 99 | return fmt.Errorf("Could not write file '%s': %s", filename, err) 100 | } 101 | return nil 102 | } 103 | 104 | func (api *Api) PushIncoming(dstId, queue, content string) error { 105 | return api.Push(dstId, "incoming", queue, content) 106 | } 107 | 108 | func (api *Api) PushOutgoing(dstId, queue, content string) error { 109 | return api.Push(dstId, "outgoing", queue, content) 110 | } 111 | 112 | func (api *Api) Pop(srcId, name, queue string) (string, error) { 113 | pattern := filepath.Join(api.Path, srcId, name, queue, "*") 114 | files, err := filepath.Glob(pattern) 115 | if err != nil { 116 | return "", fmt.Errorf("Could not glob files: %s", err) 117 | } 118 | 119 | if len(files) == 0 { 120 | return "", fmt.Errorf("Nothing to pop") 121 | } 122 | 123 | filename := files[0] 124 | if content, err := ReadFile(filename); err != nil { 125 | return "", fmt.Errorf("Could not read file '%s': %s", filename, err) 126 | } else { 127 | if err := os.Remove(filename); err != nil { 128 | return "", fmt.Errorf("Couldn't remove file: %s", err) 129 | } else { 130 | return string(content), nil 131 | } 132 | } 133 | } 134 | 135 | func (api *Api) PopOutgoing(srcId, queue string) (string, error) { 136 | return api.Pop(srcId, "outgoing", queue) 137 | } 138 | 139 | func (api *Api) PopIncoming(srcId, queue string) (string, error) { 140 | return api.Pop(srcId, "incoming", queue) 141 | } 142 | 143 | func (api *Api) Size(id, name, queue string) (int, error) { 144 | pattern := filepath.Join(api.Path, id, name, queue, "*") 145 | files, err := filepath.Glob(pattern) 146 | if err != nil { 147 | return 0, fmt.Errorf("Could not glob files: %s", err) 148 | } 149 | return len(files), nil 150 | } 151 | 152 | func (api *Api) OutgoingSize(id, queue string) (int, error) { 153 | return api.Size(id, "outgoing", queue) 154 | } 155 | 156 | func (api *Api) IncomingSize(id, queue string) (int, error) { 157 | return api.Size(id, "incoming", queue) 158 | } 159 | 160 | func (api *Api) Authenticate(id, key string) error { 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /fs/api_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | // Need teardown function to clean up dir 11 | 12 | func TestFSNewAPI(t *testing.T) { 13 | currentDir, _ := os.Getwd() 14 | path := filepath.Join(currentDir, "fs-test") 15 | fs, err := NewAPI(path) 16 | assert.Nil(t, err) 17 | assert.NotNil(t, fs) 18 | } 19 | 20 | func TestFSPushPopIncoming(t *testing.T) { 21 | currentDir, _ := os.Getwd() 22 | path := filepath.Join(currentDir, "fs-test") 23 | fs, err := NewAPI(path) 24 | fs.Id = "123" 25 | err = fs.PushIncoming(fs.Id, "test", "this is a test") 26 | assert.Nil(t, err) 27 | content, err := fs.PopIncoming(fs.Id, "test") 28 | assert.Nil(t, err) 29 | assert.Equal(t, content, "this is a test") 30 | } 31 | 32 | func TestFSPushPopOutgoing(t *testing.T) { 33 | currentDir, _ := os.Getwd() 34 | path := filepath.Join(currentDir, "fs-test") 35 | fs, err := NewAPI(path) 36 | fs.Id = "123" 37 | err = fs.PushOutgoing(fs.Id, "test", "this is a test") 38 | assert.Nil(t, err) 39 | content, err := fs.PopOutgoing(fs.Id, "test") 40 | assert.Nil(t, err) 41 | assert.Equal(t, content, "this is a test") 42 | } 43 | 44 | func TestFSIncomingSize(t *testing.T) { 45 | currentDir, _ := os.Getwd() 46 | path := filepath.Join(currentDir, "fs-test") 47 | fs, err := NewAPI(path) 48 | fs.Id = "123" 49 | fs.PushIncoming(fs.Id, "test", "this is a test") 50 | size, err := fs.IncomingSize(fs.Id, "test") 51 | assert.Nil(t, err) 52 | assert.NotEqual(t, size, 0) 53 | } 54 | -------------------------------------------------------------------------------- /fs/helpers.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | ) 7 | 8 | const publicFileMode os.FileMode = 0644 9 | const publicDirMode os.FileMode = 0755 10 | const privateFileMode os.FileMode = 0600 11 | const privateDirMode os.FileMode = 0700 12 | 13 | func Exists(path string) (bool, error) { 14 | if f, err := os.Open(path); err != nil { 15 | if os.IsNotExist(err) { 16 | return false, nil 17 | } else { 18 | return false, err 19 | } 20 | } else { 21 | f.Close() 22 | return true, nil 23 | } 24 | } 25 | 26 | func ReadFile(path string) (string, error) { 27 | if content, err := ioutil.ReadFile(path); err != nil { 28 | return "", err 29 | } else { 30 | return string(content), nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fs/home.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/fs as fs 2 | package fs 3 | 4 | import ( 5 | "fmt" 6 | "github.com/mitchellh/go-homedir" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | const homePath string = ".pki.io" 13 | 14 | type Home struct { 15 | Path string 16 | } 17 | 18 | func NewHome(path string) (*Home, error) { 19 | home := new(Home) 20 | var homeDir string 21 | var err error 22 | if path == "" { 23 | homeDir, err = homedir.Dir() 24 | if err != nil { 25 | return nil, fmt.Errorf("Couldn't get home directory: %s", err) 26 | } 27 | } else { 28 | homeDir = path 29 | } 30 | 31 | home.Path = filepath.Join(homeDir, homePath) 32 | 33 | if err := os.MkdirAll(home.Path, privateDirMode); err != nil { 34 | return nil, fmt.Errorf("Could not create path: %s", err) 35 | } 36 | return home, nil 37 | } 38 | 39 | func (home *Home) FullPath(name string) string { 40 | return filepath.Join(home.Path, name) 41 | } 42 | 43 | // ThreatSpec TMv0.1 for Home.Write 44 | // It writes files relative to home location for App:FileSystem 45 | // Mitigates App:FileSystem against unauthorised access with strict file permissions 46 | 47 | func (home *Home) Write(name, content string) error { 48 | if err := ioutil.WriteFile(home.FullPath(name), []byte(content), privateFileMode); err != nil { 49 | return fmt.Errorf("Could not write file: %s", err) 50 | } 51 | return nil 52 | } 53 | 54 | // ThreatSpec TMv0.1 for Home.Read 55 | // It reads files relative to home location for App:FileSystem 56 | // Exposes App:FileSystem to arbitrary data reads with insufficient file permission validation 57 | 58 | func (home *Home) Read(name string) (string, error) { 59 | if content, err := ReadFile(home.FullPath(name)); err != nil { 60 | return "", fmt.Errorf("Could not read file: %s", err) 61 | } else { 62 | return string(content), nil 63 | } 64 | } 65 | 66 | func (home *Home) Exists(name string) (bool, error) { 67 | return Exists(filepath.Join(home.Path, name)) 68 | } 69 | 70 | func (home *Home) Delete(name string) error { 71 | exists, err := home.Exists(name) 72 | if err != nil { 73 | return fmt.Errorf("Couldn't check file existence for %s: %s", name, err) 74 | } 75 | 76 | if exists { 77 | if err := os.Remove(home.FullPath(name)); err != nil { 78 | return fmt.Errorf("Couldn't delete config file: %s", err) 79 | } else { 80 | return nil 81 | 82 | } 83 | } else { 84 | return nil 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /fs/home_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestHomeNew(t *testing.T) { 9 | home, err := NewHome("") 10 | assert.Nil(t, err) 11 | assert.NotNil(t, home) 12 | } 13 | 14 | func TestHomeWriteExistsReadDelete(t *testing.T) { 15 | home, _ := NewHome("") 16 | file := "test" 17 | content := "testing" 18 | err := home.Write(file, content) 19 | assert.Nil(t, err) 20 | exists, err := home.Exists(file) 21 | assert.Nil(t, err) 22 | assert.True(t, exists) 23 | newContent, err := home.Read(file) 24 | assert.Nil(t, err) 25 | assert.Equal(t, content, newContent) 26 | err = home.Delete(file) 27 | assert.Nil(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /fs/local.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/fs as fs 2 | package fs 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | type Local struct { 12 | Path string 13 | } 14 | 15 | // ThreatSpec TMv0.1 for NewLocal 16 | // Creates new local environment for App:FileSystem 17 | 18 | func NewLocal(path string) (*Local, error) { 19 | local := new(Local) 20 | 21 | var currentDir string 22 | var err error 23 | if path == "" { 24 | currentDir, err = os.Getwd() 25 | if err != nil { 26 | return nil, fmt.Errorf("Couldn't get current directory: %s", err) 27 | } 28 | } else { 29 | currentDir = path 30 | } 31 | local.Path = currentDir 32 | return local, nil 33 | } 34 | 35 | // ThreatSpec TMv0.1 for Local.CreateDirectory 36 | // Creates filesystem directory for App:FileSystem 37 | // Sends mkdir from App:FileSystem to OS:FileSystem 38 | 39 | func (local *Local) CreateDirectory(dir string) error { 40 | if err := os.MkdirAll(filepath.Join(local.Path, dir), privateDirMode); err != nil { 41 | return fmt.Errorf("Could not create path: %s", err) 42 | } 43 | return nil 44 | } 45 | 46 | // ThreatSpec TMv0.1 for Local.ChangeToDirectiory 47 | // Does a change of working directory for App:FileSystem 48 | 49 | func (local *Local) ChangeToDirectory(dir string) error { 50 | local.Path = filepath.Join(local.Path, dir) 51 | return nil 52 | } 53 | 54 | // ThreatSpec TMv0.1 for Local.FullPath 55 | // Returns full local path for App:FileSystem 56 | 57 | func (local *Local) FullPath(name string) string { 58 | return filepath.Join(local.Path, name) 59 | } 60 | 61 | func (local *Local) Write(name, content string) error { 62 | if err := ioutil.WriteFile(local.FullPath(name), []byte(content), privateFileMode); err != nil { 63 | return fmt.Errorf("Could not write file: %s", err) 64 | } 65 | return nil 66 | } 67 | 68 | func (local *Local) Read(name string) (string, error) { 69 | if content, err := ReadFile(local.FullPath(name)); err != nil { 70 | return "", fmt.Errorf("Could not read file: %s", err) 71 | } else { 72 | return string(content), nil 73 | } 74 | } 75 | 76 | func (local *Local) Exists(name string) (bool, error) { 77 | return Exists(filepath.Join(local.Path, name)) 78 | } 79 | 80 | func (local *Local) Delete(name string) error { 81 | exists, err := local.Exists(name) 82 | if err != nil { 83 | return fmt.Errorf("Couldn't check file existence for %s: %s", name, err) 84 | } 85 | 86 | if exists { 87 | if err := os.Remove(local.FullPath(name)); err != nil { 88 | return fmt.Errorf("Couldn't delete config file: %s", err) 89 | } else { 90 | return nil 91 | 92 | } 93 | } else { 94 | return nil 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /fs/local_test.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestLocalNew(t *testing.T) { 9 | local, err := NewLocal("") 10 | assert.Nil(t, err) 11 | assert.NotNil(t, local) 12 | } 13 | 14 | func TestLocalWriteExistsReadDelete(t *testing.T) { 15 | local, _ := NewLocal("") 16 | file := "test" 17 | content := "testing" 18 | err := local.Write(file, content) 19 | assert.Nil(t, err) 20 | exists, err := local.Exists(file) 21 | assert.Nil(t, err) 22 | assert.True(t, exists) 23 | newContent, err := local.Read(file) 24 | assert.Nil(t, err) 25 | assert.Equal(t, content, newContent) 26 | err = local.Delete(file) 27 | assert.Nil(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /index/helpers.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | func AppendUnique(slice []string, val string) []string { 4 | found := false 5 | for _, v := range slice { 6 | if v == val { 7 | found = true 8 | break 9 | } 10 | } 11 | 12 | if found { 13 | return slice 14 | } else { 15 | return append(slice, val) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /index/node.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pki-io/core/document" 6 | ) 7 | 8 | const NodeIndexDefault string = `{ 9 | "scope": "pki.io", 10 | "version": 1, 11 | "type": "node-index-document", 12 | "options": "", 13 | "body": { 14 | "parent-id": "", 15 | "tags": { 16 | "cert-forward": {}, 17 | "cert-reverse": {} 18 | } 19 | } 20 | }` 21 | 22 | const NodeIndexSchema string = `{ 23 | "$schema": "http://json-schema.org/draft-04/schema#", 24 | "title": "NodeIndexDocument", 25 | "description": "Node Index Document", 26 | "type": "object", 27 | "required": ["scope","version","type","options","body"], 28 | "additionalProperties": false, 29 | "properties": { 30 | "scope": { 31 | "description": "Scope of the document", 32 | "type": "string" 33 | }, 34 | "version": { 35 | "description": "Document schema version", 36 | "type": "integer" 37 | }, 38 | "type": { 39 | "description": "Type of document", 40 | "type": "string" 41 | }, 42 | "options": { 43 | "description": "Options data", 44 | "type": "string" 45 | }, 46 | "body": { 47 | "description": "Body data", 48 | "type": "object", 49 | "required": ["id", "parent-id", "tags"], 50 | "additionalProperties": false, 51 | "properties": { 52 | "id" : { 53 | "description": "ID", 54 | "type": "string" 55 | }, 56 | "parent-id" : { 57 | "description": "Parent ID", 58 | "type": "string" 59 | }, 60 | "tags": { 61 | "description": "Tags", 62 | "type": "object", 63 | "required": ["cert-forward","cert-reverse"], 64 | "additionalProperties": false, 65 | "properties": { 66 | "cert-forward": { 67 | "description": "Tags to Certs", 68 | "type": "object" 69 | }, 70 | "cert-reverse": { 71 | "description": "Cert to tags", 72 | "type": "object" 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | }` 80 | 81 | type NodeIndexData struct { 82 | Scope string `json:"scope"` 83 | Version int `json:"version"` 84 | Type string `json:"type"` 85 | Options string `json:"options"` 86 | Body struct { 87 | Id string `json:"id"` 88 | ParentId string `json:"parent-id"` 89 | Tags struct { 90 | CertForward map[string][]string `json:"cert-forward"` 91 | CertReverse map[string][]string `json:"cert-reverse"` 92 | } `json:"tags"` 93 | } `json:"body"` 94 | } 95 | 96 | type NodeIndex struct { 97 | document.Document 98 | Data NodeIndexData 99 | } 100 | 101 | func NewNode(jsonString interface{}) (*NodeIndex, error) { 102 | index := new(NodeIndex) 103 | index.Schema = NodeIndexSchema 104 | index.Default = NodeIndexDefault 105 | if err := index.Load(jsonString); err != nil { 106 | return nil, fmt.Errorf("Could not create new Index: %s", err) 107 | } else { 108 | return index, nil 109 | } 110 | } 111 | 112 | func (index *NodeIndex) Load(jsonString interface{}) error { 113 | data := new(NodeIndexData) 114 | if data, err := index.FromJson(jsonString, data); err != nil { 115 | return fmt.Errorf("Could not load Index JSON: %s", err) 116 | } else { 117 | index.Data = *data.(*NodeIndexData) 118 | return nil 119 | } 120 | } 121 | 122 | func (index *NodeIndex) Dump() string { 123 | if jsonString, err := index.ToJson(index.Data); err != nil { 124 | return "" 125 | } else { 126 | return jsonString 127 | } 128 | } 129 | 130 | func (index *NodeIndex) Id() string { 131 | return index.Data.Body.Id 132 | } 133 | 134 | func (index *NodeIndex) AddCertTags(cert string, i interface{}) error { 135 | var inTags []string 136 | switch t := i.(type) { 137 | case string: 138 | inTags = []string{i.(string)} 139 | case []string: 140 | inTags = i.([]string) 141 | default: 142 | return fmt.Errorf("Could not add Cert tags. Wrong data type for tags: %T", t) 143 | } 144 | 145 | for _, tag := range inTags { 146 | index.Data.Body.Tags.CertForward[tag] = AppendUnique(index.Data.Body.Tags.CertForward[tag], cert) 147 | index.Data.Body.Tags.CertReverse[cert] = AppendUnique(index.Data.Body.Tags.CertReverse[cert], tag) 148 | } 149 | 150 | return nil 151 | } 152 | -------------------------------------------------------------------------------- /index/node_test.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestNodeIndexNew(t *testing.T) { 9 | index, err := NewNode(nil) 10 | assert.Nil(t, err) 11 | assert.NotNil(t, index) 12 | assert.Equal(t, index.Data.Scope, "pki.io") 13 | } 14 | 15 | func TestNodeIndexDump(t *testing.T) { 16 | index, _ := NewNode(nil) 17 | indexJson := index.Dump() 18 | assert.NotEqual(t, len(indexJson), 0) 19 | } 20 | 21 | func TestNodeIndexAddCertTagsString(t *testing.T) { 22 | index, _ := NewNode(nil) 23 | err := index.AddCertTags("cert1", "tag1") 24 | assert.Nil(t, err) 25 | } 26 | 27 | func TestNodeIndexAddCertTagsSlice(t *testing.T) { 28 | index, _ := NewNode(nil) 29 | inIndex := []string{"tag1", "tag2"} 30 | err := index.AddCertTags("cert2", inIndex) 31 | assert.Nil(t, err) 32 | } 33 | 34 | func TestNodeIndexAddTags(t *testing.T) { 35 | index, _ := NewNode(nil) 36 | inTags1 := []string{"tag1", "tag2"} 37 | inTags2 := []string{"tag2", "tag3"} 38 | err := index.AddCertTags("cert1", inTags2) 39 | assert.Nil(t, err) 40 | err = index.AddCertTags("cert2", inTags1) 41 | assert.Nil(t, err) 42 | } 43 | -------------------------------------------------------------------------------- /index/org.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pki-io/core/document" 6 | ) 7 | 8 | const OrgIndexDefault string = `{ 9 | "scope": "pki.io", 10 | "version": 1, 11 | "type": "org-index-document", 12 | "options": "", 13 | "body": { 14 | "id": "", 15 | "parent-id": "", 16 | "tags": { 17 | "ca-forward": {}, 18 | "ca-reverse": {}, 19 | "entity-forward": {}, 20 | "entity-reverse": {}, 21 | "cert-forward": {}, 22 | "cert-reverse": {}, 23 | "csr-forward": {}, 24 | "csr-reverse": {} 25 | }, 26 | "nodes": {}, 27 | "admins": {}, 28 | "pairing-keys": {}, 29 | "invite-keys": {}, 30 | "cas": {}, 31 | "certs": {}, 32 | "csrs": {} 33 | } 34 | }` 35 | 36 | const OrgIndexSchema string = `{ 37 | "$schema": "http://json-schema.org/draft-04/schema#", 38 | "title": "OrgIndexDocument", 39 | "description": "Org Index Document", 40 | "type": "object", 41 | "required": ["scope","version","type","options","body"], 42 | "additionalProperties": false, 43 | "properties": { 44 | "scope": { 45 | "description": "Scope of the document", 46 | "type": "string" 47 | }, 48 | "version": { 49 | "description": "Document schema version", 50 | "type": "integer" 51 | }, 52 | "type": { 53 | "description": "Type of document", 54 | "type": "string" 55 | }, 56 | "options": { 57 | "description": "Options data", 58 | "type": "string" 59 | }, 60 | "body": { 61 | "description": "Body data", 62 | "type": "object", 63 | "required": ["id", "parent-id", "invite-keys", "pairing-keys", "nodes", "admins", "cas", "certs", "tags"], 64 | "additionalProperties": false, 65 | "properties": { 66 | "id": { 67 | "description": "ID", 68 | "type": "string" 69 | }, 70 | "parent-id" : { 71 | "description": "Parent ID", 72 | "type": "string" 73 | }, 74 | "pairing-keys": { 75 | "description": "Pairing Keys", 76 | "type": "object" 77 | }, 78 | "invite-keys": { 79 | "description": "Invite Keys", 80 | "type": "object" 81 | }, 82 | "nodes": { 83 | "description": "Nodes name to ID map", 84 | "type": "object" 85 | }, 86 | "admins": { 87 | "description": "Admins name to ID map", 88 | "type": "object" 89 | }, 90 | "cas": { 91 | "description": "CAs name to ID map", 92 | "type": "object" 93 | }, 94 | "certs": { 95 | "description": "Certs name to ID map", 96 | "type": "object" 97 | }, 98 | "csrs": { 99 | "description": "CSRs name to ID map", 100 | "type": "object" 101 | }, 102 | "tags": { 103 | "description": "Tags", 104 | "type": "object", 105 | "required": ["ca-forward","ca-reverse","entity-forward","entity-reverse", "cert-forward", "cert-reverse", "csr-forward", "csr-reverse"], 106 | "additionalProperties": false, 107 | "properties": { 108 | "ca-forward": { 109 | "description": "Tags to CAs", 110 | "type": "object" 111 | }, 112 | "ca-reverse": { 113 | "description": "CA to tags", 114 | "type": "object" 115 | }, 116 | "entity-forward": { 117 | "description": "Tags to entities", 118 | "type": "object" 119 | }, 120 | "entity-reverse": { 121 | "description": "Entities to tags", 122 | "type": "object" 123 | }, 124 | "cert-forward": { 125 | "description": "Tags to Certs", 126 | "type": "object" 127 | }, 128 | "cert-reverse": { 129 | "description": "Cert to tags", 130 | "type": "object" 131 | }, 132 | "csr-forward": { 133 | "description": "Tags to CSRs", 134 | "type": "object" 135 | }, 136 | "csr-reverse": { 137 | "description": "CSRs to tags", 138 | "type": "object" 139 | } 140 | } 141 | } 142 | } 143 | } 144 | } 145 | }` 146 | 147 | type PairingKey struct { 148 | Key string `json:"key"` 149 | Tags []string `json:"tags"` 150 | } 151 | 152 | type InviteKey struct { 153 | Name string `json:"name"` 154 | Key string `json:"key"` 155 | } 156 | 157 | type OrgIndexData struct { 158 | Scope string `json:"scope"` 159 | Version int `json:"version"` 160 | Type string `json:"type"` 161 | Options string `json:"options"` 162 | Body struct { 163 | Id string `json:"id"` 164 | ParentId string `json:"parent-id"` 165 | PairingKeys map[string]*PairingKey `json:"pairing-keys"` 166 | InviteKeys map[string]*InviteKey `json:"invite-keys"` 167 | Nodes map[string]string `json:"nodes"` 168 | Admins map[string]string `json:"admins"` 169 | CAs map[string]string `json:"cas"` 170 | Certs map[string]string `json:"certs"` 171 | CSRs map[string]string `json:"csrs"` 172 | Tags struct { 173 | CAForward map[string][]string `json:"ca-forward"` 174 | CAReverse map[string][]string `json:"ca-reverse"` 175 | EntityForward map[string][]string `json:"entity-forward"` 176 | EntityReverse map[string][]string `json:"entity-reverse"` 177 | CertForward map[string][]string `json:"cert-forward"` 178 | CertReverse map[string][]string `json:"cert-reverse"` 179 | CSRForward map[string][]string `json:"csr-forward"` 180 | CSRReverse map[string][]string `json:"csr-reverse"` 181 | } `json:"tags"` 182 | } `json:"body"` 183 | } 184 | 185 | type OrgIndex struct { 186 | document.Document 187 | Data OrgIndexData 188 | } 189 | 190 | func NewOrg(jsonString interface{}) (*OrgIndex, error) { 191 | index := new(OrgIndex) 192 | index.Schema = OrgIndexSchema 193 | index.Default = OrgIndexDefault 194 | if err := index.Load(jsonString); err != nil { 195 | return nil, fmt.Errorf("Could not create new Index: %s", err) 196 | } else { 197 | return index, nil 198 | } 199 | } 200 | 201 | func (index *OrgIndex) Load(jsonString interface{}) error { 202 | data := new(OrgIndexData) 203 | if data, err := index.FromJson(jsonString, data); err != nil { 204 | return fmt.Errorf("Could not load Index JSON: %s", err) 205 | } else { 206 | index.Data = *data.(*OrgIndexData) 207 | return nil 208 | } 209 | } 210 | 211 | func (index *OrgIndex) Dump() string { 212 | if jsonString, err := index.ToJson(index.Data); err != nil { 213 | return "" 214 | } else { 215 | return jsonString 216 | } 217 | } 218 | 219 | func (index *OrgIndex) Id() string { 220 | return index.Data.Body.Id 221 | } 222 | 223 | func (index *OrgIndex) AddCATags(ca string, i interface{}) error { 224 | var inTags []string 225 | switch t := i.(type) { 226 | case string: 227 | inTags = []string{i.(string)} 228 | case []string: 229 | inTags = i.([]string) 230 | default: 231 | return fmt.Errorf("Could not add CA tags. Wrong data type for tags: %T", t) 232 | } 233 | 234 | for _, tag := range inTags { 235 | index.Data.Body.Tags.CAForward[tag] = AppendUnique(index.Data.Body.Tags.CAForward[tag], ca) 236 | index.Data.Body.Tags.CAReverse[ca] = AppendUnique(index.Data.Body.Tags.CAReverse[ca], tag) 237 | } 238 | 239 | return nil 240 | } 241 | 242 | func (index *OrgIndex) ClearCATags(ca string) error { 243 | _, ok := index.Data.Body.Tags.CAReverse[ca] 244 | if ok { 245 | delete(index.Data.Body.Tags.CAReverse, ca) 246 | } 247 | for tag, _ := range index.Data.Body.Tags.CAForward { 248 | for i, taggedCA := range index.Data.Body.Tags.CAForward[tag] { 249 | if taggedCA == ca { 250 | index.Data.Body.Tags.CAForward[tag] = append(index.Data.Body.Tags.CAForward[tag][:i], index.Data.Body.Tags.CAForward[tag][i+1:]...) 251 | } 252 | } 253 | } 254 | return nil 255 | } 256 | 257 | func (index *OrgIndex) AddEntityTags(entity string, i interface{}) error { 258 | var inTags []string 259 | switch t := i.(type) { 260 | case string: 261 | inTags = []string{i.(string)} 262 | case []string: 263 | inTags = i.([]string) 264 | default: 265 | return fmt.Errorf("Could not add Entity tags. Wrong data type for tags: %T", t) 266 | } 267 | for _, tag := range inTags { 268 | index.Data.Body.Tags.EntityForward[tag] = AppendUnique(index.Data.Body.Tags.EntityForward[tag], entity) 269 | index.Data.Body.Tags.EntityReverse[entity] = AppendUnique(index.Data.Body.Tags.EntityReverse[entity], tag) 270 | } 271 | 272 | return nil 273 | } 274 | 275 | func (index *OrgIndex) AddPairingKey(id, key string, i interface{}) error { 276 | _, ok := index.Data.Body.PairingKeys[id] 277 | if ok { 278 | return fmt.Errorf("key %s already exists", id) 279 | } 280 | 281 | var inTags []string 282 | switch t := i.(type) { 283 | case string: 284 | inTags = []string{i.(string)} 285 | case []string: 286 | inTags = i.([]string) 287 | default: 288 | return fmt.Errorf("Could not add pairing key. Wrong data type for tags: %T", t) 289 | } 290 | 291 | pairingKey := new(PairingKey) 292 | pairingKey.Key = key 293 | 294 | for _, tag := range inTags { 295 | pairingKey.Tags = AppendUnique(pairingKey.Tags, tag) 296 | } 297 | index.Data.Body.PairingKeys[id] = pairingKey 298 | return nil 299 | } 300 | 301 | func (index *OrgIndex) GetPairingKeys() map[string]*PairingKey { 302 | return index.Data.Body.PairingKeys 303 | } 304 | 305 | func (index *OrgIndex) GetPairingKey(id string) (*PairingKey, error) { 306 | _, ok := index.Data.Body.PairingKeys[id] 307 | if !ok { 308 | return nil, fmt.Errorf("key %s does not exist", id) 309 | } 310 | return index.Data.Body.PairingKeys[id], nil 311 | } 312 | 313 | func (index *OrgIndex) RemovePairingKey(id string) error { 314 | _, ok := index.Data.Body.PairingKeys[id] 315 | if !ok { 316 | return fmt.Errorf("key %s does not exist", id) 317 | } 318 | delete(index.Data.Body.PairingKeys, id) 319 | return nil 320 | } 321 | 322 | func (index *OrgIndex) AddInviteKey(id, key, name string) error { 323 | _, ok := index.Data.Body.InviteKeys[id] 324 | if ok { 325 | return fmt.Errorf("key %s already exists", id) 326 | } 327 | inviteKey := new(InviteKey) 328 | inviteKey.Name = name 329 | inviteKey.Key = key 330 | index.Data.Body.InviteKeys[id] = inviteKey 331 | return nil 332 | } 333 | 334 | func (index *OrgIndex) GetInviteKey(id string) (*InviteKey, error) { 335 | _, ok := index.Data.Body.InviteKeys[id] 336 | if !ok { 337 | return nil, fmt.Errorf("key %s does not exist", id) 338 | } 339 | return index.Data.Body.InviteKeys[id], nil 340 | } 341 | 342 | func (index *OrgIndex) AddNode(name, id string) error { 343 | _, ok := index.Data.Body.Nodes[name] 344 | if ok { 345 | return fmt.Errorf("key %s already exists", name) 346 | } 347 | index.Data.Body.Nodes[name] = id 348 | return nil 349 | } 350 | 351 | func (index *OrgIndex) GetNode(name string) (string, error) { 352 | _, ok := index.Data.Body.Nodes[name] 353 | if !ok { 354 | return "", fmt.Errorf("key %s does not exist", name) 355 | } 356 | return index.Data.Body.Nodes[name], nil 357 | } 358 | 359 | func (index *OrgIndex) RemoveNode(name string) error { 360 | _, ok := index.Data.Body.Nodes[name] 361 | if !ok { 362 | return fmt.Errorf("key %s does not exist", name) 363 | } 364 | delete(index.Data.Body.Nodes, name) 365 | return nil 366 | } 367 | 368 | func (index *OrgIndex) GetNodes() map[string]string { 369 | return index.Data.Body.Nodes 370 | } 371 | 372 | func (index *OrgIndex) AddAdmin(name, id string) error { 373 | _, ok := index.Data.Body.Admins[name] 374 | if ok { 375 | return fmt.Errorf("key %s already exists", name) 376 | } 377 | index.Data.Body.Admins[name] = id 378 | return nil 379 | } 380 | 381 | func (index *OrgIndex) RemoveAdmin(name string) error { 382 | _, ok := index.Data.Body.Admins[name] 383 | if !ok { 384 | return fmt.Errorf("key %s does not exist", name) 385 | } 386 | delete(index.Data.Body.Admins, name) 387 | return nil 388 | } 389 | 390 | func (index *OrgIndex) GetAdmin(name string) (string, error) { 391 | _, ok := index.Data.Body.Admins[name] 392 | if !ok { 393 | return "", fmt.Errorf("key %s does not exist", name) 394 | } 395 | return index.Data.Body.Admins[name], nil 396 | } 397 | 398 | func (index *OrgIndex) GetAdmins() (map[string]string, error) { 399 | return index.Data.Body.Admins, nil 400 | } 401 | 402 | func (index *OrgIndex) AddCA(name, id string) error { 403 | _, ok := index.Data.Body.CAs[name] 404 | if ok { 405 | return fmt.Errorf("key %s already exists", name) 406 | } 407 | index.Data.Body.CAs[name] = id 408 | return nil 409 | } 410 | 411 | func (index *OrgIndex) GetCA(name string) (string, error) { 412 | _, ok := index.Data.Body.CAs[name] 413 | if !ok { 414 | return "", fmt.Errorf("key %s does not exist", name) 415 | } 416 | return index.Data.Body.CAs[name], nil 417 | } 418 | 419 | func (index *OrgIndex) GetCAs() map[string]string { 420 | return index.Data.Body.CAs 421 | } 422 | 423 | func (index *OrgIndex) RemoveCA(name string) error { 424 | _, ok := index.Data.Body.CAs[name] 425 | if !ok { 426 | return fmt.Errorf("CA %s does not exist", name) 427 | } 428 | delete(index.Data.Body.CAs, name) 429 | return nil 430 | } 431 | 432 | func (index *OrgIndex) AddCertTags(cert string, i interface{}) error { 433 | var inTags []string 434 | switch t := i.(type) { 435 | case string: 436 | inTags = []string{i.(string)} 437 | case []string: 438 | inTags = i.([]string) 439 | default: 440 | return fmt.Errorf("Could not add Cert tags. Wrong data type for tags: %T", t) 441 | } 442 | 443 | for _, tag := range inTags { 444 | index.Data.Body.Tags.CertForward[tag] = AppendUnique(index.Data.Body.Tags.CertForward[tag], cert) 445 | index.Data.Body.Tags.CertReverse[cert] = AppendUnique(index.Data.Body.Tags.CertReverse[cert], tag) 446 | } 447 | 448 | return nil 449 | } 450 | 451 | func (index *OrgIndex) AddCert(name, id string) error { 452 | _, ok := index.Data.Body.Certs[name] 453 | if ok { 454 | return fmt.Errorf("key %s already exists", name) 455 | } 456 | index.Data.Body.Certs[name] = id 457 | return nil 458 | } 459 | 460 | func (index *OrgIndex) GetCert(name string) (string, error) { 461 | _, ok := index.Data.Body.Certs[name] 462 | if !ok { 463 | return "", fmt.Errorf("key %s does not exist", name) 464 | } 465 | return index.Data.Body.Certs[name], nil 466 | } 467 | 468 | func (index *OrgIndex) GetCerts() map[string]string { 469 | return index.Data.Body.Certs 470 | } 471 | 472 | func (index *OrgIndex) RemoveCert(name string) error { 473 | _, ok := index.Data.Body.Certs[name] 474 | if !ok { 475 | return fmt.Errorf("Cert %s does not exist", name) 476 | } 477 | delete(index.Data.Body.Certs, name) 478 | return nil 479 | } 480 | 481 | func (index *OrgIndex) ClearCertTags(cert string) error { 482 | _, ok := index.Data.Body.Tags.CertReverse[cert] 483 | if ok { 484 | delete(index.Data.Body.Tags.CertReverse, cert) 485 | } 486 | for tag, _ := range index.Data.Body.Tags.CertForward { 487 | for i, taggedCert := range index.Data.Body.Tags.CertForward[tag] { 488 | if taggedCert == cert { 489 | index.Data.Body.Tags.CertForward[tag] = append(index.Data.Body.Tags.CertForward[tag][:i], index.Data.Body.Tags.CertForward[tag][i+1:]...) 490 | } 491 | } 492 | } 493 | return nil 494 | } 495 | 496 | func (index *OrgIndex) AddCSRTags(csr string, i interface{}) error { 497 | var inTags []string 498 | switch t := i.(type) { 499 | case string: 500 | inTags = []string{i.(string)} 501 | case []string: 502 | inTags = i.([]string) 503 | default: 504 | return fmt.Errorf("Could not add CSR tags. Wrong data type for tags: %T", t) 505 | } 506 | 507 | for _, tag := range inTags { 508 | index.Data.Body.Tags.CSRForward[tag] = AppendUnique(index.Data.Body.Tags.CSRForward[tag], csr) 509 | index.Data.Body.Tags.CSRReverse[csr] = AppendUnique(index.Data.Body.Tags.CSRReverse[csr], tag) 510 | } 511 | 512 | return nil 513 | } 514 | 515 | func (index *OrgIndex) AddCSR(name, id string) error { 516 | _, ok := index.Data.Body.CSRs[name] 517 | if ok { 518 | return fmt.Errorf("key %s already exists", name) 519 | } 520 | index.Data.Body.CSRs[name] = id 521 | return nil 522 | } 523 | 524 | func (index *OrgIndex) GetCSR(name string) (string, error) { 525 | _, ok := index.Data.Body.CSRs[name] 526 | if !ok { 527 | return "", fmt.Errorf("key %s does not exist", name) 528 | } 529 | return index.Data.Body.CSRs[name], nil 530 | } 531 | 532 | func (index *OrgIndex) GetCSRs() map[string]string { 533 | return index.Data.Body.CSRs 534 | } 535 | 536 | func (index *OrgIndex) RemoveCSR(name string) error { 537 | _, ok := index.Data.Body.CSRs[name] 538 | if !ok { 539 | return fmt.Errorf("Cert %s does not exist", name) 540 | } 541 | delete(index.Data.Body.CSRs, name) 542 | return nil 543 | } 544 | 545 | func (index *OrgIndex) ClearCSRTags(csr string) error { 546 | _, ok := index.Data.Body.Tags.CSRReverse[csr] 547 | if ok { 548 | delete(index.Data.Body.Tags.CSRReverse, csr) 549 | } 550 | for tag, _ := range index.Data.Body.Tags.CSRForward { 551 | for i, taggedCSR := range index.Data.Body.Tags.CSRForward[tag] { 552 | if taggedCSR == csr { 553 | index.Data.Body.Tags.CSRForward[tag] = append(index.Data.Body.Tags.CSRForward[tag][:i], index.Data.Body.Tags.CSRForward[tag][i+1:]...) 554 | } 555 | } 556 | } 557 | return nil 558 | } 559 | -------------------------------------------------------------------------------- /index/org_test.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestOrgIndexNew(t *testing.T) { 9 | index, err := NewOrg(nil) 10 | assert.Nil(t, err) 11 | assert.NotNil(t, index) 12 | assert.Equal(t, index.Data.Scope, "pki.io") 13 | } 14 | 15 | func TestOrgIndexDump(t *testing.T) { 16 | index, _ := NewOrg(nil) 17 | indexJson := index.Dump() 18 | assert.NotEqual(t, len(indexJson), 0) 19 | } 20 | 21 | func TestOrgIndexAddCATagsString(t *testing.T) { 22 | index, _ := NewOrg(nil) 23 | err := index.AddCATags("ca1", "tag1") 24 | assert.Nil(t, err) 25 | } 26 | 27 | func TestOrgIndexAddCATagsSlice(t *testing.T) { 28 | index, _ := NewOrg(nil) 29 | inIndex := []string{"tag1", "tag2"} 30 | err := index.AddCATags("ca2", inIndex) 31 | assert.Nil(t, err) 32 | } 33 | 34 | func TestOrgIndexAddEntityTagsString(t *testing.T) { 35 | index, _ := NewOrg(nil) 36 | err := index.AddEntityTags("entity1", "tag1") 37 | assert.Nil(t, err) 38 | } 39 | 40 | func TestOrgIndexAddEntityTagsSlice(t *testing.T) { 41 | index, _ := NewOrg(nil) 42 | inIndex := []string{"tag1", "tag2"} 43 | err := index.AddEntityTags("entity2", inIndex) 44 | assert.Nil(t, err) 45 | } 46 | 47 | func TestOrgIndexAddTags(t *testing.T) { 48 | index, _ := NewOrg(nil) 49 | inTags1 := []string{"tag1", "tag2"} 50 | inTags2 := []string{"tag2", "tag3"} 51 | err := index.AddEntityTags("entity1", inTags1) 52 | assert.Nil(t, err) 53 | err = index.AddEntityTags("entity2", inTags2) 54 | assert.Nil(t, err) 55 | err = index.AddCATags("ca1", inTags2) 56 | assert.Nil(t, err) 57 | err = index.AddCATags("ca2", inTags1) 58 | assert.Nil(t, err) 59 | } 60 | 61 | func TestOrgIndexAddPairingKey(t *testing.T) { 62 | index, _ := NewOrg(nil) 63 | id := "123" 64 | key := "abc" 65 | tags := []string{"tag1", "tag2"} 66 | err := index.AddPairingKey(id, key, tags) 67 | assert.Nil(t, err) 68 | } 69 | -------------------------------------------------------------------------------- /node/node.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/node as node 2 | package node 3 | 4 | import ( 5 | "fmt" 6 | "github.com/pki-io/core/entity" 7 | ) 8 | 9 | type Node struct { 10 | entity.Entity 11 | } 12 | 13 | // ThreatSpec TMv0.1 for New 14 | // Creates new node for App:Node 15 | 16 | func New(jsonString interface{}) (*Node, error) { 17 | node := new(Node) 18 | if err := node.New(jsonString); err != nil { 19 | return nil, fmt.Errorf("Couldn't create node: %s", err) 20 | } else { 21 | return node, nil 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /node/node_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | //"fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestNodeNew(t *testing.T) { 10 | node, err := New(nil) 11 | assert.Nil(t, err) 12 | assert.NotNil(t, node) 13 | assert.Equal(t, node.Data.Type, "entity-document") 14 | } 15 | -------------------------------------------------------------------------------- /ssh/ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "time" 11 | ) 12 | 13 | const ( 14 | defaultTimeout time.Duration = time.Minute 15 | muxFormat string = "%r@%h:%p" 16 | aliveTries int = 3 17 | aliveInterval time.Duration = 3 * time.Second 18 | ) 19 | 20 | type SSH struct { 21 | host string 22 | args []string 23 | tmp string 24 | mux string 25 | timeout time.Duration 26 | controlDone chan error 27 | } 28 | 29 | func New() *SSH { 30 | s := new(SSH) 31 | s.timeout = defaultTimeout 32 | return s 33 | } 34 | 35 | func Connect(host string, args []string) (*SSH, error) { 36 | s := New() 37 | 38 | if err := s.Connect(host, args); err != nil { 39 | return nil, err 40 | } 41 | 42 | return s, nil 43 | } 44 | 45 | func (s *SSH) Connect(host string, args []string) error { 46 | var err error 47 | 48 | s.host = host 49 | s.args = args 50 | s.controlDone = make(chan error, 1) 51 | 52 | s.tmp, err = ioutil.TempDir("", "pki.io") 53 | if err != nil { 54 | return err 55 | } 56 | 57 | s.mux = path.Join(s.tmp, muxFormat) 58 | 59 | params := make([]string, 0) 60 | params = append(params, []string{s.host, "-M", "-S", s.mux, "-o", "ControlPersist=yes"}...) 61 | params = append(params, s.args...) 62 | 63 | controlCmd := exec.Command("ssh", params...) 64 | controlCmd.Stdout = os.Stdout 65 | controlCmd.Stderr = os.Stderr 66 | 67 | err = controlCmd.Start() 68 | if err != nil { 69 | return err 70 | } 71 | 72 | go func() { 73 | s.controlDone <- controlCmd.Wait() 74 | }() 75 | 76 | timeout := time.After(s.timeout) 77 | select { 78 | case <-timeout: 79 | if err := controlCmd.Process.Signal(os.Interrupt); err != nil { 80 | return err 81 | } 82 | <-s.controlDone 83 | return fmt.Errorf("control process timeout. Killed.") 84 | case err := <-s.controlDone: 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | 90 | alive := false 91 | for i := 0; i < aliveTries; i++ { 92 | if s.ControlAlive() { 93 | alive = true 94 | break 95 | } 96 | time.Sleep(aliveInterval) 97 | } 98 | 99 | if !alive { 100 | fmt.Errorf("Could not check mux") 101 | } 102 | 103 | return nil 104 | } 105 | 106 | func (s *SSH) ExecuteCmd(cmd *exec.Cmd, stdin io.Reader, stdout, stderr io.Writer) error { 107 | var err error 108 | cmdDone := make(chan error, 1) 109 | 110 | cmd.Stdin = stdin 111 | cmd.Stdout = stdout 112 | cmd.Stderr = stderr 113 | 114 | err = cmd.Start() 115 | if err != nil { 116 | return err 117 | } 118 | 119 | go func() { 120 | cmdDone <- cmd.Wait() 121 | }() 122 | 123 | timeout := time.After(s.timeout) 124 | select { 125 | case <-timeout: 126 | if err := cmd.Process.Signal(os.Interrupt); err != nil { 127 | return err 128 | } 129 | <-cmdDone 130 | return fmt.Errorf("execution process timeout. Killed.") 131 | case err := <-cmdDone: 132 | if err != nil { 133 | return err 134 | } 135 | } 136 | 137 | return nil 138 | } 139 | 140 | func (s *SSH) Execute(cmd string, stdin io.Reader, stdout, stderr io.Writer) error { 141 | return s.ExecuteCmd(exec.Command("ssh", s.host, "-S", s.mux, cmd), stdin, stdout, stderr) 142 | } 143 | 144 | func (s *SSH) ControlAlive() bool { 145 | if err := s.ExecuteCmd(exec.Command("ssh", s.host, "-S", s.mux, "-O", "check"), nil, nil, nil); err != nil { 146 | return false 147 | } 148 | 149 | return true 150 | } 151 | 152 | func (s *SSH) PutFiles(dest string, sources ...string) error { 153 | var err error 154 | cmdDone := make(chan error, 1) 155 | 156 | params := make([]string, 0) 157 | params = append(params, []string{"-o", fmt.Sprintf("ControlPath=%s", s.mux)}...) 158 | params = append(params, sources...) 159 | params = append(params, fmt.Sprintf("%s:%s", s.host, dest)) 160 | 161 | cmd := exec.Command("scp", params...) 162 | 163 | err = cmd.Start() 164 | if err != nil { 165 | return err 166 | } 167 | 168 | go func() { 169 | cmdDone <- cmd.Wait() 170 | }() 171 | 172 | timeout := time.After(s.timeout) 173 | select { 174 | case <-timeout: 175 | if err := cmd.Process.Signal(os.Interrupt); err != nil { 176 | return err 177 | } 178 | <-cmdDone 179 | return fmt.Errorf("execution process timeout. Killed.") 180 | case err := <-cmdDone: 181 | if err != nil { 182 | return err 183 | } 184 | } 185 | 186 | return nil 187 | } 188 | 189 | func (s *SSH) GetFile(source, dest string) error { 190 | fmt.Println("getting file") 191 | return nil 192 | } 193 | 194 | func (s *SSH) Close() error { 195 | s.ExecuteCmd(exec.Command("ssh", s.host, "-S", s.mux, "-O", "exit"), nil, nil, nil) 196 | err := os.RemoveAll(s.tmp) 197 | s.tmp = "" 198 | s.mux = "" 199 | return err 200 | } 201 | -------------------------------------------------------------------------------- /x509/ca.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/x509 as x509 2 | package x509 3 | 4 | import ( 5 | "crypto/rand" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "fmt" 9 | "github.com/pki-io/core/crypto" 10 | "github.com/pki-io/core/document" 11 | "time" 12 | ) 13 | 14 | const CADefault string = `{ 15 | "scope": "pki.io", 16 | "version": 1, 17 | "type": "ca-document", 18 | "options": "", 19 | "body": { 20 | "id": "", 21 | "name": "", 22 | "certificate": "", 23 | "ca-expiry": 1, 24 | "cert-expiry": 1, 25 | "key-type": "ec", 26 | "private-key": "", 27 | "dn-scope": { 28 | "country": "", 29 | "organization": "", 30 | "organizational-unit": "", 31 | "locality": "", 32 | "province": "", 33 | "street-address": "", 34 | "postal-code": "" 35 | } 36 | } 37 | }` 38 | 39 | const CASchema string = `{ 40 | "$schema": "http://json-schema.org/draft-04/schema#", 41 | "title": "CADocument", 42 | "description": "CA Document", 43 | "type": "object", 44 | "required": ["scope","version","type","options","body"], 45 | "additionalProperties": false, 46 | "properties": { 47 | "scope": { 48 | "description": "Scope of the document", 49 | "type": "string" 50 | }, 51 | "version": { 52 | "description": "Document schema version", 53 | "type": "integer" 54 | }, 55 | "type": { 56 | "description": "Type of document", 57 | "type": "string" 58 | }, 59 | "options": { 60 | "description": "Options data", 61 | "type": "string" 62 | }, 63 | "body": { 64 | "description": "Body data", 65 | "type": "object", 66 | "required": ["id", "name", "ca-expiry", "cert-expiry", "key-type", "certificate", "private-key", "dn-scope"], 67 | "additionalProperties": false, 68 | "properties": { 69 | "id" : { 70 | "description": "Entity ID", 71 | "type": "string" 72 | }, 73 | "name" : { 74 | "description": "Entity name", 75 | "type": "string" 76 | }, 77 | "ca-expiry" : { 78 | "description": "CA expiry period in days", 79 | "type": "integer" 80 | }, 81 | "cert-expiry" : { 82 | "description": "Cert expiry period in days", 83 | "type": "integer" 84 | }, 85 | "certificate" : { 86 | "description": "PEM encoded X.509 certificate", 87 | "type": "string" 88 | }, 89 | "key-type": { 90 | "description": "the type of keys to use. Must be either rsa or ec.", 91 | "type": "string" 92 | }, 93 | "private-key" : { 94 | "description": "PEM encoded private key", 95 | "type": "string" 96 | }, 97 | "dn-scope": { 98 | "description": "Scope the DN for all child certs", 99 | "type": "object", 100 | "required": [ 101 | "country", 102 | "organization", 103 | "organizational-unit", 104 | "locality", 105 | "province", 106 | "street-address", 107 | "postal-code" 108 | ], 109 | "additionalProperties": false, 110 | "properties": { 111 | "country": { 112 | "description": "X.509 distinguished name country field", 113 | "type": "string" 114 | }, 115 | "organization": { 116 | "description": "X.509 distinguished name organization field", 117 | "type": "string" 118 | }, 119 | "organizational-unit": { 120 | "description": "X.509 distinguished name organizational-unit field", 121 | "type": "string" 122 | }, 123 | "locality": { 124 | "description": "X.509 distinguished name locality field", 125 | "type": "string" 126 | }, 127 | "province": { 128 | "description": "X.509 distinguished name province field", 129 | "type": "string" 130 | }, 131 | "street-address": { 132 | "description": "X.509 distinguished name street-address field", 133 | "type": "string" 134 | }, 135 | "postal-code": { 136 | "description": "X.509 distinguished name postal-code field", 137 | "type": "string" 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | }` 145 | 146 | type CAData struct { 147 | Scope string `json:"scope"` 148 | Version int `json:"version"` 149 | Type string `json:"type"` 150 | Options string `json:"options"` 151 | Body struct { 152 | Id string `json:"id"` 153 | Name string `json:"name"` 154 | CAExpiry int `json:"ca-expiry"` 155 | CertExpiry int `json:"cert-expiry"` 156 | Certificate string `json:"certificate"` 157 | PrivateKey string `json:"private-key"` 158 | KeyType string `json:"key-type"` 159 | DNScope struct { 160 | Country string `json:"country"` 161 | Organization string `json:"organization"` 162 | OrganizationalUnit string `json:"organizational-unit"` 163 | Locality string `json:"locality"` 164 | Province string `json:"province"` 165 | StreetAddress string `json:"street-address"` 166 | PostalCode string `json:"postal-code"` 167 | } `json:"dn-scope"` 168 | } `json:"body"` 169 | } 170 | 171 | type CA struct { 172 | document.Document 173 | Data CAData 174 | } 175 | 176 | // ThreatSpec TMv0.1 for NewCA 177 | // Does new CA creation for App:X509 178 | 179 | func NewCA(jsonString interface{}) (*CA, error) { 180 | ca := new(CA) 181 | ca.Schema = CASchema 182 | ca.Default = CADefault 183 | if err := ca.Load(jsonString); err != nil { 184 | return nil, fmt.Errorf("Could not create new CA: %s", err) 185 | } else { 186 | return ca, nil 187 | } 188 | } 189 | 190 | // ThreatSpec TMv0.1 for CA.Load 191 | // Does CA JSON loading for App:X509 192 | 193 | func (ca *CA) Load(jsonString interface{}) error { 194 | data := new(CAData) 195 | if data, err := ca.FromJson(jsonString, data); err != nil { 196 | return fmt.Errorf("Could not load CA JSON: %s", err) 197 | } else { 198 | ca.Data = *data.(*CAData) 199 | return nil 200 | } 201 | } 202 | 203 | func (ca *CA) Id() string { 204 | return ca.Data.Body.Id 205 | } 206 | 207 | func (ca *CA) Name() string { 208 | return ca.Data.Body.Name 209 | } 210 | 211 | // ThreatSpec TMv0.1 for CA.Dump 212 | // Does CA JSON dumping for App:X509 213 | 214 | func (ca *CA) Dump() string { 215 | if jsonString, err := ca.ToJson(ca.Data); err != nil { 216 | return "" 217 | } else { 218 | return jsonString 219 | } 220 | } 221 | 222 | // ThreatSpec TMv0.1 for CA.GenerateRoot 223 | // Does Root CA certificate generation for App:X509 224 | 225 | func (ca *CA) GenerateRoot() error { 226 | return ca.GenerateSub(nil) 227 | } 228 | 229 | // ThreatSpec TMv0.1 for CA.GenerateSub 230 | // Does Sub-CA certificate generation for App:X509 231 | func (ca *CA) GenerateSub(parentCA interface{}) error { 232 | //https://www.socketloop.com/tutorials/golang-create-x509-certificate-private-and-public-keys 233 | 234 | // Override from parent if necessary 235 | // Ugly as hell. Need to fix. 236 | switch parentCA.(type) { 237 | case *CA: 238 | p := parentCA.(*CA) 239 | if p.Data.Body.DNScope.Country != "" { 240 | ca.Data.Body.DNScope.Country = p.Data.Body.DNScope.Country 241 | } 242 | if p.Data.Body.DNScope.Organization != "" { 243 | ca.Data.Body.DNScope.Organization = p.Data.Body.DNScope.Organization 244 | } 245 | if p.Data.Body.DNScope.OrganizationalUnit != "" { 246 | ca.Data.Body.DNScope.OrganizationalUnit = p.Data.Body.DNScope.OrganizationalUnit 247 | } 248 | if p.Data.Body.DNScope.Locality != "" { 249 | ca.Data.Body.DNScope.Locality = p.Data.Body.DNScope.Locality 250 | } 251 | if p.Data.Body.DNScope.Province != "" { 252 | ca.Data.Body.DNScope.Province = p.Data.Body.DNScope.Province 253 | } 254 | if p.Data.Body.DNScope.StreetAddress != "" { 255 | ca.Data.Body.DNScope.StreetAddress = p.Data.Body.DNScope.StreetAddress 256 | } 257 | if p.Data.Body.DNScope.PostalCode != "" { 258 | ca.Data.Body.DNScope.PostalCode = p.Data.Body.DNScope.PostalCode 259 | } 260 | } 261 | 262 | subject := new(pkix.Name) 263 | subject.CommonName = ca.Data.Body.Name 264 | 265 | // Set using CA's DNScope 266 | if ca.Data.Body.DNScope.Country != "" { 267 | subject.Country = []string{ca.Data.Body.DNScope.Country} 268 | } 269 | if ca.Data.Body.DNScope.Organization != "" { 270 | subject.Organization = []string{ca.Data.Body.DNScope.Organization} 271 | } 272 | if ca.Data.Body.DNScope.OrganizationalUnit != "" { 273 | subject.OrganizationalUnit = []string{ca.Data.Body.DNScope.OrganizationalUnit} 274 | } 275 | if ca.Data.Body.DNScope.Locality != "" { 276 | subject.Locality = []string{ca.Data.Body.DNScope.Locality} 277 | } 278 | if ca.Data.Body.DNScope.Province != "" { 279 | subject.Province = []string{ca.Data.Body.DNScope.Province} 280 | } 281 | if ca.Data.Body.DNScope.StreetAddress != "" { 282 | subject.StreetAddress = []string{ca.Data.Body.DNScope.StreetAddress} 283 | } 284 | if ca.Data.Body.DNScope.PostalCode != "" { 285 | subject.PostalCode = []string{ca.Data.Body.DNScope.PostalCode} 286 | } 287 | 288 | serial, err := NewSerial() 289 | if err != nil { 290 | return fmt.Errorf("Could not create serial: %s", err) 291 | } 292 | 293 | notBefore := time.Now() 294 | notAfter := notBefore.AddDate(0, 0, ca.Data.Body.CAExpiry) 295 | 296 | template := &x509.Certificate{ 297 | IsCA: true, 298 | BasicConstraintsValid: true, 299 | //SubjectKeyId: []byte{1, 2, 3}, 300 | SerialNumber: serial, 301 | Subject: *subject, 302 | NotBefore: notBefore, 303 | NotAfter: notAfter, 304 | // see http://golang.org/pkg/crypto/x509/#KeyUsage 305 | // http://security.stackexchange.com/questions/49229/root-certificate-key-usage-non-self-signed-end-entity 306 | //ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 307 | KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 308 | } 309 | var privateKey interface{} 310 | var publicKey interface{} 311 | keyType := crypto.KeyType(ca.Data.Body.KeyType) 312 | 313 | switch keyType { 314 | case crypto.KeyTypeRSA: 315 | rsaKey, err := crypto.GenerateRSAKey() 316 | if err != nil { 317 | return fmt.Errorf("Failed to generate RSA Key: %s", err) 318 | } 319 | privateKey = rsaKey 320 | publicKey = &rsaKey.PublicKey 321 | case crypto.KeyTypeEC: 322 | ecKey, err := crypto.GenerateECKey() 323 | if err != nil { 324 | return fmt.Errorf("") 325 | } 326 | privateKey = ecKey 327 | publicKey = &ecKey.PublicKey 328 | default: 329 | return fmt.Errorf("Invalid key type: %s", keyType) 330 | } 331 | 332 | var parent *x509.Certificate 333 | var signingKey interface{} 334 | 335 | switch t := parentCA.(type) { 336 | case *CA: 337 | parent, err = parentCA.(*CA).Certificate() 338 | if err != nil { 339 | return fmt.Errorf("Could not get certificate: %s", err) 340 | } 341 | signingKey, err = parentCA.(*CA).PrivateKey() 342 | if err != nil { 343 | return fmt.Errorf("Could not get private key: %s", err) 344 | } 345 | case nil: 346 | // Self signed 347 | parent = template 348 | signingKey = privateKey 349 | default: 350 | return fmt.Errorf("Invalid parent type: %T", t) 351 | } 352 | 353 | der, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, signingKey) 354 | if err != nil { 355 | return fmt.Errorf("Could not create certificate: %s", err) 356 | } 357 | ca.Data.Body.Id = NewID() 358 | ca.Data.Body.Certificate = string(PemEncodeX509CertificateDER(der)) 359 | enc, err := crypto.PemEncodePrivate(privateKey) 360 | if err != nil { 361 | return fmt.Errorf("Could not pem encode private key: %s", err) 362 | } 363 | ca.Data.Body.PrivateKey = string(enc) 364 | 365 | return nil 366 | } 367 | 368 | // ThreatSpec TMv0.1 for CA.Certificate 369 | // Returns CA certificate for App:X509 370 | 371 | func (ca *CA) Certificate() (*x509.Certificate, error) { 372 | return PemDecodeX509Certificate([]byte(ca.Data.Body.Certificate)) 373 | } 374 | 375 | // ThreatSpec TMv0.1 for CA.PrivateKey 376 | // Returns CA private key for App:X509 377 | 378 | func (ca *CA) PrivateKey() (interface{}, error) { 379 | if privateKey, err := crypto.PemDecodePrivate([]byte(ca.Data.Body.PrivateKey)); err != nil { 380 | return nil, fmt.Errorf("Could not decode rsa private key: %s", err) 381 | } else { 382 | return privateKey, nil 383 | } 384 | } 385 | 386 | // ThreatSpec TMv0.1 for CA.Sign 387 | // Does CSR signing by CA for App:X509 388 | func (ca *CA) Sign(csr *CSR, useCSRSubject bool) (*Certificate, error) { 389 | 390 | subject := new(pkix.Name) 391 | 392 | if useCSRSubject { 393 | decodedCSR, err := PemDecodeX509CSR([]byte(csr.Data.Body.CSR)) 394 | if err != nil { 395 | return nil, err 396 | } 397 | subject = &decodedCSR.Subject 398 | } else { 399 | subject.CommonName = csr.Data.Body.Name 400 | 401 | if ca.Data.Body.DNScope.Country != "" { 402 | subject.Country = []string{ca.Data.Body.DNScope.Country} 403 | } 404 | if ca.Data.Body.DNScope.Organization != "" { 405 | subject.Organization = []string{ca.Data.Body.DNScope.Organization} 406 | } 407 | if ca.Data.Body.DNScope.OrganizationalUnit != "" { 408 | subject.OrganizationalUnit = []string{ca.Data.Body.DNScope.OrganizationalUnit} 409 | } 410 | if ca.Data.Body.DNScope.Locality != "" { 411 | subject.Locality = []string{ca.Data.Body.DNScope.Locality} 412 | } 413 | if ca.Data.Body.DNScope.Province != "" { 414 | subject.Province = []string{ca.Data.Body.DNScope.Province} 415 | } 416 | if ca.Data.Body.DNScope.StreetAddress != "" { 417 | subject.StreetAddress = []string{ca.Data.Body.DNScope.StreetAddress} 418 | } 419 | if ca.Data.Body.DNScope.PostalCode != "" { 420 | subject.PostalCode = []string{ca.Data.Body.DNScope.PostalCode} 421 | } 422 | } 423 | 424 | serial, err := NewSerial() 425 | if err != nil { 426 | return nil, fmt.Errorf("Could not create serial: %s", err) 427 | } 428 | 429 | notBefore := time.Now() 430 | notAfter := notBefore.AddDate(0, 0, ca.Data.Body.CertExpiry) 431 | 432 | template := &x509.Certificate{ 433 | IsCA: false, 434 | BasicConstraintsValid: true, 435 | //SubjectKeyId: []byte{1, 2, 3}, 436 | SerialNumber: serial, 437 | Subject: *subject, 438 | NotBefore: notBefore, 439 | NotAfter: notAfter, 440 | // see http://golang.org/pkg/crypto/x509/#KeyUsage 441 | // KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 442 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, 443 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 444 | } 445 | parent, _ := ca.Certificate() 446 | csrPublicKey, err := csr.PublicKey() 447 | if err != nil { 448 | return nil, fmt.Errorf("Could not get public key from CSR: %s", err) 449 | } 450 | signingKey, _ := ca.PrivateKey() 451 | 452 | der, err := x509.CreateCertificate(rand.Reader, template, parent, csrPublicKey, signingKey) 453 | if err != nil { 454 | return nil, fmt.Errorf("Could not create certificate der: %s", err) 455 | } 456 | 457 | cert, err := NewCertificate(nil) 458 | if err != nil { 459 | return nil, fmt.Errorf("Could not create certificate: %s", err) 460 | } 461 | cert.Data.Body.Id = csr.Data.Body.Id 462 | cert.Data.Body.Name = csr.Data.Body.Name 463 | cert.Data.Body.Certificate = string(PemEncodeX509CertificateDER(der)) 464 | cert.Data.Body.KeyType = ca.Data.Body.KeyType 465 | cert.Data.Body.CACertificate = ca.Data.Body.Certificate 466 | return cert, nil 467 | } 468 | -------------------------------------------------------------------------------- /x509/ca_test.go: -------------------------------------------------------------------------------- 1 | package x509 2 | 3 | import ( 4 | //"fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestX509NewCA(t *testing.T) { 11 | ca, err := NewCA(nil) 12 | assert.Nil(t, err) 13 | assert.NotNil(t, ca) 14 | assert.Equal(t, ca.Data.Scope, "pki.io") 15 | } 16 | 17 | func TestX509CADump(t *testing.T) { 18 | ca, _ := NewCA(nil) 19 | caJson := ca.Dump() 20 | assert.NotEqual(t, len(caJson), 0) 21 | } 22 | 23 | func TestX509CAGenerateRoot(t *testing.T) { 24 | ca, _ := NewCA(nil) 25 | err := ca.GenerateRoot() 26 | assert.Nil(t, err) 27 | assert.NotEqual(t, ca.Data.Body.Certificate, "") 28 | assert.NotEqual(t, ca.Data.Body.Id, "") 29 | 30 | cert, err := PemDecodeX509Certificate([]byte(ca.Data.Body.Certificate)) 31 | assert.Nil(t, err) 32 | assert.True(t, cert.NotBefore.After(time.Now().AddDate(0, 0, -1))) 33 | assert.True(t, cert.NotAfter.Before(time.Now().AddDate(0, 0, 1))) 34 | } 35 | 36 | func TestX509CAGenerateSub(t *testing.T) { 37 | rootCA, _ := NewCA(nil) 38 | rootCA.Data.Body.Name = "RootCA" 39 | rootCA.Data.Body.DNScope.Country = "UK" 40 | rootCA.Data.Body.DNScope.Organization = "pki.io" 41 | rootCA.GenerateRoot() 42 | 43 | subCA, _ := NewCA(nil) 44 | subCA.Data.Body.Name = "DevCA" 45 | subCA.Data.Body.DNScope.OrganizationalUnit = "Development" 46 | err := subCA.GenerateSub(rootCA) 47 | 48 | assert.Nil(t, err) 49 | assert.NotEqual(t, subCA.Data.Body.Certificate, "") 50 | 51 | cert, err := PemDecodeX509Certificate([]byte(subCA.Data.Body.Certificate)) 52 | assert.Nil(t, err) 53 | assert.True(t, cert.NotBefore.After(time.Now().AddDate(0, 0, -1))) 54 | assert.True(t, cert.NotAfter.Before(time.Now().AddDate(0, 0, 1))) 55 | } 56 | -------------------------------------------------------------------------------- /x509/certificate.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/x509 as x509 2 | package x509 3 | 4 | import ( 5 | "crypto/rand" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "fmt" 9 | "github.com/pki-io/core/crypto" 10 | "github.com/pki-io/core/document" 11 | "time" 12 | ) 13 | 14 | const CertificateDefault string = `{ 15 | "scope": "pki.io", 16 | "version": 1, 17 | "type": "certificate-document", 18 | "options": "", 19 | "body": { 20 | "id": "", 21 | "name": "", 22 | "key-type": "ec", 23 | "expiry": 0, 24 | "tags": [], 25 | "certificate": "", 26 | "private-key": "", 27 | "ca-certificate": "" 28 | } 29 | }` 30 | 31 | const CertificateSchema string = `{ 32 | "$schema": "http://json-schema.org/draft-04/schema#", 33 | "title": "CertificateDocument", 34 | "description": "Certificate Document", 35 | "type": "object", 36 | "required": ["scope","version","type","options","body"], 37 | "additionalProperties": false, 38 | "properties": { 39 | "scope": { 40 | "description": "Scope of the document", 41 | "type": "string" 42 | }, 43 | "version": { 44 | "description": "Document schema version", 45 | "type": "integer" 46 | }, 47 | "type": { 48 | "description": "Type of document", 49 | "type": "string" 50 | }, 51 | "options": { 52 | "description": "Options data", 53 | "type": "string" 54 | }, 55 | "body": { 56 | "description": "Body data", 57 | "type": "object", 58 | "required": ["id", "name", "key-type", "tags", "certificate", "private-key", "ca-certificate"], 59 | "additionalProperties": false, 60 | "properties": { 61 | "id" : { 62 | "description": "Entity ID", 63 | "type": "string" 64 | }, 65 | "name" : { 66 | "description": "Entity name", 67 | "type": "string" 68 | }, 69 | "expiry": { 70 | "description": "Expiry period in days", 71 | "type": "integer" 72 | }, 73 | "key-type": { 74 | "description": "Key type. Must be either rsa or ec", 75 | "type": "string" 76 | }, 77 | "tags": { 78 | "description": "Tags defined for cert", 79 | "type": "array" 80 | }, 81 | "certificate" : { 82 | "description": "PEM encoded X.509 certificate", 83 | "type": "string" 84 | }, 85 | "private-key" : { 86 | "description": "PEM encoded private key", 87 | "type": "string" 88 | }, 89 | "ca-certificate" : { 90 | "description": "PEM encoded CA certificate", 91 | "type": "string" 92 | } 93 | } 94 | } 95 | } 96 | }` 97 | 98 | type CertificateData struct { 99 | Scope string `json:"scope"` 100 | Version int `json:"version"` 101 | Type string `json:"type"` 102 | Options string `json:"options"` 103 | Body struct { 104 | Id string `json:"id"` 105 | Name string `json:"name"` 106 | Expiry int `json:"expiry"` 107 | KeyType string `json:"key-type"` 108 | Tags []string `json:"tags"` 109 | Certificate string `json:"certificate"` 110 | PrivateKey string `json:"private-key"` 111 | CACertificate string `json:"ca-certificate"` 112 | } `json:"body"` 113 | } 114 | 115 | type Certificate struct { 116 | document.Document 117 | Data CertificateData 118 | } 119 | 120 | // ThreatSpec TMv0.1 for NewCertificate 121 | // Creates new certificate for App:X509 122 | 123 | func NewCertificate(jsonString interface{}) (*Certificate, error) { 124 | certificate := new(Certificate) 125 | certificate.Schema = CertificateSchema 126 | certificate.Default = CertificateDefault 127 | if err := certificate.Load(jsonString); err != nil { 128 | return nil, fmt.Errorf("Could not create new Certificate: %s", err) 129 | } else { 130 | return certificate, nil 131 | } 132 | } 133 | 134 | // ThreatSpec TMv0.1 for Certificate.Load 135 | // Does certificate JSON loading for App:X509 136 | 137 | func (certificate *Certificate) Load(jsonString interface{}) error { 138 | data := new(CertificateData) 139 | if data, err := certificate.FromJson(jsonString, data); err != nil { 140 | return fmt.Errorf("Could not load Certificate JSON: %s", err) 141 | } else { 142 | certificate.Data = *data.(*CertificateData) 143 | return nil 144 | } 145 | } 146 | 147 | // ThreatSpec TMv0.1 for Certificate.Dump 148 | // Does certificate JSON dumping for App:X509 149 | func (certificate *Certificate) Dump() string { 150 | if jsonString, err := certificate.ToJson(certificate.Data); err != nil { 151 | return "" 152 | } else { 153 | return jsonString 154 | } 155 | } 156 | 157 | func (certificate *Certificate) Name() string { 158 | return certificate.Data.Body.Name 159 | } 160 | 161 | func (certificate *Certificate) Id() string { 162 | return certificate.Data.Body.Id 163 | } 164 | 165 | // ThreatSpec TMv0.1 for Certificate.Generate 166 | // Does certificate generation for App:X509 167 | 168 | func (certificate *Certificate) Generate(parentCertificate interface{}, subject *pkix.Name) error { 169 | //https://www.socketloop.com/tutorials/golang-create-x509-certificate-private-and-public-keys 170 | 171 | serial, err := NewSerial() 172 | if err != nil { 173 | return fmt.Errorf("Could not create serial: %s", err) 174 | } 175 | 176 | notBefore := time.Now() 177 | notAfter := notBefore.AddDate(0, 0, certificate.Data.Body.Expiry) 178 | 179 | template := &x509.Certificate{ 180 | IsCA: false, 181 | BasicConstraintsValid: true, 182 | SerialNumber: serial, 183 | Subject: *subject, 184 | NotBefore: notBefore, 185 | NotAfter: notAfter, 186 | // see http://golang.org/pkg/crypto/x509/#KeyUsage 187 | // http://security.stackexchange.com/questions/24106/which-key-usages-are-required-by-each-key-exchange-method 188 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, 189 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 190 | } 191 | 192 | var privateKey interface{} 193 | var publicKey interface{} 194 | 195 | switch crypto.KeyType(certificate.Data.Body.KeyType) { 196 | case crypto.KeyTypeRSA: 197 | rsaKey, err := crypto.GenerateRSAKey() 198 | if err != nil { 199 | return fmt.Errorf("Could not generate RSA key: %s", err) 200 | } 201 | privateKey = rsaKey 202 | publicKey = &rsaKey.PublicKey 203 | case crypto.KeyTypeEC: 204 | ecKey, err := crypto.GenerateECKey() 205 | if err != nil { 206 | return fmt.Errorf("Could not generate ec key: %s", err) 207 | } 208 | privateKey = ecKey 209 | publicKey = &ecKey.PublicKey 210 | } 211 | 212 | var parent *x509.Certificate 213 | var signingKey interface{} 214 | 215 | switch t := parentCertificate.(type) { 216 | case *CA: 217 | parent, err = parentCertificate.(*CA).Certificate() 218 | if err != nil { 219 | return fmt.Errorf("Could not get certificate: %s", err) 220 | } 221 | signingKey, err = parentCertificate.(*CA).PrivateKey() 222 | if err != nil { 223 | return fmt.Errorf("Could not get private key: %s", err) 224 | } 225 | // TODO - Should probably track CA by name and load cert if required. 226 | certificate.Data.Body.CACertificate = parentCertificate.(*CA).Data.Body.Certificate 227 | case nil: 228 | // Self signed 229 | parent = template 230 | signingKey = privateKey 231 | default: 232 | return fmt.Errorf("Invalid parent type: %T", t) 233 | } 234 | 235 | der, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, signingKey) 236 | if err != nil { 237 | return fmt.Errorf("Could not create certificate: %s", err) 238 | } 239 | certificate.Data.Body.Certificate = string(PemEncodeX509CertificateDER(der)) 240 | certificate.Data.Body.Id = NewID() 241 | enc, err := crypto.PemEncodePrivate(privateKey) 242 | if err != nil { 243 | return fmt.Errorf("Failed to pem encode private key: %s", err) 244 | } 245 | certificate.Data.Body.PrivateKey = string(enc) 246 | 247 | return nil 248 | } 249 | 250 | // ThreatSpec TMv0.1 for Certificate.Certificate 251 | // Returns certificate for App:X509 252 | 253 | func (certificate *Certificate) Certificate() (*x509.Certificate, error) { 254 | return PemDecodeX509Certificate([]byte(certificate.Data.Body.Certificate)) 255 | } 256 | 257 | // ThreatSpec TMv0.1 for Certificate.PrivateKey 258 | // Returns certificate private key for App:X509 259 | func (certificate *Certificate) PrivateKey() (interface{}, error) { 260 | if privateKey, err := crypto.PemDecodePrivate([]byte(certificate.Data.Body.PrivateKey)); err != nil { 261 | return nil, fmt.Errorf("Could not decode rsa private key: %s", err) 262 | } else { 263 | return privateKey, nil 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /x509/certificate_test.go: -------------------------------------------------------------------------------- 1 | package x509 2 | 3 | import ( 4 | //"fmt" 5 | "crypto/x509/pkix" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestX509NewCertificate(t *testing.T) { 11 | certficiate, err := NewCertificate(nil) 12 | assert.Nil(t, err) 13 | assert.NotNil(t, certficiate) 14 | assert.Equal(t, certficiate.Data.Scope, "pki.io") 15 | } 16 | 17 | func TestX509CertificateDump(t *testing.T) { 18 | certficiate, _ := NewCertificate(nil) 19 | certficiateJson := certficiate.Dump() 20 | assert.NotEqual(t, len(certficiateJson), 0) 21 | } 22 | 23 | func TestX509CertificateGenerateSelfSigned(t *testing.T) { 24 | certificate, _ := NewCertificate(nil) 25 | 26 | subject := &pkix.Name{CommonName: "Test"} 27 | certificate.Data.Body.Expiry = 10 28 | 29 | err := certificate.Generate(nil, subject) 30 | assert.Nil(t, err) 31 | assert.NotEqual(t, certificate.Data.Body.Certificate, "") 32 | } 33 | -------------------------------------------------------------------------------- /x509/csr.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/x509 as x509 2 | package x509 3 | 4 | import ( 5 | "crypto/rand" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "fmt" 9 | "github.com/pki-io/core/crypto" 10 | "github.com/pki-io/core/document" 11 | ) 12 | 13 | const CSRDefault string = `{ 14 | "scope": "pki.io", 15 | "version": 1, 16 | "type": "csr-document", 17 | "options": "", 18 | "body": { 19 | "id": "", 20 | "name": "", 21 | "csr": "", 22 | "key-type": "ec", 23 | "private-key": "" 24 | } 25 | }` 26 | 27 | const CSRSchema string = `{ 28 | "$schema": "http://json-schema.org/draft-04/schema#", 29 | "title": "CSRDocument", 30 | "description": "CSR Document", 31 | "type": "object", 32 | "required": ["scope","version","type","options","body"], 33 | "additionalProperties": false, 34 | "properties": { 35 | "scope": { 36 | "description": "Scope of the document", 37 | "type": "string" 38 | }, 39 | "version": { 40 | "description": "Document schema version", 41 | "type": "integer" 42 | }, 43 | "type": { 44 | "description": "Type of document", 45 | "type": "string" 46 | }, 47 | "options": { 48 | "description": "Options data", 49 | "type": "string" 50 | }, 51 | "body": { 52 | "description": "Body data", 53 | "type": "object", 54 | "required": ["id", "name", "csr", "key-type"], 55 | "additionalProperties": false, 56 | "properties": { 57 | "id" : { 58 | "description": "Entity ID", 59 | "type": "string" 60 | }, 61 | "name" : { 62 | "description": "Entity name", 63 | "type": "string" 64 | }, 65 | "csr" : { 66 | "description": "PEM encoded X.509 csr", 67 | "type": "string" 68 | }, 69 | "key-type": { 70 | "description": "Key type. Must be either RSA or EC", 71 | "type": "string" 72 | }, 73 | "private-key" : { 74 | "description": "PEM encoded private key", 75 | "type": "string" 76 | } 77 | } 78 | } 79 | } 80 | }` 81 | 82 | type CSRData struct { 83 | Scope string `json:"scope"` 84 | Version int `json:"version"` 85 | Type string `json:"type"` 86 | Options string `json:"options"` 87 | Body struct { 88 | Id string `json:"id"` 89 | Name string `json:"name"` 90 | CSR string `json:"csr"` 91 | KeyType string `json:"key-type"` 92 | PrivateKey string `json:"private-key"` 93 | } `json:"body"` 94 | } 95 | 96 | type CSR struct { 97 | document.Document 98 | Data CSRData 99 | } 100 | 101 | // ThreatSpec TMv0.1 for NewCSR 102 | // Creates new CSR for App:X509 103 | 104 | func NewCSR(jsonString interface{}) (*CSR, error) { 105 | csr := new(CSR) 106 | csr.Schema = CSRSchema 107 | csr.Default = CSRDefault 108 | if err := csr.Load(jsonString); err != nil { 109 | return nil, fmt.Errorf("Could not create new CSR: %s", err) 110 | } else { 111 | return csr, nil 112 | } 113 | } 114 | 115 | // ThreatSpec TMv0.1 for CSR.Load 116 | // Does CSR JSON loading for App:X509 117 | 118 | func (csr *CSR) Load(jsonString interface{}) error { 119 | data := new(CSRData) 120 | if data, err := csr.FromJson(jsonString, data); err != nil { 121 | return fmt.Errorf("Could not load CSR JSON: %s", err) 122 | } else { 123 | csr.Data = *data.(*CSRData) 124 | return nil 125 | } 126 | } 127 | 128 | func (csr *CSR) Name() string { 129 | return csr.Data.Body.Name 130 | } 131 | 132 | func (csr *CSR) Id() string { 133 | return csr.Data.Body.Id 134 | } 135 | 136 | // ThreatSpec TMv0.1 for CSR.Dump 137 | // Does CSR JSON dumping for App:X509 138 | 139 | func (csr *CSR) Dump() string { 140 | if jsonString, err := csr.ToJson(csr.Data); err != nil { 141 | return "" 142 | } else { 143 | return jsonString 144 | } 145 | } 146 | 147 | // ThreatSpec TMv0.1 for CSR.Generate 148 | // Does CSR generation for App:Crypto 149 | 150 | func (csr *CSR) Generate(subject *pkix.Name) error { 151 | 152 | var privateKey interface{} 153 | var err error 154 | switch crypto.KeyType(csr.Data.Body.KeyType) { 155 | case crypto.KeyTypeRSA: 156 | privateKey, err = crypto.GenerateRSAKey() 157 | if err != nil { 158 | return fmt.Errorf("Failed to generate rsa key: %s", err) 159 | } 160 | case crypto.KeyTypeEC: 161 | privateKey, err = crypto.GenerateECKey() 162 | if err != nil { 163 | return fmt.Errorf("Failed to generate ec key: %s", err) 164 | } 165 | } 166 | 167 | enc, err := crypto.PemEncodePrivate(privateKey) 168 | if err != nil { 169 | return fmt.Errorf("Failed to pem encode private key: %s", err) 170 | } 171 | 172 | csr.Data.Body.PrivateKey = string(enc) 173 | 174 | template := &x509.CertificateRequest{ 175 | //Raw []byte // Complete ASN.1 DER content (CSR, signature algorithm and signature). 176 | //RawTBSCertificateRequest []byte // Certificate request info part of raw ASN.1 DER content. 177 | //RawSubjectPublicKeyInfo []byte // DER encoded SubjectPublicKeyInfo. 178 | //RawSubject []byte // DER encoded Subject. 179 | 180 | //Version int 181 | //Signature []byte 182 | //SignatureAlgorithm SignatureAlgorithm 183 | 184 | //PublicKeyAlgorithm PublicKeyAlgorithm 185 | //PublicKey interface{} 186 | 187 | //Subject pkix.Name 188 | Subject: *subject, 189 | 190 | // Attributes is a collection of attributes providing 191 | // additional information about the subject of the certificate. 192 | // See RFC 2986 section 4.1. 193 | //Attributes []pkix.AttributeTypeAndValueSET 194 | 195 | // Extensions contains raw X.509 extensions. When parsing CSRs, this 196 | // can be used to extract extensions that are not parsed by this 197 | // package. 198 | //Extensions []pkix.Extension 199 | 200 | // ExtraExtensions contains extensions to be copied, raw, into any 201 | // marshaled CSR. Values override any extensions that would otherwise 202 | // be produced based on the other fields but are overridden by any 203 | // extensions specified in Attributes. 204 | // 205 | // The ExtraExtensions field is not populated when parsing CSRs, see 206 | // Extensions. 207 | //ExtraExtensions []pkix.Extension 208 | 209 | // Subject Alternate Name values. 210 | //DNSNames []string 211 | //EmailAddresses []string 212 | //IPAddresses []net.IP 213 | } 214 | 215 | der, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey) 216 | if err != nil { 217 | return fmt.Errorf("Could not create certificate: %s", err) 218 | } 219 | csr.Data.Body.CSR = string(PemEncodeX509CSRDER(der)) 220 | return nil 221 | } 222 | 223 | // ThreatSpec TMv0.1 for CSR.Public 224 | // Returns CSR for App:X509 225 | 226 | func (csr *CSR) Public() (*CSR, error) { 227 | selfJson := csr.Dump() 228 | publicCSR, err := NewCSR(selfJson) 229 | if err != nil { 230 | return nil, fmt.Errorf("Could not create public CSR: %s", err) 231 | } 232 | publicCSR.Data.Body.PrivateKey = "" 233 | return publicCSR, nil 234 | } 235 | 236 | // ThreatSpec TMv0.1 for CSR.PublicKey 237 | // Returns CSR public key for App:X509 238 | 239 | func (csr *CSR) PublicKey() (interface{}, error) { 240 | if rawCSR, err := PemDecodeX509CSR([]byte(csr.Data.Body.CSR)); err != nil { 241 | return nil, fmt.Errorf("Could not decode csr key: %s", err) 242 | } else { 243 | return rawCSR.PublicKey, nil 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /x509/csr_test.go: -------------------------------------------------------------------------------- 1 | package x509 2 | 3 | import ( 4 | "crypto/x509/pkix" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestX509NewCSR(t *testing.T) { 10 | csr, err := NewCSR(nil) 11 | assert.Nil(t, err) 12 | assert.NotNil(t, csr) 13 | assert.Equal(t, csr.Data.Scope, "pki.io") 14 | } 15 | 16 | func TestX509CSRDump(t *testing.T) { 17 | csr, _ := NewCSR(nil) 18 | csrJson := csr.Dump() 19 | assert.NotEqual(t, len(csrJson), 0) 20 | } 21 | 22 | func TestX509CSRGenerate(t *testing.T) { 23 | csr, _ := NewCSR(nil) 24 | subject := pkix.Name{CommonName: "test"} 25 | err := csr.Generate(&subject) 26 | assert.Nil(t, err) 27 | assert.NotEqual(t, csr.Data.Body.CSR, "") 28 | } 29 | 30 | func TestX509CSRPublic(t *testing.T) { 31 | csr, _ := NewCSR(nil) 32 | subject := pkix.Name{CommonName: "test"} 33 | csr.Generate(&subject) 34 | publicCSR, err := csr.Public() 35 | assert.Nil(t, err) 36 | assert.Equal(t, csr.Data.Body.CSR, publicCSR.Data.Body.CSR) 37 | assert.Equal(t, publicCSR.Data.Body.PrivateKey, "") 38 | } 39 | -------------------------------------------------------------------------------- /x509/helpers.go: -------------------------------------------------------------------------------- 1 | // ThreatSpec package github.com/pki-io/core/x509 as x509 2 | package x509 3 | 4 | import ( 5 | "crypto/x509" 6 | "encoding/hex" 7 | "encoding/pem" 8 | "fmt" 9 | "github.com/pki-io/core/crypto" 10 | "math/big" 11 | "strings" 12 | ) 13 | 14 | // ThreatSpec TMv0.1 for PemEncodeX509CertificateDER 15 | // Does PEM encoding of an X509 certificate for App:X509 16 | 17 | func PemEncodeX509CertificateDER(cert []byte) []byte { 18 | b := &pem.Block{Type: "CERTIFICATE", Bytes: cert} 19 | return pem.EncodeToMemory(b) 20 | } 21 | 22 | // ThreatSpec TMv0.1 for PemDecodeX509Certificate 23 | // Does PEM decoding of a X509 certificate for App:X509 24 | 25 | func PemDecodeX509Certificate(in []byte) (*x509.Certificate, error) { 26 | b, _ := pem.Decode(in) 27 | if certs, err := x509.ParseCertificates(b.Bytes); err != nil { 28 | return nil, fmt.Errorf("Could not parse certificate: %s", err) 29 | } else { 30 | return certs[0], nil 31 | } 32 | } 33 | 34 | // ThreatSpec TMv0.1 for PemEncodeX509CSRDER 35 | // Does PEM encoding of X509 CSR for App:X509 36 | 37 | func PemEncodeX509CSRDER(cert []byte) []byte { 38 | b := &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: cert} 39 | return pem.EncodeToMemory(b) 40 | } 41 | 42 | // ThreatSpec TMv0.1 for PemDecodeX509CSR 43 | // Does PEM decoding of X509 CSR for App:X509 44 | 45 | func PemDecodeX509CSR(in []byte) (*x509.CertificateRequest, error) { 46 | b, _ := pem.Decode(in) 47 | if csr, err := x509.ParseCertificateRequest(b.Bytes); err != nil { 48 | return nil, fmt.Errorf("Could not parse csr: %s", err) 49 | } else { 50 | return csr, nil 51 | } 52 | } 53 | 54 | // ThreatSpec TMv0.1 for NewID 55 | // Creates new ID for App:X509 56 | 57 | func NewID() string { 58 | idBytes, _ := crypto.RandomBytes(16) 59 | return hex.EncodeToString(idBytes) 60 | } 61 | 62 | // ThreatSpec TMv0.1 for NewSerial 63 | // Does new certificate serial creation for App:X509 64 | 65 | func NewSerial() (*big.Int, error) { 66 | uuid := crypto.TimeOrderedUUID() 67 | clean := strings.Replace(uuid, "-", "", -1) 68 | i := new(big.Int) 69 | _, err := fmt.Sscanf(clean, "%x", i) 70 | if err != nil { 71 | return nil, fmt.Errorf("Could not scan UUID to int: %s", err) 72 | } else { 73 | return i, nil 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /x509/x509_test.go: -------------------------------------------------------------------------------- 1 | package x509 2 | 3 | import ( 4 | "crypto/x509/pkix" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestX509SignCSR(t *testing.T) { 11 | rootCA, _ := NewCA(nil) 12 | rootCA.Data.Body.Name = "RootCA" 13 | rootCA.Data.Body.DNScope.Country = "UK" 14 | rootCA.Data.Body.DNScope.Organization = "pki.io" 15 | rootCA.GenerateRoot() 16 | 17 | subCA, _ := NewCA(nil) 18 | subCA.Data.Body.Name = "DevCA" 19 | subCA.Data.Body.DNScope.OrganizationalUnit = "Development" 20 | subCA.GenerateSub(rootCA) 21 | 22 | csr, _ := NewCSR(nil) 23 | csr.Data.Body.Name = "Server1" 24 | subject := pkix.Name{CommonName: csr.Data.Body.Name} 25 | csr.Generate(&subject) 26 | 27 | csrPublic, _ := csr.Public() 28 | 29 | cert, err := subCA.Sign(csrPublic, false) 30 | assert.Nil(t, err) 31 | assert.NotNil(t, cert) 32 | assert.NotEqual(t, cert.Data.Body.Certificate, "") 33 | 34 | certificate, err := PemDecodeX509Certificate([]byte(cert.Data.Body.Certificate)) 35 | assert.Nil(t, err) 36 | assert.True(t, certificate.NotBefore.After(time.Now().AddDate(0, 0, -1))) 37 | assert.True(t, certificate.NotAfter.Before(time.Now().AddDate(0, 0, 1))) 38 | } 39 | 40 | func TestX509SignCSRKeepSubject(t *testing.T) { 41 | rootCA, _ := NewCA(nil) 42 | rootCA.Data.Body.Name = "RootCA" 43 | rootCA.GenerateRoot() 44 | 45 | csr, _ := NewCSR(nil) 46 | csr.Data.Body.Name = "Server1" 47 | subject := pkix.Name{CommonName: csr.Data.Body.Name} 48 | csr.Generate(&subject) 49 | 50 | csrPublic, _ := csr.Public() 51 | 52 | cert, err := rootCA.Sign(csrPublic, true) 53 | assert.Nil(t, err) 54 | assert.NotNil(t, cert) 55 | assert.NotEqual(t, cert.Data.Body.Certificate, "") 56 | 57 | certificate, err := PemDecodeX509Certificate([]byte(cert.Data.Body.Certificate)) 58 | assert.Nil(t, err) 59 | assert.Equal(t, certificate.Subject.CommonName, subject.CommonName) 60 | assert.True(t, certificate.NotBefore.After(time.Now().AddDate(0, 0, -1))) 61 | assert.True(t, certificate.NotAfter.Before(time.Now().AddDate(0, 0, 1))) 62 | } 63 | --------------------------------------------------------------------------------