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_test
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
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
|
|
|
|
"fmt"
|
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
|
|
|
|
"strings"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
|
|
"g.activething.com/go/DateKeys/authorkey"
|
|
|
|
|
|
"g.activething.com/go/DateKeys/codec/bech32"
|
|
|
|
|
|
"g.activething.com/go/DateKeys/internal/ed25519strict"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
func TestStrings(t *testing.T) {
|
|
|
|
|
|
seed := bytes.Repeat([]byte{7}, 32)
|
|
|
|
|
|
k, err := authorkey.NewFromSeed(seed)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
pub, err := authorkey.PublicString(k.Public())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(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
|
|
|
|
secret := k.Secret()
|
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
|
|
|
|
if len(pub) != authorkey.PublicLength || !strings.HasPrefix(pub, "dkauthor1") {
|
|
|
|
|
|
t.Errorf("public %q", pub)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(secret) != authorkey.SecretLength || !strings.HasPrefix(secret, "DKAUTHOR-SECRET-KEY-1") {
|
|
|
|
|
|
t.Errorf("secret %q", secret)
|
|
|
|
|
|
}
|
|
|
|
|
|
back, err := authorkey.ParseSecret(secret)
|
|
|
|
|
|
if err != nil || !bytes.Equal(back.Public(), k.Public()) {
|
|
|
|
|
|
t.Fatalf("ParseSecret: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
a, err := authorkey.ParsePublic(pub)
|
|
|
|
|
|
if err != nil || !bytes.Equal(a, k.Public()) {
|
|
|
|
|
|
t.Fatalf("ParsePublic: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
msg := []byte("datekeys:dkc3:author-signature:v1")
|
|
|
|
|
|
if !ed25519strict.Verify(a, msg, k.Sign(msg)) {
|
|
|
|
|
|
t.Error("the signature does not verify")
|
|
|
|
|
|
}
|
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
|
|
|
|
// Printing a key, or an options struct that holds one, never shows the
|
|
|
|
|
|
// secret.
|
|
|
|
|
|
for _, s := range []string{fmt.Sprint(k), fmt.Sprintf("%v %+v %#v %s", k, k, k, k), fmt.Sprintf("%+v", struct{ K *authorkey.Key }{k})} {
|
|
|
|
|
|
if strings.Contains(s, secret[len("DKAUTHOR-SECRET-KEY-1"):]) {
|
|
|
|
|
|
t.Fatalf("the secret key is printed: %s", s)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
k.Clear()
|
|
|
|
|
|
if !bytes.Equal(k.Public(), make([]byte, 32)) {
|
|
|
|
|
|
t.Error("Clear leaves the key")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestParseRejects(t *testing.T) {
|
|
|
|
|
|
k, _ := authorkey.Generate()
|
|
|
|
|
|
pub, _ := authorkey.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
|
|
|
|
secret := k.Secret()
|
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
|
|
|
|
short, _ := bech32.Encode("dkauthor", make([]byte, 31))
|
|
|
|
|
|
other, _ := bech32.Encode("dkauthoz", k.Public())
|
|
|
|
|
|
last := "q"
|
|
|
|
|
|
if pub[66] == 'q' {
|
|
|
|
|
|
last = "p"
|
|
|
|
|
|
}
|
|
|
|
|
|
identity := make([]byte, 32)
|
|
|
|
|
|
identity[0] = 1
|
|
|
|
|
|
small, _ := authorkey.PublicString(identity)
|
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
|
|
|
|
// y = 2 is canonical, and (y² − 1)/(d·y² + 1) is not a square: no point.
|
|
|
|
|
|
two := make([]byte, 32)
|
|
|
|
|
|
two[0] = 2
|
|
|
|
|
|
offCurve, _ := authorkey.PublicString(two)
|
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
|
|
|
|
for _, c := range []struct{ s, want string }{
|
|
|
|
|
|
{strings.ToUpper(pub), "lower case"},
|
|
|
|
|
|
{pub[:66] + last, "checksum"},
|
|
|
|
|
|
{short, "characters"},
|
|
|
|
|
|
{other, "is not a public key"},
|
|
|
|
|
|
{small, "small order"},
|
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
|
|
|
|
{offCurve, "not a point of the curve"},
|
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
|
|
|
|
} {
|
|
|
|
|
|
if _, err := authorkey.ParsePublic(c.s); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
|
|
|
|
t.Errorf("ParsePublic(%q) = %v, want %q", c.s, err, c.want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, c := range []struct{ s, want string }{
|
|
|
|
|
|
{strings.ToLower(secret), "upper case"},
|
|
|
|
|
|
{secret[:78], "characters"},
|
|
|
|
|
|
{"DKAUTHOR-SECRET-KEY-1" + strings.Repeat("Q", 58), "checksum"},
|
|
|
|
|
|
} {
|
|
|
|
|
|
if _, err := authorkey.ParseSecret(c.s); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
|
|
|
|
t.Errorf("ParseSecret(%q) = %v, want %q", c.s, err, c.want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := authorkey.PublicString(make([]byte, 31)); err == nil {
|
|
|
|
|
|
t.Error("PublicString takes 31 bytes")
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := authorkey.NewFromSeed(make([]byte, 31)); err == nil {
|
|
|
|
|
|
t.Error("NewFromSeed takes 31 bytes")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestFiles(t *testing.T) {
|
|
|
|
|
|
k, _ := authorkey.Generate()
|
|
|
|
|
|
var enc bytes.Buffer
|
|
|
|
|
|
if err := authorkey.Encrypt(&enc, k, "contraseña larga"); err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if !bytes.HasPrefix(enc.Bytes(), []byte("age-encryption.org/v1\n-> scrypt ")) || !bytes.Contains(enc.Bytes(), []byte(" 16\n")) {
|
|
|
|
|
|
t.Errorf("not age scrypt with a work factor of 16: %q", enc.Bytes()[:60])
|
|
|
|
|
|
}
|
|
|
|
|
|
got, err := authorkey.Read(bytes.NewReader(enc.Bytes()), "contraseña larga")
|
|
|
|
|
|
if err != nil || !bytes.Equal(got.Public(), k.Public()) {
|
|
|
|
|
|
t.Fatalf("Read: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := authorkey.Read(bytes.NewReader(enc.Bytes()), "otra"); err == nil {
|
|
|
|
|
|
t.Error("another passphrase opens the file")
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := authorkey.Read(bytes.NewReader(enc.Bytes()), ""); err == nil || !strings.Contains(err.Error(), "needs its passphrase") {
|
|
|
|
|
|
t.Errorf("no passphrase: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := authorkey.Encrypt(&enc, k, ""); err == nil {
|
|
|
|
|
|
t.Error("Encrypt takes an empty passphrase")
|
|
|
|
|
|
}
|
|
|
|
|
|
plain := authorkey.Marshal(k)
|
|
|
|
|
|
if got, err := authorkey.Read(bytes.NewReader(plain), ""); err != nil || !bytes.Equal(got.Public(), k.Public()) {
|
|
|
|
|
|
t.Fatalf("Read of a plain file: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, c := range []struct{ file, want string }{
|
|
|
|
|
|
{"# nothing\n\n", "no secret key"},
|
|
|
|
|
|
{string(plain) + k.String() + "\n", "one secret key"},
|
|
|
|
|
|
{"age1notakey\n", "characters"},
|
|
|
|
|
|
{strings.Repeat("#", 64<<10+1), "more than"},
|
|
|
|
|
|
} {
|
|
|
|
|
|
if _, err := authorkey.Read(strings.NewReader(c.file), ""); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
|
|
|
|
t.Errorf("Read(%.20q) = %v, want %q", c.file, err, c.want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|