Skip to content

Commit

Permalink
Add support for Yubikey pin
Browse files Browse the repository at this point in the history
For now it implements decryption for yubikey's SHA1 challenge-response
mode.

This commit also contains shell scripts that can be used as a extension
for the upstream project.

Closes #3
  • Loading branch information
anatol committed Jun 2, 2021
1 parent 2562a0e commit 82c971a
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 0 deletions.
73 changes: 73 additions & 0 deletions clevis-decrypt-yubikey
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/bin/bash -e
# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
#
# Copyright (c) 2021 Anatol Pomazau
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

[ $# -eq 1 ] && [ "$1" == "--summary" ] && exit 2

if [ -t 0 ]; then
exec >&2
echo
echo "Usage: clevis decrypt yubikey < JWE > PLAINTEXT"
echo
exit 2
fi

read -r -d . hdr

if ! jhd="$(jose b64 dec -i- <<< "$hdr")"; then
echo "Error decoding JWE protected header!" >&2
exit 1
fi

if [ "$(jose fmt -j- -Og clevis -g pin -u- <<< "$jhd")" != "yubikey" ]; then
echo "JWE pin mismatch!" >&2
exit 1
fi

type=$(jose fmt -j- -Og clevis -g yubikey -g type -u- <<< "$jhd")
if [ "$type" != "chalresp" ]; then
echo "JWE yubikey type mismatch!" >&2
exit 1
fi

slot=$(jose fmt -j- -Og clevis -g yubikey -g slot -Io- <<< "$jhd")

challenge=$(jose fmt -j- -Og clevis -g yubikey -g challenge -u- <<< "$jhd" | jose b64 dec -i - | xxd -p -c 1000)
response=$(echo -n $challenge | ykchalresp -x -i- -$slot)

# nettle-pbkdf2 used below hardcodes pbkdf2 with sha256
kdf_type=$(jose fmt -j- -Og clevis -g yubikey -g kdf -g type -u- <<< "$jhd")
if [ "$kdf_type" != "pbkdf2" ]; then
echo "JWE yubikey supports pbkdf2 kdf only!" >&2
exit 1
fi

kdf_hash=$(jose fmt -j- -Og clevis -g yubikey -g kdf -g hash -u- <<< "$jhd")
if [ "$kdf_hash" != "sha256" ]; then
echo "JWE yubikey supports sha256 hash only!" >&2
exit 1
fi

salt_hex=$(jose fmt -j- -Og clevis -g yubikey -g kdf -g salt -u- <<< "$jhd" | jose b64 dec -i - | xxd -p -c 1000)
iter=$(jose fmt -j- -Og clevis -g yubikey -g kdf -g iter -Io- <<< "$jhd")

key=$(echo -n $response | xxd -r -p | nettle-pbkdf2 --raw --iterations $iter --length 32 --hex-salt $salt_hex | jose b64 enc -I -)

jwk="$(jose jwk gen -i '{"alg":"A256GCM"}')"
jwk="$(jose fmt -j "$jwk" -q "$key" -s k -Uo-)"

(echo -n "$jwk$hdr."; /bin/cat) | jose jwe dec -k- -i-
69 changes: 69 additions & 0 deletions clevis-encrypt-yubikey
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/bin/bash -e
# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
#
# Copyright (c) 2021 Anatol Pomazau.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

SUMMARY="Encrypts using a Yubikey binding policy"

if [ "$1" == "--summary" ]; then
echo "$SUMMARY"
exit 0
fi

if [ -t 0 ]; then
exec >&2
echo
echo "Usage: clevis encrypt yubikey CONFIG < PLAINTEXT > JWE"
echo
echo "$SUMMARY"
echo
echo "This command uses the following configuration properties:"
echo
echo " slot: <integer> Yubikey slot number (default: 1)"
exit 2
fi

if ! cfg="$(jose fmt -j- -Oo- <<<"$1" 2>/dev/null)"; then
echo "Configuration is malformed!" >&2
exit 1
fi

slot=$(jose fmt -j- -Og slot -Io- <<<"$cfg") || slot=1

CHALLENGE_SIZE=32
SALT_SIZE=32

challenge=$(openssl rand -hex $CHALLENGE_SIZE)
response="$(echo -n $challenge | ykchalresp -x -$slot -i-)"

iter=1000
salt_hex=$(openssl rand -hex $SALT_SIZE)
key=$(echo -n "$response" | xxd -r -p | nettle-pbkdf2 --raw --iterations $iter --length 32 --hex-salt $salt_hex | jose b64 enc -I -)

jwk="$(jose jwk gen -i '{"alg":"A256GCM"}')"
jwk="$(jose fmt -j "$jwk" -q "$key" -s k -Uo-)"

jwe='{"protected":{"clevis":{"pin":"yubikey","yubikey":{"type":"chalresp"}}}}'

kdf='{"type":"pbkdf2","hash":"sha256"}'
base64_salt=$(echo -n "$salt_hex" | xxd -r -p | jose b64 enc -I -)
kdf="$(jose fmt -j "$kdf" -j $iter -Is iter -U -q "$base64_salt" -Ss salt -Uo-)"

base64_challenge=$(echo -n $challenge | xxd -r -p | jose b64 enc -I -)
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g yubikey -q "$base64_challenge" -Ss challenge -U -j $slot -Is slot -U -j $kdf -Os kdf -UUUUo-)"

