Skip to content

Commit

Permalink
⚡️ Preparing for future developments.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mihara committed Jun 19, 2023
1 parent eff21c2 commit 4d21fe5
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 23 deletions.
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

This is highly experimental.

Do not use this program for anything critical. Right now it's still very much an evening project waiting for feedback from people trying to use it.
Do not use this program for anything critical. Right now it's still very much an evening project waiting for feedback from people trying to use it. The signature format is not yet stable, and changes in the near future are likely to introduce an incompatibility.

Please experiment with it, that's the right word.

## What is it?

Expand Down Expand Up @@ -42,6 +44,14 @@ Which means that if LoTW made a new layer #2 key after you got your #3 key and r

`lotw-trust` attempts to work around this by keeping a list of layer #1 and #2 keys known to belong to LoTW, -- that is, I took them from *my* `.tq6` file -- and, when signing things, packing every public key that comes in your `.tq6` file that it hasn't seen before in with the signature. However, I anticipate this will not be sufficient long term, and `lotw-trust` will need to be updated on average no less than once a year to keep working.

### Certificate revocation

There is currently no way for us to know if a user's certificate has been revoked or not. LoTW does not expose this information publicly anywhere, so you can only know if it expired, because that's written inside the certificate itself.

### Date of signing

There is currently no provision to date the signing of the file, i.e. if you check the signature of a file signed in the past after the public key used in it has expired, it will be considered invalid. I am not sure what is the correct way to deal with this just yet.

### RSA keys

`lotw-trust` currently assumes that LoTW issues and will forever issue only RSA-based x509 certificates. This is not guaranteed. In fact, it'd be better if they switched to something more modern, even if I would have to code to handle that.
Expand All @@ -63,16 +73,26 @@ You can get a `.p12` file with your private key and all the associated public ke

See `lotw-trust --help` and `lotw-trust <command> --help` for further options, not that there are any yet, except the one to supply a password for your `.p12` file, if you've set one for whatever reason.

