Verify CSR key fingerprint with attestation certificate key

This commit makes sure that the attestation certificate key matches the
key used on the CSR on an ACME device attestation flow.
This commit is contained in:
Mariano Cano 2023-02-09 16:48:43 -08:00
parent ec3be2359a
commit 6ba20209c2
No known key found for this signature in database
9 changed files with 792 additions and 450 deletions

View file

@ -16,6 +16,7 @@ type Authorization struct {
Challenges []*Challenge `json:"challenges"` Challenges []*Challenge `json:"challenges"`
Wildcard bool `json:"wildcard"` Wildcard bool `json:"wildcard"`
ExpiresAt time.Time `json:"expires"` ExpiresAt time.Time `json:"expires"`
Fingerprint string `json:"fingerprint,omitempty"`
Error *Error `json:"error,omitempty"` Error *Error `json:"error,omitempty"`
} }

View file

@ -27,6 +27,7 @@ import (
"github.com/fxamacker/cbor/v2" "github.com/fxamacker/cbor/v2"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/pemutil" "go.step.sm/crypto/pemutil"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
@ -347,6 +348,13 @@ type attestationObject struct {
// TODO(bweeks): move attestation verification to a shared package. // TODO(bweeks): move attestation verification to a shared package.
func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error { func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error {
// Load authorization to store the key fingerprint.
az, err := db.GetAuthorization(ctx, ch.AuthorizationID)
if err != nil {
return WrapErrorISE(err, "error loading authorization")
}
// Parse payload.
var p payloadType var p payloadType
if err := json.Unmarshal(payload, &p); err != nil { if err := json.Unmarshal(payload, &p); err != nil {
return WrapErrorISE(err, "error unmarshalling JSON") return WrapErrorISE(err, "error unmarshalling JSON")
@ -385,7 +393,6 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
} }
return WrapErrorISE(err, "error validating attestation") return WrapErrorISE(err, "error validating attestation")
} }
// Validate nonce with SHA-256 of the token. // Validate nonce with SHA-256 of the token.
if len(data.Nonce) != 0 { if len(data.Nonce) != 0 {
sum := sha256.Sum256([]byte(ch.Token)) sum := sha256.Sum256([]byte(ch.Token))
@ -401,6 +408,9 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
if data.UDID != ch.Value && data.SerialNumber != ch.Value { if data.UDID != ch.Value && data.SerialNumber != ch.Value {
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match")) return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match"))
} }
// Update attestation key fingerprint to compare against the CSR
az.Fingerprint = data.Fingerprint
case "step": case "step":
data, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att) data, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att)
if err != nil { if err != nil {
@ -426,6 +436,9 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
) )
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem)) return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem))
} }
// Update attestation key fingerprint to compare against the CSR
az.Fingerprint = data.Fingerprint
default: default:
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "unexpected attestation object format")) return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "unexpected attestation object format"))
} }
@ -435,6 +448,15 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
ch.Error = nil ch.Error = nil
ch.ValidatedAt = clock.Now().Format(time.RFC3339) ch.ValidatedAt = clock.Now().Format(time.RFC3339)
// Store the fingerprint in the authorization.
//
// TODO: add method to update authorization and challenge atomically.
if az.Fingerprint != "" {
if err := db.UpdateAuthorization(ctx, az); err != nil {
return WrapErrorISE(err, "error updating authorization")
}
}
if err := db.UpdateChallenge(ctx, ch); err != nil { if err := db.UpdateChallenge(ctx, ch); err != nil {
return WrapErrorISE(err, "error updating challenge") return WrapErrorISE(err, "error updating challenge")
} }
@ -471,6 +493,7 @@ type appleAttestationData struct {
UDID string UDID string
SEPVersion string SEPVersion string
Certificate *x509.Certificate Certificate *x509.Certificate
Fingerprint string
} }
func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *attestationObject) (*appleAttestationData, error) { func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *attestationObject) (*appleAttestationData, error) {
@ -527,6 +550,9 @@ func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challen
data := &appleAttestationData{ data := &appleAttestationData{
Certificate: leaf, Certificate: leaf,
} }
if data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil {
return nil, WrapErrorISE(err, "error calculating key fingerprint")
}
for _, ext := range leaf.Extensions { for _, ext := range leaf.Extensions {
switch { switch {
case ext.Id.Equal(oidAppleSerialNumber): case ext.Id.Equal(oidAppleSerialNumber):
@ -572,6 +598,7 @@ var oidYubicoSerialNumber = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 41482, 3, 7}
type stepAttestationData struct { type stepAttestationData struct {
Certificate *x509.Certificate Certificate *x509.Certificate
SerialNumber string SerialNumber string
Fingerprint string
} }
func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*stepAttestationData, error) { func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*stepAttestationData, error) {
@ -667,6 +694,9 @@ func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challeng
data := &stepAttestationData{ data := &stepAttestationData{
Certificate: leaf, Certificate: leaf,
} }
if data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil {
return nil, WrapErrorISE(err, "error calculating key fingerprint")
}
for _, ext := range leaf.Extensions { for _, ext := range leaf.Extensions {
if !ext.Id.Equal(oidYubicoSerialNumber) { if !ext.Id.Equal(oidYubicoSerialNumber) {
continue continue

View file

@ -54,6 +54,13 @@ func (m *mockClient) TLSDial(network, addr string, tlsConfig *tls.Config) (*tls.
return m.tlsDial(network, addr, tlsConfig) return m.tlsDial(network, addr, tlsConfig)
} }
func fatalError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
func mustNonAttestationProvisioner(t *testing.T) Provisioner { func mustNonAttestationProvisioner(t *testing.T) Provisioner {
t.Helper() t.Helper()
@ -88,6 +95,108 @@ func mustAttestationProvisioner(t *testing.T, roots []byte) Provisioner {
return prov return prov
} }
func mustAccountAndKeyAuthorization(t *testing.T, token string) (*jose.JSONWebKey, string) {
t.Helper()
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
fatalError(t, err)
keyAuth, err := KeyAuthorization(token, jwk)
fatalError(t, err)
return jwk, keyAuth
}
func mustAttestApple(t *testing.T, nonce string) ([]byte, *x509.Certificate, *x509.Certificate) {
t.Helper()
ca, err := minica.New()
fatalError(t, err)
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
fatalError(t, err)
nonceSum := sha256.Sum256([]byte(nonce))
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidAppleSerialNumber, Value: []byte("serial-number")},
{Id: oidAppleUniqueDeviceIdentifier, Value: []byte("udid")},
{Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte("16.0")},
{Id: oidAppleNonce, Value: nonceSum[:]},
},
})
fatalError(t, err)
attObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
},
})
fatalError(t, err)
payload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
})
fatalError(t, err)
return payload, leaf, ca.Root
}
func mustAttestYubikey(t *testing.T, nonce, keyAuthorization string, serial int) ([]byte, *x509.Certificate, *x509.Certificate) {
ca, err := minica.New()
fatalError(t, err)
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
fatalError(t, err)
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
fatalError(t, err)
cborSig, err := cbor.Marshal(sig)
fatalError(t, err)
serialNumber, err := asn1.Marshal(serial)
fatalError(t, err)
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidYubicoSerialNumber, Value: serialNumber},
},
})
fatalError(t, err)
attObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
})
fatalError(t, err)
payload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
})
fatalError(t, err)
return payload, leaf, ca.Root
}
func Test_storeError(t *testing.T) { func Test_storeError(t *testing.T) {
type test struct { type test struct {
ch *Challenge ch *Challenge
@ -661,13 +770,6 @@ func TestChallenge_Validate(t *testing.T) {
} }
}, },
"fail/device-attest-01": func(t *testing.T) test { "fail/device-attest-01": func(t *testing.T) test {
ch := &Challenge{
ID: "chID",
Token: "token",
Type: "device-attest-01",
Status: StatusPending,
Value: "12345678",
}
payload, err := json.Marshal(struct { payload, err := json.Marshal(struct {
Error string `json:"error"` Error string `json:"error"`
}{ }{
@ -675,9 +777,20 @@ func TestChallenge_Validate(t *testing.T) {
}) })
assert.NoError(t, err) assert.NoError(t, err)
return test{ return test{
ch: ch, ch: &Challenge{
ID: "chID",
AuthorizationID: "azID",
Token: "token",
Type: "device-attest-01",
Status: StatusPending,
Value: "12345678",
},
payload: payload, payload: payload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
@ -699,76 +812,39 @@ func TestChallenge_Validate(t *testing.T) {
} }
}, },
"ok/device-attest-01": func(t *testing.T) test { "ok/device-attest-01": func(t *testing.T) test {
ctx := context.Background() jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
ca, err := minica.New() payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 1234)
assert.NoError(t, err)
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
ctx = NewProvisionerContext(ctx, mustAttestationProvisioner(t, caRoot))
makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidYubicoSerialNumber, Value: serialNumber},
},
})
if err != nil {
t.Fatal(err)
}
return leaf
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
assert.NoError(t, err) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
serialNumber, err := asn1.Marshal(1234)
assert.NoError(t, err)
leaf := makeLeaf(signer, serialNumber)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) return test{
assert.NoError(t, err) ch: &Challenge{
token := "token"
keyAuth, err := KeyAuthorization(token, jwk)
assert.NoError(t, err)
keyAuthSum := sha256.Sum256([]byte(keyAuth))
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
assert.NoError(t, err)
cborSig, err := cbor.Marshal(sig)
assert.NoError(t, err)
ch := &Challenge{
ID: "chID", ID: "chID",
Token: token, AuthorizationID: "azID",
Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
Value: "1234", Value: "1234",
}
attObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
}, },
})
assert.NoError(t, err)
payload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
})
assert.NoError(t, err)
return test{
ch: ch,
payload: payload, payload: payload,
ctx: ctx, ctx: ctx,
jwk: jwk, jwk: jwk,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
assert.NoError(t, err)
assert.Equal(t, "azID", az.ID)
assert.Equal(t, fingerprint, az.Fingerprint)
return nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, token, updch.Token) assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status) assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type) assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "1234", updch.Value) assert.Equal(t, "1234", updch.Value)
@ -2745,6 +2821,10 @@ func Test_doAppleAttestationFormat(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
fingerprint, err := keyutil.Fingerprint(signer.Public())
if err != nil {
t.Fatal(err)
}
type args struct { type args struct {
ctx context.Context ctx context.Context
@ -2769,6 +2849,7 @@ func Test_doAppleAttestationFormat(t *testing.T) {
UDID: "udid", UDID: "udid",
SEPVersion: "16.0", SEPVersion: "16.0",
Certificate: leaf, Certificate: leaf,
Fingerprint: fingerprint,
}, false}, }, false},
{"fail apple issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{}, &attestationObject{ {"fail apple issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{}, &attestationObject{
Format: "apple", Format: "apple",
@ -2871,6 +2952,10 @@ func Test_doStepAttestationFormat(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
leaf := makeLeaf(signer, serialNumber) leaf := makeLeaf(signer, serialNumber)
fingerprint, err := keyutil.Fingerprint(signer.Public())
if err != nil {
t.Fatal(err)
}
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
if err != nil { if err != nil {
@ -2926,6 +3011,7 @@ func Test_doStepAttestationFormat(t *testing.T) {
}}, &stepAttestationData{ }}, &stepAttestationData{
SerialNumber: "1234", SerialNumber: "1234",
Certificate: leaf, Certificate: leaf,
Fingerprint: fingerprint,
}, false}, }, false},
{"fail yubico issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{ {"fail yubico issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{
Format: "step", Format: "step",
@ -3196,16 +3282,44 @@ func Test_deviceAttest01Validate(t *testing.T) {
wantErr *Error wantErr *Error
} }
tests := map[string]func(t *testing.T) test{ tests := map[string]func(t *testing.T) test{
"fail/getAuthorization": func(t *testing.T) test {
return test{
args: args{
ch: &Challenge{
ID: "chID",
AuthorizationID: "azID",
Token: "token",
Type: "device-attest-01",
Status: StatusPending,
Value: "12345678",
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return nil, errors.New("not found")
},
},
payload: []byte(invalidPayload),
},
wantErr: NewErrorISE("error loading authorization: not found"),
}
},
"fail/json.Unmarshal": func(t *testing.T) test { "fail/json.Unmarshal": func(t *testing.T) test {
return test{ return test{
args: args{ args: args{
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
Value: "12345678", Value: "12345678",
}, },
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
},
payload: []byte(invalidPayload), payload: []byte(invalidPayload),
}, },
wantErr: NewErrorISE("error unmarshalling JSON: invalid character '!' looking for beginning of value"), wantErr: NewErrorISE("error unmarshalling JSON: invalid character '!' looking for beginning of value"),
@ -3217,6 +3331,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
args: args{ args: args{
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
@ -3224,6 +3339,10 @@ func Test_deviceAttest01Validate(t *testing.T) {
}, },
payload: errorPayload, payload: errorPayload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
@ -3251,6 +3370,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
args: args{ args: args{
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
@ -3258,6 +3378,10 @@ func Test_deviceAttest01Validate(t *testing.T) {
}, },
payload: errorPayload, payload: errorPayload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
@ -3285,11 +3409,18 @@ func Test_deviceAttest01Validate(t *testing.T) {
args: args{ args: args{
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
Value: "12345678", Value: "12345678",
}, },
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
},
payload: errorBase64Payload, payload: errorBase64Payload,
}, },
wantErr: NewErrorISE("error base64 decoding attObj: illegal base64 data at input byte 0"), wantErr: NewErrorISE("error base64 decoding attObj: illegal base64 data at input byte 0"),
@ -3300,71 +3431,35 @@ func Test_deviceAttest01Validate(t *testing.T) {
args: args{ args: args{
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
Value: "12345678", Value: "12345678",
}, },
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
},
payload: errorCBORPayload, payload: errorCBORPayload,
}, },
wantErr: NewErrorISE("error unmarshalling CBOR: cbor: cannot unmarshal positive integer into Go value of type acme.attestationObject"), wantErr: NewErrorISE("error unmarshalling CBOR: cbor: cannot unmarshal positive integer into Go value of type acme.attestationObject"),
} }
}, },
"ok/prov.IsAttestationFormatEnabled": func(t *testing.T) test { "ok/prov.IsAttestationFormatEnabled": func(t *testing.T) test {
ca, err := minica.New() jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
require.NoError(t, err) payload, _, _ := mustAttestYubikey(t, "nonce", keyAuth, 12345678)
makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidYubicoSerialNumber, Value: serialNumber},
},
})
if err != nil {
t.Fatal(err)
}
return leaf
}
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
serialNumber, err := asn1.Marshal(1234)
require.NoError(t, err)
leaf := makeLeaf(signer, serialNumber)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
require.NoError(t, err)
token := "token"
keyAuth, err := KeyAuthorization(token, jwk)
require.NoError(t, err)
keyAuthSum := sha256.Sum256([]byte(keyAuth))
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
require.NoError(t, err)
cborSig, err := cbor.Marshal(sig)
require.NoError(t, err)
ctx := NewProvisionerContext(context.Background(), mustNonAttestationProvisioner(t)) ctx := NewProvisionerContext(context.Background(), mustNonAttestationProvisioner(t))
attObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
})
require.NoError(t, err)
payload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
})
require.NoError(t, err)
return test{ return test{
args: args{ args: args{
ctx: ctx, ctx: ctx,
jwk: jwk,
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
@ -3372,6 +3467,10 @@ func Test_deviceAttest01Validate(t *testing.T) {
}, },
payload: payload, payload: payload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
@ -3415,6 +3514,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
ctx: ctx, ctx: ctx,
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
@ -3422,6 +3522,10 @@ func Test_deviceAttest01Validate(t *testing.T) {
}, },
payload: payload, payload: payload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
@ -3445,57 +3549,36 @@ func Test_deviceAttest01Validate(t *testing.T) {
} }
}, },
"ok/doAppleAttestationFormat-non-matching-nonce": func(t *testing.T) test { "ok/doAppleAttestationFormat-non-matching-nonce": func(t *testing.T) test {
ca, err := minica.New() jwk, _ := mustAccountAndKeyAuthorization(t, "token")
require.NoError(t, err) payload, _, root := mustAttestApple(t, "bad-nonce")
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err) caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidAppleSerialNumber, Value: []byte("serial-number")},
{Id: oidAppleUniqueDeviceIdentifier, Value: []byte("udid")},
{Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte("16.0")},
{Id: oidAppleNonce, Value: []byte("nonce")},
},
})
require.NoError(t, err)
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
attObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
},
})
require.NoError(t, err)
payload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
})
require.NoError(t, err)
return test{ return test{
args: args{ args: args{
ctx: ctx, ctx: ctx,
jwk: jwk,
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
Value: "12345678", Value: "serial-number",
}, },
payload: payload, payload: payload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status) assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type) assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "12345678", updch.Value) assert.Equal(t, "serial-number", updch.Value)
err := NewError(ErrorBadAttestationStatementType, "challenge token does not match") err := NewError(ErrorBadAttestationStatementType, "challenge token does not match")
@ -3513,45 +3596,18 @@ func Test_deviceAttest01Validate(t *testing.T) {
} }
}, },
"ok/doAppleAttestationFormat-non-matching-challenge-value": func(t *testing.T) test { "ok/doAppleAttestationFormat-non-matching-challenge-value": func(t *testing.T) test {
ca, err := minica.New() jwk, _ := mustAccountAndKeyAuthorization(t, "token")
require.NoError(t, err) payload, _, root := mustAttestApple(t, "nonce")
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err) caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
nonce := sha256.Sum256([]byte("nonce"))
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidAppleSerialNumber, Value: []byte("serial-number")},
{Id: oidAppleUniqueDeviceIdentifier, Value: []byte("udid")},
{Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte("16.0")},
{Id: oidAppleNonce, Value: nonce[:]},
},
})
require.NoError(t, err)
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
attObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "apple",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
},
})
require.NoError(t, err)
payload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
})
require.NoError(t, err)
return test{ return test{
args: args{ args: args{
ctx: ctx, ctx: ctx,
jwk: jwk,
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "nonce", Token: "nonce",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
@ -3559,6 +3615,10 @@ func Test_deviceAttest01Validate(t *testing.T) {
}, },
payload: payload, payload: payload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "nonce", updch.Token) assert.Equal(t, "nonce", updch.Token)
@ -3620,6 +3680,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
ctx: ctx, ctx: ctx,
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
@ -3627,6 +3688,10 @@ func Test_deviceAttest01Validate(t *testing.T) {
}, },
payload: payload, payload: payload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
@ -3650,62 +3715,19 @@ func Test_deviceAttest01Validate(t *testing.T) {
} }
}, },
"ok/doStepAttestationFormat-non-matching-identifier": func(t *testing.T) test { "ok/doStepAttestationFormat-non-matching-identifier": func(t *testing.T) test {
ca, err := minica.New() jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
require.NoError(t, err) payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 87654321)
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
require.NoError(t, err)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
require.NoError(t, err)
token := "token"
keyAuth, err := KeyAuthorization(token, jwk)
require.NoError(t, err)
keyAuthSum := sha256.Sum256([]byte(keyAuth))
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
require.NoError(t, err)
cborSig, err := cbor.Marshal(sig)
require.NoError(t, err)
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidYubicoSerialNumber, Value: serialNumber},
},
})
if err != nil {
t.Fatal(err)
}
return leaf
}
require.NoError(t, err)
serialNumber, err := asn1.Marshal(87654321)
require.NoError(t, err)
leaf := makeLeaf(signer, serialNumber)
attObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
})
require.NoError(t, err)
payload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
})
require.NoError(t, err)
return test{ return test{
args: args{ args: args{
ctx: ctx, ctx: ctx,
jwk: jwk,
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
@ -3713,6 +3735,17 @@ func Test_deviceAttest01Validate(t *testing.T) {
}, },
payload: payload, payload: payload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
assert.NoError(t, err)
assert.Equal(t, "azID", az.ID)
assert.Equal(t, fingerprint, az.Fingerprint)
return nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
@ -3736,7 +3769,6 @@ func Test_deviceAttest01Validate(t *testing.T) {
return nil return nil
}, },
}, },
jwk: jwk,
}, },
wantErr: nil, wantErr: nil,
} }
@ -3797,6 +3829,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
ctx: ctx, ctx: ctx,
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
@ -3804,6 +3837,10 @@ func Test_deviceAttest01Validate(t *testing.T) {
}, },
payload: payload, payload: payload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
@ -3827,63 +3864,20 @@ func Test_deviceAttest01Validate(t *testing.T) {
wantErr: nil, wantErr: nil,
} }
}, },
"fail/db.UpdateChallenge": func(t *testing.T) test { "fail/db.UpdateAuthorization": func(t *testing.T) test {
ca, err := minica.New() jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
require.NoError(t, err) payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 12345678)
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
require.NoError(t, err)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
require.NoError(t, err)
token := "token"
keyAuth, err := KeyAuthorization(token, jwk)
require.NoError(t, err)
keyAuthSum := sha256.Sum256([]byte(keyAuth))
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
require.NoError(t, err)
cborSig, err := cbor.Marshal(sig)
require.NoError(t, err)
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidYubicoSerialNumber, Value: serialNumber},
},
})
if err != nil {
t.Fatal(err)
}
return leaf
}
require.NoError(t, err)
serialNumber, err := asn1.Marshal(12345678)
require.NoError(t, err)
leaf := makeLeaf(signer, serialNumber)
attObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
})
require.NoError(t, err)
payload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
})
require.NoError(t, err)
return test{ return test{
args: args{ args: args{
ctx: ctx, ctx: ctx,
jwk: jwk,
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
@ -3891,6 +3885,54 @@ func Test_deviceAttest01Validate(t *testing.T) {
}, },
payload: payload, payload: payload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
assert.NoError(t, err)
assert.Equal(t, "azID", az.ID)
assert.Equal(t, fingerprint, az.Fingerprint)
return errors.New("force")
},
},
},
wantErr: NewError(ErrorServerInternalType, "error updating authorization: force"),
}
},
"fail/db.UpdateChallenge": func(t *testing.T) test {
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 12345678)
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
return test{
args: args{
ctx: ctx,
jwk: jwk,
ch: &Challenge{
ID: "chID",
AuthorizationID: "azID",
Token: "token",
Type: "device-attest-01",
Status: StatusPending,
Value: "12345678",
},
payload: payload,
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
assert.NoError(t, err)
assert.Equal(t, "azID", az.ID)
assert.Equal(t, fingerprint, az.Fingerprint)
return nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
@ -3901,68 +3943,24 @@ func Test_deviceAttest01Validate(t *testing.T) {
return errors.New("force") return errors.New("force")
}, },
}, },
jwk: jwk,
}, },
wantErr: NewError(ErrorServerInternalType, "error updating challenge: force"), wantErr: NewError(ErrorServerInternalType, "error updating challenge: force"),
} }
}, },
"ok": func(t *testing.T) test { "ok": func(t *testing.T) test {
ca, err := minica.New() jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
require.NoError(t, err) payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 12345678)
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
require.NoError(t, err)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
require.NoError(t, err)
token := "token"
keyAuth, err := KeyAuthorization(token, jwk)
require.NoError(t, err)
keyAuthSum := sha256.Sum256([]byte(keyAuth))
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
require.NoError(t, err)
cborSig, err := cbor.Marshal(sig)
require.NoError(t, err)
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
leaf, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "attestation cert"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{Id: oidYubicoSerialNumber, Value: serialNumber},
},
})
if err != nil {
t.Fatal(err)
}
return leaf
}
require.NoError(t, err)
serialNumber, err := asn1.Marshal(12345678)
require.NoError(t, err)
leaf := makeLeaf(signer, serialNumber)
attObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "step",
AttStatement: map[string]interface{}{
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
"alg": -7,
"sig": cborSig,
},
})
require.NoError(t, err)
payload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
})
require.NoError(t, err)
return test{ return test{
args: args{ args: args{
ctx: ctx, ctx: ctx,
jwk: jwk,
ch: &Challenge{ ch: &Challenge{
ID: "chID", ID: "chID",
AuthorizationID: "azID",
Token: "token", Token: "token",
Type: "device-attest-01", Type: "device-attest-01",
Status: StatusPending, Status: StatusPending,
@ -3970,6 +3968,17 @@ func Test_deviceAttest01Validate(t *testing.T) {
}, },
payload: payload, payload: payload,
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
assert.NoError(t, err)
assert.Equal(t, "azID", az.ID)
assert.Equal(t, fingerprint, az.Fingerprint)
return nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID) assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token) assert.Equal(t, "token", updch.Token)
@ -3980,7 +3989,6 @@ func Test_deviceAttest01Validate(t *testing.T) {
return nil return nil
}, },
}, },
jwk: jwk,
}, },
wantErr: nil, wantErr: nil,
} }

