github.com/greenpau/go-identity@v1.1.6/public_key.go (about)

     1  // Copyright 2020 Paul Greenberg greenpau@outlook.com
     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.
    14  
    15  package identity
    16  
    17  import (
    18  	"bytes"
    19  	"crypto/rsa"
    20  	"crypto/x509"
    21  	"encoding/base64"
    22  	"encoding/hex"
    23  	"encoding/pem"
    24  	"fmt"
    25  	"github.com/greenpau/go-identity/pkg/errors"
    26  	"github.com/greenpau/go-identity/pkg/requests"
    27  	"golang.org/x/crypto/openpgp"
    28  	"golang.org/x/crypto/ssh"
    29  	"strconv"
    30  	"strings"
    31  	"time"
    32  )
    33  
    34  var supportedPublicKeyTypes = map[string]bool{
    35  	"ssh": true,
    36  	"gpg": true,
    37  }
    38  
    39  // PublicKeyBundle is a collection of public keys.
    40  type PublicKeyBundle struct {
    41  	keys []*PublicKey
    42  	size int
    43  }
    44  
    45  // PublicKey is a puiblic key in a public-private key pair.
    46  type PublicKey struct {
    47  	ID    string `json:"id,omitempty" xml:"id,omitempty" yaml:"id,omitempty"`
    48  	Usage string `json:"usage,omitempty" xml:"usage,omitempty" yaml:"usage,omitempty"`
    49  	// Type is any of the following: dsa, rsa, ecdsa, ed25519
    50  	Type           string    `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty"`
    51  	Fingerprint    string    `json:"fingerprint,omitempty" xml:"fingerprint,omitempty" yaml:"fingerprint,omitempty"`
    52  	FingerprintMD5 string    `json:"fingerprint_md5,omitempty" xml:"fingerprint_md5,omitempty" yaml:"fingerprint_md5,omitempty"`
    53  	Comment        string    `json:"comment,omitempty" xml:"comment,omitempty" yaml:"comment,omitempty"`
    54  	Payload        string    `json:"payload,omitempty" xml:"payload,omitempty" yaml:"payload,omitempty"`
    55  	OpenSSH        string    `json:"openssh,omitempty" xml:"openssh,omitempty" yaml:"openssh,omitempty"`
    56  	Expired        bool      `json:"expired,omitempty" xml:"expired,omitempty" yaml:"expired,omitempty"`
    57  	ExpiredAt      time.Time `json:"expired_at,omitempty" xml:"expired_at,omitempty" yaml:"expired_at,omitempty"`
    58  	CreatedAt      time.Time `json:"created_at,omitempty" xml:"created_at,omitempty" yaml:"created_at,omitempty"`
    59  	Disabled       bool      `json:"disabled,omitempty" xml:"disabled,omitempty" yaml:"disabled,omitempty"`
    60  	DisabledAt     time.Time `json:"disabled_at,omitempty" xml:"disabled_at,omitempty" yaml:"disabled_at,omitempty"`
    61  }
    62  
    63  // NewPublicKeyBundle returns an instance of PublicKeyBundle.
    64  func NewPublicKeyBundle() *PublicKeyBundle {
    65  	return &PublicKeyBundle{
    66  		keys: []*PublicKey{},
    67  	}
    68  }
    69  
    70  // Add adds PublicKey to PublicKeyBundle.
    71  func (b *PublicKeyBundle) Add(k *PublicKey) {
    72  	b.keys = append(b.keys, k)
    73  	b.size++
    74  }
    75  
    76  // Get returns PublicKey instances of the PublicKeyBundle.
    77  func (b *PublicKeyBundle) Get() []*PublicKey {
    78  	return b.keys
    79  }
    80  
    81  // Size returns the number of PublicKey instances in PublicKeyBundle.
    82  func (b *PublicKeyBundle) Size() int {
    83  	return b.size
    84  }
    85  
    86  // NewPublicKey returns an instance of PublicKey.
    87  func NewPublicKey(r *requests.Request) (*PublicKey, error) {
    88  	p := &PublicKey{
    89  		Comment:   r.Key.Comment,
    90  		ID:        GetRandomString(40),
    91  		Payload:   r.Key.Payload,
    92  		Usage:     r.Key.Usage,
    93  		CreatedAt: time.Now().UTC(),
    94  	}
    95  	if err := p.parse(); err != nil {
    96  		return nil, err
    97  	}
    98  	if r.Key.Disabled {
    99  		p.Disabled = true
   100  		p.DisabledAt = time.Now().UTC()
   101  	}
   102  	return p, nil
   103  }
   104  
   105  // Disable disables PublicKey instance.
   106  func (p *PublicKey) Disable() {
   107  	p.Expired = true
   108  	p.ExpiredAt = time.Now().UTC()
   109  	p.Disabled = true
   110  	p.DisabledAt = time.Now().UTC()
   111  }
   112  
   113  func (p *PublicKey) parse() error {
   114  	if _, exists := supportedPublicKeyTypes[p.Usage]; !exists {
   115  		return errors.ErrPublicKeyInvalidUsage.WithArgs(p.Usage)
   116  	}
   117  	if p.Payload == "" {
   118  		return errors.ErrPublicKeyEmptyPayload
   119  	}
   120  	switch {
   121  	case strings.Contains(p.Payload, "RSA PUBLIC KEY"):
   122  		return p.parsePublicKeyRSA()
   123  	case strings.HasPrefix(p.Payload, "ssh-rsa "):
   124  		return p.parsePublicKeyOpenSSH()
   125  	case strings.Contains(p.Payload, "BEGIN PGP PUBLIC KEY BLOCK"):
   126  		return p.parsePublicKeyPGP()
   127  	}
   128  	return errors.ErrPublicKeyUsageUnsupported.WithArgs(p.Usage)
   129  }
   130  
   131  func (p *PublicKey) parsePublicKeyOpenSSH() error {
   132  	// Attempt parsing as authorized OpenSSH keys.
   133  	payloadBytes := bytes.TrimSpace([]byte(p.Payload))
   134  	i := bytes.IndexAny(payloadBytes, " \t")
   135  	if i == -1 {
   136  		i = len(payloadBytes)
   137  	}
   138  
   139  	var comment []byte
   140  	payloadBase64 := payloadBytes[:i]
   141  	if len(payloadBase64) < 20 {
   142  		// skip preamble, i.e. ssh-rsa, etc.
   143  		payloadBase64 = bytes.TrimSpace(payloadBytes[i:])
   144  		i = bytes.IndexAny(payloadBase64, " \t")
   145  		if i > 0 {
   146  			comment = bytes.TrimSpace(payloadBase64[i:])
   147  			payloadBase64 = payloadBase64[:i]
   148  		}
   149  		p.OpenSSH = string(payloadBase64)
   150  	}
   151  	k := make([]byte, base64.StdEncoding.DecodedLen(len(payloadBase64)))
   152  	n, err := base64.StdEncoding.Decode(k, payloadBase64)
   153  	if err != nil {
   154  		return errors.ErrPublicKeyParse.WithArgs(err)
   155  	}
   156  	publicKey, err := ssh.ParsePublicKey(k[:n])
   157  	if err != nil {
   158  		return errors.ErrPublicKeyParse.WithArgs(err)
   159  	}
   160  	p.Type = publicKey.Type()
   161  	if string(comment) != "" {
   162  		p.Comment = string(comment)
   163  	}
   164  	p.FingerprintMD5 = ssh.FingerprintLegacyMD5(publicKey)
   165  	p.Fingerprint = ssh.FingerprintSHA256(publicKey)
   166  
   167  	// Convert OpenSSH key to RSA PUBLIC KEY
   168  	switch publicKey.Type() {
   169  	case "ssh-rsa":
   170  		publicKeyBytes := publicKey.Marshal()
   171  		parsedPublicKey, err := ssh.ParsePublicKey(publicKeyBytes)
   172  		if err != nil {
   173  			return errors.ErrPublicKeyParse.WithArgs(err)
   174  		}
   175  		cryptoKey := parsedPublicKey.(ssh.CryptoPublicKey)
   176  		publicCryptoKey := cryptoKey.CryptoPublicKey()
   177  		rsaKey := publicCryptoKey.(*rsa.PublicKey)
   178  		rsaKeyASN1, err := x509.MarshalPKIXPublicKey(rsaKey)
   179  		if err != nil {
   180  			return errors.ErrPublicKeyParse.WithArgs(err)
   181  		}
   182  		encodedKey := pem.EncodeToMemory(&pem.Block{
   183  			Type: "RSA PUBLIC KEY",
   184  			//Bytes: x509.MarshalPKCS1PublicKey(rsaKey),
   185  			Bytes: rsaKeyASN1,
   186  		})
   187  		p.Payload = string(encodedKey)
   188  	default:
   189  		return errors.ErrPublicKeyTypeUnsupported.WithArgs(publicKey.Type())
   190  	}
   191  	return nil
   192  }
   193  
   194  func (p *PublicKey) parsePublicKeyPGP() error {
   195  	var user, algo, comment string
   196  	p.Payload = strings.TrimSpace(p.Payload)
   197  	for _, w := range []string{"BEGIN", "END"} {
   198  		s := fmt.Sprintf("-----%s PGP PUBLIC KEY BLOCK-----", w)
   199  		if (w == "BEGIN" && !strings.HasPrefix(p.Payload, s)) || (w == "END" && !strings.HasSuffix(p.Payload, s)) {
   200  			return errors.ErrPublicKeyParse.WithArgs(fmt.Errorf("%s PGP PUBLIC KEY BLOCK not found", w))
   201  		}
   202  	}
   203  	kr, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(p.Payload))
   204  	if err != nil {
   205  		return errors.ErrPublicKeyParse.WithArgs(err)
   206  	}
   207  	if len(kr) != 1 {
   208  		return errors.ErrPublicKeyParse.WithArgs(fmt.Errorf("PGP keyring contains %d entries", len(kr)))
   209  	}
   210  	if kr[0].PrimaryKey == nil {
   211  		return errors.ErrPublicKeyParse.WithArgs(fmt.Errorf("PGP keyring entry has no public key"))
   212  	}
   213  	if kr[0].Identities == nil || len(kr[0].Identities) == 0 {
   214  		return errors.ErrPublicKeyParse.WithArgs(fmt.Errorf("PGP keyring entry has no identities"))
   215  	}
   216  	pk := kr[0].PrimaryKey
   217  	p.ID = strconv.FormatUint(pk.KeyId, 16)
   218  	p.Fingerprint = hex.EncodeToString(pk.Fingerprint[:])
   219  	switch pk.PubKeyAlgo {
   220  	case 1, 2, 3:
   221  		algo = "RSA"
   222  		p.Type = "rsa"
   223  	case 17:
   224  		algo = "DSA"
   225  		p.Type = "dsa"
   226  	case 18:
   227  		algo = "ECDH"
   228  		p.Type = "ecdh"
   229  	case 19:
   230  		algo = "ECDSA"
   231  		p.Type = "ecdsa"
   232  	default:
   233  		return errors.ErrPublicKeyParse.WithArgs(fmt.Errorf("PGP keyring entry has unsupported public key algo %v", pk.PubKeyAlgo))
   234  	}
   235  	for _, u := range kr[0].Identities {
   236  		user = u.Name
   237  		break
   238  	}
   239  	comment = fmt.Sprintf("%s, algo %s, created %s", user, algo, pk.CreationTime.UTC())
   240  	if p.Comment != "" {
   241  		p.Comment = fmt.Sprintf("%s (%s)", p.Comment, comment)
   242  	} else {
   243  		p.Comment = comment
   244  	}
   245  	return nil
   246  }
   247  
   248  func (p *PublicKey) parsePublicKeyRSA() error {
   249  	// Processing PEM file format
   250  	if p.Usage != "ssh" {
   251  		return errors.ErrPublicKeyUsagePayloadMismatch.WithArgs(p.Usage)
   252  	}
   253  	block, _ := pem.Decode(bytes.TrimSpace([]byte(p.Payload)))
   254  	if block == nil {
   255  		return errors.ErrPublicKeyBlockType.WithArgs("")
   256  	}
   257  	if block.Type != "RSA PUBLIC KEY" {
   258  		return errors.ErrPublicKeyBlockType.WithArgs(block.Type)
   259  	}
   260  	publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
   261  	if err != nil {
   262  		return errors.ErrPublicKeyParse.WithArgs(err)
   263  	}
   264  	publicKey, err := ssh.NewPublicKey(publicKeyInterface)
   265  	if err != nil {
   266  		return fmt.Errorf("failed ssh.NewPublicKey: %s", err)
   267  	}
   268  	p.Type = publicKey.Type()
   269  	p.FingerprintMD5 = ssh.FingerprintLegacyMD5(publicKey)
   270  	p.Fingerprint = ssh.FingerprintSHA256(publicKey)
   271  	p.Fingerprint = strings.ReplaceAll(p.Fingerprint, "SHA256:", "")
   272  	p.OpenSSH = string(ssh.MarshalAuthorizedKey(publicKey))
   273  	p.OpenSSH = strings.TrimLeft(p.OpenSSH, p.Type+" ")
   274  	return nil
   275  }