133 lines
3.5 KiB
Go
133 lines
3.5 KiB
Go
//
|
|
// Copyright (c) 2018, Joyent, Inc. All rights reserved.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
//
|
|
|
|
package authentication
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
type PrivateKeySigner struct {
|
|
formattedKeyFingerprint string
|
|
keyFingerprint string
|
|
algorithm string
|
|
accountName string
|
|
userName string
|
|
hashFunc crypto.Hash
|
|
|
|
privateKey *rsa.PrivateKey
|
|
}
|
|
|
|
type PrivateKeySignerInput struct {
|
|
KeyID string
|
|
PrivateKeyMaterial []byte
|
|
AccountName string
|
|
Username string
|
|
}
|
|
|
|
func NewPrivateKeySigner(input PrivateKeySignerInput) (*PrivateKeySigner, error) {
|
|
keyFingerprintMD5 := strings.Replace(input.KeyID, ":", "", -1)
|
|
|
|
block, _ := pem.Decode(input.PrivateKeyMaterial)
|
|
if block == nil {
|
|
return nil, errors.New("Error PEM-decoding private key material: nil block received")
|
|
}
|
|
|
|
rsakey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unable to parse private key")
|
|
}
|
|
|
|
sshPublicKey, err := ssh.NewPublicKey(rsakey.Public())
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unable to parse SSH key from private key")
|
|
}
|
|
|
|
matchKeyFingerprint := formatPublicKeyFingerprint(sshPublicKey, false)
|
|
displayKeyFingerprint := formatPublicKeyFingerprint(sshPublicKey, true)
|
|
if matchKeyFingerprint != keyFingerprintMD5 {
|
|
return nil, errors.New("Private key file does not match public key fingerprint")
|
|
}
|
|
|
|
signer := &PrivateKeySigner{
|
|
formattedKeyFingerprint: displayKeyFingerprint,
|
|
keyFingerprint: input.KeyID,
|
|
accountName: input.AccountName,
|
|
|
|
hashFunc: crypto.SHA1,
|
|
privateKey: rsakey,
|
|
}
|
|
|
|
if input.Username != "" {
|
|
signer.userName = input.Username
|
|
}
|
|
|
|
_, algorithm, err := signer.SignRaw("HelloWorld")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Cannot sign using ssh agent: %s", err)
|
|
}
|
|
signer.algorithm = algorithm
|
|
|
|
return signer, nil
|
|
}
|
|
|
|
func (s *PrivateKeySigner) Sign(dateHeader string) (string, error) {
|
|
const headerName = "date"
|
|
|
|
hash := s.hashFunc.New()
|
|
hash.Write([]byte(fmt.Sprintf("%s: %s", headerName, dateHeader)))
|
|
digest := hash.Sum(nil)
|
|
|
|
signed, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, s.hashFunc, digest)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "unable to sign date header")
|
|
}
|
|
signedBase64 := base64.StdEncoding.EncodeToString(signed)
|
|
|
|
var keyID string
|
|
if s.userName != "" {
|
|
|
|
keyID = path.Join("/", s.accountName, "users", s.userName, "keys", s.formattedKeyFingerprint)
|
|
} else {
|
|
keyID = path.Join("/", s.accountName, "keys", s.formattedKeyFingerprint)
|
|
}
|
|
return fmt.Sprintf(authorizationHeaderFormat, keyID, "rsa-sha1", headerName, signedBase64), nil
|
|
}
|
|
|
|
func (s *PrivateKeySigner) SignRaw(toSign string) (string, string, error) {
|
|
hash := s.hashFunc.New()
|
|
hash.Write([]byte(toSign))
|
|
digest := hash.Sum(nil)
|
|
|
|
signed, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, s.hashFunc, digest)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(err, "unable to sign date header")
|
|
}
|
|
signedBase64 := base64.StdEncoding.EncodeToString(signed)
|
|
return signedBase64, "rsa-sha1", nil
|
|
}
|
|
|
|
func (s *PrivateKeySigner) KeyFingerprint() string {
|
|
return s.formattedKeyFingerprint
|
|
}
|
|
|
|
func (s *PrivateKeySigner) DefaultAlgorithm() string {
|
|
return s.algorithm
|
|
}
|