exec jose jwe enc -i- -k- -I- -c < <(echo -n "$jwe$jwk"; /bin/cat)
2 changes: 2 additions & 0 deletions clevis.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ func Decrypt(data []byte) ([]byte, error) {
return DecryptSss(msg, clevis)
case "tpm2":
return DecryptTpm2(msg, clevis)
case "yubikey":
return DecryptYubikey(msg, clevis)
default:
return nil, fmt.Errorf("clevis.go: unknown pin '%v'", pin)
}
Expand Down
112 changes: 112 additions & 0 deletions yubikey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package clevis

import (
"bytes"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"hash"
"os/exec"
"strconv"

"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwe"
"golang.org/x/crypto/pbkdf2"
)

func DecryptYubikey(msg *jwe.Message, clevisNode map[string]interface{}) ([]byte, error) {
yubikeyNode, ok := clevisNode["yubikey"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("clevis.go/yubikey: cannot parse provided token, node 'clevis.yubikey'")
}

yubType, ok := yubikeyNode["type"]
if !ok {
return nil, fmt.Errorf("clevis.go/yubikey: cannot parse provided token, node 'clevis.yubikey.type'")
}

switch yubType {
case "chalresp":
return challengeResponse(msg, yubikeyNode)
default:
return nil, fmt.Errorf("clevis.go/yubikey: unknown type %s", yubType)
}
}

func challengeResponse(msg *jwe.Message, node map[string]interface{}) ([]byte, error) {
challenge, ok := node["challenge"].(string)
if !ok {
return nil, fmt.Errorf("clevis.go/yubikey: cannot parse provided token, node 'clevis.yubikey.challenge'")
}

slotNode, ok := node["slot"].(float64)
if !ok {
return nil, fmt.Errorf("clevis.go/yubikey: cannot parse provided token, node 'clevis.yubikey.slot'")
}
slot := int(slotNode)

challengeBin, err := base64.RawURLEncoding.DecodeString(challenge)
if err != nil {
return nil, err
}
if len(challengeBin) != 32 {
return nil, fmt.Errorf("expected challenge length is 32")
}

var outBuffer, errBuffer bytes.Buffer
cmd := exec.Command("ykchalresp", "-i-", "-"+strconv.Itoa(slot))
cmd.Stdin = bytes.NewReader(challengeBin)
cmd.Stdout = &outBuffer
cmd.Stderr = &errBuffer
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("%v: %s", err, errBuffer.String())
}
// out is hex
response := outBuffer.Bytes()[:40] // cut the trailing newline
responseBin := make([]byte, 20)
if _, err := hex.Decode(responseBin, response); err != nil {
return nil, err
}

kdf, ok := node["kdf"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("clevis.go/yubikey: cannot parse provided token, missing node 'clevis.yubikey.kdf'")
}

var key []byte

switch kdf["type"].(string) {
case "pbkdf2":
iter := int(kdf["iter"].(float64))
h := hashByName(kdf["hash"].(string))
if h == nil {
return nil, fmt.Errorf("clevis.go/yubikey: unknown hash specified at node 'clevis.yubikey.kdf.hash': %s", kdf["hash"].(string))
}
salt, err := base64.RawURLEncoding.DecodeString(kdf["salt"].(string))
if err != nil {
return nil, err
}
if len(salt) != 32 {
return nil, fmt.Errorf("expected salt length is 32, got %d", len(salt))
}

key = pbkdf2.Key(responseBin, salt, iter, 32, h)
default:
return nil, fmt.Errorf("clevis.go/yubikey: unknown kdf type specified at node 'clevis.yubikey.kdf.type': %s", kdf["type"].(string))
}

return msg.Decrypt(jwa.DIRECT, key)
}

func hashByName(name string) func() hash.Hash {
switch name {
case "sha256":
return sha256.New
case "sha1":
return sha1.New
default:
return nil
}
}
52 changes: 52 additions & 0 deletions yubikey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package clevis

import (
"bytes"
"os"
"os/exec"
"strings"
"testing"
)

func TestDecryptYubikey(t *testing.T) {
inputText := "testing yubikey"

clevisConfigs := []string{
`{"slot":2}`,
}

for _, c := range clevisConfigs {
var outbuf bytes.Buffer
cmd := exec.Command("./clevis-encrypt-yubikey", c)
cmd.Stdin = strings.NewReader(inputText)
cmd.Stdout = &outbuf
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
t.Fatal(err)
}

compactForm := outbuf.Bytes()
jsonForm, err := convertToJsonForm(compactForm)
if err != nil {
t.Fatal(err)
}

// decrypt compact form using our implementation
plaintext1, err := Decrypt(compactForm)
if err != nil {
t.Fatal(err)
}
if string(plaintext1) != inputText {
t.Fatalf("tpm2 decryption failed: expected '%s', got '%s'", inputText, string(plaintext1))
}

// decrypt json form using our implementation
plaintext2, err := Decrypt(jsonForm)
if err != nil {
t.Fatal(err)
}
if string(plaintext2) != inputText {
t.Fatalf("tpm2 decryption failed: expected '%s', got '%s'", inputText, string(plaintext2))
}
}
}

0 comments on commit 82c971a

Please sign in to comment.