You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/cmd/datekeys/author.go

181 lines
5.2 KiB

package main
import (
"errors"
"fmt"
"io"
"os"
"strings"
"g.activething.com/go/DateKeys/authorkey"
Review fixes: author keys, the writer, the CLI, extensions and the locator Fixes of the review of the session of 1 and 2 October that the text of spec v0.11 already asks for: - authorkey: String and GoString hide the secret key, which only Secret returns; ParsePublic refuses a key that is not a point of the curve (ed25519strict.OnCurve, checked against the square root of testkit). - capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never a capsule without the signature or the seal that was asked for. A panic while evaluating the signature or the seal fails only that part, F1 or S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can refuse to publish the files. - extension.CheckWrite, the rule of encoders of spec 72: the writers of capsules and .dkk files refuse datekeys.note and datekeys.capsule outside the arrays where they are registered, or with invalid data. - CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE before it signs (rule 20); decrypt -expect-author compares the key of an F4 and writes nothing unless it matches; decrypt notifies a public note that it does not show; the lines of the verdicts break at the last space that fits, each row after the first behind a mark, so that the terminal never breaks them; L is the payload, not the content. - locator: a reader rejects an address that breaks 44.1 and keeps the others; addresses refuse the special-purpose blocks of IANA, IPv6 outside 2000::/3, localhost and local names, characters outside RFC 3986, dot segments, and a CID that does not decode to version 1 and a multihash; ParseInfo checks that the locator is an age file with one tlock stanza for the round of its DateKey; Info.Extension reads what it writes; its errors carry no normative code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
"g.activething.com/go/DateKeys/capsule"
)
// maxPassFile bounds the file of a passphrase.
const maxPassFile = 4 << 10
Review fixes: author keys, the writer, the CLI, extensions and the locator Fixes of the review of the session of 1 and 2 October that the text of spec v0.11 already asks for: - authorkey: String and GoString hide the secret key, which only Secret returns; ParsePublic refuses a key that is not a point of the curve (ed25519strict.OnCurve, checked against the square root of testkit). - capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never a capsule without the signature or the seal that was asked for. A panic while evaluating the signature or the seal fails only that part, F1 or S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can refuse to publish the files. - extension.CheckWrite, the rule of encoders of spec 72: the writers of capsules and .dkk files refuse datekeys.note and datekeys.capsule outside the arrays where they are registered, or with invalid data. - CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE before it signs (rule 20); decrypt -expect-author compares the key of an F4 and writes nothing unless it matches; decrypt notifies a public note that it does not show; the lines of the verdicts break at the last space that fits, each row after the first behind a mark, so that the terminal never breaks them; L is the payload, not the content. - locator: a reader rejects an address that breaks 44.1 and keeps the others; addresses refuse the special-purpose blocks of IANA, IPv6 outside 2000::/3, localhost and local names, characters outside RFC 3986, dot segments, and a CID that does not decode to version 1 and a multihash; ParseInfo checks that the locator is an age file with one tlock stanza for the round of its DateKey; Info.Extension reads what it writes; its errors carry no normative code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// announced is an author key that says, before it signs, which key signs
// and the code of AUTHOR_MESSAGE, as spec §62.1 rule 20 asks of a writer
// before each signature: whoever checks the capsule later compares the code.
type announced struct {
capsule.AuthorKey
w io.Writer
}
func (a announced) Sign(message []byte) []byte {
pub, _ := authorkey.PublicString(a.Public())
fmt.Fprintf(a.w, "Signing with the author key %s\n AUTHOR_MESSAGE code %s\n", pub, capsule.AuthorCode(message))
return a.AuthorKey.Sign(message)
}
// readPass returns the passphrase in file, one line without its line ending,
// or in the standard input when file is "-". The CLI takes no passphrase on
// the command line, where the shell history keeps it, nor from the
// environment, where other processes can read it.
func readPass(cmd, file string, stdin io.Reader) (string, error) {
var r io.Reader = stdin
if file == "-" {
// A terminal would echo the passphrase and keep it in the scrollback.
if f, ok := stdin.(*os.File); ok {
if info, err := f.Stat(); err == nil && info.Mode()&os.ModeCharDevice != 0 {
return "", fmt.Errorf("%s: the passphrase would be read from a terminal and shown on screen: pipe it in, or use a file", cmd)
}
}
} else {
f, err := os.Open(file)
if err != nil {
return "", err
}
defer f.Close()
r = f
}
b, err := io.ReadAll(io.LimitReader(r, maxPassFile+1))
defer clear(b)
if err != nil {
return "", err
}
if len(b) > maxPassFile {
return "", fmt.Errorf("%s: the passphrase file is longer than %d bytes", cmd, maxPassFile)
}
s := strings.TrimSuffix(strings.TrimSuffix(string(b), "\n"), "\r")
if s == "" {
return "", fmt.Errorf("%s: the passphrase is empty", cmd)
}
return s, nil
}
// loadAuthorKey reads the key file path. An encrypted one needs passFile.
func loadAuthorKey(cmd, path, passFile string, stdin io.Reader) (*authorkey.Key, error) {
var pass string
if passFile != "" {
var err error
if pass, err = readPass(cmd, passFile, stdin); err != nil {
return nil, err
}
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
k, err := authorkey.Read(f, pass)
if err != nil {
if pass == "" && strings.Contains(err.Error(), "passphrase") {
return nil, fmt.Errorf("%s: %s is encrypted: give its passphrase with -pass-file FILE, or - for the standard input", cmd, path)
}
return nil, fmt.Errorf("%s: %s: %w", cmd, path, err)
}
return k, nil
}
// author runs "datekeys author keygen|public" (spec v0.11, §29.12).
func author(args []string, stdout, stderr io.Writer, stdin io.Reader) error {
if len(args) == 0 {
return errUsage
}
switch args[0] {
case "keygen":
return authorKeygen(args[1:], stdout, stderr, stdin)
case "public":
return authorPublic(args[1:], stdout, stdin)
}
return errUsage
}
func authorKeygen(args []string, stdout, stderr io.Writer, stdin io.Reader) error {
fs := newFlags("author keygen")
out := fs.String("out", "", "new file for the secret key; never overwritten")
passFile := fs.String("pass-file", "", "file with the passphrase that encrypts the key, or - for the standard input")
plain := fs.Bool("plain", false, "write the key without encryption, as text anyone who reads the file can use")
if err := parse(fs, args); err != nil {
return err
}
switch {
case *out == "":
return errors.New("author keygen: -out is required")
case *plain == (*passFile != ""):
return errors.New("author keygen: give -pass-file, to encrypt the key, or -plain, to write it as text, and not both")
}
if err := checkNew(*out); err != nil {
return err
}
var pass string
if !*plain {
var err error
if pass, err = readPass("author keygen", *passFile, stdin); err != nil {
return err
}
}
k, err := authorkey.Generate()
if err != nil {
return err
}
defer k.Clear()
err = writeAtomic(*out, func(w io.Writer) error {
if *plain {
b := authorkey.Marshal(k)
defer clear(b)
_, err := w.Write(b)
return err
}
return authorkey.Encrypt(w, k, pass)
})
if err != nil {
return err
}
pub, err := authorkey.PublicString(k.Public())
if err != nil {
return err
}
fmt.Fprintln(stdout, pub)
secret := "keep it, and its passphrase, secret"
if *plain {
secret = "it is not encrypted: keep the file secret"
}
fmt.Fprintf(stderr, "Secret key written to %s: %s. The line above is the public key: give it to whoever must know your signature.\n", *out, secret)
return nil
}
func authorPublic(args []string, stdout io.Writer, stdin io.Reader) error {
fs := newFlags("author public")
key := fs.String("key", "", "file with the secret key")
passFile := fs.String("pass-file", "", "file with the passphrase of an encrypted key, or - for the standard input")
if err := parse(fs, args); err != nil {
return err
}
if *key == "" {
return errors.New("author public: -key is required")
}
k, err := loadAuthorKey("author public", *key, *passFile, stdin)
if err != nil {
return err
}
defer k.Clear()
pub, err := authorkey.PublicString(k.Public())
if err != nil {
return err
}
fmt.Fprintln(stdout, pub)
return nil
}

Powered by TurnKey Linux.