refactor su3 readers to support signature verification

This commit is contained in:
apeace
2022-07-28 22:41:25 -04:00
parent ad7a828d43
commit feef6e6bbf
2 changed files with 148 additions and 130 deletions

View File

@@ -1,40 +1,43 @@
// Package su3 implements reading the SU3 file format.
//
// SU3 files provide content that is signed by a known identity.
// They are used to distributed many types of data, including reseed files,
// They are used to distribute many types of data, including reseed files,
// plugins, blocklists, and more.
//
// See: https://geti2p.net/spec/updates#su3-file-specification
//
// The Read() function takes an io.Reader, and it returns four values:
// - meta: The SU3 file metadata, describing the type of file and the identity that signed it.
// - content: An io.Reader of the file contents.
// - signature: An io.Reader of the signature.
// - err: An error if something went wrong.
// The Read() function takes an io.Reader, and it returns a *SU3. The *SU3 contains
// the SU3 file metadata, such as the type of the content and the signer ID.
// In order to get the file contents, one must pass in the public key associated
// with the file's signer, so that the signature can be validated. The content
// can still be read without passing in the key, but after returning the full
// content the error ErrInvalidSignature will be returned.
//
// Example usage:
// // Let's say we are reading an SU3 file from an HTTP body.
// meta, content, signature, err := su3.Read(body)
// // Let's say we are reading an SU3 file from an HTTP body, which is an io.Reader.
// su3File, err := su3.Read(body)
// if err != nil {
// // Handle error.
// }
// bytes, err := ioutil.ReadAll(content)
// // Look up this signer's key.
// key := somehow_lookup_the_key(su3File.SignerID)
// // Read the content.
// contentReader := su3File.Content(key)
// bytes, err := ioutil.ReadAll(contentReader)
// if errors.Is(err, su3.ErrInvalidSignature) {
// // The signature is invalid.
// // The signature is invalid, OR a nil key was provided.
// } else if err != nil {
// // Handle error.
// }
//
// If you want to parse from a []byte, you can wrap them like this:
// meta, content, signature, err := su3.Read(bytes.NewReader([]byte{0x00, 0x01, 0x02, 0x03}))
// mySU3FileBytes := []byte{0x00, 0x01, 0x02, 0x03}
// su3File, err := su3.Read(bytes.NewReader(mySU3FileBytes))
//
// Note: the content io.Reader must be read *before* the signature io.Reader.
// If you read the signature first, the content bytes will be thrown away.
// If you then attempt to read the content, you will get an error.
// For clarification, see TestReadSignatureFirst.
//
// PLEASE NOTE: Signature validation is not implemented at this time.
// Use with caution.
// Note: if you want to read the content, the Content() io.Reader must be read
// *before* the Signature() io.Reader. If you read the signature first, the
// content bytes will be thrown away. If you then attempt to read the content,
// you will get an error. For clarification, see TestReadSignatureFirst.
package su3
import (
@@ -136,7 +139,7 @@ var ErrInvalidSignature = errors.New("invalid signature")
const magicBytes = "I2Psu3"
type SU3Meta struct {
type SU3 struct {
SignatureType SignatureType
SignatureLength uint16
ContentLength uint64
@@ -144,114 +147,132 @@ type SU3Meta struct {
ContentType ContentType
Version string
SignerID string
mut sync.Mutex
reader io.Reader
bytesRead uint64
publicKey interface{}
contentReader *su3Reader
signatureReader *su3Reader
}
func Read(reader io.Reader) (meta *SU3Meta, content io.Reader, signature io.Reader, err error) {
func (su3 *SU3) Content(publicKey interface{}) io.Reader {
su3.publicKey = publicKey
return su3.contentReader
}
func (su3 *SU3) Signature() io.Reader {
return su3.signatureReader
}
func Read(reader io.Reader) (su3 *SU3, err error) {
// Magic bytes.
mbytes := make([]byte, len(magicBytes))
l, err := reader.Read(mbytes)
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading magic bytes: %w", err)
return nil, fmt.Errorf("reading magic bytes: %w", err)
}
if l != len(mbytes) {
return nil, nil, nil, ErrMissingMagicBytes
return nil, ErrMissingMagicBytes
}
if string(mbytes) != magicBytes {
return nil, nil, nil, ErrMissingMagicBytes
return nil, ErrMissingMagicBytes
}
// Unused byte 6.
unused := [1]byte{}
l, err = reader.Read(unused[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading unused byte 6: %w", err)
return nil, fmt.Errorf("reading unused byte 6: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingUnusedByte6
return nil, ErrMissingUnusedByte6
}
// SU3 file format version (always 0).
l, err = reader.Read(unused[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading SU3 file format version: %w", err)
return nil, fmt.Errorf("reading SU3 file format version: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingFileFormatVersion
return nil, ErrMissingFileFormatVersion
}
if unused[0] != 0x00 {
return nil, nil, nil, ErrMissingFileFormatVersion
return nil, ErrMissingFileFormatVersion
}
meta = &SU3Meta{}
su3 = &SU3{
mut: sync.Mutex{},
reader: reader,
}
// Signature type.
sigTypeBytes := [2]byte{}
l, err = reader.Read(sigTypeBytes[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading signature type: %w", err)
return nil, fmt.Errorf("reading signature type: %w", err)
}
if l != 2 {
return nil, nil, nil, ErrMissingSignatureType
return nil, ErrMissingSignatureType
}
sigType, ok := sigTypes[sigTypeBytes]
if !ok {
return nil, nil, nil, ErrMissingSignatureType
return nil, ErrMissingSignatureType
}
meta.SignatureType = sigType
su3.SignatureType = sigType
// Signature length.
sigLengthBytes := [2]byte{}
l, err = reader.Read(sigLengthBytes[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading signature length: %w", err)
return nil, fmt.Errorf("reading signature length: %w", err)
}
if l != 2 {
return nil, nil, nil, ErrMissingSignatureLength
return nil, ErrMissingSignatureLength
}
sigLen := binary.BigEndian.Uint16(sigLengthBytes[:])
// TODO check that sigLen is the correct length for sigType.
meta.SignatureLength = sigLen
su3.SignatureLength = sigLen
// Unused byte 12.
l, err = reader.Read(unused[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading unused byte 12: %w", err)
return nil, fmt.Errorf("reading unused byte 12: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingUnusedByte12
return nil, ErrMissingUnusedByte12
}
// Version length.
verLengthBytes := [1]byte{}
l, err = reader.Read(verLengthBytes[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading version length: %w", err)
return nil, fmt.Errorf("reading version length: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingVersionLength
return nil, ErrMissingVersionLength
}
verLen := binary.BigEndian.Uint16([]byte{0x00, verLengthBytes[0]})
if verLen < 16 {
return nil, nil, nil, ErrVersionTooShort
return nil, ErrVersionTooShort
}
// Unused byte 14.
l, err = reader.Read(unused[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading unused byte 14: %w", err)
return nil, fmt.Errorf("reading unused byte 14: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingUnusedByte14
return nil, ErrMissingUnusedByte14
}
// Signer ID length.
sigIDLengthBytes := [1]byte{}
l, err = reader.Read(sigIDLengthBytes[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading signer id length: %w", err)
return nil, fmt.Errorf("reading signer id length: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingSignerIDLength
return nil, ErrMissingSignerIDLength
}
signIDLen := binary.BigEndian.Uint16([]byte{0x00, sigIDLengthBytes[0]})
@@ -259,70 +280,70 @@ func Read(reader io.Reader) (meta *SU3Meta, content io.Reader, signature io.Read
contentLengthBytes := [8]byte{}
l, err = reader.Read(contentLengthBytes[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading content length: %w", err)
return nil, fmt.Errorf("reading content length: %w", err)
}
if l != 8 {
return nil, nil, nil, ErrMissingContentLength
return nil, ErrMissingContentLength
}
contentLen := binary.BigEndian.Uint64(contentLengthBytes[:])
meta.ContentLength = contentLen
su3.ContentLength = contentLen
// Unused byte 24.
l, err = reader.Read(unused[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading unused byte 24: %w", err)
return nil, fmt.Errorf("reading unused byte 24: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingUnusedByte24
return nil, ErrMissingUnusedByte24
}
// File type.
fileTypeBytes := [1]byte{}
l, err = reader.Read(fileTypeBytes[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading file type: %w", err)
return nil, fmt.Errorf("reading file type: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingFileType
return nil, ErrMissingFileType
}
fileType, ok := fileTypes[fileTypeBytes[0]]
if !ok {
return nil, nil, nil, ErrMissingFileType
return nil, ErrMissingFileType
}
meta.FileType = fileType
su3.FileType = fileType
// Unused byte 26.
l, err = reader.Read(unused[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading unused byte 26: %w", err)
return nil, fmt.Errorf("reading unused byte 26: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingUnusedByte26
return nil, ErrMissingUnusedByte26
}
// Content type.
contentTypeBytes := [1]byte{}
l, err = reader.Read(contentTypeBytes[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading content type: %w", err)
return nil, fmt.Errorf("reading content type: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingContentType
return nil, ErrMissingContentType
}
contentType, ok := contentTypes[contentTypeBytes[0]]
if !ok {
return nil, nil, nil, ErrMissingContentType
return nil, ErrMissingContentType
}
meta.ContentType = contentType
su3.ContentType = contentType
// Unused bytes 28-39.
for i := 0; i < 12; i++ {
l, err = reader.Read(unused[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading unused bytes 28-39: %w", err)
return nil, fmt.Errorf("reading unused bytes 28-39: %w", err)
}
if l != 1 {
return nil, nil, nil, ErrMissingUnusedBytes28To39
return nil, ErrMissingUnusedBytes28To39
}
}
@@ -330,117 +351,105 @@ func Read(reader io.Reader) (meta *SU3Meta, content io.Reader, signature io.Read
versionBytes := make([]byte, verLen)
l, err = reader.Read(versionBytes[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading version: %w", err)
return nil, fmt.Errorf("reading version: %w", err)
}
if l != int(verLen) {
return nil, nil, nil, ErrMissingVersion
return nil, ErrMissingVersion
}
version := strings.TrimRight(string(versionBytes), "\x00")
meta.Version = version
su3.Version = version
// Signer ID.
signerIDBytes := make([]byte, signIDLen)
l, err = reader.Read(signerIDBytes[:])
if err != nil && !errors.Is(err, io.EOF) {
return nil, nil, nil, fmt.Errorf("reading signer id: %w", err)
return nil, fmt.Errorf("reading signer id: %w", err)
}
if l != int(signIDLen) {
return nil, nil, nil, ErrMissingSignerID
return nil, ErrMissingSignerID
}
signerID := string(signerIDBytes)
meta.SignerID = signerID
su3.SignerID = signerID
csr := &contentSignatureReader{
reader: reader,
contentLength: contentLen,
signatureLength: sigLen,
}
// Track the number of bytes read so that the su3Readers know their position.
su3.bytesRead = uint64(39 + int(verLen) + int(signIDLen))
return meta, csr.Content(), csr.Signature(), nil
}
// contentSignatureReader synchronizes reading the content, and then the signature,
// out of the same io.Reader that we are reading the SU3 file from. It allows us
// to return two io.Readers, one for the content, and one for the signature, and
// have them both be read out of the SU3 file io.Reader.
type contentSignatureReader struct {
sync.Mutex
reader io.Reader
contentLength uint64
signatureLength uint16
bytesRead uint64
}
func (csr *contentSignatureReader) Content() io.Reader {
return &byteReader{
csr: csr,
numBytes: csr.contentLength,
startByte: 0,
su3.contentReader = &su3Reader{
su3: su3,
startByte: su3.bytesRead,
numBytes: su3.ContentLength,
outOfBytesError: ErrMissingContent,
}
}
func (csr *contentSignatureReader) Signature() io.Reader {
return &byteReader{
csr: csr,
numBytes: uint64(csr.signatureLength),
startByte: csr.contentLength,
su3.signatureReader = &su3Reader{
su3: su3,
startByte: su3.bytesRead + su3.ContentLength,
numBytes: uint64(su3.SignatureLength),
outOfBytesError: ErrMissingSignature,
}
return su3, nil
}
type byteReader struct {
csr *contentSignatureReader
numBytes uint64
type su3Reader struct {
su3 *SU3
startByte uint64
numBytes uint64
outOfBytesError error
}
func (br *byteReader) Read(p []byte) (n int, err error) {
br.csr.Lock()
defer br.csr.Unlock()
func (r *su3Reader) Read(p []byte) (n int, err error) {
r.su3.mut.Lock()
defer r.su3.mut.Unlock()
// If we have already read past where we are supposed to, return an error.
// This would happen if someone read the signature before reading the content,
// and then tried to read the content.
if br.csr.bytesRead > br.startByte {
if r.su3.bytesRead > r.startByte {
return 0, errors.New("out of bytes, maybe you read the signature before you read the content")
}
// If we have not read up until where we are supposed to, throw away the bytes.
// This would happen if someone read the signature before reading the content.
// We want to allow them to read the signature. The above condition will return
// an error if they try to read the content.
if br.csr.bytesRead < br.startByte {
bytesToThrowAway := br.startByte - br.csr.bytesRead
// an error if they try to read the content after the bytes have been thrown away.
if r.su3.bytesRead < r.startByte {
bytesToThrowAway := r.startByte - r.su3.bytesRead
throwaway := make([]byte, bytesToThrowAway)
l, err := br.csr.reader.Read(throwaway)
br.csr.bytesRead += uint64(l)
l, err := r.su3.reader.Read(throwaway)
r.su3.bytesRead += uint64(l)
if err != nil && !errors.Is(err, io.EOF) {
return 0, fmt.Errorf("reading throwaway bytes: %w", err)
}
if l != int(bytesToThrowAway) {
return 0, br.outOfBytesError
return 0, r.outOfBytesError
}
}
// We are at the correct position.
// If numBytes is 0, we have read all the bytes.
if br.numBytes == 0 {
if r.numBytes == 0 {
// TODO when we finish reading content, we should then read the signature and verify it.
// If the signature doesn't match, we would return ErrInvalidSignature here.
return 0, io.EOF
}
// Otherwise, we have some bytes to read.
numBytesToRead := len(p)
if numBytesToRead > int(br.numBytes) {
numBytesToRead = int(br.numBytes)
if numBytesToRead > int(r.numBytes) {
numBytesToRead = int(r.numBytes)
}
l, err := br.csr.reader.Read(p[:numBytesToRead])
l, err := r.su3.reader.Read(p[:numBytesToRead])
// Advance the counters to keep track of how many bytes we've read.
br.csr.bytesRead += uint64(l)
br.numBytes = br.numBytes - uint64(l)
br.startByte = br.startByte + uint64(l)
r.su3.bytesRead += uint64(l)
r.numBytes = r.numBytes - uint64(l)
r.startByte = r.startByte + uint64(l)
// We should have read the correct number of bytes.
if l < numBytesToRead {
return l, br.outOfBytesError
return l, r.outOfBytesError
}
return l, err
}

View File

@@ -38,8 +38,9 @@ func TestRead(t *testing.T) {
tests := []struct {
name string
reader io.Reader
key interface{}
wantErr string
wantMeta *SU3Meta
wantSU3 *SU3
wantContent []byte
wantSignature []byte
}{
@@ -449,7 +450,8 @@ func TestRead(t *testing.T) {
[]byte("apeace rules"), // Content
[]byte{0x99}, // Signature
)),
wantMeta: &SU3Meta{
key: nil,
wantSU3: &SU3{
SignatureType: ECDSA_SHA512_P521,
SignatureLength: 1,
ContentLength: 12,
@@ -464,7 +466,8 @@ func TestRead(t *testing.T) {
{
name: "reseed-i2pgit.su3",
reader: fileReader(t, "testdata/reseed-i2pgit.su3"),
wantMeta: &SU3Meta{
key: nil,
wantSU3: &SU3{
SignatureType: RSA_SHA512_4096,
SignatureLength: 512,
ContentLength: 80138,
@@ -480,12 +483,12 @@ func TestRead(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
meta, contentReader, signatureReader, err := Read(test.reader)
su3, err := Read(test.reader)
var content, signature []byte
if err == nil {
content, err = ioutil.ReadAll(contentReader)
content, err = ioutil.ReadAll(su3.Content(test.key))
if err == nil {
signature, err = ioutil.ReadAll(signatureReader)
signature, err = ioutil.ReadAll(su3.Signature())
}
}
if test.wantErr != "" && err == nil {
@@ -495,7 +498,13 @@ func TestRead(t *testing.T) {
} else if err != nil {
assert.Nil(t, err, "expected nil error")
} else {
assert.Equal(t, test.wantMeta, meta, "expected metdata to match")
assert.Equal(t, test.wantSU3.SignatureType, su3.SignatureType, "expected SignatureType to match")
assert.Equal(t, test.wantSU3.SignatureLength, su3.SignatureLength, "expected SignatureLength to match")
assert.Equal(t, test.wantSU3.ContentLength, su3.ContentLength, "expected ContentLength to match")
assert.Equal(t, test.wantSU3.FileType, su3.FileType, "expected FileType to match")
assert.Equal(t, test.wantSU3.ContentType, su3.ContentType, "expected ContentType to match")
assert.Equal(t, test.wantSU3.Version, su3.Version, "expected Version to match")
assert.Equal(t, test.wantSU3.SignerID, su3.SignerID, "expected SignerID to match")
assert.Equal(t, test.wantContent, content, "expected content to match")
assert.Equal(t, test.wantSignature, signature, "expected signature to match")
}
@@ -507,15 +516,15 @@ func TestReadSignatureFirst(t *testing.T) {
assert := assert.New(t)
reader := fileReader(t, "testdata/reseed-i2pgit.su3")
_, contentReader, signatureReader, err := Read(reader)
su3, err := Read(reader)
assert.Nil(err)
// Read only the signature.
sig, err := ioutil.ReadAll(signatureReader)
sig, err := ioutil.ReadAll(su3.Signature())
assert.Nil(err)
assert.Equal(fileBytes(t, "testdata/reseed-i2pgit-signature"), sig)
// Reading content should give an error.
_, err = ioutil.ReadAll(contentReader)
_, err = ioutil.ReadAll(su3.Content(nil))
assert.NotNil(err)
}