carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package maintenance
2
3import (
4 "crypto/aes"
5 "crypto/cipher"
6 "crypto/ecdh"
7 "crypto/rand"
8 "crypto/sha256"
9 "encoding/base64"
10 "encoding/binary"
11 "encoding/hex"
12 "errors"
13 "io"
14
15 "golang.org/x/crypto/hkdf"
16)
17
18const (
19 EnvelopeVersion uint32 = 1
20 // MaxEnvelopePlaintext bounds the crypto allocation. Request/result parsers
21 // impose their smaller operation-specific limits before execution.
22 MaxEnvelopePlaintext = 1 << 20
23 envelopePrefix = "CME1"
24 ephemeralBytes = 65
25 nonceBytes = 12
26 tagBytes = 16
27 envelopeInfo = "carlos-maintenance-v1"
28)
29
30type Direction string
31
32const (
33 DirectionInput Direction = "input"
34 DirectionResult Direction = "result"
35 DirectionEnvironment Direction = "environment"
36)
37
38var ErrEnvelope = errors.New("maintenance: invalid private envelope")
39
40// Binding authenticates identifiers already validated by the request handler.
41// It does not establish ownership or authorize an operation.
42type Binding struct {
43 Version uint32
44 Direction Direction
45 Account string
46 App string
47 Host string
48 RequestID string
49 AttemptID string
50 Recipient string
51}
52
53// RecipientFingerprint names the P-256 key that can open the envelope.
54func RecipientFingerprint(pub *ecdh.PublicKey) (string, error) {
55 if pub == nil || pub.Curve() != ecdh.P256() {
56 return "", ErrEnvelope
57 }
58 sum := sha256.Sum256(pub.Bytes())
59 return hex.EncodeToString(sum[:]), nil
60}
61
62func appendField(dst []byte, field []byte) []byte {
63 dst = binary.BigEndian.AppendUint32(dst, uint32(len(field)))
64 return append(dst, field...)
65}
66func bindingAAD(b Binding, pub *ecdh.PublicKey) ([]byte, error) {
67 fingerprint, err := RecipientFingerprint(pub)
68 if err != nil || b.Version != EnvelopeVersion || b.Recipient != fingerprint {
69 return nil, ErrEnvelope
70 }
71 if b.Direction != DirectionInput && b.Direction != DirectionResult && b.Direction != DirectionEnvironment {
72 return nil, ErrEnvelope
73 }
74 fields := []struct {
75 s string
76 max int
77 }{{b.Account, 64}, {b.App, 64}, {b.Host, 253}, {b.RequestID, 64}, {b.AttemptID, 64}}
78 for _, f := range fields {
79 if len(f.s) == 0 || len(f.s) > f.max {
80 return nil, ErrEnvelope
81 }
82 for i := range len(f.s) {
83 if f.s[i] < 0x21 || f.s[i] > 0x7e {
84 return nil, ErrEnvelope
85 }
86 }
87 }
88 aad := appendField(nil, []byte(envelopePrefix))
89 aad = appendField(aad, binary.BigEndian.AppendUint32(nil, b.Version))
90 aad = appendField(aad, []byte(b.Direction))
91 for _, f := range fields {
92 aad = appendField(aad, []byte(f.s))
93 }
94 return appendField(aad, []byte(b.Recipient)), nil
95}
96func envelopeGCM(shared []byte) (cipher.AEAD, error) {
97 key := make([]byte, 32)
98 if _, err := io.ReadFull(hkdf.New(sha256.New, shared, nil, []byte(envelopeInfo)), key); err != nil {
99 return nil, ErrEnvelope
100 }
101 block, err := aes.NewCipher(key)
102 clear(key)
103 if err != nil {
104 return nil, ErrEnvelope
105 }
106 gcm, err := cipher.NewGCM(block)
107 if err != nil {
108 return nil, ErrEnvelope
109 }
110 return gcm, nil
111}
112
113// Seal encrypts private input, output or an environment snapshot for one
114// recipient and one immutable operation binding.
115func Seal(pub *ecdh.PublicKey, b Binding, plaintext []byte) (string, error) {
116 if len(plaintext) > MaxEnvelopePlaintext {
117 return "", ErrEnvelope
118 }
119 if _, err := bindingAAD(b, pub); err != nil {
120 return "", ErrEnvelope
121 }
122 eph, err := ecdh.P256().GenerateKey(rand.Reader)
123 if err != nil {
124 return "", ErrEnvelope
125 }
126 nonce := make([]byte, nonceBytes)
127 if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
128 return "", ErrEnvelope
129 }
130 return sealWith(eph, nonce, pub, b, plaintext)
131}
132
133// sealWith permits deterministic vectors without replacing production entropy.
134func sealWith(eph *ecdh.PrivateKey, nonce []byte, pub *ecdh.PublicKey, b Binding, plaintext []byte) (string, error) {
135 if eph == nil || eph.Curve() != ecdh.P256() || len(nonce) != nonceBytes || len(plaintext) > MaxEnvelopePlaintext {
136 return "", ErrEnvelope
137 }
138 aad, err := bindingAAD(b, pub)
139 if err != nil {
140 return "", ErrEnvelope
141 }
142 shared, err := eph.ECDH(pub)
143 if err != nil {
144 return "", ErrEnvelope
145 }
146 defer clear(shared)
147 gcm, err := envelopeGCM(shared)
148 if err != nil {
149 return "", ErrEnvelope
150 }
151 raw := make([]byte, 0, len(envelopePrefix)+ephemeralBytes+nonceBytes+len(plaintext)+tagBytes)
152 raw = append(raw, []byte(envelopePrefix)...)
153 raw = append(raw, eph.PublicKey().Bytes()...)
154 raw = append(raw, nonce...)
155 raw = gcm.Seal(raw, nonce, plaintext, aad)
156 return base64.StdEncoding.EncodeToString(raw), nil
157}
158
159// Open refuses changed bindings and noncanonical or oversized envelopes before
160// returning plaintext. No error includes caller input or decrypted material.
161func Open(priv *ecdh.PrivateKey, b Binding, envelope string) ([]byte, error) {
162 const overhead = len(envelopePrefix) + ephemeralBytes + nonceBytes + tagBytes
163 if priv == nil || priv.Curve() != ecdh.P256() || len(envelope) > base64.StdEncoding.EncodedLen(overhead+MaxEnvelopePlaintext) {
164 return nil, ErrEnvelope
165 }
166 aad, err := bindingAAD(b, priv.PublicKey())
167 if err != nil {
168 return nil, ErrEnvelope
169 }
170 raw, err := base64.StdEncoding.Strict().DecodeString(envelope)
171 if err != nil || len(raw) < overhead || len(raw) > overhead+MaxEnvelopePlaintext || base64.StdEncoding.EncodeToString(raw) != envelope {
172 return nil, ErrEnvelope
173 }
174 if string(raw[:len(envelopePrefix)]) != envelopePrefix {
175 return nil, ErrEnvelope
176 }
177 pointEnd := len(envelopePrefix) + ephemeralBytes
178 eph, err := ecdh.P256().NewPublicKey(raw[len(envelopePrefix):pointEnd])
179 if err != nil {
180 return nil, ErrEnvelope
181 }
182 shared, err := priv.ECDH(eph)
183 if err != nil {
184 return nil, ErrEnvelope
185 }
186 defer clear(shared)
187 gcm, err := envelopeGCM(shared)
188 if err != nil {
189 return nil, ErrEnvelope
190 }
191 plaintext, err := gcm.Open(nil, raw[pointEnd:pointEnd+nonceBytes], raw[pointEnd+nonceBytes:], aad)
192 if err != nil {
193 return nil, ErrEnvelope
194 }
195 return plaintext, nil
196}
197