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/locator/locator.go

1003 lines
31 KiB

// Package locator implements the extension datekeys.capsule of a .dkk and
// what it points to (spec v0.11, §44.1): the data of the extension, which
// says what a key's capsule is and when it opens; the locator, an age file
// sealed with tlock for that date, which says where the capsule is; and the
// envelope, the .dkc encrypted with age and split into a header, which the
// locator carries, and a rest, the only thing that is kept outside, alone or
// inside another file.
//
// It does not download anything: a reader fetches the rest only when the
// person asks, after showing her the host or the CID (§44.1), and gives it
// to OpenEnvelope.
package locator
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/netip"
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
"strconv"
"strings"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/agewrap"
"g.activething.com/go/DateKeys/codec"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
)
// Limits of §44.1.
const (
// MaxAddresses is the most addresses of a locator.
MaxAddresses = 8
// MaxURILen is the longest address, in bytes.
MaxURILen = 1024
// MaxHeaderLen is the longest header of an envelope.
MaxHeaderLen = 1024
// Block is the unit of the plaintext of a locator: it measures exactly
// 4096 bytes, or the least multiple of 4096 that holds it.
Block = 4096
// maxSealed bounds a sealed locator that a reader decrypts.
maxSealed = 1 << 20
)
// Info is the data of the extension datekeys.capsule (spec §44.1).
type Info struct {
// Note is the copy of the public note of the capsule, "" for none.
Note string
// DateKey is the DateKey of the capsule: it says when it opens.
DateKey datekey.DateKey
// Sealed is the age file of the locator, sealed with tlock for the round
// of DateKey, nil for none.
Sealed []byte
}
// Extension returns the extension for the noncritical array of a .dkk.
func (i *Info) Extension() (extension.Extension, error) {
if d, err := datekey.Parse(i.DateKey.Compact()); err != nil || d != i.DateKey {
return extension.Extension{}, errors.New("locator: Info.DateKey is not a canonical DateKey")
}
// A writer writes the DateKey of a capsule it wrote: of a profile that
// this module pins, whose chain the check of the locator then compares.
if reg, err := profile.Default(); err != nil {
return extension.Extension{}, err
} else if _, ok := reg.Lookup(i.DateKey.ProfileID); !ok {
return extension.Extension{}, fmt.Errorf("locator: Info.DateKey is of the profile %q, which this module does not pin", i.DateKey.ProfileID)
}
if i.Sealed != nil && (len(i.Sealed) < 1 || len(i.Sealed) > maxSealed) {
return extension.Extension{}, fmt.Errorf("locator: a sealed locator of %d bytes, not 1 to %d", len(i.Sealed), maxSealed)
}
if i.Note != "" {
if err := extension.CheckNote(i.Note); err != nil {
return extension.Extension{}, err
}
}
var e codec.Encoder
pairs := 1
if i.Note != "" {
pairs++
}
if i.Sealed != nil {
pairs++
}
e.Map(pairs)
if i.Note != "" {
e.Uint(0)
e.Text(i.Note)
}
e.Uint(1)
e.Text(i.DateKey.Compact())
if i.Sealed != nil {
e.Uint(2)
e.Bstr(i.Sealed)
}
data, err := e.Out()
if err != nil {
return extension.Extension{}, 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
x, err := extension.New(extension.CapsuleID, 1, data)
if err != nil {
return extension.Extension{}, err
}
// Spec §72: the encoder reads what it writes with the rules of a reader,
// which also ties the locator to the round of DateKey.
if _, err := ParseInfo(x); err != nil {
return extension.Extension{}, fmt.Errorf("locator: self-check: a reader rejects this extension: %v", err)
}
return x, nil
}
// ParseInfo reads the data of a datekeys.capsule extension. A failure makes
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 extension unusable, not the .dkk (spec §54): its only normative code is
// ErrExtensionDataInvalid. A locator must be an age file with one tlock
// stanza, for the round of the DateKey; its chain is checked when it opens.
func ParseInfo(x extension.Extension) (*Info, error) {
if x.ID != extension.CapsuleID || x.Version != 1 || x.Data == nil {
return nil, fmt.Errorf("locator: not datekeys.capsule version 1 with data: %w", datekeys.ErrExtensionDataInvalid)
}
var i Info
var dk string
decode := func(d *codec.Decoder) error {
pairs, err := d.Map(3)
if err != nil {
return err
}
var seen uint
for range pairs {
k, err := d.Key()
if err != nil {
return err
}
switch k {
case 0:
i.Note, err = d.Text(extension.MaxNoteLen)
if err == nil {
err = extension.CheckNote(i.Note)
}
case 1:
dk, err = d.Text(1024)
case 2:
i.Sealed, err = d.Bstr(1, maxSealed)
default:
return fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
}
if err != nil {
return fmt.Errorf("key %d: %w", k, err)
}
seen |= 1 << k
}
if seen&2 == 0 {
return fmt.Errorf("key 1 is missing: %w", datekeys.ErrNonCanonicalCBOR)
}
return d.EndMap()
}
encode := func(e *codec.Encoder) {
pairs := 1
if i.Note != "" {
pairs++
}
if i.Sealed != nil {
pairs++
}
e.Map(pairs)
if i.Note != "" {
e.Uint(0)
e.Text(i.Note)
}
e.Uint(1)
e.Text(dk)
if i.Sealed != nil {
e.Uint(2)
e.Bstr(i.Sealed)
}
}
if err := codec.Unmarshal(x.Data, decode, encode); err != nil {
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 nil, fmt.Errorf("locator: datekeys.capsule: %v: %w", err, datekeys.ErrExtensionDataInvalid)
}
d, err := datekey.Parse(dk)
if err != nil || d.Compact() != dk {
return nil, fmt.Errorf("locator: compact_datekey is not a canonical DateKey: %w", datekeys.ErrExtensionDataInvalid)
}
i.DateKey = d
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 i.Sealed != nil {
if err := checkSealed(i.Sealed, d); err != nil {
return nil, fmt.Errorf("locator: %v: %w", err, datekeys.ErrExtensionDataInvalid)
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 &i, nil
}
// checkSealed checks the form of a sealed locator against the DateKey d of
// its extension, as far as it can be checked before the date (spec §44.1):
// an age file with one tlock stanza, whose arguments are the round of d in
// decimal and a chain hash in lower-case hexadecimal (§28.1), the chain of
// the profile of d when this module pins it; and a body that holds a
// plaintext of 4096 bytes or a multiple, as every locator has. A locator of
// another round or chain never opens, and one of another length is not a
// locator.
func checkSealed(sealed []byte, d datekey.DateKey) error {
st, err := agewrap.Stanzas(bytes.NewReader(sealed))
if err != nil || len(st) != 1 || st[0].Type != agewrap.StanzaTLock || len(st[0].Args) != 2 || st[0].Args[0] != strconv.FormatUint(d.Round, 10) {
return fmt.Errorf("the locator is not an age file with one tlock stanza for round %d, the one of its DateKey", d.Round)
}
chain := st[0].Args[1]
if len(chain) != 64 || strings.Trim(chain, "0123456789abcdef") != "" {
return errors.New("the tlock stanza of the locator has no chain hash in lower-case hexadecimal")
}
if reg, err := profile.Default(); err == nil {
if p, ok := reg.Lookup(d.ProfileID); ok && chain != hex.EncodeToString(p.ChainHash[:]) {
return fmt.Errorf("the locator is sealed for the chain %s, not for the one of the profile %s of its DateKey", chain, d.ProfileID)
}
}
// agewrap.Stanzas read the header up to its MAC line, so it ends.
end, err := headerEnd(sealed)
if err != nil {
return err
}
if end == len(sealed) {
return errors.New("the locator is an age header without a body")
}
if !sealedBodyLength(uint64(len(sealed) - end)) {
return fmt.Errorf("the body of the locator is %d bytes, which no plaintext of 4096 bytes or a multiple gives", len(sealed)-end)
}
return nil
}
// sealedBodyLength reports whether n bytes after the age header are the
// body of a plaintext of 4096·k bytes, k from 1: the nonce of 16 bytes and
// the plaintext in chunks of 64 KiB, each with its tag of 16 bytes.
func sealedBodyLength(n uint64) bool {
for p := uint64(Block); p <= maxSealed; p += Block {
if n == 16+p+16*((p+64<<10-1)/(64<<10)) {
return true
}
}
return false
}
// Address says where the rest of the envelope is.
type Address struct {
// URI is ASCII, RFC 3986, with the scheme https or ipfs (a CID v1), and
// no userinfo.
URI string
// Offset is the byte of the resource where the rest starts, 0 when the
// rest is the whole resource.
Offset uint64
}
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
// CheckURI checks an address with the rules of spec §44.1: ASCII of RFC 3986,
// with its percent signs followed by two hexadecimal digits, the scheme
// https or ipfs in lower case, no "." or ".." segment in its path, and the
// host or the CID that checkHost and isCIDv1 accept.
func CheckURI(uri string) error {
if uri == "" || len(uri) > MaxURILen {
return fmt.Errorf("locator: an address of %d bytes, not 1 to %d", len(uri), MaxURILen)
}
for i := 0; i < len(uri); i++ {
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
c := uri[i]
switch {
case c == '%':
if i+2 >= len(uri) || !isHex(uri[i+1]) || !isHex(uri[i+2]) {
return errors.New("locator: an address with a percent sign not followed by two hexadecimal digits")
}
case !uriChar(c):
return fmt.Errorf("locator: an address with the character %q, which RFC 3986 does not allow", c)
}
}
scheme, host, err := splitAuthority(uri)
if err != nil {
return 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
if dotSegment(uri) {
return errors.New("locator: an address with a \".\" or \"..\" segment in its path")
}
switch scheme {
case "https":
return checkHost(host)
case "ipfs":
if !isCIDv1(host) {
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 errors.New("locator: an ipfs address without a CID v1 in base32")
}
return nil
}
return fmt.Errorf("locator: the scheme %q: only https and ipfs", scheme)
}
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
// uriChar reports whether c may appear in a URI of RFC 3986, a percent sign
// apart: the unreserved characters, gen-delims and sub-delims.
func uriChar(c byte) bool {
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.IndexByte("-._~:/?#[]@!$&'()*+,;=", c) >= 0
}
func isHex(c byte) bool {
return c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'
}
// dotSegment reports whether the path of uri has a segment "." or "..",
// written or percent-encoded: a client or a gateway that resolves it would
// ask for something other than what the address shows, as another CID behind
// an ipfs address.
func dotSegment(uri string) bool {
_, rest, _ := strings.Cut(uri, "://")
i := strings.IndexByte(rest, '/')
if i < 0 {
return false
}
path := rest[i:]
if j := strings.IndexAny(path, "?#"); j >= 0 {
path = path[:j]
}
for _, s := range strings.Split(path, "/") {
s = strings.ReplaceAll(strings.ReplaceAll(s, "%2e", "."), "%2E", ".")
if s == "." || s == ".." {
return true
}
}
return false
}
// splitAuthority returns the scheme and the raw host of an address, without
// decoding anything: a percent sign in the authority, userinfo and a
// malformed port are refused, so that the host a reader shows is the host an
// HTTP client would use (spec v0.11, §44.1).
func splitAuthority(uri string) (scheme, host string, err error) {
scheme, rest, ok := strings.Cut(uri, "://")
if !ok || scheme == "" {
return "", "", errors.New("locator: an address without a scheme and ://")
}
authority := rest
if i := strings.IndexAny(rest, "/?#"); i >= 0 {
authority = rest[:i]
}
if strings.ContainsAny(authority, "%@\\") {
return "", "", errors.New("locator: an address with a percent sign, userinfo or a backslash in its authority")
}
host = authority
if scheme == "https" {
if strings.HasPrefix(authority, "[") {
end := strings.Index(authority, "]")
if end < 0 {
return "", "", errors.New("locator: an address with an unclosed IPv6 literal")
}
host = authority[:end+1]
if tail := authority[end+1:]; tail != "" {
if err := checkPort(tail); err != nil {
return "", "", err
}
}
} else if h, port, found := strings.Cut(authority, ":"); found {
host = h
if err := checkPort(":" + port); err != nil {
return "", "", err
}
}
}
return scheme, host, nil
}
// checkPort checks the port of an authority, its ':' included: a number from 1
// to 65535 without leading zeros (spec v0.12, §44.1), so that a port is
// written in one way only.
func checkPort(s string) error {
if len(s) < 2 || s[0] != ':' || len(s) > 6 {
return errors.New("locator: an address with a malformed port")
}
n := 0
for _, c := range s[1:] {
if c < '0' || c > '9' {
return errors.New("locator: an address with a malformed port")
}
n = n*10 + int(c-'0')
}
if n < 1 || n > 65535 {
return errors.New("locator: an address with a port outside 1 to 65535")
}
if s[1] == '0' {
return errors.New("locator: an address with a port written with a leading zero")
}
return nil
}
// checkHost accepts a name of labels of 1 to 63 letters, digits and hyphens,
// separated by dots, none of which starts or ends with a hyphen, or an IP
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
// literal that is public: the spec forbids following a redirect to the
// others, and an address that starts there would defeat the same rule
// (§44.1). A name whose last label is numeric, or starts with 0x in either
// case, is refused unless it is a public IPv4 address in dotted decimal
// without leading zeros, the only form netip reads: some clients read the
// others, 0x7f000001 or 0177.0.0.1, as an IPv4 address too. So are the names
// that only resolve inside a machine or a local network, in either case:
// localhost, a name of one label, and the special-use names of localName.
func checkHost(host string) error {
if host == "" {
return errors.New("locator: an https address without a host")
}
if strings.HasPrefix(host, "[") {
// One pair of brackets: splitAuthority ends the literal at the
// first ']', and "[[2000::]" is not an IPv6 literal.
a, err := netip.ParseAddr(strings.TrimSuffix(strings.TrimPrefix(host, "["), "]"))
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 err != nil || !a.Is6() || a.Zone() != "" || !publicIP(a) {
return errors.New("locator: an https address with an IPv6 literal that is not public")
}
return nil
}
labels := strings.Split(host, ".")
for _, l := range labels {
if l == "" || len(l) > 63 || l[0] == '-' || l[len(l)-1] == '-' {
return errors.New("locator: an https address with a malformed host")
}
for i := 0; i < len(l); i++ {
c := l[i]
if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-') {
return errors.New("locator: an https address whose host is not letters, digits and hyphens: write its punycode form")
}
}
}
last := labels[len(labels)-1]
if allDigits(last) || strings.HasPrefix(strings.ToLower(last), "0x") {
a, err := netip.ParseAddr(host)
if err != nil || !a.Is4() || !publicIP(a) {
return errors.New("locator: an https address with a numeric host that is not a public IPv4 address")
}
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 nil
}
if len(labels) == 1 || localName(strings.ToLower(host)) {
return errors.New("locator: an https address with a name that only a machine or a local network resolves")
}
return nil
}
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
// localName reports whether the name, in lower case, is one of the
// special-use names that never resolve on the public Internet: localhost
// (RFC 6761), .local (RFC 6762), .home.arpa (RFC 8375), .internal, .invalid,
// .test, .example and .onion (RFC 7686), or below one of them.
func localName(name string) bool {
for _, s := range []string{"localhost", "local", "home.arpa", "internal", "invalid", "test", "example", "onion"} {
if name == s || strings.HasSuffix(name, "."+s) {
return true
}
}
return false
}
func allDigits(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
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
// The blocks of the IANA registries of special-purpose addresses that an
// address of a locator may not use (spec §44.1). An IPv6 address must also
// be a global unicast one, of 2000::/3.
var (
notPublic4 = prefixes("0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12",
"192.0.0.0/24", "192.0.2.0/24", "192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24",
"203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4")
global6 = prefixes("2000::/3")
notPublic6 = prefixes("2001::/23", "2001:db8::/32", "2002::/16", "3fff::/20")
)
func prefixes(s ...string) []netip.Prefix {
out := make([]netip.Prefix, len(s))
for i, p := range s {
out[i] = netip.MustParsePrefix(p)
}
return out
}
func inAny(a netip.Addr, ps []netip.Prefix) bool {
for _, p := range ps {
if p.Contains(a) {
return true
}
}
return false
}
// publicIP reports whether a is an address of the public Internet: an IPv4
// address outside the blocks of notPublic4, or an IPv6 address of 2000::/3
// outside those of notPublic6. An IPv6 address that holds an IPv4 one, mapped,
// compatible, of NAT64, 6to4 or Teredo, is not: it would reach the IPv4
// address without the check of an IPv4 address.
func publicIP(a netip.Addr) bool {
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 a.Is4() {
return !inAny(a, notPublic4)
}
return inAny(a, global6) && !inAny(a, notPublic6)
}
// isCIDv1 reports whether s is a CID v1 of at most 128 characters in
// canonical base32, which starts with 'b' (spec v0.12, §44.1): the alphabet
// in lower case, without padding, the bits left over set to zero, and
// decoded, minimal varints of at most 9 bytes, the version 1, a content codec
// and a multihash with a digest of at least one byte and of the length it
// gives, with nothing after it.
func isCIDv1(s string) bool {
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 len(s) < 2 || len(s) > 128 || s[0] != 'b' {
return false
}
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
var out []byte
var acc, bits uint
for i := 1; i < len(s); i++ {
c := s[i]
var v byte
switch {
case c >= 'a' && c <= 'z':
v = c - 'a'
case c >= '2' && c <= '7':
v = c - '2' + 26
default:
return false
}
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
acc, bits = acc<<5|uint(v), bits+5
if bits >= 8 {
bits -= 8
out = append(out, byte(acc>>bits))
acc &= 1<<bits - 1
}
}
// Canonical base32 without padding: the last character carries fewer
// than 5 bits beyond the last byte, all zero. A character more, even
// one of zero bits, is another encoding of the same bytes.
if bits >= 5 || acc != 0 {
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 false
}
version, out, ok := uvarint(out)
if !ok || version != 1 {
return false
}
if _, out, ok = uvarint(out); !ok { // the content codec
return false
}
if _, out, ok = uvarint(out); !ok { // the hash function
return false
}
n, out, ok := uvarint(out)
return ok && n > 0 && uint64(len(out)) == n
}
// uvarint reads an unsigned varint of multiformats, minimal and of at most 9
// bytes, from the start of b.
func uvarint(b []byte) (uint64, []byte, bool) {
var v uint64
for i := 0; i < len(b) && i < 9; i++ {
v |= uint64(b[i]&0x7f) << (7 * i)
if b[i]&0x80 == 0 {
if i > 0 && b[i] == 0 {
return 0, nil, false
}
return v, b[i+1:], true
}
}
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 0, nil, false
}
// Host returns what a reader shows before it downloads: the host of an https
// address, or the CID of an ipfs one (spec §44.1).
func (a Address) Host() string {
if CheckURI(a.URI) != nil {
return ""
}
_, host, _ := splitAuthority(a.URI)
return strings.Trim(host, "[]")
}
// Locator is the plaintext of the sealed locator (spec §44.1).
type Locator struct {
Addresses []Address
// EnvelopeKey is I_SOBRE, the raw X25519 identity of the envelope. SECRET.
EnvelopeKey [32]byte
// EnvelopeHeader is the age header of the envelope, MAC line included.
EnvelopeHeader []byte
// RestDigest is the SHA-256 of the rest, and RestSize its length.
RestDigest [32]byte
RestSize uint64
// CapsuleDigest is the SHA-256 of the .dkc (spec §43).
CapsuleDigest [32]byte
}
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
// Usable returns the addresses that meet the rules of spec §44.1, in their
// order. A reader rejects each address that breaks them, and uses the others:
// a locator whose addresses are all rejected has nothing to download.
func (l *Locator) Usable() []Address {
var out []Address
for _, a := range l.Addresses {
if CheckURI(a.URI) == nil {
out = append(out, a)
}
}
return out
}
// validate checks what Marshal writes: the form, and each address, since a
// writer never writes one that a reader would reject.
func (l *Locator) validate() error {
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 err := l.validateForm(); err != nil {
return err
}
for _, a := range l.Addresses {
if err := CheckURI(a.URI); err != nil {
return 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
return nil
}
// validateForm checks the form that a reader requires of the whole locator:
// a broken address makes only that address unusable (Usable).
func (l *Locator) validateForm() error {
if len(l.Addresses) < 1 || len(l.Addresses) > MaxAddresses {
return fmt.Errorf("locator: %d addresses, not 1 to %d", len(l.Addresses), MaxAddresses)
}
for _, a := range l.Addresses {
if a.URI == "" || len(a.URI) > MaxURILen {
return fmt.Errorf("locator: an address of %d bytes, not 1 to %d", len(a.URI), MaxURILen)
}
}
if n := len(l.EnvelopeHeader); n < 1 || n > MaxHeaderLen {
return fmt.Errorf("locator: an envelope header of %d bytes, not 1 to %d", n, MaxHeaderLen)
}
if l.RestSize > codec.MaxSafeUint {
return errors.New("locator: a rest larger than 2^53 - 1 bytes")
}
for _, a := range l.Addresses {
if a.Offset > codec.MaxSafeUint {
return errors.New("locator: an offset larger than 2^53 - 1")
}
}
return nil
}
// encode writes the map; pad < 0 leaves key 6 out.
func (l *Locator) encode(e *codec.Encoder, pad int) {
n := 6
if pad >= 0 {
n = 7
}
e.Map(n)
e.Uint(0)
e.Array(len(l.Addresses))
for _, a := range l.Addresses {
if a.Offset == 0 {
e.Map(1)
} else {
e.Map(2)
}
e.Uint(0)
e.Text(a.URI)
if a.Offset != 0 {
e.Uint(1)
e.Uint(a.Offset)
}
}
e.Uint(1)
e.Bstr(l.EnvelopeKey[:])
e.Uint(2)
e.Bstr(l.EnvelopeHeader)
e.Uint(3)
e.Bstr(l.RestDigest[:])
e.Uint(4)
e.Uint(l.RestSize)
e.Uint(5)
e.Bstr(l.CapsuleDigest[:])
if pad >= 0 {
e.Uint(6)
e.Bstr(make([]byte, pad))
}
}
func bstrHeadLen(n int) int {
switch {
case n < 24:
return 1
case n < 256:
return 2
case n < 65536:
return 3
}
return 5
}
// padFor returns the length of key 6 that makes the plaintext measure the
// least multiple of Block that holds it, or -1 when n0, the length without
// key 6, already is one. When no length of key 6 gives a given multiple, as
// happens at the boundaries of the CBOR length, it takes the next one.
func padFor(n0 int) int {
if n0%Block == 0 {
return -1
}
for total := (n0/Block + 1) * Block; ; total += Block {
for pad := 1; pad <= total-n0; pad++ {
if n0+1+bstrHeadLen(pad)+pad == total {
return pad
}
}
}
}
// PlaintextLength returns the length of the plaintext of a locator whose CBOR
// without key 6 measures base bytes: base when it already is a multiple of
// Block, and otherwise the least multiple that key 6 can fill exactly.
func PlaintextLength(base int) int {
pad := padFor(base)
if pad < 0 {
return base
}
return base + 1 + bstrHeadLen(pad) + pad
}
// Marshal returns the plaintext of the locator: CBOR with the profile of
// spec §58, completed with zeros in key 6 up to the least multiple of 4096
// bytes that holds it, so that its length does not tell how many addresses
// there are.
func (l *Locator) Marshal() ([]byte, error) {
if err := l.validate(); 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
return l.marshal()
}
// marshal is Marshal once the locator is checked.
func (l *Locator) marshal() ([]byte, error) {
var e codec.Encoder
l.encode(&e, -1)
base, err := e.Out()
if err != nil {
return nil, err
}
pad := padFor(len(base))
if pad < 0 {
return base, nil
}
defer clear(base) // it holds I_SOBRE
var p codec.Encoder
l.encode(&p, pad)
out, err := p.Out()
if err != nil {
return nil, err
}
if len(out)%Block != 0 {
return nil, fmt.Errorf("locator: internal error: %d bytes of plaintext", len(out))
}
return out, nil
}
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
// Unmarshal reads the plaintext of a locator, checking its profile and the
// length that Marshal gives. Its errors carry no normative code: a locator
// that does not read is unusable (spec §44.1, §57). An address that breaks
// the rules of §44.1 is kept, and Usable leaves it out.
func Unmarshal(b []byte) (*Locator, error) {
var l Locator
pad := -1
decode := func(d *codec.Decoder) error {
pairs, err := d.Map(7)
if err != nil {
return err
}
var seen uint
for range pairs {
k, err := d.Key()
if err != nil {
return err
}
switch k {
case 0:
err = decodeAddresses(d, &l)
case 1:
err = copyBstr(d, l.EnvelopeKey[:])
case 2:
l.EnvelopeHeader, err = d.Bstr(1, MaxHeaderLen)
case 3:
err = copyBstr(d, l.RestDigest[:])
case 4:
l.RestSize, err = d.Uint(codec.MaxSafeUint)
case 5:
err = copyBstr(d, l.CapsuleDigest[:])
case 6:
var z []byte
if z, err = d.Bstr(1, 1<<20); err == nil {
if len(bytes.Trim(z, "\x00")) != 0 {
return errors.New("the padding is not zeros")
}
pad = len(z)
}
default:
return fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
}
if err != nil {
return fmt.Errorf("key %d: %w", k, err)
}
seen |= 1 << k
}
if seen&0x3f != 0x3f {
return fmt.Errorf("a key from 0 to 5 is missing: %w", datekeys.ErrNonCanonicalCBOR)
}
return d.EndMap()
}
encode := func(e *codec.Encoder) { l.encode(e, pad) }
if err := codec.Unmarshal(b, decode, encode); err != nil {
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 nil, fmt.Errorf("locator: %v", 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
if err := l.validateForm(); err != nil {
return nil, err
}
// The length is the one Marshal gives: nothing else is canonical.
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
want, err := l.marshal()
defer clear(want)
if err != nil || !bytes.Equal(want, b) {
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 nil, fmt.Errorf("locator: the plaintext is not %d or the least multiple of %d that holds it", Block, Block)
}
return &l, nil
}
func copyBstr(d *codec.Decoder, dst []byte) error {
b, err := d.Bstr(len(dst), len(dst))
if err != nil {
return err
}
copy(dst, b)
return nil
}
func decodeAddresses(d *codec.Decoder, l *Locator) error {
n, err := d.Array(MaxAddresses)
if err != nil {
return err
}
for range n {
pairs, err := d.Map(2)
if err != nil {
return err
}
var a Address
var seen uint
for range pairs {
k, err := d.Key()
if err != nil {
return err
}
switch k {
case 0:
a.URI, err = d.Text(MaxURILen)
case 1:
if a.Offset, err = d.Uint(codec.MaxSafeUint); err == nil && a.Offset == 0 {
err = fmt.Errorf("an offset of 0 is written by leaving it out: %w", datekeys.ErrNonCanonicalCBOR)
}
default:
return fmt.Errorf("address key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
}
if err != nil {
return err
}
seen |= 1 << k
}
if seen&1 == 0 {
return fmt.Errorf("an address without URI: %w", datekeys.ErrNonCanonicalCBOR)
}
if err := d.EndMap(); err != nil {
return err
}
l.Addresses = append(l.Addresses, a)
}
return nil
}
// Seal returns the locator as an age file with a single tlock stanza for the
// round of the DateKey (spec §44.1): nobody reads it before the date, the
// holder of the key included.
func Seal(p *profile.Profile, round uint64, l *Locator) ([]byte, error) {
plain, err := l.Marshal()
if err != nil {
return nil, err
}
defer clear(plain)
r, err := agewrap.NewTimeRecipient(p, round)
if err != nil {
return nil, err
}
var buf bytes.Buffer
w, err := age.Encrypt(&buf, r)
if err != nil {
return nil, err
}
if _, err := w.Write(plain); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Open opens a sealed locator with the release of its round, and reads its
// plaintext. A locator for another round or another chain does not open: it
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
// is unusable (spec §44.1), and its errors carry no normative code.
func Open(p *profile.Profile, round uint64, release provider.Release, sealed []byte) (*Locator, error) {
id, err := agewrap.NewTimeIdentity(p, round, release)
if err != nil {
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 nil, fmt.Errorf("locator: %v", err)
}
r, err := age.Decrypt(bytes.NewReader(sealed), id)
if err != nil {
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 nil, fmt.Errorf("locator: %v", err)
}
plain, err := io.ReadAll(io.LimitReader(r, maxSealed))
if err != nil {
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 nil, fmt.Errorf("locator: %v", err)
}
defer clear(plain)
return Unmarshal(plain)
}
// NewEnvelope encrypts the .dkc dkc with age for a new identity, I_SOBRE, and
// splits the age file: the locator it returns has the key, the header, the
// digests and the size of the rest, and no address yet; rest, with no mark,
// is what the person keeps outside. A caller adds the addresses where it
// stored rest, alone or inside another file, and then seals the locator.
func NewEnvelope(dkc []byte) (loc *Locator, rest []byte, err error) {
id, err := age.GenerateX25519Identity()
if err != nil {
return nil, nil, err
}
raw, err := agewrap.RawX25519Identity(id)
if err != nil {
return nil, nil, err
}
defer clear(raw)
var buf bytes.Buffer
w, err := age.Encrypt(&buf, id.Recipient())
if err != nil {
return nil, nil, err
}
if _, err := w.Write(dkc); err != nil {
return nil, nil, err
}
if err := w.Close(); err != nil {
return nil, nil, err
}
file := buf.Bytes()
end, err := headerEnd(file)
if err != nil {
return nil, nil, err
}
loc = &Locator{EnvelopeHeader: bytes.Clone(file[:end]), RestDigest: sha256.Sum256(file[end:]), RestSize: uint64(len(file) - end), CapsuleDigest: sha256.Sum256(dkc)}
copy(loc.EnvelopeKey[:], raw)
return loc, bytes.Clone(file[end:]), nil
}
// headerEnd returns the length of the age header of file, up to and
// including the line feed after the MAC line: the line that starts with
// "--- ". No line of the header before it starts so, and the lines of the
// body of a stanza are base64, which has no '-'.
func headerEnd(file []byte) (int, error) {
i := bytes.Index(file, []byte("\n--- "))
if i < 0 {
return 0, errors.New("locator: the age file has no MAC line")
}
j := bytes.IndexByte(file[i+1:], '\n')
if j < 0 {
return 0, errors.New("locator: the MAC line of the age file does not end")
}
return i + 1 + j + 1, nil
}
// OpenEnvelope joins the header of the locator and rest, which a reader got
// from an address, and decrypts the .dkc. It checks the size and the SHA-256
// of rest, and the SHA-256 of the .dkc, before the caller uses it (spec
// §44.1): they protect against whoever stores the rest, not against whoever
// wrote the .dkk.
func (l *Locator) OpenEnvelope(rest []byte) ([]byte, error) {
if uint64(len(rest)) != l.RestSize {
return nil, fmt.Errorf("locator: the rest is %d bytes, not %d", len(rest), l.RestSize)
}
if sha256.Sum256(rest) != l.RestDigest {
return nil, errors.New("locator: the SHA-256 of the rest is not the one of the locator")
}
id, err := agewrap.X25519IdentityFromRaw(l.EnvelopeKey[:])
if err != nil {
return nil, err
}
r, err := age.Decrypt(io.MultiReader(bytes.NewReader(l.EnvelopeHeader), bytes.NewReader(rest)), id)
if err != nil {
return nil, fmt.Errorf("locator: the envelope: %w", err)
}
dkc, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("locator: the envelope: %w", err)
}
if sha256.Sum256(dkc) != l.CapsuleDigest {
return nil, errors.New("locator: the SHA-256 of the .dkc is not the capsule_digest of the locator")
}
return dkc, nil
}

Powered by TurnKey Linux.