Signature plan, step 2: author keys
Package authorkey writes and reads the keys of the author signature of
alg 1 (spec v0.11 draft, 29.12): an Ed25519 seed as
DKAUTHOR-SECRET-KEY-1..., 79 characters in upper case, and its public key
as dkauthor1..., 67 in lower case, with the Bech32 of the module. A
public key in another case, of another length or prefix, or one that the
strict profile could never accept, is refused.
A key file is the line of the secret key with a comment of its public
key, and by default it is encrypted with age and a passphrase, scrypt
with a work factor of 16, 64 MiB, which a phone can afford.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
// Package authorkey handles the keys of the author signature of alg 1 (spec
|
|
|
|
|
// v0.11, §29.12): an Ed25519 seed of 32 bytes, written in Bech32 as
|
|
|
|
|
// DKAUTHOR-SECRET-KEY-1…, 79 characters in upper case, whose public key A is
|
|
|
|
|
// written dkauthor1…, 67 characters in lower case. A file of a secret key
|
|
|
|
|
// holds that line and, by default, is encrypted with age and a passphrase,
|
|
|
|
|
// scrypt with a work factor of WorkFactor.
|
|
|
|
|
//
|
|
|
|
|
// A signature of alg 1 proves that someone with the secret key signed, not
|
|
|
|
|
// who holds it: whoever opens a capsule knows a public key only through
|
|
|
|
|
// another channel (§29.12).
|
|
|
|
|
package authorkey
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"bytes"
|
|
|
|
|
"crypto/ed25519"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"filippo.io/age"
|
|
|
|
|
"g.activething.com/go/DateKeys/codec/bech32"
|
|
|
|
|
"g.activething.com/go/DateKeys/internal/ed25519strict"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// PublicPrefix is the Bech32 prefix of a public key, in lower case.
|
|
|
|
|
PublicPrefix = "dkauthor"
|
|
|
|
|
// SecretPrefix is the Bech32 prefix of a secret key, in upper case.
|
|
|
|
|
SecretPrefix = "DKAUTHOR-SECRET-KEY-"
|
|
|
|
|
// PublicLength and SecretLength are the lengths of their Bech32 strings.
|
|
|
|
|
PublicLength = 67
|
|
|
|
|
SecretLength = 79
|
|
|
|
|
// WorkFactor is the scrypt work factor, logN, of an encrypted key file:
|
|
|
|
|
// 64 MiB, which a phone can afford, where age's 18 would take 256 MiB.
|
|
|
|
|
WorkFactor = 16
|
|
|
|
|
// maxFile bounds a key file.
|
|
|
|
|
maxFile = 64 << 10
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Key is a secret key of an author.
|
|
|
|
|
type Key struct {
|
|
|
|
|
priv ed25519.PrivateKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate returns a new key from the CSPRNG.
|
|
|
|
|
func Generate() (*Key, error) {
|
|
|
|
|
_, priv, err := ed25519.GenerateKey(nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &Key{priv}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewFromSeed returns the key of a seed of 32 bytes.
|
|
|
|
|
func NewFromSeed(seed []byte) (*Key, error) {
|
|
|
|
|
if len(seed) != ed25519.SeedSize {
|
Review fixes: addresses, safe integers, the note on screen, -expect-author, author keys
CheckURI works on the raw authority, as an HTTP client reads it: no percent
signs, userinfo or backslashes, a host of letters, digits and hyphens or a
public IP literal, a port from 1 to 65535, and Host returns that host. The
integers of the locator stop at 2^53 - 1, and Info.Extension refuses what
ParseInfo would. extension.Standard validates datekeys.capsule through
locator.Standard, and Info.OpenLocator ties the locator to the round of its
own DateKey.
inspect shows the public note as text of the creator, with its prefix and
wrapping and the warning, and says when a note is unusable. encrypt -note
warns that it is public. decrypt -expect-author fails before the release
is requested when the capsule is not format 3. Author key files are read
with the work factor of the spec as their maximum, the passphrase is not
read from a terminal, and two copies of secrets are cleared.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
return nil, fmt.Errorf("authorkey: a seed has %d bytes, not %d", len(seed), ed25519.SeedSize)
|
Signature plan, step 2: author keys
Package authorkey writes and reads the keys of the author signature of
alg 1 (spec v0.11 draft, 29.12): an Ed25519 seed as
DKAUTHOR-SECRET-KEY-1..., 79 characters in upper case, and its public key
as dkauthor1..., 67 in lower case, with the Bech32 of the module. A
public key in another case, of another length or prefix, or one that the
strict profile could never accept, is refused.
A key file is the line of the secret key with a comment of its public
key, and by default it is encrypted with age and a passphrase, scrypt
with a work factor of 16, 64 MiB, which a phone can afford.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
}
|
|
|
|
|
return &Key{ed25519.NewKeyFromSeed(seed)}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Public returns the public key A, 32 bytes.
|
|
|
|
|
func (k *Key) Public() []byte { return bytes.Clone(k.priv[32:]) }
|
|
|
|
|
|
|
|
|
|
// Sign returns the Ed25519 signature of msg, 64 bytes.
|
|
|
|
|
func (k *Key) Sign(msg []byte) []byte { return ed25519.Sign(k.priv, msg) }
|
|
|
|
|
|
|
|
|
|
// Clear wipes the key; it cannot sign afterwards.
|
|
|
|
|
func (k *Key) Clear() { clear(k.priv) }
|
|
|
|
|
|
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>
6 days ago
|
|
|
// Secret returns the secret key, DKAUTHOR-SECRET-KEY-1…. Only a key file
|
|
|
|
|
// should ever hold it.
|
|
|
|
|
func (k *Key) Secret() string {
|
Signature plan, step 2: author keys
Package authorkey writes and reads the keys of the author signature of
alg 1 (spec v0.11 draft, 29.12): an Ed25519 seed as
DKAUTHOR-SECRET-KEY-1..., 79 characters in upper case, and its public key
as dkauthor1..., 67 in lower case, with the Bech32 of the module. A
public key in another case, of another length or prefix, or one that the
strict profile could never accept, is refused.
A key file is the line of the secret key with a comment of its public
key, and by default it is encrypted with age and a passphrase, scrypt
with a work factor of 16, 64 MiB, which a phone can afford.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
s, err := bech32.Encode(SecretPrefix, k.priv.Seed())
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err) // the prefix and the length are fixed
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
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>
6 days ago
|
|
|
// String hides the secret key, so that a %v in a log or in an error never
|
|
|
|
|
// prints it; Secret returns it.
|
|
|
|
|
func (k *Key) String() string { return SecretPrefix + "1… (hidden)" }
|
|
|
|
|
|
|
|
|
|
// GoString hides the secret key for %#v, as String does for %v.
|
|
|
|
|
func (k *Key) GoString() string { return k.String() }
|
|
|
|
|
|
Signature plan, step 2: author keys
Package authorkey writes and reads the keys of the author signature of
alg 1 (spec v0.11 draft, 29.12): an Ed25519 seed as
DKAUTHOR-SECRET-KEY-1..., 79 characters in upper case, and its public key
as dkauthor1..., 67 in lower case, with the Bech32 of the module. A
public key in another case, of another length or prefix, or one that the
strict profile could never accept, is refused.
A key file is the line of the secret key with a comment of its public
key, and by default it is encrypted with age and a passphrase, scrypt
with a work factor of 16, 64 MiB, which a phone can afford.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
// PublicString returns the public key pub as dkauthor1….
|
|
|
|
|
func PublicString(pub []byte) (string, error) {
|
|
|
|
|
if len(pub) != ed25519.PublicKeySize {
|
|
|
|
|
return "", fmt.Errorf("authorkey: a public key has %d bytes, not %d", ed25519.PublicKeySize, len(pub))
|
|
|
|
|
}
|
|
|
|
|
return bech32.Encode(PublicPrefix, pub)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParsePublic returns the public key of a string dkauthor1…: lower case, of
|
|
|
|
|
// PublicLength characters, with the right prefix and padding, and a key that
|
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>
6 days ago
|
|
|
// the strict profile could accept: canonical, a point of the curve and not of
|
|
|
|
|
// small order.
|
Signature plan, step 2: author keys
Package authorkey writes and reads the keys of the author signature of
alg 1 (spec v0.11 draft, 29.12): an Ed25519 seed as
DKAUTHOR-SECRET-KEY-1..., 79 characters in upper case, and its public key
as dkauthor1..., 67 in lower case, with the Bech32 of the module. A
public key in another case, of another length or prefix, or one that the
strict profile could never accept, is refused.
A key file is the line of the secret key with a comment of its public
key, and by default it is encrypted with age and a passphrase, scrypt
with a work factor of 16, 64 MiB, which a phone can afford.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
func ParsePublic(s string) ([]byte, error) {
|
|
|
|
|
if len(s) != PublicLength {
|
|
|
|
|
return nil, fmt.Errorf("authorkey: a public key has %d characters, not %d", PublicLength, len(s))
|
|
|
|
|
}
|
|
|
|
|
if strings.ToLower(s) != s {
|
|
|
|
|
return nil, errors.New("authorkey: a public key is written in lower case")
|
|
|
|
|
}
|
|
|
|
|
hrp, data, err := bech32.Decode(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("authorkey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if hrp != PublicPrefix || len(data) != ed25519.PublicKeySize {
|
|
|
|
|
return nil, fmt.Errorf("authorkey: %q is not a public key %s1…", s, PublicPrefix)
|
|
|
|
|
}
|
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>
6 days ago
|
|
|
if !ed25519strict.Canonical(data) || !ed25519strict.OnCurve(data) || ed25519strict.SmallOrder(data) {
|
|
|
|
|
return nil, errors.New("authorkey: the public key is not canonical, not a point of the curve or of small order: no signature would verify")
|
Signature plan, step 2: author keys
Package authorkey writes and reads the keys of the author signature of
alg 1 (spec v0.11 draft, 29.12): an Ed25519 seed as
DKAUTHOR-SECRET-KEY-1..., 79 characters in upper case, and its public key
as dkauthor1..., 67 in lower case, with the Bech32 of the module. A
public key in another case, of another length or prefix, or one that the
strict profile could never accept, is refused.
A key file is the line of the secret key with a comment of its public
key, and by default it is encrypted with age and a passphrase, scrypt
with a work factor of 16, 64 MiB, which a phone can afford.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
}
|
|
|
|
|
return data, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseSecret returns the key of a string DKAUTHOR-SECRET-KEY-1…: upper case,
|
|
|
|
|
// of SecretLength characters, with the right prefix and padding.
|
|
|
|
|
func ParseSecret(s string) (*Key, error) {
|
|
|
|
|
if len(s) != SecretLength {
|
|
|
|
|
return nil, fmt.Errorf("authorkey: a secret key has %d characters, not %d", SecretLength, len(s))
|
|
|
|
|
}
|
|
|
|
|
if strings.ToUpper(s) != s {
|
|
|
|
|
return nil, errors.New("authorkey: a secret key is written in upper case")
|
|
|
|
|
}
|
|
|
|
|
hrp, data, err := bech32.Decode(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("authorkey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer clear(data)
|
|
|
|
|
if hrp != SecretPrefix || len(data) != ed25519.SeedSize {
|
|
|
|
|
return nil, fmt.Errorf("authorkey: not a secret key %s1…", SecretPrefix)
|
|
|
|
|
}
|
|
|
|
|
return NewFromSeed(data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Marshal returns the file of a key without encryption: a comment with the
|
|
|
|
|
// public key and the line of the secret key.
|
|
|
|
|
func Marshal(k *Key) []byte {
|
|
|
|
|
pub, _ := PublicString(k.Public())
|
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>
6 days ago
|
|
|
return []byte("# public key: " + pub + "\n" + k.Secret() + "\n")
|
Signature plan, step 2: author keys
Package authorkey writes and reads the keys of the author signature of
alg 1 (spec v0.11 draft, 29.12): an Ed25519 seed as
DKAUTHOR-SECRET-KEY-1..., 79 characters in upper case, and its public key
as dkauthor1..., 67 in lower case, with the Bech32 of the module. A
public key in another case, of another length or prefix, or one that the
strict profile could never accept, is refused.
A key file is the line of the secret key with a comment of its public
key, and by default it is encrypted with age and a passphrase, scrypt
with a work factor of 16, 64 MiB, which a phone can afford.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Encrypt writes the file of a key encrypted with age and passphrase,
|
|
|
|
|
// scrypt with a work factor of WorkFactor.
|
|
|
|
|
func Encrypt(w io.Writer, k *Key, passphrase string) error {
|
|
|
|
|
if passphrase == "" {
|
|
|
|
|
return errors.New("authorkey: an empty passphrase")
|
|
|
|
|
}
|
|
|
|
|
r, err := age.NewScryptRecipient(passphrase)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
r.SetWorkFactor(WorkFactor)
|
|
|
|
|
plain := Marshal(k)
|
|
|
|
|
defer clear(plain)
|
|
|
|
|
aw, err := age.Encrypt(w, r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if _, err := aw.Write(plain); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return aw.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read returns the key of a file: encrypted with age and a passphrase, as
|
|
|
|
|
// Encrypt writes it, or plain, as Marshal does, with one line of a secret key
|
|
|
|
|
// and, besides it, only empty lines and comments that start with '#'. The
|
|
|
|
|
// passphrase is needed only for an encrypted file.
|
|
|
|
|
func Read(r io.Reader, passphrase string) (*Key, error) {
|
|
|
|
|
b, err := io.ReadAll(io.LimitReader(r, maxFile+1))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer clear(b)
|
|
|
|
|
if len(b) > maxFile {
|
|
|
|
|
return nil, fmt.Errorf("authorkey: a key file of more than %d bytes", maxFile)
|
|
|
|
|
}
|
|
|
|
|
if bytes.HasPrefix(b, []byte("age-encryption.org/v1\n")) {
|
|
|
|
|
if passphrase == "" {
|
|
|
|
|
return nil, errors.New("authorkey: the key file is encrypted: it needs its passphrase")
|
|
|
|
|
}
|
|
|
|
|
id, err := age.NewScryptIdentity(passphrase)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
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>
6 days ago
|
|
|
// A work factor above ours would let a hostile file ask for gigabytes
|
|
|
|
|
// of memory, so it is refused. A lower one is a weaker file that the
|
|
|
|
|
// person made with another tool, and it opens (§29.12 fixes only the
|
|
|
|
|
// default).
|
Review fixes: addresses, safe integers, the note on screen, -expect-author, author keys
CheckURI works on the raw authority, as an HTTP client reads it: no percent
signs, userinfo or backslashes, a host of letters, digits and hyphens or a
public IP literal, a port from 1 to 65535, and Host returns that host. The
integers of the locator stop at 2^53 - 1, and Info.Extension refuses what
ParseInfo would. extension.Standard validates datekeys.capsule through
locator.Standard, and Info.OpenLocator ties the locator to the round of its
own DateKey.
inspect shows the public note as text of the creator, with its prefix and
wrapping and the warning, and says when a note is unusable. encrypt -note
warns that it is public. decrypt -expect-author fails before the release
is requested when the capsule is not format 3. Author key files are read
with the work factor of the spec as their maximum, the passphrase is not
read from a terminal, and two copies of secrets are cleared.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
id.SetMaxWorkFactor(WorkFactor)
|
Signature plan, step 2: author keys
Package authorkey writes and reads the keys of the author signature of
alg 1 (spec v0.11 draft, 29.12): an Ed25519 seed as
DKAUTHOR-SECRET-KEY-1..., 79 characters in upper case, and its public key
as dkauthor1..., 67 in lower case, with the Bech32 of the module. A
public key in another case, of another length or prefix, or one that the
strict profile could never accept, is refused.
A key file is the line of the secret key with a comment of its public
key, and by default it is encrypted with age and a passphrase, scrypt
with a work factor of 16, 64 MiB, which a phone can afford.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
ar, err := age.Decrypt(bytes.NewReader(b), id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("authorkey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
plain, err := io.ReadAll(io.LimitReader(ar, maxFile+1))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("authorkey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer clear(plain)
|
|
|
|
|
return parseFile(plain)
|
|
|
|
|
}
|
|
|
|
|
return parseFile(b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseFile(b []byte) (*Key, error) {
|
|
|
|
|
var key *Key
|
|
|
|
|
sc := bufio.NewScanner(bytes.NewReader(b))
|
|
|
|
|
for sc.Scan() {
|
|
|
|
|
line := strings.TrimSpace(sc.Text())
|
|
|
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if key != nil {
|
|
|
|
|
return nil, errors.New("authorkey: a key file holds one secret key")
|
|
|
|
|
}
|
|
|
|
|
k, err := ParseSecret(line)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
key = k
|
|
|
|
|
}
|
|
|
|
|
if err := sc.Err(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if key == nil {
|
|
|
|
|
return nil, errors.New("authorkey: no secret key in the file")
|
|
|
|
|
}
|
|
|
|
|
return key, nil
|
|
|
|
|
}
|