mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 14:59:53 +08:00
* Add Yubikey PrivateKey implementation for use by Teleport clients. - Add yubikey login logic, reusing previously stored private keys. - Fix identity file decoding with PIV keys, which sign ecdsa certificates. - Add libpcsclite-dev pre-req for building on linux. - Remove unnecessary keys.Signer interface and move its functionality to keys.PrivateKey. - Move retry and jitter utils to new api/utils/retryutils package.
204 lines
9.1 KiB
Go
204 lines
9.1 KiB
Go
/*
|
|
Copyright 2021 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Package ppk provides functions implementing conversion between Teleport's native RSA
|
|
// keypairs and PuTTY's PPK format. It also provides functions for working with RFC4251-formatted
|
|
// mpints and strings.
|
|
package ppk
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/gravitational/trace"
|
|
|
|
"github.com/gravitational/teleport/api/constants"
|
|
)
|
|
|
|
// ConvertToPPK takes a regular RSA-formatted keypair and converts it into the PPK file format used by the PuTTY SSH client.
|
|
// The file format is described here: https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk
|
|
func ConvertToPPK(privateKey *rsa.PrivateKey, pub []byte) ([]byte, error) {
|
|
// https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk
|
|
// RSA keys are stored using an algorithm-name of 'ssh-rsa'. (Keys stored like this are also used by the updated RSA signature schemes that use
|
|
// hashes other than SHA-1. The public key data has already provided the key modulus and the public encoding exponent. The private data stores:
|
|
// mpint: the private decoding exponent of the key.
|
|
// mpint: one prime factor p of the key.
|
|
// mpint: the other prime factor q of the key. (RSA keys stored in this format are expected to have exactly two prime factors.)
|
|
// mpint: the multiplicative inverse of q modulo p.
|
|
ppkPrivateKey := new(bytes.Buffer)
|
|
|
|
// mpint: the private decoding exponent of the key.
|
|
// this is known as 'D'
|
|
binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(privateKey.D))
|
|
|
|
// mpint: one prime factor p of the key.
|
|
// this is known as 'P'
|
|
// the RSA standard dictates that P > Q
|
|
// for some reason what PuTTY names 'P' is Primes[1] to Go, and what PuTTY names 'Q' is Primes[0] to Go
|
|
P, Q := privateKey.Primes[1], privateKey.Primes[0]
|
|
binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(P))
|
|
|
|
// mpint: the other prime factor q of the key. (RSA keys stored in this format are expected to have exactly two prime factors.)
|
|
// this is known as 'Q'
|
|
binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(Q))
|
|
|
|
// mpint: the multiplicative inverse of q modulo p.
|
|
// this is known as 'iqmp'
|
|
iqmp := new(big.Int).ModInverse(Q, P)
|
|
binary.Write(ppkPrivateKey, binary.BigEndian, getRFC4251Mpint(iqmp))
|
|
|
|
// now we need to base64-encode the PPK-formatted private key which is made up of the above values
|
|
ppkPrivateKeyBase64 := make([]byte, base64.StdEncoding.EncodedLen(ppkPrivateKey.Len()))
|
|
base64.StdEncoding.Encode(ppkPrivateKeyBase64, ppkPrivateKey.Bytes())
|
|
|
|
// read Teleport public key
|
|
// fortunately, this is the one thing that's in exactly the same format that the PPK file uses, so we can just copy it verbatim
|
|
// remove ssh-rsa plus additional space from beginning of string if present
|
|
if !bytes.HasPrefix(pub, []byte(constants.SSHRSAType+" ")) {
|
|
return nil, trace.BadParameter("pub does not appear to be an ssh-rsa public key")
|
|
}
|
|
pub = bytes.TrimSuffix(bytes.TrimPrefix(pub, []byte(constants.SSHRSAType+" ")), []byte("\n"))
|
|
|
|
// the PPK file contains an anti-tampering MAC which is made up of various values which appear in the file.
|
|
// copied from Section C.3 of https://the.earth.li/~sgtatham/putty/0.76/htmldoc/AppendixC.html#ppk:
|
|
// hex-mac-data is a hexadecimal-encoded value, 64 digits long (i.e. 32 bytes), generated using the HMAC-SHA-256 algorithm with the following binary data as input:
|
|
// string: the algorithm-name header field.
|
|
// string: the encryption-type header field.
|
|
// string: the key-comment-string header field.
|
|
// string: the binary public key data, as decoded from the base64 lines after the 'Public-Lines' header.
|
|
// string: the plaintext of the binary private key data, as decoded from the base64 lines after the 'Private-Lines' header.
|
|
|
|
// these values are also used in the MAC generation, so we declare them as variables
|
|
keyType := constants.SSHRSAType
|
|
encryptionType := "none"
|
|
// as work for the future, it'd be nice to get the proxy/user pair name in here to make the name more
|
|
// of a unique identifier. this has to be done at generation time because the comment is part of the MAC
|
|
fileComment := "teleport-generated-ppk"
|
|
|
|
// string: the algorithm-name header field.
|
|
macKeyType := getRFC4251String([]byte(keyType))
|
|
// create a buffer to hold the elements needed to generate the MAC
|
|
macInput := new(bytes.Buffer)
|
|
binary.Write(macInput, binary.LittleEndian, macKeyType)
|
|
|
|
// string: the encryption-type header field.
|
|
macEncryptionType := getRFC4251String([]byte(encryptionType))
|
|
binary.Write(macInput, binary.BigEndian, macEncryptionType)
|
|
|
|
// string: the key-comment-string header field.
|
|
macComment := getRFC4251String([]byte(fileComment))
|
|
binary.Write(macInput, binary.BigEndian, macComment)
|
|
|
|
// base64-decode the Teleport public key, as we need its binary representation to generate the MAC
|
|
decoded := make([]byte, base64.StdEncoding.EncodedLen(len(pub)))
|
|
n, err := base64.StdEncoding.Decode(decoded, pub)
|
|
if err != nil {
|
|
return nil, trace.Errorf("could not base64-decode public key: %v, got %v bytes successfully", err, n)
|
|
}
|
|
decoded = decoded[:n]
|
|
// append the decoded public key bytes to the MAC buffer
|
|
macPublicKeyData := getRFC4251String(decoded)
|
|
binary.Write(macInput, binary.BigEndian, macPublicKeyData)
|
|
|
|
// append our PPK-formatted private key bytes to the MAC buffer
|
|
macPrivateKeyData := getRFC4251String(ppkPrivateKey.Bytes())
|
|
binary.Write(macInput, binary.BigEndian, macPrivateKeyData)
|
|
|
|
// as per the PPK spec, the key for the MAC is blank when the PPK file is unencrypted.
|
|
// therefore, the key is a zero-length byte slice.
|
|
hmacHash := hmac.New(sha256.New, []byte{})
|
|
// generate the MAC using HMAC-SHA-256
|
|
hmacHash.Write(macInput.Bytes())
|
|
macString := hex.EncodeToString(hmacHash.Sum(nil))
|
|
|
|
// build the string-formatted output PPK file
|
|
ppk := new(bytes.Buffer)
|
|
fmt.Fprintf(ppk, "PuTTY-User-Key-File-3: %v\n", keyType)
|
|
fmt.Fprintf(ppk, "Encryption: %v\n", encryptionType)
|
|
fmt.Fprintf(ppk, "Comment: %v\n", fileComment)
|
|
// chunk the Teleport-formatted public key into 64-character length lines
|
|
chunkedPublicKey := chunk(string(pub), 64)
|
|
fmt.Fprintf(ppk, "Public-Lines: %v\n", len(chunkedPublicKey))
|
|
for _, r := range chunkedPublicKey {
|
|
fmt.Fprintf(ppk, "%s\n", r)
|
|
}
|
|
// chunk the PPK-formatted private key into 64-character length lines
|
|
chunkedPrivateKey := chunk(string(ppkPrivateKeyBase64), 64)
|
|
fmt.Fprintf(ppk, "Private-Lines: %v\n", len(chunkedPrivateKey))
|
|
for _, r := range chunkedPrivateKey {
|
|
fmt.Fprintf(ppk, "%s\n", r)
|
|
}
|
|
fmt.Fprintf(ppk, "Private-MAC: %v\n", macString)
|
|
|
|
return ppk.Bytes(), nil
|
|
}
|
|
|
|
// chunk converts a string into a []string with chunks of size chunkSize;
|
|
// used to split base64-encoded strings across multiple lines with an even width.
|
|
// note: this function operates on Unicode code points rather than bytes, therefore
|
|
// using it with multi-byte characters will result in unevenly chunked strings.
|
|
// it's intended usage is only for chunking base64-encoded strings.
|
|
func chunk(s string, size int) []string {
|
|
var chunks []string
|
|
for b := []byte(s); len(b) > 0; {
|
|
n := size
|
|
if n > len(b) {
|
|
n = len(b)
|
|
}
|
|
chunks = append(chunks, string(b[:n]))
|
|
b = b[n:]
|
|
}
|
|
return chunks
|
|
}
|
|
|
|
// getRFC4251Mpint returns a stream of bytes representing a mixed-precision integer (a big.Int in Go)
|
|
// prepended with a big-endian uint32 expressing the length of the data following.
|
|
// This is the 'mpint' format in RFC4251 Section 5 (https://datatracker.ietf.org/doc/html/rfc4251#section-5)
|
|
func getRFC4251Mpint(n *big.Int) []byte {
|
|
buf := new(bytes.Buffer)
|
|
b := n.Bytes()
|
|
// RFC4251: If the most significant bit would be set for a positive number, the number MUST be preceded by a zero byte.
|
|
if b[0]&0x80 > 0 {
|
|
b = append([]byte{0}, b...)
|
|
}
|
|
// write a uint32 with the length of the byte stream to the buffer
|
|
binary.Write(buf, binary.BigEndian, uint32(len(b)))
|
|
// write the byte stream representing of the rest of the integer to the buffer
|
|
binary.Write(buf, binary.BigEndian, b)
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// getRFC4251String returns a stream of bytes representing a string prepended with a big-endian unit32
|
|
// expressing the length of the data following.
|
|
// This is the 'string' format in RFC4251 Section 5 (https://datatracker.ietf.org/doc/html/rfc4251#section-5)
|
|
func getRFC4251String(data []byte) []byte {
|
|
buf := new(bytes.Buffer)
|
|
// write a uint32 with the length of the byte stream to the buffer
|
|
binary.Write(buf, binary.BigEndian, uint32(len(data)))
|
|
// write the byte stream representing of the rest of the data to the buffer
|
|
for _, v := range data {
|
|
binary.Write(buf, binary.BigEndian, v)
|
|
}
|
|
return buf.Bytes()
|
|
}
|