// Copyright 2023 Cloudflare, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package protocol import "fmt" // ErrorType is an error type. type ErrorType uint16 const ( ErrorDecode ErrorType = iota ErrorNonceLen ErrorRequestLen ErrorUnsupportedVersion ErrorMissingVersion ) // Error represents a protocol error. type Error struct { // Type is the error type. Type ErrorType // Info includes optional info. Info string } func (e Error) Error() string { s := "" switch e.Type { case ErrorDecode: s += "decode" case ErrorNonceLen: s += "nonce length" case ErrorRequestLen: s += "request length" case ErrorUnsupportedVersion: s += "no version in common" case ErrorMissingVersion: s += "missing VER tag" default: s += "unknown" } if len(e.Info) > 0 { s += ": " + e.Info } return s } func errDecode(info string) Error { return Error{ Type: ErrorDecode, Info: info, } } func errUnsupportedVersion(vers []Version) Error { return Error{ Type: ErrorUnsupportedVersion, Info: fmt.Sprintf("%q", vers), } } var ( errNonceLen = Error{ Type: ErrorNonceLen, Info: "", } errRequestLen = Error{ Type: ErrorRequestLen, Info: "", } errMissingVersion = Error{ Type: ErrorMissingVersion, Info: "", } )
// Copyright 2016 The Roughtime Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. */ // Modifications copyright 2023 Cloudflare, Inc. // // The code has been extended to support IETF-Roughtime. // Package protocol implements the core of the Roughtime protocol. package protocol import ( "bytes" "crypto/ed25519" "crypto/sha512" "encoding/binary" "errors" "fmt" "io" "math" "sort" "time" ) const ( ietfRoughtimeFrame = "ROUGHTIM" maxNonceSize = sha512.Size // MinRequestSize is the minimum number of bytes in a request. MinRequestSize = 1024 certificateContext = "RoughTime v1 delegation signature--\x00" signedResponseContext = "RoughTime v1 response signature\x00" ) // makeTag converts a four character string into a Roughtime tag value. func makeTag(tag string) uint32 { if len(tag) != 4 { panic("makeTag: len(tag) != 4: " + tag) } return uint32(tag[0]) | uint32(tag[1])<<8 | uint32(tag[2])<<16 | uint32(tag[3])<<24 } var ( // Various tags used in the Roughtime protocol. tagCERT = makeTag("CERT") tagDELE = makeTag("DELE") tagINDX = makeTag("INDX") tagMAXT = makeTag("MAXT") tagMIDP = makeTag("MIDP") tagMINT = makeTag("MINT") tagNONC = makeTag("NONC") tagPAD = makeTag("PAD\xff") tagPATH = makeTag("PATH") tagPUBK = makeTag("PUBK") tagRADI = makeTag("RADI") tagROOT = makeTag("ROOT") tagSIG = makeTag("SIG\x00") tagSREP = makeTag("SREP") tagSRV = makeTag("SRV\x00") tagVER = makeTag("VER\x00") tagZZZZ = makeTag("ZZZZ") ) // tagsSlice is the type of an array of tags. It provides utility functions so // that they can be sorted. type tagsSlice []uint32 func (t tagsSlice) Len() int { return len(t) } func (t tagsSlice) Less(i, j int) bool { return t[i] < t[j] } func (t tagsSlice) Swap(i, j int) { t[i], t[j] = t[j], t[i] } // Encode converts a map of tags to bytestrings into an encoded message. The // number of elements in msg and the sum of the lengths of all the bytestrings // must be ≤ 2**32. func Encode(msg map[uint32][]byte) ([]byte, error) { if len(msg) == 0 { return make([]byte, 4), nil } if len(msg) >= math.MaxInt32 { return nil, errors.New("encode: too many tags") } var payloadSum uint64 for _, payload := range msg { if len(payload)%4 != 0 { return nil, errors.New("encode: length of value is not a multiple of four") } payloadSum += uint64(len(payload)) } if payloadSum >= 1<<32 { return nil, errors.New("encode: payloads too large") } tags := tagsSlice(make([]uint32, 0, len(msg))) for tag := range msg { tags = append(tags, tag) } sort.Sort(tags) numTags := uint64(len(tags)) encoded := make([]byte, 4*(1+numTags-1+numTags)+payloadSum) binary.LittleEndian.PutUint32(encoded, uint32(len(tags))) offsets := encoded[4:] tagBytes := encoded[4*(1+(numTags-1)):] payloads := encoded[4*(1+(numTags-1)+numTags):] currentOffset := uint32(0) for i, tag := range tags { payload := msg[tag] if i > 0 { binary.LittleEndian.PutUint32(offsets, currentOffset) offsets = offsets[4:] } binary.LittleEndian.PutUint32(tagBytes, tag) tagBytes = tagBytes[4:] if len(payload) > 0 { copy(payloads, payload) payloads = payloads[len(payload):] currentOffset += uint32(len(payload)) } } return encoded, nil } // Decode parses the output of encode back into a map of tags to bytestrings. func Decode(bytes []byte) (map[uint32][]byte, error) { if len(bytes) < 4 { return nil, errDecode("message too short to be valid") } if len(bytes)%4 != 0 { return nil, errDecode("message is not a multiple of four bytes") } numTags := uint64(binary.LittleEndian.Uint32(bytes)) if numTags == 0 { return make(map[uint32][]byte), nil } minLen := 4 * (1 + (numTags - 1) + numTags) if uint64(len(bytes)) < minLen { return nil, errDecode("message too short to be valid") } offsets := bytes[4:] tags := bytes[4*(1+numTags-1):] payloads := bytes[minLen:] if len(payloads) > math.MaxInt32 { return nil, errDecode("message too large") } payloadLength := uint32(len(payloads)) currentOffset := uint32(0) var lastTag uint32 ret := make(map[uint32][]byte) for i := uint64(0); i < numTags; i++ { tag := binary.LittleEndian.Uint32(tags) tags = tags[4:] if i > 0 && lastTag >= tag { return nil, errDecode("tags out of order") } var nextOffset uint32 if i < numTags-1 { nextOffset = binary.LittleEndian.Uint32(offsets) offsets = offsets[4:] } else { nextOffset = payloadLength } if nextOffset%4 != 0 { return nil, errDecode("payload length is not a multiple of four bytes") } if nextOffset < currentOffset { return nil, errDecode("offsets out of order") } length := nextOffset - currentOffset if uint32(len(payloads)) < length { return nil, errDecode("message truncated") } payload := payloads[:length] payloads = payloads[length:] ret[tag] = payload currentOffset = nextOffset lastTag = tag } return ret, nil } // messageOverhead returns the number of bytes needed for Encode to encode the // given number of tags. func messageOverhead(versionIETF bool, numTags int) int { framing := 0 if versionIETF { framing = 12 // "ROUGHTIM" + message length (4 bytes) } return framing + 4*2*numTags } // nonceSize returns the nonce length. func nonceSize(versionIETF bool) int { if !versionIETF { return 64 } return 32 } // CalculateChainNonce fills the `nonce` buffer with the nonce used in the next // request in a chain given a reply and a blinding factor. The length of the // buffer is expected to match the nonce length for the protocol version. func CalculateChainNonce(nonce, prevReply, blind []byte) { var out [maxNonceSize]byte h := sha512.New() h.Write(prevReply) h.Sum(out[:0]) h.Reset() h.Write(out[:]) h.Write(blind) h.Sum(out[:0]) copy(nonce, out[:]) } // encodeFramed adds IETF message framing to a message. func encodeFramed(versionIETF bool, msg []byte) []byte { if versionIETF { framedMsg := make([]byte, 0, 12+len(msg)) framedMsg = append(framedMsg, ietfRoughtimeFrame...) framedMsg = binary.LittleEndian.AppendUint32(framedMsg, uint32(len(msg))) framedMsg = append(framedMsg, msg...) msg = framedMsg } return msg } // decodeFramed determines if the requester is a legacy client // (Google-Roughtime) or supports the IETF version. In the later case, it // removes the IETF message framing. func decodeFramed(req []byte) ([]byte, bool, error) { // In the IETF version of Roughtime, the first eight bytes of the datagram are // equal to "ROUGHTIM". In Google-Roughtime, the first four bytes encode the // number of tags. This is therefore a good distinguisher as long as "ROUG", // interpreted as a little-endian uint32, is not a valid number of tags. versionIETF := len(req) >= 8 && bytes.Equal(req[:8], []byte(ietfRoughtimeFrame)) if versionIETF { if len(req) < 8 { return nil, false, errDecode("request is too short to encode message frame") } req = req[8:] if len(req) < 4 { return nil, false, errDecode("request is too short to encode the message length") } roughtimeMessageLen := binary.LittleEndian.Uint32(req[:4]) req = req[4:] if len(req) != int(roughtimeMessageLen) { return nil, false, errDecode("message has unexpected length") } } return req, versionIETF, nil } func handleSRVTag(advertisedPreference []Version) bool { for _, version := range advertisedPreference { // The SRV tag is first defined in draft-ietf-ntp-roughtime-11 if version == VersionDraft11 { return true } } return false } // CreateRequest creates a Roughtime request given an entropy source and the // contents of a previous reply for chaining. If this request is the first of a // chain, prevReply can be empty. It returns the nonce (needed to verify the // reply), the blind (needed to prove correct chaining to an external party) // and the request itself. func CreateRequest(versionPreference []Version, rand io.Reader, prevReply []byte, rootPublicKey ed25519.PublicKey) (nonce, blind []byte, request []byte, err error) { advertisedVersions, versionIETF, err := advertisedVersionsFromPreference(versionPreference) if err != nil { return nil, nil, nil, err } nonceSize := nonceSize(versionIETF) nonce = make([]byte, nonceSize) blind = make([]byte, nonceSize) if _, err := io.ReadFull(rand, blind); err != nil { return nil, nil, nil, err } CalculateChainNonce(nonce, prevReply, blind) // Construct the packet. packet := make(map[uint32][]byte) valuesLen := 0 numTags := 0 // NONC packet[tagNONC] = nonce valuesLen += len(nonce) numTags += 1 // VER if versionIETF { encoded := make([]byte, 0, len(advertisedVersions)*4) for _, ver := range advertisedVersions { encoded = binary.LittleEndian.AppendUint32(encoded, uint32(ver)) } packet[tagVER] = encoded valuesLen += len(encoded) numTags += 1 } // SRV if handleSRVTag(advertisedVersions) { srv := make([]byte, 0, 64) h := sha512.New() h.Write([]byte{0xff}) h.Write(rootPublicKey) h.Sum(srv) srv = srv[:32] packet[tagSRV] = srv valuesLen += len(nonce) numTags += 1 } // Padding (PAD in Google-Roughtime or ZZZZ in the IETF version) var paddingTag uint32 if versionIETF { paddingTag = tagZZZZ } else { paddingTag = tagPAD } padding := make([]byte, MinRequestSize-messageOverhead(versionIETF, numTags+1)-valuesLen) packet[paddingTag] = padding msg, err := Encode(packet) if err != nil { return nil, nil, nil, err } return nonce, blind, encodeFramed(versionIETF, msg), nil } // tree represents a Merkle tree of nonces. Each element of values is a layer // in the tree, with the widest layer first. type tree struct { values [][][maxNonceSize]byte } var ( hashLeafTweak = []byte{0} hashNodeTweak = []byte{1} ) // hashLeaf hashes an nonce to form the leaf of the Merkle tree. func hashLeaf(out *[maxNonceSize]byte, in []byte) { h := sha512.New() h.Write(hashLeafTweak) h.Write(in) h.Sum(out[:0]) } // hashNode hashes two child elements of the Merkle tree to produce an interior // node. func hashNode(out *[maxNonceSize]byte, left, right []byte) { h := sha512.New() h.Write(hashNodeTweak) h.Write(left) h.Write(right) h.Sum(out[:0]) } // newTree creates a Merkle tree given one or more nonces. func newTree(nonceSize int, requests []Request) *tree { if len(requests) == 0 { panic("newTree: passed empty slice") } levels := 1 width := len(requests) for width > 1 { width = (width + 1) / 2 levels++ } ret := &tree{ values: make([][][maxNonceSize]byte, 0, levels), } leaves := make([][maxNonceSize]byte, ((len(requests)+1)/2)*2) for i, req := range requests { var leaf [maxNonceSize]byte hashLeaf(&leaf, req.Nonce) leaves[i] = leaf } // Fill any extra leaves with an existing leaf, to simplify analysis // that we are not inadvertently signing other messages. for i := len(requests); i < len(leaves); i++ { leaves[i] = leaves[0] } ret.values = append(ret.values, leaves) for i := 1; i < levels; i++ { lastLevel := ret.values[i-1] width := len(lastLevel) / 2 if width%2 == 1 { width++ } level := make([][maxNonceSize]byte, width) for j := 0; j < len(lastLevel)/2; j++ { hashNode(&level[j], lastLevel[j*2][:nonceSize], lastLevel[j*2+1][:nonceSize]) } // Fill the extra node with an existing node, to simplify // analysis that we are not inadvertently signing other // messages. if len(lastLevel)/2 < len(level) { level[len(lastLevel)/2] = level[0] } ret.values = append(ret.values, level) } return ret } // Root returns the root value of t. func (t *tree) Root() *[maxNonceSize]byte { return &t.values[len(t.values)-1][0] } // Levels returns the number of levels in t. func (t *tree) Levels() int { return len(t.values) } // Path returns elements from t needed to prove, given the root, that the leaf // at the given index is in the tree. func (t *tree) Path(index int) (path [][]byte) { path = make([][]byte, 0, len(t.values)) for level := 0; level < len(t.values)-1; level++ { if index%2 == 1 { path = append(path, t.values[level][index-1][:]) } else { path = append(path, t.values[level][index+1][:]) } index /= 2 } return path } // Request is a request sent by a client. type Request struct { // Nonce is the request nonce. Nonce []byte // Nonce is the sequence of versions advertised by the client, ordered from // most to least preferred. Versions []Version // srv is the SRV tag indicating which root public key the client is // expecting to verify the response with. srv []byte } // ParseRequest resolves the supported versions indicated by the client and // parses the values required to produce a response. func ParseRequest(bytes []byte) (req *Request, err error) { if len(bytes) < MinRequestSize { return nil, errRequestLen } msg, versionIETF, err := decodeFramed(bytes) if err != nil { return nil, err } packet, err := Decode(msg) if err != nil { return nil, err } nonce, ok := packet[tagNONC] if !ok || len(nonce) != nonceSize(versionIETF) { return nil, errNonceLen } var versions []Version if versionIETF { encoded, ok := packet[tagVER] if !ok { return nil, errMissingVersion } for len(encoded) > 0 { if len(encoded) < 4 { return nil, errDecode("malformed version list") } ver := Version(binary.LittleEndian.Uint32(encoded[:4])) if ver.isSupported() { // De-duplicate any repeated version. ok := true for i := range versions { if versions[i] == ver { ok = false break } } if ok { versions = append(versions, ver) } } encoded = encoded[4:] } } else { versions = []Version{VersionGoogle} } var srv []byte if handleSRVTag(versions) { srv = packet[tagSRV] } return &Request{nonce, versions, srv}, nil } // CreateReplies signs, using privateKey, a batch of nonces along with the // given time and radius. It returns one reply for each nonce using that // signature and includes cert in each. // // The same version is indicated in each reply. It's the callers responsibility // to ensure that each client supports this version. Likewise, the server // indicated by each request, if any, must match the certificate. func CreateReplies(ver Version, requests []Request, midpoint time.Time, radius time.Duration, cert *Certificate) ([][]byte, error) { versionIETF := ver != VersionGoogle nonceSize := nonceSize(versionIETF) for i := range requests { if len(requests[i].srv) > 0 && !bytes.Equal(requests[i].srv, cert.srv) { return nil, fmt.Errorf("request %d indicates the wrong server", i) } } if len(requests) == 0 { return nil, nil } tree := newTree(nonceSize, requests) // Convert the midpoint and radius to their Roughtime representation. var midPointUint64 uint64 var radiusUint32 uint32 if versionIETF { midPointUint64 = uint64(midpoint.Unix()) radiusUint32 = uint32(radius.Seconds()) } else { midPointUint64 = uint64(midpoint.UnixMicro()) radiusUint32 = uint32(radius.Microseconds()) } var midpointBytes [8]byte binary.LittleEndian.PutUint64(midpointBytes[:], midPointUint64) var radiusBytes [4]byte binary.LittleEndian.PutUint32(radiusBytes[:], radiusUint32) signedReply := map[uint32][]byte{ tagMIDP: midpointBytes[:], tagRADI: radiusBytes[:], tagROOT: tree.Root()[:nonceSize], } signedReplyBytes, err := Encode(signedReply) if err != nil { return nil, err } toBeSigned := signedResponseContext + string(signedReplyBytes) sig := ed25519.Sign(cert.onlinePrivateKey, []byte(toBeSigned)) reply := map[uint32][]byte{ tagSREP: signedReplyBytes, tagSIG: sig, tagCERT: cert.BytesForVersion(ver), } if versionIETF { encoded := make([]byte, 0, 4) encoded = binary.LittleEndian.AppendUint32(encoded, uint32(ver)) reply[tagVER] = encoded } replies := make([][]byte, 0, len(requests)) for i := range requests { var indexBytes [4]byte binary.LittleEndian.PutUint32(indexBytes[:], uint32(i)) reply[tagINDX] = indexBytes[:] reply[tagNONC] = requests[i].Nonce path := tree.Path(i) pathBytes := make([]byte, 0, nonceSize*len(path)) for _, pathStep := range path { pathBytes = append(pathBytes, pathStep[:nonceSize]...) } reply[tagPATH] = pathBytes replyBytes, err := Encode(reply) if err != nil { return nil, err } replies = append(replies, encodeFramed(versionIETF, replyBytes)) } return replies, nil } type Certificate struct { // googleBytes is the certificate we send to legacy clients // (Google-Roughtime). The MINT and MAXT fields encode timestamps in // microseconds. googleBytes []byte bytes []byte // onlinePrivateKey is the online private key. onlinePrivateKey ed25519.PrivateKey // srv is the payload of the SRV tag that the client would send to indicate // the root public key delegated by this certificate. srv []byte } // BytesForVersion returns a serialized certificate compatible with the given // version. Legacy clients (Google-Roughtime) expect a non-standard encoding of // the MINT and MAXT fields. func (cert *Certificate) BytesForVersion(ver Version) []byte { switch ver { case VersionGoogle: return cert.googleBytes default: return cert.bytes } } // Select a certificate suitable for responding to the request. func SelectCertificateForRequest(req *Request, certs []Certificate) *Certificate { // Return the first certificate for which the root public key was indicated // by the client. for _, cert := range certs { if bytes.Equal(req.srv, cert.srv) { return &cert } } // If no SRV tag was sent, then guess the first certificate. if len(req.srv) == 0 && len(certs) > 0 { return &certs[0] } // The SRV tag indicates an unknown root public key, or the certificate // list is empty. return nil } // NewCertificate returns a signed certificate, using rootPrivateKey, // delegating authority for the given timestamp to onlinePrivateKey. func NewCertificate(minTime, maxTime time.Time, onlinePrivateKey, rootPrivateKey ed25519.PrivateKey) (cert *Certificate, err error) { if maxTime.Before(minTime) { return nil, errors.New("protocol: maxTime < minTime") } certs := make([][]byte, 2) for i, t := range []struct { minTime uint64 maxTime uint64 }{ // Legacy (Google-Roughtime) { minTime: uint64(minTime.UnixMicro()), maxTime: uint64(maxTime.UnixMicro()), }, // IETF { minTime: uint64(minTime.Unix()), maxTime: uint64(maxTime.Unix()), }, } { var minTimeBytes, maxTimeBytes [8]byte binary.LittleEndian.PutUint64(minTimeBytes[:], t.minTime) binary.LittleEndian.PutUint64(maxTimeBytes[:], t.maxTime) signed := map[uint32][]byte{ tagPUBK: ed25519.PrivateKey(onlinePrivateKey).Public().(ed25519.PublicKey), tagMINT: minTimeBytes[:], tagMAXT: maxTimeBytes[:], } signedBytes, err := Encode(signed) if err != nil { return nil, err } toBeSigned := certificateContext + string(signedBytes) sig := ed25519.Sign(rootPrivateKey, []byte(toBeSigned)) cert := map[uint32][]byte{ tagSIG: sig, tagDELE: signedBytes, } certs[i], err = Encode(cert) if err != nil { return nil, err } } // SRV srv := make([]byte, 0, 64) h := sha512.New() h.Write([]byte{0xff}) h.Write(ed25519.PrivateKey(rootPrivateKey).Public().(ed25519.PublicKey)) h.Sum(srv) srv = srv[:32] return &Certificate{ googleBytes: certs[0], bytes: certs[1], onlinePrivateKey: onlinePrivateKey, srv: srv, }, nil } func getValue(msg map[uint32][]byte, tag uint32, name string) (value []byte, err error) { value, ok := msg[tag] if !ok { return nil, errors.New("protocol: missing " + name) } return value, nil } func getFixedLength(msg map[uint32][]byte, tag uint32, name string, length int) (value []byte, err error) { value, err = getValue(msg, tag, name) if err != nil { return nil, err } if len(value) != length { return nil, errors.New("protocol: incorrect length for " + name) } return value, nil } func getUint32(msg map[uint32][]byte, tag uint32, name string) (result uint32, err error) { valueBytes, err := getFixedLength(msg, tag, name, 4) if err != nil { return 0, err } return binary.LittleEndian.Uint32(valueBytes), nil } func getUint64(msg map[uint32][]byte, tag uint32, name string) (result uint64, err error) { valueBytes, err := getFixedLength(msg, tag, name, 8) if err != nil { return 0, err } return binary.LittleEndian.Uint64(valueBytes), nil } func getSubmessage(msg map[uint32][]byte, tag uint32, name string) (result map[uint32][]byte, err error) { valueBytes, err := getValue(msg, tag, name) if err != nil { return nil, err } result, err = Decode(valueBytes) if err != nil { return nil, errors.New("protocol: failed to parse " + name + ": " + err.Error()) } return result, nil } // VerifyReply parses the Roughtime reply in replyBytes, authenticates it using // publicKey and verifies that nonce is included in it. It returns the included // timestamp and radius. func VerifyReply(versionPreference []Version, replyBytes, publicKey []byte, nonce []byte) (midp time.Time, radi time.Duration, err error) { advertisedVersions, versionIETF, err := advertisedVersionsFromPreference(versionPreference) if err != nil { return midp, radi, err } nonceSize := nonceSize(versionIETF) unframedReply, _, err := decodeFramed(replyBytes) if err != nil { return midp, radi, err } reply, err := Decode(unframedReply) if err != nil { return midp, radi, errors.New("protocol: failed to parse top-level reply: " + err.Error()) } // Make sure the version selected by the server matches one that we advertised. var responseVer Version if versionIETF { encoded, ok := reply[tagVER] if !ok { return midp, radi, errMissingVersion } if len(encoded) != 4 { return midp, radi, errDecode("malformed version") } responseVer = Version(binary.LittleEndian.Uint32(encoded[:])) } else { responseVer = VersionGoogle } versionOK := false for _, ver := range advertisedVersions { if responseVer == ver { versionOK = true break } } if !versionOK { return midp, radi, errUnsupportedVersion([]Version{responseVer}) } // Make sure the NONC tag is present and indicates the same nonce as sent in // request. if versionIETF { responseNONC, ok := reply[tagNONC] if !ok { return midp, radi, errors.New("protocol: response is missing NONC tag") } if !bytes.Equal(nonce, responseNONC) { return midp, radi, errors.New("protocol: nonce in responce is different from nonce in request") } } cert, err := getSubmessage(reply, tagCERT, "certificate") if err != nil { return midp, radi, err } signatureBytes, err := getFixedLength(cert, tagSIG, "signature", ed25519.SignatureSize) if err != nil { return midp, radi, err } delegationBytes, err := getValue(cert, tagDELE, "delegation") if err != nil { return midp, radi, err } if !ed25519.Verify(publicKey, []byte(certificateContext+string(delegationBytes)), signatureBytes) { return midp, radi, errors.New("protocol: invalid delegation signature") } delegation, err := Decode(delegationBytes) if err != nil { return midp, radi, errors.New("protocol: failed to parse delegation: " + err.Error()) } minTime, err := getUint64(delegation, tagMINT, "minimum time") if err != nil { return midp, radi, err } maxTime, err := getUint64(delegation, tagMAXT, "maximum time") if err != nil { return midp, radi, err } delegatedPublicKey, err := getFixedLength(delegation, tagPUBK, "public key", ed25519.PublicKeySize) if err != nil { return midp, radi, err } responseSigBytes, err := getFixedLength(reply, tagSIG, "signature", ed25519.SignatureSize) if err != nil { return midp, radi, err } signedResponseBytes, ok := reply[tagSREP] if !ok { return midp, radi, errors.New("protocol: response is missing signed portion") } if !ed25519.Verify(delegatedPublicKey, []byte(signedResponseContext+string(signedResponseBytes)), responseSigBytes) { return midp, radi, errors.New("protocol: invalid response signature") } signedResponse, err := Decode(signedResponseBytes) if err != nil { return midp, radi, errors.New("protocol: failed to parse signed response: " + err.Error()) } root, err := getFixedLength(signedResponse, tagROOT, "root", nonceSize) if err != nil { return midp, radi, err } midpoint, err := getUint64(signedResponse, tagMIDP, "midpoint") if err != nil { return midp, radi, err } radius, err := getUint32(signedResponse, tagRADI, "radius") if err != nil { return midp, radi, err } if maxTime < minTime { return midp, radi, errors.New("protocol: invalid delegation range") } if midpoint < minTime || maxTime < midpoint { return midp, radi, errors.New("protocol: timestamp out of range for delegation") } index, err := getUint32(reply, tagINDX, "index") if err != nil { return midp, radi, err } path, err := getValue(reply, tagPATH, "path") if err != nil { return midp, radi, err } if len(path)%nonceSize != 0 { return midp, radi, errors.New("protocol: path is not a multiple of the hash size") } var hash [maxNonceSize]byte hashLeaf(&hash, nonce) for len(path) > 0 { pathElementIsRight := index&1 == 0 if pathElementIsRight { hashNode(&hash, hash[:nonceSize], path[:nonceSize]) } else { hashNode(&hash, path[:nonceSize], hash[:nonceSize]) } index >>= 1 path = path[nonceSize:] } if !bytes.Equal(hash[:nonceSize], root) { return midp, radi, errors.New("protocol: calculated tree root doesn't match signed root") } if versionIETF { midp = time.Unix(int64(midpoint), 0) radi = time.Duration(radius) * time.Second } else { midp = time.UnixMicro(int64(midpoint)) radi = time.Duration(radius) * time.Microsecond } return }
// Copyright 2023 Cloudflare, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package protocol import "fmt" // Version indicates the version of the Roughtime protocol in use. type Version uint32 const ( // VersionGoogle is Google-Roughtime as described here: // https://roughtime.googlesource.com/roughtime/+/HEAD/PROTOCOL.md VersionGoogle Version = 0 // VersionDraft08 is draft-ietf-ntp-roughtime-08. VersionDraft08 Version = 0x80000008 // VersionDraft11 is draft-ietf-ntp-roughtime-11. VersionDraft11 Version = 0x8000000b ) // allVersions is a list of all supported versions in order from newest to oldest. var allVersions = []Version{ VersionDraft11, VersionDraft08, VersionGoogle, } // ietfVersions is a list of all IETF drafts in order from newest to oldest. var ietfVersions = []Version{ VersionDraft11, VersionDraft08, } func (ver Version) isSupported() bool { for i := range ietfVersions { if ver == ietfVersions[i] { return true } } return false } func (ver Version) String() string { switch ver { case VersionGoogle: return "Google-Roughtime" case VersionDraft08: return "draft-ietf-ntp-roughtime-08" case VersionDraft11: return "draft-ietf-ntp-roughtime-11" default: return fmt.Sprintf("%d", uint32(ver)) } } // advertisedVersionsFromPreference derives the list of versions advertised in // its request by a client with the given preferences. // // If len(versionPreference) == 0, then a safe default is used. // // If versionPreference includes Google-Roughtime, then it must be the only // version that is supported. If not, an error is returned. // // This function also returns a boolean indicating whether to use // IETF-Roughtime. func advertisedVersionsFromPreference(versionPreference []Version) ([]Version, bool, error) { if len(versionPreference) == 0 { return []Version{VersionDraft11, VersionDraft08}, true, nil } versionIETF := true for _, vers := range versionPreference { if vers == VersionGoogle { versionIETF = false break } } if !versionIETF && len(versionPreference) != 1 { return nil, false, fmt.Errorf("cannot support %s simultaneously with other versions", VersionGoogle) } return versionPreference, versionIETF, nil } // ResponseVersionFromSupported selects a version to use from the list of // versions supported by the clients. Returns an error if the input slice is // zero-length. func ResponseVersionFromSupported(supportedVersions []Version) (Version, error) { for _, ver := range allVersions { for _, supportedVer := range supportedVersions { if ver == supportedVer { return ver, nil } } } return 0, errUnsupportedVersion(supportedVersions) }