1// 2// Copyright (c) 2018, Joyent, Inc. All rights reserved. 3// 4// This Source Code Form is subject to the terms of the Mozilla Public 5// License, v. 2.0. If a copy of the MPL was not distributed with this 6// file, You can obtain one at http://mozilla.org/MPL/2.0/. 7// 8 9package authentication 10 11import ( 12 "crypto" 13 "crypto/rand" 14 "crypto/rsa" 15 "crypto/x509" 16 "encoding/base64" 17 "encoding/pem" 18 "fmt" 19 "strings" 20 21 "github.com/pkg/errors" 22 "golang.org/x/crypto/ssh" 23) 24 25type PrivateKeySigner struct { 26 formattedKeyFingerprint string 27 keyFingerprint string 28 algorithm string 29 accountName string 30 userName string 31 hashFunc crypto.Hash 32 33 privateKey *rsa.PrivateKey 34} 35 36type PrivateKeySignerInput struct { 37 KeyID string 38 PrivateKeyMaterial []byte 39 AccountName string 40 Username string 41} 42 43func NewPrivateKeySigner(input PrivateKeySignerInput) (*PrivateKeySigner, error) { 44 keyFingerprintMD5 := strings.Replace(input.KeyID, ":", "", -1) 45 46 block, _ := pem.Decode(input.PrivateKeyMaterial) 47 if block == nil { 48 return nil, errors.New("Error PEM-decoding private key material: nil block received") 49 } 50 51 rsakey, err := x509.ParsePKCS1PrivateKey(block.Bytes) 52 if err != nil { 53 return nil, errors.Wrap(err, "unable to parse private key") 54 } 55 56 sshPublicKey, err := ssh.NewPublicKey(rsakey.Public()) 57 if err != nil { 58 return nil, errors.Wrap(err, "unable to parse SSH key from private key") 59 } 60 61 matchKeyFingerprint := formatPublicKeyFingerprint(sshPublicKey, false) 62 displayKeyFingerprint := formatPublicKeyFingerprint(sshPublicKey, true) 63 if matchKeyFingerprint != keyFingerprintMD5 { 64 return nil, errors.New("Private key file does not match public key fingerprint") 65 } 66 67 signer := &PrivateKeySigner{ 68 formattedKeyFingerprint: displayKeyFingerprint, 69 keyFingerprint: input.KeyID, 70 accountName: input.AccountName, 71 72 hashFunc: crypto.SHA1, 73 privateKey: rsakey, 74 } 75 76 if input.Username != "" { 77 signer.userName = input.Username 78 } 79 80 _, algorithm, err := signer.SignRaw("HelloWorld") 81 if err != nil { 82 return nil, fmt.Errorf("Cannot sign using ssh agent: %s", err) 83 } 84 signer.algorithm = algorithm 85 86 return signer, nil 87} 88 89func (s *PrivateKeySigner) Sign(dateHeader string, isManta bool) (string, error) { 90 const headerName = "date" 91 92 hash := s.hashFunc.New() 93 hash.Write([]byte(fmt.Sprintf("%s: %s", headerName, dateHeader))) 94 digest := hash.Sum(nil) 95 96 signed, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, s.hashFunc, digest) 97 if err != nil { 98 return "", errors.Wrap(err, "unable to sign date header") 99 } 100 signedBase64 := base64.StdEncoding.EncodeToString(signed) 101 102 key := &KeyID{ 103 UserName: s.userName, 104 AccountName: s.accountName, 105 Fingerprint: s.formattedKeyFingerprint, 106 IsManta: isManta, 107 } 108 109 return fmt.Sprintf(authorizationHeaderFormat, key.generate(), "rsa-sha1", headerName, signedBase64), nil 110} 111 112func (s *PrivateKeySigner) SignRaw(toSign string) (string, string, error) { 113 hash := s.hashFunc.New() 114 hash.Write([]byte(toSign)) 115 digest := hash.Sum(nil) 116 117 signed, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, s.hashFunc, digest) 118 if err != nil { 119 return "", "", errors.Wrap(err, "unable to sign date header") 120 } 121 signedBase64 := base64.StdEncoding.EncodeToString(signed) 122 return signedBase64, "rsa-sha1", nil 123} 124 125func (s *PrivateKeySigner) KeyFingerprint() string { 126 return s.formattedKeyFingerprint 127} 128 129func (s *PrivateKeySigner) DefaultAlgorithm() string { 130 return s.algorithm 131} 132