View file

@ -17,6 +17,7 @@ type dbAuthz struct {
Identifier acme.Identifier `json:"identifier"` Identifier acme.Identifier `json:"identifier"`
Status acme.Status `json:"status"` Status acme.Status `json:"status"`
Token string `json:"token"` Token string `json:"token"`
Fingerprint string `json:"fingerprint,omitempty"`
ChallengeIDs []string `json:"challengeIDs"` ChallengeIDs []string `json:"challengeIDs"`
Wildcard bool `json:"wildcard"` Wildcard bool `json:"wildcard"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
@ -69,6 +70,7 @@ func (db *DB) GetAuthorization(ctx context.Context, id string) (*acme.Authorizat
Wildcard: dbaz.Wildcard, Wildcard: dbaz.Wildcard,
ExpiresAt: dbaz.ExpiresAt, ExpiresAt: dbaz.ExpiresAt,
Token: dbaz.Token, Token: dbaz.Token,
Fingerprint: dbaz.Fingerprint,
Error: dbaz.Error, Error: dbaz.Error,
}, nil }, nil
} }
@ -97,6 +99,7 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) e
Identifier: az.Identifier, Identifier: az.Identifier,
ChallengeIDs: chIDs, ChallengeIDs: chIDs,
Token: az.Token, Token: az.Token,
Fingerprint: az.Fingerprint,
Wildcard: az.Wildcard, Wildcard: az.Wildcard,
} }
@ -111,8 +114,8 @@ func (db *DB) UpdateAuthorization(ctx context.Context, az *acme.Authorization) e
} }
nu := old.clone() nu := old.clone()
nu.Status = az.Status nu.Status = az.Status
nu.Fingerprint = az.Fingerprint
nu.Error = az.Error nu.Error = az.Error
return db.save(ctx, old.ID, nu, old, "authz", authzTable) return db.save(ctx, old.ID, nu, old, "authz", authzTable)
} }
@ -144,6 +147,7 @@ func (db *DB) GetAuthorizationsByAccountID(ctx context.Context, accountID string
Wildcard: dbaz.Wildcard, Wildcard: dbaz.Wildcard,
ExpiresAt: dbaz.ExpiresAt, ExpiresAt: dbaz.ExpiresAt,
Token: dbaz.Token, Token: dbaz.Token,
Fingerprint: dbaz.Fingerprint,
Error: dbaz.Error, Error: dbaz.Error,
}) })
} }

View file

@ -473,6 +473,7 @@ func TestDB_UpdateAuthorization(t *testing.T) {
ExpiresAt: now.Add(5 * time.Minute), ExpiresAt: now.Add(5 * time.Minute),
ChallengeIDs: []string{"foo", "bar"}, ChallengeIDs: []string{"foo", "bar"},
Wildcard: true, Wildcard: true,
Fingerprint: "fingerprint",
} }
b, err := json.Marshal(dbaz) b, err := json.Marshal(dbaz)
assert.FatalError(t, err) assert.FatalError(t, err)
@ -552,6 +553,7 @@ func TestDB_UpdateAuthorization(t *testing.T) {
Token: dbaz.Token, Token: dbaz.Token,
Wildcard: dbaz.Wildcard, Wildcard: dbaz.Wildcard,
ExpiresAt: dbaz.ExpiresAt, ExpiresAt: dbaz.ExpiresAt,
Fingerprint: "fingerprint",
Error: acme.NewError(acme.ErrorMalformedType, "malformed"), Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
} }
return test{ return test{
@ -582,6 +584,7 @@ func TestDB_UpdateAuthorization(t *testing.T) {
assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard) assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard)
assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt) assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt)
assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt) assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt)
assert.Equals(t, dbNew.Fingerprint, dbaz.Fingerprint)
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error()) assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
return nu, true, nil return nu, true, nil
}, },

View file

@ -3,6 +3,7 @@ package acme
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/subtle"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"net" "net"
@ -11,6 +12,7 @@ import (
"time" "time"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/x509util" "go.step.sm/crypto/x509util"
) )
@ -125,6 +127,27 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
return nil return nil
} }
// getKeyFingerprint returns a fingerprint from the list of authorizations. This
// fingerprint is used on the device-attest-01 flow to verify the attestation
// certificate public key with the CSR public key.
//
// There's no point on reading all the authorizations as there will be only one
// for a permanent identifier.
func (o *Order) getAuthorizationFingerprint(ctx context.Context, db DB) (string, error) {
for _, azID := range o.AuthorizationIDs {
az, err := db.GetAuthorization(ctx, azID)
if err != nil {
return "", WrapErrorISE(err, "error getting authorization %q", azID)
}
// There's no point on reading all the authorizations as there will
// be only one for a permanent identifier.
if az.Fingerprint != "" {
return az.Fingerprint, nil
}
}
return "", nil
}
// Finalize signs a certificate if the necessary conditions for Order completion // Finalize signs a certificate if the necessary conditions for Order completion
// have been met. // have been met.
// //
@ -150,6 +173,24 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID) return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID)
} }
// Get key fingerprint if any. And then compare it with the CSR fingerprint.
//
// In device-attest-01 challenges we should check that the keys in the CSR
// and the attestation certificate are the same.
fingerprint, err := o.getAuthorizationFingerprint(ctx, db)
if err != nil {
return err
}
if fingerprint != "" {
fp, err := keyutil.Fingerprint(csr.PublicKey)
if err != nil {
return WrapErrorISE(err, "error calculating key fingerprint")
}
if subtle.ConstantTimeCompare([]byte(fingerprint), []byte(fp)) == 0 {
return NewError(ErrorUnauthorizedType, "order %s csr does not match the attested key", o.ID)
}
}
// canonicalize the CSR to allow for comparison // canonicalize the CSR to allow for comparison
csr = canonicalize(csr) csr = canonicalize(csr)

View file

@ -2,6 +2,7 @@ package acme
import ( import (
"context" "context"
"crypto"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/asn1" "encoding/asn1"
@ -18,6 +19,7 @@ import (
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/x509util" "go.step.sm/crypto/x509util"
) )
@ -308,6 +310,14 @@ func (m *mockSignAuth) Revoke(context.Context, *authority.RevokeOptions) error {
} }
func TestOrder_Finalize(t *testing.T) { func TestOrder_Finalize(t *testing.T) {
mustSigner := func(kty, crv string, size int) crypto.Signer {
s, err := keyutil.GenerateSigner(kty, crv, size)
if err != nil {
t.Fatal(err)
}
return s
}
type test struct { type test struct {
o *Order o *Order
err *Error err *Error
@ -400,10 +410,18 @@ func TestOrder_Finalize(t *testing.T) {
{Type: "permanent-identifier", Value: "a-permanent-identifier"}, {Type: "permanent-identifier", Value: "a-permanent-identifier"},
}, },
} }
signer := mustSigner("EC", "P-256", 0)
fingerprint, err := keyutil.Fingerprint(signer.Public())
if err != nil {
t.Fatal(err)
}
csr := &x509.CertificateRequest{ csr := &x509.CertificateRequest{
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: "a-different-identifier", CommonName: "a-different-identifier",
}, },
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{ ExtraExtensions: []pkix.Extension{
{ {
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
@ -414,6 +432,29 @@ func TestOrder_Finalize(t *testing.T) {
return test{ return test{
o: o, o: o,
csr: csr, csr: csr,
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
switch id {
case "a":
return &Authorization{
ID: id,
Status: StatusValid,
}, nil
case "b":
return &Authorization{
ID: id,
Fingerprint: fingerprint,
Status: StatusValid,
}, nil
default:
assert.FatalError(t, errors.Errorf("unexpected authorization %s", id))
return nil, errors.New("force")
}
},
MockUpdateOrder: func(ctx context.Context, o *Order) error {
return nil
},
},
err: &Error{ err: &Error{
Type: "urn:ietf:params:acme:error:badCSR", Type: "urn:ietf:params:acme:error:badCSR",
Detail: "The CSR is unacceptable", Detail: "The CSR is unacceptable",
@ -452,6 +493,11 @@ func TestOrder_Finalize(t *testing.T) {
return nil, errors.New("force") return nil, errors.New("force")
}, },
}, },
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
},
err: NewErrorISE("error retrieving authorization options from ACME provisioner: force"), err: NewErrorISE("error retrieving authorization options from ACME provisioner: force"),
} }
}, },
@ -491,6 +537,11 @@ func TestOrder_Finalize(t *testing.T) {
} }
}, },
}, },
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
},
err: NewErrorISE("error creating template options from ACME provisioner: error unmarshaling template data: invalid character 'o' in literal false (expecting 'a')"), err: NewErrorISE("error creating template options from ACME provisioner: error unmarshaling template data: invalid character 'o' in literal false (expecting 'a')"),
} }
}, },
@ -532,6 +583,11 @@ func TestOrder_Finalize(t *testing.T) {
return nil, errors.New("force") return nil, errors.New("force")
}, },
}, },
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
},
err: NewErrorISE("error signing certificate for order oID: force"), err: NewErrorISE("error signing certificate for order oID: force"),
} }
}, },
@ -578,6 +634,9 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID) assert.Equals(t, cert.OrderID, o.ID)
@ -632,6 +691,9 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID" cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
@ -654,7 +716,7 @@ func TestOrder_Finalize(t *testing.T) {
err: NewErrorISE("error updating order oID: force"), err: NewErrorISE("error updating order oID: force"),
} }
}, },
"ok/permanent-identifier": func(t *testing.T) test { "fail/csr-fingerprint": func(t *testing.T) test {
now := clock.Now() now := clock.Now()
o := &Order{ o := &Order{
ID: "oID", ID: "oID",
@ -666,10 +728,14 @@ func TestOrder_Finalize(t *testing.T) {
{Type: "permanent-identifier", Value: "a-permanent-identifier"}, {Type: "permanent-identifier", Value: "a-permanent-identifier"},
}, },
} }
signer := mustSigner("EC", "P-256", 0)
csr := &x509.CertificateRequest{ csr := &x509.CertificateRequest{
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: "a-permanent-identifier", CommonName: "a-permanent-identifier",
}, },
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{ ExtraExtensions: []pkix.Extension{
{ {
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
@ -680,6 +746,7 @@ func TestOrder_Finalize(t *testing.T) {
leaf := &x509.Certificate{ leaf := &x509.Certificate{
Subject: pkix.Name{CommonName: "a-permanent-identifier"}, Subject: pkix.Name{CommonName: "a-permanent-identifier"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{ ExtraExtensions: []pkix.Extension{
{ {
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
@ -709,6 +776,117 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{
ID: id,
Fingerprint: "other-fingerprint",
Status: StatusValid,
}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID)
assert.Equals(t, cert.Leaf, leaf)
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
return nil
},
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.CertificateID, "certID")
assert.Equals(t, updo.Status, StatusValid)
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
assert.Equals(t, updo.Identifiers, o.Identifiers)
return nil
},
},
err: NewError(ErrorUnauthorizedType, "order oID csr does not match the attested key"),
}
},
"ok/permanent-identifier": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
},
}
signer := mustSigner("EC", "P-256", 0)
fingerprint, err := keyutil.Fingerprint(signer.Public())
if err != nil {
t.Fatal(err)
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "a-permanent-identifier",
},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
Value: []byte("a-permanent-identifier"),
},
},
}
leaf := &x509.Certificate{
Subject: pkix.Name{CommonName: "a-permanent-identifier"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
Value: []byte("a-permanent-identifier"),
},
},
}
inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}}
root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
ca: &mockSignAuth{
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return []*x509.Certificate{leaf, inter, root}, nil
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
switch id {
case "a":
return &Authorization{
ID: id,
Status: StatusValid,
}, nil
case "b":
return &Authorization{
ID: id,
Fingerprint: fingerprint,
Status: StatusValid,
}, nil
default:
assert.FatalError(t, errors.Errorf("unexpected authorization %s", id))
return nil, errors.New("force")
}
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID" cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
@ -743,11 +921,19 @@ func TestOrder_Finalize(t *testing.T) {
{Type: "permanent-identifier", Value: "a-permanent-identifier"}, {Type: "permanent-identifier", Value: "a-permanent-identifier"},
}, },
} }
signer := mustSigner("EC", "P-256", 0)
fingerprint, err := keyutil.Fingerprint(signer.Public())
if err != nil {
t.Fatal(err)
}
csr := &x509.CertificateRequest{ csr := &x509.CertificateRequest{
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: "a-permanent-identifier", CommonName: "a-permanent-identifier",
}, },
DNSNames: []string{"foo.internal"}, DNSNames: []string{"foo.internal"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{ ExtraExtensions: []pkix.Extension{
{ {
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
@ -758,6 +944,7 @@ func TestOrder_Finalize(t *testing.T) {
leaf := &x509.Certificate{ leaf := &x509.Certificate{
Subject: pkix.Name{CommonName: "a-permanent-identifier"}, Subject: pkix.Name{CommonName: "a-permanent-identifier"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{ ExtraExtensions: []pkix.Extension{
{ {
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}, Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
@ -792,6 +979,13 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{
ID: id,
Fingerprint: fingerprint,
Status: StatusValid,
}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID" cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
@ -856,6 +1050,9 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID" cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
@ -917,6 +1114,9 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID" cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
@ -981,6 +1181,9 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID" cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
@ -1688,3 +1891,55 @@ func TestOrder_sans(t *testing.T) {
}) })
} }
} }
func TestOrder_getAuthorizationFingerprint(t *testing.T) {
ctx := context.Background()
type fields struct {
AuthorizationIDs []string
}
type args struct {
ctx context.Context
db DB
}
tests := []struct {
name string
fields fields
args args
want string
wantErr bool
}{
{"ok", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
}}, "", false},
{"ok fingerprint", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
if id == "az1" {
return &Authorization{ID: id, Status: StatusValid}, nil
}
return &Authorization{ID: id, Fingerprint: "fingerprint", Status: StatusValid}, nil
},
}}, "fingerprint", false},
{"fail", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return nil, errors.New("force")
},
}}, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &Order{
AuthorizationIDs: tt.fields.AuthorizationIDs,
}
got, err := o.getAuthorizationFingerprint(tt.args.ctx, tt.args.db)
if (err != nil) != tt.wantErr {
t.Errorf("Order.getAuthorizationFingerprint() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Order.getAuthorizationFingerprint() = %v, want %v", got, tt.want)
}
})
}
}

4
go.mod
View file

@ -12,7 +12,7 @@ require (
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 github.com/Masterminds/sprig/v3 v3.2.3
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
github.com/aws/aws-sdk-go v1.44.185 // indirect github.com/aws/aws-sdk-go v1.44.195 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/fatih/color v1.9.0 // indirect github.com/fatih/color v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 github.com/fxamacker/cbor/v2 v2.4.0
@ -43,7 +43,7 @@ require (
github.com/urfave/cli v1.22.12 github.com/urfave/cli v1.22.12
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
go.step.sm/cli-utils v0.7.5 go.step.sm/cli-utils v0.7.5
go.step.sm/crypto v0.23.2 go.step.sm/crypto v0.25.0
go.step.sm/linkedca v0.19.0 go.step.sm/linkedca v0.19.0
golang.org/x/crypto v0.5.0 golang.org/x/crypto v0.5.0
golang.org/x/net v0.5.0 golang.org/x/net v0.5.0

8
go.sum
View file

@ -84,8 +84,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.44.185 h1:stasiou+Ucx2A0RyXRyPph4sLCBxVQK7DPPK8tNcl5g= github.com/aws/aws-sdk-go v1.44.195 h1:d5xFL0N83Fpsq2LFiHgtBUHknCRUPGHdOlCWt/jtOJs=
github.com/aws/aws-sdk-go v1.44.185/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go v1.44.195/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -690,8 +690,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
go.step.sm/cli-utils v0.7.5 h1:jyp6X8k8mN1B0uWJydTid0C++8tQhm2kaaAdXKQQzdk= go.step.sm/cli-utils v0.7.5 h1:jyp6X8k8mN1B0uWJydTid0C++8tQhm2kaaAdXKQQzdk=
go.step.sm/cli-utils v0.7.5/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71I= go.step.sm/cli-utils v0.7.5/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71I=
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
go.step.sm/crypto v0.23.2 h1:XGmQH9Pkpxop47cjYlUhF10L5roPCbu1BCZXopbeW8I= go.step.sm/crypto v0.25.0 h1:a+7sKyozZH9B30s0dHluygxreUxI1NtCBEmuNXx7a4k=
go.step.sm/crypto v0.23.2/go.mod h1:/IXGz8al8k7u7OV0RTWIi8TRVqO2FMyZVpedV+6Da6U= go.step.sm/crypto v0.25.0/go.mod h1:kr1rzO6SzeQnLm6Zu6lNtksHZLiFe9k8LolSJNhoc94=
go.step.sm/linkedca v0.19.0 h1:xuagkR35wrJI2gnu6FAM+q3VmjwsHScvGcJsfZ0GdsI= go.step.sm/linkedca v0.19.0 h1:xuagkR35wrJI2gnu6FAM+q3VmjwsHScvGcJsfZ0GdsI=
go.step.sm/linkedca v0.19.0/go.mod h1:b7vWPrHfYLEOTSUZitFEcztVCpTc+ileIN85CwEAluM= go.step.sm/linkedca v0.19.0/go.mod h1:b7vWPrHfYLEOTSUZitFEcztVCpTc+ileIN85CwEAluM=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=