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 }