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.

The pin is also implemented as a bash script here https://github.com/anatol/clevis-extra-pins

Closes #3
  • Loading branch information
anatol committed May 20, 2021
1 parent 2562a0e commit e904c9c
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 0 deletions.
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
80 changes: 80 additions & 0 deletions yubikey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package clevis

import (
"bytes"
"encoding/base64"
"encoding/hex"
"fmt"
"os/exec"
"strconv"

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

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'")
}

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

slot, ok := yubikeyNode["slot"].(string)
if !ok {
return nil, fmt.Errorf("clevis.go/yubikey: cannot parse provided token, node 'clevis.yubikey.slot'")
}
if _, err := strconv.Atoi(slot); err != nil {
return nil, fmt.Errorf("clevis.go/yubikey: node 'clevis.yubikey.slot' expected to be a number")
}

challengeBin, err := base64.RawURLEncoding.DecodeString(challenge)
if err != nil {
return nil, err
}

var outBuffer, errBuffer bytes.Buffer

cmd := exec.Command("ykchalresp", "-i-", "-"+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
}

salt, ok := yubikeyNode["salt"].(string)
if !ok {
return nil, fmt.Errorf("clevis.go/yubikey: cannot parse provided token, node 'clevis.yubikey.salt'")
}
saltBin, err := base64.RawURLEncoding.DecodeString(salt)
if err != nil {
return nil, err
}

prf := sha3.New256()
prf.Write(saltBin)
prf.Write(responseBin)
key := prf.Sum(nil)

return msg.Decrypt(jwa.DIRECT, key)
} else {
return nil, fmt.Errorf("clevis.go/yubikey: unknown type %s", yubType)
}
}
54 changes: 54 additions & 0 deletions yubikey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package clevis

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

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

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

for _, c := range clevisConfigs {
var outbuf, errbuf bytes.Buffer
cmd := exec.Command("clevis", "encrypt", "yubikey", c)
cmd.Stdin = strings.NewReader(inputText)
cmd.Stdout = &outbuf
cmd.Stderr = &errbuf

if err := cmd.Run(); err != nil {
fmt.Print(errbuf.String())
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 e904c9c

Please sign in to comment.