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 }