github.com/greenpau/go-authcrunch@v1.1.4/pkg/identity/public_key.go (about)

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