@@ -12,9 +12,11 @@ package main
1212import (
1313 "bufio"
1414 "context"
15+ "database/sql"
1516 "encoding/json"
1617 "errors"
1718 "fmt"
19+ "io"
1820 "os"
1921 "strings"
2022 "time"
@@ -342,6 +344,7 @@ Running without a subcommand will launch the interactive TUI.`,
342344 deployCmd ,
343345 rotateKeyCmd ,
344346 auditCmd ,
347+ auditCompareCmd ,
345348 importCmd ,
346349 transferCmd ,
347350 trustHostCmd ,
@@ -512,6 +515,73 @@ Use --mode=serial to only verify the Keymaster header serial number on the remot
512515 },
513516}
514517
518+ // auditCompareCmd compares a local or fetched authorized_keys file against
519+ // the stored `accounts.key_hash` for a single account.
520+ var auditCompareCmd = & cobra.Command {
521+ Use : "audit-compare <account-identifier> [file]" ,
522+ Short : "Compare an authorized_keys file to account key_hash" ,
523+ Long : "Provide an account identifier and a local file (or omit the file to fetch from the host)." ,
524+ Args : cobra .RangeArgs (1 , 2 ),
525+ PreRunE : setupDefaultServices ,
526+ Run : func (cmd * cobra.Command , args []string ) {
527+ identifier := args [0 ]
528+ var fileArg string
529+ if len (args ) > 1 {
530+ fileArg = args [1 ]
531+ }
532+
533+ st := & cliStoreAdapter {}
534+ accounts , err := st .GetAllAccounts ()
535+ if err != nil {
536+ log .Fatalf ("error fetching accounts: %v" , err )
537+ }
538+ accPtr , err := core .FindAccountByIdentifier (identifier , accounts )
539+ if err != nil {
540+ log .Fatalf ("%v" , err )
541+ }
542+ account := * accPtr
543+
544+ var content []byte
545+ if fileArg != "" {
546+ if fileArg == "-" {
547+ content , err = io .ReadAll (os .Stdin )
548+ if err != nil {
549+ log .Fatalf ("read stdin: %v" , err )
550+ }
551+ } else {
552+ content , err = os .ReadFile (fileArg )
553+ if err != nil {
554+ log .Fatalf ("open file: %v" , err )
555+ }
556+ }
557+ } else {
558+ dm := & cliDeployerManager {}
559+ content , err = dm .FetchAuthorizedKeys (account )
560+ if err != nil {
561+ log .Fatalf ("fetch remote authorized_keys: %v" , err )
562+ }
563+ }
564+
565+ gotHash := db .HashAuthorizedKeysContent (content )
566+
567+ // Read stored hash from DB
568+ var stored sql.NullString
569+ if err := db .QueryRawInto (context .Background (), db .BunDB (), & stored , "SELECT key_hash FROM accounts WHERE id = ?" , account .ID ); err != nil {
570+ log .Fatalf ("query key_hash: %v" , err )
571+ }
572+ if ! stored .Valid || stored .String == "" {
573+ fmt .Printf ("Account %s (id=%d) has no stored key_hash; computed=%s\n " , account .String (), account .ID , gotHash )
574+ return
575+ }
576+
577+ if stored .String == gotHash {
578+ fmt .Printf ("MATCH: account=%s id=%d key_hash=%s\n " , account .String (), account .ID , gotHash )
579+ } else {
580+ fmt .Printf ("MISMATCH: account=%s id=%d\n stored=%s\n computed=%s\n " , account .String (), account .ID , stored .String , gotHash )
581+ }
582+ },
583+ }
584+
515585// importCmd represents the 'import' command.
516586// It parses a standard authorized_keys file and adds the public keys
517587// found within it to the Keymaster database.
0 commit comments