Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
package capsule
import (
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"
"slices"
"strings"
"time"
"unicode/utf8"
"g.activething.com/go/DateKeys/internal/pathrule"
)
// Source is a file that EncryptFiles writes into a format 3 capsule.
type Source struct {
// Path is the path of the file in the capsule, relative, with '/'
// between its segments (spec §29.5). It is stored as given: EncryptFiles
// rejects a path that breaks a rule, with a message that names the rule
// and the character, and never corrects it (spec §62.1 rule 15).
Path string
// Size is the number of bytes of the file. EncryptFiles checks it in
// each of its two readings.
Size int64
// ModTime is the modification time of the file at its source, taken
// when it is loaded, as os.FileInfo.ModTime gives it, or the zero Time
// when unknown. It is stored in seconds when it falls from 1970-01-01 to
// 9999-12-31T23:59:59Z, and omitted otherwise, never clipped (spec
// §62.1 rule 16). It is informative: it proves nothing.
ModTime time . Time
// Open returns a reader of the file from its start. EncryptFiles calls
// it twice, and closes each reader.
Open func ( ) ( io . ReadCloser , error )
}
// EncryptFiles writes a format 3 .dkc holding the files of sources and the
// comment and declared author of opts (spec §29.2 to §29.6, §61, §62,
// §62.1). It needs no network: the round is resolved locally and tlock uses
// only the pinned public key.
//
// It reads each file twice, and writes nothing to dst before the second
// reading. First it checks the paths and the texts with the rules of the
// reader, measures L with a head whose salt and SHA-256 are zero, as long as
// the final one, and hashes each file. Then it seals the control, with L,
// and streams PAYLOAD_AGE, reading each file again: a file whose size or
// SHA-256 has changed makes it fail (spec §62.1 rule 18), and dst then holds
// a partial capsule that must be discarded and never presented as a capsule
// (rule 9). The files go in the byte order of their paths, whatever the
// order of sources (R8), without the empty folders, which a path cannot
// name.
//
// The head, the control and the security area are decoded with the rules of
// the reader before anything is written (spec §62.1 rule 17), and the
// self-checks of Encrypt apply too. The security area is in an area of
// AreaLen bytes, or LargeAreaLen with opts.LargeArea (rule 13): empty, or
// with the signature of opts.AuthorKey, made before anything is written
// with the commitments of the final control and the head (spec v0.11, §29.8)
// and checked with the strict profile (rule 19).
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
//
// opts is as for Encrypt, with the head in Comment, Author and the head
// extensions, and with Length 0: L is the length of BODY.
func EncryptFiles ( dst io . Writer , sources [ ] Source , opts EncryptOptions ) ( * Result , error ) {
if opts . Length != 0 {
return nil , errors . New ( "capsule: EncryptOptions.Length is for Encrypt: EncryptFiles computes L from the files" )
}
common := uint32 ( AreaLen )
switch t := opts . TestAreaLen ; {
case t == 0 :
case ! opts . TestVectors :
return nil , errors . New ( "capsule: EncryptOptions.TestAreaLen is for generators of test vectors: it needs TestVectors (spec §62.1 rule 13)" )
case t % AreaUnit != 0 || t > MaxAreaLen || opts . LargeArea :
return nil , fmt . Errorf ( "capsule: a test area of %d bytes: a multiple of %d up to %d, without LargeArea" , t , AreaUnit , MaxAreaLen )
default :
common = t
}
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
s , err := newSealer ( opts , 0 )
if err != nil {
return nil , err
}
h , order , err := newHead ( sources , opts )
if err != nil {
return nil , err
}
// Step 2 of spec §61: L, with a head as long as the final one.
measured , err := EncodeHead ( h )
if err != nil {
return nil , err
}
if len ( measured ) > MaxHeadLen {
return nil , fmt . Errorf ( "capsule: the head is %d bytes, more than %d: fewer files or shorter paths" , len ( measured ) , MaxHeadLen )
}
if err := selfCheckHead ( measured ) ; err != nil {
return nil , err
}
var content uint64
if n := len ( h . Files ) ; n > 0 {
content = h . Files [ n - 1 ] . End
}
// newHead bounds content by MaxPayloadLength: the sum does not overflow.
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
// This L is the first one, with the common area: the signature decides
// whether the area grows (spec §29.8), and prepare returns the final L.
area := common
length := BodyFrameSize + uint64 ( area ) + uint64 ( len ( measured ) ) + content
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
if _ , err := PaddedLength ( length , s . code ) ; err != nil {
return nil , err
}
// Step 3: the first reading, for the SHA-256 of each file.
for i := range h . Files {
if h . Files [ i ] . SHA256 , err = readSource ( nil , sources [ order [ i ] ] , h . Files [ i ] . Size , false ) ; err != nil {
return nil , err
}
}
// Step 12: the head, with a fresh salt, and SECURITY_CBOR, decoded with
// the rules of the reader; write decodes CONTROL_CBOR.
_ , _ = rand . Read ( h . Salt [ : ] ) // never fails since Go 1.24
head , err := EncodeHead ( h )
if err != nil {
return nil , err
}
if len ( head ) != len ( measured ) {
return nil , fmt . Errorf ( "capsule: internal error: the head is %d bytes, measured %d" , len ( head ) , len ( measured ) )
}
if err := selfCheckHead ( head ) ; err != nil {
return nil , err
}
// SECURITY_CBOR and the frame are final once the control is: the
// signature commits to it (spec v0.11, §29.8). prepare builds them
// before anything is written, and write decodes CONTROL_CBOR.
var security [ ] byte
var fb [ BodyFrameSize ] byte
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
prepare := func ( c * Control ) ( uint64 , error ) {
var err error
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
if security , err = s . security ( c , head ) ; err != nil {
return 0 , err
}
// The area is the common one, or the large one only when what was
// signed does not fit and LargeArea allows it (§62.1 rule 13).
switch {
case len ( security ) <= int ( common ) :
area = common
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
case ! opts . LargeArea :
return 0 , fmt . Errorf ( "capsule: SECURITY_CBOR of %d bytes does not fit in the area of %d bytes: LargeArea lets the writer widen it to %d" , len ( security ) , common , LargeAreaLen )
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
case len ( security ) <= LargeAreaLen :
area = LargeAreaLen
default :
return 0 , fmt . Errorf ( "capsule: SECURITY_CBOR of %d bytes does not fit in the area of %d bytes, the largest" , len ( security ) , LargeAreaLen )
}
final := BodyFrameSize + uint64 ( area ) + uint64 ( len ( head ) ) + content
if _ , err := PaddedLength ( final , s . code ) ; err != nil {
return 0 , err
}
frame := BodyFrame { AreaLen : area , SecurityLen : uint32 ( len ( security ) ) , HeadLen : uint32 ( len ( head ) ) }
fb = frame . Bytes ( )
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
if _ , err := ParseBodyFrame ( fb [ : ] , final ) ; err != nil {
return 0 , fmt . Errorf ( "capsule: self-check: %w" , err )
}
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
if err := CheckHeadEnd ( h , frame . ContentLength ( final ) ) ; err != nil {
return 0 , fmt . Errorf ( "capsule: self-check: %w" , err )
}
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
return final , nil
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
}
// Step 16: BODY, and the second reading of each file.
res , err := s . write ( dst , Format3 , length , prepare , func ( w io . Writer ) error {
for _ , b := range [ ] [ ] byte { fb [ : ] , security , make ( [ ] byte , int ( area ) - len ( security ) ) , head } {
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
if _ , err := w . Write ( b ) ; err != nil {
return err
}
}
for i := range h . Files {
f := & h . Files [ i ]
sum , err := readSource ( w , sources [ order [ i ] ] , f . Size , true )
if err != nil {
return err
}
if sum != f . SHA256 {
return fmt . Errorf ( "capsule: file %q changed after its first reading: its SHA-256 is another" , f . Path )
}
}
return nil
} )
if err != nil {
return nil , err
}
v0.16: a seal without accuracy proves nothing before the opening date
A valid seal is S4 only when its token carries accuracy and t plus the
accuracy is before round_time; otherwise S5, whose text gives the reason,
the first that holds: sealed after or too close, no accuracy under the BTSP
policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy (spec v0.16,
29.7, 29.11). The line of a signer of F6 whose seal does not prove it says
so with the same reason. cms.Token gains HasAccuracy, Policy and BTSP;
Verdicts gain SealReason and SignerLine.Reason; EncryptFiles returns the
verdicts of the area it wrote in Result.Security, so that a writer warns of
a seal without accuracy (rule 19).
security_cms.json is made again: 143 cases, the seals about something else
with an accuracy of a second, and the new cases of 64 with seal_reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
res . Head , res . Security = h , s . verdicts
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
return res , nil
}
// security returns SECURITY_CBOR for the capsule whose final control is c and
// whose head is head: empty, or with the signature of opts.AuthorKey or
// opts.CMSSigner, and the seal of opts.Sealer (spec v0.11, §29.3, §29.8 to
// §29.11). The signature is made first and the seal after it, which seals it.
// It decodes and evaluates what it returns with the rules of the reader, in
Review fixes: the CMS reader, the area after the signature, and the issuer on screen
The TSTInfo is read field by field in DER, with accuracy from zero and
millis and micros from 1 to 999, genTime in UTC with Z, no default written
and nothing after the last field. The ContentInfo and the SignerInfo must be
SEQUENCEs, a SignerInfo version must match its sid, an attribute needs a
value and is counted by attribute and not by value, a signing-certificate
beside the v2 decides nothing, PSS parameters come in order without the
trailer, and der.Check refuses the end of contents and the universal tags
the profile does not use.
The writer signs before L is fixed: write asks prepare for the final L, so
the area grows to 64 KiB only when what was signed does not fit and LargeArea
allows it, and nobody signs twice for it. Typed nils are nil, the exclusions
are checked before a file is read, Encrypt refuses the signing options, and
EvaluateSecurityIn gives X if a parser panics. The issuer of a certificate is
filtered like its holder.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
// the context of this capsule (§62.1 rules 17, 19 and 21). The caller decides
// the area from its length.
func ( s * sealer ) security ( c * Control , head [ ] byte ) ( [ ] byte , error ) {
o := s . opts
sc := & SecurityContext { HeadDigest : HeadDigest ( head ) , RoundTime : s . unlock }
if o . AuthorKey == nil && o . CMSSigner == nil && o . Sealer == nil {
security := EncodeSecurity ( )
v0.16: a seal without accuracy proves nothing before the opening date
A valid seal is S4 only when its token carries accuracy and t plus the
accuracy is before round_time; otherwise S5, whose text gives the reason,
the first that holds: sealed after or too close, no accuracy under the BTSP
policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy (spec v0.16,
29.7, 29.11). The line of a signer of F6 whose seal does not prove it says
so with the same reason. cms.Token gains HasAccuracy, Policy and BTSP;
Verdicts gain SealReason and SignerLine.Reason; EncryptFiles returns the
verdicts of the area it wrote in Result.Security, so that a writer warns of
a seal without accuracy (rule 19).
security_cms.json is made again: 143 cases, the seals about something else
with an accuracy of a second, and the new cases of 64 with seal_reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
v := EvaluateSecurityIn ( security , sc )
if v != ( Verdicts { Signature : VerdictNoSignature , Seal : VerdictNoSeal } ) {
return nil , fmt . Errorf ( "capsule: self-check: the reader finds the verdicts %s and %s in this security area" , v . Signature , v . Seal )
}
v0.16: a seal without accuracy proves nothing before the opening date
A valid seal is S4 only when its token carries accuracy and t plus the
accuracy is before round_time; otherwise S5, whose text gives the reason,
the first that holds: sealed after or too close, no accuracy under the BTSP
policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy (spec v0.16,
29.7, 29.11). The line of a signer of F6 whose seal does not prove it says
so with the same reason. cms.Token gains HasAccuracy, Policy and BTSP;
Verdicts gain SealReason and SignerLine.Reason; EncryptFiles returns the
verdicts of the area it wrote in Result.Security, so that a writer warns of
a seal without accuracy (rule 19).
security_cms.json is made again: 143 cases, the seals about something else
with an accuracy of a second, and the new cases of 64 with seal_reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
s . verdicts = v
return security , nil
}
var err error
if sc . ControlCommit , err = ControlCommit ( c , Format3 ) ; err != nil {
return nil , err
}
var signature , seal [ ] byte
wantSig := VerdictNoSignature
var key [ 32 ] byte
switch {
case o . AuthorKey != nil :
pub := o . AuthorKey . Public ( )
if len ( pub ) != 32 {
return nil , fmt . Errorf ( "capsule: the author key is %d bytes, not 32" , len ( pub ) )
}
msg := AuthorMessage ( sc . ControlCommit , sc . HeadDigest , SignersDigest ( AlgEd25519 , nil ) )
if signature , err = EncodeAuthorSignature ( AlgEd25519 , pub , o . AuthorKey . Sign ( msg ) ) ; err != nil {
return nil , err
}
wantSig = VerdictSignedOther
copy ( key [ : ] , pub )
case o . CMSSigner != nil :
list , err := EncodeSigners ( o . CMSSigner . Signers ( ) )
if err != nil {
return nil , err
}
// AUTHOR_MESSAGE is what the person sees and signs elsewhere: the
// callback may take as long as she needs.
msg := AuthorMessage ( sc . ControlCommit , sc . HeadDigest , SignersDigest ( AlgCMS , list ) )
der , err := o . CMSSigner . Sign ( msg )
if err != nil {
return nil , fmt . Errorf ( "capsule: signing: %w" , err )
}
if signature , err = EncodeAuthorSignature ( AlgCMS , list , der ) ; err != nil {
return nil , err
}
wantSig = VerdictSignedComplete
}
wantSeal := VerdictNoSeal
if o . Sealer != nil {
subject := SealSubject ( sc . ControlCommit , sc . HeadDigest , SigPart ( signature ) )
token , err := o . Sealer . Seal ( subject )
if err != nil {
return nil , fmt . Errorf ( "capsule: sealing: %w" , err )
}
if seal , err = EncodeSeal ( SealTypeRFC3161 , token ) ; err != nil {
return nil , err
}
wantSeal = VerdictSealed
}
security , err := EncodeSecurityWith ( signature , seal )
if err != nil {
return nil , err
}
v := EvaluateSecurityIn ( security , sc )
// A seal that proves nothing before the round time (S5) is still a seal
v0.16: a seal without accuracy proves nothing before the opening date
A valid seal is S4 only when its token carries accuracy and t plus the
accuracy is before round_time; otherwise S5, whose text gives the reason,
the first that holds: sealed after or too close, no accuracy under the BTSP
policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy (spec v0.16,
29.7, 29.11). The line of a signer of F6 whose seal does not prove it says
so with the same reason. cms.Token gains HasAccuracy, Policy and BTSP;
Verdicts gain SealReason and SignerLine.Reason; EncryptFiles returns the
verdicts of the area it wrote in Result.Security, so that a writer warns of
a seal without accuracy (rule 19).
security_cms.json is made again: 143 cases, the seals about something else
with an accuracy of a second, and the new cases of 64 with seal_reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
// that verifies: the writer's clock and the authority's may differ, or
// the token may carry no accuracy. Result.Security lets the caller warn
// of it (§62.1 rule 19).
sealOK := v . Seal == wantSeal || wantSeal == VerdictSealed && v . Seal == VerdictSealedLate
if v . Signature != wantSig || ! sealOK || v . AuthorKey != key {
return nil , fmt . Errorf ( "capsule: self-check: the reader finds the verdicts %s and %s in this security area, not %s and %s%s" , v . Signature , v . Seal , wantSig , wantSeal , detailText ( v . Detail ) )
}
v0.16: a seal without accuracy proves nothing before the opening date
A valid seal is S4 only when its token carries accuracy and t plus the
accuracy is before round_time; otherwise S5, whose text gives the reason,
the first that holds: sealed after or too close, no accuracy under the BTSP
policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy (spec v0.16,
29.7, 29.11). The line of a signer of F6 whose seal does not prove it says
so with the same reason. cms.Token gains HasAccuracy, Policy and BTSP;
Verdicts gain SealReason and SignerLine.Reason; EncryptFiles returns the
verdicts of the area it wrote in Result.Security, so that a writer warns of
a seal without accuracy (rule 19).
security_cms.json is made again: 143 cases, the seals about something else
with an accuracy of a second, and the new cases of 64 with seal_reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 hours ago
s . verdicts = v
return security , nil
}
// detailText names the signers that failed, for the error of a writer.
func detailText ( d * Detail ) string {
if d == nil {
return ""
}
var parts [ ] string
for _ , l := range d . Signers {
if l . Result != "valid" {
parts = append ( parts , l . Holder + ": " + l . Result )
}
}
if len ( parts ) == 0 {
return ""
}
return " (" + strings . Join ( parts , "; " ) + ")"
}
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// newHead checks the files and the texts of opts with the rules of spec
// §29.4 to §29.6, in the words of a writer (spec §62.1 rule 15), and returns
// the head with the files in the byte order of their paths, their layout and
// mtime, and a zero salt and zero SHA-256; order[i] is the source of entry
// i. The comment has its CR LF, and any lone CR, turned into LF (§29.6).
func newHead ( sources [ ] Source , opts EncryptOptions ) ( * Head , [ ] int , error ) {
comment := strings . ReplaceAll ( strings . ReplaceAll ( opts . Comment , "\r\n" , "\n" ) , "\r" , "\n" )
switch {
case len ( sources ) == 0 && comment == "" :
return nil , nil , errors . New ( "capsule: a format 3 capsule holds at least one file or a comment (spec §62.1 rule 14)" )
case len ( sources ) > MaxFiles :
return nil , nil , fmt . Errorf ( "capsule: %d files, more than %d" , len ( sources ) , MaxFiles )
}
if err := checkHeadText ( "comment" , comment , MaxCommentLen , pathrule . CheckComment ) ; err != nil {
return nil , nil , err
}
if err := checkHeadText ( "declared author" , opts . Author , MaxAuthorLen , pathrule . CheckAuthor ) ; err != nil {
return nil , nil , err
}
order := make ( [ ] int , len ( sources ) )
for i := range order {
order [ i ] = i
}
// R8: the byte order of the paths, which is the order of Go strings.
slices . SortStableFunc ( order , func ( a , b int ) int { return strings . Compare ( sources [ a ] . Path , sources [ b ] . Path ) } )
h := & Head { Comment : comment , Author : opts . Author , Critical : opts . HeadCritical , Noncritical : opts . HeadNoncritical }
paths := make ( [ ] string , len ( order ) )
var end uint64
for i , j := range order {
src , p := sources [ j ] , sources [ j ] . Path
switch {
case i > 0 && p == paths [ i - 1 ] :
return nil , nil , fmt . Errorf ( "capsule: path %q given twice" , p )
case ! utf8 . ValidString ( p ) :
return nil , nil , fmt . Errorf ( "capsule: path %q: R1: not valid UTF-8" , p )
case len ( p ) == 0 || len ( p ) > MaxPathLen :
return nil , nil , fmt . Errorf ( "capsule: path %q: R1: %d bytes, not 1 to %d" , p , len ( p ) , MaxPathLen )
case src . Size < 0 :
return nil , nil , fmt . Errorf ( "capsule: file %q: negative size %d" , p , src . Size )
case src . Open == nil :
return nil , nil , fmt . Errorf ( "capsule: file %q: Source.Open is nil" , p )
case uint64 ( src . Size ) > MaxPayloadLength - end :
return nil , nil , fmt . Errorf ( "capsule: the files add up to more than %d bytes, the maximum of L" , uint64 ( MaxPayloadLength ) )
}
if err := pathrule . CheckPath ( p ) ; err != nil {
return nil , nil , fmt . Errorf ( "capsule: path %q: %w" , p , err )
}
f := File { Path : p , Size : uint64 ( src . Size ) , Start : end , End : end + uint64 ( src . Size ) }
if t := src . ModTime ; ! t . IsZero ( ) {
if u := t . Unix ( ) ; u >= 0 && u <= MaxMTime {
f . MTime , f . HasMTime = uint64 ( u ) , true
}
}
h . Files = append ( h . Files , f )
paths [ i ] , end = p , f . End
}
if err := pathrule . CheckTree ( paths ) ; err != nil {
var e * pathrule . Error
if errors . As ( err , & e ) && e . Paths [ 0 ] > 0 {
return nil , nil , fmt . Errorf ( "capsule: paths %q and %q: %w" , paths [ e . Paths [ 1 ] - 1 ] , paths [ e . Paths [ 0 ] - 1 ] , err )
}
return nil , nil , fmt . Errorf ( "capsule: paths: %w" , err )
}
return h , order , nil
}
// checkHeadText checks the comment or the declared author, when present:
// valid UTF-8, at most max bytes, and the characters of spec §29.6.
func checkHeadText ( what , s string , max int , check func ( string ) error ) error {
switch {
case s == "" :
return nil
case ! utf8 . ValidString ( s ) :
return fmt . Errorf ( "capsule: %s: not valid UTF-8" , what )
case len ( s ) > max :
return fmt . Errorf ( "capsule: %s: %d bytes, more than %d" , what , len ( s ) , max )
}
if err := check ( s ) ; err != nil {
return fmt . Errorf ( "capsule: %s: %w" , what , err )
}
return nil
}
// selfCheckHead decodes HEAD_CBOR with the rules of the reader, but for the
// knowledge of its critical extensions, which depends on the reader, as
// selfCheckControl does with the control (spec §62.1 rule 17). A head that
// the reader rejects would only be found after the date.
func selfCheckHead ( b [ ] byte ) error {
if _ , err := decodeHead ( b ) ; err != nil {
return fmt . Errorf ( "capsule: self-check: the reader rejects this head: %w" , err )
}
return nil
}
// readSource reads the file of src, which must be exactly size bytes, and
// returns its SHA-256; w, when not nil, receives its bytes. In the second
// reading, a file whose size differs has changed (spec §62.1 rule 18).
func readSource ( w io . Writer , src Source , size uint64 , second bool ) ( [ 32 ] byte , error ) {
var sum [ 32 ] byte
mismatch := func ( format string , args ... any ) error {
if second {
return fmt . Errorf ( "capsule: file %q changed after its first reading: %s" , src . Path , fmt . Sprintf ( format , args ... ) )
}
return fmt . Errorf ( "capsule: file %q: %s" , src . Path , fmt . Sprintf ( format , args ... ) )
}
rc , err := src . Open ( )
if err != nil {
return sum , fmt . Errorf ( "capsule: file %q: %w" , src . Path , err )
}
defer rc . Close ( )
h := sha256 . New ( )
buf := make ( [ ] byte , 32 << 10 )
defer clear ( buf )
var n uint64
for {
k , err := rc . Read ( buf )
if uint64 ( k ) > size - n {
return sum , mismatch ( "more than its size of %d bytes" , size )
}
h . Write ( buf [ : k ] )
if w != nil && k > 0 {
if _ , err := w . Write ( buf [ : k ] ) ; err != nil {
return sum , err
}
}
n += uint64 ( k )
if err == io . EOF {
break
}
if err != nil {
return sum , fmt . Errorf ( "capsule: file %q: %w" , src . Path , err )
}
}
if n != size {
return sum , mismatch ( "%d bytes, not its size of %d" , n , size )
}
h . Sum ( sum [ : 0 ] )
return sum , nil
}