diff --git a/.gitignore b/.gitignore index f6234f3..15a51a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /i2p-reseeder /cert.pem /key.pem -/netdb -i2pseeds.su3 \ No newline at end of file +/_netdb +i2pseeds.su3 +reseed_cert.pem +reseed_private.pem diff --git a/cmd/keygen.go b/cmd/keygen.go new file mode 100644 index 0000000..b334f4a --- /dev/null +++ b/cmd/keygen.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "os" + "time" + + "github.com/codegangsta/cli" +) + +func NewKeygenCommand() cli.Command { + return cli.Command{ + Name: "keygen", + Usage: "Generate keys for reseed Su3 signing", + Description: "Generate keys for reseed Su3 signing", + Action: keygenAction, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "signer", + Usage: "Your email address (ex. something@mail.i2p)", + }, + }, + } +} + +func keygenAction(c *cli.Context) { + //"CN=" + cname + ",OU=" + ou + ",O=I2P Anonymous Network,L=XX,ST=XX,C=XX", + template := &x509.Certificate{ + BasicConstraintsValid: true, + IsCA: true, + SubjectKeyId: []byte{1, 2, 3}, + SerialNumber: big.NewInt(1234), + Subject: pkix.Name{ + Organization: []string{"I2P Anonymous Network"}, + OrganizationalUnit: []string{"I2P"}, + Locality: []string{"XX"}, + StreetAddress: []string{"XX"}, + Country: []string{"XX"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + } + + // generate private key + privatekey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + log.Fatalln(err) + } + + publickey := &privatekey.PublicKey + + // create a self-signed certificate. template = parent + var parent = template + cert, err := x509.CreateCertificate(rand.Reader, template, parent, publickey, privatekey) + if err != nil { + log.Fatalln(err) + } + + // save private key + pemfile, err := os.Create("reseed_private.pem") + if err != nil { + log.Fatalf("failed to open reseed_cert.pem for writing: %s", err) + } + var pemkey = &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privatekey)} + pem.Encode(pemfile, pemkey) + pemfile.Close() + fmt.Println("private key saved to reseed_private.pem") + + // save cert + certOut, err := os.Create("reseed_cert.pem") + if err != nil { + log.Fatalf("failed to open reseed_cert.pem for writing: %s", err) + } + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert}) + certOut.Close() + fmt.Println("certificate saved to reseed_cert.pem") +} diff --git a/cmd/reseeder.go b/cmd/reseeder.go index b12aa58..4998790 100644 --- a/cmd/reseeder.go +++ b/cmd/reseeder.go @@ -1,18 +1,20 @@ package cmd import ( + "fmt" "log" + "net/http" - // "github.com/MDrollette/go-i2p/reseed" + "github.com/MDrollette/go-i2p/reseed" "github.com/codegangsta/cli" ) -func NewReseederCommand() cli.Command { +func NewReseedCommand() cli.Command { return cli.Command{ - Name: "reseeder", + Name: "reseed", Usage: "Start a reseed server", Description: "Start a reseed server", - Action: reseederAction, + Action: reseedAction, Flags: []cli.Flag{ cli.StringFlag{ Name: "addr", @@ -23,6 +25,24 @@ func NewReseederCommand() cli.Command { } } -func reseederAction(c *cli.Context) { +func reseedAction(c *cli.Context) { log.Println("Starting server on", c.String("addr")) + + netdb := reseed.NewLocalNetDb(c.Args().Get(0)) + reseeder := reseed.NewReseeder(netdb) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + peer := reseeder.Peer(r) + seeds, err := reseeder.Seed(peer) + if nil != err { + fmt.Fprintf(w, "Problem: '%s'", err) + return + } + + for _, s := range seeds { + fmt.Fprintf(w, "%s\n", s.Name) + } + }) + + http.ListenAndServe("127.0.0.1:9090", nil) } diff --git a/cmd/su3.go b/cmd/su3.go new file mode 100644 index 0000000..3e767eb --- /dev/null +++ b/cmd/su3.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "crypto/x509" + "encoding/pem" + "io/ioutil" + "log" + "net/http" + + "github.com/MDrollette/go-i2p/reseed" + "github.com/MDrollette/go-i2p/su3" + "github.com/codegangsta/cli" +) + +func NewSu3Command() cli.Command { + return cli.Command{ + Name: "su3", + Usage: "Do SU3 things", + Description: "Do SU3 things", + Action: su3Action, + Flags: []cli.Flag{}, + } +} + +func su3Action(c *cli.Context) { + netdb := reseed.NewLocalNetDb(c.Args().Get(0)) + reseeder := reseed.NewReseeder(netdb) + + // make a fake request to get a peer + r, _ := http.NewRequest("GET", "/i2pseeds.su3", nil) + + peer := reseeder.Peer(r) + seeds, err := reseeder.Seed(peer) + if nil != err { + log.Fatalln(err) + return + } + + // load our signing privKey + privPem, err := ioutil.ReadFile("reseed_private.pem") + if nil != err { + log.Fatalln(err) + return + } + privDer, _ := pem.Decode(privPem) + privKey, err := x509.ParsePKCS1PrivateKey(privDer.Bytes) + if nil != err { + log.Fatalln(err) + return + } + + // create an SU3 from the seed + su3File, err := reseeder.CreateSu3(seeds) + su3File.SetSignerId("matt@drollette.com") + // sign the su3 with our key + su3File.Sign(privKey, su3.SIGTYPE_RSA_SHA512) + + //write the file to disk + ioutil.WriteFile("i2pseeds.su3", su3File.Bytes(), 0777) +} diff --git a/cmd/verify.go b/cmd/verify.go index 8c0a806..194b4dd 100644 --- a/cmd/verify.go +++ b/cmd/verify.go @@ -30,11 +30,11 @@ func su3VerifyAction(c *cli.Context) { panic(err) } + fmt.Println(su3File.String()) + if err := su3File.VerifySignature(); nil != err { panic(err) } - fmt.Println(su3File.String()) - fmt.Println("Verified signature.") } diff --git a/main.go b/main.go index 8fd6b3d..fb57d62 100644 --- a/main.go +++ b/main.go @@ -14,8 +14,10 @@ func main() { app.Usage = "I2P commands" app.Flags = []cli.Flag{} app.Commands = []cli.Command{ - cmd.NewReseederCommand(), + cmd.NewReseedCommand(), cmd.NewSu3VerifyCommand(), + cmd.NewKeygenCommand(), + cmd.NewSu3Command(), } if err := app.Run(os.Args); err != nil { diff --git a/reseed/server.go b/reseed/server.go index f4dc837..ef69188 100644 --- a/reseed/server.go +++ b/reseed/server.go @@ -1 +1,128 @@ package reseed + +import ( + "io/ioutil" + "log" + "math/rand" + "net/http" + "os" + "path/filepath" + "regexp" + "sync" + + "github.com/MDrollette/go-i2p/su3" +) + +type routerInfo struct { + Name string + Data []byte +} + +type Peer string +type Seed []routerInfo + +type Reseeder interface { + // seed a peer with routerinfos + Seed(p Peer) (Seed, error) + // get a peer from a given request + Peer(r *http.Request) Peer + // create an Su3 file from the given seeds + CreateSu3(seeds Seed) (*su3.Su3File, error) +} + +type ReseederImpl struct { + netdb NetDbProvider + peers map[string]Peer + m sync.Mutex +} + +func NewReseeder(netdb NetDbProvider) *ReseederImpl { + return &ReseederImpl{ + netdb: netdb, + peers: make(map[string]Peer), + } +} + +func (rs *ReseederImpl) Seed(p Peer) (Seed, error) { + all, err := rs.netdb.RouterInfos() + if nil != err { + return nil, err + } + + return Seed(all), nil +} + +func (rs *ReseederImpl) Peer(r *http.Request) Peer { + rs.m.Lock() + defer rs.m.Unlock() + + if p, ok := rs.peers[r.RemoteAddr]; !ok { + rs.peers[r.RemoteAddr] = Peer(r.RemoteAddr) + } else { + return p + } + + return rs.peers[r.RemoteAddr] +} + +func (rs *ReseederImpl) CreateSu3(seeds Seed) (*su3.Su3File, error) { + su3File := su3.NewSu3File() + su3File.FileType = su3.FILE_TYPE_ZIP + su3File.ContentType = su3.CONTENT_TYPE_RESEED + + zipped, err := zipSeeds(seeds) + if nil != err { + return nil, err + } + su3File.SetContent(zipped) + + return su3File, nil +} + +type NetDbProvider interface { + // Get all router infos + RouterInfos() ([]routerInfo, error) +} + +type LocalNetDbImpl struct { + Path string +} + +func NewLocalNetDb(path string) *LocalNetDbImpl { + return &LocalNetDbImpl{ + Path: path, + } +} + +func (db *LocalNetDbImpl) RouterInfos() (routerInfos []routerInfo, err error) { + var src []os.FileInfo + if src, err = ioutil.ReadDir(db.Path); nil != err { + return + } + + // randomize the file order + files := make([]os.FileInfo, len(src)) + perm := rand.Perm(len(src)) + for i, v := range perm { + files[v] = src[i] + } + + r, _ := regexp.Compile("^routerInfo-[A-Za-z0-9-=~]+.dat$") + + for _, file := range files { + if r.MatchString(file.Name()) { + riBytes, err := ioutil.ReadFile(filepath.Join(db.Path, file.Name())) + if nil != err { + log.Println(err) + continue + } + + routerInfos = append(routerInfos, routerInfo{ + Name: file.Name(), + Data: riBytes, + }) + } + } + + return +} diff --git a/reseed/service.go b/reseed/service.go new file mode 100644 index 0000000..4eaca94 --- /dev/null +++ b/reseed/service.go @@ -0,0 +1,4 @@ +package reseed + +type Su3Provider interface { +} diff --git a/reseed/utils.go b/reseed/utils.go new file mode 100644 index 0000000..018f976 --- /dev/null +++ b/reseed/utils.go @@ -0,0 +1,32 @@ +package reseed + +import ( + "archive/zip" + "bytes" +) + +func zipSeeds(seeds Seed) ([]byte, error) { + // Create a buffer to write our archive to. + buf := new(bytes.Buffer) + + // Create a new zip archive. + zipWriter := zip.NewWriter(buf) + + // Add some files to the archive. + for _, file := range seeds { + zipFile, err := zipWriter.Create(file.Name) + if err != nil { + return nil, err + } + _, err = zipFile.Write(file.Data) + if err != nil { + return nil, err + } + } + + if err := zipWriter.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/su3/reseed_certs.go b/su3/reseed_certs.go index f640d75..c1d63a9 100644 --- a/su3/reseed_certs.go +++ b/su3/reseed_certs.go @@ -19,6 +19,38 @@ func certForSigner(signer string) (*x509.Certificate, error) { var ( reseedKeys = map[string][]byte{ + "matt@drollette.com": []byte(`-----BEGIN CERTIFICATE----- +MIIFgjCCA2ygAwIBAgICBNIwCwYJKoZIhvcNAQELMFUxCzAJBgNVBAYTAlhYMR4w +HAYDVQQKExVJMlAgQW5vbnltb3VzIE5ldHdvcmsxDDAKBgNVBAsTA0kyUDELMAkG +A1UEBxMCWFgxCzAJBgNVBAkTAlhYMB4XDTE0MTIxMDA1MzQ1M1oXDTI0MTIxMDA1 +MzQ1M1owVTELMAkGA1UEBhMCWFgxHjAcBgNVBAoTFUkyUCBBbm9ueW1vdXMgTmV0 +d29yazEMMAoGA1UECxMDSTJQMQswCQYDVQQHEwJYWDELMAkGA1UECRMCWFgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDo0Y1eOyG/5GHJLZssrKCdtLCU +8MIfguIv0sU+Q69kGS5yCe1TlrhpSEvHDneZRWc+rbc+eKpPXUdReIFLeuS8cPh7 +dkLArx88qEaff2cx1Ss4g+wErVZcPlMf+6og50Z2JxEJe/7WW1B38eQ1f+6i5j33 +twruk0Xa9vAnyP+bO53PNyJw548N7qFA/nGeW/r88iYquFtGxGyVv8zMzGeHsmXm +7G+B6LG2FHecg5ZT2shaOCY27i6Rq08qiGV7+qZ6tGGUFFEr6cpcudrzA0yGzsyy +pIUdhWw2+r9AftF0Si4+ic5aIiZzmaBvuzdn6GQkmEQDt5KG6pj7RJN73qCWz85T +tUf/5QI4/0itTQnEtHFJA2Hh1OWRha6HSbHVcHuZdJUtCSKRyXHwMZgYM5e3PAW2 +uEuTPA+F81AKBnDWy2FVs/I80epr8526mkRIMTswqLJ8+/MZenmzJ2fUHItwNb4e +qSMrczmlTbB4xEWGKqEb/gij8qr+6Sd4EqR6B5jDo23I39iy33QN924KirlXb2Hl +kMWGjXf/qOEdoqvCMynhathAnVtYhJ1M7scw3Z5v035j/3uidyYX+lKQpf8kSCDo +igImjYewmzCZBgU8n3iCIfhuR+Fw8l3d6f6UdEIVF9Qi0EpJ9kCXyrXYhg3ulckU +S+MQcHqjUewDJUwMLwIDAQABo2AwXjAOBgNVHQ8BAf8EBAMCAIQwHQYDVR0lBBYw +FAYIKwYBBQUHAwIGCCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMBAf8wDAYDVR0OBAUE +AwECAzAOBgNVHSMEBzAFgAMBAgMwCwYJKoZIhvcNAQELA4ICAQARD/2NHcnXUE1F +W5exDU4RHQt4Y8FfwXY/ijBtdbHz39BEQ6+P5eS/JwcdXYEn3Kfoan5j42jiYZQq +LpQEJDSDYEy22FkN+NnDawdXX/PMOUs84TIYpPv5h0mESjA+3j6k+cXmnacATsIv +qv/ssrRkAL3yJ5T3MOqRTkYKWXPUAttylLbahRlMyi2l4tq1dsIuGGdEdarpkt78 +r0OkYaOO3yJDgloZhDZ1TrUgZ60HKdyUSUNn3QlXU5LlMPNJ2godqRpfsgBa3XZK +fxh0kM1KPJuVy9UzGiZuKWXEGY/q/hMDKcviKFWZb2P6mIU4Q7aT9ph/kQWJG8We +GK1uqLdHqKKHRMAK+KHVVwkbwy60mMDunsl6y9Q9q9uh5Mrre7uc36uO2h1IJUNV +O+1ifGNKrd27sgTmw7RofKetM/k9x/22wU7UDnUhnBCkZOjLyYFCNF6rT0l9CvVC +1aud5NWOPIIEoYMw7QRPWijw6wNqQ1hslm7d+boz/p0b+qk/bq12sw1Nfi5wOGV7 +dOZRnaoG2eCD+/RsrhbXymFbIpU+sLGuoFH4lTcfChhXvq2YgedzhrYNC6Sy1sq9 +rHSwJ5eBGMqE59+Sda/JkPiMw06VtPYqiGmqLPlFxnziX0o9Gw+v4dNZvuU3DZ+y +5wYKnjW5pFMnWeBlDXkVUPOIM+/paw== +-----END CERTIFICATE-----`), "backup@mail.i2p": []byte(`-----BEGIN CERTIFICATE----- MIIFfTCCA2WgAwIBAgIEOprmhjANBgkqhkiG9w0BAQ0FADBvMQswCQYDVQQGEwJY WDELMAkGA1UECBMCWFgxCzAJBgNVBAcTAlhYMR4wHAYDVQQKExVJMlAgQW5vbnlt diff --git a/su3/su3.go b/su3/su3.go index 736f057..bf5af40 100644 --- a/su3/su3.go +++ b/su3/su3.go @@ -3,14 +3,20 @@ package su3 import ( "archive/zip" "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" "crypto/x509" "encoding/binary" "fmt" "os" + "strconv" + "time" ) const ( - MAGIC_BYTES = "I2Psu3" + MAGIC_BYTES = "I2Psu3" + MIN_VERSION_LENGTH = 16 SIGTYPE_DSA = uint16(0) SIGTYPE_ECDSA_SHA256 = uint16(1) @@ -34,14 +40,14 @@ const ( type Su3File struct { Magic [6]byte - Format [1]byte + Format uint8 SignatureType uint16 SignatureLength uint16 VersionLength uint8 SignerIdLength uint8 ContentLength uint64 - FileType [1]byte - ContentType [1]byte + FileType uint8 + ContentType uint16 Version []byte SignerId []byte @@ -50,6 +56,129 @@ type Su3File struct { SignedBytes []byte } +func NewSu3File() *Su3File { + var a [6]byte + copy(a[:], MAGIC_BYTES) + s := Su3File{Magic: a} + s.SetVersion(strconv.FormatInt(time.Now().Unix(), 10)) + return &s +} + +func (s *Su3File) SetSignerId(signer string) { + s.SignerId = []byte(signer) + s.SignerIdLength = uint8(len(s.SignerId)) +} + +func (s *Su3File) SetContent(content []byte) { + s.Content = content + s.ContentLength = uint64(len(s.Content)) +} + +func (s *Su3File) SetVersion(version string) { + s.Version = []byte(version) + + minBytes := make([]byte, MIN_VERSION_LENGTH) + if len(s.Version) < len(minBytes) { + copy(minBytes, s.Version) + s.Version = minBytes + } + + s.VersionLength = uint8(len(s.Version)) +} + +func (s *Su3File) Sign(privkey *rsa.PrivateKey, sigType uint16) error { + var hashType crypto.Hash + switch sigType { + // case SIGTYPE_DSA: + // case SIGTYPE_ECDSA_SHA256: + // case SIGTYPE_ECDSA_SHA384: + // case SIGTYPE_ECDSA_SHA512: + // case SIGTYPE_RSA_SHA256: + // case SIGTYPE_RSA_SHA384: + case SIGTYPE_RSA_SHA512: + s.SignatureType = SIGTYPE_RSA_SHA512 + s.SignatureLength = uint16(512) + hashType = crypto.SHA512 + default: + return fmt.Errorf("Unknown signature type") + } + + h := hashType.New() + h.Write(s.ContentBytes()) + digest := h.Sum(nil) + + sig, err := rsa.SignPKCS1v15(rand.Reader, privkey, 0, digest) + if nil != err { + return err + } + + s.Signature = sig + + return nil +} + +func (s *Su3File) ContentBytes() []byte { + buf := new(bytes.Buffer) + + var ( + skip [1]byte + bigSkip [12]byte + ) + + // 0-5 + binary.Write(buf, binary.BigEndian, s.Magic) + // 6 + binary.Write(buf, binary.BigEndian, skip) + // 7 + binary.Write(buf, binary.BigEndian, s.Format) + // 8-9 + binary.Write(buf, binary.BigEndian, s.SignatureType) + // 10-11 + binary.Write(buf, binary.BigEndian, s.SignatureLength) + // 12 + binary.Write(buf, binary.BigEndian, skip) + // 13 + binary.Write(buf, binary.BigEndian, s.VersionLength) + // 14 + binary.Write(buf, binary.BigEndian, skip) + // 15 + binary.Write(buf, binary.BigEndian, s.SignerIdLength) + // 16-23 + binary.Write(buf, binary.BigEndian, s.ContentLength) + // 24 + binary.Write(buf, binary.BigEndian, skip) + // 25 + binary.Write(buf, binary.BigEndian, s.FileType) + // 26 + binary.Write(buf, binary.BigEndian, skip) + // 27 + binary.Write(buf, binary.BigEndian, s.ContentType) + // 28-39 + binary.Write(buf, binary.BigEndian, bigSkip) + // 40-55+ Version, UTF-8 padded with trailing 0x00, 16 bytes minimum, length specified at byte 13. Do not append 0x00 bytes if the length is 16 or more. + binary.Write(buf, binary.BigEndian, s.Version) + // xx+ ID of signer, (e.g. "zzz@mail.i2p") UTF-8, not padded, length specified at byte 15 + binary.Write(buf, binary.BigEndian, s.SignerId) + // xx+ Content, length and format specified in header + binary.Write(buf, binary.BigEndian, s.Content) + + return buf.Bytes() +} + +func (s *Su3File) Bytes() []byte { + buf := new(bytes.Buffer) + buf.Write(s.ContentBytes()) + + // xx+ Signature, length specified in header, covers everything starting at byte 0 + binary.Write(buf, binary.BigEndian, s.Signature) + + return buf.Bytes() +} + +func (s *Su3File) VerifySignature() error { + return verifySig(s.SignatureType, s.SignerId, s.Signature, s.SignedBytes) +} + func (s *Su3File) String() string { var b bytes.Buffer @@ -72,17 +201,13 @@ func (s *Su3File) String() string { fmt.Fprintf(&b, "Version: %q\n", bytes.Trim(s.Version, "\x00")) fmt.Fprintf(&b, "SignerId: %q\n", s.SignerId) // fmt.Fprintf(&b, "Content: %q\n", s.Content) - // fmt.Fprintf(&b, "Signature: %q\n", s.Signature) + fmt.Fprintf(&b, "Signature: %q\n", s.Signature) fmt.Fprintln(&b, "---------------------------") return b.String() } -func (s *Su3File) VerifySignature() error { - return verifySig(s.SignatureType, s.SignerId, s.Signature, s.SignedBytes) -} - func uzipData(c []byte) ([]byte, error) { input := bytes.NewReader(c) zipReader, err := zip.NewReader(input, int64(len(c)))