-
Notifications
You must be signed in to change notification settings - Fork 3
/
check.go
194 lines (160 loc) · 5.38 KB
/
check.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package cosesign1
import (
"crypto/x509"
"fmt"
didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver"
"github.com/sirupsen/logrus"
"github.com/veraison/go-cose"
)
// helper functions to check that elements in the protected header are
// of the expected types.
// issuer and feed MUST be strings or not present
func getStringValue(m cose.ProtectedHeader, k any) string {
val, ok := m[k]
if !ok {
return ""
}
str, ok := val.(string)
if !ok {
return ""
}
return str
}
// See https://datatracker.ietf.org/doc/draft-ietf-cose-x509/09/ x5chain section,
// The "chain" can be an array of arrays of bytes or just a single array of bytes
// in the single cert case. Each of these two functions handles of of these cases
// a DER chain is an array of arrays of bytes
func isDERChain(val interface{}) bool {
valArray, ok := val.([]interface{})
if !ok {
return false
}
for _, element := range valArray {
_, ok = element.([]byte)
if !ok {
return false
}
}
return len(valArray) > 0
}
// a DER is an array of bytes
func isDEROnly(val interface{}) bool {
_, ok := val.([]byte)
return ok
}
type UnpackedCoseSign1 struct {
Issuer string
Feed string
ContentType string
Pubkey string
Pubcert string
ChainPem string
Payload []byte
CertChain []*x509.Certificate
}
// This function is rather unpleasant in that it both decodes the COSE Sign1 document and its various
// crypto parts AND checks that those parts are sound in this context. Higher layers may yet refuse the
// payload for reasons beyond the scope of the checking of the document itself.
// While this function could be decomposed into "unpack" and "verify" there would need to be extra state,
// such as the cert pools, stored in some state object. Then the sensible pattern would be to have
// accessors and member functions such as "verity()". However that was done there could exist state objects
// for badly formed COSE Sign1 documents and that would complicate the jobs of callers.
//
// raw: an array of bytes comprising the COSE Sign1 document.
func UnpackAndValidateCOSE1CertChain(raw []byte) (*UnpackedCoseSign1, error) {
var msg cose.Sign1Message
err := msg.UnmarshalCBOR(raw)
if err != nil {
return nil, err
}
protected := msg.Headers.Protected
val, ok := protected[cose.HeaderLabelAlgorithm]
if !ok {
return nil, fmt.Errorf("algorithm missing")
}
algo, ok := val.(cose.Algorithm)
if !ok {
return nil, fmt.Errorf("algorithm wrong type")
}
logrus.Debugf("COSE Sign1 unpack: algorithm %d", algo)
// The spec says this is ordered - leaf, intermediates, root. X5Bag is unordered and would need sorting
chainDER, ok := protected[cose.HeaderLabelX5Chain]
if !ok {
return nil, fmt.Errorf("x5Chain missing")
}
var chainIA []interface{}
// The HeaderLabelX5Chain entry in the cose header may be a blob (single cert) or an array of blobs (a chain) see https://datatracker.ietf.org/doc/draft-ietf-cose-x509/08/
if isDERChain(chainDER) {
chainIA = chainDER.([]interface{})
} else if isDEROnly(chainDER) {
chainIA = append(chainIA, chainDER)
} else {
return nil, fmt.Errorf("x5Chain wrong type")
}
var chain []*x509.Certificate
for index, element := range chainIA {
cert, err := x509.ParseCertificate(element.([]byte))
if err == nil {
chain = append(chain, cert)
} else {
logrus.Debugf("Parse certificate failed on %d: %s", index, err.Error())
return nil, err
}
}
// A reasonable chain will have a handful of certs, typically 3 or 4,
// so limit to an arbitary 100 which would be quite unreasonable
chainLen := len(chain)
if chainLen > 100 || chainLen < 1 {
return nil, fmt.Errorf("unreasonable number of certs (%d) in COSE_Sign1 document", chainLen)
}
var leafCert = chain[0]
var leafCertBase64 = x509ToBase64(leafCert)
var leafPubKey = leafCert.PublicKey
var leafPubKeyBase64 = keyToBase64(leafPubKey)
var chainPEM string
for i, c := range chain {
if i > 0 {
chainPEM += "\n"
}
chainPEM += convertx509ToPEM(c)
}
logrus.Debugln("Certificate chain:")
logrus.Debugln(chainPEM)
// First check that the chain is itself good.
// Note that a single cert would fail here as it
// falls back to the system root certs which will
// never have issued the leaf cert.
if len(chain) > 1 {
// Any valid cert chain is good for us as we will be matching
// part of the chain with a customer provided cert fingerprint.
_, err = didx509resolver.VerifyCertificateChain(chain, chain[len(chain)-1:], false)
if err != nil {
return nil, fmt.Errorf("certificate chain verification failed - %w", err)
}
}
// Next check that the signature over the document was made with the private key matching the
// public key we extracted from the leaf cert.
verifier, err := cose.NewVerifier(algo, leafPubKey)
if err != nil {
logrus.Debugf("cose.NewVerifier failed (algo %d): %s", algo, err.Error())
return nil, err
}
err = msg.Verify(nil, verifier)
if err != nil {
logrus.Debugf("msg.Verify failed: algo = %d err = %s", algo, err.Error())
return nil, err
}
issuer := getStringValue(protected, "iss")
feed := getStringValue(protected, "feed")
contenttype := getStringValue(protected, cose.HeaderLabelContentType)
return &UnpackedCoseSign1{
Pubcert: leafCertBase64,
Feed: feed,
Issuer: issuer,
Pubkey: leafPubKeyBase64,
ChainPem: chainPEM,
ContentType: contenttype,
Payload: msg.Payload,
CertChain: chain,
}, nil
}