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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
12
cmd/sgard/fido2.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user