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