The signature block tries to be compact, *(about 1500 bytes if everything is well, can't be much shorter than that)* and is appended to the end of the file. For a good number of files, extra data tacked onto the end will not have any effect on the way their native programs process them: `zip` files unpack just as they did, `png` and `jpg` files remain viewable, and only plaintext formats will suffer from the appearance of a binary blob on the end.
The signature block tries to be compact, *(about 1500 bytes if everything is well, can't be much shorter than that)* and is appended to the end of the file. For a good number of file formats, extra data tacked onto the end will not have any effect on the way their native programs process them: `zip` files unpack just as they did, `png` and `jpg` files remain viewable, and only plaintext formats will suffer from the appearance of a binary blob on the end.

## Installation and compilation

This is a [Go](https://go.dev/) program, so this should be easy enough, provided you have a working Go installation:

go install github.com/mihara/lotw-trust

Binaries are provided in the releases section. At the moment, it's very probable most of them don't actually run. There will be a version for Raspberry later on.
Binaries are provided in the releases section. At the moment, it's very probable most of them don't actually run.

## Plans for future development

Since so far, every comment about this that I received has been positive, even if the number of comments have been small, here's what I'm going to do next:

1. Clean the thing up and make error messages make sense where possible.
2. Reading and writing standard input and standard output.
3. An ASCII-armor style file format specifically designed for signing text messages, so that you could in theory stick the signer inside Winlink as a filter.
4. Ability to save the signature block completely separately from the signed file and read such signatures.
5. Ability to omit the public key from the message, which should reduce the file size increase introduced by signing from ~1500 bytes to ~180, as well as the ability to cache public keys when signatures are verified. This way, you would send your first message to someone with a full signature, and subsequent ones could be abbreviated.

## License

This program is released under the term of MIT license. See the full text in [LICENSE](LICENSE)
This program is released under the terms of MIT license. See the full text in [LICENSE](LICENSE)
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/Mihara/lotw-trust
go 1.20

require (
github.com/blang/semver/v4 v4.0.0
github.com/fxamacker/cbor/v2 v2.4.0
github.com/integrii/flaggy v1.5.2
software.sslmate.com/src/go-pkcs12 v0.2.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
Expand Down
71 changes: 53 additions & 18 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"reflect"

"github.com/blang/semver/v4"
"github.com/fxamacker/cbor/v2"
"github.com/integrii/flaggy"
"software.sslmate.com/src/go-pkcs12"
Expand All @@ -21,6 +22,7 @@ import (
var version string

const sigHeader = "\nLOTW-TRUST SIG\n"
const footerSize = 2 // uint16 to keep the size of the sig block.

var signCmd *flaggy.Subcommand
var verifyCmd *flaggy.Subcommand
Expand All @@ -31,10 +33,16 @@ var inputFile string
var outputFile string

// SigBlock is a struct containing the signature and associated data.
// This structure is meant to be stable from here on out, and is encoded
// in the file in a standard CBOR packet, see https://cbor.io/
type SigBlock struct {
Sig []byte `cbor:"0,keyasint"`
Cer []byte `cbor:"1,keyasint"`
Ca [][]byte `cbor:"2,keyasint,omitempty"`
Version string `cbor:"0,keyasint"`
Callsign string `cbor:"1,keyasint"`
Signature []byte `cbor:"2,keyasint,omitempty"`
// 3 is reserved.
Certificate []byte `cbor:"4,keyasint,omitempty"`
// 5 is reserved.
CA [][]byte `cbor:"6,keyasint,omitempty"`
}

//go:embed roots/*.der
Expand Down Expand Up @@ -101,6 +109,8 @@ func main() {
l := log.New(os.Stderr, "", 1)
l.SetFlags(0)

myVersion, _ := semver.Parse(version)

// Parse our embedded root certificates.
// Because x509 stupidly does not export more data about certpool structure,
// I have to duplicate it.
Expand All @@ -124,7 +134,6 @@ func main() {
pKey, cert, caChain, err := pkcs12.DecodeChain(keyData, keyPass)
check(err)

// Yes, the callsign really has this type.
callsign := getCallsign(*cert)
if callsign == "" {
l.Fatal("The signing key does not appear to be a LoTW key.")
Expand All @@ -136,10 +145,13 @@ func main() {
check(err)

hashed := sha256.Sum256(fileData)

signature, err := rsa.SignPKCS1v15(nil, pKey.(*rsa.PrivateKey), crypto.SHA256, hashed[:])
signature, err := rsa.SignPKCS1v15(nil,
pKey.(*rsa.PrivateKey),
crypto.SHA256,
hashed[:],
)
if err != nil {
l.Fatal("Signing failure, which probably means a new type of LoTW key:", err)
l.Fatal("Signing failure, which probably means a new type of LoTW key: ", err)
return
}

Expand All @@ -153,14 +165,18 @@ func main() {
}

// And there we have it, our signature data.
sig := SigBlock{signature, cert.Raw, certlist}
sig := SigBlock{
Version: version,
Callsign: callsign,
Signature: signature,
Certificate: cert.Raw,
CA: certlist,
}

// Now we're back to trying to stuff them into a sig block.
buf, err := cbor.Marshal(sig)
check(err)

// TODO: Set up base64 sigs around here, if I'm doing this at all.

sigBlock := append([]byte(sigHeader), buf...)
// If the sig block somehow got longer than 65kb, we have a problem anyway.
sigLen := uint16(len(sigBlock))
Expand All @@ -184,38 +200,51 @@ func main() {
check(err)

// The last two bytes of the file are the size of the sig block.
lb := fileData[len(fileData)-2:]
lb := fileData[len(fileData)-footerSize:]
lbBuf := new(bytes.Buffer)
_, _ = lbBuf.Write(lb)
var sigLen uint16
err = binary.Read(lbBuf, binary.BigEndian, &sigLen)
check(err)

split := len(fileData) - 2 - int(sigLen)
split := len(fileData) - footerSize - int(sigLen)
sigBlock := fileData[split:]
fileData = fileData[:split]

if !reflect.DeepEqual(sigBlock[:len(sigHeader)], []byte(sigHeader)) {
l.Fatal("File does not appear to be signed.")
}
hashed := sha256.Sum256(fileData)

// Now we need to unmarshal the sig.
var sigData SigBlock
err = cbor.Unmarshal(sigBlock[len(sigHeader):], &sigData)
check(err)
cert, err := x509.ParseCertificate(sigData.Cer)
cert, err := x509.ParseCertificate(sigData.Certificate)
check(err)

// Build the pool of intermediary certs supplied with it.
// We can verify the signatures on versions lower than ours, but not vice versa.
sigVersion, err := semver.Parse(sigData.Version)
check(err)
if myVersion.Compare(sigVersion) < 0 {
l.Fatal("File is signed with a newer version of lotw-trust than v{}", myVersion)
}

// Build the pool of intermediary certs supplied with the sig.
extraCerts := x509.NewCertPool()
for _, der := range sigData.Ca {
crt, _ := x509.ParseCertificate(der)
for _, der := range sigData.CA {
crt, err := x509.ParseCertificate(der)
check(err)
extraCerts.AddCert(crt)
}

// Verify the actual signature.
err = rsa.VerifyPKCS1v15(cert.PublicKey.(*rsa.PublicKey), crypto.SHA256, hashed[:], sigData.Sig)
hashed := sha256.Sum256(fileData)
err = rsa.VerifyPKCS1v15(
cert.PublicKey.(*rsa.PublicKey),
crypto.SHA256,
hashed[:],
sigData.Signature,
)
check(err)

_, err = cert.Verify(x509.VerifyOptions{
Expand All @@ -225,6 +254,12 @@ func main() {
})
check(err)
l.Println("Signed by:", getCallsign(*cert))

if err := os.WriteFile(outputFile, fileData, 0666); err != nil {
l.Fatal(err)
}

// Everything went fine!
os.Exit(0)

} else {
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.1
0.0.2

0 comments on commit 4d21fe5

Please sign in to comment.