Step 25: Real FIDO2 hardware key support.

HardwareFIDO2 implements FIDO2Device via go-libfido2 (CGo bindings to
Yubico's libfido2). Gated behind //go:build fido2 tag to keep default
builds CGo-free. Nix flake adds sgard-fido2 package variant.

CLI changes: --fido2-pin flag, unlockDEK helper tries FIDO2 first,
add-fido2/encrypt init --fido2 use real hardware, auto-unlock added
to restore/checkpoint/diff for encrypted entries.

Tested manually: add-fido2, add --encrypt, restore, checkpoint, diff
all work with hardware FIDO2 key (touch-to-unlock, no passphrase).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 12:40:46 -07:00
parent 5529fff649
commit 490db0599c
17 changed files with 358 additions and 34 deletions

View File

@@ -30,7 +30,7 @@ var addCmd = &cobra.Command{
if !g.HasEncryption() {
return fmt.Errorf("encryption not initialized; run sgard encrypt init first")
}
if err := g.UnlockDEK(promptPassphrase); err != nil {
if err := unlockDEK(g); err != nil {
return err
}
}

View File

@@ -18,6 +18,12 @@ var checkpointCmd = &cobra.Command{
return err
}
if g.HasEncryption() && g.NeedsDEK(g.List()) {
if err := unlockDEK(g); err != nil {
return err
}
}
if err := g.Checkpoint(checkpointMessage); err != nil {
return err
}

View File

@@ -17,6 +17,12 @@ var diffCmd = &cobra.Command{
return err
}
if g.HasEncryption() && g.NeedsDEK(g.List()) {
if err := unlockDEK(g); err != nil {
return err
}
}
d, err := g.Diff(args[0])
if err != nil {
return err

View File

@@ -36,8 +36,16 @@ var encryptInitCmd = &cobra.Command{
fmt.Println("Encryption initialized with passphrase slot.")
if fido2InitFlag {
fmt.Println("FIDO2 support requires a hardware device implementation.")
fmt.Println("Run 'sgard encrypt add-fido2' when a FIDO2 device is available.")
device := garden.DetectHardwareFIDO2(fido2PinFlag)
if device == nil {
fmt.Println("No FIDO2 device detected. Run 'sgard encrypt add-fido2' when one is connected.")
} else {
fmt.Println("Touch your FIDO2 device to register...")
if err := g.AddFIDO2Slot(device, fido2LabelFlag); err != nil {
return fmt.Errorf("adding FIDO2 slot: %w", err)
}
fmt.Println("FIDO2 slot added.")
}
}
return nil
@@ -59,13 +67,22 @@ var addFido2Cmd = &cobra.Command{
return fmt.Errorf("encryption not initialized; run sgard encrypt init first")
}
if err := g.UnlockDEK(promptPassphrase); err != nil {
if err := unlockDEK(g); err != nil {
return err
}
// Real FIDO2 device implementation would go here.
// For now, this is a placeholder that explains the requirement.
return fmt.Errorf("FIDO2 hardware support not yet implemented; requires libfido2 binding")
device := garden.DetectHardwareFIDO2(fido2PinFlag)
if device == nil {
return fmt.Errorf("no FIDO2 device detected; connect a FIDO2 key and try again")
}
fmt.Println("Touch your FIDO2 device to register...")
if err := g.AddFIDO2Slot(device, fido2LabelFlag); err != nil {
return err
}
fmt.Println("FIDO2 slot added.")
return nil
},
}
@@ -130,9 +147,8 @@ var changePassphraseCmd = &cobra.Command{
return fmt.Errorf("encryption not initialized")
}
// Unlock with current passphrase.
fmt.Println("Enter current passphrase:")
if err := g.UnlockDEK(promptPassphrase); err != nil {
// Unlock with current credentials.
if err := unlockDEK(g); err != nil {
return err
}
@@ -166,15 +182,15 @@ var rotateDEKCmd = &cobra.Command{
return fmt.Errorf("encryption not initialized")
}
// Unlock with current passphrase.
fmt.Println("Enter passphrase to unlock:")
if err := g.UnlockDEK(promptPassphrase); err != nil {
// Unlock with current credentials.
if err := unlockDEK(g); err != nil {
return err
}
// Rotate — re-prompts for passphrase to re-wrap slot.
fmt.Println("Enter passphrase to re-wrap DEK:")
if err := g.RotateDEK(promptPassphrase); err != nil {
device := garden.DetectHardwareFIDO2(fido2PinFlag)
if err := g.RotateDEK(promptPassphrase, device); err != nil {
return err
}
@@ -184,7 +200,7 @@ var rotateDEKCmd = &cobra.Command{
}
func init() {
encryptInitCmd.Flags().BoolVar(&fido2InitFlag, "fido2", false, "also set up FIDO2 (placeholder)")
encryptInitCmd.Flags().BoolVar(&fido2InitFlag, "fido2", false, "also register a FIDO2 hardware key")
addFido2Cmd.Flags().StringVar(&fido2LabelFlag, "label", "", "slot label (default: fido2/<hostname>)")
encryptCmd.AddCommand(encryptInitCmd)

12
cmd/sgard/fido2.go Normal file
View File

@@ -0,0 +1,12 @@
package main
import "github.com/kisom/sgard/garden"
var fido2PinFlag string
// unlockDEK attempts to unlock the DEK, trying FIDO2 hardware first
// (if available) and falling back to passphrase.
func unlockDEK(g *garden.Garden) error {
device := garden.DetectHardwareFIDO2(fido2PinFlag)
return g.UnlockDEK(promptPassphrase, device)
}

View File

@@ -116,6 +116,7 @@ func main() {
rootCmd.PersistentFlags().StringVar(&sshKeyFlag, "ssh-key", "", "path to SSH private key")
rootCmd.PersistentFlags().BoolVar(&tlsFlag, "tls", false, "use TLS for remote connection")
rootCmd.PersistentFlags().StringVar(&tlsCAFlag, "tls-ca", "", "path to CA certificate for TLS verification")
rootCmd.PersistentFlags().StringVar(&fido2PinFlag, "fido2-pin", "", "PIN for FIDO2 device (if PIN-protected)")
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)

View File

@@ -21,6 +21,12 @@ var restoreCmd = &cobra.Command{
return err
}
if g.HasEncryption() && g.NeedsDEK(g.List()) {
if err := unlockDEK(g); err != nil {
return err
}
}
confirm := func(path string) bool {
fmt.Printf("Overwrite %s? [y/N] ", path)
scanner := bufio.NewScanner(os.Stdin)