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 }