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